C++: 构造函数进阶
最后更新:2026-08-26
第28课我们学了构造函数的基础。
但真实场景中,你可能需要拷贝对象、移动对象、用初始化列表提高效率……
这一课,我们来学构造函数的进阶用法。
1. 默认构造函数
(1) 1.1 什么是默认构造函数?
默认构造函数是无参的构造函数。
#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 问题:写了有参构造函数后,无法默认构造
#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)。
// 方法一:显式写
Student() {
name = "Unknown";
age = 0;
}
// 方法二:用 = default(C++11,推荐)
Student() = default;
2. 初始化列表(Initializer List)
(1) 2.1 什么是初始化列表?
初始化列表是在构造函数冒号后面直接初始化成员,而不是在函数体里赋值。
传统写法(赋值):
Student(const std::string& n, int a) {
name = n; // 这是赋值,不是初始化
age = a;
}
初始化列表写法(推荐):
Student(const std::string& n, int a) : name(n), age(a) {
// 函数体可以为空
}
💡 优势:
- 效率更高(直接初始化,不是先默认构造再赋值)
- 必须用初始化列表的场景:
const成员(必须在初始化时赋值)- 引用成员(必须初始化)
- 没有默认构造函数的成员类
(2) 2.2 必须用初始化列表的场景
场景一:const 成员
#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:默认的拷贝构造函数(难度⭐)
#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;
}
输出:
s2 的名字:Alice
s2 的年龄:20
💡 重点: 如果你没有写拷贝构造函数,编译器会自动生成一个(逐成员拷贝)。
(2) 3.2 自定义拷贝构造函数
#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 问题:默认的拷贝是"浅拷贝"
如果类里有指针成员(指向堆内存),默认的拷贝构造函数只会拷贝指针的值(地址),不会拷贝指针指向的内容。
#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 解决方法:自定义深拷贝
#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 引入的新特性——"偷"别人的资源,而不是拷贝。
场景: 如果你有一个临时对象(马上就要销毁了),拷贝它的内容很浪费——不如直接"偷"它的资源。
#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 类(难度⭐⭐)
#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;
}
输出:
普通构造函数:Alice
拷贝构造函数:Alice
析构函数:Alice
析构函数:Alice
我叫 Alice,今年 20 岁。
我叫 Alice,今年 20 岁。
❓ 常见问题
Q:什么时候需要自定义拷贝构造函数? A:当类里有指针成员(指向堆内存)时。
如果类里只有基本类型(
int、double)或 STL 类型(std::string、std::vector),不用自定义——编译器自动生成的拷贝构造函数就能正确工作(STL 类型自己实现了深拷贝)。
Q:初始化列表和函数体赋值,效率差多少? A:对于基本类型,差不了多少。但对于 STL 类型(
std::string、std::vector),差很多——
- 函数体赋值:先默认构造一个空对象,再赋值(可能要分配内存、拷贝内容)
- 初始化列表:直接初始化(只分配一次内存)
建议: 一律用初始化列表。
Q:移动构造函数有什么用? A:提高临时对象的拷贝效率。
比如函数返回一个大对象时,移动构造函数可以避免深拷贝(直接"偷"临时对象的内存)。
▶ 示例 3:委托构造函数(难度⭐)
#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;
}
输出:
张三,20岁
李四,18岁
未知,18岁
- 默认构造函数是无参构造函数
- 初始化列表效率更高,且能处理
const成员、引用成员 - 拷贝构造函数用来用一个对象初始化另一个对象
- 如果类里有指针成员,必须自定义深拷贝
- 移动构造函数(C++11)可以提高临时对象的拷贝效率
📖 小节
- 委托构造:构造函数调用另一个构造函数
- 继承构造:派生类继承基类构造函数
- 移动构造:转移资源所有权,避免深拷贝
- explicit:防止隐式转换
📝 作业
-
基础题 (Difficulty ⭐): 定义一个
Book类,包含书名、作者两个成员。用初始化列表写构造函数。 -
进阶题 (Difficulty ⭐⭐): 自定义
Book类的拷贝构造函数,实现深拷贝(假设书名是用char*存的,需要分配新内存)。 -
挑战题 (Difficulty ⭐⭐⭐): 定义一个
DynamicArray类(动态数组),包含: -
int* data(指向堆内存) -
int size(数组大小)
实现: 3. 普通构造函数(分配内存) 4. 拷贝构造函数(深拷贝) 5. 析构函数(释放内存)
测试:创建 DynamicArray obj1(10),再用 obj1 初始化 obj2,验证两个对象的 data 指向不同的内存地址。
- 默认构造函数:无参数或全默认参数
- 初始化列表比构造函数体内赋值更高效
- 委托构造函数:一个构造函数调用另一个
- explicit 防止隐式类型转换
- 析构函数:对象销毁时自动调用,释放资源
7. 🚀 下一步
学会了构造函数的进阶用法,接下来我们学习 运算符重载(第30课)—— 让自定义类型也能用 +、-、<< 等运算符!