作为 C++ 初学者,类与对象是从 "面向过程" 转向 "面向对象" 的第一道坎,而默认成员函数又是类与对象的核心骨架。这篇文章用萌新能看懂的语言,拆解 6 个默认成员函数的底层逻辑、使用坑点和实战技巧,帮你彻底搞懂 "类到底是怎么工作的"!
📚目录
1.【开胃小菜】空类真的 "空" 吗?------6 个默认成员函数揭秘
2.【对象的出生证明】构造函数:给对象 "初始化" 的专属保姆
3.【对象的告别仪式】析构函数:帮对象 "清理垃圾" 的清洁工
4.【对象的克隆术】拷贝构造函数:如何复制一个一模一样的对象?
5.【对象的交换术】赋值运算符重载:给已有对象 "换新装"
6.【++ 的小秘密】前置 ++ vs 后置 ++:为啥参数多一个 int?
7.【对象的只读模式】const 成员函数:不让改的对象该怎么用?
8.【冷门但必考】取地址重载:几乎用不上但要知道的知识点
9.【实战大练兵】完整日期类:把所有知识点串起来10.【萌新避坑清单】核心知识点 + 常见错误总结
1. 【开胃小菜】空类真的 "空" 吗?------6 个默认成员函数揭秘
刚学 C++ 时,我一度以为写个空类class Date {};就真的啥都没有了,直到老师说 "编译器会偷偷给你加 6 个函数",才知道自己太天真了!
**核心结论:**如果一个类中没有显式定义任何成员,编译器会自动生成 6 个默认成员函数,这些函数是类对象能正常使用的基础,就像给空房子配了必备的家具。
初始化 / 清理组:构造函数(给对象 "装修")、析构函数(给对象 "收尾")
拷贝 / 赋值组:拷贝构造函数(复制新对象)、赋值运算符重载(给旧对象换新值)
取地址组:普通对象取地址、const 对象取地址(几乎用不上,但编译器会默认生成)
理解:你可以把类想象成 "汽车模板",对象是具体的 "一辆车"。默认成员函数就是工厂给汽车配的基础功能:造新车(构造)、报废车(析构)、复制新车(拷贝构造)、给车换零件(赋值)、查车牌(取地址)------ 哪怕你没说要,工厂都会默认给。
2. 【对象的出生证明】构造函数:给对象 "初始化" 的专属保姆
构造函数是我初学最容易搞混的函数:明明叫 "构造",却不是 "创建对象",而是 "初始化对象"!
2.1 核心特性(必记)
1.函数名和类名一模一样(比如Date()),没有返回值(连void都不用写,这点我总忘);
2.对象创建时自动调用,一生只调一次(就像人出生只办一次出生证明);
3.支持重载(可以写多个,比如无参、带参);
4.如果你没写,编译器会自动生成一个 "无参默认构造函数"------ 但这个默认的有坑!
2.2 最容易踩的坑
坑 1:默认构造函数对内置类型不初始化
编译器生成的默认构造函数,只会给自定义类型成员初始化(比如类里套了另一个类),但对int/char这些内置类型,直接留随机值
cpp
class Date {
private:
int _year; // 内置类型,默认构造后是随机值
};
int main() {
Date d;
// 打印_year会是一串乱码,新手容易以为"初始化了就有值"
return 0;
}
解决办法:C++11 后,内置类型可以在类里直接给默认值(强烈推荐):
cpp
class Date {
private:
int _year = 1900; // 给默认值,解决随机值问题
int _month = 1;
int _day = 1;
};
坑 2:无参构造创建对象不能加括号
cpp
Date d1; // ✅ 正确(我一开始总写成下面这样)
Date d1(); // ❌ 错误!编译器会以为你声明了一个返回Date的函数
坑 3:默认构造函数只能有一个
无参构造、全缺省构造、编译器默认构造,都叫 "默认构造函数",写多了会报错!推荐写全缺省构造函数(最实用):
cpp
class Date {
public:
// 全缺省构造:想初始化就传参,不想传就用默认值
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 新手用法示例
int main() {
Date d1; // 用默认值:1900-1-1
Date d2(2024); // 只传年:2024-1-1
Date d3(2024, 3); // 传年和月:2024-3-1
Date d4(2024, 3, 10); // 传全部:2024-3-10
return 0;
}
2.3 总结
构造函数的核心是 "初始化",不是 "创建"。新手优先写全缺省构造函数,避免踩内置类型未初始化的坑!
3. 【对象的告别仪式】析构函数:帮对象 "清理垃圾" 的清洁工
析构函数是我初学最容易忽略的函数,直到写栈(Stack)类出现内存泄漏,才知道它的重要性。
3.1 核心特性(白话版)
1.函数名是类名(比如Date()),无参数、无返回值;
2.对象生命周期结束时自动调用(比如函数执行完、main 函数结束);
3.一个类只能有一个析构函数,不能重载;
4.编译器生成的默认析构函数:只清理自定义类型,内置类型不管(和构造函数反过来)。
3.2 什么时候需要手写析构函数?(关键)
核心原则:类里有没有申请 "堆内存 / 文件句柄 / 网络连接" 等资源?
✅ 不需要手写:Date 类(只有 int 成员,无资源申请);
❌ 必须手写:Stack 类(用 malloc 申请了堆内存,不释放会内存泄漏)。
cpp
typedef int DataType;
class Stack {
public:
// 构造函数:申请堆内存
Stack(size_t capacity = 3) {
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == NULL) {
perror("malloc失败"); // 新手要加错误提示,方便调试
exit(-1);
}
_capacity = capacity;
_size = 0;
}
// 析构函数:释放堆内存(新手一定要写!)
~Stack() {
if (_array) { // 先判断是否为空,避免重复释放
free(_array); // 释放申请的内存
_array = NULL; // 置空,避免野指针(新手必做)
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array; // 堆内存指针
int _capacity;
int _size;
};
3.3 避坑点
1.别漏写析构函数 :堆内存申请了必须释放,否则程序运行久了会卡顿(内存泄漏 );
2.别重复释放:析构函数里先判断指针是否为空,避免程序崩溃 ;
3.析构函数不销毁对象 :只是清理资源 ,对象本身的内存由编译器回收。
4. 【对象的克隆术】拷贝构造函数:如何复制一个一模一样的对象?
拷贝构造是我初学最难理解的函数,尤其是 "为什么必须传引用",直到老师画了递归图才明白。
4.1 核心特性(拆解)
是构造函数的重载,函数名和类名相同;
1.参数必须是类的 const 引用(const Date& d)------ 新手记死这条!
2.为什么加 const ?防止不小心改了被拷贝的对象;
3.为什么传引用?传值会触发 "无穷递归"(传值参数要拷贝,拷贝又要传值,无限循环);
4.没手写的话,编译器生成默认拷贝构造 ------ 做 "浅拷贝"(按字节复制)。
4.2 浅拷贝 vs 深拷贝(必懂)
浅拷贝(编译器默认):像 "复制文件快捷方式"
原理:按内存字节逐字复制,指针变量只复制地址,不复制指向的内容 ;
适用场景:无资源申请的类(Date 类) ;
坑点:有资源申请的类(Stack 类)会出大问题!
踩坑示例:Stack 类浅拷贝的灾难
cpp
int main() {
Stack s1;
Stack s2 = s1; // 浅拷贝:s1和s2的_array指向同一块堆内存
// 函数结束时,s2先析构,释放了_array;s1再析构,重复释放同一块内存→程序崩溃!
return 0;
}
深拷贝(必须手写):像 "复制文件本身"
原理:不仅复制指针,还会重新申请一块内存,把指针指向的内容也复制过去;
适用场景:有资源申请的类(Stack 类);
实战:Stack 类深拷贝构造函数
cpp
// Stack类中添加拷贝构造函数
Stack(const Stack& s) {
// 1. 重新申请内存(和s的容量一样)
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (_array == NULL) {
perror("malloc失败");
exit(-1);
}
// 2. 复制内容(不是复制地址!)
memcpy(_array, s._array, sizeof(DataType) * s._size);
// 3. 复制其他成员
_size = s._size;
_capacity = s._capacity;
}
4.3 拷贝构造的调用场景(易漏)
除了Date d2 = d1,这两种场景也会调用(笔试常考):
1.函数参数是类对象(值传递):void func(Date d) {};
2.函数返回值是类对象(值返回):Date func() { Date d; return d; };
优化建议:函数参数尽量用引用(const Date& d),避免不必要的拷贝!
5. 【对象的交换术】赋值运算符重载:给已有对象 "换新装"
赋值重载和拷贝构造很像,我初学总搞混 ------ 核心区别是:拷贝构造是 "创建新对象",赋值重载是 "给已有对象赋值"。
5.1 运算符重载基础(入门)
C++ 允许我们给运算符(+、=、== 等)重新定义规则,让类对象也能像 int 一样用运算符。
函数名格式:operator+运算符(比如operator=、operator==);
注意:5 个运算符不能重载(.*、::、sizeof、?:、.),记不住就先记 "赋值运算符只能重载为成员函数"。
5.2 赋值运算符重载的 "黄金格式"(必背)
cpp
类名& operator=(const 类名& 变量名) {
// 1. 检测自赋值(避免自己赋值给自己)这里需要重点记忆一下
if (this != &变量名) {
// 2. 清理当前对象的旧资源(有资源时写)
// 3. 复制新值/申请新资源
// 4. 返回*this(支持连续赋值)
}
return *this;
}
5.3 实战:Date 类赋值重载
cpp
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 赋值运算符重载(黄金格式)
Date& operator=(const Date& d) {
// 1. 检测自赋值(新手容易忘,比如d1 = d1)
if (this != &d) {
// 2. 复制值(Date类无资源,直接赋值)
_year = d._year;
_month = d._month;
_day = d._day;
}
// 3. 返回*this,支持d1 = d2 = d3
return *this;
}
private:
int _year, _month, _day;
};
// 新手用法示例
int main() {
Date d1(2024, 3, 10);
Date d2;
d2 = d1; // 调用赋值重载,d2变成2024-3-10
Date d3;
d3 = d2 = d1; // 连续赋值,新手要知道为什么能行:返回的是引用
return 0;
}
5.4 区分:拷贝构造 vs 赋值重载

6. 【++ 的小秘密】前置 ++ vs 后置 ++:为啥参数多一个 int?
初学 ++ 重载时,我最疑惑的是 "后置 ++ 为啥要加个没用的 int ",其实这是 C++ 的特殊规定 ------ 专门用来区分前置和后置。
6.1 核心区别

6.2 实战:Date 类 ++ 重载
cpp
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 前置++:先加后用,返回引用(新手记:无int参数)
Date& operator++() {
_day += 1; // 先自增
return *this; // 返回自己,支持++(++d)
}
// 后置++:先用后加,返回值(新手记:有int参数)
Date operator++(int) {
Date temp(*this); // 先保存当前值(关键!)
_day += 1; // 再自增
return temp; // 返回旧值,临时对象不能用引用
}
// 新手调试用:打印日期
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year, _month, _day;
};
// 新手测试示例
int main() {
Date d(2024, 3, 10);
Date d1 = ++d; // 前置++:d变成3-11,d1也是3-11
d1.Print(); // 输出:2024-3-11
Date d2(2024, 3, 10);
Date d3 = d2++; // 后置++:d2变成3-11,d3是3-10
d3.Print(); // 输出:2024-3-10
d2.Print(); // 输出:2024-3-11
return 0;
}
6.3 理解
后置 ++ 的 int 参数只是 "标记",编译器会自动传一个 0 进去,我们不用管它。新手记住:前置 ++ 无 int、返回引用;后置 ++ 有 int、返回值!
7. 【对象的只读模式】const 成员函数:不让改的对象该怎么用?
const 成员函数是我初学最容易出错的点 ------ 写了 const 对象却调不了成员函数,查了半天才知道是 const 的问题。
7.1 核心原理(白话版)
const 修饰成员函数,其实是给隐藏的this指针加 const:
普通成员函数:this是Date const(指针本身不能改,指向的内容能改); *
const 成员函数:this是const Date const(指针和指向的内容都不能改)。 *
理解:const 成员函数里,不能修改类的任何成员变量(相当于 "只读模式")。
7.2 调用规则(笔试高频,记死)
1.const 对象 ❌ 不能调用非 const 成员函数(只读对象不能改);
2.非 const 对象 ✅ 可以调用 const 成员函数(可写对象允许只读);
3.const 成员函数 ❌ 不能调用非 const 成员函数(只读函数里不能改对象);
4.非 const 成员函数 ✅ 可以调用 const 成员函数(可写函数里允许只读)
cpp
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 非const成员函数(可以改成员变量)
void SetDay(int day) {
_day = day; // 能修改
}
// const成员函数(不能改成员变量)
void Print() const {
// _day = 10; // ❌ 错误!const函数里不能改成员变量
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year, _month, _day;
};
int main() {
Date d1(2024, 3, 10); // 非const对象
d1.SetDay(11); // ✅ 能调非const函数
d1.Print(); // ✅ 能调const函数
const Date d2(2024, 3, 10); // const对象
// d2.SetDay(11); // ❌ 错误!const对象不能调非const函数
d2.Print(); // ✅ 能调const函数
return 0;
}
7.4 建议
写成员函数时,只要函数不修改成员变量,就加 const(比如 Print 函数)------ 这样 const 对象和非 const 对象都能调用,更通用!
8. 【冷门但必考】取地址重载:几乎用不上但要知道的知识点
取地址重载是 6 个默认成员函数里最冷门的,新手几乎用不到,但笔试偶尔会问,简单了解即可。
8.1 核心知识点(速记)
1.包括两个函数:普通对象取地址、const 对象取地址;
2.编译器默认生成的版本:直接返回this指针(对象的地址);
3.自定义场景:想隐藏对象的真实地址(比如单例模式),新手暂时不用管。
cpp
class Date {
public:
// 普通对象取地址重载
Date* operator&() {
return this; // 默认就是返回this
}
// const对象取地址重载
const Date* operator&() const {
return this; // 默认就是返回this
}
private:
int _year, _month, _day;
};
9. 【实战大练兵】完整日期类:把所有知识点串起来
学完单个知识点,新手一定要整合起来写完整类 ------ 这是检验是否真的掌握的关键!以下是适合新手的完整 Date 类声明(带详细注释):
cpp
#include <iostream>
#include <cstdlib> // 用于exit
using namespace std;
class Date {
public:
// 1. 获取某年某月的天数(新手工具函数)
int GetMonthDay(int year, int month) {
// 新手注意:数组下标从0开始,month从1开始,所以加个空的0月
static int days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
// 闰年2月特殊处理(新手要记闰年规则:能被4整除且不能被100整除,或能被400整除)
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
}
return days[month];
}
// 2. 全缺省构造函数(新手优先)
Date(int year = 1900, int month = 1, int day = 1) {
// 新手优化:加合法性检查,避免无效日期
if (year < 1900 || month < 1 || month > 12 || day < 1 || day > GetMonthDay(year, month)) {
cout << "日期非法:" << year << "-" << month << "-" << day << endl;
exit(-1);
}
_year = year;
_month = month;
_day = day;
}
// 3. 拷贝构造函数(const+引用)
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
// 4. 赋值运算符重载(黄金格式)
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 5. 析构函数(Date类无资源,可省略,新手写出来更规范)
~Date() {}
// 6. 日期+=天数(新手核心:处理跨月/跨年)
Date& operator+=(int day) {
if (day < 0) { // 处理负数,复用-=(新手偷懒技巧)
return *this -= -day;
}
_day += day;
// 新手逻辑:天数超过当月,就减当月天数,月份+1
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
// 月份超过12,年份+1,月份归1
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
// 7. 日期+天数(复用+=,新手注意返回值)
Date operator+(int day) {
Date temp(*this); // 先拷贝当前对象
temp += day; // 复用+=,避免重复写逻辑
return temp; // 值返回,因为temp是局部对象
}
// 8. 前置++(无int参数)
Date& operator++() {
*this += 1; // 复用+=,新手少写代码
return *this;
}
// 9. 后置++(有int参数)
Date operator++(int) {
Date temp(*this);
*this += 1;
return temp;
}
// 10. 关系运算符重载(==)
bool operator==(const Date& d) const {
return _year == d._year && _month == d._month && _day == d._day;
}
// 11. 关系运算符重载(>)
bool operator>(const Date& d) const {
if (_year > d._year) return true;
else if (_year == d._year && _month > d._month) return true;
else if (_year == d._year && _month == d._month && _day > d._day) return true;
return false;
}
// 12. 复用==和>,实现其他关系运算符(新手偷懒技巧)
bool operator>=(const Date& d) const {
return *this > d || *this == d;
}
bool operator<(const Date& d) const {
return !(*this >= d);
}
bool operator<=(const Date& d) const {
return !(*this > d);
}
bool operator!=(const Date& d) const {
return !(*this == d);
}
// 13. 打印函数(const修饰,新手好习惯)
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 新手测试代码
int main() {
Date d(2024, 2, 28);
d += 3; // 2024是闰年,2月有29天,结果应该是2024-3-2
d.Print(); // 输出:2024-3-2
Date d2 = d + 10; // 2024-3-12
d2.Print();
Date d3 = ++d2; // 2024-3-13
d3.Print();
return 0;
}
10. 【萌新避坑清单】核心知识点 + 常见错误总结
10.1 核心知识点(必背)
1.空类不空:编译器自动生成 6 个默认成员函数;
2.构造函数:初始化对象,全缺省最实用,内置类型要给默认值;
3.析构函数:清理资源,有堆内存必须手写,避免内存泄漏;
4.拷贝构造:参数必须是 const 引用,有资源要写深拷贝;
5.赋值重载:黄金格式(自赋值检测 + 返回 * this),区分拷贝构造;
6.const 成员函数:不修改成员变量就加 const,const 对象只能调 const 函数
10.2 新手常见错误(我踩过的雷)
1.无参构造创建对象加括号(Date d(););
2.拷贝构造参数没加引用,导致无穷递归;
3.Stack 类漏写析构函数,导致内存泄漏;
4.后置 ++ 忘记保存旧值,或返回引用;
5.const 对象调用非 const 成员函数;
6.赋值重载忘记检测自赋值,导致资源重复释放。
