C++多态基础

文章目录

  • 1.多态概念
  • 2.多态使用
  • 3.多态析构
  • 4.多态隐藏
  • 5.多态原理
    • 5.1.单类继承
      • 5.1.1.问题一:非指针或引用无法调用多态
      • 5.1.2.问题二:同类对象共用虚表
      • 5.1.3.问题三:子类对象拷贝父类对象虚表
      • 5.1.4.问题四:打印虚表地址和虚表内容
    • 5.3.多类继承
    • 5.4.棱形继承
      • 5.4.1.普通棱形继承下的虚函数表
      • 5.4.2.虚继承下的虚函数表
  • 6.抽象概念

1.多态概念

就是“多种形态”,完成某个方法的时候,使用不同的对象就会得出不同的结果。

2.多态使用

多态会使用一种叫做”虚函数“的东西,多态关键字和虚继承是一样的,但是两者没有关系,只是共用了一个关键字virtual,这个关键字只能修饰成员。

书写一个子类的虚函数,可以叫做“对父类成员函数的重写/覆盖(注意和继承的隐藏/重定义做好区分)”。

区分:隐藏/重定义、重写/覆盖、重载的区别

  1. 隐藏/重定义发生在两个具有父子关系的类中,只需要父子各自拥有的函数或者变量名字相同即可构成隐藏/重定义
  2. 重写/覆盖发生在两个具有父子关系的类中,需要父子各自拥有的函数的函数签名(函数返回值、函数名、函数参数列表)严格相同,并且带有关键字virtual(除去协变的情况)
  3. 重载发生在同一个作用域中(不能发生在两个类域),需要函数名字相同,参数列表不同
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-半价" << endl;}
};class Child : public Person
{
public:virtual void BuyTicket()//虚函数{cout << "买票-免费" << endl;}
};void Function(Person& p)//只能使用指针和引用去调用达成多态
{p.BuyTicket();
}int main()
{//1.父类Person p;//2.子类Student s;Child c;//3.调用不同的函数Function(p);Function(s);Function(c);return 0;
}

这里使用多态的时候就产生了切片。这里要注意多态的构成条件:

  1. 构成多态必须有虚函数重写
  2. 必须使用父类的指针或引用去调用虚函数

这里的虚函数重写怎么理解呢?必须是具有父子继承关系的两个成员虚函数,并且两者的函数签名(函数返回值、函数名、函数参数列表)完全相同(函数参数主要是看类型是否相同,与缺省值是否相同或者参数名字是否相同无关),但是内部定义可以不同的。

注意:构成函数重写和是不是虚函数是两回事!

但是,C++有一些例外情况,使得哪怕函数签名不完全相同也能构成多态。

虚函数的返回值可以发生不同,这种情况也叫做”协变“(很少使用协变)。但是此时构成多态的函数的返回值类型必须是具有父子继承关系类的类型的指针或引用(如果是其他不具有继承关系的类型的返回值就有可能会报错)。

//协变例子
#include <iostream>
using namespace std;//一对具有父子关系的类
class Father
{int _father_value;
};
class Child: public Father
{int _child_value;
};//A、B父子类,内含虚函数
class A
{
public:virtual A* func(){cout << "virtual A* func()" << endl;return nullptr;}
};
class B : public A
{
public:virtual B* func()//构成多态{cout << "virtual B* func()" << endl;return nullptr;}
};int main()
{A a;B b;A* pa = &a;A* pb = &b;pa->func();pb->func();return 0;
}

另外如果父类的函数加了virtual成为了虚函数,那么子类就可以不加virtual,依旧是构成了多态的重写(挺让人吐槽的),但是我们建议加上,因为这样可读性更好。

补充1:两个有关虚函数的关键字

  1. final:必须是修饰虚函数,表示该函数不能被重写,加在父类函数名后面(如果是加在类后,该类就不能被继承)

  2. override:检查子类的虚函数是否真的重写了父类的某个虚函数,没有重写就编译报错,也是写在子类函数名字的后面

补充2:实际上虚函数继承就是一种接口继承,而普通的继承是一种实现继承。

3.多态析构

之前我们提到过:在继承的时候,析构函数是很特殊的,是编译器自己调用,并且还统一改名为destrutor()。编译器为了避免内存泄露自己调控析构的顺序这我们能理解,但是为什么需要改名呢?

//不用多态
#include <iostream>
using namespace std;
class Person
{
public:void func(){cout << "Person:virtual void func()" << endl;}~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:void func(){cout << "Student:virtual void func()" << endl;}~Student(){cout << "~Student()" << endl;}
};int main()
{Person* ptr1 = new Person;Student* ptr2 = new Student;ptr1->func();ptr2->func();delete ptr1;delete ptr2;return 0;
}
//输出:
//Person:virtual void func()
//Student:virtual void func()
//~Person()
//~Student()
//~Person()
//没毛病,这里析构的前两句都是在ptr2内完成的,在ptr2释放完自己的资源后,编译器自动调用父类的资源,释放父类的资源

但是如果使用指针或者引用来构成多态就会出现问题了:

//使用多态(修改前)
#include <iostream>
using namespace std;
class Person
{
public:virtual void func(){cout << "Person:virtual void func()" << endl;}~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:virtual void func(){cout << "Student:virtual void func()" << endl;}~Student(){cout << "~Student()" << endl;}
};int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;ptr1->func();ptr2->func();delete ptr1;delete ptr2;return 0;
}
//输出:
//Person:virtual void func()
//Student:virtual void func()
//~Person()
//~Person()
//出问题了,ptr2内部的子类资源没有被释放掉,原因是因为析构函数没有构成多态

为什么呢,因为这里的析构函数没有构成多态。两个delete分别调用ptr1->destructor()ptr2->destructor(),而由于析构函数没有关键字virtual,没有构成多态。

因此在编译器看来,使用了什么类型的指针就应该调用什么类型的析构函数,就用对应类型的析构函数,也就造成了内存泄漏。

那如何构成多态呢?让我们回忆一下多态的构成条件:虚函数重写、指针或引用调用。

如果析构函数达成多态,首先函数签名是要相同吧?但是每个子类和父类的名字都是不一样的,而在语法规则上,每个类的析构函数签名都是~类名(),由于这条规则,首先函数名字就一定不一样了,这样就很难构成多态。

总不能修改规则让每个类的析构函数名字都变成一样吧?答案是:虽然我们自己改不可以,但是我们可以让编译器干这种活呀!编译器统一将具有继承关系的析构函数名称改为destructor(),让析构函数有机会构成父子隐藏,这样使用析构函数就会变成使用ptr1->destructor()ptr2->destructor()

为什么说是有机会呢?因为还缺少了关键字virtual,这个时候只要给所有具有继承关系的析构函数加上关键字virtual,即可让析构函数构成多态,使用ptr1->destructor()ptr2->destructor()也就具有了多态的行为

上面也就是单独对析构改名的原因,而其他成员函数则无需这样,他们不需要构成多态。

因此代码需要这么修改:

//使用多态(修改后)
#include <iostream>
using namespace std;
class Person
{
public:virtual void func(){cout << "Person:virtual void func()" << endl;}virtual ~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:virtual void func(){cout << "Student:virtual void func()" << endl;}virtual ~Student(){cout << "~Student()" << endl;}
};int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;ptr1->func();ptr2->func();delete ptr1;delete ptr2;return 0;
}
//输出:
//Person:virtual void func()
//Student:virtual void func()
//~Person()
//~Student()
//~Person()

再结合我们之前在2.多态使用最后中提到的”如果父类的函数加了virtual成为了虚函数,那么子类就可以不加virtual,依旧是构成了多态的重写“,我们其实只需要在父类的析构函数加上关键字足够了。

//使用多态(修改后)
#include <iostream>
using namespace std;
class Person
{
public:virtual void func(){cout << "Person:virtual void func()" << endl;}virtual ~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:void func(){cout << "Student:virtual void func()" << endl;}~Student(){cout << "~Student()" << endl;}
};int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;ptr1->func();ptr2->func();delete ptr1;delete ptr2;return 0;
}
//输出:
//Person:virtual void func()
//Student:virtual void func()
//~Person()
//~Student()
//~Person()

当然需要注意,只有在使用指针newdelete的时候才会出现这样的问题。如果是直接创建子类对象,然后将地址传给父类指针,就不会有这种问题,因为子类对象自己就会调用正确的析构函数,无需程序员手动使用delete。因此换句话说:这种因为析构没有达成多态条件构成的内存泄漏,主要是发生在使用new上。

下面是和上面代码对比的另外一段代码,请好好思考他们之间的不同(为何上一份不加virtual就会发生内存泄漏,而下面这一份却不会呢?):

//使用多态(修改前)
#include <iostream>
using namespace std;
class Person
{
public:virtual void func(){cout << "Person:virtual void func()" << endl;}~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:virtual void func(){cout << "Student:virtual void func()" << endl;}~Student(){cout << "~Student()" << endl;}
};int main()
{Student s;Person p;Person& rs = s;Person& rp = p;rs.func();rp.func();return 0;
}
//输出:
//Student:virtual void func()
//Person : virtual void func()
//~Person()
//~Student()
//~Person()
//没有出问题

4.多态隐藏

有些时候,多态隐藏得很深,有可能会隐藏到this里,下面这个题目就很坑:

//隐藏的多态
#include <iostream>
using namespace std;
class A
{
public:virtual void func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
};
class B : public A
{
public:void func(int val = 0)//隐藏/重定义了func{cout << "B->" << val << endl;}//继承了 virtual void test(A* const this),因此继承的实质是可以使用父类的函数,而不是真的有一个函数在子类中
};
int main()
{B* p = new B;p->test();//调用 B 类内的 test(),由于继承的实质是可以使用父类的函数,//因此实际是使用了父类函数里的 test(),全写为 void test(A* const this)//父类 this 指针接受 p 指针,使用 this->func() 就造成多态调用(是父类指针的调用),//因此内部调用的是子类的 func()//而虚函数的继承实际上是继承了了父类函数的接口,然后重写了实现,//因此子类内的缺省值用的是父类的缺省值(子类)p->func();//这里就没有构成多态了,正常调用了子类成员函数return 0;
}

吐槽:实际当中谁要是写了这种代码必会被人吐槽…

这个例子主要是想要提醒您继承的实质和多态调用的条件,很好将继承和多态融合在了一起。

另外这里还简单提及了“接口继承”,这在Java中也有相关的概念。

5.多态原理

吐槽:下面的原理如果您看不懂,也可以以后再来查看,因为确实难度提升了很多…

我们先来看一段代码:

#include <iostream>
using namespace std;class Base
{
public:virtual void Function(){cout << "Function()" << endl;}
private:int _b = 1;
};int main()
{Base b;cout << sizeof(b) << endl;//32位输出8,64位输出16return 0;
}

这个现象很奇怪,仿佛类的实例化变量还有一个成员一样,这和我们多态的原理有关,我们一一来进行解释。

5.1.单类继承

您不觉得这很神奇么,虚函数为什么和指针类型无关,而是指针指向的类型有关呢?

这是因为使用虚函数的父类对象在编译时就会多一个_vfptr虚函数表指针成员。该指针指向一个虚函数指针数组,而该数组存储对象中所有虚函数成员的地址,这就是多态的原理。

吐槽:在VS中是叫_vfptr这个名字,这个名称可能起得不太好,应该叫_vft之类的(virtual function table),代表指向一个虚函数表的指针。

父类对象的虚表指针成员指向的虚表存储父类虚函数,而子类对象的虚表指针从父类继承来。虚表只能存储虚函数的地址(这个地址很可能不是函数真正的地址,在VS中,所有的函数地址,都只是在汇编代码中跳转到对应函数前jmp指令的地址),而父子的虚函数本身存储在代码段。

但是从父类拷贝到子类的虚函数表指针指向的虚函数表有可能会发生变动。

  1. 父子各自指向的表内的众多虚函数指针原本是一样的(因为子类拷贝的是父类的虚函数地址)
  2. 但是编译器检测到有虚函数接口重写,编译器将会对子类对象内部的虚函数表指针指向的虚函数表的内容进行覆盖(子类覆盖从父类拷贝过来的虚函数地址)
  3. 导致子类的指向的虚函数表内存储的虚函数地址发生了变化
  4. 这个过程就是虚函数重写,如果没有覆盖,那么数组内容就会一样,这也是为什么虚函数重写会被称为“覆盖”的原因(因此重写是语言层面的概念,而覆盖是原理层面的概念)

对于下述代码,我做了图解解释,方便您理解多态原理:

#include <iostream>
using namespace std;class Base
{
public:virtual void Function_1(){cout << "Base:Function_1()" << endl;}virtual void Function_2(){cout << "Base:Function_2()" << endl;}
private:int _b_number = 1;
};class Derive : public Base
{virtual void Function_1(){cout << "Derive:Function_1()" << endl;}
private:int _d_number = 2;
};void Function(Base* b)
{b->Function_1();
}int main()
{Base b;Derive d;Function(&b);Function(&d);return 0;
}

在这里插入图片描述

我们还可以注意到,虚函数谁先被声明(再次强调是声明,而不是定义)谁的下标就越靠前:

#include <iostream>
using namespace std;class Base
{
public:virtual void Function_1();//先声明,后定义virtual void Function_2();//后声明,先定义
private:int _b_number = 1;
};class Derive : public Base
{virtual void Function_1(){cout << "Derive:Function_1()" << endl;}
private:int _d_number = 2;
};void Base::Function_2()
{cout << "Base:Function_2()" << endl;
}
void Base::Function_1()
{cout << "Base:Function_1()" << endl;
}void Function(Base* b)
{b->Function_1();
}int main()
{Base b;Derive d;Function(&b);Function(&d);return 0;
}

在这里插入图片描述

补充1:如果没有关键字virtual,那么上述说的所有现象都不会存在。

补充2:所有带有关键字virtual的虚函数一定会被存储在虚函数表里,但是选择覆盖还是不覆盖是构成多态的关键(也就是构成两个条件:虚函数重写、指针或引用调用,即可完成覆盖,达成多态的使用)。

知道了上述原理,我们解答下面的问题就会更加深入,而不是简单记忆语法。

5.1.1.问题一:非指针或引用无法调用多态

为什么直接使用子类对象赋值给父类对象,使用父类对象调用虚函数时无法达成多态?这是因为父类只拷贝了子类的成员,而没有拷贝子类的虚函数表指针(父类自己就有,只会调用父类自己的虚函数表指针),从原理上来说,的确可以这么做,但是C++明确规定不能这么做,您简单记忆即可。下面是代码示例:

#include <iostream>
using namespace std;class Base
{
public:virtual void Function_1(){cout << "Base:Function_1()" << endl;}virtual void Function_2(){cout << "Base:Function_2()" << endl;}
private:int _b_number = 1;
};
class Derive : public Base
{virtual void Function_1(){cout << "Derive:Function_1()" << endl;}
private:int _d_number = 2;
};void Function(Base x)//赋值调用,不构成多态
{x.Function_1();
}int main()
{Base b;Derive d;Function(b);Function(d);return 0;
}

在这里插入图片描述

而指针和引用给子类对象的虚函数表指针(内部数组已经经过覆盖)部分提供了指向或别名,就不会有上面的问题。

补充:从实现上来看,如果赋值的时候,将子类对象的虚函数表指针拷贝给父类对象会发生什么?无法保证父类对象内部的虚表是父类的虚表,父类对象一旦不小心被子类对象赋值,就会完全转变为调用子类虚函数。

这在有些场景会极度坑人:析构函数会被错误调用,因为析构函数是有可能会进入虚表的,父类对象被new出来后,一旦被子类对象赋值就会调错析构函数。

Perosn* p = new Person;
Students s;
*p = s;
delete p;//调错析构函数,调成子类的了

5.1.2.问题二:同类对象共用虚表

为什么不直接存在对象内?因为有可能会有多个虚函数,直接存储的话,一个对象就会非常大。而一个程序往往会有很多同类型的对象,这样就会重复存储冗余的数据。

而所有的对象内的虚函数表指针指向同一个虚函数表,在创建较多同类型对象时,就会大大节省空间。

5.1.3.问题三:子类对象拷贝父类对象虚表

如果有父子都有虚函数,但是没有构成重写/覆盖,那么父子对象用的不是也同一张虚表(因为有可能会发生覆盖,父子对象要是使用的相同虚表就会自己覆盖自己了),但同类的对象就会共用一张虚表。

#include <iostream>
using namespace std;class Base
{
public:virtual void Function_1(){cout << "Base:Function_1()" << endl;}virtual void Function_2(){cout << "Base:Function_2()" << endl;}
private:int _b_number = 1;
};
class Derive : public Base
{
private:int _d_number = 2;
};void Function(Base* x)
{x->Function_1();
}int main()
{Base b;Derive d;Function(&b);Function(&d);Base bb;return 0;
}

在这里插入图片描述

5.1.4.问题四:打印虚表地址和虚表内容

虚表是在编译时生成好的,实际并不神秘。而对象中的虚表指针是在构造函数的初始化列表阶段生成的(这点可以调试出来,这一过程是编译器自己做的),那怎么打印出虚表呢?

VS 2022中,这个虚表指针指向的数组会以0为结点,我们可以利用指针的强制转化在VS 2022的环境下打印虚函数表内的成员内容(其他环境可能会不会这样了,有可能需要写有限循环才可以打印地址)。

并且可以和其他区域的地址对比,推断出虚函数表的存储位置,下面我们在32位环境下,完成打印虚函数表地址和打印虚函数表内内容的任务:

//打印虚函数表地址和虚函数地址,以及虚函数表地址和其他区域地址的对比
#include <iostream>
using namespace std;class Father
{
public:virtual void Function_1(){cout << "Father:Function_1()" << endl;}virtual void Function_2(){cout << "Father:Function_2()" << endl;}
private:int _b_number = 1;
};class Son : public Father
{
public:virtual void Function_1(){cout << "Son:Function_1()" << endl;}virtual void Function_3(){cout << "Son:Function_3()" << endl;}virtual void Function_4(){cout << "Son:Function_4()" << endl;}void Function_5(){cout << "Son:Function_5()" << endl;}
private:int _d_number = 2;
};class Grandson : public Son
{
public:virtual void Function_3(){cout << "Grandson:Function_3()" << endl;}
};void Function(Father* f)
{f->Function_1();
}void test_1()
{Father f;Son s;Grandson g;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello";printf("静态区:%p\n", &a);printf("栈区:%p\n", &b);printf("堆区:%p\n", p1);printf("代码段/常量区:%p\n", p2);printf("虚表地址:%p\n", *((int*)(&s)));printf("Father::Function_1 虚函数地址:%p\n", &Father::Function_1);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Father::Function_2 虚函数地址:%p\n", &Father::Function_2);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Son::Function_1 虚函数地址:%p\n", &Son::Function_1);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Son::Function_2 虚函数地址:%p\n", &Son::Function_2);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Son::Function_3 虚函数地址:%p\n", &Son::Function_3);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Son::Function_4 虚函数地址:%p\n", &Son::Function_4);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Grandson::Function_1 虚函数地址:%p\n", &Grandson::Function_1);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Grandson::Function_2 虚函数地址:%p\n", &Grandson::Function_2);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Grandson::Function_3 虚函数地址:%p\n", &Grandson::Function_3);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("Grandson::Function_4 虚函数地址:%p\n", &Grandson::Function_4);//成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址printf("普通函数地址:%p\n\n", Function);//注意:成员函数这里有一个语法规定,不能只使用函数名,必须加上取地址//注意:不可以使用 bTest.Function_1 这种方式打印虚函数地址,因为函数地址没有被存储在对象中
}typedef void(*VFUNC)();
void PrintVFT(VFUNC arr[])//接受虚函数表指针,并且打印虚函数表内的函数地址内容,然后进行调用
{for (size_t i = 0; arr[i] != 0; i++)//这里是根据 VS 的特殊处理才可以这么写停止循环的条件的,其他环境可能只能自己写固定的循环次数{printf("_vfptr[%d]:%p->", i, arr[i]);arr[i]();//调用函数}printf("\n");
}
void test_2()
{Father f;VFUNC* vPtrT1 = (VFUNC*)(*(int*)&f);//虚函数表地址,如果是 64 位可以改 (int*) 为 (long long*)PrintVFT(vPtrT1);Son s;VFUNC* vPtrT2 = (VFUNC*)(*(int*)&s);//虚函数表地址,如果是 64 位可以改 (int*) 为 (long long*)PrintVFT(vPtrT2);Grandson g;VFUNC* vPtrT3 = (VFUNC*)(*(int*)&g);//虚函数表地址,如果是 64 位可以改 (int*) 为 (long long*)PrintVFT(vPtrT3);
}int main()
{//64位的输出会不一样(因为指针长度变成了 8 字节),建议换成32位平台再运行test_1();test_2();return 0;
}
输出结果如下:
静态区:004CD434
栈区:012FFDA4
堆区:01550C40
代码段/常量区:004CABF8
虚表地址:004CAB74
Father::Function_1 虚函数地址:004C1091
Father::Function_2 虚函数地址:004C1384
Son::Function_1 虚函数地址:004C12A8
Son::Function_2 虚函数地址:004C1384
Son::Function_3 虚函数地址:004C1249
Son::Function_4 虚函数地址:004C1195
Grandson::Function_1 虚函数地址:004C12A8
Grandson::Function_2 虚函数地址:004C1384
Grandson::Function_3 虚函数地址:004C10DC
Grandson::Function_4 虚函数地址:004C1195
普通函数地址:004C110E_vfptr[0]:004C12F3->Father:Function_1()
_vfptr[1]:004C114F->Father:Function_2()_vfptr[0]:004C1523->Son:Function_1()
_vfptr[1]:004C114F->Father:Function_2()
_vfptr[2]:004C1212->Son:Function_3()
_vfptr[3]:004C10E1->Son:Function_4()_vfptr[0]:004C1523->Son:Function_1()
_vfptr[1]:004C114F->Father:Function_2()
_vfptr[2]:004C147E->Grandson:Function_3()
_vfptr[3]:004C10E1->Son:Function_4()

其中很快可以推断虚表地址存储在代码端/常量区。

5.3.多类继承

多类继承就会产生多个虚表,原理和单类继承类似,但是会有this指针修正的问题。

#include <iostream>
using namespace std;class Father
{
public:virtual void Function_1(){cout << "Father:Function_1()" << endl;}virtual void Function_2(){cout << "Father:Function_2()" << endl;}
private:int _f_number = 1;
};class Mother
{
public:virtual void Function_1(){cout << "Mother:Function_1()" << endl;}virtual void Function_2(){cout << "Mother:Function_2()" << endl;}
private:int _m_number = 2;
};
class Son : public Father, public Mother
{
public:virtual void Function_1(){cout << "Son:Function_1()" << endl;}virtual void Function_3()//这个虚函数被放在第一个虚表里了{cout << "Son:Function_3()" << endl;}private:int _s_number = 3;
};typedef void(*VFUNC)();
void PrintVFT(VFUNC arr[])//打印虚函数表内的函数
{for (size_t i = 0; arr[i] != 0; i++)//这里是根据VS的特殊处理才可以这么写停止循环的条件的,其他环境可能只能自己写固定的循环次数{printf("[%d]:%p->", i, arr[i]);arr[i]();//调用函数}printf("\n");
}int main()
{Son s;//打印虚表VFUNC* table1 = (VFUNC*)(*(int*)&s);//第一张虚表//VFUNC* table2 = (VFUNC*)( *( (int*)(  (char*)&s + sizeof(Father))));//第二张虚表(直接使用指针操作)Mother* ptr = &s;VFUNC* table2 = (VFUNC*)(*((int*)ptr));//第二张虚表(利用内置切片让指针偏移)PrintVFT(table1);PrintVFT(table2);printf("Son::Function_1: %p\n", &Son::Function_1);//调用函数Father* p1 = &s;Mother* p2 = &s;p1->Function_1();p2->Function_1();return 0;
}

注意:虚函数都会放到虚表内,但是不一定会造成多态。

多继承也是类似的原理,但是注意有可能会修改this指针的问题。

在这里插入图片描述

注意:上述的Son类还自己定义了一个独属于自己的虚函数,这个虚函数的地址被默认放在该对象的第一张虚表里,这在上述图中也有体现。

5.4.棱形继承

5.4.1.普通棱形继承下的虚函数表

#include <iostream>
using namespace std;class A {
public: virtual void func1() { cout << "A::func1" << endl; }
public:	int _a = 1;
};class B : public A {
public:	virtual void func1() { cout << "B::func1" << endl; }
public:	int _b = 2;
};class C : public A {
public:	virtual void func1() { cout << "C::func1" << endl; }
public:	int _c = 3;
};class D : public B, public C {
public:	virtual void func1() { cout << "D::func1" << endl; }
public:	virtual void func2() { cout << "D::func2" << endl; }
public:	int _c = 4;
};int main() {D d;return 0;
}

实际上棱形继承也是多继承的一种,可以理解为:两个子类继承父类中的虚表(各自拷贝一份虚表),然后孙类继承自两个子类(从两个子类中拷贝了两个虚表),因此孙类具有两个虚表。

其中需要注意,D类还自己定义了一个独属于自己的虚函数func2(),这个虚函数的地址被默认放在该对象的第一张虚表里。

5.4.2.虚继承下的虚函数表

那如果是虚继承下,又有多少张虚表呢?

#include <iostream>
using namespace std;class A {
public: virtual void func1() { cout << "A::func1" << endl; }
public:	int _a = 1;
};class B : virtual public A {
public:	virtual void func1() { cout << "B::func1" << endl; }
public:	int _b = 2;
};class C : virtual public A {
public:	virtual void func1() { cout << "C::func1" << endl; }
public:	int _c = 3;
};class D : public B, public C {
public:	virtual void func1() { cout << "D::func1" << endl; }
public:	virtual void func2() { cout << "D::func2" << endl; }
public:	int _c = 4;
};int main() {D d;return 0;
}

答案是两张,也很好理解,实际上所谓继承父类的虚表,就是继承了父类的虚函数表指针成员,这里BC类有重复的成员,因此将都属于A类的公共部分存储到一个地址处。也就是说:BC共用一份虚函数表。

那么另外一个虚函数表又是哪里来的呢?是D自己创建的,之前为什么没有呢?因为在D类内部还有一个独属于自己的虚函数func2(),这个虚函数的地址原本在没有虚继承的请款下,会被默认放在对象的第一张虚表里。但是这里发生了虚继承,之前的两张虚表已经合二为一了,放进去不太合适,于是D就自己创建了一个虚表,存储这个func2()的地址。

我们再变一下:

#include <iostream>
using namespace std;class A {
public: virtual void func1() { cout << "A::func1" << endl; }
public:	int _a = 1;
};class B : virtual public A {
public:	virtual void func1() { cout << "B::func1" << endl; }
public:	virtual void func3() { cout << "B::func3" << endl; }
public:	int _b = 2;
};class C : virtual public A {
public:	virtual void func1() { cout << "C::func1" << endl; }
public:	virtual void func4() { cout << "C::func4" << endl; }
public:	int _c = 3;
};class D : public B, public C {
public:	virtual void func1() { cout << "D::func1" << endl; }
public:	virtual void func2() { cout << "D::func2" << endl; }
public:	int _c = 4;
};int main() {D d;return 0;
}

这里就有三份虚表了,为什么呢?

  1. 首先BC公共部分共用一份虚表
  2. B有自己的独有虚函数,因此对象d中需要这份虚表
  3. C有自己的独有虚函数,因此对象d中需要这份虚表
  4. D虽然有自己的独有虚函数,但是可以存储到第一份虚表里,也就是B部分的虚表中

因此总结为三种虚表。

补充:更多的相关内容可以看这两篇文章

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

6.抽象概念

在虚函数的后面加上= 0那么这个函数就会变成纯虚函数,只要包含一个及以上的纯虚函数的类就叫抽象类(接口类),抽象类不能实例出对象,这也是我们第一次接触”抽象“的概念。

抽象类强制子类重写虚函数,并且无法实例化出对象,但是可以构建对应类型的指针类型。

#include <iostream>
using namespace std;
class A
{virtual void function() = 0;
};
class B : public A
{void function(){cout << "I am a function." << endl;}
};
int main()
{//A a;//无法创建出对象A* pa;//允许B b;//允许return 0;
}

那么有什么用呢?首先父类是抽象类,因此不存在父类对象,这个抽象只是为了多态而设计的出来的,抽象类可以拥有很多子类(Java就大量使用了这种抽象类)。

#include <iostream>
using namespace std;
class A
{
public:virtual void function() = 0;
};
class B : public A
{
public:void function(){cout << "B:I am a function." << endl;}
};
class C : public A
{
public:void function(){cout << "C:I am a function." << endl;}
};void func(A* a)
{a->function();
}
int main()
{B b;C c;b.function();c.function();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/164047.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux文件系统的功能规划

对于运行的进程来说&#xff0c;内存就像一个纸箱子&#xff0c;仅仅是一个暂存数据的地方&#xff0c;而且空间有限。如果我们想要进程结束之后&#xff0c;数据依然能够保存下来&#xff0c;就不能只保存在内存里&#xff0c;而是应该保存在外部存储中。就像图书馆这种地方&a…

centos7.9 postgresql 16.0 源码安装部署

postgresql 16.0 源码安装部署 环境准备 系统主机名IP地址centos7.9postgres192.168.200.56 软件准备 postgresql-16.0.tar.gz https://ftp.postgresql.org/pub/source/v16.0/postgresql-16.0.tar.gz依赖安装 yum -y install systemd-devel readline readline-devel zlib-devel…

Spring Cloud智慧工地源码,利用计算机技术、互联网、物联网、云计算、大数据等新一代信息技术开发,微服务架构

智慧工地系统充分利用计算机技术、互联网、物联网、云计算、大数据等新一代信息技术&#xff0c;以PC端&#xff0c;移动端&#xff0c;设备端三位一体的管控方式为企业现场工程管理提供了先进的技术手段。让劳务、设备、物料、安全、环境、能源、资料、计划、质量、视频监控等…

【神经网络】【GoogleNet】

1、引言 卷积神经网络是当前最热门的技术&#xff0c;我想深入地学习这门技术&#xff0c;从他的发展历史开始&#xff0c;了解神经网络算法的兴衰起伏&#xff1b;同时了解他在发展过程中的**里程碑式算法**&#xff0c;能更好的把握神经网络发展的未来趋势&#xff0c;了解神…

vue3+ts 项目遇到的问题和bug

1.router中使用pinia报错 pinia.mjs:1709 Uncaught Error: [&#x1f34d;]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"? See https://pinia.vuejs.org/core-concep…

[Kettle] Excel输入

Excel文件采用表格的形式&#xff0c;数据显示直观&#xff0c;操作方便 Excel文件采用工作表存储数据&#xff0c;一个文件有多张不同名称的工作表&#xff0c;分别存放相同字段或不同字段的数据 数据源 物理成绩(Kettle数据集2).xls https://download.csdn.net/download/H…

【TES745D】青翼自研基于复旦微的FMQL45T900全国产化ARM核心模块(100%国产化)

板卡概述 TES745D是一款基于上海复旦微电子FMQL45T900的全国产化ARM核心板。该核心板将复旦微的FMQL45T900&#xff08;与XILINX的XC7Z045-2FFG900I兼容&#xff09;的最小系统集成在了一个87*117mm的核心板上&#xff0c;可以作为一个核心模块&#xff0c;进行功能性扩展&…

【ChatGPT】人工智能的下一个前沿

&#x1f38a;专栏【ChatGPT】 &#x1f33a;每日一句&#xff1a;慢慢变好,我是,你也是 ⭐欢迎并且感谢大家指出我的问题 文章目录 一、引言 二、ChatGPT的工作原理 三、ChatGPT的主要特点 四、ChatGPT的应用场景 五、结论与展望 ​​​​​​​ 一、引言 随着人工智能技…

深度学习入门

全连接批量归一化 目的是&#xff1a;只有一个学习率&#xff0c; 通过归一化&#xff0c;让所有的 x i x_i xi​具有一样的分布&#xff0c;则对每个参数 w i w_i wi​梯度的作用是相当的实现是&#xff1a;实际上是在全连接中增加了两个节点 γ \gamma γ, β \beta β 卷积…

【Linux】程序地址空间回顾

我们的地址空间划分成如下几个区域&#xff1a; 代码区&#xff0c;字符常量区&#xff0c;已初始化全局变量区&#xff0c;未已初始化全局数据区&#xff0c;堆区&#xff0c;栈区&#xff1b;其中有代码区至栈区的代码地址依次增大。 局部变量是具有临时性的局部变量&#x…

Danswer 接入 Llama 2 模型 | 免费在 Google Colab 上托管 Llama 2 API

一、前言 前面在介绍本地部署免费开源的知识库方案时&#xff0c;已经简单介绍过 Danswer《Danswer 快速指南&#xff1a;不到15分钟打造您的企业级开源知识问答系统》&#xff0c;它支持即插即用不同的 LLM 模型&#xff0c;可以很方便的将本地知识文档通过不同的连接器接入到…

fpga时序相关概念与理解

一、基本概念理解 对于数字系统而言&#xff0c;建立时间&#xff08;setup time&#xff09;和保持时间&#xff08;hold time&#xff09;是数字电路时序的基础。数字电路系统的稳定性&#xff0c;基本取决于时序是否满足建立时间和保持时间。 建立时间Tsu&#xff1a;触发器…