《Effective Modern C++》学习笔记 - Item 17:理解特殊成员函数的生成

特殊成员函数(special member functions) 指C++在类中会自动生成的函数。

  • C++98有四个:默认构造函数(default constructor),析构函数(destructor),拷贝构造函数(copy constructor),拷贝赋值运算符(copy assignment operator)。当然在这里有些细则要注意。这些函数仅在需要的时候才生成,比如某个代码使用它们但是它们没有在类中明确声明。默认构造函数仅在类完全没有构造函数的时候才生成。
  • C++11迎来了两位新成员:移动构造函数(move constructor)和移动赋值运函数(move assignment operator)。大原则还是相同:需要时才生成。它们的行为与一对复制函数相似,也是将类的非静态成员“逐成员地(memberwise)移动”。**不同点在于,这里的行为更应该说是“尽量”这样做——每个成员如果能够被移动构造,那么就移动构造;如果不能移动构造,那就复制构造。**注意这里的“不能移动构造”指的是没有显式声明移动构造/赋值函数,而非通过 delete 主动屏蔽了移动构造函数,绝大多数来自C++98的类都如此。

一、 默认构造函数

我们这里所说的默认构造函数指的就是类的无参数构造函数。如果在代码中没有显式声明任何构造函数,那么编译器就会默认为class生成一个默认构造函数。但是在下面这些情况下是不会生成默认构造函数的:

struct A {
    int x;
    A(int x = 1): x(x){} // user-defined default constructor
};

struct B: A {
    // B::B() is implicitly-defined, calls A::A()
};

struct C {
    A a;
    // C::C() is implicitly-defined, calls A::A()
};

struct D: A {
    D(int y): A(y) {}
    // D::D() is not declared because another constructor exits(存在别的构造函数,所以不会生成default constructor)
};

struct E:A {
    E(int y): A(y) {}
    E() = default; // explicitly defaulted, calls A::A()
};

struct F {
    int& ref; // reference member
    const int c; // const member
    // F::F() is implicitly defined as delete(存在常量和引用成员变量,这里不会自动生成默认构造函数)
};

// user declared copy constructor (either user-provided, deleted or default)
// prevents the implicit generation of a default constructor

struct G{
    G(const G&) {}
    // G::G() is implicitly defined as deleted
};

struct H {
    H(const H&) = delete;
    // H::H() is implicitly defined as deleted
};

struct I {
    I(const I&) = default;
    // I::I() is implicitly defined as deleted
};

总结

  1. 显式声明的任意形式的构造函数都会阻止默认构造函数得生成
  2. =delete=default都可以看做是构造函数的显式声明
  3. A& operator=(const A&)拷贝赋值函数不会阻止默认构造函数得生成

二、 拷贝函数

对于拷贝函数而言,两个函数是独立的(c++98 的不严谨),也就是说声明了其中一个,如果另外一个满足默认生成得条件,编译器还是会默认生成的,以下情况会阻止class T拷贝赋值函数的生成:

  1. T拥有一个移动构造函数或者移动赋值函数
  2. T拥有一个non-static的成员变量被const修饰,或者是引用类型
  3. T的成员变量或者base class不能进行拷贝赋值

三、 移动函数

移动函数不是独立的,只要声明了其中一个,另一个就不会自动生成,以下情况会阻止class T移动赋值函数的生成:

  1. T拥有一个拷贝构造函数或者拷贝赋值函数
  2. T拥有一个non-static的成员变量被const修饰,或者是引用类型
  3. T的成员变量或者base class不能进行移动赋值
  4. T拥有一个移动构造函数

显式声明的拷贝函数会阻止移动函数的生成,显式声明的移动函数也会组织拷贝函数得生成。其中的道理在于:如果默认生成的 memberwise的拷贝/构造函数中的某一个不能满足要求,那么其它几个大概也是不正确的。最典型的例子就是当要在类内管理某种资源(指针)时,如果需要在某个拷贝函数中进行某种操作(比如memcpy),那么几乎可以肯定在另一个函数当中同样需要进行该操作,而且析构函数中也要进行操作(一般是释放资源)。这就是C++98中著名的 Rule of Three,标准库中涉及内存管理的类(如STL中的容器类)都会同时声明这三个函数。

加上C++11的移动构造后,可以拓展为零/三/五原则:要么不声明这五个函数中的任意一个(默认行为足够),要么声明两拷贝+析构三个函数(移动构造时会有性能损失,但逻辑ok,兼容C++98的类),要么声明两复制+两移动+析构五个函数。

四、 初始化列表

以下三种情况,定义构造函数的时候最好使用初始化列表语法:

  • 初始化引用类型的成员变量
  • 初始化常量类型的成员变量
  • 调用基类或成员变量的类的有参构造函数

对情况一,引用类型必须在声明时初始化,类中的成员变量是个例外,可以只声明引用类型的成员变量,而不指定默认值(例如:int &a;),不过这种情况下,在任何函数体中存在调用该成员变量的代码都会编译失败,抛Undefined symbols for architecture x86_64: "C::a", referenced from: ...之类的链接错误。这种情况只有两种选择:要么在class中声明引用类型的成员变量时指定初始值;要么在构造函数初始化列表中初始化该引用型成员变量。

对情况二,在任何函数体中给const常量赋值会引发编译错误,抛Can't assign to ...之类的编译错误。这种情况也只有两种选择:这种情况只有两种选择:要么在class中声明const成员变量时指定初始值;要么在构造函数初始化列表中初始化该const成员变量。

对情况三,如果不使用初始化列表,而是在构造函数中去实现与初始化列表等价的代码,是不会导致任何编译器警告或者错误的,但是这种方式看起来比较傻,重点是执行效率低。

#include <cstdio>

struct A {
  int a_;
  A() { printf("%s\n", __PRETTY_FUNCTION__); }
  A(int a) {
    a_ = a;
    printf("%s\n", __PRETTY_FUNCTION__);
  }
};

struct B {
  A a_;
  B(int a) { a_ = A(a); }		// 发生两次调用"A::A(); A::A(int)"
  B(int a): a_(a) {}			// 发生一次调用"A::A(int)"
};

int main(int argc, char *argv[]) {
  B b(10);
  return 0;
}

编译器对初始化列表的处理是有特定顺序的,注意是按照成员变量在class中的声明顺序,而不是按照初始化列表所指定的顺序

五、 赋值和拷贝构造的调用时机

拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。

#include <cstdio>

struct A {
  A() {}
  A(const A &) { printf("%s\n", __PRETTY_FUNCTION__); }
  A &operator=(const A &) {
    printf("%s\n", __PRETTY_FUNCTION__);
    return *this;
  }
};

void copy_constructor() {
  A a1;
  A a2 = a1;
}

void copy_assign() {
  A a1, a2;
  a2 = a1;
}

int main(int argc, char *argv[]) {
  copy_constructor();				// A::A(const A &)
  copy_assign();					// A &A::operator=(const A &)
  return 0;
}

六、 explicit

#include <cstdint>
#include <cstdio>

struct Circle {
  Circle(double r) { printf("%s\n", __PRETTY_FUNCTION__); }
  Circle(int x, int y = 0) { printf("%s\n", __PRETTY_FUNCTION__); }
};

int main(int argc, char *argv[]) {
  Circle c1 = 1.1;				// Circle::Circle(double)
  Circle c2 = 2;				// Circle(int x, int y = 0) { printf("%s\n", __PRETTY_FUNCTION__); }
  return 0;
}

上面的例子中Circle c1 = 1.1Circle c2 = 2都触发了隐式转换。这种代码的可读性较差,而且容易给人造成误解使用explicit就可以禁止这种情况:

#include <cstdint>
#include <cstdio>

struct Circle {
  explicit Circle(double r) { printf("%s\n", __PRETTY_FUNCTION__); }
  explicit Circle(int x, int y = 0) { printf("%s\n", __PRETTY_FUNCTION__); }
};

int main(int argc, char *argv[]) {
  Circle c1 = 1.1;        // error: no viable conversion from 'double' to 'Circle'
  Circle c2 = 2;          // error: no viable conversion from 'int' to 'Circle'
  return 0;
}

参考

  1. item-17学习笔记
  2. cpp基础
  3. item-17中文翻译
  4. 关于初始化列表
  5. cppreference-默认构造函数
  6. java中继承与构造函数的关系
  7. java中的super关键字
  8. 拷贝构造和赋值函数的调用时机
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值