C++中的初始化列表

1.什么是初始化列表?

在C++中,初始化列表(Initializer List)是一种特殊的构造函数语法,用于在创建对象时初始化类的成员变量。初始化列表在构造函数的参数列表之后,使用冒号(:)和逗号分隔的列表来指定成员变量的初始值

初始化列表主要用于以下情况:

  1. 初始化常量成员变量:常量成员变量必须在声明时就进行初始化,并且一旦初始化后就不能再修改。使用初始化列表可以确保常量成员变量在构造函数执行之前就被正确初始化。
  2. 初始化引用成员变量:引用成员变量也必须在声明时就进行初始化,并且不能重新赋值。使用初始化列表可以确保引用成员变量在构造函数执行之前就被正确绑定到一个对象或变量上。
  3. 初始化对象成员变量:当类中包含其他类的对象作为成员变量时,可以使用初始化列表来调用这些对象类的构造函数,以确保它们被正确初始化。
  4. 性能优化:对于某些类型(如内置类型、POD类型等),使用初始化列表可能比在构造函数体内赋值更加高效。因为初始化列表直接在构造函数执行之前进行成员变量的初始化,避免了额外的拷贝或赋值操作。

下面是一个简单的示例,展示了如何使用初始化列表:

#include <iostream>  
#include <string>  
  
class Person {  
public:  
    Person(const std::string& name, int age) : m_name(name), m_age(age) {}  
      
    void display() const {  
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;  
    }  
      
private:  
    std::string m_name;  
    int m_age;  
};  
  
int main() {  
    Person person("Alice", 30);  
    person.display();  // 输出:Name: Alice, Age: 30  
    return 0;  
}

在上面的示例中,Person 类有两个成员变量 m_name 和 m_age。构造函数使用初始化列表来初始化这两个成员变量。当创建一个 Person 对象时,构造函数会调用初始化列表来设置 m_name 和 m_age 的初始值。然后,通过调用 display() 成员函数来显示这些值。

 

2.成员变量的初始值为()是什么原因

在C++的初始化列表中,如果成员变量的初始值被设置为空括号(),这通常意味着使用了该类型的默认构造函数来初始化该成员。对于内置类型(如intfloatchar等),这样的语法是不正确的,因为内置类型没有默认构造函数。但是,对于类类型(即用户定义的类型或标准库中的类型),这样的语法是有效的

例如,考虑以下类定义:

class MyClass {  
public:  
    MyClass() { /* 默认构造函数 */ }  
    // ... 其他成员函数和数据成员 ...  
};  
  
class ContainingClass {  
public:  
    ContainingClass() : myMember() {} // 使用MyClass的默认构造函数初始化myMember  
  
private:  
    MyClass myMember;  
};

在这个例子中,ContainingClass 的初始化列表中的 myMember() 表示使用 MyClass 的默认构造函数来初始化 myMember 成员变量。如果 MyClass 没有默认构造函数(即没有提供不接受任何参数的构造函数),那么这样的初始化将会导致编译错误。

然而,对于内置类型,你不能使用空括号。例如,以下代码是无效的:

class InvalidClass {  
public:  
    InvalidClass() : myInt() {} // 错误:内置类型int没有默认构造函数  
  
private:  
    int myInt;  
};

对于内置类型,你需要在初始化列表中提供一个初始值,如 myInt(0) 来将 myInt 初始化为0,或者你可以完全省略它,让它在构造函数体内部被初始化(尽管这不是最佳实践)。

如果你在初始化列表中看到了空括号,并且该成员是内置类型,那么这很可能是一个错误。但是,对于类类型成员,空括号表示调用该类型的默认构造函数。如果类类型的默认构造函数被删除或不可访问,那么这样的初始化也会导致编译错误。

 

3.成员变量的初始值为{}是什么原因,和()对比有什么区别?

在C++中,成员变量的初始值为{}(花括号)表示使用了列表初始化(也称为统一初始化或花括号初始化)。这与使用()(圆括号)进行初始化有所不同,尤其是在处理不同类型和构造函数重载时。

使用{}(花括号)进行初始化

  • 列表初始化:对于内置类型,{}内部没有值时,会进行值初始化,即内置类型会被初始化为零(对于intfloat等)或相应的“空”值(对于charbool等)。对于类类型,它会尝试调用该类的默认构造函数(如果存在)。
  • 无歧义:当存在多个构造函数或初始化选项时,{}通常能更清晰地表达初始化意图,因为它会选择最合适的构造函数或初始化方法,而不会与函数调用产生歧义。
  • 防止窄化转换:使用{}初始化可以防止不安全的窄化转换(narrowing conversion),这是C++11引入的一个新特性,用于提高类型安全。

使用()(圆括号)进行初始化

  • 直接初始化:对于内置类型,()内部的值会直接用于初始化。对于类类型,它会调用相应的构造函数。但是,对于内置类型,你不能只写()而不提供值,这是非法的。
  • 可能存在歧义:当存在多个构造函数或初始化选项时,使用()可能会产生歧义,因为编译器可能会将其解释为函数调用而不是初始化。
  • 允许窄化转换:与{}不同,使用()进行初始化时允许窄化转换,这可能会导致数据丢失或类型不安全的情况。

结论:初始化列表中推荐直接使用{}进行初始化,因为可以避免内置类型()初始化的错误。

示例:

#include <iostream>  
#include <vector>  
  
class MyClass {  
public:  
    MyClass() { std::cout << "Default constructor called" << std::endl; }  
    explicit MyClass(int) { std::cout << "Int constructor called" << std::endl; }  
};  
  
int main() {  
    // 使用{}进行初始化  
    MyClass obj1{}; // 调用默认构造函数  
    // MyClass obj2{42}; // 调用接受int的构造函数 (如果它是非explicit的)  
  
    // 使用()进行初始化  
    MyClass obj3(); // 错误!这被解析为函数声明,而不是对象初始化  
    MyClass obj4(42); // 调用接受int的构造函数(需要explicit关键字来阻止直接初始化)  
  
    // 对于内置类型  
    int a{}; // a被初始化为0  
    int b(); // 错误!这不是有效的初始化方式  
    int c(42); // c被初始化为42,但这种形式在C++中较少见,更常见的是int c = 42;  
  
    return 0;  
}

注意:在上面的代码中,MyClass obj3();实际上是一个函数声明,而不是一个对象定义。这是C++语法中的一个陷阱。要使用圆括号进行初始化,你必须确保它不会被误解为函数声明。另外,如果构造函数被声明为explicit,则不能使用{}以外的形式进行隐式转换初始化(如MyClass obj2{42};会失败,除非去掉explicit关键字)。但是上面的注释有误,实际上如果构造函数不是explicit的,那么MyClass obj2{42};是合法的,并且会调用接受int的构造函数。如果构造函数是explicit的,那么这样的初始化将是非法的,因为列表初始化不允许通过explicit构造函数进行隐式转换。

总的来说,使用{}进行初始化通常更安全、更清晰,并且在处理复杂类型和重载构造函数时更加灵活。然而,在某些情况下,你可能仍然需要使用()来明确地调用特定的构造函数或进行其他类型的初始化。

 

4.初始化列表中class类型如何调用带参数的构造函数?

在C++中,初始化列表允许调用类的带参数的构造函数来初始化成员变量。你可以在初始化列表中指定构造函数及其参数,以确保成员对象在包含它的对象创建时被正确初始化

下面是一个示例,展示了如何在初始化列表中调用带参数的构造函数:

#include <iostream>  
#include <string>  
  
class MemberClass {  
public:  
    MemberClass(int value) : m_value(value) {  
        std::cout << "MemberClass constructor called with value: " << m_value << std::endl;  
    }  
  
    int getValue() const { return m_value; }  
  
private:  
    int m_value;  
};  
  
class ContainingClass {  
public:  
    ContainingClass(int memberValue) : m_member(memberValue) {  
        std::cout << "ContainingClass constructor called." << std::endl;  
    }  
  
    void displayMemberValue() const {  
        std::cout << "Member value in ContainingClass: " << m_member.getValue() << std::endl;  
    }  
  
private:  
    MemberClass m_member; // MemberClass类型的成员变量  
};  
  
int main() {  
    ContainingClass obj(42); // 创建ContainingClass对象,同时初始化MemberClass成员变量  
    obj.displayMemberValue(); // 显示MemberClass成员变量的值  
    return 0;  
}

 

5.构造函数=default是什么意思?

在C++中,= default是一个显式默认指令,它告诉编译器为某个特殊的成员函数(如构造函数、拷贝构造函数、移动构造函数、拷贝赋值操作符、移动赋值操作符或析构函数)生成默认的实现。这在你想要一个类具有某种默认行为,但又不想完全依赖编译器的隐式生成时非常有用

例如,假设你有一个类,你希望它有一个默认构造函数,但你也想明确地指出这一点:

class MyClass {  
public:  
    MyClass() = default; // 显式默认构造函数  
    // ... 其他成员函数和数据成员 ...  
};

这里,MyClass() = default;显式地要求编译器为MyClass生成一个默认构造函数。这实际上与完全省略这个构造函数并让编译器隐式地为你生成一个默认构造函数有相同的效果,但使用= default可以使得你的意图更加明确。

类似地,你也可以使用= delete来禁止某个成员函数的生成

class NonCopyable {  
public:  
    NonCopyable(const NonCopyable&) = delete; // 禁止拷贝构造函数  
    NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值操作符  
    // ... 其他成员函数和数据成员 ...  
};

在这个例子中,NonCopyable类不能被拷贝,因为拷贝构造函数和拷贝赋值操作符都被显式地删除了。任何尝试拷贝NonCopyable对象的代码都会导致编译错误

posted @ 2024-03-18 15:33  青山牧云人  阅读(5039)  评论(2)    收藏  举报