重载、构造函数、析构函数、虚函数
Last edited time
Sep 17, 2024 10:58 AM
AI summary
文档讨论了C++中的构造函数、析构函数和虚函数的概念,包括构造函数的作用、虚函数的多态机制及其实现方式、虚函数表的结构、继承时构造和析构函数的执行顺序,以及虚函数的安全性和内存管理问题。强调了析构函数必须为虚函数以避免内存泄漏,并指出构造函数不能是虚函数。
Last edited by
Tags
c++重载、重写、隐藏的区别
- 重载:是指同⼀作⽤域内被声明的⼏个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调⽤哪个函数,重载不关⼼函数返回类型(统⼀为void,否则报错)。。。重载就是⼀种多态,编译时多态。
- 重写(覆盖):是指派生类(⼦类)中存在重新定义的函数。其函数名,参数列表,返回值类型等所有声明都必须同基类中被重写的函数⼀致。只有函数体不同(花括号内),派⽣类调用时会调用派⽣类的重写函数,不会调用基类中的被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。所以有继承时基类的析构函数⼀定要设置成虚函数,防⽌内存泄漏。
- 隐藏:是指派⽣类(⼦类)的函数屏蔽了与其同名的基类(⽗类)函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重载的参数类型不同,函数体不同;隐藏只要函数名相同,参数和函数体等可以不同;重写仅仅函数体不同。
重载、构造函数、析构函数
- 重载为什么改变参数类型就可以实现调⽤不同的函数?
因为C++在编译的时候会对函数进⾏重命名,保证函数名的唯⼀性,⽽重载函数的参数不同,就会被命名为不同的函数名。
- 构造函数可以被重载吗?析构函数可以被重载吗?
构造函数可以被重载,因为构造函数可以有多个且可以带参数。析构函数不可以被重载,因为析构函数只能有⼀个,且不能带参数。
- 构造函数的作⽤?
给创建的对象简历⼀个标识符为对象数据成员开辟内存空间完成对象数据成员的初始化
- 构造函数有返回值吗?
没有,构造函数和析构函数都没有返回值。无法返回。
c++多态
- 什么是多态机制?
多态就是说同⼀个名字的函数可以有多种不同的功能。分为编译时的多态和运行时的多态。编译时的多态就是函数重载,包括运算符重载,编译时根据实参确定调用哪个函数。运⾏时的多态则和虚函数、继承有关。
- 多态底层是如何实现的?
- 编译时多态(重载):因为C++在编译的时候会对函数进行重命名,保证函数名的唯⼀性,⽽重载函数的参数不同,就会被命名为不同的函数名。
- 运⾏时多态:利⽤虚函数表,先构建⼀个基类,然后在基类的构造函数中会建⽴虚函数表,也就是⼀个存储虚函数地址的数组, 内存地址的前四个字节保存指向虚函数表的指针,然后当多个⼦类继承⽗类之后,主函数中可以通过⽗类指针调⽤⼦类的继承函数。 虚函数表属于类,也属于它的⼦类等各种派⽣类。虚函数表由编译器在编译时⽣成,保存在.rdata 只读数据段。
- ⼦类的多态函数是怎么被调⽤的?
因为每个⼦类都继承并设置了⾃⼰的虚函数表,每次⽤⽤⽗类指针创建新⼦类时就会出现,从⽽最终调⽤⾃⼰的表。
- 怎么知道多态时,指向那个虚函数?
定义的⽗类指针 new 出哪个⼦类就是指向哪个⼦类的虚函数。
虚函数/虚函数表
参考:
- 虚函数基础知识
C++中,⼀个类存在虚函数,那么编译器就会为这个类⽣成⼀个虚函数表,在虚函数表⾥存放的是这个类所有虚函数的地址。当⽣成类的对象的时候,编译器会⾃动的将类对象的前四个字节设置为虚函数表的地址,这四个字节就可以看作是⼀个指向虚函数表的指针。虚函数表可以看做⼀个指针数组,该数组中每个元素都是虚函数的地址。
- 虚函数表总结
- ⼀个类⾥⾯只要定义了虚函数编译时就会产⽣⼀个虚函数表(⼀个数组),表中存的都是虚函数的地址,⽽且类经过实例化之后,虚函数表占的是该对象的前 4 个字节(32bit系统)地址,表⽰的是⼀个虚函数的头指针。
- 如果⼀个类A有两个实例化对象a1, a2,那么a1, a2的地址是不相同的,但是a1, a2的虚函数表地址是相同的,也就是说同⼀个类的不同实例共⽤同⼀份虚函数表,即
- 单继承且本⾝不存在虚函数的继承类的内存布局
- 存在基类虚函数覆盖的单继承类的内存布局
- 定义了基类没有的虚函数的单继承的类对象布局
- 多继承且存在虚函数覆盖同时⼜存在⾃⾝定义的虚函数的类对象布局
- 多继承且第⼀个基类没有虚函数时⼜存在⾃⾝定义的虚函数的类对象布局
- 多继承且基类都没有虚函数
不考虑继承时
考虑继承时
可以看出 d1 这个对象直接把基类的虚函数表给继承了。
上图可以看出这种情况下,多态出现了。也即是说⽆论是通过 ⼦类 Derive1 的指针还是 基类Base1 的指针来调⽤此⽅法,调⽤的都将是被继承类重写后的那个⽅法(函数)。也即是说⼦类对象与指向⼦类的基类指针(Base p = derive1)指向的对象,使⽤同⼀个虚函数表。当然,这并不会改变基类的虚函数表,如果单独实例化基类对象,并调⽤基类的成员函数,最后执⾏的还是基类函数。
这种情况下,继承类Derive1的虚函数表被加在基类的后⾯,对于基类来说,他的虚函数表还是只有他⾃⼰的虚函数,并不会将⼦类的虚函数也算到他的虚函数表中
Derive1 d1 的虚函数表依然是保存到第 1 个拥有虚函数表的那个基类的后⾯的。
此时,第⼆个有虚函数的基类将会占据内存的前⾯,也就是说谁有虚函数表, 谁就放在前⾯。
总结:
- 对象怎么找到对应的虚函数表?
对象内存的前 4 个字节(32bit)存的是虚函数表的⾸地址,可以根据虚函数表的地址偏移找到虚函数的地址
- 虚函数表的结构是怎样的?
虚函数表是⼀个函数指针数组,数组⾥存放的都是函数指针,指向虚函数所在的位置。对象调⽤虚函数时,会根据虚指针找到虚表的位置,再根据虚函数声明的顺序找到虚函数在数组的哪个位置,找到虚函数的地址,从⽽调⽤虚函数。
- A,B两个类,类中有虚函数。C继承AB,有⼏张虚函数表?
2张,多继承就会有多个虚函数表。因为每个⽗类的虚函数是不同的,指针也是不同的。如果共⽤⼀张虚函数表,就分不清到底⼦类的实例化是针对哪⼀个基函数的。
- ⽗类构造函数中是否可以调⽤虚函数?注意是说可不可以调⽤,并不是说构造函数能不能设置成虚函数
*可以。不过调⽤会屏蔽多态机制,最终会把基类中的该虚函数作为普通函数调⽤,⽽不会调⽤派⽣类中的被重写的函数。**构造函数调⽤ 层次会导致⼀个有趣的两难选择。试想:如果我们在构造函数中并且调⽤了虚函数,那么会发⽣什么现象呢?在普通的成员函数中,我们可 以想象所发⽣的情况——虚函数的调⽤是在运⾏时决定的。这是因为编译时这个对象并不能知道它是属于这个成员函数所在的那个类,还是 属于由它派⽣出来的某个类。于是,我们也许会认为在构造函数中也会发⽣同样的事情。然⽽,对于在构造函数中调⽤⼀个虚函数的情况, 被调⽤的只是这个函数的本地版本。也就是说,虚机制在构造函数中不⼯作。
因为构造函数的⼯作是⽣成⼀个对象,并且为该对象初始化,如果构造函数也和虚函数⼀样的性质,那么很有可能我们所调⽤的函数会操作 某些还没有被初始化的成员,这将导致问题。
- 继承时构造函数是如何执⾏的?析构时析构函数是如何执⾏的?
继承时构造函数调⽤是按照从基类到最晚派⽣类的顺序的执⾏的,⽽不是直接执⾏最晚派⽣类。析构函数是从最晚派⽣类开始逐渐析构到基 类的,和构造顺序相反。
- 构造函数可以是虚函数吗?
*不可以,因为虚函数存在的唯⼀⽬的就是为了多态。⽽⼦类并不继承⽗类的构造函数,构造函数是创建对象时⾃⼰主动调⽤的,不可能被 继承,所以没有使⽗类构造函数变成虚函数的必要。**另外,虚函数表需要⽗类执⾏构造函数后才能⽣成,如果⽗类的构造函数设置成了虚 函数,那么由于多态特性⼦类在继承时⽗类的时候⽗类的构造函数不执⾏也就没办法出现后序的虚函数。
- 静态函数可以是虚函数么?为什么?
- static 成员不属于任何类对象或类实例,所以即使给此函数加上 virutal 也是没有任何意义的。
- 静态与⾮静态成员函数之间有⼀个主要的区别。那就是静态成员函数没有 this 指针。所以⽆法访问 vptr . 进⽽不能访问虚函数表。所以静态函数不能是虚函数
- 虚函数的安全性有什么问题?
可以通过虚函数表,让⽗类指针(就是指指向⼦类地址的⽗类指针)访问⼦类的特有虚函数。这带来⼀定的安全问题。另外,即使⽗类的虚 函数是私有函数或者保护函数,仍然可以通过虚函数表访问,带来⼀定的安全问题。
- 析构函数可以是虚函数吗?
在有继承的情况下,析构函数必须是虚函数。
当析构函数是⾮虚函数时,主函数通过指针访问⾮虚函数时,编译器会根据指针的类型来确定要调⽤的函数(也就是静态绑定);⽽指针是父类指针(就是指指向⼦类地址的父类指针),所以调⽤父类的析构函数。析构函数必须是虚函数。因为如果不是虚函数,当在主函数中⽤父类的指针new出⼀个⼦类对象,最后析构的时候,只会调⽤⽗类析构函数⽽不会调⽤⼦类析构函数。⽽且如果不为虚函数,父类指针就不会调⽤⼦类成员函数。父类析构函数成为虚函数时,⼦类的析构函数会⾃动也变为虚函数。这个时候编译器会忽略指针的类型,⽽根据指针的指向来选择函数;也就是说,指针指向哪个类的对象就调⽤哪个类的函数。pb、pd 都指向了派⽣类的对象,所以会调⽤派⽣类的析构函数,继⽽再调⽤基类的析构函数。
- 析构函数可以是纯虚函数么?
析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调⽤是在⼦类中隐含的。
- 如果析构函数不是虚函数,⼀定会出现内存泄露吗?
析构函数是虚函数的主要原因是,如果不是虚函数,每次结束时因为是⽗类指针所以只会调⽤⽗类的析构函数,⽽不会调⽤⼦类的析构函数,如果⼦类中有指针开辟空间,⼦类没有调⽤析构函数释放这个空间,就会导致内存泄露。但是如果⼦类中没有⽤指针开辟空间,都是普通的变量,应该就不会出现内存泄漏。
- 定义⼀个
A*pa= new A[5]; delete pa;
类A的构造函数和析构函数分别执⾏了⼏次?
构造函数执⾏了5次,每new⼀个对象都会调⽤⼀个构造函数,析构函数只调⽤⼀次,如果调⽤delete[] pa 析构函数才会调⽤5次。
- 虚函数可以是内联的吗?
内联函数不能为虚函数,原因在于虚表机制需要⼀个真正的函数地址,⽽内联函数展开以后,就不是⼀个函数,⽽是⼀段简单的代码(多数 C++对象模型使⽤虚表实现多态,对此标准提供⽀持),可能有些内联函数会⽆法内联展开,⽽编译成为函数。
- 类⾥⾯不能同时存在函数名和参数都⼀样的虚函数和静态函数。
⽗类的析构函数是⾮虚的,但是⼦类的析构函数是虚的,delete⼦类对象指针会调⽤⽗类的析构函数。
Loading...