C++ 学习笔记(二):类与对象—— 构造函数与析构函数

一、构造函数和析构函数

1. 核心作用

C++ 中每个对象都会经历创建初始化 和销毁清理 两个阶段,编译器提供两种特殊函数自动完成这两个操作,无需程序员手动调用:

构造函数:对象创建时自动调用,用于给成员属性初始化,保证对象出厂就有合法初始值,避免随机垃圾数据。

析构函数:对象销毁前自动调用,用于释放堆内存、关闭文件、释放资源等清理工作,防止内存泄漏。

2. 构造函数语法规则

复制代码

类名() { 函数体 }

硬性要求:

函数名必须与类名完全相同,大小写一致;

无返回值,不写void,也不能写int等任何返回类型;

可以有参数,支持函数重载;

对象创建时自动调用,且仅调用 1 次,不能手动重复调用。

3. 析构函数语法规则

复制代码

~类名() { 清理逻辑 }

硬性要求:

函数名是~加类名;

无返回值、无参数,不支持重载,一个类只能有 1 个析构函数;

对象生命周期结束时自动调用,仅调用 1 次。

4. 基础示例代码

复制代码

#include

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;,缺少无参构造编译报错;

析构无参数、不能重载,一个类只能有一个;

匿名对象本行执行完立刻销毁,不会延续生命周期。