关于cpp的多态(详细梳理)

本文围绕C++多态展开,介绍了多态的概念、定义及实现条件,包括虚函数、虚函数重写等。还阐述了抽象类的概念和作用,分析了多态的原理,如虚函数表的生成和使用。此外,对比了单继承和多继承的虚函数表,最后通过问答巩固知识,并补充了重载的原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 多态的概念:

  就是去完成某一项行为的时候,对于不同的对象会产生不同的效果。比如当一个人去开车的时候,他开的是五菱宏光还是劳斯莱斯所带来的感受是完全不一样的。

  2. 多态的定义及实现

  2.1 多态的构成条件

  多态是在不同关系的类对象,去调用同一函数,产生了不同的效果。

  多态使用的两个条件:

  1.必须是通过父类指针或者引用调用虚函数。

  2.被调用的函数必须是虚函数,而且子类重写了父类这个虚函数。

  2.2 虚函数

  虚函数:被virtual修饰的类成员函数就是虚函数。

  注意,此处不要和虚继承搞混淆了,在继承关键字前加virtual是虚继承。虚继承是为了解决菱形继承带来的数据冗余和二义性问题的。

  2.3 虚函数的重写

  虚函数的重写(覆盖):就是子类中有一个完全跟父类虚函数相同的虚函数(即满足三同)。

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

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

  注意,此处在子类重写的虚函数前可以不加virtual(因为继承后父类的虚函数在子类中依旧保留了虚函数属性),但是这个写法不规范,不建议这样写。

  关于虚函数重写有两个例外:

  1. 协变(父类和子类虚函数的返回值不同)。

  正常情况下,重写需要三同。但是当返回值是父类关系且同时为指针或者同时为引用时,可以不同,就是协变。

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;}
};

  就算不是这个类的,只要满足了父子关系就行。

  2.析构函数的重写。(父类与子类的析构函数名不相同)

  析构函数也可以重写,但是它们的函数名确不同。其实,在这里编译器对于析构函数名做了特殊处理,统一将函数名改为destructor

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

  这个地方还是很重要的,这能帮助编译器调用到正确的析构函数。假设上述代码中,假设父类指针指向了子类对象构成了多态,但是在结束时析构的时候又没有多态,那么当子类动态开辟了空间时,它又没有调用到子类的析构函数,那么就会造成内存泄漏。

  2.4 C++11 override和final

  这两个关键字是帮助程序猿,防止在需要进行重写的地方忘记重写等原因导致编译期间不会出错,但是运行的时候又出幺蛾子的情况。所以C++11提供了这两个关键字,帮助用户检查是否重写。

  1.final:修饰虚函数,表示该虚函数不能再被重写。

class Car
{
public:
virtual void Drive() final
 {}
};

  2.override:检查子类的虚函数是否重写了父类的某个虚函数,如果没有重写就会编译报错。

  

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

  2.5 重载,重写(覆盖),隐藏(重定义)的对比

  3.抽象类

  3.1 概念

  在虚函数后面加上一个=0,这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能被实例化。子类继承后如果没有重写纯虚函数,那么它也不能实例化出对象。

  纯虚函数规范了子类必须重写,另外虚函数更体现了接口继承

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

  3.2 接口继承和实现继承

  普通函数的继承是一种实现继承,它继承的是父类的函数实现。虚函数的继承是一种接口继承,子类继承的是父类的函数接口,目的是为了重写,实现多态,如果不需要多态就不要定义成虚函数。

  4. 多态的原理

  4.1 虚函数表

  简称虚表。当一个类中有虚函数的时,那么它的对象中就会多一个虚函数表地址,它指向了一个虚函数表,里面放着该类虚函数的地址。

  当子类继承了有虚函数的父类时,那么它的对象中也会有一个虚表指针,不同父类的是,子类的虚表是在父类的基础上进行拷贝后,把自己重写的虚函数在虚函数表中进行了覆盖。即重写是语法层面的叫法,覆盖是原理层的叫法。注意:只有虚函数才会被放入虚表。

  虚函数表的本质是一个存了虚函数指针的指针数组,一般情况下,这个数组的最后一个会存nullptr。

  注意:纯虚函数是没有虚函数指针的。

  总结虚表的生成:先将父类的虚表内容拷贝一份到子类的虚函数表中,然后子类将自己重写的虚函数覆盖到虚表中。另外如果子类自己增加的虚函数会按声明顺序添加到虚表的最后。

  最后关于一些易错的地方:虚函数表在vs下是存在代码区(常量区)的。对象中存的是虚表指针而不是虚表,一个类它只能生成一张虚表,它可以通过继承即多继承获得一张或多张虚表。当多继承时,子类有自己的虚函数时,它会把这个虚函数放到第一个父类的虚表的最后。注意:继承是对父类虚表的覆盖,比如B继承A,那么它里面的虚表就是B对A虚表的覆盖,多继承时也是如此,比如C继承A和B,那么它里面就俩张虚表,一张对A的覆盖,另一张对B的覆盖。

  4.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;
}

  在上述代码中,当父类引用实例化父类对象时,在调用函数时,它就会去在父类对象中的虚表中去查找,如果实例化子类对象时,那么它就在子类对象中的虚表去查找,以此来达成多态。可以说,满足多态以后的函数的调用,它不是在编译阶段确定的,它是在运行阶段找到的。而不满足多态的函数的调用,它是在编译阶段就确定的。

  4.3 静态绑定与动态绑定

  静态绑定也叫前期绑定(早绑定),它是在程序编译阶段就确定好的,比如:函数重载。

  动态绑定也叫晚期绑定(晚绑定),是在程序运行阶段,根据具体拿到的类型,确定程序具体的行为,调用不同函数。比如:多态。

  5. 单继承和多继承的虚函数表

  5.1 单继承的虚函数表

  这里比较简单,就如之前所说,先将父类的虚表进行拷贝给子类,然后子类再进行覆盖,另外就是子类中有自己的虚函数,那么会加在虚函数表的最后(按声明顺序)。

  5.2 多继承的虚函数表

  多继承的子类的为重写的虚函数会放在第一个继承的父类部分的虚表当中。另外就是后面的虚表其实也是通过编译器的操作用偏移量来实现多态的。

  另外就是要尽量避免菱形继承和零星虚拟继承。

  6. 加强巩固

  1.什么是多态?

  答:完成某个行为时,当不同的对象去完成时会产生不同的状态。

  2.什么是重载,重定义,重写?

  答:

  重载就是在一个类中定义多个函数名相同,但参数不同的函数。 它要求在同一作用域中,且函数名要相同,参数不相同。

  重定义就是子类中有与父类相同名称的成员,可以是变量也可以是函数,子类会屏蔽对父类同名成员的直接访问。

  重写就是子类有跟父类完全相同的虚函数。它要求返回值,函数名,参数相同。当然也会有协变和析构函数重写的特殊情况。

  3.多态的实现原理?

  答:在程序运行时期,在父类指针引用调用重写的函数的时候,会通过子类对象中的虚表指针找到虚表,在虚表中找到被对应覆盖的函数进行调用。

  4.inline函数可以是虚函数吗?

  答:可以。但是编译器就会忽略inline属性,这个函数就不再是inline函数,因为它会被放到虚表中去。

  5.静态函数可以是虚函数吗?

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

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

  答:不可以。因为虚函数表指针是在初始化列表才初始化的。

  7.析构函数可以是虚函数吗?在什么场景下使用?

  答:可以。并且把最好父类的析构函数定义为虚函数。比如当子类对象动态开辟了空间,如果没有多态去调用子类对象的话,就会导致内存泄露。

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

  答: 首先如果是普通对象,那么是一样快的。如果是指针对象或者是引用对象构成了多态,那么是调用普通函数要快一些,因为构成多态,调用虚函数需要到虚函数表中去查找。

  9.虚函数表是在哪个阶段生成的,存在哪?

  答: 是在编辑阶段生成的,一般情况存在代码段(常量区)。

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

  答: 菱形继承会导致数据冗余和二义性问题。在虚继承后,当子类继承多个父类时,父类的公有部分的成员只有一份且放在子类的底端,在父类的内部原本公有部分的成员通通化身为一个虚基表指针,它指向了一个虚基表,里面存的是与子类底端爷爷部分成员的与虚基表指针的偏移量,他会通过这个偏移量来找到爷爷的成员进行访问。

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

  答: 包含纯虚函数的类叫做抽象类。抽象类强制重写了虚函数,另外抽象类体现出接口继承关系。

  补充:

  1.一个类的多个对象共享该类的虚表。 

  2.有虚函数的父类和子类对象前四个字节都是虚表地址,且各不相同,指向各自的虚表。

  3.子类自己的虚函数只会放到第一个父类的虚表后面,其它父类的虚表不需要储存,因为储存了也不能调用。

  4.如果满足多态的子类的函数是私有的,它也能调用。因为它是通过虚表地址找到虚表里面的覆盖了的函数地址,通过地址可以调用。

---------------------------------------------------------------------------------------------------------------------------------

2023-8-30

补充相关问题:

1.重载的原理是什么?

  答:编译器在编译时会对函数进行名称修饰,它是根据函数名和参数列表类型等信息组合成一个唯一的函数名,比如在linux系统下,有多个函数名相同构成重载的函数,有一个参数类型是int,那么在编译时它的函数名在经过修饰后末尾会加上一个i来表示它的参数类型是int,double的话就同理加上一个d,这样在调用的时候就可以根据传入的参数来匹配合适的函数。

<think>我们正在解析一个C++的ONNX推理类(假设类名为ONNXInference)。根据引用内容,我们将重点分析其继承关系、虚函数、成员变量及其作用。由于没有提供具体的类定义,我们将基于常见的ONNX推理实现模式进行解析,并参考引用中提到的C++特性(如虚函数、类大小、成员初始化等)。 1. **继承关系**:ONNX推理类可能继承自一个基类(例如一个通用的推理接口类),以实现多态。引用[1]提到虚函数用于实现多态,通过基类访问派生类定义的函数。因此,基类中可能定义了虚函数接口(如`predict`、`loadModel`等),而ONNXInference类会重写这些虚函数。 2. **虚函数**:在ONNXInference类中,可能会有以下虚函数: - `virtual bool loadModel(const std::string& modelPath);` 用于加载模型,可能被重写以提供ONNX特定的加载逻辑。 - `virtual void runInference(const std::vector<float>& input, std::vector<float>& output);` 执行推理,基类定义接口,派生类实现具体推理。 引用[1]指出,虚函数表(vtable)会增加一个指针(vptr)的大小,引用[2]也提到有虚函数的类会有一个指向虚函数表的指针(4字节)。 3. **成员变量**: - 必须包含一个指向ONNX运行时环境的指针,如`Ort::Env* env_;`(使用ONNX Runtime库时)。 - 会话指针:`Ort::Session* session_;`,用于保存加载的模型。 - 输入输出节点信息:`std::vector<const char*> input_names_;` 和 `std::vector<const char*> output_names_;`。 - 其他辅助变量,如模型路径`std::string model_path_;`,是否初始化标志`bool initialized_;`等。 引用[3]提到成员变量的初始化顺序按照声明的顺序进行。 4. **构造函数与析构函数**: - 构造函数:初始化成员变量,特别是指针初始化为`nullptr`,字符串初始化为空,布尔值初始化为`false`。 - 析构函数:释放资源(如销毁会话、环境等)。 5. **内存对齐与类大小**:引用[2]提到类大小会考虑成员变量和虚函数表指针(如果有虚函数),并涉及内存对齐。例如,一个包含虚函数、一个指针和一个字符串的类,其大小可能为:指针(8字节,64位系统)+字符串(通常24字节,但实现依赖)+虚表指针(8字节,64位系统),然后考虑对齐(如对齐到8字节边界)。 下面是一个模拟的ONNXInference类的结构(使用ONNX Runtime库): ```cpp #include <string> #include <vector> #include <onnxruntime_cxx_api.h> // 基类:定义通用推理接口 class InferenceInterface { public: virtual ~InferenceInterface() = default; virtual bool loadModel(const std::string& modelPath) = 0; virtual void runInference(const std::vector<float>& input, std::vector<float>& output) = 0; }; // 派生类:ONNX推理实现 class ONNXInference : public InferenceInterface { public: ONNXInference(); // 构造函数 virtual ~ONNXInference(); // 虚析构函数 // 重写基类虚函数 virtual bool loadModel(const std::string& modelPath) override; virtual void runInference(const std::vector<float>& input, std::vector<float>& output) override; private: // 成员变量 Ort::Env* env_; // ONNX运行时环境 Ort::Session* session_; // ONNX会话 std::vector<const char*> input_names_; // 输入节点名称 std::vector<const char*> output_names_; // 输出节点名称 std::string model_path_; // 模型路径 bool initialized_; // 初始化标志 // 辅助函数 void preprocess(const std::vector<float>& input, Ort::Value& tensor); void postprocess(Ort::Value& tensor, std::vector<float>& output); }; ``` **成员变量作用**: - `env_`: ONNX Runtime的环境,每个进程通常只需要一个实例。 - `session_`: 代表加载的ONNX模型,用于执行推理。 - `input_names_`, `output_names_`: 存储模型的输入和输出节点的名称,用于在运行推理时指定输入输出。 - `model_path_`: 保存模型路径,便于重新加载等操作。 - `initialized_`: 标记模型是否已成功加载,避免重复加载或未加载就推理。 **虚函数实现**: - `loadModel`: 创建环境和会话,加载ONNX模型,并获取模型的输入输出节点信息。 - `runInference`: 将输入数据转换为ONNX Runtime需要的张量格式,运行会话,获取输出张量并转换为输出向量。 **注意事项**: - 由于有虚函数,类中会有一个虚函数表指针(vptr),因此类的大小至少增加一个指针的大小(64位系统为8字节)。 - 成员变量的初始化顺序:按照声明顺序,先初始化`env_`(指针,未显式初始化则随机),然后`session_`,然后`input_names_`等。因此,在构造函数中应进行显式初始化,确保安全。 **构造函数与析构函数实现示例**: ```cpp ONNXInference::ONNXInference() : env_(nullptr), session_(nullptr), initialized_(false) { // 可以在这里创建环境?但通常建议在loadModel中创建,因为环境是共享的,可能由外部传入更好 } ONNXInference::~ONNXInference() { if (session_) { delete session_; // 注意:Ort::Session的销毁方式可能不同,这里仅示意 } // 注意:环境(env_)通常不在此销毁,因为可能被多个对象共享 } ``` 以上解析基于典型的ONNX推理实现。实际项目中,类设计可能有所不同,但核心元素(环境、会话、输入输出处理)是相似的。 §§相关问题§§ 1. 在ONNX Runtime中,如何管理多个模型共享同一个环境(Env)? 2. ONNXInference类中,如何处理动态输入形状? 3. 在多线程环境下使用ONNXInference类需要注意什么?</think>### C++ ONNX推理类结构解析 以下基于ONNX Runtime库的典型实现,解析ONNX推理类的结构设计,涵盖继承关系、虚函数、成员变量及其作用: #### 1. **继承关系** ```cpp class ONNXInference : public InferenceEngine { private: Ort::Env* env_; // ONNX运行时环境 Ort::Session* session_; // 推理会话 // ... }; ``` - **设计目的**:通常继承自通用推理接口基类(如`InferenceEngine`),实现多态调用 - **引用依据**:虚函数核心目的是通过基类访问派生类定义的函数[^1] #### 2. **虚函数实现** ```cpp class InferenceEngine { public: virtual bool loadModel(const std::string& path) = 0; // 纯虚函数 virtual void infer(const Tensor& input, Tensor& output) = 0; virtual ~InferenceEngine() {} // 虚析构函数确保正确释放资源 }; class ONNXInference : public InferenceEngine { public: bool loadModel(const std::string& path) override { // ONNX模型加载实现 session_ = Ort::Session(*env_, path.c_str(), Ort::SessionOptions{}); } void infer(const Tensor& input, Tensor& output) override { // ONNX推理执行 session_->Run(Ort::RunOptions{}, ...); } }; ``` - **关键点**: - 虚函数表(vtable)存储函数地址,实现运行时多态[^2] - 虚析构函数保证派生类资源正确释放 - `override`关键字显式声明重写基类虚函数 #### 3. **核心成员变量及作用** | 成员变量 | 类型 | 作用描述 | |---------------------------|-----------------------|--------------------------------------------------------------------------| | `env_` | `Ort::Env*` | ONNX运行时环境句柄,管理全局状态(线程池/日志等) | | `session_` | `Ort::Session*` | 加载的模型会话,存储计算图和权重参数 | | `input_names_` | `std::vector<const char*>` | 模型输入节点名称列表,用于绑定输入数据 | | `output_names_` | `std::vector<const char*>` | 模型输出节点名称列表,用于获取推理结果 | | `input_dims_` | `std::vector<int64_t>` | 输入张量维度信息(如{1,3,224,224}) | | `is_initialized_` | `bool` | 标志位,指示模型是否成功加载并初始化完成 | #### 4. **内存与初始化机制 - **内存对齐**:虚函数表指针(vptr)使类大小增加4/8字节(32/64位系统)[^2] - **初始化顺序**: 1. 基类构造函数 2. 成员变量按声明顺序构造(`env_`先于`session_`)[^3] 3. 派生类构造函数 ```cpp ONNXInference::ONNXInference() : env_(new Ort::Env(ORT_LOGGING_LEVEL_WARNING, "ONNXInference")), // 成员初始化列表 session_(nullptr), is_initialized_(false) { // 显式初始化基本类型 } ``` #### 5. **完整类结构示例 ```cpp class ONNXInference : public InferenceEngine { public: ONNXInference(); virtual ~ONNXInference(); bool loadModel(const std::string& path) override; void infer(const Tensor& input, Tensor& output) override; private: // ONNX运行时资源 Ort::Env* env_; Ort::Session* session_; // 模型元数据 std::vector<const char*> input_names_; std::vector<const char*> output_names_; std::vector<int64_t> input_dims_; // 状态标志 bool is_initialized_; // 辅助方法 void parseModelMetadata(); // 解析模型输入/输出信息 }; ``` ### 设计要点总结 1. **多态接口**:通过虚函数实现统一的模型加载/推理接口 2. **资源管理**: - `env_` 生命周期通常与应用程序一致 - `session_` 在模型加载时创建,析构时释放 3. **线程安全**: - 每个线程需独立`session_`实例(因ONNX Session非线程安全) 4. **元数据缓存**:存储`input_names_/output_names_`避免运行时重复查询 > 典型应用场景:部署预训练模型(如ResNet/YOLO)到C++生产环境,通过接口标准化实现模型热切换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值