在各种编程语言中,初始化都是一个至关重要的步骤,确了保对象在使用前具备一个明确的初始状态。C++提供了多种初始化方法,每种方法都有其特定的适用场景和需要注意的事项。

下面将详细介绍 C++ 的 9 初始化方法:

1. 默认初始化(Default-initialization)

  • 形如T objnew T等方式的初始化,其中T为类型名称、obj为对象名称,T也可以是数组类型。
  • 对于具有自动存储期和动态存储期的对象,如局部变量、用new分配的结构体等:
  • 对于基本类型的对象,默认初始化仅分配对象空间,对象的值是不确定的。
  • 对于类对象,默认初始化会调用其默认构造函数,如果默认构造函数没有显式定义或由=default定义,成员的值也是不确定的。
  • 对于具有静态或线程存储期的对象,如全局对象、用staticthread_local关键字限定的对象:
  • 对于基本类型的对象,会将对象的值初始化为 0。
  • 对于类对象,会调用其默认构造函数,如果默认构造函数没有显式定义或由=default定义,成员的值也被初始化为 0。
  • 没有调用显式默认构造函数的初始化属于“零初始化”,后文介绍。
  • 如果对象的初始值是不确定的,而且程序用到了这种不确定的值,不算正确初始化,会导致程序不可预测的行为,造成严重错误。
int i;  // 全局对象,初始值为 0

struct A {
    int x;
};
int fun1() {
    A a;         // 默认初始化,成员 x 的值是不确定的
    return a.x;  // 未定义的行为
}
A* fun2() {
    return new A[5];  // 默认初始化数组,成员 x 的值都是不确定的
}
struct B {
    int x;
    B(): x(1) {}  // 默认构造函数
};
int fun3() {
    B b;         // 默认初始化,调用默认构造函数
    return b.x;  // OK
}
B* fun4() {
    return new B;  // 默认初始化,调用默认构造函数,成员 x 的值为 1
}
  • 关于对象的存储周期(即生命周期),请参见“storage durations”。

2. 直接初始化(Direct-initialization)

  • 对于内置基本类型的对象,直接初始化相当于直接赋值。
  • 对于类对象,直接初始化会调用相应的构造函数进行初始化。
int i = 1;  // 直接初始化
int j(0);   // 直接初始化

struct A {
    int x, y;
    A(int x, int y): x(x), y(y) {}
};
A a(1, 2);           // 直接初始化
A* p = new A(1, 2);  // 直接初始化

3. 拷贝初始化(Copy-initialization)

  • 对于内置基本类型的对象,拷贝初始化和直接初始化几乎没有区别。
  • 对于类对象,拷贝初始化调用拷贝构造函数将已存在的对象复制成新对象。
int i = 1;  // 直接初始化
int j = i;  // 拷贝初始化

struct T {
    int x;
    T(int i = 0): x(i) {}     // 默认构造函数
    T(const T& a): x(a.x) {}  // 拷贝构造函数
};
T a;             // 默认初始化
T b(a);          // 拷贝初始化
T c = a;         // 拷贝初始化
T* p = new T(a); // 拷贝初始化
  • 应注意浅拷贝问题,即仅复制对象的成员变量值(如指针),而不复制其指向的数据,导致多个对象共享同一份数据,一个对象修改了数据会影响其他对象,也可能造成资源被重复释放。为避免这种问题,应显式定义拷贝构造函数和拷贝赋值操作符,实现深拷贝。
  • 按值传递的参数对象、按值返回的对象、按值抛出的异常、按值捕获的异常均为拷贝初始化。
int fun1(int x) {
    return x + 1;  // 拷贝初始化,返回值是 x + 1 的副本
}

void fun2() {
    fun1(0);  // 用 0 拷贝初始化参数 x
}

void fun3() {
    std::exception e;
    throw e;  // 拷贝初始化,抛出的对象是 e 的副本
}

void fun4() {
    try { fun3(); }
    catch (std::exception e)  // 拷贝初始化,但使用引用捕获异常更合理
    {}
}
  • 从 C++11 开始,通过移动构造函数初始化对象也被视为一种特殊的拷贝初始化,尽管实际上并不涉及拷贝,而是资源的转移。下面的例子中,s1 的数据被转移到 s2 中,s2 与原来的 s1 相同,而 s1 不再持有有效数据。
std::string s1("abc");
std::string s2(std::move(s1));  // 将 s1 的数据移动到 s2 中
  • 在某些情况下,C++ 标准还允许省略拷贝或移动操作( copy/move elision),以减少不必要的对象拷贝或移动,进一步提高性能。

4. 聚合初始化(Aggregate-initialization)

  • ={}初始化数组、结构体、联合体等聚合类型的对象。
  • 可被聚合初始化的对象要求:所有非静态数据成员都是公有的,没有定义用户提供的构造函数,没有定义私有或受保护的非静态数据成员,没有基类,也没有虚函数。
  • 聚合初始化是为了与 C 语言兼容而提出的,C++11 后应使用更完善的列表初始化。
int a[] = {1, 2, 3}; // 聚合初始化数组

struct Point {
    int x, y;
};
Point p = {0, 1};   // 聚合初始化结构体
Point q[3] = {{1, 2}, {3, 4}, {5, 6}}; // 聚合初始化结构体数组

5. 列表初始化(List-initialization)

  • 使用花括号{}进行初始化,包含聚合初始化。
  • 由 C++11 引入,又称为万能初始化(uniform initialization),建议使用列表初始化代替其他初始化方法。
  • 列表初始化会进行更严格的类型检查,如果类型转换会造成数据丢失等错误,则不会通过编译。下面的例子中,double 类型的参数转为 float 变量可能会丢失数据,用列表初始化可有效避免意料之外的错误。
void fun(double x) {
    float a = x;   // 可能丢失数据
    float b(x);    // 可能丢失数据
    float c{x};    // 可能丢失数据,但不会通过编译

    float d{static_cast<float>(x)};  // OK,有意转换
    // ...
}
  • 列表出初始化也可以用于直接、拷贝等初始化,如对于类类型,列表初始化也会调用相应的构造函数,如果列表为空,则调用默认构造函数。
struct A {
    int x, y;
    A(int x, int y): x{x}, y{y} {}
};
A a{1, 2};  // 直接初始化,调用构造函数
A b{a};     // 拷贝初始化
  • 通过={}初始化在理论上是拷贝初始化,不带等号的{}才是直接初始化,虽然复制成本可被优化,但仍应避免使用多余的等号。
struct T {
    int x;
    explicit T(int i): x(i) {}
};
T a{1};     // OK,直接初始化
T b = {1};  // 无法通过编译

例中 ={1} 实际上先由 {1} 初始化一个临时对象,再由 = 完成拷贝初始化,但由于构造函数由 explicit 关键字限定,临时对象无法隐式转为 T 类型的对象,所以无法通过编译。

  • 初始化列表的类型为std::initializer_list<T>T为元素类型,如果相关构造函数对其有重载,则调用相关重载了的构造函数。
std::vector<int> v(5, 0);  // 五个值为 0 的元素
std::vector<int> w{5, 0};  // 两个元素,第一个是 5,第二个是 0

std::vector 对 initializer_list 进行了重载,可以像初始化数组一样初始化 vector,v 有 5 个元素,每个元素都是 0,与 v 不同,w 有两个元素,第一个是 5,第二个是 0,这一点列表初始化无法代替直接初始化。

  • C++20 引入通过指派符初始化的方法,与 C 语言的指派初始化相似,以 “.成员名称 = ... ” 的形式对结构体对象进行初始化。
  • 可通过指派符初始化的对象要求:只包含有 public 的直接非静态数据成员,没有用户声明的构造函数或者继承的构造函数,没有虚基类、private 基类或 protected 基类,也没有虚成员函数。
struct A {
    int x, y, z;
};
A a{.y = 2, .x = 1};  // 语法错误, 指派符 .x 应排在 .y 之前
A b{.x = 1, .z = 2};  // 正确,b.x 为 1,b.z 为 2,而 b.y 会被初始化为 0

6. 零初始化(Zero-initialization)

  • 用空括号()、空花括号{},以及用花括号对部分数组元素初始化。
  • 零初始化是以上几种初始化方法的特殊形式,可以将变量、数组、类对象成员初始化为零。注意,例中int f();不是零初始化,而是声明了一个函数,这是一种常见笔误,改用{}可以避免这种问题。
static int n;  // 零初始化,n 的值为 0
static int* p;  //  零初始化,ptr 的值为 nullptr

int i{};  // 零初始化,i 的值为 0
int f();  // 非初始化,f 是一个函数

int* pi = new int{};     // 零初始化, *pi 的值为 0
int* qi = new int[5]();  // 零初始化堆数组

int a[8]{};   // 零初始化数组,所有元素均为 0
int b[8]{0};  // 零初始化数组
int c[8]{1};  // c[0] 为 1,从第二个元素开始零初始化,c[1] 到 c[7] 均为 0
  • 对于类对象,如果默认构造函数没有显式定义,或用=default定义,可以进行零初始化。
struct A {
    int x, y;
};

A a{};      // 零初始化,成员均为 0
A b = A();    // 零初始化
auto c = A();   // 零初始化

A* p = new A();     // 零初始化
A* q = new A[4]();  // 零初始化数组
  • 对于类对象,如果显式定义了默认构造函数,则调用默认构造函数,不属于零初始化,属于“值初始化”。
struct A {
    int x, y;
    A(): x(1), y(2) {}  // 默认构造函数
};
A a{};          // 非零初始化,成员 x 的值为 1,y 的值为 2
A* p = new A();   // 同上
A* q = new A[4]();  // 同上
  • 应尽量完善类的构造函数,对于无法显式定义构造函数的类型,则应及时使用零始初化。

7. 值初始化(Value-initialization)

  • 也是用空括号()、空花括号{}初始化。
  • 值初始化包含零初始化。
  • 如果类对象的默认构造函数没有显式定义,或由=default定义,则优先进行零初始化,否则进行值初始化。
struct A {
    int x, y;
    A():
        x(),  // 值初始化,也是零初始化
        y(1)  // 直接初始化,值为 1
    {}
};
A a{};  // 值初始化,a.x 为 0,a.y 为 1

8. 常量初始化(Constant initialization)

  • 用于常量的编译期初始化。
  • 常量初始化和零初始化统称为静态初始化,其他初始化均为动态初始化。
  • 静态初始化应在动态初始化之前完成。
const int i = 5;  // 常量初始化
int j = 3;
const int k = j;  // 直接初始化,但不是常量初始化

void foo() {
    std::array<int, i> ai;  // OK
    std::array<int, k> ak;  // 编译错误,k 不是编译期常量
    // ...
}

例中,i 是常量初始化,在编译期完成,j 是变量,不能用于常量初始化。

9. 引用初始化(Reference initialization)

  • 用于将对象绑定到引用。
int i = 0;
int& r = i;  // 引用初始化

const double& crd = i;  // 引用初始化,引用的是由 i 转成的临时 double 对象
double&& rrd = i;      // 引用初始化,引用的是由 i 转成的临时 double 对象

double& rd = i;  // 编译错误

例中 crd 和 rrd 引用的是临时对象,临时对象的生命周期也被延长。

综上所述,C++ 的初始化方法各有特点和使用场景,开发者在选择初始化方法时需要根据具体情况谨慎考虑,并注意避免常见的错误和陷阱。

更进一步地,可参见如下详细介绍:

  1.  不可访问未初始化或已释放的资源
  2. 全局对象的初始化不可依赖未初始化的对象
  3. 合理初始化各枚举项
  4. 用 {} 代替 = 或 () 进行初始化
  5. 在初始化列表中对聚合体也应使用初始化列表
  6. 初始化列表中不应存在重复的指派符
  7. 对象初始化不可依赖自身的值
  8. 全局对象的初始化过程不可抛出异常
  9. 局部对象在使用前应被初始化
  10. 成员须在声明处或构造时初始化
  11. 成员初始化应遵循声明的顺序
  12. 不可解引用未初始化的指针
  13. 拷贝构造函数应避免实现复制之外的功能
  14. 移动构造函数应避免实现数据移动之外的功能