详解C++类与对象:构造函数、析构函数与拷贝构造
C++中的类与对象通过特殊的成员函数管理生命周期。本文详解构造函数(初始化)、析构函数(清理)与拷贝构造函数(复制),剖析系统默认行为与自定义实现的差异,重点解决参数名冲突、浅拷贝双重释放等常见错误,提供安全的资源管理与对象拷贝方案。
一. 构造函数
构造函数是类中一种特殊的成员函数,其名称必须与类名完全相同。它的主要职责是在创建对象时自动调用,确保每个数据成员都有一个合适的初始值。在对象的生命周期内,构造函数只被调用一次。与C语言不同,C++允许在对象实例化的同时完成成员变量的初始化。
第1步:理解构造函数的特征
构造函数具备以下核心特征:
- 命名规范:函数名必须与类名一致。
- 返回值:无返回值类型,甚至不能有
void。 - 自动调用:编译器在实例化对象时自动触发,而非手动调用。
- 支持重载:可以定义多个同名但参数不同的构造函数。
第2步:系统默认与用户定义的构造行为
当类未显式定义任何构造函数时,编译器会生成一个默认构造函数。需注意:
- 内置类型(如
int,char)的成员变量不会被初始化,保留随机值。 - 自定义类型(如结构体或类)的成员变量会自动调用其自身的默认构造函数。
第3步:自定义构造函数的实现
用户通常需要通过自定义构造函数来初始化对象,主要分为无参构造和带参构造。
1. 无参构造
不提供任何参数的构造函数,常用于为所有成员设置统一的默认初始状态。
2. 带参构造与全缺省参数
带参构造函数允许在实例化时指定初始值。为了避免版本混乱,推荐使用全缺省参数的构造函数,它在功能上等同于无参构造和有参构造的结合,且符合“默认构造函数只能有一个”的规则(注:此处指只有一个无参的或全缺省的默认构造函数入口)。
常见问题排查:参数遮蔽(Shadowing)
在使用带参构造时,若形参与成员变量同名,容易引发赋值错误。
class A {
public:
A(int year) {
year = year; // 错误!就近原则导致自身赋值
}
private:
int year;
};
int main() {
A a(20);
}
上述代码中,year = year; 执行的是局部变量对自身赋值,导致成员变量 year 仍为随机值。解决该问题的标准做法是使用 this 指针明确指向成员变量:this->year = year;。
推荐写法:
使用全缺省构造函数可以提供灵活的初始化方式:
class Data {
public:
Data(int year = 2022, int month = 12, int day = 12) {
_year = year;
_month = month;
_day = day;
}
private:
int _year, _month, _day;
};
二. 析构函数
当对象生命周期结束时,需要清理其占用的资源(特别是堆内存)。析构函数承担这一责任。
第1步:析构函数的定义与规则
- 以波浪号
~加上类名命名,例如~Stack()。 - 无参数,无返回值。
- 一个类只能有一个析构函数。
- 若未显式定义,编译器生成默认析构函数。
- 局部对象销毁由编译器处理,析构函数负责清理非栈资源的附属工作。
第2步:动态资源的清理实践
对于在构造函数中使用 malloc 或 new 申请的资源,必须在析构函数中进行释放,以防止内存泄漏。
class Stack {
public:
Stack(int capacity = 4) {
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr) {
cout << "malloc fail" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
// 析构函数清理堆内存
~Stack() {
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
注意:析构后最好将指针置为 nullptr,防止悬垂指针引发的二次释放风险。对于内置类型成员,析构函数无需特殊处理;对于自定义类型成员,则会自动调用其析构函数。
三. 拷贝构造函数
当需要使用一个已存在的对象去初始化另一个新对象时,编译器会调用拷贝构造函数。
第1步:拷贝构造的定义与避坑指南
- 定义:只有一个参数,即对本类类型对象的引用(通常用
const修饰)。 - 关键特征:参数必须是引用(
&)。 - 无限递归陷阱:如果采用值传递(
Data s),为了实参传递给形参会再次调用拷贝构造函数,导致死循环直至栈溢出。 - 正确签名:
Data(const Data& other)。
第2步:浅拷贝的危害
若用户未显式定义拷贝构造函数,系统会生成默认的“浅拷贝”。这意味着仅进行字节层面的数值拷贝。如果类中包含指针成员,两个对象将共享同一块堆内存。
下面演示包含指针成员的 String 类在浅拷贝下的崩溃场景:
class String {
public:
String(const char* str = "jack") {
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String() {
cout << "~String()" << endl;
free(_str); // 释放内存
}
private:
char* _str;
};
int main() {
String s;
String s1(s); // 触发默认浅拷贝
return 0;
}
程序在返回时会抛出异常。原因分析如下:
1. s 和 s1 的 _str 指向同一块堆内存。
2. s1 先析构,调用 free(_str),内存被释放。
3. s 后析构,再次调用 free(_str),导致对已释放内存的重复操作,引发运行时错误。
第3步:如何实现深拷贝
为防止共享资源被重复释放,必须实现深拷贝(Deep Copy),即为新对象分配独立的内存空间,并复制原内容。
自定义拷贝构造函数的步骤:
- 检查源对象的有效性。
- 根据源对象的大小/长度重新申请当前对象的内存。
- 将源对象的数据拷贝到新内存中。
- 利用
this指针隔离命名冲突。
class String {
public:
// ... (构造函数略)
// 自定义深拷贝构造函数
String(const String& other) {
_str = (char*)malloc(strlen(other._str) + 1);
strcpy(_str, other._str);
}
~String() {
free(_str);
}
private:
char* _str;
};
这样修改后,s 和 s1 拥有各自独立的堆内存,析构时不会发生冲突。
总结
C++ 类的四个默认成员函数——构造函数、析构函数、拷贝构造函数和赋值运算符重载,共同构成了对象生命周期管理的基石。初学者常犯的错误包括:忽略参数遮蔽导致初始化失败、忘记析构动态内存导致泄漏、以及使用浅拷贝指针导致重复释放。遵循 C++ 的最佳实践(如使用 this 指针、始终对资源管理类实现深拷贝),能显著提升程序的稳定性。
以上就是C++类与对象核心概念的详细内容,更多关于C++面向对象编程的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









