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

C++类成员初始化的默认值设置

2024-01-294.3k 阅读

C++类成员初始化的默认值设置基础概念

成员变量默认初始化规则

在C++ 中,类的成员变量如果没有显式初始化,其初始化行为取决于变量的类型。对于内置类型(如整数、浮点数、指针等),如果是局部变量(在函数内部定义),它们不会被自动初始化,其值是未定义的。例如:

class MyClass {
    int num;
public:
    void printNum() {
        std::cout << num << std::endl;
    }
};

在上述代码中,MyClass 类的 num 成员变量是局部变量且未初始化。如果在 printNum 函数中直接使用 num,程序会产生未定义行为。

然而,如果成员变量是类的静态成员变量或者全局变量,内置类型会被初始化为 0(对于指针则初始化为空指针)。例如:

class MyGlobalClass {
    static int globalNum;
public:
    static void printGlobalNum() {
        std::cout << globalNum << std::endl;
    }
};
int MyGlobalClass::globalNum;

这里 MyGlobalClassglobalNum 是静态成员变量,虽然没有显式初始化,但它会被初始化为 0。当调用 printGlobalNum 时,会输出 0。

对于用户自定义类型(即类类型)的成员变量,如果没有显式初始化,会调用其默认构造函数进行初始化。例如:

class SubClass {
public:
    SubClass() {
        std::cout << "SubClass default constructor" << std::endl;
    }
};
class MainClass {
    SubClass sub;
public:
    MainClass() {
        std::cout << "MainClass constructor" << std::endl;
    }
};

在创建 MainClass 对象时,会先调用 SubClass 的默认构造函数,然后再调用 MainClass 的构造函数。

显式初始化成员变量的方式

  1. 在构造函数初始化列表中初始化:这是最常见的初始化成员变量的方式。构造函数初始化列表在构造函数参数列表之后,以冒号开始,多个初始化项之间用逗号分隔。例如:
class Point {
    int x;
    int y;
public:
    Point(int a, int b) : x(a), y(b) {
        std::cout << "Point constructor" << std::endl;
    }
};

Point 类的构造函数中,通过初始化列表将 x 初始化为 ay 初始化为 b。这种方式效率较高,尤其是对于用户自定义类型的成员变量,因为它避免了先默认构造再赋值的过程。

  1. 在构造函数体中赋值:也可以在构造函数体中对成员变量进行赋值,但这与在初始化列表中初始化有所不同。例如:
class AnotherPoint {
    int x;
    int y;
public:
    AnotherPoint(int a, int b) {
        x = a;
        y = b;
        std::cout << "AnotherPoint constructor" << std::endl;
    }
};

在这个例子中,xy 首先会进行默认初始化(对于内置类型,局部变量默认初始化是未定义行为,但这里可以理解为未初始化),然后在构造函数体中进行赋值。对于用户自定义类型的成员变量,会先调用默认构造函数,然后再调用赋值运算符进行赋值,这比在初始化列表中直接初始化效率低。

设置默认值的不同场景

构造函数参数的默认值

在C++ 中,可以为构造函数的参数设置默认值。这样,在创建对象时,如果没有提供相应的实参,就会使用默认值。例如:

class Circle {
    double radius;
public:
    Circle(double r = 1.0) : radius(r) {
        std::cout << "Circle constructor" << std::endl;
    }
    double getArea() {
        return 3.14159 * radius * radius;
    }
};

Circle 类的构造函数中,参数 r 有一个默认值 1.0。因此,以下两种创建 Circle 对象的方式都是合法的:

Circle c1; // 使用默认半径 1.0
Circle c2(2.0); // 使用指定半径 2.0

这种方式为对象创建提供了灵活性,同时也为成员变量 radius 设置了默认的初始值。

成员变量的默认成员初始化器

从 C++11 开始,可以为类的成员变量提供默认成员初始化器。这意味着可以在类定义时直接为成员变量指定初始值。例如:

class Rectangle {
    int width = 10;
    int height = 5;
public:
    Rectangle() {
        std::cout << "Rectangle constructor" << std::endl;
    }
    int getArea() {
        return width * height;
    }
};

Rectangle 类中,width 初始化为 10height 初始化为 5。即使构造函数没有显式初始化这些成员变量,它们也会有默认值。当使用默认构造函数创建 Rectangle 对象时,widthheight 已经有了初始值。

静态成员变量的默认值设置

静态成员变量属于类,而不是类的对象。对于静态成员变量,需要在类定义之外进行初始化。如果要设置默认值,也在这个外部初始化处进行。例如:

class Counter {
    static int count;
public:
    Counter() {
        count++;
    }
    static int getCount() {
        return count;
    }
};
int Counter::count = 0;

Counter 类中,count 是静态成员变量,它在类定义之外被初始化为 0。每次创建 Counter 对象时,count 会自增。通过 getCount 函数可以获取当前的计数。

初始化顺序和默认值设置的关系

成员变量初始化顺序

类成员变量的初始化顺序取决于它们在类定义中的声明顺序,而不是它们在构造函数初始化列表中的顺序。例如:

class InitOrder {
    int a;
    int b;
public:
    InitOrder(int value) : b(value), a(b + 1) {
        std::cout << "InitOrder constructor" << std::endl;
    }
    void printValues() {
        std::cout << "a: " << a << ", b: " << b << std::endl;
    }
};

InitOrder 类中,尽管在构造函数初始化列表中 b 先于 a 初始化,但由于 a 在类定义中先声明,所以 a 会先被初始化。在这种情况下,a 初始化时 b 还未被初始化,所以 a 的值是未定义的。正确的做法是按照声明顺序初始化成员变量:

class CorrectInitOrder {
    int a;
    int b;
public:
    CorrectInitOrder(int value) : a(value + 1), b(value) {
        std::cout << "CorrectInitOrder constructor" << std::endl;
    }
    void printValues() {
        std::cout << "a: " << a << ", b: " << b << std::endl;
    }
};

默认值设置对初始化顺序的影响

当使用默认成员初始化器时,这些初始化操作会在构造函数初始化列表之前执行。例如:

class DefaultInitOrder {
    int a = 10;
    int b;
public:
    DefaultInitOrder(int value) : b(value) {
        std::cout << "DefaultInitOrder constructor" << std::endl;
    }
    void printValues() {
        std::cout << "a: " << a << ", b: " << b << std::endl;
    }
};

DefaultInitOrder 类中,a 使用默认成员初始化器初始化为 10,这个初始化操作在构造函数初始化列表初始化 b 之前执行。

复杂类型成员变量的默认值设置

数组类型成员变量

对于数组类型的成员变量,可以在初始化列表中或者通过默认成员初始化器进行初始化。例如:

class ArrayClass {
    int arr[3];
public:
    ArrayClass() : arr{1, 2, 3} {
        std::cout << "ArrayClass constructor" << std::endl;
    }
    void printArray() {
        for (int i = 0; i < 3; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }
};

ArrayClass 类中,通过构造函数初始化列表对 arr 数组进行初始化。也可以使用默认成员初始化器:

class AnotherArrayClass {
    int arr[3] = {4, 5, 6};
public:
    AnotherArrayClass() {
        std::cout << "AnotherArrayClass constructor" << std::endl;
    }
    void printArray() {
        for (int i = 0; i < 3; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }
};

指针类型成员变量

指针类型成员变量的默认值设置需要谨慎。如果是指向内置类型的指针,通常需要初始化为空指针或者指向动态分配的内存。例如:

class PointerClass {
    int *ptr;
public:
    PointerClass() : ptr(nullptr) {
        std::cout << "PointerClass constructor" << std::endl;
    }
    ~PointerClass() {
        if (ptr) {
            delete ptr;
        }
    }
    void setValue(int value) {
        if (!ptr) {
            ptr = new int;
        }
        *ptr = value;
    }
    int getValue() {
        return ptr? *ptr : 0;
    }
};

PointerClass 类中,ptr 指针在构造函数中初始化为 nullptr。当需要设置值时,先检查 ptr 是否为空,为空则动态分配内存。

如果是指向用户自定义类型的指针,同样需要合理初始化。例如:

class SubPointerClass {
public:
    SubPointerClass() {
        std::cout << "SubPointerClass constructor" << std::endl;
    }
};
class MainPointerClass {
    SubPointerClass *subPtr;
public:
    MainPointerClass() : subPtr(new SubPointerClass) {
        std::cout << "MainPointerClass constructor" << std::endl;
    }
    ~MainPointerClass() {
        if (subPtr) {
            delete subPtr;
        }
    }
};

MainPointerClass 类中,subPtr 指针在构造函数中初始化为指向新创建的 SubPointerClass 对象,并在析构函数中释放内存。

容器类型成员变量

C++ 标准库中的容器(如 std::vectorstd::liststd::map 等)作为成员变量时,也可以设置默认值。例如:

#include <vector>
class VectorClass {
    std::vector<int> vec;
public:
    VectorClass() : vec{1, 2, 3} {
        std::cout << "VectorClass constructor" << std::endl;
    }
    void printVector() {
        for (int num : vec) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }
};

VectorClass 类中,vec 向量通过构造函数初始化列表进行初始化。也可以使用默认成员初始化器:

#include <vector>
class AnotherVectorClass {
    std::vector<int> vec = {4, 5, 6};
public:
    AnotherVectorClass() {
        std::cout << "AnotherVectorClass constructor" << std::endl;
    }
    void printVector() {
        for (int num : vec) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }
};

对于 std::map 等关联容器,同样可以进行类似的初始化:

#include <map>
class MapClass {
    std::map<int, std::string> myMap;
public:
    MapClass() : myMap{{1, "one"}, {2, "two"}} {
        std::cout << "MapClass constructor" << std::endl;
    }
    void printMap() {
        for (const auto &pair : myMap) {
            std::cout << pair.first << ": " << pair.second << std::endl;
        }
    }
};

继承体系中的默认值设置

基类成员变量的初始化

在继承体系中,派生类的构造函数首先会调用基类的构造函数来初始化基类部分的成员变量。如果基类构造函数有默认参数,那么在派生类构造函数调用基类构造函数时可以使用默认值。例如:

class Base {
    int baseValue;
public:
    Base(int value = 0) : baseValue(value) {
        std::cout << "Base constructor" << std::endl;
    }
};
class Derived : public Base {
    int derivedValue;
public:
    Derived(int value) : Base(), derivedValue(value) {
        std::cout << "Derived constructor" << std::endl;
    }
    void printValues() {
        std::cout << "Base value: " << baseValue << ", Derived value: " << derivedValue << std::endl;
    }
};

在上述代码中,Derived 类继承自 Base 类。Derived 类的构造函数在初始化列表中调用了 Base 类的默认构造函数(因为 Base 类构造函数有默认参数 0),然后初始化自己的 derivedValue 成员变量。

派生类成员变量的默认值

派生类成员变量的默认值设置与普通类类似,可以使用构造函数初始化列表、默认成员初始化器等方式。例如:

class Base2 {
    int baseValue;
public:
    Base2(int value) : baseValue(value) {
        std::cout << "Base2 constructor" << std::endl;
    }
};
class Derived2 : public Base2 {
    int derivedValue = 10;
public:
    Derived2(int baseVal) : Base2(baseVal) {
        std::cout << "Derived2 constructor" << std::endl;
    }
    void printValues() {
        std::cout << "Base value: " << baseValue << ", Derived value: " << derivedValue << std::endl;
    }
};

Derived2 类中,derivedValue 使用默认成员初始化器初始化为 10。在构造函数中,先调用 Base2 类的构造函数初始化 baseValue,然后 derivedValue 按照默认成员初始化器进行初始化。

虚基类的初始化和默认值

当存在虚继承时,虚基类的初始化有特殊的规则。虚基类由最底层的派生类负责初始化,并且在派生类构造函数的初始化列表中,虚基类的初始化要先于其他基类。例如:

class VirtualBase {
    int virtualValue;
public:
    VirtualBase(int value = 0) : virtualValue(value) {
        std::cout << "VirtualBase constructor" << std::endl;
    }
};
class Intermediate : virtual public VirtualBase {
public:
    Intermediate(int value) : VirtualBase(value) {
        std::cout << "Intermediate constructor" << std::endl;
    }
};
class Final : public Intermediate {
public:
    Final(int value) : VirtualBase(value), Intermediate(value) {
        std::cout << "Final constructor" << std::endl;
    }
};

在上述代码中,Final 类通过 Intermediate 类间接继承自 VirtualBase 类,并且 VirtualBase 是虚基类。Final 类的构造函数初始化列表中,先初始化 VirtualBase 类,然后再初始化 Intermediate 类。如果 VirtualBase 类构造函数有默认参数,在 Final 类构造函数不提供参数时,会使用默认值初始化 VirtualBase 类的成员变量。

类成员初始化默认值设置的常见问题及解决方法

未初始化变量导致的未定义行为

如前文所述,局部变量如果未初始化就使用会导致未定义行为。为了避免这种情况,一定要确保在使用成员变量之前对其进行初始化。可以通过构造函数初始化列表、默认成员初始化器或者在构造函数体中赋值等方式进行初始化。例如,对于前面提到的 MyClass 类,可以修改如下:

class MyClass {
    int num;
public:
    MyClass() : num(0) {
        std::cout << "MyClass constructor" << std::endl;
    }
    void printNum() {
        std::cout << num << std::endl;
    }
};

通过在构造函数初始化列表中初始化 num0,避免了未定义行为。

初始化顺序不当导致的错误

由于成员变量初始化顺序取决于声明顺序,而不是初始化列表顺序,所以要特别注意初始化顺序。如果依赖关系处理不当,可能会导致错误。例如,在 InitOrder 类中,如果按照声明顺序初始化成员变量,就可以避免错误:

class FixedInitOrder {
    int a;
    int b;
public:
    FixedInitOrder(int value) : a(value + 1), b(value) {
        std::cout << "FixedInitOrder constructor" << std::endl;
    }
    void printValues() {
        std::cout << "a: " << a << ", b: " << b << std::endl;
    }
};

内存管理问题与默认值设置

对于指针类型的成员变量,如果设置默认值为指向动态分配的内存,一定要注意在析构函数中释放内存,以避免内存泄漏。例如,在 PointerClass 类中,通过在析构函数中检查 ptr 并释放内存,避免了内存泄漏:

class PointerClass {
    int *ptr;
public:
    PointerClass() : ptr(nullptr) {
        std::cout << "PointerClass constructor" << std::endl;
    }
    ~PointerClass() {
        if (ptr) {
            delete ptr;
        }
    }
    void setValue(int value) {
        if (!ptr) {
            ptr = new int;
        }
        *ptr = value;
    }
    int getValue() {
        return ptr? *ptr : 0;
    }
};

同样,对于包含动态分配内存的容器类型成员变量,不需要手动释放内存,因为容器的析构函数会自动处理内存释放。但在某些复杂情况下,如容器中存储指针,可能需要额外的处理来确保内存正确释放。

性能优化与默认值设置

初始化方式对性能的影响

如前文提到,在构造函数初始化列表中初始化成员变量比在构造函数体中赋值效率更高,尤其是对于用户自定义类型的成员变量。在初始化列表中初始化,对于用户自定义类型直接调用相应的构造函数,而在构造函数体中赋值,会先调用默认构造函数,然后再调用赋值运算符,多了一些额外的操作。例如,对于 Point 类:

class Point {
    int x;
    int y;
public:
    Point(int a, int b) : x(a), y(b) {
        std::cout << "Point constructor" << std::endl;
    }
};

这种在初始化列表中初始化的方式比在构造函数体中赋值效率高。对于内置类型,虽然差别可能不明显,但按照良好的编程习惯,也建议在初始化列表中初始化。

默认值设置对对象创建性能的影响

合理设置默认值可以减少对象创建时的开销。例如,使用默认成员初始化器时,初始化操作在构造函数初始化列表之前执行,并且对于简单类型,编译器可能会进行优化。例如:

class OptimizedClass {
    int num = 10;
public:
    OptimizedClass() {
        std::cout << "OptimizedClass constructor" << std::endl;
    }
};

OptimizedClass 类中,num 使用默认成员初始化器初始化为 10。由于这是一个简单的内置类型,编译器可能会在编译阶段就完成初始化,从而提高对象创建的性能。

另外,对于构造函数参数的默认值,如果在大多数情况下使用默认值创建对象,设置默认值可以简化对象创建过程,提高代码的可读性和性能。例如,在 Circle 类中:

class Circle {
    double radius;
public:
    Circle(double r = 1.0) : radius(r) {
        std::cout << "Circle constructor" << std::endl;
    }
    double getArea() {
        return 3.14159 * radius * radius;
    }
};

如果经常创建半径为 1.0Circle 对象,设置默认值可以避免每次都显式提供半径值,提高创建对象的效率。

与其他C++特性的结合

与模板的结合

在模板类中,同样可以设置类成员的默认值。模板参数的默认值与类成员默认值设置相互独立,但可能会相互影响。例如:

template <typename T, int size = 5>
class TemplateClass {
    T data[size];
public:
    TemplateClass() {
        for (int i = 0; i < size; ++i) {
            data[i] = T();
        }
        std::cout << "TemplateClass constructor" << std::endl;
    }
    void printData() {
        for (int i = 0; i < size; ++i) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }
};

TemplateClass 类中,模板参数 T 可以是任何类型,size 有一个默认值 5。在构造函数中,根据模板参数 T 的类型对数组 data 进行初始化。如果 T 是用户自定义类型,会调用其默认构造函数。

与多态的结合

在多态的场景下,类成员的默认值设置也有其特点。当通过基类指针或引用调用派生类对象的函数时,基类和派生类成员变量的初始化和默认值设置需要协同工作。例如:

class Shape {
    std::string name;
public:
    Shape(const std::string &n = "Shape") : name(n) {
        std::cout << "Shape constructor" << std::endl;
    }
    virtual double getArea() {
        return 0.0;
    }
};
class Circle : public Shape {
    double radius;
public:
    Circle(double r, const std::string &n = "Circle") : Shape(n), radius(r) {
        std::cout << "Circle constructor" << std::endl;
    }
    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

在上述代码中,Shape 类是基类,Circle 类是派生类。Shape 类构造函数有一个默认参数用于设置 name 成员变量的默认值。Circle 类构造函数在初始化列表中先调用 Shape 类构造函数,然后初始化自己的 radius 成员变量。当通过 Shape 指针或引用调用 getArea 函数时,会根据实际对象的类型(Circle 或其他派生类)调用相应的函数,同时各个类的成员变量按照其默认值设置和初始化规则进行初始化。

与异常处理的结合

在设置类成员默认值和初始化过程中,如果涉及动态内存分配或其他可能抛出异常的操作,需要考虑异常处理。例如,在 PointerClass 类中,如果动态分配内存失败,new 操作会抛出 std::bad_alloc 异常。可以通过异常处理机制来确保对象处于一个合理的状态:

class PointerClass {
    int *ptr;
public:
    PointerClass() : ptr(nullptr) {
        try {
            ptr = new int;
        } catch (const std::bad_alloc &e) {
            std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        }
        std::cout << "PointerClass constructor" << std::endl;
    }
    ~PointerClass() {
        if (ptr) {
            delete ptr;
        }
    }
    void setValue(int value) {
        if (!ptr) {
            try {
                ptr = new int;
            } catch (const std::bad_alloc &e) {
                std::cerr << "Memory allocation failed: " << e.what() << std::endl;
            }
        }
        if (ptr) {
            *ptr = value;
        }
    }
    int getValue() {
        return ptr? *ptr : 0;
    }
};

在上述代码中,在构造函数和 setValue 函数中,当动态分配内存失败时,捕获 std::bad_alloc 异常并进行相应处理,避免了程序因为内存分配失败而崩溃,同时确保了对象的 ptr 成员变量处于一个合理的状态(nullptr)。