继承和多态
继承的本质:
- 代码的复用
#include <iostream>
using namespace std;
int main() {
class A {
public:
int ma;
protected:
int mb;
private:
int mc;
};
class B : public A { // B 继承 A
public:
// ...
};
return 0;
}
总结:
外部只能访问 public,private 和 protected 的成员无法直接访问;
在继承结构中,派生类可以直接从基类继承来 public 和 protected 的成员,继承后的权限为继承方式和基类中的权限中较小的那个。
基类中 private 的成员可以被继承,但是无法访问。
派生类的构造过程
尝试编写以下代码:
class base {
public:
explicit base(int data) : ma(data) { cout << "base()" << endl;}
~base() { cout << "~base()" << endl; }
protected:
int ma;
};
class derived : public base {
public:
derived(int data) : ma(data), mb(data) { cout << "derived()" << endl; }
~derived() { cout << "~derived()" << endl; }
private:
int mb;
};
编译发生错误:
[1/2] Building CXX object CMakeFiles/7_17.dir/main.cpp.o
FAILED: CMakeFiles/7_17.dir/main.cpp.o
/.../main.cpp
/.../main.cpp:17:29: error: member initializer 'ma' does not name a non-static data member or base class
17 | derived(int data) : ma(data), mb(data) { cout << "derived()" << endl; }
| ^~~~~~~~
1 error generated.
ninja: build stopped: subcommand failed.
错误原因:base 没有可用的初始化
派生类如何初始化从基类继承来的成员变量?
派生类可以继承基类的所有成员,除了构造函数和析构函数。
通过调用基类相应的构造函数。
派生类构造函数和析构函数负责初始化和清理派生类的部分。
基类的成员由基类的构造和析构函数负责。
正确写法:
class Base {
public:
explicit Base(int data) : ma(data) { cout << "Base()" << endl;}
~Base() { cout << "~Base()" << endl; }
protected:
int ma;
};
class Derived : public Base {
public:
Derived(int data) : Base(data), mb(data) { cout << "Derived()" << endl; }
~Derived() { cout << "~Derived()" << endl; }
private:
int mb;
};
重写、覆盖和隐藏
class Base {
public:
Base(int data = 20) : ma(data) { cout << "Base()" << endl;}
~Base() { cout << "~Base()" << endl;}
void show() { cout << "Base::show()" << endl; }
void show(int data) { cout << "Base::show(int)" << endl; }
private:
int ma;
};
class Derived : public Base {
public:
Derived(int data) : Base(data), mb(data) { cout << "Derived()" << endl; }
~Derived() { cout << "~Derived()" << endl; }
private:
int mb;
};
int main() {
Derived aa(20);
aa.show();
aa.show(11);
return 0;
}
对于以上代码,当在子类中添加一个 show 方法会怎样?
class Derived : public Base {
public:
Derived(int data) : Base(data), mb(data) { cout << "Derived()" << endl; }
~Derived() { cout << "~Derived()" << endl; }
void show() { cout << "Derived::show() << endl; }
private:
int mb;
};
/.../main.cpp:52:8: error: too many arguments to function call, expected 0, have 1; did you mean 'Base::show'?
52 | aa.show(11);
| ^~~~
| Base::show
/.../main.cpp:34:10: note: 'Base::show' declared here
34 | void show(int data) { cout << "Base::show(int)" << endl; }
| ^
1 error generated.
ninja: build stopped: subcommand failed.
函数重载:一组函数处于相同作用域中,函数名相同参数列表不同,此例中函数不处于相同作用域,所以不构成重载。
隐藏:在继承结构中派生类的同名成员把基类的成员隐藏调用了,此例中函数构成隐藏关系。
赋值行为
Base b(10);
Derived d(20);
b = d; // Y
Base *pb = &d; // Y
pb->show();
// ((Derive *)pd)->show();
pb->show(10); // 正常调用
Derived *pd = (Derived *)&b; // N 存在指针的非法访问
pd->show();
cout << pd->mb << endl; // 非法访问成员变量(已经将 mb 设为共有)
虚函数、动态绑定和静态绑定
Derived d(50);
Base *pb = &d;
pb->show(); // 静态(编译时期)的绑定(函数的调用) call Base::show
pb->show(10); // call Base::show(int)
将 show()
设置为虚函数
class Base {
public:
Base(int data = 20) : ma(data) { cout << "Base()" << endl;}
~Base() { cout << "~Base()" << endl;}
// 虚函数
virtual void show() { cout << "Base::show()" << endl; }
virtual void show(int data) { cout << "Base::show(int)" << endl; }
private:
int ma;
};
class Derived : public Base {
public:
Derived(int data) : Base(data), mb(data) { cout << "Derived()" << endl; }
~Derived() { cout << "~Derived()" << endl; }
private:
int mb;
};
如果类里定义了虚函数那么编译阶段编译器给这个类类型产生一个 vftable 虚函数表。
虚函数表中存储的主要内容就是 RTTI 指针和虚函数的地址。
程序运行时,每一张虚函数表都会加载到内存的
.rodata
区。一个类里面定义了虚函数,那么这个类定义的对象运行时,其开始部分会多存储一个
vfptr
虚函数指针,指向相应类型的虚函数表。一个类定义的对象指向的是同一个虚函数表如果派生类中的方法和基类中的某个虚函数返回值参数名和参数列表都相同,那么派生类的这个方法自动处理为虚函数
“重写“==”覆盖“
覆盖:虚函数表中虚函数地址的覆盖
chipen@bogon 7.17 % tail -n 7 main.cpp
Derived d(50);
Base *pb = &d;
pb->show();
# 调用
# mv eax, dword ptr[pb]
# mv ecx, dword ptr[eax]
# call ecx
# 从对象的地址中先读出虚函数指针,再从指针中读出虚函数地址
pb->show(10);
return 0;
}%
chipen@bogon 7.17 % ./cmake-build-debug/7_17
Derived::show()
Base::show(int)
可见,覆盖不同于重写。
如果 Base 没有虚函数,那么 *pb 识别的就是编译时期的类型。
如果 Base 有虚函数,那么 *pb 识别的就是运行时期的类型。
注意:对于
Base *pb = Derived d;
cout << typeid(pb).name << endl; // 为 Base *,无争议
cout << typeid(*pb).name << endl;
// 取决于 Base 中是否具有虚函数,如果没有虚函数,那么打印的就是 Base 类型。
// 如果有虚函数,那么打印的就是虚函数表中 *&RTTI 的类型,即 Derived 类型,就是动态绑定了。
虚析构函数
哪些函数不能实现成虚构函数?
虚函数能产生地址,存储在 vftable 中
对象必须存在 (vfpttr -> vftable -> 虚函数地址)
构造函数:
vitrual + 构造函数 —— NO
构造函数中调用虚函数也不会发生动态绑定(调用任何函数都是静态绑定)
派生类构造过程:先调用的是基类的构造函数,才调用派生类的构造函数
虚析构函数:析构函数调用的时候,对象是存在的!
当处理以下问题时:
Base *pb = new Derived(20);
pb->show();
delete pb
// 正如上文所说,调用时,delete 发现 pb 为 Base * 类型,
// 只调用了 Base 的析构函数,使得派生类的析构函数没有被调用到。
基类的析构函数是 virtual 虚函数,那么派生类的析构函数自动成为虚函数
如何解释多态
静态(编译时期)的多态:函数重载、模板和类模板
在编译时期就已经确定好:
bool compare(int, int) {} --> call compare_int_int
bool compare(double, double) {} --> call compare_double_double
动态(运行时期)的多态
继承的好处:
可以做代码的复用。
在基类中提供统一的虚函数接口,让派生类重写,就可以使用多态了