[C++] 虚函数、纯虚函数和虚析构(virtual)

2023-12-16 19:36

本文主要是介绍[C++] 虚函数、纯虚函数和虚析构(virtual),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  • 📢博客主页:https://blog.csdn.net/weixin_43197380
  • 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
  • 📢本文由 Loewen丶原创,首发于 CSDN,转载注明出处🙉
  • 📢现在的付出,都会是一种沉淀,只为让你成为更好的人✨

文章预览:

      • 一. 虚函数(virtual)
      • 二. 虚函数中的关键字
      • 三. 纯虚函数
      • 四*. 基类的析构函数务必写成虚函数(虚析构函数)
      • 五. 总结


一. 虚函数(virtual)

定义:在某基类中的成员函数:

  • 成员函数声明基类中为 virtual开头;
  • 该成员函数在一个或多个子类(派生类)中被重新声明、定义;

格式virtual 函数返回类型 函数名 ( 参数表 ) { 函数体 }

目的通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数,实现多态性

  • 例如 Human *phumen = new Men(); //可通过基类Human的指针phumen调用子类中的同名函数,实现多态

多态性

  • 顾名思义就是“多个性态”。更具体一点的就是,用一个名字定义多个函数,这些函数执行不同但相似的工作。最简单的多态性的实现方式就是函数重载模板,这两种属于静态多态性。还有一种是动态多态性,其实现方式就是我们今天要说的虚函数

下面来看一段简单的代码:

class Human
{
public:void print() { cout << "This is 人类" << endl; }
};class Men :public Human 
{
public:void print() { cout << "This is 男人" << endl; }
};int main() 
{   Human human;Men men;human.print();men.print();
}

通过class Humanclass Menprint()这个接口,输出的结果也是我们预料中的,分别是This is 人类This is 男人

但这是否真正做到了多态性呢?

  • No多态还有个关键之处就是一切用指向派生类的基类指针或引用来操作对象。那现在就把main()处的代码改一改。
int main() 
{   //Human human;//Men men;//human.print();//men.print();Human * phuman = new Human;Human * phuman1 = new Men;phuman->print();phuman1->print();
}

在这里插入图片描述

可以看出,父类指针phuman1明明指向的是子类class Men对象但却是调用的父类class Humanprint()函数,这不是我们所期望的结果。

那么解决这个问题,即通过一个父类指针或对象调用所有子类中的成员函数或变量,就需要用到虚函数:

class Human
{
public:virtual void print() { cout << "This is 人类" << endl; }  //现在成了虚函数了
};class Men :public Human
{
public:virtual void print() { cout << "This is 男人" << endl; } //这里需要在前面加上关键字virtual吗?
};

现在重新运行main的代码,这样输出的结果就是This is 人类This is 男人

在这里插入图片描述

毫无疑问,class A的成员函数print()已经成了虚函数,那么class Bprint()成了虚函数了吗?

回答是Yes,我们只需在把基类的成员函数声明前加virtual,其派生类的相应的同名同参成员函数也会自动变为虚函数。所以,class Bprint()也成了虚函数。对于在派生类的相应函数前是否需要用virtual关键字修饰, 看个人编程习惯。

总结:指向基类的指针在操作它的多态类对象时,会根据不同的派生类对象,调用其相应的函数,这个函数就是虚函数。‎


二. 虚函数中的关键字

override关键字

为了避免在子类中写错虚函数(没有和基类的成员函数同名同参),在C++11中,可以在子类虚函数声明后增加一个关键字 override

注意,override关键字用在子类中,而且是虚函数专用,用了这个关键字后,编译器会认为子类的虚函数覆盖了基类中的同名函数,那么编译器就会在父类中找同名同参的虚函数,如果没找到,编译器就会报错。这样,如果不小心在子类中把虚函数名称或参数写错了,编译器会帮助纠错。

final关键字

final关键字也是虚函数专用,但是是用在父类中的,作用是在父类的函数声明中加了final,那么任何尝试覆盖该函数的操作都将引发错误。


三. 纯虚函数

定义: 纯虚函数是在①基类中声明的虚函数,但它在基类中②没有定义,但③要求任何派生类都要定义自己的实现方法

格式: 在基类中实现纯虚函数的方法是在函数原型后加“=0” 

virtual void funtion1() = 0; //纯虚函数,在基类中定义,没有函数体,只有一个函数声明

抽象类由来:一旦基类中有纯虚函数,那么则不能生成这个类的对象,这个了就成为了“抽象类”。

抽象类目的:用来统一管理子类对象。

Human  human;                //不合法
Human *phuman = new Human;   //不合法

在这里插入图片描述

核心两点总结:

  • 含有纯虚函数的类叫抽象类,抽象类不能生成该类对象,主要用于当做基类来生成子类用的
  • 子类中必须要实现该基类中定义的纯虚函数;

问题:我们知道纯虚函数在基类中没有定义,那么虚函数在基类中一定要定义实现吗?

class Location
{
public:Location(){}~Location(){}public:virtual bool Check();  // 这里一定要实现吗?
};class LineLocation : public Location
{
public:LineLocation(){}~LineLocation(){}public:virtual bool Check() {return 1;}
};int _tmain(int argc, _TCHAR* argv[])
{Location* loc = NULL;loc = new LineLocation();bool b= loc->Check();return 0;
}

回答: 虚函数在基类中一定要实现,如果基类中的虚函数不想实现,只想通过派生类来实现,需要将基类中的虚函数换成纯虚函数(=0)。因为虚函数的地址在链接的时候需要放到类的虚函数表中,所以即使你的代码里面没有调用这个函数,编译器也需要取它的地址,已经有对它的引用了,就必须要实现才行。

注:因为纯虚函数就相当于接口,无法实例化,即Location loc;编译是不能通过的。即有纯虚函数的类,将其作为参数也好,另一个类的成员变量也好,只能将其定义为指针或引用,只要不给基类实例化对象就行。


四*. 基类的析构函数务必写成虚函数(虚析构函数)

基类中的虚拟成员希望其派生类定义自己的版本。特别是基类通常应该定义一个虚拟析构函数,即使它不起作用,析构函数必须是虚拟的,以允许动态分配和销毁继承层次结构中的对象。

那么为什么析构函数必须是虚拟的,而我们新建程序时,默认的析构函数却不是虚拟的呢?

1、为什么析构函数必须是虚拟的?

在这里插入图片描述
因为指针指向的是一个派生类实例,我们销毁这个实例时,肯定是希望先清理派生类自己的资源,同时又清理从基类继承过来的资源。而当基类的析构函数为非虚函数时,删除一个基类指针指向的派生类实例时只清理了派生类从基类继承过来的资源而派生类自己独有的资源却没有被清理

总结:如果一个类想要做基类(被其他类继承),那么我们必须定义这个类的析构函数并且还要将其写成虚函数(普通类可不定义析构函数为虚函数或直接不写析构函数)。这样,在delete释放指向的派生类实例的基类指针时,清理工作才能全面进行,才不会发生内存泄漏。

2、为什么默认的析构函数不是虚函数?

虚函数不同于普通成员函数,当类中有虚成员函数时,类会自动进行一些额外工作。这些额外的工作包括生成虚函数表虚表指针,虚表指针指向虚函数表。每个类都有自己的虚函数表,虚函数表的作用就是保存本类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是各个虚函数的地址。
这样一来,就会占用额外的内存,当们定义的类不被其他类继承时,这种内存开销无疑是浪费的。

这样一说,问题就不言而喻了。当我们创建一个类时,系统默认我们不会将该类作为基类,所以就将默认的析构函数定义成非虚函数,这样就不会占用额外的内存空间。同时,系统也相信程序开发者在定义一个基类时,会显示地将基类的析构函数定义成虚函数,此时该类才会维护虚函数表和虚表指针。

参考博文:为什么析构函数必须是虚函数?为什么默认的析构函数不是虚函数?


五. 总结

1、定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
2、虚函数必须实现,如果不实现,编译器将报错。
3、调用虚函数执行的是“动态绑定”。动态:表示的就是在我们程序运行的时候才能知道调用了哪个子类的虚函数。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。


下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。

这篇关于[C++] 虚函数、纯虚函数和虚析构(virtual)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/501640

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Python Counter 函数使用案例

《PythonCounter函数使用案例》Counter是collections模块中的一个类,专门用于对可迭代对象中的元素进行计数,接下来通过本文给大家介绍PythonCounter函数使用案例... 目录一、Counter函数概述二、基本使用案例(一)列表元素计数(二)字符串字符计数(三)元组计数三、C

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

MySQL中REPLACE函数与语句举例详解

《MySQL中REPLACE函数与语句举例详解》在MySQL中REPLACE函数是一个用于处理字符串的强大工具,它的主要功能是替换字符串中的某些子字符串,:本文主要介绍MySQL中REPLACE函... 目录一、REPLACE()函数语法:参数说明:功能说明:示例:二、REPLACE INTO语句语法:参数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

python中update()函数的用法和一些例子

《python中update()函数的用法和一些例子》update()方法是字典对象的方法,用于将一个字典中的键值对更新到另一个字典中,:本文主要介绍python中update()函数的用法和一些... 目录前言用法注意事项示例示例 1: 使用另一个字典来更新示例 2: 使用可迭代对象来更新示例 3: 使用

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c