牛客C++面经
一. C++部分
1.说一说指针和引用的区别
指针内部保存的是指向的对象的地址信息,而引用是变量的别名;
指针可以随时初始化,而引用被定义之后就要初始化,不可以为空
sizeof引用是被引用的对象的大小,而sizeof指针是指针类型的大小
指针既有顶层const(指针本身不能变),也有底层const(指针指向的对象不能变),而引用只有底层const,顶层const是无意义的。
二者的++
操作不同,指针++
表示指针运算,而引用++
表示变量执行++
。
引用只是C++的语法糖,可以看作是自动取地址解引用的常量指针。在汇编层面是一样的。
2.struct和class的区别
默认的(继承)访问权限,class是private,struct默认是public
class可以实现模板类,而struct不可以
struct是值类型,而class是引用类型
3.C++中多态
为了使代码重用性增加,使得代码可以模块化
静态多态:通过重载函数和泛型编程实现,通过基类指针指向的对象不同而调用不同的函数。是在编译器就完成的,编译器根据实参类型来确定。具有更好的类型安全性,因为编译阶段会对所有的绑定类型进行检查。
动态多态:通过虚函数和继承实现,运行时确定。虚函数表保存在只读数据段,编译器将类对象的前四个字节设置为指向虚函数表的指针。
4.面向对象
面向对象的三大特征:封装、继承、多态。封装:将客观事物进行抽象,将其属性和方法合成为一个类,类封装了成员变量和成员函数,同时又实现对属性和方法的权限控制,降低与外界的耦合度 继承:子类继承父类的各种属性和方法,同时子类还可以在父类的基础上重新定义和扩展父类的属性和方法,使其具有不同的功能,继承提高了代码的复用性及可维护性 多态:同一调用语句在父类和子类间使用时具有不同的表现形式,可以使用同一段代码处理不行类型的对象,提高代码的复用性
5.浅拷贝和深拷贝
浅拷贝 (Shallow Copy) 只复制某个对象的指针, 而不复制对象本身, 新旧对象还是共享同一块内存。在对象析构后,容易产生指针访问一个不存在的对象,从而产生悬空指针。
深拷贝 (Deep Copy) 在拷贝的过程中会另外创造一个一模一样的对象. 新对象跟原对象不共享内存, 修改新对象不会改到原对象.
6.STL容器的实现和查找时间复杂度
- vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N) 2. deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N) 3. list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1) 4. map、set、multimap、multiset 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN) 5. unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N) 注意:容器的时间复杂度取决于其底层实现方式。
7.STL 中容器的类型,每种分别有哪些容器
- 序列式容器 array、vector、deque、list、forward_list 2. 关联式容器 map、multimap、set、multiset 3. 无序关联式容器 unordered_map、unordered_multimap、unordered_set、unordered_multiset 4. 容器适配器 stack、queue、priority_queue
8.简述一下 C++ 的重载和重写
- 重载
a. 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同(参数列表不同)。调用的时候根据函数的参数来区别不同的函数,函数重载跟返回值无关。
b. 重载的规则 - 函数名相同 - 必须具有不同的参数列表 - 可以有不同的访问修饰符
c. 重载用来实现静态多态(函数名相同,功能不一样)。
d. 重载是多个函数或者同一个类中方法之间的关系,是平行关系。 - 重写
a. 重写(也叫覆盖)是指在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样。
b. 重写的规则: - 方法声明必须完全与父类中被重写的方法相同 - 访问修饰符的权限要大于或者等于父类中被重写的方法的访问修饰符 - 子类重写的方法可以加virtual,也可以不加
c. 重写用来实现动态多态(根据调用方法的对象的类型来执行不同的函数)。
d. 重写是父类和子类之间的关系,是垂直关系。
隐藏的实质是:在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。
9.简述一下虚函数的实现原理
https://zhuanlan.zhihu.com/p/75172640
- C++ 中的虚函数的作用主要是实现了动态多态的机制。 2. 虚函数实现原理 编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表(virtual function table,vtbl)。虚函数表中存储的是类中的虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址,如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。使用虚函数时,对于内存和执行速度方面会有一定的成本: 1. 每个对象都会变大,变大的量为存储虚函数表指针; 2. 对于每个类,编译器都会创建一个虚函数表; 3. 对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址。
10.为什么将析构函数设置成虚函数
虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄露。如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时就会造成内存泄露。
11.哈希冲突的原因和影响因素,哈希冲突的解决方法
- 哈希冲突产生的原因 哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值,这时候就产生了哈希冲突。
- 产生哈希冲突的影响因素 装填因子(装填因子=数据总数 / 哈希表长)、哈希函数、处理冲突的方法
- 哈希冲突的解决方法
a.开放地址方法:如果冲突,则根据冲突的值再建立一个值,如果还冲突就重复操作直到不再冲突
b.链式地址法:将所有冲突的都放入一个链表中
c.建立公共溢出区:基本表和溢出表
d.再哈希法:同时构建多个哈希函数,一个发生冲突就使用另一个
12.map,unordered_map 的区别
map:内部实现了一个红黑树,该结构具有自动排序的功能,因此 map 内部的所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素,因此,对于 map 进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了 map 的效率。
unordered_map:内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的。
13.红黑树的特性,为什么要有红黑树
在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
1、具有二叉查找树的特点;
2、根节点是黑色的;
3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据;
4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。
14.vector 的扩容机制,扩容以后,它的内存地址会变化吗?
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
vector底层使用三个指针来实现,分别是:顺序表头,顺序表的有效长度位置,顺序表末尾。
https://blog.csdn.net/m0_51723227/article/details/121374795#:~:text=%E5%9C%A8%E6%A0%87%E5%87%86C%2B%2B%E4%B8%AD%2C,%2C%E8%BE%BE%E5%88%B0%E5%AD%98%E5%82%A8%E6%95%88%E6%9E%9C.
15.请你说说 map 实现原理,各操作的时间复杂度是多少
- map 实现原理 map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。 2. 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
16.shared_ptr 怎么知道跟它共享对象的指针释放了
shared_ptr基于”引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。
17.请你说说左值、右值、左值引用、右值引用、右值引用的使用场景
- 左值 在 C++ 中可以取地址的、有名字的就是左值 int a = 10; // 其中 a 就是左值
- 右值 不能取地址的、没有名字的就是右值 int a = 10; // 其中 10 就是右值右值
- 左值引用 左值引用就是对一个左值进行引用。传统的 C++ 引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址: int n; int * pt = new int; const int b = 101; int & rn = n; int & rt = *pt; const int & rb = b; const int & rb = 10;
- 右值引用 右值引用就是对一个右值进行引用。C++ 11 新增了右值引用(rvalue reference),这种引用可指向右值(即可出现在赋值表达式右边的值),但不能对其应用地址运算符。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x + y 等表达式以及返回值的函数(条件是该函数返回的不是引用),右值引用使用 && 声明: int x = 10; int y = 23; int && r1 = 13; int && r2 = x + y; double && r3 = std::sqrt(2.0);
- 右值引用的使用场景 右值引用可以实现移动语义、完美转发。
17.weak_ptr 如何解决 shared_ptr 的循环引用问题?
weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数,依此特性可以解决 shared_ptr 的循环引用问题。 weak_ptr 没有解引用 * 和获取指针 -> 运算符,它只能通过 lock 成员函数去获取对应的 shared_ptr 智能指针对象,从而获取对应的地址和内容。 不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。
18.请你说说虚函数可以是内联函数吗
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联。virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
19.请你说说迭代器失效原因,有哪些情况
STL 中某些容器调用了某些成员方法后会导致迭代器失效。例如 vector 容器,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。
- 序列式容器迭代器失效 对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。解决:erase 返回下一个有效的迭代器。
- 关联式容器迭代器失效 对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。
20.请你说说 auto 和 decltype 如何使用
auto 实现自动类型推断,要求进行显示初始化,让编译器能够将变量的类型设置为初始值的类型
decltype 将变量的类型声明为表达式指定的类型。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值。
21.说说 C++ 中智能指针和指针的区别是什么?
智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。指针是一种数据类型,用于保存内存地址,而智能指针是类模板。
22.简述一下 C++ 中的四种类型转换
1.static_cast 静态转换 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换 - 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的 - 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的 用于基本数据类型之间的转换,如把 int 转换成 char,把 char 转换成 int。这种转换的安全性也要开发人员来保证
2.dynamic_cast 动态转换 dynamic_cast 主要用于类层次间的上行转换和下行转换 在类层次间进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的 在进行下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全
3. const_cast 常量转换 该运算符用来修改类型的const属性 常量指针被转化成非常量指针,并且仍然指向原来的对象 常量引用被转换成非常量引用,并且仍然指向原来的对象 注意:不能直接对非指针和非引用的变量使用 const_cast 操作符
4. reinterpret_cast 重新解释转换 这是最不安全的一种转换机制,最有可能出问题 主要用于将一种数据类型从一种类型转换为另一种类型,它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针
new 和 delete 是如何实现的
1.new的实现过程是:首先调用名为operator new 的标准库函数,分配足够大的内存来保存指定类型的一个对象,接下来运行该类型的构造函数,用于初始化该对象,最后返回该对象的指针。
2.delete的实现过程:对指针指向的对象运行适当的析构函数,然后调用operator delete的标准库函数释放该对象的内存
指针占多少字节
一个指针占内存的大小跟编译环境有关,而与机器的位数无关。32位编译环境下为4字节,64位编译环境下为8字节
顶层const和底层const
1.顶层const可以表示任意的对象是常量,这一点对任何数据类型都使用,底层const则与指针和引用的复合类型有关
2.当执行对象的拷贝操作时,顶层const不受什么影响,但是底层const的限制不能被忽略。
3.对于重载,顶层const不构成重载,底层const构成重载
数据库三大范式
1.列不可再分
2.属性完全依赖于主键(一张表中包含了多种不同的属性,那么必须要分成多张表)
3.属性不依赖于其他非主属性(要求已经分好了多张表的话,一张表中只能有另一张表的ID,而不能有其他任何信息)
类成员继承权限问题
1.若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
2.若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;
3.若继承方式是protected,基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。
禁止隐式转换
C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。
数组和指针的区别
1.数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);
2.用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。
3.在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;
写一个比较大小的模板
template<typename type1, typename type2>
type1 max(type1 a, type2 b){
return a > b ? a : b;
}
void main(){
cout << max(5.5, 'a') << endl;
}
其实该模板有个比较隐晦的bug,那就是a、b只有在能进行转型的时候才能进行比较,否则 a > b 这一步是会报错的。这个时候往往需要对于 > 号进行重载,这代码量瞬间上来了。
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数,比如epoll中可以通过回调函数找到对应的红黑树节点。
delete和delete[]的区别
delete只会调用一次析构函数。
delete[]会调用数组中每个元素的析构函数
类的对象存储空间
1.非静态成员的数据类型大小之和。
2.编译器加入的额外成员变量(如指向虚函数表的指针)。
3.为了边缘对齐优化加入的padding。
4.空类(无非静态数据成员)的对象的size为1,这样可以保证每个实例均有独占的内存地址,当作为基类时, size为0,如果带有虚函数,则大小比1大,因为还需要包含一个虚函数表指针
5.成员函数不占用对象的内存。这是因为所有的函数都是存放在代码区的,不管是全局函数,还是成员函数。
在成员函数中调用delete this会出现什么问题?对象还可以使用吗?
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题
如果在类的析构函数中调用delete this,会发生什么?
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
auto和decltype的区别
lambda函数
1.利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;
2.每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。
3.lambda语法如下:
[capture] (parameters) mutable ->return-type {statement};
4.lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;
STL中hashtable的实现
1.STL中的hashtable使用的是开链法解决hash冲突问题
2.hashtable中的bucket所维护的list既不是list也不是slist,而是其自己定义的由hashtable_node数据结构组成的linked-list,而bucket聚合体本身使用vector进行存储。hashtable的迭代器只提供前进操作,不提供后退操作
vector如何释放空间
1.由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。
2.如果使用vector,可以用swap()来帮助你释放多余内存或者清空全部内存。
容器内删除一个元素
1.顺序容器(序列式容器,比如vector、deque)
erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;
it = c.erase(it);
2.关联容器(关联式容器,比如map、set、multimap、multiset等)
erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;
c.erase(it++)
STL每种容器对应的迭代器
1.vector和deque:随机访问迭代器
2.stack、queue、priority_queue:无
3.list、(multi)set/map:双向迭代器
unordered_(multi)set/map、forward_list:前向迭代器
sizeof和strlen的区别
1、sizeof会将空字符\0计算在内,而strlen不会将空字符\0计算在内;
2、sizeof会计算到字符串最后一个空字符\0并结束,而strlen如果遇到第一个空字符\0的话就会停止并计算遇到的第一个空字符\0前面的长度。
int main(void)
{
char str[100] = "abcde";
printf("sizeof(str) = %lu\n", sizeof(str)); //字节大小为100
char str1[] = "abcde";
printf("sizeof(str1) = %lu\n", sizeof(str1)); //字节大小为6
char str2[] = "\0abcde";
printf("sizeof(str2) = %lu\n", sizeof(str2)); //字节大小为7
char str3[] = "\0ab\0c de";
printf("sizeof(str3) = %lu\n", sizeof(str3)); //字节大小为9
char str4[] = "abcde";
printf("strlen(str4) = %lu\n", strlen(str4)); //字符串长度为5
char str5[100] = "abcde";
printf("strlen(str5) = %lu\n", strlen(str5)); //字符串长度为5
char str6[] = "\0abcde";
printf("strlen(str6) = %lu\n", strlen(str6)); //字符串长度为0
char str7[] = "ab cde";
printf("strlen(str7) = %lu\n", strlen(str7)); //字符串长度为6
return 0;
}
哪些函数不能是虚函数?把你知道的都说一说
1.构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造
2.内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
3.静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
4.友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
5.普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
什么时候必须要自定义析构函数
如果本类中一个成员变量是别的对象的指针,而且这个指针不是传进来的地址而是这个指针指向的对象,是在本类中(如果是栈里的定位分配,也不用考虑内存)在堆中开辟的空间创建的。并且该指针没有进行过delete操作,那么久需要在析构方法中进行delete操作,此时我们就必须自己写析构函数。
析构函数的析构过程
析构函数首先执行函数体,然后销毁成员,成员按照初始化顺序的逆序销毁。注意析构函数本身不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的,销毁类类型成员执行它自己的析构函数,销毁内置类型不需要做什么。
何时会调用析构函数
- 变量在离开其作用域时
- 当一个对象被销毁时,其成员被销毁
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
- 对于动态分配的对象,当指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
什么时候需要自己重写拷贝构造函数
根据三/五法则,如果需要定义一个非空的析构函数,那么通常情况下也需要自定义一个拷贝构造函数。即包含动态分配成员或者包含指针成员的类都应该提供拷贝构造函数,并且考虑重载赋值运算符。