简介
本条款中主要介绍了编译器自动生成的 default 构造函数,copy 构造函数,copy assignment 操作符以及析构函数。并对 copy 构造函数,copy assignment 操作符 的生成原因进行了一定分析。
引子
想象一下我们写了如下空类:
1 | class Empty{}; |
看似这个类里没有任何成员变量和方法,但是通过编译器编译之后,会自动地为我们的类添加一些函数,添加了函数之后的类和以下类等效:
1 | class Empty { |
但是要注意到这些函数的定义并不是无条件生成的,只有当程序有出现过该类函数被调用的时候才会生成相应的函数体定义(参考 cpprefernce:Default constructors),如下所示:
1 | class Empty {}; |
下面我们来讨论一下这四类函数的具体情况,详细介绍 copy 构造函数和 copy assignment 操作符。
default 构造函数和析构函数
default 构造函数和析构函数的作用主要是:调用该类 base class 和 non-static 成员变量的构造函数和析构函数。这里注意两点:
- 由于 default 构造函数只会执行 non-static 成员变量的构造函数,所以对基本类型的成员变量并不会自发地进行初始化,如同 条款04 所说,如果我们的类有基本类型成员变量,我们应该手动创建构造函数通过初始化列表对其进行初始化;
- 编译器自动生成的析构函数是 non-virtual 的,除非该类的 base class 自身声明有 virtual 析构函数。
copy 构造函数和 copy assignment 操作符
考虑以下模板类:
1 | template<typename T> |
由于该类已经声明了构造函数,所以编译器不会再自动生成 default 构造函数。因此在这种情况下,我们声明了一个要求传入参数的构造函数之后就无须担心编译器会自动生成一个不需要传入参数的构造函数。而由于类中没有声明 copy 构造函数和 copy assignment 操作符,所以编译器会为它创建这两个函数。
copy 构造函数
copy 构造函数的用法如下:
1 | NamedObject<int> no1("Smallest Prime Number", 2); // 调用用户定义的构造函数 |
编译器生成的 copy 构造函数以传入类中的每一个成员变量,对自身所有成员变量进行构造或者拷贝。对于上例而言,由编译器生成的 copy 构造函数使用 no1.nameValue
和 no1.objectValue
为初值来设定 no2.nameValue
和 no2.objectValue
。设定的方法分为以下两类:
nameValue
:本身类型std::string
,不是基本类型。其本身有 copy 构造函数,因此设定方法为以no1.nameValue
为实参对no2.nameValue
进行 copy 构造;objectValue
:类型为int
,是基本类型,因此不具备构造函数。设定方法为拷贝no1.objectValue
的每一个 bit 并复制到no2.objectValue
中。
copy assignment 操作符
由编译器生成的 copy assignment 操作符在一般情况下和 copy 构造函数的行为并无区别。但是能够自动生成 operator=
的条件是上述操作必须合法。因此,在某些情况下,编译器会拒绝为 class 生成 operator=
,参考以下例子(具体代码见 item01),在下面这个类中,我们对其进行一部分修改,nameValue
现在是一个 std::string
的 reference,而 objectValue
则是 class T
的 const:
1 | template<class T> |
接下来我们思考一下以下代码例子的结果:
1 | std::string newDog("A"); |
假设编译器同样为 NamedObject
生成 operator=
并且行为和上述一致的话会出现什么结果呢?
- 对于
nameValue
:会将其p.nameValue
修改 reference 到s.nameValue
吗?这样违反了 C++ 的标准,因为 C++ 规定 reference 只能引用至同一对象不能改变 - 对于
objectValue
:类似的道理,由于其类型是const int
也是不允许被修改的,因此如果直接赋值(或拷贝)的操作的也是不合法的。
在这两种情况下,编译器会拒绝自动生成 operator=
,编译会直接不通过并报以下错误:
1 | main.cpp: In function ‘int main()’: |
报错原因和上述分析基本一致,不能直接对 non-static reference 和 non-static const 进行赋值操作。如果我们想要在这种情况进行类之间的赋值操作的话,我们必须自己定义 operator=
并解决上述问题。
此外,还有一种情况也会导致编译器拒绝生成 operator=
,参考以下例子(具体代码见 item02):
1 | class Base { |
以上例子中,基类将 operator=
声明为 private
方法,当我们运行以下代码时:
1 | Derived a("a", 1); |
同样会在编译器报以下错误:
1 | main.cpp: In function ‘int main()’: |
其原因是当涉及到到继承时,子类中自动生成的 operator=
会需要调用父类的 operator=
来进行父类中成员变量的赋值。但是如果我们采用 public
继承的方式,当子类无法调用父类的 operator=
时编译器会拒绝生成并将其标记为 deleted
。
结论
本条款中主要介绍了编译器自动生成的 default 构造函数,copy 构造函数,copy assignment 操作符以及析构函数。并对 copy 构造函数,copy assignment 操作符 的生成原因进行了一定分析。
完整可运行代码地址:Effective-Cpp-Reading-Note