特殊成员函数(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
};
总结
- 显式声明的任意形式的构造函数都会阻止默认构造函数得生成
=delete
和=default
都可以看做是构造函数的显式声明A& operator=(const A&)
拷贝赋值函数不会阻止默认构造函数得生成
二、 拷贝函数
对于拷贝函数而言,两个函数是独立的(c++98 的不严谨
),也就是说声明了其中一个,如果另外一个满足默认生成得条件,编译器还是会默认生成的,以下情况会阻止class T
的拷贝赋值函数的生成:
T
拥有一个移动构造函数或者移动赋值函数T
拥有一个non-static的成员变量被const修饰,或者是引用类型T
的成员变量或者base class
不能进行拷贝赋值
三、 移动函数
移动函数不是独立的,只要声明了其中一个,另一个就不会自动生成,以下情况会阻止class T
的移动赋值函数的生成:
T
拥有一个拷贝构造函数或者拷贝赋值函数T
拥有一个non-static的成员变量被const修饰,或者是引用类型T
的成员变量或者base class
不能进行移动赋值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.1
和Circle 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;
}