本文最后更新于 2024-07-21,文章内容可能已经过时。

C++ 对象初始化是一个关键过程,确保在对象的生命周期开始时为其分配适当的初始值。但是 C++ 中的对象初始化语法有很多选择,例如可以使用括号,等号,花括号。不同的初始化语法提供了灵活性,使得程序员可以根据需要选择合适的初始化方式。通过正确理解和使用对象初始化,可以编写更安全和更高效的代码。

使用 {} 来初始化对象在 C++ 中有许多好处,这些好处可以通过具体的例子来更好地理解。以下是一些主要的好处以及对应的示例:

1. 统一初始化语法

花括号 {} 初始化语法可以在几乎所有地方使用,并且能够处理多种初始化场景,从而简化代码编写。

std::vector<int> v{1, 3, 5}; // v的初始内容是1、3、5

在这个例子中,我们使用 {} 初始化一个 std::vector 对象,这种方法比传统的 push_back 方法更简洁。

传统方法

  1. 构造函数初始化:

    std::vector<int> v;
    v.push_back(1);
    v.push_back(3);
    v.push_back(5);
    
  2. 赋值初始化:

    int a = 5;
    
  3. 列表初始化:

    int arr[] = {1, 2, 3};
    

现代方法(C++11及以后)

使用花括号 {} 进行统一初始化:

  1. 统一初始化:

    std::vector<int> v{1, 3, 5}; // v的初始内容是1、3、5
    int a{5};                    // a的值是5
    int arr[]{1, 2, 3};          // arr的内容是1、2、3
    

对比

  • 简洁性:现代方法用 {} 一步到位,比传统的多步初始化简洁。
  • 一致性:现代方法统一了不同类型的初始化方式。
  • 安全性:现代方法避免了窄化转换问题,更安全。

2. 防止隐式缩窄转换

在C++中,隐式缩窄转换是指将一种类型的数据转换为较小范围的另一种类型时可能发生的数据丢失现象。为了防止这种情况的发生,可以使用 {} 初始化,它会强制编译器检查是否存在潜在的缩窄转换问题,并在必要时触发编译错误。

以下是通过逐步讲解,展示如何通过 {} 初始化来防止隐式缩窄转换的过程:

  1. 定义一些浮点数表示的权重:

    double weightA = 5.7, weightB = 8.3, weightC = 4.6;
    

    这里我们定义了三个 double 类型的变量 weightAweightBweightC

  2. 没有 {} 初始化:

    • 操作: 同样不会进行缩窄转换检查。表达式 weightA + weightB + weightC 结果为 18.6

    • 结果: 18.6 被截断为 18

    • 问题: 数据丢失,浮点数的精度丢失。

    • 操作: 编译器不会进行缩窄转换检查。表达式 weightA + weightB + weightC 结果为 18.6

    • 结果: 由于 int 类型不能表示小数部分,18.6 被截断为 18

    • 问题: 数据丢失,浮点数的精度丢失。

    • 使用圆括号 () 初始化 int 类型的变量:

      int totalWeight(weightA + weightB + weightC); // 可以,表达式的值被截断为int
      
    • 使用赋值运算符 = 初始化 int 类型的变量:

      int totalWeight = weightA + weightB + weightC; // 可以,表达式的值被截断为int
      
  3. {} 初始化:

    • 操作: 编译器会进行严格的缩窄转换检查。

    • 结果: 编译器检测到表达式 weightA + weightB + weightC 结果为 18.6,它不能安全地转换为 int

    • 问题: 编译器报错,防止了数据丢失。

    • 解决方案: 提醒程序员显式处理类型转换,避免隐式缩窄转换。

    • 使用 {} 初始化 int 类型的变量:

      int totalWeight{weightA + weightB + weightC}; // 错误!双精度浮点数的和可能无法表示为int
      

通过对比可以看出:

  • 没有 {} 初始化时:
    • 编译器不会进行缩窄转换检查,可能导致数据丢失(如浮点数转换为整数时的小数部分丢失)。
    • 程序执行时会发生意外的数据截断,导致结果不准确。
  • {} 初始化时:
    • 编译器会进行严格的缩窄转换检查,防止不安全的类型转换。
    • 编译器报错,强制程序员处理潜在的数据丢失问题,确保程序的安全性和正确性。

因此,推荐在需要防止隐式缩窄转换时,使用 {} 初始化,以提高代码的安全性和健壮性。

3. 规避解析问题

在某些情况下,使用 () 初始化会被解析为函数声明,而不是对象初始化。使用 {} 初始化可以避免这种问题。

假设我们有一个类 Device,它有多个构造函数:

class Device {
public:
    Device(int id, bool status);
    Device(int id, double voltage);
    Device(std::initializer_list<double> params);
};

Device device1(42, true);   // 调用第一个构造函数
Device device2{42, true};  // 调用 std::initializer_list 构造函数
Device device3(42, 3.3);    // 调用第二个构造函数
Device device4{42, 3.3};   // 调用 std::initializer_list 构造函数

Device device5();          // 最棘手的解析,声明了一个名为 device5 的函数
Device device6{};          // 调用默认构造函数

使用 {} 初始化 device6,我们避免了最棘手的解析问题,确保调用了默认构造函数。

  1. 问题描述:函数声明的二义性在C++中,使用 () 初始化对象时,某些情况下编译器会将其解析为函数声明,而不是对象初始化。例如:

    Device device5(); // 这实际上是声明了一个返回 Device 对象的函数,而不是创建一个 Device 对象
    

    在这段代码中,编译器将 Device device5(); 解析为一个名为 device5 的函数声明,该函数没有参数,并返回一个 Device 对象。

  2. 解决方案:使用 {} 初始化使用 {} 初始化可以明确告诉编译器,这是一个对象初始化,而不是函数声明。例如:

    Device device6{}; // 这明确告诉编译器创建一个 Device 对象,并调用其默认构造函数
    
  3. 对比例子:类 Device 的多个构造函数假设我们有如下类 Device,它有多个构造函数,包括一个接受 std::initializer_list<double> 的构造函数:

    class Device {
    public:
        Device(int id, bool status);
        Device(int id, double voltage);
        Device(std::initializer_list<double> params);
    };
    
  4. 不同初始化方式的影响看几个初始化例子,理解使用 (){} 的区别:

    Device device1(42, true);   // 使用 () 调用第一个构造函数 Device(int, bool)
    Device device2{42, true};   // 使用 {} 调用 std::initializer_list<double> 构造函数
    Device device3(42, 3.3);    // 使用 () 调用第二个构造函数 Device(int, double)
    Device device4{42, 3.3};    // 使用 {} 调用 std::initializer_list<double> 构造函数
    
  5. 最棘手的解析问题最棘手的问题出现在以下情况:

    Device device5(); // 编译器将其解析为一个函数声明
    Device device6{}; // 编译器明确地将其解析为对象初始化
    

    通过使用 {},我们避免了函数声明的二义性问题,确保 device6 被正确地初始化为一个 Device 对象。

使用 {} 初始化可以避免 C++ 中某些情况下的二义性问题,确保对象被正确地初始化。这种初始化方式特别适用于避免编译器将初始化表达式误解析为函数声明的情况。在上述例子中,Device device5() 声明了一个返回 Device 对象的函数,而 Device device6{} 确保了对象的初始化。

4. 支持非静态数据成员的初始化

花括号 {} 可以用于指定非静态数据成员的默认初始化值。

假设我们有一个类 Sensor,它有一些非静态数据成员:

class Sensor {
private:
    int sensor_id{0};           // 使用花括号初始化,默认值是0
    double sensor_voltage = 3.3; // 使用等号初始化,默认值是3.3
    bool sensor_status(1);      // 使用括号初始化,这是不允许的
};
  1. 问题描述:初始化非静态数据成员在C++中,初始化非静态数据成员有多种方法,包括使用括号、等号和花括号。然而,这些方法并不都适用于所有情况。例如:

    class Sensor {
    private:
        int sensor_id(0); // 这是不允许的,会产生编译错误
    };
    

    在这段代码中,使用括号 () 初始化 sensor_id 会导致编译错误,因为这种语法不被允许用于非静态数据成员的初始化。

  2. 解决方案:使用花括号 {} 初始化使用花括号 {} 可以明确且有效地初始化非静态数据成员。例如:

    class Sensor {
    private:
        int sensor_id{0}; // 使用花括号初始化,默认值是0
    };
    

    这种方式不仅语法上是正确的,还能提高代码的可读性和一致性。

  3. 对比例子:类 Sensor 的多种初始化方法假设我们有如下类 Sensor,它包含三种不同的初始化方法:

    class Sensor {
    private:
        int sensor_id{0};           // 使用花括号初始化
        double sensor_voltage = 3.3; // 使用等号初始化
        bool sensor_status(1);      // 使用括号初始化(错误)
    };
    
  4. 不同初始化方式的影响看几个初始化例子,理解不同方式的区别:

    class Sensor {
    private:
        int sensor_id{0};           // 使用花括号初始化,正确且推荐
        double sensor_voltage = 3.3; // 使用等号初始化,正确
        bool sensor_status(1);      // 使用括号初始化,错误
    };
    
  5. 非静态数据成员的花括号初始化花括号初始化提供了一种一致且清晰的方式来为非静态数据成员赋予默认值。这不仅避免了某些语法错误,还使得代码更具可读性。例如:

    class Sensor {
    private:
        int sensor_id{0};           // 使用花括号初始化,默认值是0
        double sensor_voltage = 3.3; // 使用等号初始化,默认值是3.3
    public:
        Sensor() = default;   // 默认构造函数
    };
    

    在这段代码中,sensor_idsensor_voltage 被正确地初始化为0和3.3,确保了对象在创建时具有合理的默认状态。

使用花括号 {} 初始化非静态数据成员,可以避免括号 () 初始化时的语法错误,并提供一种一致且清晰的方式来为成员变量赋予默认值。这种方式提高了代码的可读性和一致性,确保类的定义更加清晰。例如,在上述例子中,sensor_id 被正确地初始化为0,而 sensor_voltage 被正确地初始化为3.3。

5. 处理 std::initializer_list 构造函数

使用 {} 初始化时,会优先匹配 std::initializer_list 类型的构造函数,这在某些情况下会改变构造函数的选择。

假设我们有一个类 Gadget,它有多个构造函数,包括一个接受 std::initializer_list<double> 的构造函数:

class Gadget {
public:
    Gadget(int id, bool active);
    Gadget(int id, double value);
    Gadget(std::initializer_list<double> il);
};

Gadget g1(10, true);  // 调用第一个构造函数
Gadget g2{10, true};  // 调用 std::initializer_list 构造函数(10 和 true 转换为 double)
Gadget g3(10, 5.0);   // 调用第二个构造函数
Gadget g4{10, 5.0};   // 调用 std::initializer_list 构造函数(10 和 5.0 转换为 double)

使用 {} 初始化时,会优先匹配 std::initializer_list 类型的构造函数,这会导致与预期不同的构造函数被调用。

  1. 问题描述:构造函数的选择在C++中,类可以有多个重载的构造函数。在某些情况下,使用 (){} 初始化对象时,可能会调用不同的构造函数。例如:

    class Gadget {
    public:
        Gadget(int id, bool active);
        Gadget(int id, double value);
        Gadget(std::initializer_list<double> il);
    };
    
  2. 使用 () 初始化使用 () 初始化对象时,编译器会选择与参数类型最匹配的构造函数。例如:

    Gadget g1(10, true); // 调用第一个构造函数 Gadget(int, bool)
    Gadget g3(10, 5.0);  // 调用第二个构造函数 Gadget(int, double)
    
  3. 使用 {} 初始化使用 {} 初始化对象时,编译器会优先选择 std::initializer_list 类型的构造函数。例如:

    Gadget g2{10, true}; // 调用 std::initializer_list 构造函数(10 和 true 转换为 double)
    Gadget g4{10, 5.0};  // 调用 std::initializer_list 构造函数(10 和 5.0 转换为 double)
    

    这种情况下,即使参数类型与其他构造函数匹配,编译器仍会选择 std::initializer_list 构造函数。

  4. 对比分析在以下代码中,我们可以看到两种初始化方式的不同结果:

    Gadget g1(10, true);  // 调用第一个构造函数 Gadget(int, bool)
    Gadget g2{10, true};  // 调用 std::initializer_list 构造函数(10 和 true 转换为 double)
    Gadget g3(10, 5.0);   // 调用第二个构造函数 Gadget(int, double)
    Gadget g4{10, 5.0};   // 调用 std::initializer_list 构造函数(10 和 5.0 转换为 double)
    
  5. 需要注意的地方使用 {} 初始化时,需要特别注意是否存在 std::initializer_list 类型的构造函数,因为这会改变预期的构造函数调用。为了避免意外调用 std::initializer_list 构造函数,可以显式地使用 () 或确保 {} 初始化的参数类型和数量与预期的构造函数匹配。

使用 {} 初始化对象时,编译器会优先选择 std::initializer_list 类型的构造函数。这种行为在处理复杂的重载解析时需要特别注意,以避免意外调用 std::initializer_list 构造函数。在上述例子中,Gadget g2{10, true}Gadget g4{10, 5.0} 都调用了 std::initializer_list 构造函数,而不是预期的其他构造函数。这种行为可能会导致意外的结果,因此在使用 {} 初始化时,需要特别注意构造函数的选择。

6. 默认构造函数和空的 std::initializer_list

当使用空的 {} 初始化时,它表示调用默认构造函数,而不是 std::initializer_list 构造函数。

假设我们有一个类 Gadget,它有默认构造函数和接受 std::initializer_list<int> 的构造函数:

class Gadget {
public:
    Gadget() {
        std::cout << "Default constructor called" << std::endl;
    }
    Gadget(std::initializer_list<int> il) {
        std::cout << "std::initializer_list constructor called" << std::endl;
    }
};

Gadget g1{};   // 调用默认构造函数
Gadget g2({}); // 用空列表调用 std::initializer_list 构造函数
Gadget g3{{}}; // 同上
  1. 问题描述:默认构造函数与 std::initializer_list 构造函数在C++中,一个类可以有多个重载的构造函数,其中包括默认构造函数和接受 std::initializer_list 的构造函数。在某些情况下,使用 {} 进行初始化时,会涉及到调用哪一个构造函数的问题。

    class Gadget {
    public:
        Gadget() {
            std::cout << "Default constructor called" << std::endl;
        }
        Gadget(std::initializer_list<int> il) {
            std::cout << "std::initializer_list constructor called" << std::endl;
        }
    };
    
  2. 使用空的 {} 初始化使用空的 {} 进行初始化时,会调用默认构造函数,而不是 std::initializer_list 构造函数。这提供了一种明确的方式来调用默认构造函数。

    Gadget g1{}; // 调用默认构造函数
    
  3. 使用空列表 () 初始化使用 () 并传入空的初始化列表时,会调用 std::initializer_list 构造函数。

    Gadget g2({}); // 用空列表调用 std::initializer_list 构造函数
    
  4. 使用嵌套的 {} 初始化使用嵌套的 {} 进行初始化时,也会调用 std::initializer_list 构造函数。

    Gadget g3{{}}; // 用空列表调用 std::initializer_list 构造函数
    
  5. 对比分析在以下代码中,我们可以看到三种不同的初始化方式及其结果:

    Gadget g1{};   // 调用默认构造函数
    Gadget g2({}); // 用空列表调用 std::initializer_list 构造函数
    Gadget g3{{}}; // 用空列表调用 std::initializer_list 构造函数
    

    结果将会分别输出:

    Default constructor called
    std::initializer_list constructor called
    std::initializer_list constructor called
    
  6. 需要注意的地方使用 {} 初始化时,需要注意空的 {} 和嵌套的 {} 在构造函数调用上的区别。空的 {} 表示调用默认构造函数,而嵌套的 {} 则表示调用 std::initializer_list 构造函数。

使用 {} 初始化提供了一个明确且一致的语法,可以有效避免默认构造函数与 std::initializer_list 构造函数之间的歧义。空的 {} 会调用默认构造函数,而嵌套的 {} 会调用 std::initializer_list 构造函数。这使得代码在处理默认构造和 std::initializer_list 构造时更加明确。

总结

综上我们可以看到:

  1. 使用 {} 初始化不仅提供了统一的语法。
  2. 能够防止隐式缩窄转换。
  3. 对解析问题具有免疫力。
  4. 支持非静态数据成员初始化。
  5. 在处理 std::initializer_list 构造函数时更加明确。

这些优点使得 {} 初始化成为一种强大的工具,帮助开发者编写更安全、更高效的代码。