说说C++的几个默认函数

前言

这两天继续进阶C++的另一个模块:C++的几个默认函数。 我们知道,在构造一个类时,如果我们啥都不写,编译器会帮我们默认合成几个函数(准确点说,只有在某些情况下会帮我们合成,后文会说)。按照C11的标准,这些默认函数包括:
(1)、构造函数
(2)、移动构造函数
(3)、移动赋值操作符
(4)、复制构造函数
(5)、赋值操作符
(6)、析构函数

这里,我想谈谈其中的几个(1,4,5,6), 简单分析这些函数内部都做了啥。 我把它称为四大函数。 如下图所示。
在这里插入图片描述

一、关于默认构造函数和拷贝构造函数


这两个函数放在一起,因为它们都是构造函数。
 

1.1 构造函数的由来

什么是构造函数呢? 为什么发明C++的前辈们要好端端的给类搞一个构造函数这个东东呢?

按照我的理解,按照类模板生成一个对象主要有两大步骤,第一是找到一片能容下这个对象的内存空间; 第二是在这片内存空间中初始化,也就是在使用这篇空间之前填充一些默认值。 而构造函数的作用最主要的就是填充这篇内存空间,学术名叫做初始化对象。

在这里插入图片描述

 
有没有从C来的小伙伴起来较真,说我不初始化也可以,生成之后,我手动赋值不行吗? 我set、 set 、 set 不香吗?

不得不说,ok,可以。但是不好。

你牛逼,你都知道每次生成一个对象一定会初始化相关其内存空间。但是我们普通的大众,可能就会不小心忘了,而正是这个“小心” 可能会带来巨大的问题。 知道为啥提倡定义变量后尽量给其赋个初值,说的是同样的理。

为了处理这个问题,C++的先辈们发明了这个机制,在生成一个对象的时候,默认会调用这个构造函数,你可以在这个构造函数中初始化具体得数据成员。这样不用每次生成对应都手动set、set、set了。 当然一个好的习惯是这样,如果你非要犟,非不在构造函数里初始化数据成员,那以后因为此出现大bug的时候,可不要怪C++的前辈们。(* ^ ▽ ^ *)

这里顺便说点题外话,在我现在理解,语言的发展一方面进行抽象,屏蔽掉该屏蔽的东西(如底层实现细节),让程序员关注该关注的东西,方便程序员,以此来提高软件开发的效率。 另一个方面,它会从语言的层面提供一些机制,帮助程序员尽量减少 bug出现的频率(如上面对象的初始化),以这种方式来提供软件开发的效率。

 

1.2 何时编译器会帮我们合成这些默认的构造函数?

下面来谈谈什么时候编译器会帮我们合成这些默认的构造函数,是否如果我们没有自定义默认构造函数,编译器就会默认自动为我们产生一个默认的呢?

  好像有点疑惑,有的书上说,如果用户没有自定义的默认构造函数,编译器就会帮我们合成一个?;而有的地方,如《深度探索C++对象模型》中又说,如果不是必要的,编译器不会帮我们生成默认的构造函数(即使我们用户自己没生成的情况下); 这是不是冲突了呢?
 
我目前是这样理解的,如果我们用户自己定义了默认构造函数,ok,那没问题,编译器会无条件相信用户,不会再“自作主张”替用户主动生成一个构造函数。

如果用户没有在类中自定义构造函数,那么编译器只会在其认为需要的时候,合成默认的构造函数(此时合成的默认构造函数也叫做non-trivial构造函数),其他的情况下,编译器不会帮用户合成默认的构造函数,或者说也会合成,但是合成的是trivial 构造函数,这里面什么都不做,是空的。(某些资料上把这种类叫做平凡类【2】,或者说符合bitwise的类) (所以说上面的说法都正确,关键看你怎么理解)

 
ok,那这又引出来一个问题,上面说编译器认为其“必要的”时候,是什么时候呢? 主要有以下四种

  1. 如果一个类里面某个成员对象有nontrivial default constructor,编译器就会为我们的类产生nontrivial default constructor。
  2. 如果一个派生类的基类有nontrivial default constructor,那么编译器会为派生类合成一个nontrivial default constructor。
  3. 如何一个类里面隐式的含有任何virtual function table(或vtbl)、pointer member(或vptr)。
  4. 如果一个类虚继承于其它类。

以上也叫做不符合bitwise的情况。
关于上面4中情况的解释,具体可以参照【4】,这里就不赘述了。

 

1.3 构造函数做哪些事?

对于普通的程序员来说,构造函数的作用当然是初始化类内的成员。 对于那些隐藏在构造函数之后的,或者说编译器帮我们做的,我们也需要了解了解。一般来说,在构造函数中(无论是默认构造函数和用户自定义的构造函数) 编译器为了实现C++的多种特性,会在构造函数中“增添”一些代码,这些代码是屏蔽普通的程序员的。 作为了解,这里就不以具体的例子进行说明了,笼统的说明一波。

编译器可能会在构造函数中做哪些事呢?

(1)、调用类内对象的默认构造函数(如果类内的对象有默认的构造函数的话)
(2)、调用继承的父类的默认构造函数(如果类内的父类有默认的构造函数的话)
(3)、设定虚表指针vptr(如果类中有虚函数的话)
(4)、调整this指针(如果base class 是多重继承下的第二或后继的base class的话)
(5)、…

note:注意上面所说的情况只是很可能,具体编译器是不是需要进行扩充,还需要根据具体的情况而定。

 

1.4 关于拷贝构造函数的一丢丢说明

上面说的情况大都是以默认的普通构造函数为例,实际上很多东西拷贝构造函数也差不多。重复的东西,这里就不多说了。 这里简要再说说关于拷贝构造函数的一点:

在这里插入图片描述

 

二、关于复制操作符

问你个问题,下面两部分有什么区别?
在这里插入图片描述
 
 
好的,我懂,我懂,你明白,你明白。
但还是稍微提一下。

前者是初始化,内部调用的是拷贝构造函数,其只能在生成对象的时候初始化一次; 后者是赋值,调用的是内部的赋值操作符(operator =),只要你喜欢,可以无限次玩耍。

但是两者还是有联系的,从某种程度上来说,它们都是填充一片内存区域,它们都会有深拷贝和浅拷贝的问题,它们也会有上面提到的编译器在内部悄悄做的一些事情,即如果类不符合bitwise copy的语义,编译器会自动完成的一些工作。

 

三、关于析构函数

  析构函数是和构造函数相反的一组操作,前者用于初始化对象环境,后者用于清除对象环境。 就像前文所说的,它们都是C++的设计者为了减少程序员出错的所提供的一种机制。

和构造函数类似,这里也想讨论两件事情:
(1)、何时编译器会帮我们自动生成一个默认的析构函数?
(2)、析构函数里一般做了啥?
 

3.1 何时编译器会帮我们自动生成一个默认的析构函数?

当然,如果用户自定义了析构函数,编译器就会无条件的信任我们,不会在为我们合成析构函数。(但是,可能会扩充析构函数)。

如果在类的内部没有定义析构函数, 和前面构造函数略有不同,只有在类内涵的对象(或者类的基类)拥有析构函数的情况下,编译器才会自动合成一个来【5】。 否则的话,即使类的内部含有虚函数或者是存在虚继承的情况下,也不会合成默认的析构函数(或者也可以说合成了一个空的析构函数)。

按照【5】中说法,析构往往说明了对象的内存空间需要被回收了,因为在这种情况下不需要重新把类内的成员重置为某个值,也就不用大动干戈的调整vptr或者this指针了。(当然这都是在类内涵的对象(或者类的基类)都没有拥有析构函数的情况下)

按照我的理解,析构函数的最大作用就是清除环境。(这里的清除环境更多的是一些文件的关闭、连接的关闭等等,而不是类对象内存的重置。) 所以在这种情况下,一般需要用户自定义析构函数,编译器在这部分的作用就是负责调用用户自定义的析构函数(包括基类或者类内对象的析构函数)。
 

3.2 析构函数的内容

析构函数一般有如下内容:

(1)、析构函数的本题说先被执行
(2)、如果类拥有成员对象,而成员对象含有析构函数,那么它们会以其声明的顺序的相反顺序被调用
(3)、如果类内涵一个vptr, 那就会重新设定其值,指向适当的base class 的虚表
(4)、如果有任何直接的(上一层)nonvitual base classes 拥有析构函数,它们会以其声明的顺序的相反顺序被调用
(5)、如果有任何virtual base classes 拥有析构函数,而目前讨论的这个class是最尾端(most-derived)的class, 那么它们会以其原来的构造顺序相反的顺序被调用。

 

总结

enen… 该说的都在上面说了,这里也没啥好说的了。 ^ _ ^ ’

参考

【1】、C++默认构造函数——深入理解
【2】、C++对象模型(五)构造与析构
【3】、c++的 trivial constructor
【4】、关于默认构造函数的几个错误认识(四种情况下,编译器会生成默认构造函数)
【5】、《深入理解C++对象模型》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值