【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}

多态

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第1张图片

  1. 多态(Polymorphism)是面向对象编程中的一个重要概念,指的是同一种操作或方法可以在不同的对象上产生不同的行为。具体来说,多态是通过继承和虚函数实现的。

  2. 多态可以提高代码的灵活性和可扩展性。通过多态,我们可以编写通用的代码,而不必考虑对象的具体类型。这样可以使代码更加简洁、易于维护和扩展。

  3. 例如:同样是买票这种行为,普通人是全价买票,学生是半价买票,军人则是优先买票。这就是一种多态的体现。

一、多态的定义

1.1 多态的构成条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第2张图片

1.2 虚函数

什么是虚函数?

  1. 虚函数是C++中的一个重要概念,它用于实现运行时多态。虚函数是在基类中声明的(virtual),它可以被派生类重写,并且在运行时根据对象的实际类型调用相应的函数。虚函数通过使用虚函数表(也称为虚表)来实现动态绑定。
  2. 在C++中,如果一个成员函数被声明为虚函数,那么它会被编译器标记为虚函数,并且在类的内存布局中会包含一个指向虚函数表的指针。虚函数表是一个存储虚函数地址的表格,它是在编译时由编译器生成的,用于实现动态绑定。每个包含虚函数的类都有自己的虚表,虚表中存储着该类的虚函数地址
  3. 多态的实现原理:当一个对象调用虚函数时,编译器会通过对象的虚表指针找到该对象所属类的虚表,然后根据虚函数在类中的位置,找到对应的虚函数地址。这个过程称为动态绑定,它是在运行时确定的,而不是在编译时确定的。
  4. 虚函数可以被派生类重写,也可以被派生类继承并保留为虚函数

虚函数的重写(覆盖)条件:

  1. 基类中的函数必须被声明为虚函数。

  2. 基类和派生类中的函数必须具有相同的返回值类型、函数名、参数列表(类型)

  3. 派生类中的函数也建议使用virtual关键字进行声明

注意:

  1. virtual只能用于修饰普通成员函数,不能修饰静态成员函数,virtual和static不能共用。(最后解释)
  2. virtual关键字只在声明时加上,虚函数在类外实现时不加virtual。(这点和static相同)
  3. 在重写基类虚函数时,派生类的虚函数不加virtual关键字,也可以构成重写。因为基类虚函数的接口被继承下来,在派生类中依旧保持虚函数属性。但是该种写法不是很规范,不建议这样使用

多态示例代码:

class Person {
public:
    //基类中的函数必须被声明为虚函数
	virtual void BuyTicket() { cout << "全价买票" << endl; }
};

class Student : public Person {
public:
    //基类和派生类中的函数必须具有相同的返回值类型、函数名、参数列表
	virtual void BuyTicket() { cout << "半价买票" << endl; }
	
    //派生类的虚函数不加virtual关键字也可以构成重写,不过不建议使用。
	//void BuyTicket() { cout << "买票-半价" << endl; }
};

class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "优先买票" << endl; }
};

void Func(Person& p){ //必须通过基类的指针或者引用调用虚函数
    p.BuyTicket(); 
}

int main()
{
	Person ps;
	Student st;
    Soldier sd;
	Func(ps);
	Func(st);
    Func(sd);
	return 0;
}

运行结果:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第3张图片

提示:

  1. 如果将Func函数的参数Person&改为Person,即使用父类对象去调用虚函数无法构成多态。是普通调用,都显示全价买票。
  2. 如果将父类虚函数的virtual关键字去调,就不会生成虚表,也无法构成多态。是普通调用,都显示全价买票。

1.3 虚函数重写的两个例外

1.3.1 协变 (基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(甚至可以是其他父子关系的指针或引用,如下面代码中的A,B)

class A{};

class B : public A {};

class Person {
public:
	virtual A* f() {return new A;}
};

class Student : public Person {
public:
	virtual B* f() {return new B;}
};

1.3.2 析构函数的重写 (基类与派生类析构函数的名字不同)

  1. 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。

  2. 虽然基类与派生类析构函数名字不同,看起来好像违背了重写的规则,其实不然:其实编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

  3. 建议将基类的析构函数声明为虚函数

class Person {
public:
	virtual ~Person() {cout << "~Person()" << endl;}
};

class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

/*只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用
析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。*/
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1; //p1->destructor(); operator delete(p1);
	delete p2; //p2->destructor(); operator delete(p2);
	return 0;
}

析构函数不加virtual

第二次new的是子类对象,但由于指针是父类类型,所以调用的是父类析构。不匹配,存在内存泄漏。

在这里插入图片描述

析构函数加virtual

基类的析构函数声明为虚函数,同时子类的析构函数构成重写。此时使用基类指针多态调用:在运行时根据对象的实际类型调用相应的函数,指向父类调父类析构,指向子类调子类析构。

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第4张图片

1.4 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写(不常用)
class Car
{
public:
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	virtual void Drive() {cout << "Benz-舒适" << endl;}
};
  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
	virtual void Drive(){}
};

class Benz :public Car {
public:
	virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

1.5 重载、覆盖(重写)、隐藏(重定义)的对比

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第5张图片


二、抽象类

2.1 概念

  1. 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

  2. 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

  3. 派生类继承抽象类后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。

  4. 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

class Car //抽象类
{
public:
	virtual void Drive() = 0; //纯虚函数
};

class Benz :public Car
{
public:
	virtual void Drive() //派生类必须重写纯虚函数
	{
		cout << "Benz-舒适" << endl;
	}
};

class BMW :public Car
{
public:
	virtual void Drive() //派生类必须重写纯虚函数
	{
		cout << "BMW-操控" << endl;
	}
};

void Func(Car &car){    
  car.Drive();    
}    
    
int main(){   
  //Car c; //抽象类不能实例化出对象
  Benz bc;    
  BMW bm;                                                                                                                        
  Func(bc);    
  Func(bm);    
  return 0;    
}    

运行结果:

在这里插入图片描述

2.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

以下程序输出结果是什么:

class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
	virtual void test(){ func();}
};

class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};

int main(int argc ,char* argv[])
{
	B*p = new B;
	p->test();
	return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

  1. 首先通过B类(派生类)的指针p调用test函数,test函数的this指向A类对象(基类),此处发生切片赋值。
  2. 然后通过指向基类的指针this调用虚函数this->func();且虚函数func构成重写;满足多态的所有条件。
  3. 虚函数是接口继承,目的是为了重写,达成多态。其中参数缺省值属于接口内容,会被继承下来。
  4. B类虚函数重写后的结果是:virtual void func(int val=1){ std::cout<<"B->"<< val <因此答案选B。
int main(int argc ,char* argv[])
{
	//如果将指针改为基类指针呢?
    A*p = new B; //[1] 仍然调用派生类B的重写版本
    //A*p = new A; //[2] 调用基类A的func函数
	p->test();
	return 0;
}

[1] 结果相同,只不过切片赋值操作在定义基类指针p就已经发生了。调用test函数时是同类指针的普通赋值。

[2] 实际到底调用的是谁,不是看传的是父类指针还是子类指针,而是指针指向的对象是父类还是子类。指向谁调用的就是谁


三、多态的实现原理

3.1 虚函数表

3.1.1 虚表的概念

以下程序输出结果是什么:

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

答案:32位:8; 64位:16;(内存对齐)

监视窗口:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第6张图片

在C++中,如果一个成员函数被声明为虚函数,那么它会被编译器标记为虚函数,并且在类的内存布局中会包含一个指向虚函数表的指针。**虚函数表是一个存储虚函数地址的数组,它是在编译时由编译器生成的,存储在只读常量区(代码区),用于实现动态绑定。**每个包含虚函数的类都有自己的虚表,虚表中存储着该类的虚函数地址。

虚函数是一种特殊的成员函数,具有普通成员函数的所有属性:

  • 可以被继承;

  • 可以被隐藏(重定义);

  • 可以被访问控制符修饰;

同时虚函数也具有其特殊属性:

  • 可以被声明为纯虚函数;
  • 可以被重写(覆盖);
  • 可以被动态绑定(进虚函数表);

3.1.2 虚表的结构

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Base中定义两个虚函数Func1,Func2和一个普通函数Func3
// 3.Derive中重写Func1,并定义一个自己的虚函数Func4
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};

//打印虚函数表
typedef void(*VFPTR)();
void PrintVFTable(VFPTR *table, size_t n){
  for(size_t i = 0; i<n; ++i)                        
  {    
    printf("vftable[%lu]:%p -> ", i, table[i]);    
    table[i](); //函数指针强转成VFPTR,无视函数原型调用函数。    
  }    
}                                                                

void Test1(){ //打印Base和Derive两个类的虚表
  Base b;           
  Derive d;    
  printf("Base虚表:%p\n", (int*)*(long long*)&b);    
  printf("Derive虚表:%p\n", (int*)*(long long*)&d);    
  cout << endl;    
  PrintVFTable((VFPTR*)*(long long*)&b, 2);//取出对象中的虚表指针传参      
  cout << endl;    
  PrintVFTable((VFPTR*)*(long long*)&d, 3);      
}    

运行结果:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第7张图片

监视窗口:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第8张图片

注意:vs监视窗口存在bug,虚表中不能显示派生类自己定义的虚函数指针。

通过观察和测试,我们发现了以下几点问题:

  1. 虚函数表本质是一个存放虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(vs平台下)。

  2. 派生类对象的虚表指针保存在基类部分,派生类和基类的虚表不是同一个(地址不同)。

  3. 派生类会继承基类的虚表(虽然不是同一个虚表,但会拷贝基类虚函数的地址);如果构成重写,就覆盖重写后的虚函数地址;

  4. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。Func4是派生类自己定义的虚函数,也要进虚表。

  5. 总结一下派生类的虚表生成:

a. 先将基类中的虚表内容拷贝一份到派生类虚表中

b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

  1. 虚函数存在哪的?虚表存在哪的?
  • 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。

  • 另外对象中存的不是虚表,存的是虚表指针。虚表是保存在只读常量区(代码段)中的。

void Test2() //打印打印Base和Derive两个类的虚表指针,和各数据区的地址做对比。    
{    
  Base b;    
  Derive d;    
  printf("Base虚表:%p\n", (int*)*(long long*)&b);    
  printf("Derive虚表:%p\n", (int*)*(long long*)&d);    
  const char* str = "abc";    
  printf("只读常量区:%p\n", str);    
  static int a = 1;    
  printf("静态数据区:%p\n", &a);    
  int *pi = new int;    
  printf("堆区:%p\n", pi);                 
  int c = 2;    
  printf("栈区:%p\n", &c);      
}    

运行结果:虚表是保存在只读常量区(代码段)中的。(vs和g++下)

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第9张图片

3.2 多态的原理

测试代码:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

多态的实现原理:如果符合多态的条件,那么在调用虚函数时会到指定对象的虚表中找到对应的虚函数地址,进行调用。

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第10张图片

3.3 早期绑定和晚期绑定

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第11张图片
C++语言的多态性分为编译时的多态性和运行时的多态性,也叫早期绑定和晚期绑定

  1. 编译时的多态性又称为静态或早期绑定,通过函数重载来实现的,因为函数重载是一种静态多态性,它在编译时就能确定函数调用的地址。在调用重载函数时,编译器会根据实参的类型、个数和顺序来确定调用哪个函数。编译时多态适用于非虚函数和静态函数,因为它们的函数地址在编译时就已经确定了。编译时多态的优点是速度快,缺点是不支持运行时多态性。

  2. 运行时的多态性又称为动态或晚期绑定,是指在程序运行时根据对象的实际类型来确定函数调用的地址,从而实现多态性。运行时多态通过虚函数来实现。在调用虚函数时,编译器会到指定对象的虚表中确定重写函数的地址,从而实现多态性。运行时多态适用于虚函数和纯虚函数,因为它们的函数地址在运行时才能确定。运行时多态的优点是支持多态性,缺点是速度相对较慢。

注意:

  1. 使用对象名调用虚函数,或者通过派生类的引用或指针调用虚函数,也是普通调用,属于静态绑定。
  2. 虚函数可以动态绑定,也可以静态绑定。
  3. 虚函数是专为实现多态而设计的,如果不实现多态不要定义虚函数,否则会导致调用过程变慢。

四、单继承和多继承关系的虚函数表

4.1 单继承中的虚函数表

测试代码:

class Person{
  virtual void Buyticket(){
    cout << "Person::Buyticket()" << endl;
  }
  virtual void Func1(){
    cout << "Person::Func1()" << endl;
  }                     
};

class Student:public Person{
  virtual void Buyticket(){
    cout << "Student::Buyticket()" << endl;
  }
  virtual void Func2(){
    cout << "Student::Func2()" << endl;
  }
};

typedef void(*VFPTR)();
void PrintVFTable(VFPTR *table, size_t n){
  for(size_t i = 0; i<n; ++i)                        
  {
    printf("vftable[%lu]:%p -> ", i, table[i]);
    table[i](); //函数指针强转成VFPTR,无视函数原型调用函数。
  }
}

int main(){
  Person p;    
  Person p1;    
  Student s;                   
  Student s1;    
  //测试一:打印各对象虚表的地址
  cout << "p: " << (VFPTR*)*(long long*)&p << endl;     
  cout << "p1: " << (VFPTR*)*(long long*)&p1 << endl;     
  cout << "s: " << (VFPTR*)*(long long*)&s << endl;     
  cout << "s1: " << (VFPTR*)*(long long*)&s1 << endl;     
  cout << endl;    
  
  //测试二:打印虚函数表中的虚函数地址,并调用虚函数
  PrintVFTable((VFPTR*)*(long long*)&p, 2); //取对象开头的虚表指针传参    
  cout << endl;    
  PrintVFTable((VFPTR*)*(long long*)&s, 3);    
  return 0;   

运行结果:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第12张图片

结论:

  1. 虚表的指针位于对象空间的开头前8个字节(64下),一个long long的大小;
  2. 同类型的对象共用一个虚表;不管是否完成重写,子类和父类的虚表都不是同一个。
  3. 单继承只有一个虚表。派生类对象的虚表指针保存在基类部分,派生类会继承基类的虚表(拷贝基类虚函数的地址);如果构成重写,就覆盖重写后的虚函数地址;
  4. 派生类自己定义的虚函数地址也要存入虚表。

4.2 多继承中的虚函数表

测试代码:

class Base1{
  virtual void Func1(){
    cout << "Base1::Func1()" << endl;
  }
  virtual void Func2(){
    cout << "Base1::Func2()" << endl;
  }
};

class Base2{
  virtual void Func1(){
    cout << "Base2::Func1()" << endl;
  }
  virtual void Func2(){
    cout << "Base2::Func2()" << endl;
  }
};

class Derive:public Base1, public Base2{
  virtual void Func1(){
    cout << "Derive::Func1()" << endl;                       
  }
  virtual void Func3(){
    cout << "Derive::Func3()" << endl;
  }
};

//写法一:切片赋值
void Test1(){
  Derive d;
  Base1 *pb1 = &d;
  Base2 *pb2 = &d;
  PrintVFTable((VFPTR*)*(long long*)pb1, 3);
  cout << endl;
  PrintVFTable((VFPTR*)*(long long*)pb2, 2);
}
//写法二:移动指针
void Test2(){
  Derive d;
  PrintVFTable((VFPTR*)*(long long*)&d, 3);
  cout << endl;
  PrintVFTable((VFPTR*)*(long long*)((char*)&d+sizeof(Base1)), 2);
}

运行结果:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第13张图片

结论:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第14张图片

  1. 在多继承中,派生类对象中的每个基类部分都有自己的虚函数表。
  2. 派生类自己定义的虚函数地址存放在第一个基类的虚表中。
  3. 在多继承中,如果两个基类中有同名的虚函数,那么在派生类中必须重写它们,否则会导致二义性错误。重写版本会覆盖所有基类中的虚函数。(我将其成为多重覆盖函数)

拓展内容:

接着第3点继续,这两个基类虚表中的多重覆盖函数的地址最终会跳转到同一个函数地址处。其中,该虚函数真正的地址存储在第一个基类中。第二个基类要先完成this指针的偏移,使其指向派生类对象的首地址,再跳转到虚函数真正的地址。(了解)

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第15张图片

4.3 菱形继承、菱形虚拟继承中的虚函数表

4.3.1 菱形继承

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第16张图片

通过指针打印菱形继承中派生类D的内存布局:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第17张图片

内存布局的图像表示:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第18张图片

结论:

  • 在菱形继承中,超类B的成员变量被两个基类B1,B2继承,在派生类D中有2份实例。存在数据冗余和二义性问题。

  • 超类B的的虚函数也被两个基类B1,B2继承,在派生类D中也有2份虚函数地址,分别位于在两个基类的虚表中。包括重写和未重写版本。


4.3.2 菱形虚拟继承

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第19张图片

通过指针打印菱形继承中基类B1的内存布局:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第20张图片

通过指针打印菱形虚拟继承中派生类D的内存布局:

【C++】多态 {多态的构成条件,虚函数的重写,override和final关键字;抽象类,接口继承和实现继承;多态的实现原理,虚函数表,动态绑定与静态绑定;单继承和多继承的虚函数表;}_第21张图片

结论:VC++结果为例,VC++的虚表比较清晰和有逻辑性

  • 在虚拟继承中,超类B被放到了最后,基类B1,B2不再重复继承B类。
  • 虚函数表指针后存放的是虚基表指针,虚基表的第一项是指向虚函数表的偏移量,虚基表的第二项是指向超类B的偏移量。
  • 每个基类都有自己的虚函数表和虚基表。虚函数表中只存放该基类的虚函数地址,包括重写和未重写版本(派生类的虚函数地址位于第一个基类的虚表中)。虚基表中存放的是指向该基类虚表的偏移量,和指向公共超类B的偏移量。

对于菱形继承和菱形虚拟继承的内存布局详解,推荐陈皓大佬的两篇文章:

  1. C++ 虚函数表解析
  2. C++ 对象的内存布局

五、继承和多态常见的面试问题

  1. 什么是多态?多态的构成条件?

  2. 什么是虚函数?虚函数重写的条件?

  3. 什么是重载、重写(覆盖)、重定义(隐藏)?

  4. 什么是虚函数表?

  5. 多态的实现原理?

  6. 动态绑定和静态绑定?

  7. 虚函数可以内联吗?

    答:虚函数可以被声明为内联函数,但是是否真正内联取决于编译器的实现。当函数是虚函数时,如果进行多态调用,inline就不起作用。因为多态调用在运行时决议,编译时无法确定地址就不能展开函数;如果不是多态调用,同时满足inline条件就会展开函数。

  8. 静态成员可以是虚函数吗?

    答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

  9. 构造函数可以是虚函数吗?

    答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

  10. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

  11. 拷贝构造和operator=可以是虚函数吗?

    答:拷贝构造不可以,拷贝构造也是构造,原因同构造函数。赋值重载函数可以是虚函数,但是通常不需要将其声明为虚函数,因为他不涉及多态性的问题。

  12. 对象访问普通函数快还是虚函数更快?

    答:如果虚函数不构成多态调用是静态绑定,是一样快的;如果构成多态调用是动态绑定,普通函数更快。因为动态绑定需要在运行时去虚表中找虚函数地址。

  13. 虚函数表是在什么阶段生成的,存在哪的?

    答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(只读常量区)的。

  14. C++菱形继承的问题?虚继承的原理?

    答:参考继承课件。注意这里不要把虚函数表和虚基表搞混了。

  15. 什么是抽象类?抽象类的作用?

    答:参考(二、抽象类)。抽象类强制重写虚函数,另外抽象类体现出了接口继承关系。

你可能感兴趣的:(C++,c++,linux,继承和多态)