预置构造函数

预置复制构造函数

有些细心的读者可能发现,按值传递参数是非常常见的场景,也就是说调用复制构造函数应当是经常发生的事情。但我们之前写的结构体从来没有声明过复制构造函数呀?

// 我们已经有的 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 定义任何构造函数
struct S {
    int data;
};
int main() {
    S sth; // 调用默认(无参)构造函数
}

这段代码在之前几章可以说是理所当然的,但是我们现在回过头来看一下: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 表示,不要预置这个构造函数。用例:

struct S {
    // 不许生成默认构造函数
    S() = delete;
};
int main() {
    S sth; // 错误:找不到对应的构造函数
}
struct S {
    S() { }
    // 不许生成复制构造函数
    S(const S&) = delete;
};
void f(S a) { }
int main() {
    S sth;
    f(sth); // 错误:找不到复制构造函数
}

换句话说,=delete 强制编译器生成预置构造函数,忽略之前所说的生成时机。

最近更新:
代码未运行