C++: 虚函数,一些可能被忽视的细节
C++: 虚函数,一些可能被忽视的细节
引言:关于C++虚函数,对某些细节的理解不深入,可能导致我们的程序无法按预期结果运行,或是表明我们对其基本原理理解不够透彻。本文详细解答以下几个问题:实现多态,忘记写virtual会怎么样?虚函数的默认参数可以重载吗?纯虚函数真的不能有实现吗?析构函数可以是纯虚函数吗?
1.1 虚函数是什么?
虚函数是在基类中使用关键字
virtual声明的函数,它在派生类中可以被重写,且在运行时根据对象的类型来调用相应的函数。虚函数的作用是实现多态。多态是指同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。多态分为编译时多态和运行时多态,编译时多态是指函数重载,运行时多态是指虚函数。
虚函数动态绑定的实现原理:每个含有虚函数的类都有一个虚函数表,虚函数表中存储着虚函数的地址,当基类指针绑定了类对象后,通过类对象虚表指针指向的虚函数表找到虚函数的地址,然后调用对应的虚函数。
1.2 实现多态,忘记写virtual会怎么样?
如果忘记在派生类中写virtual关键字,那么就不会实现多态,而是静态绑定。因此以下例子中,调用的是基类的函数,而不是派生类的函数。
class Base {
public:
void func() {
std::cout << "Base func" << std::endl;
}
};
class Derived : public Base {
public:
void func() {
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func
为了避免这种情况发生,C++11中引入了override关键字对需要重写的函数进行声明,这样如果派生类中没有重写基类的函数,编译器就会报错。
1.3 虚函数的默认参数可以重载吗?
虚函数的默认参数不可以重载,因为虚函数的调用是在运行时确定的,而默认参数是在编译时确定的。从设计角度来说,这样做是合理的,如果虚函数的默认参数可以重载,那么在运行时,编译器就需要在运行时选择合适的默认参数,这样就会增加编译器的复杂度。因此,虚函数的默认参数不可以重载。
class Base {
public:
virtual void func(int i = 0) = 0;
};
void Base::func(int i) {
std::cout << "Base func: " << i << endl;
}
class Derived : public Base {
public:
void func(int i = 2) {
std::cout<< "Derived func: " << i << endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func: 0
1.4 纯虚函数是什么?
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法,否则编译失败。
纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;,其中“= 0”是纯虚函数的标志,它告诉编译系统,该虚函数没有实现。含有纯虚函数的类是抽象类,抽象类是不能实例化的。
纯虚函数真的不能有实现吗?其实不然,纯虚函数是可以有自己的实现的,但是这个实现是在类外部实现的,而不是在类内部实现的。详见1.5中的代码实例。
1.5 析构函数可以是纯虚函数吗?
我们知道,析构函数是在对象销毁时调用的,而纯虚函数是没有实现的虚函数,含有纯虚函数的类是抽象类,那么,析构函数可以是纯虚函数吗?
程序验证如下:
class Base {
public:
virtual ~Base() = 0;
};
Base::~Base() {
std::cout << "Base destructor" << std::endl;
}
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base *b = new Derived();
delete b;
return 0;
}
输出:
Derived destructor
Base destructor
结论:析构函数可以是纯虚函数,含有纯虚析构函数的类无法实例化。因为析构函数是在派生类析构函数调用之后才调用基类析构函数,而派生类析构函数在派生类对象销毁时才会调用,而派生类对象的销毁必须要调用基类的析构函数,因此基类析构函数必须要在类外提供定义。
如果不定义纯虚析构函数的实现,则会链接失败,报以下错误。
:(.text$_ZN7DerivedD1Ev[__ZN7DerivedD1Ev]+0x3e): undefined reference to `Base::~Base()'
collect2.exe: error: ld returned 1 exit status
为什么在类外部实现就可以呢?因为含有纯虚函数的类是抽象类,抽象类是不能实例化的,但是抽象类可以有指针和引用,因此,我们可以通过抽象类的指针或引用调用纯虚函数,但是如果纯虚函数没有实现,那么就会出现问题,因此,我们需要在类外部实现纯虚函数。
1.6 纯虚函数可以被显示调用吗?
派生类的成员函数可以通过限定函数id自由调用基类在类外定义的纯虚函数。
class Base {
public:
virtual void func() = 0;
};
void Base::func() {
std::cout << "Base func" << std::endl;
}
class Derived : public Base {
public:
void func() {
Base::func();
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func
Derived func
参考
- https://en.cppreference.com/w/cpp/language/abstract_class
- Effective C++: 55 Specific Ways to Improve Your Programs and Designs.
C++: 虚函数,一些可能被忽视的细节的更多相关文章
- 构造函数为什么不能是虚函数 ( 转载自C/C++程序员之家)
从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的.问题出来了,如果构造函数是虚的,就需要通过 vtable来调用, ...
- 构造函数为什么不能为虚函数 & 基类的析构函数为什么要为虚函数
一.构造函数为什么不能为虚函数 1. 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的.问题出来了,假设构造函数 ...
- C++中纯虚函数
1.纯虚函数 virtual ReturnType Function()= 0; 纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义.凡是含有纯虚函数的类叫做抽象类 ...
- 2013级C++第14周(春)项目——多态性、虚函数和抽象类
课程首页在:http://blog.csdn.net/sxhelijian/article/details/11890759,内有完整教学方案及资源链接 第一部分 阅读程序1.阅读.改动和执行关于交通 ...
- C++虚函数和函数指针一起使用
C++虚函数和函数指针一起使用,写起来有点麻烦. 下面贴出一份示例代码,可作参考.(需要支持C++11编译) #include <stdio.h> #include <list> ...
- 匹夫细说C#:从园友留言到动手实现C#虚函数机制
前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...
- 【C++】多态性(函数重载与虚函数)
多态性就是同一符号或名字在不同情况下具有不同解释的现象.多态性有两种表现形式: 编译时多态性:同一对象收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定. ...
- 虚函数的使用 以及虚函数与重载的关系, 空虚函数的作用,纯虚函数->抽象类,基类虚析构函数使释放对象更彻底
为了访问公有派生类的特定成员,可以通过讲基类指针显示转换为派生类指针. 也可以将基类的非静态成员函数定义为虚函数(在函数前加上virtual) #include<iostream> usi ...
- C++ 系列:虚函数
Copyright © 1900-2016, NORYES, All Rights Reserved. http://www.cnblogs.com/noryes/ 欢迎转载,请保留此版权声明. -- ...
- EC笔记,第二部分:9.不在构造、析构函数中调用虚函数
9.不在构造.析构函数中调用虚函数 1.在构造函数和析构函数中调用虚函数会产生什么结果呢? #; } 上述程序会产生什么样的输出呢? 你一定会以为会输出: cls2 make cls2 delete ...
随机推荐
- 谈谈你对MVVM开发模式和MVT的理解?
MVVM分为Model.View.ViewModel三者. Model 代表数据模型,数据和业务逻辑都在Model层中定义: View 代表UI视图,负责数据的展示: ViewModel 负责监听 M ...
- TypeScript 学习笔记 — 类型补充void,any, tuple ,enum,nerver, Symbol , BigInt ,unknown(三)
目录 空值void 及(与Null 和 Undefined的区别) 任意值Any 元组类型 枚举类型 常量枚举 never 类型 1. 函数无法到达终点 2.通常校验逻辑的完整性,可以利用 never ...
- [oeasy]python024_vim读取文件_从头复制到尾_撤销_重做_reg_寄存器
Guido的简历 回忆上次内容 python 是Guido制作的语言 从Guido刚入职场 就开始的项目 python这个项目 一直跟随Guido Guido 曾经在 cwi cnri beope ...
- [oeasy]python0081_ANSI序列由来_终端机_VT100_DEC_VT选项_终端控制序列
更多颜色 回忆上次内容 上次 首先了解了RGB颜色设置 可以把一些抽象的色彩名字 落实到具体的 RGB颜色 计算机所做的一切 其实就是量化.编码 把生活的一切都进行数字化 标准 是ANSI制定的 这个 ...
- ScaleDet:AWS 基于标签相似性提出可扩展的多数据集目标检测器 | CVPR 2023
论文提出了一种可扩展的多数据集目标检测器(ScaleDet),可通过增加训练数据集来扩大其跨数据集的泛化能力.与现有的主要依靠手动重新标记或复杂的优化来统一跨数据集标签的多数据集学习器不同,论文引入简 ...
- NIO的三大核心组件详解,充分说明为什么NIO在网络IO中拥有高性能!
一.写在开头 我们在上一篇博文中提到了Java IO中常见得三大模型(BIO,NIO,AIO),其中NIO是我们在日常开发中使用比较多的一种IO模型,我们今天就一起来详细的学习一下. 在传统的IO中, ...
- UE5 打不开
在游戏开发中,出现了UE打不开的情况 初步推测,新增了接口Attacker, 而我们的DefaultPawn可能一下子实现了两个接口造成的 而这两个接口都在一个插件里,一个是c++实现的,一个是蓝图实 ...
- SEO自动外链工具的功效以及使用心得
SEO外链发布工具原理 1.自动SEO外链工具原理:就是把您的网址提交大站长工具类似的网站上面进行搜索,然后就会在上面留下痕迹自动生成以网址为标题的静态页面. 2.自动SEO外链发布效果:我们就是利用 ...
- 【微信小程序】 自定义组件
创建微信小程序组件 在小程序中创建组件: 1.项目根目录中创建[components]目录,存放自定义组件 2.进入components目录,给组件创建一个组件目录 3.右键组件目录,选择[创建Com ...
- 普通用户权限运行docker
docker安装后默认权限是管理员,在Ubuntu系统中需要使用sudo命令,但是很多时候docker的拉取操作都是写在脚步里面的,因此执行的时候十分的难搞,如果给脚本sudo权限后那么整个的环境路径 ...