构造与析构函数
创建派生类对象时,首先调用基类的构造函数(初始化继承的数据成员),然后调用派生类的构造函数(初始化新增的数据成员)
派生类的构造函数可以使用初始化器列表指明要使用的基类构造函数,如果没指定,那么使用基类的默认构造函数:
derived::derived(type1 x, type2, y, type3 z) : base(x, y) {}
如果基类没有默认构造函数,那么必须显式指定一个基类构造器
如果基类没有默认构造函数,那么派生类也不提供默认的构造函数
当派生对象过期时,调用顺序相反,首先调用派生类的析构函数,然后调用基类的析构函数
使用派生类
基类指针和引用只能调用基类的方法,如:
Derived derived();
Base& rt = derived;
Base* pt = &derived;
rt.name();
pt->name();
上面的两个调用都会调用 Base
的 name
方法,而不是 Derived
的 name
方法
多态公有继承
我们需要派生类对象使用派生类的方法,而不是基类的方法。即:方法的行为应该取决于调用该方法的对象
这种复杂的行为称为多态,有两种重要的机制可用于实现多态公有继承:
- 在派生类中重新定义基类的方法
- 使用虚方法
在方法前加 virtual
关键字可以让一个方法称为虚方法,它的派生类对应的方法都将变成虚方法
如果类会被继承,并且可能通过基类指针删除派生类对象,那么析构函数必须使用 virtual
关键字,这样保证了删除对象时调用正确的析构函数
静态绑定和动态绑定
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名绑定(binding)
在编译过程中进行绑定称为静态绑定,在运行时选择正确的虚方法的代码被称为动态绑定
虚函数的工作原理
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表(virtual function table,vtbl)
(并不是每个对象都存储一个虚函数表)
虚函数表存储了为类对象进行声明的虚函数的地址
调用虚函数时,程序将查看对象的 vtbl 地址,然后转向对应的 vtbl,然后看调用的是第几个函数,取出函数地址
虚函数有一定的成本:
- 每个对象增加了一个指向 vtbl 的指针
- 每个类创建了一个虚函数表
- 每次函数调用都要查虚函数表
关于虚函数的注意事项
构造函数不能是虚函数
析构函数应当是虚函数,除非类不用做基类
友元不能是虚函数,因为它不是类成员,如果因为这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决
如果没有重新定义,那么使用派生链中最新的虚函数版本
派生类中重新定义另一个同名但不同签名的函数会隐藏基类中的同名函数,如:
class Base {
public:
virtual void func(int a) const;
};
class Derived : public Base {
public:
virtual void func() const;
};
int main() {
Derived derived;
derived.func(); // valid
derived.func(1); // invalid
return 0;
}
这样并没有生成函数的两个重载版本,而是隐藏了基类中的版本
这引出了两条经验规则:
- 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或者指针,则可以修改为指向派生类的引用或指针,这种特性叫做返回类型协变(covariance of return type),注意只适用于返回值,不适用于参数(参数既没有协变也没有逆变)
- 如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本,如果只重新定义一个版本,那么另外两个版本将被隐藏,派生类对象将无法使用它们
抽象基类(Abstract Base Class)
C++ 使用纯虚函数(pure virtual function)提供未实现的函数,它的声明结尾处为 = 0
当类声明中包含纯虚函数时,不能创建该类的对象。要成为真正的 ABC,必须至少包含一个纯虚函数
C++ 甚至允许纯虚函数有定义,只需要在声明中定义为纯虚函数,在 cpp 文件中提供实现
继承和动态内存分配
假设基类使用了动态内存分配,并且重新定义了赋值运算符和拷贝构造函数,会有两种情况
第一种情况:派生类不使用 new
,不需要为派生类显式定义析构函数、拷贝构造函数和赋值运算符:
- 析构函数会按照派生类、基类的顺序执行
- C++ 提供的默认拷贝构造函数中,基类的部分会使用基类的拷贝构造,派生类的部分会逐个成员拷贝,调用每个成员的拷贝构造
- 赋值运算符与拷贝构造函数相似
第二种情况:派生类使用 new
,必须为派生类定义显式析构函数、拷贝构造函数和赋值运算符:
- 析构函数只需要清理派生类多出来的部分
- 拷贝构造函数要在成员初始化列表中显示调用基类的拷贝构造:
Derived::Derived(const Derived& other) : Base(other) { ... }
- 调用基类的赋值运算符:
Derived& Derived::operator=(const Derived& other) { Base::operator=(other); ... }
注意,不要在构造函数中调用虚函数,这也会查虚函数表进行动态绑定,使用 Base::func();
来调用
如何使用基类的友元:强制类型转换
类函数小结
其中,op= 表示 +=
、*=
等格式的赋值运算符
最好不要使用虚赋值
C++ 11 中的移动构造函数和移动赋值函数也会默认提供