一、构造函数和析构函数
1. 核心作用
C++ 中每个对象都会经历创建初始化 和销毁清理 两个阶段,编译器提供两种特殊函数自动完成这两个操作,无需程序员手动调用:
- 构造函数:对象创建时自动调用,用于给成员属性初始化,保证对象出厂就有合法初始值,避免随机垃圾数据。
- 析构函数:对象销毁前自动调用,用于释放堆内存、关闭文件、释放资源等清理工作,防止内存泄漏。
2. 构造函数语法规则
类名() { 函数体 }
硬性要求:
- 函数名必须与类名完全相同,大小写一致;
- 无返回值,不写
void,也不能写int等任何返回类型; - 可以有参数,支持函数重载;
- 对象创建时自动调用,且仅调用 1 次,不能手动重复调用。
3. 析构函数语法规则
~类名() { 清理逻辑 }
硬性要求:
- 函数名是
~加类名; - 无返回值、无参数,不支持重载,一个类只能有 1 个析构函数;
- 对象生命周期结束时自动调用,仅调用 1 次。
4. 基础示例代码
#include<iostream>
using namespace std;
class Person
{
public:
// 构造函数
Person()
{
cout << "Person构造函数执行,对象初始化" << endl;
}
// 析构函数
~Person()
{
cout << "Person析构函数执行,对象销毁清理" << endl;
}
};
void test()
{
Person p; // 进入函数创建对象,自动调用构造
} // 函数结束,局部对象p销毁,自动调用析构
int main()
{
test();
system("pause");
return 0;
}
运行输出:
Person构造函数执行,对象初始化
Person析构函数执行,对象销毁清理
5. 编译器默认函数补充
如果程序员不手动写构造 / 析构,编译器会自动生成空实现的版本:
- 默认构造:函数体为空,不会给成员赋值;
- 默认析构:函数体为空,不会释放任何堆资源。
注意:如果成员有堆区开辟的数据,默认析构无法释放,必须自己写析构。
二、构造函数的分类及调用
1. 两种分类维度
分类 1:按参数划分
-
无参构造函数(默认构造) :括号内无任何参数
Person(){ ... } -
有参构造函数 :括号内存在一个或多个形参
Person(int age, string name){ ... }
分类 2:按功能划分
-
普通构造函数:无参 / 有参,用于全新对象初始化;
-
拷贝构造函数 :形参为本类
const引用,作用:用已有对象复制出新对象Person(const Person& p){ ... }
逐段拆分解析
1. Person
构造函数的函数名,必须和类名完全一致,这是所有构造函数的硬性规则。
2. 括号内整体:const Person& p 形参(核心难点)
我们拆成三部分:const + Person + & + p
Person形参的类型:代表本类对象,说明只能接收Person类型的对象作为参数,不能传 int、string 等其他类型。&引用符号 作用:传递对象的别名 ,不是拷贝一份新对象。 如果不写&,写成Person p(值传递):调用拷贝构造时会无限递归死循环。 原理:值传递会先复制实参,复制操作又会调用拷贝构造,无限套娃报错,所以拷贝构造必须加引用&。const常量修饰 限制:在拷贝构造函数内部,不能修改传入的原对象 p 。 场景:拷贝只是复制数据,不应该改动源对象,加const防止误写修改原对象,提高代码安全性。p形参变量名,随便起,比如obj、per都可以,只是接收传入对象的别名。
3. { ... } 函数体
里面写复制逻辑:把传入对象p的所有成员变量,赋值给当前新对象的成员。 示例基础实现:
class Person{
public:
int age;
string name;
// 拷贝构造函数
Person(const Person& p){
// 把旧对象p的属性复制给新对象
this->age = p.age;
this->name = p.name;
}
};
完整特性总结
1. 为什么必须同时有 const 和 &?
- 缺
&:值传递,递归死循环,编译报错; - 缺
const:语法允许,但不安全,函数内可以篡改原对象,不符合拷贝只复制、不修改的设计逻辑,工程规范强制加const。
2. 调用场景(只有 3 种情况会自动触发这个函数)
- 已有对象初始化新对象:
Person p2 = p1; - 函数值传递 Person 对象:
void func(Person p){} - 函数按值返回Person 局部对象
3. 和普通构造区分
普通构造:Person(int a),接收数字 / 字符串等外部值; 拷贝构造:Person(const Person& p),只接收同类对象,专门用于对象复制。
4. 编译器默认行为
如果你不手动写这个拷贝构造,编译器会自动生成一个默认浅拷贝版本,内部逻辑就是逐一把成员复制; 如果类中有堆指针(int* m_height),默认浅拷贝会重复释放内存崩溃,必须手动写这个拷贝构造实现深拷贝(后续笔记会写到)。
补充错误反例(禁止写法)
// 错误1:没有引用,无限递归
Person(Person p){}
// 错误2:不加const,不规范,可修改源对象
Person(Person& p){}
// 正确标准写法
Person(const Person& p){}
2. 三种调用方式(重点区分易错点)
方式 1:括号法(最常用)
// 无参构造调用 错误写法:Person p(); 编译器识别为函数声明
Person p1;
// 有参构造
Person p2(18, "张三");
// 拷贝构造
Person p3(p2);
⚠️ 易错提醒:调用无参构造不能加括号,Person p();会被编译器认为是一个名为 p、返回 Person 的函数声明,不会创建对象。
方式 2:显式法
// 有参构造匿名对象,再赋值给变量
Person p2 = Person(20, "李四");
// 拷贝构造,用已有对象复制
Person p3 = Person(p2);
// Person(20,"李四") 是匿名对象,当前代码行执行结束后立刻销毁
方式 3:隐式转换法(简写)
// 等价于 Person p4 = Person(16, "王五");
Person p4 = 16; // 仅单参数构造支持隐式转换
⚠️ 多参数构造无法使用隐式转换。
3. 匿名对象特性
Person(10, "小明") 单独书写为匿名对象,当前代码行执行完毕直接销毁,不会保留到下一行。
三、4.2.3 拷贝构造函数调用时机
拷贝构造函数只会在3 种固定场景自动调用,其余情况不会触发:
场景 1:使用已存在对象初始化新对象
Person p1(18, "张三");
// 两种写法都会调用拷贝构造
Person p2(p1);
Person p3 = p1;
场景 2:函数值传递,实参传给形参
值传递时,会复制一份对象作为形参,触发拷贝构造:
void func(Person p) // 值传递,拷贝构造执行
{
cout << p.m_name;
}
int main()
{
Person p(18, "张三");
func(p); // 传入对象,触发拷贝构造
return 0;
}
优化:使用引用
void func(Person& p)传递,不会调用拷贝,节省内存。
场景 3:函数以值方式返回局部对象
函数内部局部对象返回时,会拷贝一份临时对象,触发拷贝构造:
Person test()
{
Person temp(20, "小李");
return temp; // 值返回,调用拷贝构造
}
int main()
{
Person p = test();
return 0;
}
补充:赋值操作不调用拷贝构造
Person p1(10,"a");
Person p2;
p2 = p1; // 赋值运算符重载,不会调用拷贝构造
创建对象时才会调用拷贝,对象已存在仅赋值不会触发。
四、构造函数调用规则
编译器默认生成规则(核心 4 条)
规则 1:默认提供三大基础函数
程序员不写任何构造 / 析构时,编译器自动生成:
- 无参默认构造:空实现,不初始化成员;
- 默认析构:空实现,不清理资源;
- 默认拷贝构造:浅拷贝,直接逐字节复制所有成员的值。
规则 2:自定义有参构造后,编译器不再提供无参构造
如果手写了任意有参构造,编译器会删除默认无参构造,此时代码中写Person p;直接报错。 示例:
class Person
{
public:
Person(int a){} // 自定义有参构造
};
int main()
{
Person p; // 报错,不存在无参构造
return 0;
}
解决:手动补充无参构造函数。
规则 3:只要自定义构造(无参 / 有参),默认拷贝构造依旧存在
只要没有手动写拷贝构造,无论有没有普通构造,编译器都会自动生成浅拷贝的拷贝构造。
规则 4:手动定义拷贝构造,编译器不再生成任何构造(无参、有参默认全部消失)
只要自己写了Person(const Person& p)拷贝构造,编译器不会生成无参、有参默认构造,创建对象只能使用拷贝构造,想要普通构造必须手动实现。
配套总结表格
| 程序员自定义函数 | 编译器自动生成 |
|---|---|
| 无任何构造 | 默认无参构造、默认析构、默认拷贝构造 |
| 仅写有参构造 | 无无参构造;保留默认拷贝、默认析构 |
| 仅写拷贝构造 | 无无参 / 有参构造;仅保留默认析构 |
| 写了无参 + 有参构造 | 无默认无参;保留默认拷贝、默认析构 |
| 手动写析构 | 依旧生成三大默认构造,不受影响 |
五、重点易错点汇总
- 构造不能手动调用,析构也不能手动调用,生命周期由系统自动管理;
- 无参构造创建对象不能加括号,否则变成函数声明;
- 值传递 / 值返回对象会触发拷贝构造,引用传递不会;
- 默认拷贝是浅拷贝,如果成员包含堆指针,必须手写深拷贝构造;
- 写了有参构造就不能直接
Person p;,缺少无参构造编译报错; - 析构无参数、不能重载,一个类只能有一个;
- 匿名对象本行执行完立刻销毁,不会延续生命周期。