前言:我们都知道C++是一门支持过程化编程,面向对象的高级语言,既然是面向对象的语言,那么对于对象而言,对象会有很多中相同的属性,举个例子:你和你老师,你们都有着共同的属性和身份,例如学生和老师都有性别,年龄这个成员变量,但你们又有着差异,你们可能学生有学号,老师有工号,老师可能还有职称,所以我们可以想象一下如何我们定义一个关于学生和老师的类,对于老师和学生是不是都有着共同点(都有年龄和性别),也有着不同点,所以如果我们定义老师类之后,又定义一个学生类,学生类是不是和老师类有一些成员变量重复了,可能有老铁认为这几个成员相同没啥太大的影响,但你想想一个工程项目得有多少个文件和多少个类,每一个类都有几个重复的成员变量和成员函数,是不是会导致代码运行效率大大降低了,也会占用更多的内存。为了解决这个问题,C++提出了继承的概念
继承:
(1)概念:面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。(继承本质就是类的复用)
简单的类的继承
语法格式:class 子类名 : 继承方式(一般是公有继承) 父类名
//基类(父类)
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
//派生类(子类)
//class 子类名 : 继承方式(一般是公有继承) 父类名
class Student : public Person//子类对父类的继承
{
protected:
int _stuid; // 学号
};
//派生类(子类)
//class 子类名 : 继承方式(一般是公有继承) 父类名
class Teacher : public Person//子类对父类的继承
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
调试看看派生类是否含有基类的成员变量
类的继承方式:
(1)private(基类的成员是private):如果基类成员(成员函数和成员变量)都是私有继承,那么派生类虽然继承基类,但基类成员在派生类中和在派生类外都是不可见。
(2)protected(基类的成员是protected):派生类继承基类,虽然在派生类外不能访问基类成员,但在派生类中可以访问基类成员
(3)public(基类的成员是public):派生类继承基类,既可以在派生类中访问基类成员,也可以在派生类外访问基类成员。
1.通过以上的对于三种访问限定符的分析,我们知道访问范围:public>protected>private
2.派生类使用class关键字时默认是private继承,struct关键字默认是public继承。(但是建议还是写出继承方式比较好),在实际中还是推荐使用public继承方式,其他俩种继承方式实用性不强。
基类和派生类对象赋值的转换
(1)派生类可以赋值给基类对象/基类指针/基类引用(形象的叫法是切割:把派生类中和父类相似的一部分切割出来赋值给基类)
(2)基类不能赋值给派生类
继承的作用域
基类和派生类都是独立的类,都有着自己独立的作用域,但是如果基类和派生类有着同名的成员,那么会出现什么情况呢?
//基类(父类)
class Person
{
protected:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person//子类对父类的继承
{
protected:
int _age = 17; // 年龄
};
int main()
{
Student s;
return 0;
}
你们看看这个代码能不能运行
运行没有任何问题,但是这个代码却非常容易混淆
Student的_age和Person的_age构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
我们再看看这段代码
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
//这里是想调用基类的成员函数fun()
fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
int main()
{
Test();
}
编译器却是调用派生类的fun函数,这是为什么呢?有的老铁可能会认为这两个函数构成重载了,但请老铁们注意,这两个函数并不是在同一个作用域,所以它们并不构成重载,它们函数名相同,是构成隐藏了。
隐藏(重定义):子类和父类有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
//这里是想调用基类的成员函数fun()
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
int main()
{
Test();
}
所以在继承体系中我们尽量不要定义同名的成员。
派生类的默认成员函数:
无论是何种成员函数,派生类都会调用基类的默认成员函数去把从基类继承的成员进行相对应得操作,例如:派生类的构造函数会自动去调用基类的构造函数,从而完成对派生类中从基类继承下来的成员的构造,如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用基类的自定义的构造函数。
对于析构函数,会先调用派生类的析构函数,再调用基类的析构函数,因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
派生类对象初始化先调用基类构造再调派生类构造
继承与友元:
基类中如果有友元函数,派生类继承基类却不能继承基类的友元函数,也就是说基类友元不能访问子类的私有和保护成员。
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
继承与静态成员:
如果基类中有静态成员,则整个体系中唯一只有这个静态成员,无论派生出多少个子类,都只有这一个static成员实例。
复杂的菱形继承及菱形虚拟继承
单继承:一个派生类只有一个直接基类
多继承:一个派生类有两个或两个以上的直接基类
菱形继承(多继承的特殊情况):
二义性:
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
}
int main()
{
Test();
return 0;
}
解决二义性
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a.Person::_name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
int main()
{
Test();
return 0;
}
虚拟继承可以解决菱形继承的数据冗余和数据二义性
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A//在继承方式前加上virtual关键字就构成虚拟继承
{
public:
int _b;
};
// class C : public A
class C : virtual public A//在继承方式前加上virtual关键字就构成虚拟继承
{
public:
int _c;
};
class D : public B, public C//在继承方式前加上virtual关键字就构成虚拟继承
{
public:
int _d;
};
虚拟继承的原理(以上面代码为例子):
虚拟继承通过把D对象中的A放到最下面,然后B和C中的成员变量各自放好,在B和C中不只有成员变量,还有各存放一个指针,各指向一张表,用于寻找公共的A,这两个指针叫虚机表指针,这两个表叫虚机表,虚机表中存放着A和B,C的相对的偏移量,B和C都可以通过各自的偏移量去寻找到公共的A
继承与组合
在工程项目都是提倡高内聚,低耦合,那什么是高内聚,什么又是低耦合呢?举个例子,一个部门一般是都是有很多个团队,每个团队实现项目中的一部分功能,我们希望每个团队实现的功能能够相对独立性比较强,便于我们维护,这个就是低耦合,高内聚是多个团队充分发挥合作,把项目完整和高效的实现出来!
继承:是低内聚,高耦合,允许你根据基类的实现来定义派生类的实现,这种生成派生类的复用被称为白箱复用,基类的内部细节是对子类开放的,所以继承一定程度上破环了基类的封装,基类的改变,对派生类有
组合:是高内聚,低耦合,组合是黑箱复用,这种复用是对象内部细节不可见的。
Is-a 关系:也称为继承,如果一个类是另一个类的一种类型,那么就存在 is-a 关系。
例如:如果你有一个 Vehicle 类和一个 Car 类,而 Car 是 Vehicle 的一种,那么 Car 就可以继承自 Vehicle。这意味着 Car 类拥有 Vehicle 类的所有属性和方法,并且还可以添加自己的特定属性和方法。
Has-a 关系:也称为聚合或组合。当一个类包含另一个类的对象作为其成员时,就是 has-a 关系。
例如,假设你有一个 Car 类和一个 Engine 类。每个 Car 都有一个 Engine,所以你可以将 Engine 对象作为一个成员变量放在 Car 类中。
这种关系不是通过继承来表达的,而是通过在一个类中声明另一个类类型的实例变量来实现。
// Car和BMW Car和Benz构成is-a的关系
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car {
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
// Tire和Car构成has-a的关系
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};