C++: 构造函数进阶

最后更新:2026-08-26

第28课我们学了构造函数的基础。

但真实场景中,你可能需要拷贝对象移动对象用初始化列表提高效率……

这一课,我们来学构造函数的进阶用法。


1. 默认构造函数

(1) 1.1 什么是默认构造函数?

默认构造函数无参的构造函数。

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // 默认构造函数(无参)
 Student() {
 name = "Unknown";
 age = 0;
 std::cout << "默认构造函数被调用" << std::endl;
 }
};

int main() {
 Student s1; // ✅ 调用默认构造函数
 return 0;
}

💡 重点: 如果你没有写任何构造函数,编译器会自动生成一个默认构造函数(什么也不做)。但如果你写了任何一个构造函数,编译器就不会再自动生成默认构造函数了。


(2) 1.2 问题:写了有参构造函数后,无法默认构造

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // 只写了有参构造函数
 Student(const std::string& n, int a) {
 name = n;
 age = a;
 }
};

int main() {
 Student s1("Alice", 20); // ✅ 可以
 Student s2; // ❌ 错误:没有默认构造函数
 return 0;
}

修复: 显式写默认构造函数,或用 = default(C++11)。

CPP
// 方法一:显式写
Student() {
 name = "Unknown";
 age = 0;
}

// 方法二:用 = default(C++11,推荐)
Student() = default;


2. 初始化列表(Initializer List)

(1) 2.1 什么是初始化列表?

初始化列表是在构造函数冒号后面直接初始化成员,而不是在函数体里赋值。

传统写法(赋值):

CPP
Student(const std::string& n, int a) {
 name = n; // 这是赋值,不是初始化
 age = a;
}

初始化列表写法(推荐):

CPP
Student(const std::string& n, int a) : name(n), age(a) {
 // 函数体可以为空
}

💡 优势:

  1. 效率更高(直接初始化,不是先默认构造再赋值)
  2. 必须用初始化列表的场景:

(2) 2.2 必须用初始化列表的场景

场景一:const 成员

CPP
#include <iostream>
#include <string>

class Student {
private:
 const std::string id; // const 成员
 std::string name;
 
public:
 // ❌ 错误:const 成员无法在函数体里赋值
 // Student(const std::string& i, const std::string& n) {
 // id = i; // ❌ const 成员不能赋值
 // name = n;
 // }
 
 // ✅ 正确:用初始化列表
 Student(const std::string& i, const std::string& n) : id(i), name(n) {
 }
};

int main() {
 Student s1("20240001", "Alice");
 return 0;
}


3. 拷贝构造函数(Copy Constructor)

(1) 3.1 什么是拷贝构造函数?

拷贝构造函数是用来用一个对象初始化另一个对象的构造函数。

触发场景:

  1. 用一个对象初始化另一个对象
  2. 函数参数按值传递(会拷贝)
  3. 函数返回对象(可能拷贝)

▶ 示例 1:默认的拷贝构造函数(难度⭐)

CPP
#include <iostream>
#include <string>

class Student {
public:
 std::string name;
 int age;
 
 // 编译器会自动生成拷贝构造函数(逐成员拷贝)
};

int main() {
 Student s1;
 s1.name = "Alice";
 s1.age = 20;
 
 Student s2 = s1; // ✅ 调用拷贝构造函数
 std::cout << "s2 的名字:" << s2.name << std::endl; // Alice
 std::cout << "s2 的年龄:" << s2.age << std::endl; // 20
 
 return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
s2 的名字:Alice
s2 的年龄:20

💡 重点: 如果你没有写拷贝构造函数,编译器会自动生成一个(逐成员拷贝)。


(2) 3.2 自定义拷贝构造函数

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // 普通构造函数
 Student(const std::string& n, int a) : name(n), age(a) {
 std::cout << "普通构造函数被调用:" << name << std::endl;
 }
 
 // 拷贝构造函数
 Student(const Student& other) : name(other.name), age(other.age) {
 std::cout << "拷贝构造函数被调用:" << name << std::endl;
 }
};

int main() {
 Student s1("Alice", 20);
 Student s2 = s1; // 调用拷贝构造函数
 
 return 0;
}


4. 浅拷贝 vs 深拷贝

(1) 4.1 问题:默认的拷贝是"浅拷贝"

如果类里有指针成员(指向堆内存),默认的拷贝构造函数只会拷贝指针的值(地址),不会拷贝指针指向的内容。

CPP
#include <iostream>
#include <cstring>

class MyClass {
public:
 char* data;
 
 MyClass() {
 data = new char[5];
 std::strcpy(data, "Hello");
 }
 
 // ❌ 没有自定义拷贝构造函数(会用默认的浅拷贝)
 
 ~MyClass() {
 delete data;
 }
};

int main() {
 MyClass obj1;
 MyClass obj2 = obj1; // 浅拷贝:obj1.data 和 obj2.data 指向同一块内存
 
 // main 结束时,obj2 先析构(释放 data)
 // 然后 obj1 析构(再释放 data,但已经释放过了)→ 崩溃!
 return 0;
}

💡 这就是"浅拷贝"的问题——两个对象的指针指向同一块内存,析构时会重复释放


(2) 4.2 解决方法:自定义深拷贝

CPP
#include <iostream>
#include <cstring>

class MyClass {
public:
 char* data;
 
 MyClass() {
 data = new char[5];
 std::strcpy(data, "Hello");
 }
 
 // 自定义拷贝构造函数(深拷贝)
 MyClass(const MyClass& other) {
 data = new char[std::strlen(other.data) + 1];
 std::strcpy(data, other.data);
 }
 
 ~MyClass() {
 delete data;
 }
};

int main() {
 MyClass obj1;
 MyClass obj2 = obj1; // 深拷贝:obj2.data 指向新分配的内存
 
 std::cout << "obj1.data = " << obj1.data << std::endl;
 std::cout << "obj2.data = " << obj2.data << std::endl;
 
 return 0;
}

💡 重点: 如果类里有指针成员,必须自定义拷贝构造函数(实现深拷贝)。



5. 移动构造函数(C++11,选学)

(1) 5.1 什么是移动构造函数?

移动构造函数是 C++11 引入的新特性——"偷"别人的资源,而不是拷贝。

场景: 如果你有一个临时对象(马上就要销毁了),拷贝它的内容很浪费——不如直接"偷"它的资源。

CPP
#include <iostream>
#include <string>

class MyClass {
private:
 std::string* data;
 
public:
 // 普通构造函数
 MyClass(const std::string& s) {
 data = new std::string(s);
 std::cout << "普通构造函数" << std::endl;
 }
 
 // 移动构造函数(C++11)
 MyClass(MyClass&& other) noexcept : data(other.data) {
 other.data = nullptr; // 把 other 的指针设为空(防止析构时释放)
 std::cout << "移动构造函数" << std::endl;
 }
 
 ~MyClass() {
 delete data;
 }
};

int main() {
 MyClass obj1("Hello");
 MyClass obj2 = std::move(obj1); // 调用移动构造函数
 
 return 0;
}

💡 提示: 移动构造函数是高级话题,初学者可以先了解,后面再深入。



6. 实战:完整的 Student 类

▶ 示例 2:带拷贝构造函数的 Student 类(难度⭐⭐)

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // 普通构造函数
 Student(const std::string& n, int a) : name(n), age(a) {
 std::cout << "普通构造函数:" << name << std::endl;
 }
 
 // 拷贝构造函数
 Student(const Student& other) : name(other.name), age(other.age) {
 std::cout << "拷贝构造函数:" << name << std::endl;
 }
 
 // 析构函数
 ~Student() {
 std::cout << "析构函数:" << name << std::endl;
 }
 
 void introduce() {
 std::cout << "我叫 " << name << ",今年 " << age << " 岁。" << std::endl;
 }
};

int main() {
 Student s1("Alice", 20);
 Student s2 = s1; // 调用拷贝构造函数
 
 s1.introduce();
 s2.introduce();
 
 return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
普通构造函数:Alice
拷贝构造函数:Alice
析构函数:Alice
析构函数:Alice
我叫 Alice,今年 20 岁。
我叫 Alice,今年 20 岁。

❓ 常见问题

Q:什么时候需要自定义拷贝构造函数? A:当类里有指针成员(指向堆内存)时。

如果类里只有基本类型(intdouble)或 STL 类型std::stringstd::vector),不用自定义——编译器自动生成的拷贝构造函数就能正确工作(STL 类型自己实现了深拷贝)。

Q:初始化列表和函数体赋值,效率差多少? A:对于基本类型,差不了多少。但对于 STL 类型(std::stringstd::vector),差很多——

  • 函数体赋值:先默认构造一个空对象,再赋值(可能要分配内存、拷贝内容)
  • 初始化列表:直接初始化(只分配一次内存)

建议: 一律用初始化列表

Q:移动构造函数有什么用? A:提高临时对象的拷贝效率。

比如函数返回一个大对象时,移动构造函数可以避免深拷贝(直接"偷"临时对象的内存)。


▶ 示例 3:委托构造函数(难度⭐)

CPP
#include <iostream>
#include <string>

class Student {
    std::string name;
    int age;
public:
    Student(std::string n, int a) : name(n), age(a) {}
    Student(std::string n) : Student(n, 18) {} // 委托构造
    Student() : Student("未知") {}

    void print() { std::cout << name << "," << age << "岁" << std::endl; }
};

int main() {
    Student s1("张三", 20);
    Student s2("李四");
    Student s3;

    s1.print();
    s2.print();
    s3.print();

    return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
张三,20岁
李四,18岁
未知,18岁
💡 提示:委托构造函数让一个构造函数调用另一个,避免重复代码。



📖 小节

📝 作业

  1. 基础题 (Difficulty ⭐): 定义一个 Book 类,包含书名、作者两个成员。用初始化列表写构造函数。

  2. 进阶题 (Difficulty ⭐⭐): 自定义 Book 类的拷贝构造函数,实现深拷贝(假设书名是用 char* 存的,需要分配新内存)。

  3. 挑战题 (Difficulty ⭐⭐⭐): 定义一个 DynamicArray 类(动态数组),包含:

  4. int* data(指向堆内存)

  5. int size(数组大小)

实现: 3. 普通构造函数(分配内存) 4. 拷贝构造函数(深拷贝) 5. 析构函数(释放内存)

测试:创建 DynamicArray obj1(10),再用 obj1 初始化 obj2,验证两个对象的 data 指向不同的内存地址。


7. 🚀 下一步

学会了构造函数的进阶用法,接下来我们学习 运算符重载(第30课)—— 让自定义类型也能用 +-<< 等运算符!

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏