c++5.virtual-菱形继承|多态
菱形继承
class A {
public:
int dataA;
};
class B : public A {
public:
int dataB;
};
class C : public A {
public:
int dataC;
};
class D : public B, public C {
public:
int dataD;
};
作用域运算符::
#include <iostream>
class A {
public:
int dataA;
};
class B : public A {
public:
int dataB;
};
class C : public A {
public:
int dataC;
};
class D : public B, public C {
public:
int dataD;
};
int main() {
D {}.B::dataA;//通过作用域运算符解析B作用域范围dataA
D {}.C::dataA;//通过作用域运算符解析C作用域范围dataA
return 0;
}
虚继承
class A {
public:
int dataA;
};
class B : virtual public A {
public:
int dataB;
};
class C : virtual public A {
public:
int dataC;
};
class D : public B, public C {
public:
int dataD;
};
问题分析
int main() {
D d;
d.B::dataA =10 ;
d.C::dataA =20 ;
std::cout<<d.B::dataA <<std::endl;
return 0;
}
- 打开工具并且cd到你的代码目录
cd D:\UE5PJ2\CodeTest\ConsoleApplication1\ConsoleApplication1
- 使用dir检查目录文件
- 输入命令 即可列出对象模型。
cl /d1 reportSingleClassLayout类名 文件名.cpp
虚继承(Virtual Inheritance): 虚继承是一种通过在继承关系中使用
virtual
关键字来解决菱形继承(Diamond Inheritance)问题的技术。它确保在继承链中只有一个共享的基类子对象,从而避免了二义性。在虚继承中,最终派生类只保留一个基类子对象的实例。虚基类(Virtual Base Class): 虚基类是在菱形继承中被声明为虚继承的基类。在上面的例子中,
A
就是一个虚基类。虚基类用于确保只有一个实例被保留在继承体系中,从而解决了二义性问题。虚表(Virtual Table,或称为vtable): 虚表是用于实现动态多态性的一种机制。对于包含虚函数的类,编译器会在该类的对象中插入一个指向虚表的指针。虚表是一个表格,其中包含了该类所有虚函数的地址。当通过基类指针或引用调用虚函数时,实际执行的是虚表中相应函数的地址。
虚指针(Virtual Pointer,或称为vptr): 虚指针是一个指向虚表的指针。对于包含虚函数的类,每个对象都有一个虚指针,指向该对象对应的虚表。通过虚指针,程序能够在运行时确定调用的是哪个版本的虚函数。
class D size(24):
+---
0 | +--- (base class B)
0 | | {vbptr}
4 | | dataB
| +---
8 | +--- (base class C)
8 | | {vbptr}
12 | | dataC
| +---
16 | dataD
+---
+--- (virtual base A)
20 | dataA
+---
D::$vbtable@B@:
0 | 0
1 | 20 (Dd(B+0)A)
D::$vbtable@C@:
0 | 0
1 | 12 (Dd(C+0)A)
vbi: class offset o.vbptr o.vbte fVtorDisp
A 20 0 4 0
多态-虚函数
#include <iostream>
class A {
public:
void speak() {
std::cout << "Hello, A!" << std::endl;
}
};
class B : public A {
public:
void speak() {
std::cout << "Hello, B!" << std::endl;
}
};
void speak(A *a) {
a->speak();
}
int main() {
B b;
speak(&b);
return 0;
}
编译期:
词法分析和语法分析: 源代码首先被分析成令牌(Token)和语法结构。
语义分析: 编译器检查代码的语义是否正确,包括类型检查等。
生成中间代码: 编译器生成中间代码,这是一种与硬件无关的表示。
优化: 编译器进行各种优化,包括但不限于死代码消除、循环展开、内联等。
代码生成: 编译器将中间代码转换成目标机器的汇编代码。
链接期:
目标文件生成: 汇编代码被汇编器转换成目标文件。
符号解析: 链接器解析符号,将函数和变量的引用与其定义关联起来。
地址绑定: 将符号引用绑定到实际的内存地址。这是绑定的一个阶段。
关于绑定:
早绑定(静态绑定): 在编译期或链接期就已经确定了调用的函数或变量的地址。这是在程序执行之前就完成的绑定。C++ 默认采用早绑定。
晚绑定(动态绑定): 在运行时确定调用的函数或变量的地址。这通常涉及虚函数和虚表的机制。在 C++ 中,使用
virtual
关键字声明的虚函数就是为了实现晚绑定。晚绑定是通过虚表和虚指针的机制来实现的。
#include <iostream>
class A {
public:
virtual void speak() {
std::cout << "Hello, A!" << std::endl;
}
};
class B : public A {
public:
void speak() override {
std::cout << "Hello, B!" << std::endl;
}
};
void speak(A *a) {
a->speak();
}
int main() {
B b;
speak(&b); // 输出 "Hello, B!"
return 0;
}
#include <iostream>
class A {
public:
virtual void speak() {
std::cout << "Hello, A!" << std::endl;
}
};
class B : public A {
public:
void speak() override {
std::cout << "Hello, B!" << std::endl;
}
};
void speak(A *a) {
if(a!= nullptr)
{
a->speak();
}
}
int main() {
B b;
speak(&b); // 输出 "Hello, B!"
return 0;
}
多态-纯虚函数
#include <iostream>
class A {
public:
virtual void speak()=0;//此时A类已经变成抽象类
};
class B : public A {
public:
void speak() override {
std::cout << "Hello, B!" << std::endl;
}//必须重写,否则也是抽象类
};
void speak(A *a) {
if(a!= nullptr)
{
a->speak();
}
}
int main() {
A a;//报错,因为现在a是抽象类。
B b;
speak(&b); // 输出 "Hello, B!"
return 0;
}
多态总结
多态性解决了在继承层次结构中的两个主要问题:灵活性和可扩展性: 多态性使得代码更加灵活和可扩展。通过使用基类指针或引用调用派生类对象的虚函数,可以在运行时动态选择执行不同的函数实现。这样,在不修改已有代码的情况下,可以轻松地添加新的派生类,使得系统更具扩展性。
class Shape { public: virtual void draw() { // 基类的虚函数实现 } }; class Circle : public Shape { public: void draw() override { // 派生类的虚函数实现 } }; class Square : public Shape { public: void draw() override { // 另一个派生类的虚函数实现 } };
隐藏细节: 多态性隐藏了对象的具体类型,使得程序员可以使用基类的接口来操作对象,而无需关心对象的实际类型。这种抽象和封装的特性使得代码更易于维护和理解。
void drawShape(Shape* shape) { shape->draw(); // 调用虚函数,根据实际对象类型执行不同的绘制操作 } int main() { Circle circle; Square square; drawShape(&circle); // 调用 Circle 的 draw 函数 drawShape(&square); // 调用 Square 的 draw 函数 return 0; }
多态分为两类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定-编译阶段确定函数地址
- 动态多态的函数地址晚绑定-运行阶段确定函数地址
多态-虚析构|纯虚析构
虚析构函数:
定义: 虚析构函数是在基类中声明为虚函数的析构函数。通过在基类中使用
virtual
关键字声明析构函数,可以实现在派生类对象销毁时调用正确的析构函数,确保释放派生类对象所占用的资源。class Base { public: virtual ~Base() { // 基类的析构函数 } };
多态性: 虚析构函数使得在通过基类指针或引用删除派生类对象时,会调用正确的派生类析构函数。这是通过运行时动态绑定实现的,确保正确释放对象的资源。
Base* obj = new Derived(); delete obj; // 调用 Derived 类的析构函数
纯虚析构函数:
定义: 纯虚析构函数是一个纯虚函数,且它是类的析构函数。通过在基类中声明纯虚析构函数,可以使基类成为抽象类,无法实例化对象。派生类必须实现该纯虚析构函数。
class AbstractBase { public: virtual ~AbstractBase() = 0; }; AbstractBase::~AbstractBase() { // 纯虚析构函数的定义 }
抽象类: 含有纯虚析构函数的类成为抽象类,它不能被实例化。任何继承自抽象类的派生类都必须实现该纯虚析构函数,否则它们也会变成抽象类。
class Derived : public AbstractBase { public: ~Derived() override { // 派生类的析构函数实现 } };
使用虚析构函数和纯虚析构函数的目的是确保在继承层次结构中正确释放资源,并在派生类中提供必要的析构函数实现。这样可以防止资源泄漏和确保正确的对象销毁。