文章目录

一、继承的本质和原理

C++继承和多态总结_虚函数


总结:

  1. 外部只能访问对象public成员,protected和private的成员无法直接访问
  2. 在继承结构中,派生类可以继承基类的private成员,但是无法直接访问
  3. protected和private的区别:在基类中定义的成员,想被派生类访问但是不想被外部访问,那把基类中的相关成员定义成protected。如果派生类和外部都不访问,那就把基类中的相关成员定义成peivate
  4. 使用class定义派生类,默认继承方式是private;使用struct定义派生类,默认继承方式是public

二、派生类的构造过程

派生类怎么初始化从基类继承来的成员变量?

  1. 派生类可以继承所有成员(包括方法和变量),除了构造函数和析构函数。通过调用基类相应的构造函数来初始化
  2. 派生类的构造和析构函数,负责初始化和清理派生类部分;派生类从基类继承来的成员的初始化以及清理部分由基类的构造和析构函数负责。

派生类对象构造和析构的过程

  1. 派生类调用基类的构造函数,初始化从基类继承的成员
  2. 派生类调用自己的构造函数,初始化派生类自己特有的成员
  3. 当对象作用域到期时,调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存、文件)
  4. 调用基类的析构函数,释放派生类内存中,从基类继承类的成员可能占用的外部资源(堆内存、文件)
class Base {
public:
Base(int data) :ma(data) {
cout << "Base()" << endl;
}
~Base() {
cout << "~Base()" << endl;
}

protected:
int ma;
};

class Derive :public Base {
public:
// 错误写法:Derive(int data):ma(data), mb(data)
Derive(int data) :Base(data), mb(data){
cout << "Derive()" << endl;
}

~Derive() {
cout << "~Derive()" << endl;
}

private:
int mb;
};

三、重载、隐藏

重载: 重载函数必须在同一作用域,函数名相同,参数列表不同,和返回值无关,virtual关键字可有可无
覆盖: 在继承结构中,派生类中有和基类函数名相同,参数相同,基类函数必须有virtual关键字
隐藏: 如果派生类的函数于基类的函数同名,但是参数不同,此时无论有无virtual关键字,基类的函数都被隐藏

#include<iostream>

using namespace std;

class Base {
public:
Base(int data) :ma(data) {
cout << "Base()" << endl;
}

void show() {
cout << "Base::show()" << endl;
}

void show(int) {
cout << "Base::show(int)" << endl;
}

~Base() {
cout << "~Base()" << endl;
}

protected:
int ma;
};

class Derive :public Base {
public:
Derive(int data) :Base(data), mb(data){
cout << "Derive()" << endl;
}

void show() {
cout << "Derive::show()" << endl;
}

~Derive() {
cout << "~Derive()" << endl;
}

private:
int mb;
};

int main() {

#if 0
Derive d(20);
d.show();
// d.show(10);//Derive::show: 函数不接受 1 个参数(隐藏)
d.Base::show(10);
#endif

Base base(10);
Derive derive(20);

base = derive;

// error
derive = base;

// 由于是Base的指针,该指针只指向Base大小的空间,只能访问Base的方法和成员,除非强转
Base* ptr_b = &derive;

// error,由于是Derive的指针,该指针指向Derive大小的空间,
// 但是实际可以访问的空间就只有Base那么大,剩下的空间属于非法访问
Derive* ptr_d = &base;
return 0;
}

总结: 指针的类型决定指针的能力,但实际能访问的区域由指向的对象决定。如果指针的能力大于实际允许访问的空间(​​Derive* ptr_d = &base​​​),则是非法访问(直接编译不过)。如果指针的能力小于实际指向的空间(​​Base* ptr_b = &derive;​​),则只能访问指针指向范围内的数据。

四、静态绑定和动态绑定

静态绑定(编译期间)

base(10);
Derive derive(20);
Base* ptr_b = &derive;

//Base::show(),静态(编译时期)绑定(函数调用),call Base::show (0D71037h)
ptr_b->show();
//Base::show(int),静态(编译时期)绑定(函数调用),call Base::show (0D712FDh)
ptr_b->show(10);

// 4
cout << sizeof(Base) << endl;
//8
cout << sizeof(Derive) << endl;
//class Base*
cout << typeid(ptr_b).name() << endl;
//class Base
cout << typeid(*ptr_b).name() << endl;

类添加虚函数后,有什么影响?

  1. 如果类里面定义了虚函数,编译器在编译阶段给这个类类型产生一个唯一的​​vftable​​​虚函数表,虚函数表中存储的内容就是​​RTTI​​指针和虚函数地址。
  2. 程序运行时,每张虚函数表都会加载到内存的​​.rodata​​区。
  3. 如果类里面定义了虚函数,程序运行时,这个类对应的对象对应的内存开始部分会多存储一个​​vfptr​​​虚函数指针,指向相应类型的虚函数表​​vftable​​​。一个类型定义的所有对象的​​vfptr​​都会指向同一张虚函数表。
  4. 一个类里虚函数的个数不影响对象占用内存的大小(共用一个​​vfptr​​),影响的是虚函数表vftable的大小
  5. 如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是​​virtual​​的,那派生类中的方法直接被处理成虚函数,也即覆盖(注意,只有方法会覆盖,数据成员不会覆盖,而是存储多个,可以通过作用域访问)。

动态绑定(运行期间)

调用的不是类名加作用域的方法,而是调用寄存器内存放的地址,而这个地址只有在运行时期才能确定

#include<iostream>
#include<typeinfo>

using namespace std;

class Base {
public:
Base(int data):ma(data) {
cout << "Base()" << endl;
}

virtual void show() {
cout << "Base::show()" << endl;
}

virtual void show(int) {
cout << "Base::show(int)" << endl;
}

~Base() {
cout << "~Base()" << endl;
}

protected:
int ma;
};

class Derive :public Base {
public:
Derive(int data) :Base(data), mb(data){
cout << "Derive()" << endl;
}

void show() {
cout << "Derive::show()" << endl;
}

~Derive() {
cout << "~Derive()" << endl;
}

private:
int mb;
};

int main() {
Base base(10);
Derive derive(20);
Base* ptr_b = &derive;

/*
查看ptr_b的类型,是Base;查看Base::show;
1. 如果Base::show是普通函数,就进行静态绑定,call Base::show
2. Base::show是虚函数,就进行动态(运行时)绑定(函数调用,从虚函数表中获取)
mov eax, dword ptr[ptr_b] ; 根据vfptr虚函数表的地址
mov ecx, dword ptr[eax] ; 从虚函数表中拿到虚函数的地址
call ecx ; 调用虚函数
*/
ptr_b->show(); //Derive::show()
ptr_b->show(10); //Base::show(int)

// 4 + 4(vfptr) = 8
cout << sizeof(Base) << endl;
// 8 + 4(vfptr) = 12
cout << sizeof(Derive) << endl;
//指针类型就是class Base*(编译时期直接确定,不会改变)
cout << typeid(ptr_b).name() << endl;
/*
首先ptr_b的类型是Base,然后查看Base是否有虚函数
1. 没有虚函数,*ptr_b就是编译时期(静态)的类型,*ptr_b就是class Base
2. 如果有虚函数,*ptr_b就是运行时期(动态)的类型,即RTTI类型
*/
cout << typeid(*ptr_b).name() << endl;
}

C++继承和多态总结_析构函数_02

由于​​Derive​​​从​​Base​​​继承了虚函数,​​RTTI​​​(Run-Time Type Identification) 指向的是一个​​RTTI​​​类型的指针,理解成一个对象名字符串即可。本来存放的是​​&Base::show()​​​(继承而来),但是由于​​Derive​​​中重写了虚函数,于是就用​​&Derive::show()​​​覆盖了​​&Base::show()​

覆盖: 如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual的,那派生类中的方法直接被处理成虚函数。那么派生类的虚函数表中原有的方法则被覆盖。

C++继承和多态总结_派生类_03


理解虚函数

虚函数依赖:

  1. 虚函数可以在vftable中产生函数地址
  2. 对象必须存在(vfptr–>vftable–>虚函数地址)

哪些函数不能实现成虚函数?

  1. 构造函数不能实现成virtual,构造函数构造完成,对象才产生,才有vfptr。
  2. 构造函数中调用虚函数,也不会发生动态绑定,调用任何函数都是静态绑定的。(动态绑定需要调用派生类的方法,然而派生类对象还没有构造完成)
  3. static静态成员方法的调用不依赖对象

虚析构函数

class Base {
public:
Base(int data):ma(data) {
cout << "Base()" << endl;
}

virtual void show() {
cout << "Base::show()" << endl;
}

virtual void show(int) {
cout << "Base::show(int)" << endl;
}

~Base() {
cout << "~Base()" << endl;
}

protected:
int ma;
};

class Derive :public Base {
public:
Derive(int data) :Base(data), mb(data), ptr(new int(data)){
cout << "Derive()" << endl;
}

~Derive() {
delete ptr;
cout << "~Derive()" << endl;
}

private:
int mb;
int* ptr;
};

int main() {
Base* ptr_b = new Derive(10);
ptr_b->show();
delete ptr_b;
}

C++继承和多态总结_析构函数_04


没有调用派生类的析构函数,在本代码中导致内存泄露

delete ptr_b执行过程说明:
查看ptr_b的类型,是Base*;接着查看Base::~Base()是普通函数还是虚函数
1. 是普通函数,对于析构函数的调用就是静态绑定,汇编代码为:call Base::~Base
2. 是虚函数,对于析构函数的调用就是动态绑定。
Derive的vftable中用&Derive::~Derive替换了基类的析构函数(只要基类中的析构函数是virtual,派生类的析构函数也是virtual)

代码改动如下:

virtual ~Base(){
cout << "~Base()" << endl;
}

C++继承和多态总结_析构函数_05


什么时候必须把Base的析构函数实现成虚函数?

基类的指针指向堆上的派生类对象时,​​delete ptr_base​​调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual。若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数。

实例对象、指针、引用分别调用虚函数

int main() {
Base b;
Derive d;
// 通过对象访问,肯定是静态绑定
b.show();
d.show();

// 动态绑定(必须是由指针调用),指向哪个对象就查哪个对象的虚函数表
Base* ptr_b1 = &b;
ptr_b1->show();

Base* ptr_b2 = &d;
ptr_b2->show();

// 引用也同指针, call eax
Base& r_b1 = b;
r_b1.show();

Base& r_b2 = d;
r_b2.show();

/*
p的类型是Derive*,所以查看Derive里的show,发现是virtual,
于是查指向对象的前4字节,从虚函数表中取出调用函数的地址
*/
Derive* p = (Derive*)&b;
p->show();
}

面试官:怎么理解多态?

分为静态(编译时期)多态和动态(运行时期)多态。

  1. 静态多态的表现形式包括​​函数重载​​​和​​类模板​​。
  2. 继承结构中,Base类指针(引用)指向Derive类对象,通过该指针(引用)调用同名覆盖方法(虚函数),该指针指向哪个Derive对象的覆盖对象,就调用哪个Derive类方法

面试官:继承的好处有哪些?

  1. 可以做代码复用
  2. 在基类中提供统一的虚函数接口,让派生类重写,就可以实现多态了

五、抽象类

当我们不希望一个class去实例化一个对象时,我们可以讲这个类写成抽象类。拥有纯虚函数的类叫做抽象类,抽象类不能实例化对象,但是可以定义指针和引用变量。而纯虚函数指的是没有函数体,必须重写的那种,例如:​​virtual void fun() = 0​

class Animal {
public:
Animal(string name) :_name(name) {

}
// 纯虚函数,必须重写
virtual void bark() = 0;
protected:
string _name;
};

class Cat : public Animal{
public:
Cat(string name) :Animal(name) {

}

void bark() {
cout << _name << " miao miao" << endl;
}
private:

};

class Dog : public Animal {
public:
Dog(string name) :Animal(name) {

}

void bark() {
cout << _name << " wang wang" << endl;
}
private:

};

int main() {
Cat cat("cat");
Cat dog("dog");
Animal* animal1 = &cat;
Animal* animal2 = &dog;
animal1->bark();
animal2->bark();
}

六、笔试题总结

笔试题1

void fun(){
Animal* p1 = new Cat("cat");
Animal* p2 = new Dog("dog");
// 强转为int*
int* p11 = (int*)p1;
int* p22 = (int*)p2;
// 实际上这里是交换了对象的前4个字节,也就是vfptr
int tmp = p11[0];
p11[0] = p22[0];
p22[0] = tmp;
// 根据交换以后的vfptr访问虚函数表
p1->bark();
p2->bark();
}

C++继承和多态总结_析构函数_06

笔试题2

#include<iostream>
#include<string>

using namespace std;

class Base {
public:
virtual void show(int i = 10) {
cout << "Base::show() i = "<< i << endl;
}
};

class Derive :public Base {
public:
void show(int i = 20) {
cout << "Derive::show() i = "<< i << endl;
}
};

int main() {
Base* p = new Derive();
p->show();
delete p;
return 0;
}

C++继承和多态总结_c++_07

C++继承和多态总结_派生类_08


解析:汇编指令在编译时期确定,参数压栈和访问权限也是在编译时期确定,由于​​p​​​是​​Base*​​​,而​​Base​​​里​​show​​​方法的默认值是10,直接就将参数默认值10压栈。压栈以后,虽然是动态绑定​​call eax​​,调用的是Derive的方法,但是参数已经压栈,打印的值也就是编译时期确定的10

笔试题3

#include<iostream>

using namespace std;

class Base {
public:
virtual void show() {
cout << "Base::show()"<< endl;
}
};

class Derive :public Base {
private:
// show方法改成了private
void show() {
cout << "Derive::show()"<< endl;
}
};

int main() {
Base* p = new Derive();
p->show();// 正常动态绑定,访问Derive::show()
delete p;
return 0;
}

解析:访问权限是编译时期确定的,编译时期用Base指针进行访问,而在编译时期编译器只能看见Base类里的show方法是public的,所以在编译时期认为这个show方法是可以访问的。而编译派生类的时候,直接将派生类show方法的地址放入vftable,动态绑定的时候(​​call eax​​),也就可以正常从vftable中取出重写的show方法地址。同理,下面代码直接无法通过编译

class Base {
// 访问权限在编译时期确定
private:
virtual void show() {
cout << "Base::show()"<< endl;
}
};

class Derive :public Base {
public:
void show() {
cout << "Derive::show()"<< endl;
}
};

int main() {
Base* p = new Derive();
p->show();
delete p;
return 0;
}

笔试题4

#include<iostream>

using namespace std;

class Base {
public:
Base() {
/*
push ebp
mov ebp, esp ; 开辟栈帧
sub esp, 4Ch
rep stos ; windows会做内存初始化
最后将&Base::vftable写入vfptr
*/
cout << "Base::Base()" << endl;
clear();
}

virtual void show() {
cout << "Base::show()"<< endl;
}

void clear() {
// 当前对象被清0
memset(this, 0, sizeof(*this));
}
};

class Derive :public Base {
public:
Derive() {
/*
每一个函数进来,都有如下4行代码
push ebp
mov ebp, esp ; 开辟栈帧
sub esp, 4Ch
rep stos ; windows会做内存初始化

最后将&Derive::vftable写入vfptr ; 在这里将vftable的地址写入vfptr
*/
cout << "Derive::Derive()" << endl;
}

void show() {
cout << "Derive::show()" << endl;
}
};

void fun1() {
Base* p1 = new Base();
p1->show(); // 动态绑定,报错
delete p1;
}

void fun2() {
Base* p2 = new Derive();
p2->show(); // 动态绑定,正常访问vftable
delete p2;
}

int main() {
fun1();
fun2();
return 0;
}

fun1分析:

C++继承和多态总结_虚函数_09


fun2分析:

派生类对象需要首先调用Base(),将​​&Base::vftable​​​写入​​vfptr​​​,当前对象内存被清0。然后调用Derive(),将​​&Derive::vftable​​​的写入​​vfptr​​,此时派生类虚函数表可以正常访问。

七、理解虚继承和虚基类

​virtual​​可以修饰方法,也可以修饰继承方式。修饰继承方式的时候,则是虚继承

C++继承和多态总结_虚函数_10

C++继承和多态总结_派生类_11

class A {};   // sizeof(A) = 1
class B : public A{}; // sizeof(B) = 1
class A {};   // sizeof(A) = 1
class B :virtual public A{}; // sizeof(B) = 4,包括一个vbptr
// sizeof(A) = 4,包括vfptr
class A {
public:
virtual void fun() {}
};
/*
sizeof(B) = 8,包括vfptr和vbptr,vfptr直接继承A的
class B size(8):
+---
0 | {vbptr}
+---
+--- (virtual base A)
4 | {vfptr}
+---

B::$vbtable@:
0 | 0
1 | 4 (Bd(B+0)A)

B::$vftable@:
| -4
0 | &A::fun
vbi: class offset o.vbptr o.vbte fVtorDisp
A 4 0 4 0
*/
class B :virtual public A{};

注:

  1. windows查看类空间命令​​cl <源文件名> /d1reportSingleClassLayout<类名>​
  2. ​vfptr​​​由派生类虚继承得到,当派生类有自己单独的虚函数时,也有自己的​​vfptr​​​。​​vfptr​​​指向​​vftable​​​,​​vftable​​​存放​​vfptr​​在内存中的偏移量、RTTI信息以及虚函数地址
  3. ​vbptr​​​指向​​vbtable​​​,​​vbtable​​存放的vbptr和虚基类数据在派生类内存中的偏移量

关于虚继承导致的delete错误

#include<iostream>
#include<typeinfo>
#include<string>

using namespace std;

class A {
public:
virtual void fun() {
cout << "call A::fun()" << endl;
}
private:
int ma;
};

class B :virtual public A{
public:
void fun() {
cout << "call B::fun()" << endl;
}
private:
int mb;
};

int main() {
// 基类指针指向派生类对象,永远是指向派生类对象中基类数据的起始地址
// 这里如果是栈上对象,编译器会自动释放空间,不会报错
A* p = new B();
p->fun();
delete p; // 释放空间error
return 0;
}

C++继承和多态总结_派生类_12


基类指针指向派生类对象,永远是指向派生类对象中基类数据的起始地址。释放空间也从该指针处开始释放,则报错(linux,g++不报错)

八、菱形继承问题

C++继承和多态总结_析构函数_13

有重复继承的问题,D的对象存放两份ma,用虚继承(重复数据替换为vbptr)解决这种重复继承的问题(谁的数据重复,谁就需要被虚继承

C++继承和多态总结_c++_14

此时,注意A已经和D一样需要自己进行构造(结构图最靠左),而不是需要构造别人提前构造自己(被迫构造),需要手动对A进行初始化。通过构造函数打印结果,也可以看到此时没有重复构造A,把A的数据都通过vbptr替换,并把A的数据放到内存最后

// 虚继承前
D(int data) :B(data), C(data), md(data){}
// 虚继承后
D(int data) :A(data),B(data), C(data), md(data){}