假设继承关系为:c->b->a
名字查找与继承体系
每个类都有自己的作用域,在继承体系中,基类(外部)派生类(内部)遵守嵌套作用域的规则:
- 内部作用域会隐藏外部作用域
名字查找:
- 如果内部作用域存在,则使用内部作用域
- 否则逐层查找外部作用域
继承特殊性:
- 派生类包含派生类部分,和直接基类部分,直接基类部分,又包含直接基类部分的直接基类部分……
普通成员函数
- 作用域:派生类同名成员和函数(即使形参列表不一致)会 隐藏 基类的成员和函数
- 类类型(静态类型):调用静态类型成员
- 指针/引用类型(动态类型):调用静态类型成员
虚函数
- 作用域:派生类同名成员和函数(形参列表必须一致)会 覆盖 基类的成员和函数
- 类类型(静态类型):调用静态类型成员
- 指针/引用类型(动态类型 & 动态绑定):调用动态类型成员,如果动态类型中没有覆盖,逐层向上查找
注意:除了虚函数的覆盖外,尽量不要定义同名的普通成员和函数
重载和继承:
- 如果派生希望基类所有重载版本都是可见,要么全部覆盖,要么一个也不覆盖
- 如果覆盖每个函数及其繁琐,可使用using 类::函数名(不用指定形参列表),
- 这样就会将所有重载函数添加到派生类中,而派生类只需要定义特有的函数
拷贝控制操作与继承体系
如果一个类没有合成拷贝控制操作,派生类不会继承基类的函数,而是编译器会为派生类合成默认的版本
删除的函数和继承
- 如果基类的拷贝控制操作被delete或private,则派生类对应的合成操作也定义为delete的,但如果想要拥有这些操作,可以自定义自己的版本
- 如果基类折构函数为delete或private,派生类的默认构造和拷贝构造和移动构造为delete
构造函数
- 在默认情况下,创建派生类对象,会自动调用派生类构造函数,而构造函数会调用直接基类的默认构造函数,初始化派生类对象中的基类部分,也可以显示的在初始化列表调用特定的基类构造
- //
- 如果创建一个派生类,会调用直接基类的构造函数,直接基类的构造又会调用直接基类的直接基类的构造,逐层向上……,最后执行最顶端基类的函数体,逐层向下……,
- 当执行基类的构造时,派生类部分是处于未被初始化状态,因此我们不能在基类构造调用派生类的构造,
- //
- 如果一个类没有合成拷贝控制操作,派生类不会继承基类的函数,而是编译器会为派生类合成默认的版本,但是我们可以使用using继承基类所有的构造函数(默认,拷贝,移动……)
- 对于基类每个函数,using会让编译器生成每个对于的派生类构造函数(形参列表是完全相同的)
- ……
拷贝 || 移动构造 & 拷贝 || 移动赋值
- 如果想要拷贝构造 操作,必须在派生类对应的初始值列表,显示调用基类的拷贝 || 移动 函数,否则派生类对象中的基类部分,将执行基类的默认构造函数,这个新的对象会变得非常奇怪
- b ( const b& x) : a(x) & b(b&& x) : a(std::move(x))
- 同样移动也需要显示调用,在重载=运算符函数中
- b& operator=( const b& x){ a::operato=(x) }
- //
- 定义了拷贝构造,编译器不会为我们定义默认的移动构造
- 如果没有移动操作,我们实际使用的是拷贝操作
折构
- 对于动态new分配的基类的指针,当delete时,会调用构造函数,但是在继承体系中,一个基类指针为动态类型,因为它可以指向派生类对象,
- 那么如果想要执行正确版本(派生类)的折构函数,需要将基类的折构函数声明为virtual
- 否则如果一个基类的折构函数不是virtual,则delete指向派生类对象的基类指针将产生未定义行为
- //
- 如果一个类定义了虚折构函数,则派生类中无论默认的还是自定义的都为虚折构函数
- 如果一个类定义了虚折构函数,则不一定需要拷贝和赋值操作
- 如果一个类定义了折构函数,则不会为这个类合成移动操作
- 当执行基类的折构函数时:执行顺序从派生类开始,逐层向上,直至继承链的最顶端(在构造中一个派生类包含上层的所有直接间接基类,树中的一条自底向上的路径,所以销毁直至最顶端)
- 折构函数不需要显示调用派生类的,它会隐式销毁派生类自己的成员,还会自动的隐式销毁直接基类的成员……