比如我们要定义学生类(Student)和老师类(Teacher),作为人这两个类共有的基本属性包括姓名、年龄等。这时写两个类的话就要各自都声明姓名和年龄这两个成员变量,能不能单独写一个 Person 类,里面只有姓名和年龄这两个成员变量,让学生类和老师类去继承Person类,这样就不用单独地再去声明姓名和年龄了。
继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展和增加功能,这样产生出来的类,我们称派生类。继承体现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,而继承是类设计层次的复用。
下图我们看到的 Person 是父类(也称作基类)。Student 是子类(也称作派生类)。
继承后父类的成员(包括成员变量和成员函数)都会成为子类的一部分:
Min权限最小的(成员在基类中的访问限定权限,继承时的权限)
,其中 public > protected > private子类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割,寓意是把子类中属于基类那部分切过去。
当基类的指针指向派生类对象时,基类的指针可以通过强制类型转换赋值给子类的指针。
通过上面的例子我们可以看到,指针能够毫无约束地操作内存中的任何东西,尽管功能强大,但是非常危险。
在继承体系中基类和子类都有独立的作用域。当子类和基类中有同名成员时,在子类内部访问子类成员将屏蔽对基类同名成员的直接访问,这种情况叫隐藏,也叫重定义。(如果想要访问必须指定基类的类域 基类::基类成员名称 这样来显示地访问基类的同名成员)。
隐藏顾名思义就是在子类内部,子类的同名成员隐藏了基类的同名成员,但基类的同名成员依然存在于子类内部,只不过默认优先调用的是子类的同名成员;想要调用基类同名成员的话,需要专门指定基类的作用域来调用。
变量名相同,就构成隐藏(重定义)
函数名相同,就构成隐藏(重定义)
区分 隐藏 和 函数重载
成员函数的隐藏 | 函数重载 | |
---|---|---|
对象特征 | 从基类继承下来的和子类同名的成员函数 | 函数名相同,但要求参数的类型、顺序、个数不一样 |
作用域 | 两个函数处在不同作用域:一个在基类,另一个在子类 | 两个函数处在同一作用域 |
调用规则 | 子类对象想要调用基类的同名成员函数,需要指定类域 | 调用时根据传入的实参来匹配具体调用谁 |
我们知道,类有 6 个默认成员函数,包括构造函数、拷贝构造函数、赋值运算符的重载和析构函数等。那么在派生类中,是如何去关联基类的这些默认成员函数的呢?
派生类的构造函数必须先调用基类的构造函数初始化基类的那一部分成员。
如果基类中没有默认的构造函数,或我们有显示调用基类构造函数的需要,这两种情况必须在派生类构造函数的 初始化列表位置 中显示调用基类的构造函数,以完成基类成员的初始化。
Note1:对基类成员的构造必须通过基类的构造函数,不能在子类的初始化列表里单独给基类的成员变量初始化
Note2:在子类构造函数的函数体内显示调用基类的构造函数行不行?
不行,要求必须在初始化列表里调用基类的构造函数的目的是保证先构造基类成员,再构造子类成员。另外在括号里面对成员变量的操作其实已经不是初始化了,而是对已经初始化过的变量再重新赋值。
在派生类的析构函数调用结束后,编译器会帮我们自动调用基类的析构函数,所以我们没必要在派生类析构函数中再显示地调用基类的析构函数。
派生类构造函数和析构函数设计原理
编译器会在派生类构造函数的初始化列表中最先调用基类的构造函数,完成基类成员的初始化,然后再执行初始化列表中的其他操作。
析构函数一般的话都是对象生命周期结束后自动调用,以完成清理工作,没有显示调用需要,并且为了保证子类成员先析构,基类成员后析构,所以在设计子类的析构函数时只用完成自己成员的清理即可,结束后系统会自动调用父类的析构函数来完成父类成员的清理工作。
构造函数和拷贝构造的任务本质上是一样的,都是完成成员变量的初始化工作,而且它们函数名相同,构成函数重载的关系。
看下面例子,可以帮助我们理解这两个函数的关系:
实际中,我们一般都是在派生类拷贝构造函数的初始化列表中通过切片显示调用基类的拷贝构造函数:
要注意基类和派生类的赋值重载构成了隐藏(因为他们函数名相同都是 operator=)所以必须要以声明基类作用域的方式来显示调用基类的赋值重载。
总结
基类的赋值重载也必须通过切片显示调用,并且要声明类域,因为基类的赋值重载被子类隐藏了。
友元关系不能继承,也就是说基类友元不能访问子类的私有和保护成员。(相当于你父亲的朋友不一定是你的朋友)
class Student;
class Person
{
public:
// Person类的友元
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._num << endl;
}
int main()
{
Person p;
Student s;
// 编译不通过,不能访问Student::_num
Display(p, s);
}
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员
即基类的静态成员也会被继承,且被所有基类和派生类对象所共享
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
从上图可以看出菱形继承具有二义性和数据冗余的问题,在 Assistant对象中 Person 成员就有两份。
虚拟继承可以解决菱形继承带来的的二义性和数据冗余的问题。
如下图的继承关系,当 Student 和 Teacher 继承 Person 类时使用虚拟继承
为了研究虚拟继承原理,我们给出了一个简化的菱形继承的继承体系,再借助内存窗口观察对象成员的模型。
扩展说明
问题:既然虚拟继承解决了虚基类(就是有多份的重复继承的那个类)成员的二义性和数据冗余问题,把两个同样的虚基类成员和到一起算一个。那虚基表(存指向公共基类的偏移量)存在有什么用呢?
原因:菱形虚拟继承时,vs编译器把虚基类成员放到整个对象的尾部,虚基表中存有偏移量,来计算到公有的虚基类对象的位置。存在下面的这种情况,切片发生时,d 需要去找出 B/C 中的属于 A 类的成员赋值过去。此时靠虚基表确定到 A 类成员的偏移量然后把他的值切割过去。
D d;
B b = d;
C c = d;
符合 has - a 就用继承,符合 is - a 就用组合。如果都可以优先用组合,因为组合的耦合度低,代码好维护。