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

C++类构造函数的多样化实现

2023-03-061.6k 阅读

C++类构造函数的多样化实现

构造函数基础概念

在C++ 中,构造函数是一种特殊的成员函数,它在创建类的对象时被自动调用。构造函数的主要目的是对对象进行初始化,确保对象在创建后处于一个有效的状态。构造函数具有与类名相同的名称,并且没有返回类型,甚至连 void 也没有。

例如,定义一个简单的 Point 类:

class Point {
public:
    int x;
    int y;
    // 构造函数
    Point() {
        x = 0;
        y = 0;
    }
};

在上述代码中,Point() 就是 Point 类的构造函数。当创建 Point 对象时,如 Point p;,这个构造函数会被自动调用,将 xy 初始化为 0。

无参构造函数

无参构造函数,也就是不接受任何参数的构造函数,如上面 Point 类中的构造函数。它为对象提供了一个默认的初始化方式。无参构造函数在很多情况下非常有用,比如当你需要创建一个对象数组时:

class Rectangle {
public:
    int width;
    int height;
    Rectangle() {
        width = 1;
        height = 1;
    }
};

int main() {
    Rectangle rects[5];
    return 0;
}

这里创建了一个 Rectangle 对象数组 rects,数组中的每个元素都会调用 Rectangle 类的无参构造函数进行初始化,将 widthheight 初始化为 1。

有参构造函数

有参构造函数允许在创建对象时传递参数,从而实现更灵活的初始化。以 Point 类为例,可以定义一个接受 xy 坐标值的有参构造函数:

class Point {
public:
    int x;
    int y;
    // 有参构造函数
    Point(int a, int b) {
        x = a;
        y = b;
    }
};

现在可以这样创建 Point 对象:

Point p1(10, 20);

上述代码通过有参构造函数创建了 Point 对象 p1,并将 x 初始化为 10,y 初始化为 20。

有参构造函数的重载

一个类可以有多个有参构造函数,只要它们的参数列表不同,这就是构造函数的重载。例如,对于 Rectangle 类,可以定义多个有参构造函数:

class Rectangle {
public:
    int width;
    int height;
    // 有参构造函数1
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
    // 有参构造函数2
    Rectangle(int side) {
        width = side;
        height = side;
    }
};

这样就可以通过不同的方式创建 Rectangle 对象:

Rectangle rect1(10, 20); // width = 10, height = 20
Rectangle rect2(5);      // width = 5, height = 5

初始化列表

虽然在构造函数体中对成员变量进行赋值可以实现初始化,但 C++ 还提供了一种更高效的方式——初始化列表。初始化列表在构造函数参数列表之后,函数体之前,以冒号开头,多个初始化项之间用逗号分隔。

例如,对于 Point 类,使用初始化列表的构造函数如下:

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

使用初始化列表有以下几个优点:

  1. 效率更高:对于一些类型,如 const 成员变量、引用成员变量,以及没有默认构造函数的类类型成员变量,必须使用初始化列表进行初始化。因为在构造函数体中对这些变量赋值是不允许的。例如:
class MyClass {
public:
    const int value;
    MyClass(int v) : value(v) {
        // 这里不能对value再次赋值,只能在初始化列表初始化
    }
};
  1. 减少构造和析构开销:对于类类型的成员变量,如果使用初始化列表,会直接调用成员变量的合适构造函数进行初始化。而在构造函数体中赋值,会先调用成员变量的默认构造函数,然后再调用赋值运算符进行赋值,增加了构造和析构的开销。

委托构造函数

C++11 引入了委托构造函数的概念。委托构造函数允许一个构造函数调用同一个类的其他构造函数,从而避免代码重复。

例如,对于 Rectangle 类:

class Rectangle {
public:
    int width;
    int height;
    // 基础有参构造函数
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
    // 委托构造函数
    Rectangle() : Rectangle(1, 1) {
        // 这里委托了Rectangle(int, int)构造函数
    }
};

在上述代码中,Rectangle() 构造函数委托了 Rectangle(int, int) 构造函数来完成初始化工作。这样,当通过 Rectangle rect; 创建对象时,会先调用 Rectangle(1, 1) 构造函数,然后再执行 Rectangle() 构造函数体中的代码(如果有)。

委托构造函数在有多个构造函数且部分初始化逻辑相同时非常有用。例如,再添加一个委托构造函数:

class Rectangle {
public:
    int width;
    int height;
    // 基础有参构造函数
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
    // 委托构造函数1
    Rectangle() : Rectangle(1, 1) {
    }
    // 委托构造函数2
    Rectangle(int side) : Rectangle(side, side) {
    }
};

这样,Rectangle(int side) 构造函数委托了 Rectangle(int, int) 构造函数,避免了重复的初始化逻辑。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,用于用一个已存在的对象来初始化新对象。它的参数是本类对象的引用。

例如,对于 Point 类,拷贝构造函数定义如下:

class Point {
public:
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {
    }
    // 拷贝构造函数
    Point(const Point& other) : x(other.x), y(other.y) {
    }
};

这里的拷贝构造函数接受一个 const Point& 类型的参数 other,通过 other 对象来初始化新创建的对象。

当进行对象赋值操作时,如 Point p1(10, 20); Point p2 = p1;,拷贝构造函数会被调用。如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,它会按成员逐一拷贝对象的成员变量。但在某些情况下,默认的拷贝构造函数可能无法满足需求,比如类中包含动态分配的内存。

例如:

class MyString {
public:
    char* str;
    int length;
    MyString(const char* s) {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
    }
    // 缺少拷贝构造函数,会导致浅拷贝问题
};

在上述 MyString 类中,如果没有显式定义拷贝构造函数,当进行对象拷贝时,如 MyString s1("hello"); MyString s2 = s1;,默认的拷贝构造函数会简单地拷贝 str 指针,这样 s1s2str 指针会指向同一块内存,当其中一个对象析构时,这块内存会被释放,导致另一个对象的 str 指针成为野指针,引发程序错误。

为了解决这个问题,需要显式定义拷贝构造函数:

class MyString {
public:
    char* str;
    int length;
    MyString(const char* s) {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
    }
    // 拷贝构造函数
    MyString(const MyString& other) {
        length = other.length;
        str = new char[length + 1];
        strcpy(str, other.str);
    }
    ~MyString() {
        delete[] str;
    }
};

这样,在进行对象拷贝时,会为新对象分配独立的内存,避免了浅拷贝问题。

移动构造函数

C++11 引入了移动构造函数,用于在对象所有权转移时提高性能。移动构造函数的参数是一个右值引用。

例如,对于 MyString 类,移动构造函数定义如下:

class MyString {
public:
    char* str;
    int length;
    MyString(const char* s) {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
    }
    MyString(const MyString& other) {
        length = other.length;
        str = new char[length + 1];
        strcpy(str, other.str);
    }
    // 移动构造函数
    MyString(MyString&& other) noexcept {
        length = other.length;
        str = other.str;
        other.length = 0;
        other.str = nullptr;
    }
    ~MyString() {
        delete[] str;
    }
};

在移动构造函数中,other 是一个右值引用,表示即将被销毁的对象。通过将 other 的资源(如 str 指针和 length)直接转移到新对象,避免了重新分配内存和拷贝数据的开销。同时,将 other 的资源设置为无效状态,防止 other 析构时重复释放资源。

移动构造函数通常在对象作为函数返回值或在容器中移动时被调用。例如:

MyString createString() {
    MyString temp("hello");
    return temp;
}

int main() {
    MyString s = createString();
    return 0;
}

在上述代码中,createString 函数返回 MyString 对象 temp 时,会调用移动构造函数将 temp 的资源移动到 s 中,而不是进行拷贝,提高了性能。

构造函数的调用顺序

当一个类包含其他类类型的成员变量,并且该类继承自其他类时,构造函数的调用顺序有一定的规则。

  1. 基类构造函数先调用:如果类 B 继承自类 A,在创建 B 对象时,会先调用 A 的构造函数,然后再调用 B 的构造函数。例如:
class A {
public:
    A() {
        std::cout << "A constructor" << std::endl;
    }
};

class B : public A {
public:
    B() {
        std::cout << "B constructor" << std::endl;
    }
};

当创建 B 对象时,会先输出 A constructor,然后输出 B constructor

  1. 成员变量构造函数按声明顺序调用:在类的构造函数中,成员变量的构造函数会按照它们在类中声明的顺序被调用,而不是按照它们在初始化列表中的顺序。例如:
class C {
public:
    int num1;
    int num2;
    C(int a, int b) : num2(b), num1(a) {
        std::cout << "C constructor" << std::endl;
    }
};

这里虽然在初始化列表中先初始化 num2 后初始化 num1,但实际上会先调用 num1 的构造函数(因为 num1 先声明),然后调用 num2 的构造函数,最后执行 C 构造函数体中的代码。

构造函数与多态

构造函数在多态性方面有一些特殊的行为。由于构造函数用于创建对象并初始化其状态,在构造函数执行期间,对象的类型还没有完全确定为最终的派生类类型,而是处于基类到派生类逐步构建的过程中。

例如,考虑以下代码:

class Base {
public:
    Base() {
        virtualFunction();
    }
    virtual void virtualFunction() {
        std::cout << "Base virtual function" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
    }
    void virtualFunction() override {
        std::cout << "Derived virtual function" << std::endl;
    }
};

当创建 Derived 对象时,会先调用 Base 的构造函数。在 Base 的构造函数中调用 virtualFunction,此时由于对象还在构建过程中,this 指针指向的是一个不完全的 Derived 对象,所以调用的是 Base 类的 virtualFunction,而不是 Derived 类的重写版本。输出结果会是 Base virtual function

这是因为在构造函数中,虚函数机制不会按照多态的方式工作,而是调用构造函数所属类的虚函数版本。这是为了确保对象在构造过程中的一致性和安全性。

构造函数的异常处理

在构造函数中,可能会发生各种异常,比如内存分配失败等。当构造函数抛出异常时,对象的构造过程会被终止,并且已经构造的部分(如基类和成员变量)会被正确析构。

例如,对于 MyString 类,在构造函数中分配内存可能会失败:

class MyString {
public:
    char* str;
    int length;
    MyString(const char* s) {
        length = strlen(s);
        try {
            str = new char[length + 1];
        } catch (const std::bad_alloc& e) {
            std::cerr << "Memory allocation failed: " << e.what() << std::endl;
            throw;
        }
        strcpy(str, s);
    }
    ~MyString() {
        delete[] str;
    }
};

在上述代码中,如果 new char[length + 1] 分配内存失败,会抛出 std::bad_alloc 异常。构造函数捕获这个异常,输出错误信息,并重新抛出,以便调用者处理。同时,由于对象构造未完成,不会调用析构函数,因为没有需要释放的有效资源。

构造函数与模板

构造函数也可以与模板结合使用,以实现更通用的初始化逻辑。例如,定义一个模板类 Box,可以为其定义模板构造函数:

template <typename T>
class Box {
public:
    T value;
    // 模板构造函数
    template <typename U>
    Box(U initValue) : value(static_cast<T>(initValue)) {
    }
};

这样,Box 类可以接受不同类型的初始化值,并将其转换为 T 类型。例如:

Box<int> box1(10);        // 正常初始化
Box<int> box2(10.5f);     // 通过模板构造函数,将float转换为int

模板构造函数为类的初始化提供了极大的灵活性,使得类可以适应多种不同类型的初始化数据。

总结构造函数的多样化实现

C++ 类的构造函数提供了丰富多样的实现方式,从基础的无参和有参构造函数,到更高级的初始化列表、委托构造函数、拷贝构造函数、移动构造函数等。每种构造函数都有其特定的用途和适用场景,了解并合理运用这些构造函数,可以使代码更加高效、健壮和灵活。同时,在涉及继承、多态、模板以及异常处理等方面,构造函数也有着独特的行为和规则,开发者需要深入理解这些知识,才能编写出高质量的 C++ 程序。在实际编程中,根据类的需求选择合适的构造函数实现方式,是构建优秀 C++ 代码的关键之一。通过对上述各种构造函数的详细介绍和示例代码,希望读者能够对 C++ 类构造函数的多样化实现有更深入的理解和掌握,并在实际项目中灵活运用这些技术。