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

C++构造函数的初始化列表使用

2023-11-292.5k 阅读

C++构造函数的初始化列表使用

初始化列表的基本概念

在C++中,当定义一个类时,构造函数用于初始化类的对象。初始化列表是构造函数中一种特殊的语法结构,用于在进入构造函数体之前对类的数据成员进行初始化。它以冒号(:)开始,后面跟着一系列用逗号分隔的数据成员初始化表达式。

例如,考虑一个简单的Point类:

class Point {
private:
    int x;
    int y;
public:
    // 使用初始化列表的构造函数
    Point(int a, int b) : x(a), y(b) {
        // 构造函数体可以为空,因为初始化已经在初始化列表中完成
    }
};

在上述代码中,Point类有两个数据成员xy。构造函数Point(int a, int b)使用初始化列表x(a), y(b)xy进行初始化。在进入构造函数体之前,x被初始化为ay被初始化为b

初始化列表的执行顺序

初始化列表中数据成员的初始化顺序与它们在类定义中的声明顺序一致,而不是按照初始化列表中出现的顺序。

看下面这个例子:

class Example {
private:
    int a;
    int b;
public:
    // 初始化列表顺序与声明顺序不同
    Example(int value) : b(value), a(b + 1) {
        // 虽然在初始化列表中b先初始化,a后初始化
        // 但实际初始化顺序是按照声明顺序,先a后b
    }
    void print() {
        std::cout << "a: " << a << ", b: " << b << std::endl;
    }
};

如果我们这样使用:

int main() {
    Example ex(5);
    ex.print();
    return 0;
}

输出结果可能会出乎一些人的意料。由于a先于b声明,所以先初始化a,此时b还未初始化,a(b + 1)中的b是未定义的值。因此,为了避免这种混乱,最好让初始化列表的顺序与声明顺序保持一致。

为什么使用初始化列表

  1. 性能优势:对于一些类型,使用初始化列表可以提高性能。例如,对于常量成员、引用成员以及没有默认构造函数的类类型成员,必须使用初始化列表进行初始化。如果不使用初始化列表,编译器会先调用默认构造函数创建临时对象,然后再使用赋值操作符进行赋值,这会增加额外的开销。

考虑下面这个类:

class NoDefaultConstructor {
public:
    NoDefaultConstructor(int value) {
        std::cout << "Constructing with value: " << value << std::endl;
    }
};

class UsingNoDefaultConstructor {
private:
    NoDefaultConstructor obj;
public:
    // 使用初始化列表
    UsingNoDefaultConstructor() : obj(5) {
        std::cout << "UsingNoDefaultConstructor constructed" << std::endl;
    }
};

如果不使用初始化列表,编译器会尝试先调用NoDefaultConstructor的默认构造函数(但该类没有默认构造函数),这会导致编译错误。即使有默认构造函数,使用初始化列表也可以避免不必要的默认构造和赋值操作。

  1. 初始化常量和引用成员:常量成员一旦初始化后就不能再修改,引用成员必须在定义时初始化。因此,它们只能在初始化列表中进行初始化。
class ConstantAndReference {
private:
    const int constantValue;
    int& referenceValue;
public:
    ConstantAndReference(int value, int& ref) : constantValue(value), referenceValue(ref) {
        // 这里不能再对constantValue和referenceValue进行赋值操作
    }
};

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

构造函数体赋值是在进入构造函数体后对数据成员进行赋值操作,而初始化列表是在进入构造函数体之前进行初始化。

class AssignmentVsInitialization {
private:
    int data;
public:
    // 构造函数体赋值
    AssignmentVsInitialization(int value) {
        data = value;
    }
    // 使用初始化列表
    AssignmentVsInitialization(int value) : data(value) {
        // 初始化在进入构造函数体之前完成
    }
};

从表面上看,这两种方式似乎达到了相同的效果,但在性能和一些特殊情况下有明显的区别。如前面提到的,对于一些类型,构造函数体赋值可能会导致额外的构造和赋值操作。而且对于常量和引用成员,构造函数体赋值根本不可行。

初始化列表在继承中的应用

  1. 调用基类构造函数:在派生类的构造函数中,必须使用初始化列表来调用基类的构造函数。如果不显示调用,编译器会调用基类的默认构造函数。
class Base {
public:
    Base(int value) {
        std::cout << "Base constructed with value: " << value << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int baseValue) : Base(baseValue) {
        std::cout << "Derived constructed" << std::endl;
    }
};

在上述代码中,Derived类继承自Base类。Derived类的构造函数使用初始化列表Base(baseValue)调用Base类的构造函数。

  1. 初始化基类的常量和引用成员:如果基类有常量或引用成员,派生类在初始化这些成员时也需要通过初始化列表调用基类构造函数。
class BaseWithConstantAndReference {
private:
    const int constant;
    int& reference;
public:
    BaseWithConstantAndReference(int value, int& ref) : constant(value), reference(ref) {
        std::cout << "BaseWithConstantAndReference constructed" << std::endl;
    }
};

class DerivedFromBaseWithConstantAndReference : public BaseWithConstantAndReference {
public:
    DerivedFromBaseWithConstantAndReference(int value, int& ref) : BaseWithConstantAndReference(value, ref) {
        std::cout << "DerivedFromBaseWithConstantAndReference constructed" << std::endl;
    }
};

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

  1. 调用成员函数进行初始化:在初始化列表中可以调用成员函数来获取初始化值。但需要注意的是,这些成员函数应该是const成员函数,以确保在对象初始化过程中不会修改对象的状态。
class InitializationWithFunctionCall {
private:
    int result;
    int calculateValue() const {
        return 10 + 5;
    }
public:
    InitializationWithFunctionCall() : result(calculateValue()) {
        std::cout << "InitializationWithFunctionCall constructed" << std::endl;
    }
    void printResult() {
        std::cout << "Result: " << result << std::endl;
    }
};

在上述代码中,InitializationWithFunctionCall类的构造函数使用初始化列表调用calculateValue成员函数来初始化result

  1. 避免在初始化列表中调用虚函数:在对象初始化期间,虚函数机制尚未完全建立。如果在初始化列表中调用虚函数,可能会导致未定义行为。
class BaseForVirtualCall {
public:
    BaseForVirtualCall() {
        virtualFunction();
    }
    virtual void virtualFunction() {
        std::cout << "Base virtualFunction" << std::endl;
    }
};

class DerivedForVirtualCall : public BaseForVirtualCall {
public:
    DerivedForVirtualCall() {
        // 这里调用的是Base类的virtualFunction,因为初始化期间虚函数机制未完全建立
    }
    void virtualFunction() override {
        std::cout << "Derived virtualFunction" << std::endl;
    }
};

在上述代码中,BaseForVirtualCall类的构造函数调用了虚函数virtualFunction。当创建DerivedForVirtualCall对象时,在BaseForVirtualCall构造期间调用的virtualFunction仍然是BaseForVirtualCall类的版本,而不是DerivedForVirtualCall类重写的版本,这可能会导致逻辑错误。

复杂类型的初始化列表使用

  1. 数组成员的初始化:对于类中的数组成员,可以在初始化列表中使用聚合初始化的方式进行初始化。
class ArrayInitialization {
private:
    int numbers[3];
public:
    ArrayInitialization() : numbers{1, 2, 3} {
        std::cout << "ArrayInitialization constructed" << std::endl;
    }
    void printArray() {
        for (int i = 0; i < 3; ++i) {
            std::cout << "numbers[" << i << "]: " << numbers[i] << std::endl;
        }
    }
};

在上述代码中,ArrayInitialization类的构造函数使用初始化列表对numbers数组进行初始化。

  1. 容器成员的初始化:对于标准库容器成员,如std::vectorstd::list等,可以在初始化列表中使用相应的构造函数进行初始化。
#include <vector>
class VectorInitialization {
private:
    std::vector<int> values;
public:
    VectorInitialization() : values{1, 2, 3, 4, 5} {
        std::cout << "VectorInitialization constructed" << std::endl;
    }
    void printVector() {
        for (int value : values) {
            std::cout << "Value: " << value << std::endl;
        }
    }
};

这里VectorInitialization类的构造函数使用初始化列表初始化std::vector<int>成员values

初始化列表的常见错误

  1. 忘记初始化成员:如果在初始化列表中遗漏了某个成员的初始化,且该成员没有默认构造函数,会导致编译错误。
class ForgottenInitialization {
private:
    int data;
public:
    ForgottenInitialization() {
        // 这里没有在初始化列表中初始化data,且没有默认构造函数
    }
};

上述代码会因为data未初始化而无法编译通过。

  1. 错误的初始化顺序:正如前面提到的,不按照声明顺序初始化可能会导致未定义行为,特别是当一个成员的初始化依赖于另一个成员时。

  2. 在初始化列表中修改对象状态:初始化列表应该只用于初始化数据成员,不应该在其中执行修改对象状态的操作,因为此时对象可能还未完全初始化。

class IllegalInitialization {
private:
    int value;
public:
    IllegalInitialization(int initialValue) : value(initialValue) {
        // 这里在初始化列表后再修改value是可以的
    }
    IllegalInitialization(int initialValue) : value(++initialValue) {
        // 虽然这种操作看似合理,但在初始化列表中修改initialValue可能会引起混淆
    }
};

初始化列表与默认构造函数的关系

  1. 默认构造函数与初始化列表:如果一个类有默认构造函数,且构造函数体为空,那么使用初始化列表和不使用初始化列表的效果基本相同。
class DefaultConstructorExample {
private:
    int data;
public:
    // 不使用初始化列表的默认构造函数
    DefaultConstructorExample() {
        data = 0;
    }
    // 使用初始化列表的默认构造函数
    DefaultConstructorExample() : data(0) {
        // 与上面效果相同
    }
};
  1. 合成默认构造函数与初始化列表:如果一个类没有定义任何构造函数,编译器会合成一个默认构造函数。这个合成的默认构造函数会调用成员对象的默认构造函数,但不会对内置类型成员进行初始化。如果类中有需要初始化的内置类型成员,就需要定义构造函数并使用初始化列表。
class NeedInitialization {
private:
    int value;
public:
    // 如果不定义构造函数,value将是未初始化的
    NeedInitialization() : value(0) {
        // 使用初始化列表初始化value
    }
};

初始化列表在模板类中的应用

  1. 模板类的构造函数初始化列表:在模板类中,同样可以使用初始化列表来初始化数据成员。
template <typename T>
class TemplateClass {
private:
    T data;
public:
    TemplateClass(const T& value) : data(value) {
        std::cout << "TemplateClass constructed" << std::endl;
    }
};

在上述模板类TemplateClass中,构造函数使用初始化列表初始化data成员。

  1. 模板类继承中的初始化列表:当模板类继承自其他类(包括模板类)时,在派生模板类的构造函数中也需要使用初始化列表调用基类构造函数。
template <typename T>
class BaseTemplate {
public:
    BaseTemplate(const T& value) {
        std::cout << "BaseTemplate constructed with value: " << value << std::endl;
    }
};

template <typename T>
class DerivedTemplate : public BaseTemplate<T> {
public:
    DerivedTemplate(const T& value) : BaseTemplate<T>(value) {
        std::cout << "DerivedTemplate constructed" << std::endl;
    }
};

这里DerivedTemplate模板类继承自BaseTemplate模板类,其构造函数使用初始化列表调用BaseTemplate的构造函数。

通过深入了解C++构造函数的初始化列表的使用,我们可以更好地控制对象的初始化过程,提高程序的性能和可维护性。无论是简单的类还是复杂的继承体系和模板类,初始化列表都是一个强大而重要的工具。在实际编程中,合理地使用初始化列表可以避免许多潜在的错误和性能问题。