MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++初始化成员列表的语法细节

2021-09-262.1k 阅读

C++初始化成员列表的基础语法

在C++中,初始化成员列表是一种用于初始化类的数据成员的特殊语法结构。其基本语法形式如下:

class MyClass {
    int member1;
    double member2;
public:
    MyClass(int value1, double value2) : member1(value1), member2(value2) {
        // 构造函数体
    }
};

在上述代码中,MyClass类有两个数据成员member1member2。构造函数MyClass(int value1, double value2)后面跟着一个冒号:,然后是用逗号分隔的数据成员初始化列表member1(value1), member2(value2)。这就是初始化成员列表的基本写法,通过这种方式,我们可以在进入构造函数体之前就初始化类的数据成员。

初始化顺序

初始化成员列表中成员的初始化顺序并非按照它们在初始化列表中的顺序,而是按照它们在类定义中声明的顺序。例如:

class InitOrder {
    int member2;
    int member1;
public:
    InitOrder(int value1, int value2) : member1(value1), member2(value2) {
        // 构造函数体
    }
};

在这个例子中,尽管member1在初始化列表中先出现,但实际上member2会先被初始化,因为member2在类定义中先声明。这种初始化顺序是由C++标准规定的,理解这一点非常重要,因为错误地依赖初始化列表中的顺序可能会导致难以调试的错误。

初始化不同类型的成员

  1. 基本数据类型:对于像intdoublechar等基本数据类型,初始化成员列表的使用非常直观,如上述MyClass类中对member1int类型)和member2double类型)的初始化。

  2. 类类型成员:当类包含其他类类型的成员时,初始化成员列表同样适用。假设我们有一个Point类:

class Point {
    int x;
    int y;
public:
    Point(int a, int b) : x(a), y(b) {}
};

class Shape {
    Point center;
public:
    Shape(int x, int y) : center(x, y) {
        // 构造函数体
    }
};

Shape类中,centerPoint类类型的成员。通过初始化成员列表center(x, y),我们调用了Point类的构造函数来初始化center

  1. 数组类型成员:初始化数组类型成员稍微复杂一些。例如:
class ArrayClass {
    int arr[3];
public:
    ArrayClass(int a, int b, int c) {
        arr[0] = a;
        arr[1] = b;
        arr[2] = c;
    }
};

在这种情况下,不能直接在初始化成员列表中像初始化单个变量那样初始化数组。通常需要在构造函数体中逐个赋值。不过,从C++11开始,对于聚合类型(如简单数组),可以使用聚合初始化:

class ArrayClass {
    int arr[3];
public:
    ArrayClass(int a, int b, int c) : arr{a, b, c} {}
};
  1. 常量成员:常量成员(const修饰的成员变量)必须在构造函数的初始化成员列表中初始化,因为一旦对象创建,常量的值就不能再改变。例如:
class ConstMember {
    const int value;
public:
    ConstMember(int v) : value(v) {
        // 构造函数体
    }
};

如果尝试在构造函数体中对常量成员赋值,会导致编译错误。

  1. 引用成员:引用成员也必须在初始化成员列表中初始化,因为引用一旦初始化就不能再绑定到其他对象。例如:
class RefMember {
    int& ref;
public:
    RefMember(int& num) : ref(num) {
        // 构造函数体
    }
};

初始化成员列表的性能考量

避免不必要的构造和析构

当类包含对象类型的成员时,使用初始化成员列表可以避免不必要的构造和析构操作。考虑以下代码:

class SubClass {
public:
    SubClass() {
        std::cout << "SubClass default constructor" << std::endl;
    }
    SubClass(const SubClass& other) {
        std::cout << "SubClass copy constructor" << std::endl;
    }
    ~SubClass() {
        std::cout << "SubClass destructor" << std::endl;
    }
};

class OuterClass {
    SubClass sub;
public:
    OuterClass() {
        sub = SubClass();
    }
};

OuterClass的构造函数中,先调用SubClass的默认构造函数创建一个临时对象,然后通过赋值操作将临时对象的值赋给sub,最后临时对象被析构。如果使用初始化成员列表:

class OuterClass {
    SubClass sub;
public:
    OuterClass() : sub() {
        // 构造函数体
    }
};

这样就直接调用SubClass的默认构造函数来初始化sub,避免了临时对象的构造和析构,从而提高了性能。

对于复杂对象的初始化优势

对于一些复杂的对象,比如包含动态内存分配的对象,初始化成员列表的性能优势更为明显。假设我们有一个BigObject类,它在构造函数中分配大量内存:

class BigObject {
    char* data;
public:
    BigObject(int size) {
        data = new char[size];
        std::cout << "BigObject constructor" << std::endl;
    }
    BigObject(const BigObject& other) {
        int size = strlen(other.data);
        data = new char[size + 1];
        strcpy(data, other.data);
        std::cout << "BigObject copy constructor" << std::endl;
    }
    ~BigObject() {
        delete[] data;
        std::cout << "BigObject destructor" << std::endl;
    }
};

class Container {
    BigObject obj;
public:
    Container(int size) {
        obj = BigObject(size);
    }
};

Container构造函数中,先默认构造obj,然后再通过赋值操作将BigObject(size)的结果赋给obj,这涉及到额外的构造和析构操作。使用初始化成员列表:

class Container {
    BigObject obj;
public:
    Container(int size) : obj(size) {
        // 构造函数体
    }
};

这样直接使用BigObject(size)来初始化obj,减少了不必要的开销,提升了性能。

初始化成员列表与构造函数体赋值的区别

执行时机

初始化成员列表在进入构造函数体之前执行,而构造函数体中的赋值操作是在进入构造函数体之后执行。例如:

class Timing {
    int value;
public:
    Timing(int v) : value(v) {
        std::cout << "In constructor body" << std::endl;
    }
};

在这个例子中,value在进入构造函数体输出In constructor body之前就已经被初始化。如果是在构造函数体中赋值:

class Timing {
    int value;
public:
    Timing(int v) {
        value = v;
        std::cout << "In constructor body" << std::endl;
    }
};

value先被默认初始化(如果类型有默认初始化行为),然后在构造函数体中被赋值为v

初始化方式

初始化成员列表是真正意义上的初始化,而构造函数体中的赋值是先初始化(如果有默认初始化)再赋值。对于像const成员和引用成员,只能在初始化成员列表中初始化,因为它们不能被赋值。例如:

class IllegalAssignment {
    const int num;
    int& ref;
public:
    IllegalAssignment(int n, int& r) {
        num = n; // 错误,const成员不能赋值
        ref = r; // 错误,引用成员不能重新赋值
    }
};

正确的方式是使用初始化成员列表:

class LegalInitialization {
    const int num;
    int& ref;
public:
    LegalInitialization(int n, int& r) : num(n), ref(r) {
        // 构造函数体
    }
};

性能差异

如前文所述,初始化成员列表通常比构造函数体赋值性能更好,尤其是对于对象类型的成员。因为初始化成员列表避免了额外的构造和析构操作,而构造函数体赋值可能会导致临时对象的创建和销毁。

初始化成员列表中的函数调用

调用普通成员函数

在初始化成员列表中可以调用类的普通成员函数来初始化成员变量。例如:

class FuncCallInInitList {
    int member;
    int calculateValue() {
        return 42;
    }
public:
    FuncCallInInitList() : member(calculateValue()) {
        // 构造函数体
    }
};

在这个例子中,member通过调用calculateValue函数来初始化。需要注意的是,在调用成员函数时,要确保函数不会依赖未初始化的成员变量,否则可能会导致未定义行为。

调用静态成员函数

也可以在初始化成员列表中调用静态成员函数。静态成员函数不依赖于对象的状态,因此可以安全地在初始化成员列表中调用。例如:

class StaticFuncCall {
    int member;
    static int getStaticValue() {
        return 100;
    }
public:
    StaticFuncCall() : member(getStaticValue()) {
        // 构造函数体
    }
};

调用基类的构造函数

当一个类继承自另一个类时,初始化成员列表用于调用基类的构造函数。例如:

class Base {
    int baseValue;
public:
    Base(int v) : baseValue(v) {}
};

class Derived : public Base {
    int derivedValue;
public:
    Derived(int baseV, int derivedV) : Base(baseV), derivedValue(derivedV) {
        // 构造函数体
    }
};

Derived类的构造函数中,通过初始化成员列表Base(baseV)调用了Base类的构造函数,然后初始化derivedValue。如果不指定调用哪个基类构造函数,编译器会尝试调用基类的默认构造函数,如果基类没有默认构造函数,会导致编译错误。

初始化成员列表的特殊情况

委托构造函数中的初始化成员列表

C++11引入了委托构造函数,即一个构造函数可以调用同一个类的其他构造函数。在委托构造函数中,初始化成员列表有特殊的行为。例如:

class DelegatingConstructor {
    int value1;
    int value2;
public:
    DelegatingConstructor(int v1) : value1(v1), value2(0) {
        // 构造函数体
    }
    DelegatingConstructor(int v1, int v2) : DelegatingConstructor(v1) {
        value2 = v2;
    }
};

DelegatingConstructor(int v1, int v2)构造函数中,通过DelegatingConstructor(v1)委托给了DelegatingConstructor(int v1)构造函数。注意,在委托构造函数中,初始化成员列表只能用于委托调用,不能再对其他成员进行初始化。在上述例子中,value2在委托构造函数体中赋值,而不能在初始化成员列表中再次初始化value2

初始化列表与模板类

在模板类中使用初始化成员列表的语法与普通类基本相同,但需要注意模板参数的类型。例如:

template <typename T>
class TemplateClass {
    T member;
public:
    TemplateClass(const T& value) : member(value) {
        // 构造函数体
    }
};

在这个模板类TemplateClass中,根据模板参数T的类型,member通过初始化成员列表member(value)进行初始化。无论T是基本类型还是类类型,初始化成员列表的语法都适用。

初始化列表与多重继承

当一个类从多个基类继承时,初始化成员列表需要依次初始化每个基类。例如:

class Base1 {
    int value1;
public:
    Base1(int v) : value1(v) {}
};

class Base2 {
    int value2;
public:
    Base2(int v) : value2(v) {}
};

class Derived : public Base1, public Base2 {
    int derivedValue;
public:
    Derived(int v1, int v2, int dV) : Base1(v1), Base2(v2), derivedValue(dV) {
        // 构造函数体
    }
};

Derived类的构造函数中,先通过Base1(v1)初始化Base1基类,再通过Base2(v2)初始化Base2基类,最后初始化derivedValue。基类的初始化顺序与它们在继承列表中的顺序一致,而不是按照初始化成员列表中的顺序。

初始化成员列表的常见错误

未初始化引用或常量成员

忘记在初始化成员列表中初始化引用或常量成员是常见错误。如前文所述,引用和常量成员必须在初始化成员列表中初始化。例如:

class UninitializedRef {
    int& ref;
public:
    UninitializedRef(int num) {
        ref = num; // 错误,引用必须在初始化成员列表中初始化
    }
};

初始化顺序错误导致的问题

依赖初始化成员列表中的顺序而不是类定义中的声明顺序可能会导致错误。例如:

class InitOrderError {
    int member1;
    int member2;
public:
    InitOrderError(int v1, int v2) : member2(v2), member1(member2 + v1) {
        // 错误,member2可能未初始化
    }
};

由于member1在类定义中先声明,实际初始化时member1会先于member2被初始化,此时member2的值是未定义的,导致member1的初始化结果也不正确。

对数组成员的错误初始化

尝试在初始化成员列表中像初始化单个变量那样初始化数组是错误的。例如:

class ArrayInitError {
    int arr[3];
public:
    ArrayInitError(int a, int b, int c) : arr(a, b, c) { // 错误
        // 构造函数体
    }
};

正确的做法是在构造函数体中逐个赋值或使用C++11的聚合初始化。

总结初始化成员列表的最佳实践

  1. 始终使用初始化成员列表:除非有特殊原因,对于所有数据成员都应优先使用初始化成员列表,尤其是对于对象类型、const成员和引用成员。这不仅可以提高性能,还能避免一些编译错误。
  2. 注意初始化顺序:确保理解并遵循类定义中成员声明的顺序来初始化成员,避免因依赖初始化列表顺序而导致的错误。
  3. 避免复杂的初始化逻辑:在初始化成员列表中尽量避免复杂的函数调用,特别是那些可能依赖未初始化成员的调用。如果需要复杂逻辑,可以考虑将其封装在一个独立的函数中,并在构造函数体中调用该函数。
  4. 在委托构造函数中遵循规则:在委托构造函数中,正确使用初始化成员列表进行委托调用,不要在委托构造函数的初始化成员列表中对已在被委托构造函数中初始化的成员再次初始化。

通过遵循这些最佳实践,可以更好地利用初始化成员列表,编写出高效、健壮的C++代码。初始化成员列表作为C++构造函数的重要组成部分,深入理解其语法细节和使用技巧对于编写高质量的C++程序至关重要。无论是小型项目还是大型系统,正确使用初始化成员列表都能在性能、可读性和可维护性方面带来显著的提升。在实际编程中,不断积累经验,根据具体的应用场景合理运用初始化成员列表,将有助于提升代码的质量和效率。同时,随着C++标准的不断演进,初始化成员列表的使用场景和规则也可能会有一些细微的变化,开发者需要持续关注并学习最新的标准规范,以保持代码的先进性和兼容性。