目录
编辑
一.多态的概念
二.多态的构建
虚函数
重写
虚函数重写的例外
协变
隐藏
析构函数的重写
三.重载、重写(覆盖)、隐藏(重定义)的对比
四.C++11新增的 override 和 final
override
final
五.抽象类
六.多态的原理
虚函数表
总结:
引用和指针如何实现多态
虚函数表存放位置
七.单继承和多继承关系的虚函数表
单继承中的虚函数表
打印虚表
总结:
多继承中的虚函数表
打印虚表
打印第二章虚表
多继承多个虚表
编辑 总结:
八.菱形继承和菱形虚拟继承
菱形继承
菱形虚拟继承
区别和联系:
九.例题
一.多态的概念
多态是面向对象编程中的一个核心概念,它允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。在C++中,多态主要通过虚函数来实现。
二.多态的构建
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
// 声明一个基类Person,其中包含一个虚函数BuyTicket
class Person
{
public:
// 虚函数声明,使用virtual关键字,使得通过基类指针或引用调用该函数时能够根据实际对象类型动态绑定
virtual void BuyTicket() //虚函数,是建立多态的条件
{
cout << "全票" << endl; // Person类中BuyTicket函数的行为是打印“全票”
}
};
// Student类是从Person类公有继承而来
class Student : public Person
{
public:
// 重写了基类中的虚函数BuyTicket,这是多态的表现
virtual void BuyTicket() // 重写基类的虚函数
{
cout << "半票" << endl; // Student类中BuyTicket函数的行为是打印“半票”
}
};
// 函数func接受一个Person类的引用作为参数
void func(Person& p)
{
p.BuyTicket(); // 调用传入对象的BuyTicket函数,由于BuyTicket是虚函数,这里会根据实际对象类型执行对应版本的函数
}
int main()
{
Person p; // 创建Person类的对象p
Student s; // 创建Student类的对象s
// 将基类和派生类的对象分别传递给func函数
func(p); // 输出“全票”,因为p是Person类型的对象
func(s); // 输出“半票”,尽管传入的是Student对象的引用,但通过基类引用调用,由于虚函数机制,会正确调用到Student的BuyTicket
return 0;
}
注:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(
因为派生类继承的是虚函数的声明),但是这种写法不规范,不建议这样使用 。
虚函数
被virtual修饰的类成员函数称为虚函数。
注:和虚继承没有任何关系,只是一个关键词两用。
virtual void BuyTicket() //携带关键字 virtual的函数
{
cout << "虚函数" << endl;
}
重写
派生类中有个跟基类相同的虚函数,则称为重写;
相同指 三同:函数名相同,返回值相同,参数列表相同;
class Base {
public:
virtual void print()
{ // 虚函数声明
cout << "Base Class" <<endl;
}
};
class Derived : public Base {
public:
void print()
{
// 使用override重写父类的虚函数
cout << "Derived Class" << endl;
}
};
int main() {
Base* basePtr = new Derived(); // 基类指针指向子类对象
basePtr->print(); // 输出 "Derived Class",展示了多态性
delete basePtr; //释放
return 0;
}
虚函数重写的例外
协变和析构是例外
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变,简单来说,就是基类的返回值和派生类的返回值是同样的继承关系
class A {};
class B : public A {}; //继承关系
class Base {
public:
virtual A* f() //基类虚函数返回值是 另外一个基类的指针/引用
{
return new A;
}
};
class Derive : public Base
{
public:
virtual B* f() //派生类重写了基类的虚函数,返回派生类对象的指针/引用
{
return new B;
}
};
隐藏
在了解析构函数的重写前,先了解隐藏
指在派生类中定义了一个与基类中同名的成员(可以是函数、变量等),但并没有使用virtual关键字进行虚函数的重写。这种情况下,基类的成员在派生类的上下文中被隐藏了,而不是被重写。这意味着,如果你有一个派生类的对象,并尝试通过派生类的引用或指针访问这个同名成员,你将访问到派生类的版本;但是,如果通过基类的引用或指针访问,则仍然只能访问到基类的成员,即使该对象实际上是派生类类型的实例。
详情点击【C++】继承|切片|菱形继承|虚继承-CSDN博客中有关于 隐藏/重定义的详解。
析构函数的重写
首先了解到,编译器会将析构函数统一处理成destructor,而C++中又有隐藏这一处理。
先看,如果没有加virtual,构成隐藏
class Base {
public:
~Base()
{
cout << "~Base" << endl;
}
};
class Derive : public Base
{
public:
~Derive()
{
cout << "~Derive" << endl;
}
};
int main()
{
//Base b;
//Derive d;
Base* ptrb1 = new Base;
Base* ptrb2 = new Derive;
delete ptrb1;
delete ptrb2;
return 0;
}
发生隐藏后,调用的时候只看自身的类型,是Base就调用Base的函数,是Derive就调用Derive的函数,不构成多态,且派生类析构没有调用,内存泄漏
加上virtual的析构函数 ,就能正常析构了
注:析构函数加virtual是在new场景下才需要, 其他环境下可以不用
解释如下:
- 对象
d
(类型为Derive)首先被销毁,调用其析构函数,输出~Derive
。- 紧接着,因为
d
的析构完成后,会自动调用其基类Base
的析构函数,输出~Base
。- 最后,对象
b
(类型为Base
)被销毁,调用其析构函数,再次输出~Base
。
三.重载、重写(覆盖)、隐藏(重定义)的对比
四.C++11新增的 override 和 final
C++11 引入了override和 final 两个关键字,它们增强了面向对象编程中的继承和多态特性,提高了代码的安全性和可读性。
override
关键字用于指示一个虚函数是其基类中虚函数的重写版本。
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
在派生类中定义虚函数时,在函数声明后紧跟 override关键字。
final
用途:
当用于类声明时,它表示该类不能被其他类继承,即禁止继承。
当用于虚函数声明时,它表示该虚函数在其派生类中不能被进一步重写,即锁定重写。
语法:
对于类,将 final 放在类声明的末尾;对于虚函数,在其声明后紧跟 final 关键字。
五.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类);
抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
六.多态的原理
虚函数表
- 每个包含虚函数的类都会有一个虚函数表。
- 每个对象的内存布局中,第一个位置通常是一个指向其类虚函数表的指针。
- 当通过基类指针或引用调用虚函数时,程序会先访问对象的虚函数表指针,然后根据虚函数在表中的索引找到正确的函数指针,并调用该函数。
- 在多重继承的情况下,每个类都有自己的虚函数表,对象的内存布局中可能包含多个虚函数表指针
class A
{
public:
int i;
virtual void Print() { } // 虚函数
};
int main()
{
A a;
cout << sizeof(a) <<endl; //输出结果是8 当然根据不同X86/X64来决定指针大小
return 0;
}
因为该类有虚函数,所以至少有一个虚函数表指针:虚函数的地址会放在这个虚函数表里面。
而如果有派生类,派生类会继承基类的虚函数表和成员变量。
class A
{
public:
virtual void Print() { } // 虚函数
virtual void Print1() { } // 虚函数
virtual void Print2() { } // 虚函数
virtual void Print3() { } // 虚函数
void func() { }
protected:
int _a = 1;
};
class B :public A
{
public:
virtual void Print() {} //重写
virtual void func() {} //派生类自己特有的虚函数
protected:
int _b = 0;
};
int main()
{
A a;
B b;
cout << sizeof(a)<<","<<sizeof(b) << endl; //8,12
return 0;
}
类A: 包含一个虚函数表指针(通常占用4或8字节,具体取决于平台),因为类A中至少有一个虚函数。成员变量_a是一个 int 通常占用4字节。
类B: B会继承类A的所有成员,包括虚函数表指针和成员变量,自身也有成员变量,所以是4/8字节的虚函数表指针,4字节的类A成员变量,4字节自己的成员变量;
详解:
类B还重写了虚函数Print(),并且新增了一个虚函数func(),
但是重写不会增加虚函数表的大小,新增的虚函数会使得虚函数表中增加一项,这不会直接影响类的大小,因为虚函数表本身只有一份,但是每个对象需要一个指向该表的指针。
总结:
- 虚函数自身自带一个_vfptr的虚函数表
- 子类虚函数继承父类虚函数时,不仅继承父类虚函数表,也可以对虚函数进行重写。
- 子类虚函数继承父类虚函数时,非虚函数不会存放在_vfptr中
- 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
引用和指针如何实现多态
为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?
传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数
传递子类,首先会进行切割 ;
将子类的内容切割掉,父类再去接受这个数据了,一样有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。
这样就完成了多态。
且记:是运行时才知道调用的是基类还是派生类的,这就是运行时多态;
同类对象的虚表一样。
如果子类没有重写父类的虚函数,那么他们的虚函数表指针不同,但里面的内容相同
虚函数表存放位置
我们通过代码来打印各个区地地址,可以判断虚函数表存放位置
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
void func()
{}
int main()
{
Base b1;
Base b2;
static int a = 0;
int b = 0;
int* p1 = new int;
const char* p2 = "hello world";
printf("静态区:%p\n", &a);
printf("栈:%p\n", &b);
printf("堆:%p\n", p1);
printf("代码段:%p\n", p2);
printf("虚表:%p\n", *((int*)&b1));
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数:%p\n", func);
}
注意打印虚表这里,vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节
b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址
七.单继承和多继承关系的虚函数表
单继承中的虚函数表
// 基类Base,包含一个虚函数func1和func2,以及一个私有成员变量a
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; } // 虚函数func1的实现
virtual void func2() { cout << "Base::func2" << endl; } // 虚函数func2的实现
private:
int a; // 私有成员变量a
};
// 派生类Derive,公有继承类Base
class Derive : public Base {
public:
// 重写基类的虚函数func1
virtual void func1() { cout << "Derive::func1" << endl; }
// 添加新的虚函数func3
virtual void func3() { cout << "Derive::func3" << endl; }
// 添加另一个新的虚函数func4
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b; // 私有成员变量b
};
// 从Derive类进一步派生出的类MM
class MM : public Derive {
public:
// 重写从Derive继承来的虚函数func3
virtual void func3() { cout << "MM::func3" << endl; }
};
int main()
{
Base b; // 创建Base类的对象b
Derive d; // 创建Derive类的对象d
MM m; // 创建MM类的对象m
return 0;
}
调试窗口有时候不可信,这时候要去看内存;
打印虚表
虚函数表指针,其实就是一个函数数组指针,这个数组中的每个元素都是一个函数指针,指向类的虚函数实现
// 基类Base,包含虚函数func1和func2,以及一个私有成员变量a
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; } // 虚函数func1的实现
virtual void func2() { cout << "Base::func2" << endl; } // 虚函数func2的实现
private:
int a; // 私有成员变量a
};
// 派生类Derive,公有继承自Base
class Derive : public Base {
public:
// 重写基类的虚函数func1
virtual void func1() { cout << "Derive::func1" << endl; }
// 添加新的虚函数func3
virtual void func3() { cout << "Derive::func3" << endl; }
// 添加另一个新的虚函数func4
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b; // 私有成员变量b
};
// 从Derive类进一步派生出的类X
class MM : public Derive {
public:
// 重写从Derive继承来的虚函数func3
virtual void func3() { cout << "MM::func3" << endl; }
};
// 函数指针类型定义,用于指向无参数无返回值的函数
typedef void(*VFTPTR)();
// 打印虚函数表的函数
void PrintVFPtr(VFTPTR a[])
{
for (size_t i = 0; a[i] != 0; i++) // 遍历虚函数表,直到遇到空指针为止
{
printf("a[%d]:%p\n", i, a[i]); // 打印当前索引的函数指针地址
}
cout << endl;
}
int main()
{
Base b; // 创建Base类的对象b
Derive d; // 创建Derive类的对象d
MM m; // 创建MM类的对象m
// 使用指针技巧获取虚函数表地址并打印其内容
// 指针和指针之间可以进行强转
// 整型家族可以跟指针进行强转
PrintVFPtr((VFTPTR*)*(int*)&b); // 打印Base对象b的虚函数表
PrintVFPtr((VFTPTR*)*(int*)&d); // 打印Derive对象d的虚函数表
PrintVFPtr((VFTPTR*)*(int*)&m); // 打印MM对象m的虚函数表
return 0;
}
- &b获取对象
b
的地址,它是一个指向Base对象的指针。- (int*)&b将这个地址强制转换为一个指向 int 的指针。大部分编译器在对象的内存布局中,虚函数指针位于对象的起始位置,并且其大小与int相同。
- *(int*)&b解引用这个int指针,实际上得到的是对象b的虚函数表指针的值。
- (VFTPTR*)... 再次进行类型转换,这次是将前面得到的值(虚函数表的地址)转换为指向函数指针的指针,即(VFTPTR*)。这一步我们可以将这个地址传递给PrintVFPtr函数,进而访问虚函数表。
我们可以将代码修改一下,得到该地址具体是什么
因为a[i]里面存放的就是函数指针,因此我们可以选择直接调用。
// 函数声明,接受一个VFTPTR类型的指针数组,VFTPTR是一个 无返回值无参数函数指针
void PrintVFPtr(VFTPTR a[]) { //VFTPTR a[] 是一个函数指针数组,元素类型为 VFTPTR
for (size_t i = 0;
a[i] != 0; // 循环条件是当前指针不为空(通常虚函数表以NULL(00000000)结尾)
i++) // 每次循环迭代,i递增,指向下一个虚函数
{
// 打印当前虚函数指针在数组中的索引及其地址
printf("a[%d]:%p->", i, a[i]);
// 创建函数指针变量 临时保存当前索引处的虚函数指针 就是指向的虚函数地址
VFTPTR p = a[i];
// 调用当前索引处的虚函数(通过函数指针调用)
/*调用 p() 使用函数指针时,通过在指针后面加上括号 () 就可以实现对指针所指向函数的调用。
这与直接调用一个函数的方式相似,例如 func(),区别在于这里是通过一个指向该函数地址的指针来实现调用。*/
p();
}
详解:VFTPTR p = a[i];
p();
- p:是一个函数指针,来自数组 a 中索引为 i 的函数地址赋给了它。这个地址指向某个具体的函数实现。
- p() :意味着“调用 p 所指向的函数”。编译器会解析 p 指针所存储的地址,然后跳转到该地址执行函数代码。
总结:
每个类都有一个自己的虚函数表,其中包含了该类所有虚函数的地址。在多重继承或更复杂的继承结构中,虚函数表的布局和内容可能会更加复杂,但基本原理相同。
多继承中的虚函数表
两个基类,都有各自的func1,func2。派生类继承了类Base1,类Base2,且重写了func1,增加了自己的func3
// 定义一个函数指针类型,用于指向无参数无返回值的函数
typedef void(*VFTPTR)();
// 打印虚函数表的函数,接受一个函数指针数组作为参数
void PrintVFPtr(VFTPTR a[]) {
// 遍历数组直到遇到空指针
for (size_t i = 0; a[i] != 0; i++) {
// 打印当前函数指针的索引和地址
printf("a[%d]:%p->", i, a[i]);
// 调用当前函数指针指向的函数
VFTPTR p = a[i];
p();
}
cout << endl; // 换行
}
// 定义基础类Base1,包含两个虚函数
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; } // 虚函数func1
virtual void func2() { cout << "Base1::func2" << endl; } // 虚函数func2
private:
int b1; // 私有成员变量
};
// 定义另一个基础类Base2,同样包含两个虚函数
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; } // 虚函数func1
virtual void func2() { cout << "Base2::func2" << endl; } // 虚函数func2
private:
int b2; // 私有成员变量
};
// 派生类Derive从Base1和Base2继承,添加自己的虚函数和成员变量
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; } // 重写func1
virtual void func3() { cout << "Derive::func3" << endl; } // 添加新的虚函数func3
private:
int d1; // 私有成员变量
};
int main() {
// 创建Derive类的对象d
Derive d;
// 通过强制类型转换和指针偏移访问Derive对象的虚函数表
PrintVFPtr((VFTPTR*)(*(int*)&d));
// 打印Derive对象的大小,用于观察多继承对对象大小的影响
// cout << sizeof(d) << endl;
return 0; // 程序正常结束
}
可以看到有两个虚表,但是自身特有的虚函数放在哪张表呢?
打印虚表
当一个类通过多重继承从多个基类继承时,派生类可能会有多个虚函数表(vtable)和虚基类表。
每个非虚基类的虚函数表:
- 如果派生类从每个非虚基类继承了虚函数,那么派生类将为每个基类拥有一个独立的虚函数表。
虚基类的虚基类表:
- 如果派生类通过继承引入了虚基类,那么将为每个虚基类有一个虚基类表,用于管理虚基类的布局信息。
派生类自己的虚函数表:
- 如果派生类本身声明了新的虚函数或重写了继承的虚函数,它将拥有自己的虚函数表。
打印出来的结果只有第一张的虚表,且自身的func3虚函数在该表中。可以推论出该对象模型;
打印第二章虚表
由上可见,
方法1:
将&d强转为Base1*,这样+1就会跳过整个Base1,就刚好到达了Base2类的开始,再进行之前的强转便可以打印了。
方法2:
直接将&d赋值给Base2* ptr;这样Base2会进行切片操作,于是ptr就直接指向了Base2的虚函数表,依然就行之前的强转操作便可以打印了。
int main()
{
Derive d; // 创建一个Derive类的对象d
// 直接通过Derive对象d访问虚函数表
PrintVFPtr((VFTPTR*)(*(int*)&d));
// 这里首先取d的地址(&d),然后强制转换为int*,接着解引用(*)得到虚函数表指针的地址,
// 最后再次强制转换为VFTPTR*来打印Derive类的虚函数表。
// 方法1: 通过将d转换为Base1指针并偏移1来尝试访问虚函数表
PrintVFPtr((VFTPTR*)(*(int*)((Base1*)&d +1)));
// 这里首先将d的地址强制转换为Base1*,为了访问Base1的虚函数表,
// (Base1*)&d +1操作是访问紧随Base1之后的内存区域,
// 方法2: 通过Base2指针访问Derive对象的虚函数表
Base2* ptr = &d; // 将Derive对象d的地址赋给Base2指针ptr 切片
PrintVFPtr((VFTPTR*)(*(int*)ptr));
// 首先将ptr转换为int*,然后解引用得到Base2的虚函数表指针地址,
// 再次转换为VFTPTR*以打印虚函数表。但是,会打印Base2的虚函数表,
// 而非Derive的,因为ptr是指向Derive对象中Base2子对象的首地址。
return 0;
}
总结:Derive类对象的虚函数会放在多继承中继承的第一个类的虚函数表里(即Base1类虚函数表)
多继承多个虚表
如果没有多个虚表,那么p1去调用func1(),p2也去调用func1(),
如若d没有重写func1(),那么这个多态就会紊乱,调用的都是那一个func1()了,而不是p1调用Base1的func1(),p2调用Base2的func1()了。因此多继承就需要多个虚表才不会出现紊乱的问题
总结:
在多继承的情况下,如果基类中有同名的虚函数(如func1()),每个基类都会维护自己的虚函数表,即使这些函数在名字和签名上完全相同。
即使Base1和Base2中都有名为func1()的虚函数,它们在各自类的虚函数表中会有不同的地址,因为它们实际上是两个不同的函数实现。 当一个派生类(如Derive)继承了这些基类,它会继承这些虚函数表。
如果派生类重写了这些同名的虚函数,则派生类的虚函数表中会包含这个重写后的函数地址。如果没有重写,派生类对象在调用这些函数时,会根据调用上下文(即通过哪个基类的指针或引用调用)来决定使用哪一个基类的函数实现。
在多继承体系中,每个基类的虚函数表都是独立的,即使它们包含同名函数,这些函数也是基类定义范围内的独立实现。这保证了多态性,即派生类对象可以正确响应通过基类指针或引用调用的虚函数,无论这些函数在多少个基类中被重载。
八.菱形继承和菱形虚拟继承
两种不同的继承结构,它们都涉及到基类和派生类之间的关系
菱形继承
菱形继承是一种继承结构,其中两个或多个派生类继承自同一个基类,然后一个更深层次的派生类继承自这两个派生类。这种结构在类图上形成了一个菱形的样子;
class Base {
public:
int value;
};
class Derive1 : public Base {
};
class Derive2 : public Base {
};
class MostDerived : public Derive1, public Derive2 {
// MostDerived 有两个 Base 的拷贝,一个来自 Derive1,一个来自 Derive2
};
菱形虚拟继承
是解决菱形继承问题的一种方法。通过将基类的继承方式改为虚继承(在继承时使用
virtual
关键字),可以确保所有派生类共享同一个基类子对象的拷贝。优点:
- 避免了内存浪费,因为所有派生类共享同一个基类实例。
- 保持了数据一致性,因为所有对基类成员的修改都作用于同一个实例。
class Base {
public:
int value;
};
class Derive1 : virtual public Base {
};
class Derive2 : virtual public Base {
};
class MostDerived : public Derive1, public Derive2 {
// MostDerived 只有一个 Base 的拷贝,所有 Derived 共享
};
区别和联系:
- 菱形继承不使用 virtual 关键字,导致基类在派生类中被多次实例化。
- 菱形虚拟继承使用 virtual 关键字来避免重复实例化基类,确保所有派生类共享同一个基类实例。
- 菱形虚拟继承解决了菱形继承中的数据一致性和内存浪费问题。
九.例题
class A
{
public:
virtual void func1(int val = 1)
{
cout << "A::func1->val = " <<val<< endl;
}
virtual void test()
{
func1();
}
};
class B:public A
{
public:
virtual void func1(int val = 0)
{
cout << "A::func1->val = " <<val<< endl;
}
};
int main()
{
B* b = new B;
b->test();
return 0;
}
是不是结果很匪夷所思
B类型的对象p去调用test();
test()是B类继承的,但是里面默认存放的this指针依然是A*,B类里面的func()是重写了A类的func() (A类func()为虚函数,B类重写了可以不写virtual)。
重写的关键点:
仅仅是重写了基类的实现,函数的声明,依然是A类的声明,因此给到的val默认值是1,调用了B类的函数实现!!! 所以输出B->1