0%

[Effective C++ 笔记]05.了解 C++ 静默编写和调用的函数

简介

本条款中主要介绍了编译器自动生成的 default 构造函数,copy 构造函数,copy assignment 操作符以及析构函数。并对 copy 构造函数,copy assignment 操作符 的生成原因进行了一定分析。

引子

想象一下我们写了如下空类:

1
class Empty{};

看似这个类里没有任何成员变量和方法,但是通过编译器编译之后,会自动地为我们的类添加一些函数,添加了函数之后的类和以下类等效:

1
2
3
4
5
6
7
8
9
10
11
class Empty {
public:
// default 构造函数
Empty() {}
// copy 构造函数
Empty(const Empty& rhs) {}
// 析构函数
~Empty() {}
// copy assignment 操作符
Empty& operator=(const Empty& rhs) {}
};

但是要注意到这些函数的定义并不是无条件生成的,只有当程序有出现过该类函数被调用的时候才会生成相应的函数体定义(参考 cpprefernce:Default constructors),如下所示:

1
2
3
4
5
class Empty {};

Empty e1; // default 构造函数被生成,同时析构函数也生成
Empty e2(e1) // copy 构造函数生成
e2 = e1 // copy assignment 操作符生成

下面我们来讨论一下这四类函数的具体情况,详细介绍 copy 构造函数和 copy assignment 操作符。

default 构造函数和析构函数

default 构造函数和析构函数的作用主要是:调用该类 base class 和 non-static 成员变量的构造函数和析构函数。这里注意两点:

  • 由于 default 构造函数只会执行 non-static 成员变量的构造函数,所以对基本类型的成员变量并不会自发地进行初始化,如同 条款04 所说,如果我们的类有基本类型成员变量,我们应该手动创建构造函数通过初始化列表对其进行初始化;
  • 编译器自动生成的析构函数是 non-virtual 的,除非该类的 base class 自身声明有 virtual 析构函数。

copy 构造函数和 copy assignment 操作符

考虑以下模板类:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(cconst std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};

由于该类已经声明了构造函数,所以编译器不会再自动生成 default 构造函数。因此在这种情况下,我们声明了一个要求传入参数的构造函数之后就无须担心编译器会自动生成一个不需要传入参数的构造函数。而由于类中没有声明 copy 构造函数和 copy assignment 操作符,所以编译器会为它创建这两个函数。

copy 构造函数

copy 构造函数的用法如下:

1
2
NamedObject<int> no1("Smallest Prime Number", 2);   // 调用用户定义的构造函数
NamedObject<int> no2(no1); // 调用 copy 构造函数

编译器生成的 copy 构造函数以传入类中的每一个成员变量,对自身所有成员变量进行构造或者拷贝。对于上例而言,由编译器生成的 copy 构造函数使用 no1.nameValueno1.objectValue 为初值来设定 no2.nameValueno2.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
2
3
4
5
6
7
8
9
template<class T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value): nameValue(name), objectValue(value) {}

private:
std::string& nameValue;
const T objectValue;
};

接下来我们思考一下以下代码例子的结果:

1
2
3
4
5
6
7
std::string newDog("A");
std::string oldDog("S");

NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);

p = s;

假设编译器同样为 NamedObject 生成 operator= 并且行为和上述一致的话会出现什么结果呢?

  • 对于 nameValue:会将其 p.nameValue 修改 reference 到 s.nameValue 吗?这样违反了 C++ 的标准,因为 C++ 规定 reference 只能引用至同一对象不能改变
  • 对于 objectValue:类似的道理,由于其类型是 const int 也是不允许被修改的,因此如果直接赋值(或拷贝)的操作的也是不合法的。

在这两种情况下,编译器会拒绝自动生成 operator=,编译会直接不通过并报以下错误:

1
2
3
4
5
6
7
8
9
main.cpp: In function ‘int main()’:
main.cpp:21:9: error: use of deleted function ‘NamedObject<int>& NamedObject<int>::operator=(const NamedObject<int>&)’
p = s;
^
main.cpp:4:7: note: ‘NamedObject<int>& NamedObject<int>::operator=(const NamedObject<int>&)’ is implicitly deleted because the default definition would be ill-formed:
class NamedObject {
^~~~~~~~~~~
main.cpp:4:7: error: non-static reference member ‘std::__cxx11::string& NamedObject<int>::nameValue’, can’t use default assignment operator
main.cpp:4:7: error: non-static const member ‘const int NamedObject<int>::objectValue’, can’t use default assignment operator

报错原因和上述分析基本一致,不能直接对 non-static reference 和 non-static const 进行赋值操作。如果我们想要在这种情况进行类之间的赋值操作的话,我们必须自己定义 operator= 并解决上述问题。

此外,还有一种情况也会导致编译器拒绝生成 operator=,参考以下例子(具体代码见 item02):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
Base(const std::string& name): nameValue(name) {}
private:
Base& operator=(const Base& rhs) { nameValue = rhs.nameValue; }
std::string nameValue;
};

class Derived: public Base {
public:
Derived(const std::string& name, int value): Base(name), objectValue(value) {}

private:
int objectValue;
};

以上例子中,基类将 operator= 声明为 private 方法,当我们运行以下代码时:

1
2
3
4
Derived a("a", 1);
Derived b("b", 2);

b = a;

同样会在编译器报以下错误:

1
2
3
4
5
6
7
8
9
10
main.cpp: In function ‘int main()’:
main.cpp:23:9: error: use of deleted function ‘Derived& Derived::operator=(const Derived&)’
b = a;
^
main.cpp:11:7: note: ‘Derived& Derived::operator=(const Derived&)’ is implicitly deleted because the default definition would be ill-formed:
class Derived: public Base {
^~~~~~~
main.cpp:11:7: error: ‘Base& Base::operator=(const Base&)’ is private within this context
main.cpp:7:11: note: declared private here
Base& operator=(const Base& rhs) { nameValue = rhs.nameValue; }

其原因是当涉及到到继承时,子类中自动生成的 operator= 会需要调用父类的 operator= 来进行父类中成员变量的赋值。但是如果我们采用 public 继承的方式,当子类无法调用父类的 operator= 时编译器会拒绝生成并将其标记为 deleted

结论

本条款中主要介绍了编译器自动生成的 default 构造函数,copy 构造函数,copy assignment 操作符以及析构函数。并对 copy 构造函数,copy assignment 操作符 的生成原因进行了一定分析。

完整可运行代码地址:Effective-Cpp-Reading-Note