C++ 多态、虚析构、模板类、常函数、虚继承、虚函数和纯虚函数相关知识和问题总结
1. C++ 中的多态
多态(Polymorphism)是面向对象编程中的一个重要特性,它允许使用相同的接口来表示不同的类型。由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。
多态分为静态多态和动态多态:
1. 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。
静态多态(编译时多态):
- 静态多态通过函数重载、运算符重载和模板实现。它在编译期间就决定了调用哪个函数。
函数重载示例:
#include <iostream>
using namespace std;
class Print {
public:
void display(int i) {
cout << "Integer: " << i << endl;
}
void display(double f) {
cout << "Float: " << f << endl;
}
};
int main() {
Print obj;
obj.display(10); // 调用int版本
obj.display(3.14); // 调用double版本
return 0;
}
在这里,display
函数被重载,不同的参数类型决定了在编译期哪个函数被调用。
2.动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
2. 通过基类类型的指针或引用来调用虚函数。
动态多态(运行时多态):
- 动态多态是通过虚函数实现的,在运行时决定调用哪个函数。
- 使用基类指针或引用指向派生类对象,可以根据实际的对象类型调用相应的派生类的函数。
动态多态示例:
#include <iostream> using namespace std;class Base { public:virtual void show() { // 虚函数cout << "Base class show" << endl;}virtual ~Base() {} // 虚析构函数,防止内存泄漏 };class Derived : public Base { public:void show() override {cout << "Derived class show" << endl;} };int main() {Base *bptr = new Derived(); // 基类指针指向派生类对象bptr->show(); // 动态绑定,调用 Derived 类的 show()delete bptr; // 防止内存泄漏return 0; }
- 基类指针(
Base* bptr
)实际指向了Derived
对象,运行时根据对象的实际类型,调用了Derived
类的show()
方法。
总结:
- 静态多态:通过函数重载和模板实现,在编译时决定调用哪个函数。
- 动态多态:通过虚函数实现,依赖于运行时的动态绑定。
2. 为什么要虚析构,为什么不能虚构造?
虚析构的必要性:
- 当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,将不会调用派生类的析构函数,可能会导致派生类的资源没有被释放。
非虚析构的示例:
#include <iostream> using namespace std;class Base { public:~Base() {cout << "Base destructor" << endl;} };class Derived : public Base { public:~Derived() {cout << "Derived destructor" << endl;} };int main() {Base *bptr = new Derived();delete bptr; // 只会调用Base的析构函数return 0; }
- 输出只会显示
Base destructor
,Derived
类的析构函数不会被调用,从而导致资源泄漏。
虚析构的解决方案:
#include <iostream> using namespace std;class Base { public:virtual ~Base() { // 虚析构函数cout << "Base destructor" << endl;} };class Derived : public Base { public:~Derived() {cout << "Derived destructor" << endl;} };int main() {Base *bptr = new Derived();delete bptr; // 调用Derived的析构函数return 0; }
- 输出会显示:
Derived destructor Base destructor表明派生类的析构函数得到了正确调用,避免了资源泄漏。
为什么不能虚构造?
- 构造函数用于初始化对象,而在构造期间,虚函数表还没有建立或准备好。构造函数依赖编译时的类型,不可能在构造时进行动态绑定。
- 而且,构造函数负责创建对象的基类部分和派生类部分,如果构造函数是虚的,会产生语义上的混淆。
- 虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生类无法被析构。 1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构 2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。 C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。
- 不能虚构造: 1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable中调用,可是对象还没有实例化,没有内存空间分配,如何调用。 2. 从实现上看,vtable在构造函数调用后才建立,因而构造函数不可能成为虚函数。
3. 模板类是在什么时候实现的?
模板类的定义和实现通常在头文件中一起写出,它们在编译时实例化,即编译器在遇到模板被使用时,才根据实际的模板参数生成相应的代码。这也是为什么模板函数的定义通常需要与声明放在同一个文件里。
模板类的示例:
#include <iostream> using namespace std;template<typename T> class Box { private:T value; public:Box(T val) : value(val) {}void display() {cout << "Value: " << value << endl;} };int main() {Box<int> intBox(10); // 模板实例化为int类型Box<double> doubleBox(5.5); // 模板实例化为double类型intBox.display(); // 输出:Value: 10doubleBox.display(); // 输出:Value: 5.5return 0; }
- 在编译时,根据
Box<int>
和Box<double>
的使用情况,编译器生成具体的Box<int>
和Box<double>
代码。
总结:
- 模板在编译时进行实例化。
- 模板代码通常写在头文件中,防止出现链接错误。
4. 构造函数为什么不能被声明为虚函数?
构造函数的主要任务是创建并初始化对象,而在构造函数中虚函数表(vtable)尚未建立。
- 构造函数依赖编译时类型,即构造对象的类型在编译时就已经确定,不需要依赖虚函数机制。
- 另一方面,虚函数机制依赖于虚表(vtable),而虚表是在构造函数执行完毕后才建立的。如果构造函数是虚的,虚表还没有准备好,因此无法进行动态绑定。
5. 什么是常函数,有什么作用?
常函数(const
成员函数)是在函数声明时,在函数名之后添加const
关键字,表明该函数不会修改对象的任何成员变量。
常函数的示例:
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
int getValue() const { // 常函数
return value;
}
void setValue(int v) {
value = v; // 非常函数,可以修改成员变量
}
};
作用:
- 常函数的主要作用是保证对象的状态不会在函数中被改变,尤其是在使用
const
对象时。 - 如果一个对象是常量(
const
),则只能调用常函数,无法调用非常函数。
int main() {const MyClass obj(10);// obj.setValue(20); // 错误:不能调用非常函数cout << obj.getValue() << endl; // 正确:常函数可以被const对象调用return 0; }
6. 什么是虚继承,解决什么问题,如何实现?
虚继承的定义:
- 虚继承用于解决菱形继承问题。当多个派生类通过不同路径继承同一个基类时,会导致基类的成员在最终派生类中存在多份拷贝,造成数据冗余或歧义。虚继承可以确保最终派生类中只有一份基类的拷贝。
菱形继承问题的示例:
#include <iostream> using namespace std;class Base { public:int value; };class Derived1 : public Base { // 非虚继承 };class Derived2 : public Base { // 非虚继承 };class FinalDerived : public Derived1, public Derived2 { public:void setValue(int v) {// value = v; // 错误:编译器无法确定要修改 Derived1 还是 Derived2 的 value} };
在这个例子中,FinalDerived
类中有两个Base
类的拷贝:一个来自Derived1
,另一个来自Derived2
。因此,value
存在二义性。
虚继承解决菱形继承问题:
#include <iostream> using namespace std;class Base { public:int value; };class Derived1 : virtual public Base { // 虚继承 };class Derived2 : virtual public Base { // 虚继承 };class FinalDerived : public Derived1, public Derived2 { public:void setValue(int v) {value = v; // 不会有二义性,因为Base类只有一份拷贝} };
- 通过虚继承,
FinalDerived
类中只有一份Base
类的拷贝,避免了菱形继承问题。
7. 虚函数和纯虚函数,以及实现原理
虚函数:
- 虚函数是使用
virtual
关键字声明的函数,允许派生类覆盖基类中的实现。虚函数支持运行时多态,通过动态绑定调用派生类的函数。
纯虚函数:
- 纯虚函数是一种特殊的虚函数,它在基类中没有定义,必须在派生类中实现。
- 语法:将函数声明中的
= 0
表示为纯虚函数。
虚函数与纯虚函数示例:
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { // 虚函数
cout << "Base class show" << endl;
}
virtual void print() = 0; // 纯虚函数
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class show" << endl;
}
void print() override {
cout << "Derived class print" << endl;
}
};
int main() {
Base *ptr = new Derived();
ptr->show(); // 调用Derived类的show
ptr->print(); // 调用Derived类的print
delete ptr;
return 0;
}
Base
类中的print()
函数是纯虚函数,派生类Derived
必须提供实现。
实现原理:
虚函数通过**虚函数表(vtable)**实现。每个包含虚函数的类都会维护一个虚函数表,表中存储了指向实际函数的指针。在运行时,编译器会根据对象的实际类型查找虚表,调用相应的函数。
8. 纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
纯虚函数的类不能实例化:
- 包含纯虚函数的类称为抽象类。由于纯虚函数没有实现,所以抽象类不能被实例化,否则在调用纯虚函数时会无从执行。
- 示例:
class AbstractClass {
public:
virtual void func() = 0; // 纯虚函数
};// AbstractClass obj; // 错误:抽象类不能实例化
派生类必须实现纯虚函数:
- 如果派生类不实现基类中的纯虚函数,派生类也会变成抽象类,无法实例化。因此,派生类必须实现所有继承的纯虚函数。
9.虚函数和纯虚函数区别。
1. 虚函数(Virtual Function)
定义:
- 虚函数是基类中使用
virtual
关键字声明的成员函数。它允许派生类提供自己的实现,支持运行时多态。
特点:
- 具有函数体:虚函数在基类中有完整的实现,派生类可以选择是否覆盖它。
- 可选的覆盖:派生类可以重写虚函数,也可以不重写。如果派生类不提供自己的实现,调用该函数时会使用基类的版本。
- 动态绑定:虚函数通过虚表(vtable)实现动态绑定,运行时根据实际对象的类型选择调用合适的函数版本。
示例:
#include <iostream> using namespace std;class Base { public:virtual void show() { // 虚函数cout << "Base class show" << endl;} };class Derived : public Base { public:void show() override { // 重写虚函数cout << "Derived class show" << endl;} };int main() {Base *b = new Derived();b->show(); // 调用Derived类的showdelete b;return 0; }
- 这里
Base
类中定义了虚函数show()
,Derived
类重写了该函数。在运行时,通过基类指针调用的show()
实际调用了Derived
类的实现。
2. 纯虚函数(Pure Virtual Function)
定义:
- 纯虚函数是没有实现的虚函数,它在基类中声明,但不提供函数体,需要在派生类中实现。纯虚函数的声明使用
= 0
表示。
特点:
- 没有函数体:纯虚函数在基类中没有定义,仅作为接口存在。
- 抽象类:包含纯虚函数的类是抽象类,不能被实例化。如果一个类包含一个或多个纯虚函数,它就无法创建对象,除非派生类实现所有的纯虚函数。
- 必须被重写:所有派生类必须实现基类中的纯虚函数,否则派生类也将成为抽象类,无法实例化。
示例:
#include <iostream> using namespace std;class Base { public:virtual void show() = 0; // 纯虚函数 };class Derived : public Base { public:void show() override { // 实现纯虚函数cout << "Derived class show" << endl;} };int main() {Base *b = new Derived();b->show(); // 调用Derived类的showdelete b;return 0; }
- 在此例中,
Base
类定义了一个纯虚函数show()
,因此它是一个抽象类,不能实例化。但Derived
类实现了该纯虚函数,因此可以创建Derived
类的对象,并通过基类指针调用重写的函数。
3. 虚函数与纯虚函数的区别
比较维度 | 虚函数(Virtual Function) | 纯虚函数(Pure Virtual Function) |
---|---|---|
函数体 | 在基类中有实现,通常派生类可以选择重写或者使用基类实现。 | 在基类中没有函数体,必须在派生类中实现。 |
类的类型 | 包含虚函数的类不是抽象类,可以实例化。 | 包含纯虚函数的类是抽象类,不能实例化。 |
派生类实现 | 派生类可以重写虚函数,但不是必须的。 | 派生类必须实现纯虚函数,除非它本身也是抽象类。 |
多态性 | 支持运行时多态,基类指针或引用可以调用派生类的重写函数。 | 也是运行时多态的一部分,依赖派生类的实现来调用函数。 |
使用目的 | 提供一个可以被覆盖的默认实现,但允许派生类根据需要进行重写。 | 定义一个接口,强制派生类提供自己的实现。 |
实现原理 | 通过虚函数表(vtable)实现动态绑定。 | 也是通过虚函数表(vtable)实现动态绑定,但必须有实现函数。 |
4. 使用场景和设计目的
-
虚函数:当你希望在基类中提供函数的默认行为,但允许派生类根据需要选择重写时,可以使用虚函数。这种设计允许部分多态行为,而不强制所有派生类都必须实现该函数。
-
纯虚函数:当你希望基类只定义接口,并且要求所有派生类都必须提供自己的实现时,使用纯虚函数。这种设计常见于抽象基类中,它们定义了派生类的共同行为接口,但不提供具体实现。
5. 总结
- 虚函数是可以在基类中提供实现的函数,派生类可以选择重写它们,也可以直接使用基类的实现。
- 纯虚函数定义了一个必须由派生类实现的接口,它没有实现,派生类必须提供它的定义,否则它们也将是抽象类,无法实例化。
相关文章:

C++ 多态、虚析构、模板类、常函数、虚继承、虚函数和纯虚函数相关知识和问题总结
1. C 中的多态 多态(Polymorphism)是面向对象编程中的一个重要特性,它允许使用相同的接口来表示不同的类型。由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态…...

计算机组成原理一句话
文章目录 计算机系统概述存储系统指令系统 计算机系统概述 指令和数据以同等地位存储在存储器中,形式上没有差别,但计算机应能区分他们。通过指令周期的不同阶段。 完整的计算机系统包括,1)软件系统:程序、文档和数据&…...

【Linux】僵尸进程和孤儿进程
一、僵尸进程 何为僵尸进程? 在 Unix/Linux 系统中,正常情况下,子进程是通过父进程创建的,且两者的运行是相互独立的,父进程永远无法预测子进程到底什么时候结束。当一个进程调用 exit 命令结束自己的生命时ÿ…...

Patchcore运行过程
论文github地址:https://github.com/amazon-science/patchcore-inspection 平台:autodl云服务器 1.将下载的代码上传到autodl-tmp/PatchCore里面解压,将数据集上传path_to_mvtec_folder/mvtec里,目录结构如图 2.安装依赖 cd au…...

一小时快速入门Android GPU Inspector
本文介绍如何使用 Android GPU Inspector (AGI) 对Android 应用进行系统性能分析和帧性能分析 。面向熟悉Android图形的开发者。 待分析应用需要的前置条件 (1) 将应用设置为可调试状态 <application [...] android:debuggable"true">(2)…...

二叉树展开为链表
二叉树展开为链表 给你二叉树的根结点 root ,请你将它展开为一个单链表: 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同…...

基于SpringBoot+Vue+uniapp微信小程序的教学质量评价系统的详细设计和实现
项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念,提供了一套默认的配置,让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…...

【二刷hot100】day 4
终于有时间刷刷力扣,求实习中。。。。 目录 1.最大子数组和 2.合并区间 3.轮转数组 4.除自身以外数组的乘积 1.最大子数组和 class Solution {public int maxSubArray(int[] nums) {//就是说可以转换为计算左边的最大值,加上中间的值,…...

10.22学习
1.求余 在C语言中,求余操作是通过取模运算符 % 来实现的。取模运算符会返回两个数相除后的余数。对于正数和负数的除法,求余的结果会有所不同,但 % 运算符总是返回被除数的符号。 下面是一个简单的例子,展示如何使用 % 运…...

【不要离开你的舒适圈】:猛兽才希望你落单,亲人总让你回家,4个维度全面构建舒适圈矩阵
单打独斗的英雄时代已经落幕 抱团取暖才是社会寒冬的良策 自然界中,每个物种都占据着自己的领地和生存空间。 生态位的差异决定了它们的生存方式,一旦离开领地,失去群体的庇护,就会沦为野兽的美餐。 人类社会同样存在隐形圈层…...

OpenIPC开源FPV之Channel配置
OpenIPC开源FPV之Channel配置 1. 源由2. 现象3. 硬件3.1 模拟频点3.2 数字频点2.4GHz频段频点表格 (802.11b/g/n):5GHz频段频点表格 (802.11a/n/ac): 4. 分析5. 实验6. 参考资料 1. 源由 无线信号,传输过程中不可避免都会受到干扰。同时,由于在一个开放…...

UG NX12.0建模入门笔记:1.0 UG NX12.0安装教程
一、如何关闭防火墙? 提示:安装软件之前,建议先 关闭防火墙和杀毒软件!!! 文章目录 一、如何关闭防火墙?二、UG NX12.0安装包三、UG NX12.0安装教程1.新建文件夹2.安装JAVA环境3.安装许可证管理…...

【C++】踏上C++学习之旅(三):“我“ 与 “引用“ 的浪漫邂逅
文章目录 前言1. "引用"的概念1.1 "引用"的语法 2. "引用"的特性3. "引用"的使用场景3.1 "引用"做参数3. 2 "引用"做返回值3.2.1 "引用"做返回值时需要注意的点 4. 常引用5. "引用"在底层的实…...

中间件之Seata
一、引言 在微服务架构日益盛行的今天,分布式事务成为了一个必须面对和解决的问题。传统的本地事务已经无法满足分布式环境下的数据一致性需求,因此分布式事务解决方案应运而生。Seata作为一款开源的分布式事务中间件,以其高性能、易用性和灵…...

MySQL 异常: “Host ‘xxx‘ is not allowed to connect to this MySQL server“
update user set host % where user root; FLUSH PRIVILEGES; 这两行代码就行...

c语言中字符串函数strlen,strcmp,strcpy,srtcat,strncpy,strncat,strncmp
1.strlen的使用和模拟实现 strlen 用来求字符串的长度,统计\0之前字符的个数。 模拟实现1:计数参数法 #include<stdio.h> #include<assert.h> size_t my_strlen(char* str) {int count0;assert(str);//assert断言是判断是字符串不能为空w…...

携程线下一面,面试内容:
面试时间:2024/9/12 • 实例方法和静态方法有什么不一样? • Java中的异常有哪几类?分别怎么使用? • 常用的集合类有哪些?比如List如何排序? • ArrayList和LinkedList内部的实现大致是怎样的?他们之间的区别和各自适应的场景是什么? • 内存溢出是怎么…...

DeepL翻译:全世界最准确的翻译
DeepL翻译是一款高质量的机器翻译工具,以下从产品描述、产品特色、适用人群、适用场景四个方面对其进行介绍: 体验地址:DeepL翻译:全世界最准确的翻译 产品描述 DeepL是一家德国公司,以其高质量的机器翻译服务而闻名…...

15分钟学Go 实战项目一:命令行工具
实战项目一:命令行工具 1. 引言 命令行工具是开发者常用的工具之一,它可以帮助用户通过命令行界面对程序进行控制和交互。在这节中,我们将创建一个简单的命令行工具,以帮助你理解Go语言的基本语法和如何处理命令行输入。在这个过…...

lesson02 作业
lesson02-01作业 小红的体重是 m 千克,她想知道自己的体重在磅(1 千克约等于 2.20462 磅)是多少 输入描述 输入一个整数表示小红的标准体重m(kg) 输出描述 输出一个整数表示转换后的磅值n 磅 示例 输入: 50 输出:…...

港大和字节提出长视频生成模型Loong,可生成具有一致外观、大运动动态和自然场景过渡的分钟级长视频。
HKU, ByteDance|⭐️ 港大和字节联合提出长视频生成模型Loong,该模型可以生成外观一致、运动动态大、场景过渡自然的分钟级长视频。选择以统一的顺序对文本标记和视频标记进行建模,并使用渐进式短到长训练方案和损失重新加权来克服长视频训练…...

RabbitMQ进阶_可靠性
文章目录 一、 发送者的可靠性1.1、 生产者重试机制1.2、 生产者确认机制1.2.1、确认机制理论1.2.2、确认机制实现1.2.2.1、定义ReturnCallback1.2.2.2、定义ConfirmCallback 二、 MQ的可靠性2.1、 数据持久化2.1.1、 交换机持久化2.1.2、 队列持久化2.1.3、 消息持久化 2.2、 …...

JavaScript字符串的常用方法有哪些?
1.1操作方法 归纳为增删查改 1.1.1增 这里不是直接增添内容,而是创建字符串的一个副本,再进行操作 处理用以及${}进行字符串拼接外,还可以通过concat 1.1.1.1concat 用于将一个或多个字符串拼接为一个新字符串(浅拷贝&#…...

jmeter发送post请求
在jmeter中,有两种常用的请求方式,get和post.它们两者的区别在于get请求的参数一般是放在路径中,可以使用用户自定义变量和函数助手等方式进行参数化,而post请求的参数不能随url发送,而是作为请求体提交给服务器。而在…...

图文深入理解Oracle Total Recall
List item 题记:本文图文深入理解Oracle Total Recall技术。 1. Oracle Total Recall 概述 Oracle Total Recall(也称为 Flashback Data Archive - 闪回数据归档)提供了一种用于跟踪数据库更改的机制,可自动跟踪数据库历史更改…...

腾讯云控制台URL刷新URL预热 使用接口刷新
如图所示的俩个控制台功能,调用腾讯云的接口执行这俩个动作 (代码可以优化)nodejs框架是express, 这里粘贴调用成功的代码示例,做个记录。 app.get(/PurgeUrlsCache, async function (req, res, next) {// Depends on tencentclo…...

构建后端为etcd的CoreDNS的容器集群(二)、下载最新的etcd容器镜像
在尝试获取etcd的容器的最新版本镜像时,使用latest作为tag取到的并非最新版本,本文尝试用实际最新版本的版本号进行pull,从而取到想的最新版etcd容器镜像。 一、用latest作为tag尝试下载最新etcd的镜像 1、下载镜像 [rootlocalhost opt]# …...

libaom-all-intra参数说明
part_sf.less_rectangular_check_level 1; 这个设置可能控制编码器在分割画面时使用非矩形分区的检查级别。part_sf.ml_prune_partition 1; 这个设置可能用于基于机器学习(ML)的分区修剪,以减少不必要的计算。part_sf.prune_ext_partition_…...

应用假死?
有个客户10月18日应用接口都访问慢,nginx层面error显示连接拒绝,当时实施同学重启了java应用运行正常,但今天又卡死了,后台登录也登录不上去,看日志没异常,最终找到了数据库层面。 查看数据库相关日志&…...

SAP MM+FI - 物料管理模块与财务会计模块的集成配置
01 采购费用过账配置表 为了方便项目实施过程中采购费用过账配置,迪森资深专家根据丰富经验总结得出采购费用过账配置表,以供大家参考: 02 材料采购订单入库及结算 2.1采购订单入库 假设:入库数量1000PC,价格 10 元…...