[cpp primer随笔] 14. 类的构造函数

一. 构造函数基本特性

  1. 构造函数是类中一种特殊的成员函数,作用是控制类对象的初始化过程。
  2. 其名称与类相同,且没有返回值类型,可以通过参数列表进行重载。
  3. 构造函数不能为const。一个常量对象在构建时,会先执行完构造函数,然后再获得所谓的常量特性。

二、类成员初始化

2.1 类内初始值

我们可以为类中的数据成员提供类内初始值。当构造函数没能提供初始化时,该数据成员将自动使用类内初始值。

struct B{
    B(int arg1, int arg2){};
};
struct A{
    int count{0};
    B b = B(10, 20);
};

需要注意的是,类内初始值只能以=或者{}的形式提供。

2.2 构造函数初始值列表

在构造函数参数列表与函数体之间,可以提供初始值列表,以为对象中的成员指定初始值。

class A{
private:
    int b; int a;
public:
    A(int arg1, int arg2): a(arg1), b(arg2){};
};

此处有三点注意事项:

  1. 初始化 vs 赋值
    在先前的文章中说过,变量的赋值与初始化是两回事。**初始化是给变量这块内存空间赋初始值,而赋值则是将变量数据擦除,重新写入新值。**在一些场景下,这会带来某种程度上的性能差异。
    初始值列表所完成的功能就是初始化。如果初始值列表中没有对一个数据成员做初始化,当进入构造函数体时,该数据成员实际上已经执行完默认初始化。而构造函数体中对成员的操作不再是初始化,而是赋值操作。
    这种差别带来的影响与类型相关,内置类型还好说,一些类类型的初始化可能较为复杂,此时性能上就会有所差异。

  2. 初始化顺序
    初始值列表仅能指明成员的初始值是多少,初始值列表中成员的顺序,并不代表成员的实际初始化顺序初始化顺序一般与成员在类内的声明顺序相同。例如在上面的例子里,是先声明B后声明A,那么初始化的顺序就与之相同,尽管初始值列表里是先A后B。
    初始化顺序在初始值存在前后依赖关系时十分重要。例如:

    class A{
    private:
        int b; int a;
    public:
        A(int arg1): a(arg1), b(a){};
    };
    

    在初始值列表中,b使用a变量的值进行初始值,然而b声明先于a,初始化优先级同样也先于a进行,但此时a还未进行初始化,这里b的值将为未定义。因此,最好统一成员声明与初始值列表的顺序,这样可以降低出错概率。

  3. 与类内初始值的关系
    正如前面所言,若该成员有类内初始值,则当初始值列表不存在,或者没能为某个成员提供初始值时,则使用该值进行初始化

三、默认构造

3.1 声明方式

  1. 编译器自动合成
    当类中没有显示声明构造函数时,编译器会自动合成默认构造函数。这种函数的行为非常简单,即如果成员有类内初始值,则使用类内初始值进行初始化;若没有,则按块内标准执行默认初始化。(所谓的块内标准,指的是内置类型值为未定义,类类型执行各自的默认构造)

  2. 使用=default来获得默认行为
    当类中存在其他有参构造函数时,编译器不会自动合成默认构造,此时需要我们对其进行显示声明。如果想简单地获得与自动合成的版本相同行为的构造函数的话,可以直接使用=default(C++ 11)完成对默认构造的定义。

    struct B{
        B() = default; // 默认行为,等同于自动合成的版本
        B(int, int);
    };
    

    此外,用户可以使用=delete来禁止编译器自动合成默认构造函数。(有人会说这种没有任何构造函数的类有啥用?可以联想基类、接口类、静态提供单例的类等,这里不展开讲)

  3. 显式定义
    如果希望在默认构造函数里做更多的工作,则需要显式定义它的函数体。

  4. 向构造函数的每一个参数都提供默认实参
    此时相当于显式定义了默认构造函数。

3.2 禁用场景

默认构造函数的禁用场景指的是,无法自定义默认构造,同时编译器也无法自动合成默认构造函数的一些情况。这些情况比较多,我们这里列举经常遇到的几点,具体地可以去看cppreference上的默认构造函数一节。

首先先明确一点,数据成员的初始化是无关静态成员的。因此下面的类数据成员,均指非静态成员。在此基础上,以下情况,默认构造会被编译器自动删除,无法使用:

  1. 类类型成员没有默认构造

  2. 类数据成员存在const限定的类型且没有类内初始值(编译器会报类似下面的错误)

    class A{
        const int a;
        int b;
    };
    
    int main(){
        A a; // error: use of deleted function 'A::A()'
        // note: 'A::A()' is implicitly deleted because the default definition would be ill-formed:
        return 0;
    }
    
  3. 类数据成员存在引用类型且没有类内初始值(报错如上)

  4. 类内的类类型成员,其所属类别存在上述情况

3.3 默认构造的调用场景

默认构造显然可以主动指定用于创建对象,请注意正确的使用形式是A obj;而非A obj();后者实际上是在声明一个函数,其返回值类型为A,参数列表为空,函数名为obj

此外,当对象被默认初始化或者值初始化时也将自动执行默认构造。回想初始化相关知识点,定义变量却无初始值时,块内非静态变量执行默认初始化,数组初始化残缺元素补齐、块内外静态对象、全局变量均执行值初始化。

然而,无论默认初始化还是值初始化,对于类类型而言都会自动执行默认构造函数。两种初始化在以下场景中将被触发:

默认初始化:

  1. 块内不适用初始值定义一个非静态变量
  2. 类本身含有类类型成员,并且使用默认构造函数创建对象时
  3. 类类型成员没有在构造函数内显式初始化

值初始化:

  1. 数组初始化时,提供的元素少于数组维数,剩余元素自动执行值初始化。
  2. 不使用初始值定义一个局部静态变量时
  3. 通过T()显式请求值初始化时(例如A obj = A();

四、委托构造

C++委托构造函数是C++11引入的一个特性,它的设计目的是为了简化构造函数的重载和代码复用。

在传统的C++中,如果一个类有多个构造函数,它们往往会有一些共同的初始化代码。为了避免重复编写这些共同的初始化代码,我们可以将这些代码提取到一个私有的辅助函数中,并在每个构造函数中调用这个辅助函数。但这样做会导致代码冗余,而且当初始化代码发生变化时,需要修改多个构造函数。

委托构造函数的设计目的就是为了解决这个问题。它允许一个构造函数调用同一个类的另一个构造函数,从而实现代码的复用。通过委托构造函数,我们可以将共同的初始化代码放在一个构造函数中,其他构造函数只需要调用这个构造函数即可。

下面展示《C++ Primer》中的一个使用委托构造函数设计的类的例子:

class A{
public:
    // 非委托构造函数使用初始值列表初始化成员
    A(std::string s, unsigned c, double p): 
        mem1(s), mem2(c), mem3(p){};
    // 委托构造函数可以将成员初始化过程委托给其他构造函数
    A(): A("", 0, 0) {}
    A(std::string s): A(s, 0, 0) {}
    A(std::istream &is): A() { read(is, *this); }
};

可以看到,委托构造函数其实可以传递性的委托其他委托构造函数。接受委托的构造函数,将先后执行其初始值列表、其进一步委托的委托构造函数、然后再执行其函数体(如果有的话),最终再将执行权交还给委托者的函数体。

你可能感兴趣的:(C++,c++)