C++ 构造函数与析构函数:对象生命周期的守护者
在 C++ 面向对象编程中,构造函数和析构函数是类的特殊成员函数,分别负责对象的初始化和资源清理。它们是管理对象生命周期的核心机制,确保对象在创建时处于有效状态,在销毁时释放占用的资源(如动态内存、文件句柄、网络连接等)。本文将系统讲解这两种函数的定义、特性、用法及注意事项。
一、构造函数:对象的“出生证明”
构造函数(Constructor)是类中一种特殊的成员函数,当对象被创建时自动调用,用于初始化对象的成员变量、分配资源等,确保对象从诞生起就处于可用状态。
1. 构造函数的基本特性
- 名称与类名相同:例如类
Person的构造函数名为Person; - 无返回值:不需要也不能声明返回类型(包括
void); - 自动调用:在对象创建时由编译器自动调用,无需手动调用;
- 可重载:允许定义多个参数不同的构造函数(满足不同初始化需求)。
2. 构造函数的类型与示例
(1)默认构造函数(无参构造函数)
不接受任何参数的构造函数,若类中未定义任何构造函数,编译器会自动生成默认构造函数(空实现)。
#include<iostream>#include<string>usingnamespacestd;classPerson{private:string name;intage;public:// 默认构造函数(无参)Person(){name="Unknown";age=0;cout<<"默认构造函数被调用"<<endl;}// 打印对象信息的成员函数voidshow(){cout<<"姓名:"<<name<<",年龄:"<<age<<endl;}};intmain(){Person p1;// 创建对象时,自动调用默认构造函数p1.show();// 输出:姓名:Unknown,年龄:0return0;}(2)带参数的构造函数
通过参数为对象的成员变量赋值,实现灵活初始化。
classPerson{private:string name;intage;public:// 默认构造函数Person(){name="Unknown";age=0;cout<<"默认构造函数被调用"<<endl;}// 带参数的构造函数(重载)Person(string n,inta){name=n;age=a;cout<<"带参数的构造函数被调用"<<endl;}voidshow(){cout<<"姓名:"<<name<<",年龄:"<<age<<endl;}};intmain(){Person p1;// 调用默认构造函数Personp2("Alice",20);// 调用带参数的构造函数p1.show();// 姓名:Unknown,年龄:0p2.show();// 姓名:Alice,年龄:20return0;}(3)拷贝构造函数(复制构造函数)
用于用已有对象初始化新对象,参数为同类对象的引用(通常加const修饰,避免修改原对象)。
- 若未手动定义,编译器会自动生成默认拷贝构造函数,实现浅拷贝(逐个复制成员变量的值)。
示例:手动定义拷贝构造函数
classPerson{private:string name;intage;public:// 带参数的构造函数Person(string n,inta):name(n),age(a){cout<<"带参数的构造函数被调用"<<endl;}// 拷贝构造函数(参数为 const 引用)Person(constPerson&other){name=other.name;// 复制姓名age=other.age;// 复制年龄cout<<"拷贝构造函数被调用"<<endl;}voidshow(){cout<<"姓名:"<<name<<",年龄:"<<age<<endl;}};intmain(){Personp1("Bob",25);// 调用带参数的构造函数Personp2(p1);// 用 p1 初始化 p2,调用拷贝构造函数p2.show();// 输出:姓名:Bob,年龄:25(与 p1 相同)return0;}3. 构造函数的初始化列表
除了在构造函数体内为成员变量赋值,还可以通过初始化列表在对象创建时直接初始化成员变量,语法为:
类名(参数列表):成员变量1(值1),成员变量2(值2),...{// 构造函数体(可省略)}优势:
- 比在函数体内赋值更高效(直接初始化,而非先默认初始化再赋值);
- 必须用于初始化const成员变量和引用成员变量(它们只能在定义时初始化)。
示例:使用初始化列表
classStudent{private:constintid;// const 成员变量(必须初始化)string&name;// 引用成员变量(必须初始化)intscore;public:// 初始化列表初始化 const 和引用成员Student(inti,string&n,ints):id(i),name(n),score(s){cout<<"Student 构造函数被调用"<<endl;}voidshow(){cout<<"ID:"<<id<<",姓名:"<<name<<",分数:"<<score<<endl;}};intmain(){string name="Charlie";Students(1001,name,90);// 初始化列表生效s.show();// 输出:ID:1001,姓名:Charlie,分数:90return0;}二、析构函数:对象的“死亡证明”
析构函数(Destructor)是类中另一种特殊的成员函数,当对象生命周期结束时自动调用,用于释放对象占用的资源(如动态分配的内存、打开的文件等),避免资源泄漏。
1. 析构函数的基本特性
- 名称为
~类名:例如类Person的析构函数名为~Person; - 无参数、无返回值:不能重载(一个类只能有一个析构函数);
- 自动调用:对象销毁时由编译器自动调用(如离开作用域、被
delete释放); - 若未手动定义,编译器会自动生成默认析构函数(空实现)。
2. 析构函数的典型用途
析构函数的核心作用是资源清理,常见场景:
- 释放动态分配的内存(
new分配的空间需在析构函数中delete); - 关闭打开的文件(
fclose)、网络连接; - 释放其他系统资源(如互斥锁、数据库连接)。
示例:用析构函数释放动态内存
classArray{private:int*data;// 动态数组指针intsize;public:// 构造函数:分配动态内存Array(ints):size(s){data=newint[size];// 分配 size 个 int 的内存cout<<"Array 构造函数:分配了 "<<size<<" 个 int 的内存"<<endl;}// 析构函数:释放动态内存~Array(){delete[]data;// 释放动态数组(注意用 delete[])cout<<"Array 析构函数:释放了动态内存"<<endl;}// 为数组赋值voidset(intindex,intvalue){if(index>=0&&index<size){data[index]=value;}}};intmain(){{// 作用域开始Arrayarr(5);// 创建对象,调用构造函数(分配内存)arr.set(0,100);}// 作用域结束,对象 arr 销毁,自动调用析构函数(释放内存)return0;}输出:
Array 构造函数:分配了 5 个 int 的内存 Array 析构函数:释放了动态内存3. 析构函数的调用时机
析构函数的调用时机由对象的生命周期决定:
- 栈上的对象:离开其作用域(如函数返回、代码块结束)时调用;
- 堆上的对象:通过
delete显式释放时调用; - 全局/静态对象:程序结束时调用。
示例:不同生命周期的对象析构时机
classTest{public:Test(string n):name(n){cout<<"构造函数:"<<name<<" 被创建"<<endl;}~Test(){cout<<"析构函数:"<<name<<" 被销毁"<<endl;}private:string name;};// 全局对象TestglobalObj("全局对象");intmain(){cout<<"进入 main 函数"<<endl;// 栈上的局部对象TestlocalObj("栈上局部对象");// 堆上的对象(动态分配)Test*heapObj=newTest("堆上对象");deleteheapObj;// 手动释放,触发析构函数cout<<"离开 main 函数"<<endl;return0;}输出(顺序说明析构时机):
构造函数:全局对象 被创建 // 程序启动时初始化全局对象 进入 main 函数 构造函数:栈上局部对象 被创建 // 进入作用域时 构造函数:堆上对象 被创建 // new 时 析构函数:堆上对象 被销毁 // delete 时 离开 main 函数 析构函数:栈上局部对象 被销毁 // 离开 main 作用域时 析构函数:全局对象 被销毁 // 程序结束时三、特殊场景与注意事项
1. 浅拷贝与深拷贝问题(拷贝构造函数的坑)
当类中包含指针成员(如动态内存)时,编译器自动生成的默认拷贝构造函数会执行浅拷贝(仅复制指针的值,而非指针指向的内容),导致两个对象的指针指向同一块内存,析构时会对同一块内存释放两次(程序崩溃)。
解决方案:手动实现拷贝构造函数,进行深拷贝(复制指针指向的内容,而非指针本身)。
示例:浅拷贝的问题与深拷贝的解决
classString{private:char*str;// 动态字符串指针public:// 构造函数:分配内存并初始化String(constchar*s){intlen=strlen(s)+1;str=newchar[len];strcpy(str,s);cout<<"构造函数:创建字符串 "<<str<<endl;}// 【问题】默认拷贝构造函数(浅拷贝)会导致指针共享内存// 手动实现深拷贝的拷贝构造函数String(constString&other){intlen=strlen(other.str)+1;str=newchar[len];// 重新分配内存strcpy(str,other.str);// 复制内容(深拷贝)cout<<"拷贝构造函数:复制字符串 "<<str<<endl;}// 析构函数:释放内存~String(){cout<<"析构函数:释放字符串 "<<str<<endl;delete[]str;}};intmain(){Strings1("Hello");Strings2(s1);// 用 s1 初始化 s2,调用深拷贝构造函数return0;// 两个对象析构时释放各自的内存,无冲突}2. 构造函数与析构函数的访问权限
- 构造函数通常为
public,确保能在类外创建对象; - 若构造函数为
private,则无法在类外创建对象(常用于单例模式,确保全局只有一个对象); - 析构函数一般为
public,若为private,则对象无法被销毁(需通过类内成员函数手动释放)。
3. 不要在析构函数中抛出异常
析构函数的职责是清理资源,若在析构函数中抛出未捕获的异常,会导致程序直接终止(因为此时可能已有一个异常在处理中,C++ 不允许同时处理多个异常)。
4. 构造函数中不能调用虚函数
C++ 中,构造函数执行时,对象的虚函数表尚未完全初始化,此时调用虚函数不会触发多态(只会调用当前类的版本),可能导致逻辑错误。
四、总结
构造函数和析构函数是 C++ 管理对象生命周期的核心机制:
- 构造函数:对象创建时自动调用,负责初始化成员变量、分配资源,确保对象可用;支持重载(默认、带参、拷贝构造函数),推荐使用初始化列表提升效率。
- 析构函数:对象销毁时自动调用,负责释放资源(动态内存、文件等),避免泄漏;无参数、不能重载,一个类只有一个析构函数。
理解这两种函数的特性和用法,是编写安全、高效 C++ 代码的基础,尤其要注意指针成员的深拷贝问题和资源释放的完整性。