预置构造函数
预置复制构造函数
有些细心的读者可能发现,按值传递参数是非常常见的场景,也就是说调用复制构造函数应当是经常发生的事情。但我们之前写的结构体从来没有声明过复制构造函数呀?
// 我们已经有的 String 结构体,并没有声明复制构造函数
struct String {
String();
String(const char* initVal);
String(unsigned num, char c);
assign(String s);
length();
char* str;
private:
unsigned len;
};
void printStr(String s) {
std::cout << s.str << std::endl;
}
int main() {
String a(5, 'x');
printStr(a); // 理应调用复制构造函数,但复制构造函数在哪儿呢?
}
上面这段代码是能够成功编译而且结果看上去没有问题。但是问题就在于,为什么 printStr
能够在没有复制构造的情形下复制 String
类型的变量呢?
答案就是,当我们没有声明复制构造函数时,编译器会自动为我们生成一个。这个自动生成的复制构造函数称为预置复制构造函数。
这个编译器生成的预置复制构造函数做的事情很简单——就是依次对每个成员都复制初始化(Copy initialization)。复制初始化是指:如果是基础数据类型,则直接复制过去;如果是结构体类型,则调用它的复制构造函数。形象地说,就是:
struct B { /* 定义略 */ };
struct A {
int a;
B b;
// [...]
// 下面是编译器生成的预置复制构造:
A(const A& rhs) {
/* 用 rhs.a 初始化成员 a ,直接复制过去*/;
/* 用 rhs.b 初始化成员 b ,通过调用 B 的复制构造函数 */;
/* 依次复制初始化其它的所有成员 ... */;
}
};
我这里不写
a = rhs.a;
,b = rhs.b;
是因为这是赋值语句,它和初始化有着本质上的不同。
所以说,预置复制构造函数是一个非常简单的逻辑,就是:如果想把整体复制过来,那就把每一个部分都复制过来就好。
最后请注意编译器何时会做这个操作:当你没有声明形如 T(T&);
或者 T(const T&);
的构造函数时,编译器会为你生成形如 T(const T&);
的预置复制构造函数。
如果其中某一个成员(设其类型为
M
)只声明形如M(M&);
这样不带const
限定的复制构造函数,那么编译器会转而生成T(T&);
类型的预置复制构造而非带const
限定版本的(因为这样会编译错误:无法把非只读引用绑定到只读变量上)。
预置默认构造函数
编译器除了会“画蛇添足”地为你添上预置复制构造函数,有时还会添上叫做“预置默认构造函数”的东西。为了理解这个概念,我们先来介绍默认构造函数(Default constructor)是什么意思——其实很简单,就是无参构造函数:
struct S {
// 就这个
S() { }
};
正因为调用无参构造函数的特殊性(无需初始化器),所以它有了这样一个特殊的名字。那么预置默认构造函数就是说:编译器会在某些情形时为你自动生成一个默认(无参)构造函数。请看例子:
这段代码在之前几章可以说是理所当然的,但是我们现在回过头来看一下:S sth;
这句话应当调用 S
的默认构造函数,但是我们并没有写它。而当你没有声明默认构造函数时,编译器会生成一个预置默认构造函数。一般情况下,它所做的事情也很简单:依次将每个成员都默认初始化(Default initialization)。对于结构体类型,默认初始化就是调用它的默认构造函数,而对于非结构体类型,默认初始化就是不初始化(或者说,初始化值是任意的)。形象地说:
struct B { /* 定义略 */ };
struct A {
int a;
B b;
// [...]
// 下面是编译器生成的预置默认构造:
A() {
/* 默认初始化成员 a,但实际上什么都没干 */;
/* 默认初始化成员 b,通过调用 B 的默认构造函数 */;
/* 依次默认初始化其它的所有成员 ... */;
}
};
如果某个成员存在默认成员初始化器,则不执行这个成员的默认初始化。详见后续章节。
对于一个基础类型变量,如果它是自动存储期的,则默认初始化的效果是初始化值为任意值;否则,它的初始化值为零。
最后请注意编译器何时会做这个操作:当你没有声明任何构造函数时,编译器会为你生成形如 T();
的预置默认构造函数。
总结
可以注意到,这两种编译器自动生成的构造函数有相似也有不同。下面的表格做了一些整理:
构造函数类型 | 声明形式 | 何时生成 | 行为 |
---|---|---|---|
预置复制构造函数 | T(const T&); | 没有声明其它复制构造函数时 | 复制初始化所有成员(调用复制构造或直接复制) |
预置默认构造函数 | T(); | 没有声明任何构造函数时 | 默认初始化所有成员(调用默认构造或不初始化) |
最后我介绍两种写法:=default
和 =delete
。
=default
表示,这个构造函数就按照预置的来就行。用例:
struct S {
// 我不想自己写默认构造函数,就按照预置的来就行
S() = default;
// 我也不想自己写复制构造函数,就按照预置的来就行
S(const S&) = default;
S(/* 其它构造函数 */) { /* [...] */ }
};
换句话说,=default
强制编译器生成预置构造函数,忽略之前所说的生成时机。
=delete
表示,不要预置这个构造函数。用例:
换句话说,=delete
强制编译器不生成预置构造函数,忽略之前所说的生成时机。