C++列表初始化 和 函数初始化

列表初始化(List Initialization)是C++11引入的一种新的初始化方式,它使用大括号 {} 来包围初始化器的值。这种初始化方式提供了更高的灵活性和安全性,特别是在处理聚合类型(如数组、结构体和类)以及在某些需要避免函数声明歧义的情况下。

下面我将详细讲解列表初始化的几个关键方面,并通过例子来说明其用法。

1. 基本数据类型的初始化

对于基本数据类型(如整数、浮点数等),列表初始化与函数声明式初始化通常具有相同的效果。

int a(10);     // 函数声明式初始化  
int b{10};     // 列表初始化  
int c = {10};  // 带有等号的列表初始化,也是合法的

 

2. 数组的初始化

列表初始化特别适用于数组的初始化,因为它可以清晰地指定每个元素的值。

int arr1[3] = {1, 2, 3};     // 使用等号进行列表初始化  
int arr2[3]{4, 5, 6};        // 不使用等号也是合法的列表初始化  
  
// 部分初始化的数组,剩余元素会被初始化为0(对于内置类型)  
int arr3[5]{7, 8};           // arr3 为 {7, 8, 0, 0, 0}

 

3. 结构体和类的初始化

对于结构体和类,列表初始化可以按照成员的顺序来初始化它们的成员变量。

struct Point {  
    int x;  
    int y;  
};  
  
Point p1{1, 2};  // 列表初始化,p1.x = 1, p1.y = 2  
Point p2 = {3, 4}; // 带有等号的列表初始化,与 p1 效果相同  
  
// 如果结构体有构造函数,列表初始化也可以用来调用构造函数  
struct Person {  
    Person(const std::string& name, int age) : name(name), age(age) {}  
    std::string name;  
    int age;  
};  
  
Person person{"Alice", 30}; // 调用构造函数进行初始化

 

4. 列表初始化与函数重载的解析

列表初始化有助于解决函数重载时的歧义问题。当函数参数既可以解释为基本类型也可以解释为某种类类型时,使用列表初始化可以明确指定我们想要调用哪个重载版本。

void foo(int a);  
void foo(const std::initializer_list<int>& list);  
  
foo({10}); // 调用接受 initializer_list 的 foo 版本  
foo(10);   // 调用接受 int 的 foo 版本

 

5. 禁止某些类型的隐式转换

列表初始化还可以用来禁止某些类型的隐式转换,这有助于提高代码的安全性。

void bar(bool b);  
  
bar(0);         // 0 可以隐式转换为 bool,调用成功  
bar({0});       // 错误:不能从 initializer_list<int> 转换为 bool

 

注意事项

  • 当使用列表初始化时,如果初始化器的元素数量与聚合类型的成员数量不匹配,编译器可能会产生错误或进行值初始化(对于内置类型,通常是0初始化)。
  • 列表初始化不能用于所有类型的对象。例如,对于没有提供适当构造函数的类类型,或者对于某些特殊设计的类型,可能无法使用列表初始化。
  • 在某些情况下,列表初始化可能会比函数声明式初始化效率稍低,因为它涉及到构造函数的调用和可能的临时对象创建。但在大多数情况下,这种差异是可以忽略不计的。

列表初始化提供了一种强大而灵活的初始化机制,使得C++代码更加清晰、安全和易于理解。在编写C++代码时,建议尽量使用列表初始化来初始化对象,特别是当涉及到聚合类型或需要避免隐式转换时。

6. 列表初始化与std::initializer_list

当函数或类的构造函数接受一个std::initializer_list类型的参数时,可以使用列表初始化来调用这些函数或构造函数。std::initializer_list是一个模板类,它封装了初始化列表中元素的集合,使得我们可以在函数或构造函数内部以迭代的方式访问这些元素。

#include <iostream>  
#include <initializer_list>  
#include <vector>  
  
void printNumbers(const std::initializer_list<int>& numbers) {  
    for (int num : numbers) {  
        std::cout << num << ' ';  
    }  
    std::cout << std::endl;  
}  
  
int main() {  
    printNumbers({1, 2, 3, 4, 5}); // 使用列表初始化调用函数  
    return 0;  
}

 

在上面的例子中,printNumbers函数接受一个std::initializer_list<int>类型的参数,我们可以使用列表初始化{1, 2, 3, 4, 5}来调用它。

类似地,类的构造函数也可以接受std::initializer_list参数来方便地初始化对象。

class MyClass {  
public:  
    MyClass(const std::initializer_list<int>& values) {  
        for (int value : values) {  
            // 使用values初始化对象的状态...  
        }  
    }  
    // ... 其他成员函数 ...  
};  
  
int main() {  
    MyClass obj{10, 20, 30}; // 使用列表初始化创建对象  
    return 0;  
}

 

7. 列表初始化与默认初始化

当使用列表初始化但初始化器为空时(即{}),对于类类型,如果该类提供了默认构造函数,则会调用该默认构造函数进行初始化。对于聚合类型(如数组和结构体),则执行默认初始化。

class DefaultConstructorDemo {  
public:  
    DefaultConstructorDemo() {  
        std::cout << "Default constructor called" << std::endl;  
    }  
};  
  
int main() {  
    DefaultConstructorDemo dcd1{}; // 调用默认构造函数  
    DefaultConstructorDemo dcd2;   // 同样调用默认构造函数  
    return 0;  
}

 

对于内置类型,列表初始化会进行值初始化,这通常意味着它们会被初始化为0。

int x{}; // x 被初始化为 0

 

8. 列表初始化与拷贝和移动构造函数

列表初始化通常不会调用拷贝或移动构造函数,除非初始化器中的对象与要初始化的对象类型完全相同。在大多数情况下,列表初始化会导致调用构造函数(可能是接受std::initializer_list的构造函数),而不是拷贝或移动构造函数。

class MyType {  
public:  
    MyType(int value) : value_(value) {}  
    MyType(const MyType& other) { /* 拷贝构造函数的实现 */ }  
    MyType(MyType&& other) noexcept { /* 移动构造函数的实现 */ }  
    int value_;  
};  
  
int main() {  
    MyType a(10);  
    MyType b{a}; // 错误:不会调用拷贝构造函数,而是导致编译错误  
    MyType c = a; // 正确:调用拷贝构造函数  
    return 0;  
}

 

在上面的例子中,尝试使用列表初始化MyType b{a};来拷贝a对象会导致编译错误,因为列表初始化不会调用拷贝构造函数。相反,我们应该使用等号=来进行拷贝初始化。

总结

列表初始化是C++中一种强大且灵活的初始化机制,它提供了比传统函数声明式初始化更多的控制和安全性。通过使用大括号{},我们可以清晰地指定对象的初始状态,避免不必要的隐式类型转换,并且可以方便地初始化聚合类型和对象的集合。然而,也需要注意列表初始化的一些限制和与拷贝/移动构造函数的交互,以确保代码的正确性和效率。

 


是的,std::atomic<int> atomicInt(0); 可以改为 std::atomic<int> atomicInt{0};。这两种写法都是有效的,并且它们在这里具有相同的效果:都是初始化一个std::atomic<int>类型的对象atomicInt,并将其值设置为0。

这两种初始化语法分别是函数声明式初始化和列表初始化。

  • 函数声明式初始化 (std::atomic<int> atomicInt(0);):这是传统的初始化方式,它看起来类似于函数调用,因为它实际上是调用类的构造函数来初始化对象。

  • 列表初始化 (std::atomic<int> atomicInt{0};):这是C++11引入的一种新的初始化方式,它使用大括号{}来包围初始化器的值。列表初始化通常用于数组、聚合类型(如结构体和类)以及某些需要明确区分初始化与函数调用的场合。

对于std::atomic<int>这样的简单类型,两种初始化方式在功能上是等价的。然而,在某些情况下,列表初始化可能更有优势,比如当构造函数有多个重载,并且你希望明确指定使用哪一个时,或者当你想避免可能的函数声明歧义时。

在你给出的例子中,由于std::atomic<int>只有一个接受int参数的构造函数,因此使用函数声明式初始化和列表初始化没有区别。你可以根据编码风格或个人偏好选择使用哪一种。然而,如果代码库或团队有统一的编码规范,最好遵循该规范。

 

posted @ 2024-03-17 00:22  He_LiangLiang  阅读(18)  评论(0编辑  收藏  举报