【基础知识】C++的几种构造函数

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

什么是构造函数?

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

构造函数的核心特点:

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

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

1. 默认构造函数

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

两种形式:

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

示例代码:

cpp 复制代码
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 来显式要求编译器生成。

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

2. 参数化构造函数

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

示例代码:

cpp 复制代码
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优化而省略)。

示例代码:

cpp 复制代码
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 很重要,标准库容器在重新分配内存时会使用移动构造函数,它要求该操作不抛异常)。

示例代码:

cpp 复制代码
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 引入)

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

示例代码:

cpp 复制代码
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. 转换构造函数(单参数构造函数)

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

示例代码:

cpp 复制代码
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 关键字来禁止它。

    cpp 复制代码
    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++代码的关键。对于资源管理类(如智能指针、容器),三/五法则(需要自定义拷贝构造/赋值、移动构造/赋值、析构函数中的一个,通常需要自定义全部)是重要的指导原则。

相关推荐
博笙困了4 小时前
AcWing学习——链表
c++·算法
此间码农4 小时前
c-依赖库汇总与缺失检测
c++
Hankin_Liu的技术研究室4 小时前
可观测副作用:C++编译器优化的“红线”
c++·编译原理
爱编程的化学家4 小时前
代码随想录算法训练营第21天 -- 回溯4 || 491.非递减子序列 / 46.全排列 /47.全排列 II
数据结构·c++·算法·leetcode·回溯·全排列·代码随想录
吾当每日三饮五升4 小时前
RapidJSON 自定义内存分配器详解与实战
c++·后端·性能优化·json
小欣加油5 小时前
leetcode 129 求根节点到叶节点数字之和
数据结构·c++·算法·leetcode
徽先生6 小时前
vscode中编写c++程序
c++·ide·vscode
铭哥的编程日记7 小时前
C++优选算法精选100道编程题(附有图解和源码)
开发语言·c++·算法
深思慎考10 小时前
LinuxC++项目开发日志——基于正倒排索引的boost搜索引擎(2——Parser解析html模块)
linux·c++·搜索引擎