构造函数是C++面向对象编程的基石,理解它们对于编写健壮、高效的代码至关重要。

什么是构造函数?

构造函数是一个特殊的成员函数,它在创建对象时自动调用,用于初始化对象的内存状态。它的名称与类名完全相同,并且没有返回类型(连void都没有)。

构造函数的核心特点:

  1. 与类同名
  2. 无返回类型
  3. 自动调用(无法手动调用)
  4. 通常被声明为public(除非有特殊设计,如单例模式)
  5. 可以重载(一个类可以有多个构造函数)

现在,让我们深入探讨各类构造函数。

1. 默认构造函数

默认构造函数是不需要任何参数就能调用的构造函数。

两种形式:

  1. 编译器合成的默认构造函数:如果你没有为类声明任何构造函数,编译器会自动为你生成一个。注意:它对内置类型(如int, double, 指针)的成员变量不进行初始化(值是未定义的),对类类型的成员变量则调用其自身的默认构造函数。
  2. 用户定义的默认构造函数:你可以显式定义一个。

示例代码:

class MyClass {
public:
    int data;
    std::string name;

    // 用户定义的默认构造函数(无参)
    MyClass() {
        data = 0;      // 显式初始化
        name = "Unknown";
    }

    // 或者使用成员初始化列表的更优写法
    // MyClass() : data(0), name("Unknown") {}
};

int main() {
    MyClass obj1;       // 调用默认构造函数
    MyClass obj2{};     // C++11 列表初始化,也调用默认构造函数
    MyClass* obj3 = new MyClass; // 动态分配,也调用默认构造函数

    return 0;
}

关键点:

  • 一旦你定义了任何构造函数,编译器将不再自动生成默认构造函数。

  • 如果你需要一个默认构造函数但又已经定义了其他构造函数,可以使用 = default 来显式要求编译器生成。

    class MyClass {
    public:
        MyClass(int x) { ... } // 参数化构造函数
        MyClass() = default;   // 显式要求编译器生成默认构造函数
    };
    

2. 参数化构造函数

参数化构造函数接受一个或多个参数,用于在创建对象时提供初始值。

示例代码:

class Date {
private:
    int day, month, year;
public:
    // 参数化构造函数
    Date(int d, int m, int y) : day(d), month(m), year(y) { // 使用成员初始化列表
        // 可以在这里添加验证逻辑,例如检查日期是否合法
    }

    void display() {
        std::cout << day << "/" << month << "/" << year << std::endl;
    }
};

int main() {
    Date today(26, 10, 2023); // 调用参数化构造函数
    Date birthday{10, 5, 1990}; // C++11 统一初始化语法,推荐使用
    today.display(); // 输出:26/10/2023

    // Date errorDate; // 错误!因为我们已经定义了构造函数,编译器不再生成默认构造函数。
    return 0;
}

成员初始化列表:

  • 注意上面代码中的 : day(d), month(m), year(y)。这是成员初始化列表
  • 强烈推荐使用,因为它直接在成员变量被创建时初始化它们,效率高于在构造函数体内使用赋值操作(day = d;)。对于常量成员const)和引用成员&),必须使用初始化列表。

3. 拷贝构造函数

拷贝构造函数用于用一个已存在的对象来初始化一个新对象。它接受一个对本类类型的常量引用作为参数。

形式: ClassName(const ClassName& other)

何时被调用?

  1. 用一个对象初始化另一个对象时:MyClass obj1; MyClass obj2 = obj1;MyClass obj2(obj1);
  2. 对象作为值传递给函数参数时。
  3. 函数返回值一个对象时(可能会因编译器RVO优化而省略)。

示例代码:

class StringWrapper {
private:
    char* m_data;
    size_t m_size;
public:
    // 参数化构造函数
    StringWrapper(const char* str) {
        m_size = strlen(str);
        m_data = new char[m_size + 1]; // 动态分配内存
        strcpy(m_data, str);
    }

    // 1. 拷贝构造函数(深拷贝)
    StringWrapper(const StringWrapper& other) : m_size(other.m_size) {
        std::cout << "拷贝构造函数被调用!" << std::endl;
        m_data = new char[m_size + 1];
        strcpy(m_data, other.m_data);
    }

    // 析构函数
    ~StringWrapper() {
        delete[] m_data;
    }

    void print() {
        std::cout << m_data << std::endl;
    }
};

int main() {
    StringWrapper str1("Hello");
    StringWrapper str2 = str1; // 调用拷贝构造函数

    str1.print(); // Hello
    str2.print(); // Hello

    return 0;
}
// 析构时不会出错,因为str1和str2拥有各自独立的内存(深拷贝)。

深浅拷贝问题:

  • 如果类中没有动态分配的资源(如指针),使用编译器自动生成的拷贝构造函数(浅拷贝)就足够了,它只是简单地逐位复制。
  • 如果类管理着动态资源(如上面的char* m_data),必须自定义拷贝构造函数实现深拷贝。否则,两个对象的指针会指向同一块内存,导致双重释放(double free)的运行时错误。

4. 移动构造函数(C++11 引入)

移动构造函数是C++11为支持移动语义而引入的,它用于将资源(如动态内存)从一个即将销毁的临时对象“移动”到新对象中,从而避免不必要的深拷贝,提升性能。

形式: ClassName(ClassName&& other) noexceptnoexcept 很重要,标准库容器在重新分配内存时会使用移动构造函数,它要求该操作不抛异常)。

示例代码:

class StringWrapper {
    // ... 其他成员同上 ...

    // 2. 移动构造函数
    StringWrapper(StringWrapper&& other) noexcept : m_data(nullptr), m_size(0) {
        std::cout << "移动构造函数被调用!" << std::endl;
        // "窃取" 临时对象的资源
        m_data = other.m_data;
        m_size = other.m_size;

        // 将临时对象置于有效但可析构的状态
        other.m_data = nullptr;
        other.m_size = 0;
    }
};

StringWrapper createString() {
    return StringWrapper("Temporary String"); // 这是一个右值
}

int main() {
    StringWrapper str3 = createString(); // 这里会优先调用移动构造函数(如果存在)
    str3.print(); // Temporary String

    // 如果没有移动构造函数,则会调用拷贝构造函数,性能较低。
    return 0;
}

关键点:

  • 参数是 右值引用 (ClassName&&)。
  • 它“偷走”源对象的资源,并将源对象置于一个有效但可安全析构的状态(通常将其指针设为nullptr)。
  • 对于管理昂贵资源的类,实现移动构造函数是性能优化的关键。

5. 委托构造函数(C++11 引入)

委托构造函数允许一个构造函数调用同一个类的另一个构造函数,以避免代码重复。

示例代码:

class Employee {
private:
    int m_id;
    std::string m_name;
    std::string m_department;
public:
    // 目标构造函数
    Employee(int id, const std::string& name, const std::string& dept)
        : m_id(id), m_name(name), m_department(dept) {
        std::cout << "三参数构造函数" << std::endl;
    }

    // 委托构造函数:委托给上面的三参数构造函数
    Employee(int id, const std::string& name) : Employee(id, name, "Unassigned") {
        std::cout << "委托构造函数" << std::endl;
    }

    // 默认构造函数也可以委托
    Employee() : Employee(0, "Unknown", "Unassigned") {}
};

6. 转换构造函数(单参数构造函数)

任何只接受一个参数的构造函数(除了拷贝/移动构造函数),都定义了一种从参数类型到该类类型的隐式转换规则

示例代码:

class MyNumber {
private:
    int value;
public:
    // 转换构造函数:从 int 到 MyNumber
    MyNumber(int v) : value(v) {}

    void display() {
        std::cout << "Value: " << value << std::endl;
    }
};

void printNumber(MyNumber num) {
    num.display();
}

int main() {
    MyNumber num = 42; // 隐式转换:int 42 被转换为 MyNumber 对象
    printNumber(100);  // 隐式转换:int 100 被转换为 MyNumber 对象

    return 0;
}

防止隐式转换:

  • 隐式转换有时会带来意想不到的错误。可以使用 explicit 关键字来禁止它。

    class MyNumber {
    public:
        explicit MyNumber(int v) : value(v) {} // 显式构造函数
    };
    
    // MyNumber num = 42; // 错误!转换是显式的,无法进行隐式转换。
    MyNumber num(42);     // 正确!直接初始化。
    MyNumber num2 = MyNumber(42); // 正确!显式转换。
    printNumber(MyNumber(100)); // 正确!必须显式转换。
    // printNumber(100);        // 错误!
    

总结

构造函数类型语法示例主要用途
默认构造函数MyClass();创建对象时不提供初始化值
参数化构造函数MyClass(int a, string s);创建对象时提供初始化值
拷贝构造函数MyClass(const MyClass& other);用一个已存在对象初始化新对象(深拷贝)
移动构造函数MyClass(MyClass&& other) noexcept;从临时对象“转移”资源,提升性能
委托构造函数MyClass() : MyClass(0, "") {}在一个构造函数中调用另一个,避免重复代码
转换构造函数MyClass(int x);定义从参数类型到类类型的隐式转换(可用explicit禁用)

理解并正确使用这些构造函数,是编写出正确、高效、易于维护的C++代码的关键。对于资源管理类(如智能指针、容器),三/五法则(需要自定义拷贝构造/赋值、移动构造/赋值、析构函数中的一个,通常需要自定义全部)是重要的指导原则。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]