构造函数与析构函数
定义与作用
构造函数在对象创建时自动调用,负责初始化对象。析构函数在对象生命周期结束时自动调用,负责释放资源。
class File {
public:
File(const std::string& path); // 构造函数:打开文件
~File(); // 析构函数:关闭文件
};
构造函数和析构函数是 RAII(资源获取即初始化)的基础——将资源生命周期绑定到对象生命周期。
核心原理
构造函数分类与调用时机
委托构造函数(C++11)
委托构造函数让一个构造函数调用同类的另一个构造函数,消除重复代码。
完整示例
示例一:课程管理系统的构造与析构
场景说明:设计 Course 和 Enrollment 类,展示初始化列表、委托构造和析构顺序。
#include <iostream>
#include <string>
#include <vector>
class Course {
public:
// 默认构造函数
Course() : Course("未命名", 0, 0.0) {
std::cout << " [默认构造] " << name_ << std::endl;
}
// 主构造函数
Course(std::string name, int credit, double hours)
: name_(std::move(name)), credit_(credit), hours_(hours) {
std::cout << " [主构造] " << name_
<< " (学分: " << credit_ << ", 课时: " << hours_ << ")" << std::endl;
validate();
}
// 委托构造函数:只需名称,其他用默认值
Course(std::string name) : Course(std::move(name), 3, 48.0) {}
// 析构函数
~Course() {
std::cout << " [析构] " << name_ << std::endl;
}
const std::string& getName() const { return name_; }
private:
void validate() const {
if (name_.empty()) {
throw std::invalid_argument("课程名称不能为空");
}
}
std::string name_;
int credit_;
double hours_;
};
class Enrollment {
public:
Enrollment(const std::string& student, const std::string& course)
: student_(student), course_(course) {
std::cout << "[选课] " << student_ << " 选了 " << course_.getName() << std::endl;
}
~Enrollment() {
std::cout << "[退课] " << student_ << " 退出 " << course_.getName() << std::endl;
}
private:
std::string student_;
Course course_; // 成员对象,构造/析构顺序由声明顺序决定
};
int main() {
std::cout << "===== 构造函数演示 =====" << std::endl;
{
std::cout << "\n-- 创建 Course 对象 --" << std::endl;
Course c1("高等数学", 5, 64.0);
Course c2("线性代数"); // 委托构造
Course c3; // 默认构造
std::cout << "\n-- 作用域结束,对象销毁 --" << std::endl;
} // c3, c2, c1 逆序析构
std::cout << "\n===== 析构顺序 =====" << std::endl;
{
Enrollment e("张三", "数据结构");
// e 析构时先析构 course_,再析构 student_
}
return 0;
}
预期输出:
===== 构造函数演示 =====
-- 创建 Course 对象 --
[主构造] 高等数学 (学分: 5, 课时: 64)
[主构造] 线性代数 (学分: 3, 课时: 48)
[主构造] 未命名 (学分: 0, 课时: 0)
[默认构造] 未命名
-- 作用域结束,对象销毁 --
[析构] 未命名
[析构] 线性代数
[析构] 高等数学
===== 析构顺序 =====
[选课] 张三 选了 数据结构
[主构造] 数据结构 (学分: 3, 课时: 48)
[退课] 张三 退出 数据结构
[析构] 数据结构
逐段分析:
- 初始化列表
: name_(std::move(name)), credit_(credit), hours_(hours)在进入构造函数体之前完成初始化——效率高于体内赋值 - 委托构造:
Course(string)委托给Course(string, int, double),所有初始化逻辑集中在一个主构造函数中 - 默认构造通过委托构造调用主构造,保证所有路径都经过同一初始化流程
- 析构顺序:逆构造顺序——先构造的后析构。成员对象按其声明顺序逆序析构
Enrollment包含Course成员对象,析构时先析构course_再析构student_(声明顺序的逆序)
示例二:转换构造函数与 explicit
场景说明:演示隐式转换的风险和 explicit 的保护作用。
#include <iostream>
#include <string>
class PhoneNumber {
public:
// 转换构造函数:string → PhoneNumber
explicit PhoneNumber(const std::string& number) : number_(number) {
std::cout << " 构造 PhoneNumber: " << number_ << std::endl;
}
const std::string& get() const { return number_; }
private:
std::string number_;
};
class Contact {
public:
Contact(const std::string& name, const PhoneNumber& phone)
: name_(name), phone_(phone) {
std::cout << " 创建联系人: " << name_ << " | "
<< phone_.get() << std::endl;
}
private:
std::string name_;
PhoneNumber phone_;
};
// 无 explicit 的危险版本
class BadPhoneNumber {
public:
BadPhoneNumber(const std::string& number) : number_(number) {}
private:
std::string number_;
};
void dial(const BadPhoneNumber& phone) {
std::cout << "拨号中..." << std::endl;
}
int main() {
std::cout << "===== explicit 保护 =====" << std::endl;
// 必须显式构造 PhoneNumber
Contact alice("张三", PhoneNumber("13800138000"));
// 下面这行编译错误(explicit 阻止了隐式转换)
// Contact bob("李四", "13900139000");
// 没有 explicit 的话,下面的调用会悄然创建一个 BadPhoneNumber
dial(std::string("110")); // 隐式转换:string → BadPhoneNumber → dial
// 对比:explicit 要求显式写出转换
// PhoneNumber pn = "13800138000"; // 编译错误(explicit)
return 0;
}
预期输出:
===== explicit 保护 =====
构造 PhoneNumber: 13800138000
创建联系人: 张三 | 13800138000
拨号中...
逐段分析:
explicit PhoneNumber(const string&)阻止了隐式类型转换,避免Contact("张三", "138...")中字符串被悄然转为PhoneNumber- 无
explicit的BadPhoneNumber允许dial(string("110"))中字符串隐式转换为BadPhoneNumber——这种行为可能出乎意料 - 单参数构造函数默认应标记为
explicit,除非确实需要隐式转换 explicit只对单参数构造函数和转换运算符有效
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 初始化列表顺序与成员声明顺序不一致 | 未定义行为 | 初始化列表按成员声明顺序写 |
| 在构造函数体中使用未在初始化列表中初始化的成员 | 使用未初始化的值 | 所有成员在初始化列表中初始化 |
单参数构造函数未加 explicit | 意外的隐式类型转换 | 默认加 explicit |
| 虚构造函数 | C++ 没有虚构造函数 | 用工厂模式代替 |
| 析构函数中抛异常 | 栈展开中双重异常 → terminate | 析构函数不应抛异常 |
常见面试问题
初始化列表和构造函数体内赋值的区别?——初始化列表是真正的初始化(调用成员的构造函数),体内赋值是先默认初始化再赋值。对 const 成员和引用成员,必须用初始化列表。
什么是委托构造函数?什么场景使用?——C++11,一个构造函数调用同类另一个构造函数。用于消除多个构造函数间的重复初始化代码。
为什么析构函数通常应是虚函数?——当通过基类指针
delete派生类对象时,如果基类析构函数不是虚函数,只会调用基类析构函数,派生类资源泄漏。explicit关键字的作用?什么时候使用?——阻止隐式类型转换。单参数构造函数和转换运算符默认应加explicit。析构顺序是怎样的?——派生类析构 → 成员析构(声明逆序) → 基类析构。构造顺序相反。
小结
- 初始化列表优于构造函数体内赋值——真正的初始化 vs 先默认再赋值
- 委托构造函数(C++11)消除重复代码,集中管理初始化逻辑
- 单参数构造函数默认加
explicit,防止隐式类型转换 - 析构函数不应抛异常,含虚函数的基类析构函数应为 virtual
- 成员初始化顺序由声明顺序决定,初始化列表顺序应与之一致