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

一、构造函数和析构函数

1. 核心作用

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

  1. 构造函数:对象创建时自动调用,用于给成员属性初始化,保证对象出厂就有合法初始值,避免随机垃圾数据。
  2. 析构函数:对象销毁前自动调用,用于释放堆内存、关闭文件、释放资源等清理工作,防止内存泄漏。

2. 构造函数语法规则

复制代码
类名() { 函数体 }

硬性要求:

  1. 函数名必须与类名完全相同,大小写一致;
  2. 无返回值,不写void,也不能写int等任何返回类型;
  3. 可以有参数,支持函数重载;
  4. 对象创建时自动调用,且仅调用 1 次,不能手动重复调用。

3. 析构函数语法规则

复制代码
~类名() { 清理逻辑 }

硬性要求:

  1. 函数名是~加类名;
  2. 无返回值、无参数,不支持重载,一个类只能有 1 个析构函数;
  3. 对象生命周期结束时自动调用,仅调用 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:按参数划分
  1. 无参构造函数(默认构造) :括号内无任何参数

    复制代码
    Person(){ ... }
  2. 有参构造函数 :括号内存在一个或多个形参

    复制代码
    Person(int age, string name){ ... }
分类 2:按功能划分
  1. 普通构造函数:无参 / 有参,用于全新对象初始化;

  2. 拷贝构造函数 :形参为本类const引用,作用:用已有对象复制出新对象

    复制代码
    Person(const Person& p){ ... }
逐段拆分解析
1. Person

构造函数的函数名,必须和类名完全一致,这是所有构造函数的硬性规则。

2. 括号内整体:const Person& p 形参(核心难点)

我们拆成三部分:const + Person + & + p

  1. Person 形参的类型:代表本类对象,说明只能接收 Person 类型的对象作为参数,不能传 int、string 等其他类型。
  2. & 引用符号 作用:传递对象的别名 ,不是拷贝一份新对象。 如果不写&,写成Person p(值传递):调用拷贝构造时会无限递归死循环。 原理:值传递会先复制实参,复制操作又会调用拷贝构造,无限套娃报错,所以拷贝构造必须加引用&
  3. const 常量修饰 限制:在拷贝构造函数内部,不能修改传入的原对象 p 。 场景:拷贝只是复制数据,不应该改动源对象,加const防止误写修改原对象,提高代码安全性。
  4. p 形参变量名,随便起,比如objper都可以,只是接收传入对象的别名。
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 种情况会自动触发这个函数)
  1. 已有对象初始化新对象:Person p2 = p1;
  2. 函数值传递 Person 对象:void func(Person p){}
  3. 函数按值返回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:默认提供三大基础函数

程序员不写任何构造 / 析构时,编译器自动生成:

  1. 无参默认构造:空实现,不初始化成员;
  2. 默认析构:空实现,不清理资源;
  3. 默认拷贝构造:浅拷贝,直接逐字节复制所有成员的值。

规则 2:自定义有参构造后,编译器不再提供无参构造

如果手写了任意有参构造,编译器会删除默认无参构造,此时代码中写Person p;直接报错。 示例:

复制代码
class Person
{
public:
    Person(int a){} // 自定义有参构造
};
int main()
{
    Person p; // 报错,不存在无参构造
    return 0;
}

解决:手动补充无参构造函数。

规则 3:只要自定义构造(无参 / 有参),默认拷贝构造依旧存在

只要没有手动写拷贝构造,无论有没有普通构造,编译器都会自动生成浅拷贝的拷贝构造。

规则 4:手动定义拷贝构造,编译器不再生成任何构造(无参、有参默认全部消失)

只要自己写了Person(const Person& p)拷贝构造,编译器不会生成无参、有参默认构造,创建对象只能使用拷贝构造,想要普通构造必须手动实现。

配套总结表格

程序员自定义函数 编译器自动生成
无任何构造 默认无参构造、默认析构、默认拷贝构造
仅写有参构造 无无参构造;保留默认拷贝、默认析构
仅写拷贝构造 无无参 / 有参构造;仅保留默认析构
写了无参 + 有参构造 无默认无参;保留默认拷贝、默认析构
手动写析构 依旧生成三大默认构造,不受影响

五、重点易错点汇总

  1. 构造不能手动调用,析构也不能手动调用,生命周期由系统自动管理;
  2. 无参构造创建对象不能加括号,否则变成函数声明;
  3. 值传递 / 值返回对象会触发拷贝构造,引用传递不会;
  4. 默认拷贝是浅拷贝,如果成员包含堆指针,必须手写深拷贝构造;
  5. 写了有参构造就不能直接Person p;,缺少无参构造编译报错;
  6. 析构无参数、不能重载,一个类只能有一个;
  7. 匿名对象本行执行完立刻销毁,不会延续生命周期。
相关推荐
江畔柳前堤1 小时前
Function Calling 与 Tool Calling:从认知到工程的全景深度解析
开发语言·网络·人工智能·深度学习·算法·机器学习·php
2601_949950631 小时前
Java 面试准备,用练题簿把“背过八股”变成“真正会答”
java·开发语言·面试·刷题·小程序推荐
老洋葱Mr_Onion1 小时前
【C++】信息学奥赛CSP通关之路——CSP-J/S第一轮原创全真模拟试卷集(2026)卷四全卷解析
开发语言·c++
zander2581 小时前
LeetCode 739:每日温度——为什么单调栈要持续弹出
开发语言·python·算法
無限進步D1 小时前
Java 函数式编程(Lambda)
java·开发语言
xzlAwin2 小时前
Win10安装Go语言多版本管理器g工具
开发语言·golang
(initial)2 小时前
C-05. Kernel Fusion 代价边界:少写回 vs 寄存器压力与 occupancy
c语言·开发语言·cuda
汤姆yu3 小时前
基于python大数据的高校舆情监测系统的设计与实现
大数据·开发语言·python·分享·分析·舆情
Cory.眼3 小时前
Python爬虫实战:从入门到进阶
开发语言·爬虫·python