
上一篇我们聊了类与对象的基础:封装、访问控制、构造函数初探。今天我们深入一步,聊聊对象从被创建、复制,到最终销毁整个生命周期中的关键机制。这个过程听起来抽象,但其实决定了你的程序是否稳定、是否内存安全。尤其当你开始管理动态内存(比如 new/malloc)时,这些知识就是你程序的“安全带”。
当对象拥有动态资源(比如堆内存)时,如何安全地创建、复制、销毁它? 一不小心,程序就会内存泄漏、双重释放,甚至直接崩溃!
这篇文章,将带你掌握对象生命周期的核心机制,关键内容:
const 成员函数与取地址重载的使用场景准备好了吗?我们出发!
即使你一个函数都不写,C++ 编译器也会为你的类自动生成 6 个默认成员函数,它们是:
const 版)💡 重点提醒: 前 4 个是核心!后两个极少需要自定义(除非你想“隐藏对象地址”,后面会讲)。 C++11 后还增加了移动构造和移动赋值,我们后续再聊。
一句话比喻: 如果说类是一栋房子的设计图纸,对象就是建好的房子; 构造函数是装修队,负责“布置好屋内”; 析构函数是拆迁队,负责“清理干净垃圾”; 拷贝构造则是“照着房子再建一栋一模一样的”。
学习默认成员函数,要思考两个问题:
构造函数的主要任务不是分配空间(栈对象在函数调用时空间已分配),而是初始化对象的状态。它替代了 C 语言中手动调用 Init() 的繁琐流程。
void 都不能写)Date(int y=1, int m=1, int d=1))int, char* 等):不初始化(值随机!)Stack):调用其默认构造函数Date d3(); 不是对象!Date d1; // ✅ 调用默认构造
Date d2(2025,1,1); // ✅ 调用带参构造
Date d3(); // ❌ 这是函数声明!不是对象!编译器会警告:warning C4930: "Date d3(void)": 未调用原型函数
记住:无参构造创建对象,后面不要加括号!
⚠️ 注意:这三者只能存在一个!否则会调用歧义。
int, char*):不初始化(值是随机的!)Stack):调用其默认构造函数Date 类的多种构造方式#include <iostream>
using namespace std;
class Date {
public:
// 1. 无参构造
Date() {
_year = 1; _month = 1; _day = 1;
}
// 2. 带参构造
Date(int year, int month, int day) {
_year = year; _month = month; _day = day;
}
// 3. 全缺省构造(与上面互斥,此处注释)
// Date(int year = 1, int month = 1, int day = 1) { ... }
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year, _month, _day;
};
int main() {
Date d1; // 调用默认构造
Date d2(2025, 1, 1); // 调用带参构造
d1.Print(); d2.Print();
// 注意:Date d3(); 不是对象!而是函数声明(经典坑)
return 0;
}✅ 最佳实践:用全缺省构造,既能当默认构造用,又能接受参数,一举两得!
析构函数不负责销毁对象本身(栈对象随栈帧自动释放),而是负责清理对象持有的资源,比如动态分配的内存、打开的文件等。
~类名(如 ~Date())Date)→ 不用写Stack)→ 必须写!否则内存泄漏!💡 经验法则: 如果类中有资源申请(如
malloc/new),必须显式写析构函数!否则会导致内存泄漏。
Stack 类的析构与 MyQueue 的自动析构#include <iostream>
using namespace std;
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n; _top = 0;
}
~Stack() {
cout << "~Stack()" << endl;
free(_a); // 释放动态内存
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
// 无需显式写析构!编译器会自动调用 Stack 的析构
private:
Stack pushst;
Stack popst;
};
int main() {
Stack st;
MyQueue mq;
return 0; // st 先析构,mq 后析构(但 mq 内部两个 Stack 会正确析构)
}✅ 对比 C 与 C++: C 中必须手动调用
Init和Destroy,一不小心就漏掉; C++ 的构造/析构让资源管理自动化、更安全!
当用一个已存在的对象去初始化另一个新对象时,会调用拷贝构造函数。
Date)→ 默认即可Stack)→ 必须写深拷贝!MyQueue)→ 只要成员类实现了深拷贝,自己不用写⚠️ 致命问题: 如果类中包含指针成员指向动态资源(如
Stack的_a), 浅拷贝会导致两个对象的指针指向同一块内存,析构时double free → 程序崩溃!
Stack 的深拷贝实现class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n; _top = 0;
}
// 深拷贝构造:为新对象分配独立内存
Stack(const Stack& st) {
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
~Stack() {
free(_a); _a = nullptr;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack st1;
st1.Push(1); st1.Push(2);
Stack st2 = st1; // 调用深拷贝构造,安全!
return 0; // 两个 Stack 各自析构,无冲突
}💡 黄金法则: 只要写了析构函数(说明有资源管理),就必须写拷贝构造! 否则默认浅拷贝会让你掉进内存陷阱。
赋值(d1 = d2)和拷贝构造(Date d1(d2))完全不同:
const 类&(避免拷贝)类&(支持连续赋值:a = b = c)Date 的赋值重载(虽简单,但展示规范写法)class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year; _month = month; _day = day;
}
Date& operator=(const Date& d) {
if (this != &d) { // 自赋值检查
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 支持连续赋值:a = b = c;
}
private:
int _year, _month, _day;
};🔁 同样适用黄金法则: 有资源管理 → 必须重载赋值运算符,实现深拷贝逻辑!
C++ 允许为类类型重载运算符,让自定义对象像内置类型一样使用。
operator@(如 operator==). .* :: sizeof ?:this,参数比操作数少一个<< 和 >> 通常重载为全局函数(否则变成 obj << cout,反人类!)Date 的比较与自增重载bool operator==(const Date& d) const {
return _year == d._year && _month == d._month && _day == d._day;
}
// 前置++
Date& operator++() {
*this += 1;
return *this;
}
// 后置++(靠 int 参数区分)
Date operator++(int) {
Date tmp = *this;
*this += 1;
return tmp;
}💡 技巧:后置++ 通过 int 参数与前置++ 构成重载,参数名可省略。
机制 | 作用 | 何时需自定义 |
|---|---|---|
构造函数 | 初始化对象 | 需要初始化成员尤其是资源 |
析构函数 | 清理资源 | 类中持有动态资源(new/malloc) |
拷贝构造函数 | 用已有的对象初始化新对象 | 有资源管理避免浅拷贝 |
赋值重载 | 已有的对象间复制 | 有资源管理避免浅拷贝 |
Date d(); 是对象? → 错!是函数声明。delete 控制,析构只负责清理资源。
💬 思考题: 如果一个类有指针成员,但你只用了
vector而不是裸指针,还需要写析构/拷贝构造吗? (提示:RAII 原则)
下篇我们将进入高级类特性:
+、<< 的 Vector3D 向量类“类与对象是 C++ 的基石,而运算符重载与静态成员,让它拥有了工程级的表达力。”
敬请期待《C++类与对象·下》!