C++ 类和对象(中):默认成员函数详解
介绍 C++ 类中的六个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址重载及 const 成员函数。重点讲解了构造函数的初始化作用、析构函数的资源清理机制,以及浅拷贝与深拷贝的区别。通过 Date 和 Stack 类的示例,阐述了何时需要自定义这些函数以避免内存泄漏或逻辑错误,帮助初学者掌握面向对象编程的核心基础。

介绍 C++ 类中的六个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址重载及 const 成员函数。重点讲解了构造函数的初始化作用、析构函数的资源清理机制,以及浅拷贝与深拷贝的区别。通过 Date 和 Stack 类的示例,阐述了何时需要自定义这些函数以避免内存泄漏或逻辑错误,帮助初学者掌握面向对象编程的核心基础。

作为 C++ 初学者,类和对象的默认成员函数是入门的核心难点,也是理解 C++ 面向对象的关键。这部分内容的核心是编译器会为我们自动生成一些函数,帮我们完成对象的初始化、清理、拷贝等工作,我们要做的就是搞懂'编译器默认帮我们做了什么''什么时候默认的不够用,需要自己写''自己该怎么写'。
在学默认成员函数前,必须先分清这两个概念,否则后面会越学越乱:
int/char/double/指针,简单理解就是'基础数据类型'。class/struct 关键字自己写的类型,比如 Date(日期类)、Stack(栈类)、Queue(队列类),简单理解就是'自己造的类型'。核心结论:编译器对这两种类型的处理逻辑完全不同,后面所有默认成员函数的讲解,都会围绕这个区别展开。
我们写一个空类(比如 class A{}),看似里面什么都没有,但 C++ 编译器会偷偷为我们自动生成 6 个函数,这些函数就叫默认成员函数。
class A{}; // 空类,编译器自动生成 6 个默认成员函数
这 6 个函数分为 3 类,重点掌握前 4 个,最后 2 个几乎不用自己写,仅作了解:
| 分类 | 函数名称 | 核心作用 |
|---|---|---|
| 初始化 & 清理 | 构造函数 | 给对象'初始化赋值' |
| 析构函数 | 给对象'清理资源' | |
| 拷贝 & 赋值 | 拷贝构造函数 | 用一个对象'造一个新对象' |
| 赋值运算符重载 | 把一个对象'赋值给已存在对象' | |
| 取地址重载 | 普通对象取地址重载 | 取普通对象的地址 |
| const 对象取地址重载 | 取 const 对象的地址 |
C++11 后还新增了移动构造、移动赋值,暂时不用管,先把基础 6 个学透。
我们之前写 C 语言时,定义一个对象后,还要手动调用 Init 函数初始化,比如 Stack st; st.Init();,很麻烦还容易忘。构造函数就是用来替代 Init 的,对象一创建,编译器自动调用它完成初始化。
用 Date 类(日期类)举例,快速理解构造函数的 7 个核心特点,前 6 个是重点:
class Date {
public:
// 构造函数:函数名和类名完全一样,无返回值(连 void 都不用写)
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
Date,构造函数也叫 Date;void,也不用 return 任何东西,C++ 硬性规定;Date d1(2024,7,5);,创建 d1 时自动调用构造函数;Date(){});Date(int year=1, int month=1, int day=1){})。
关键注意:这 3 种默认构造只能存在一个,否则编译器会报'调用歧义'(不知道该调用哪个);_year 可能是一个垃圾值);Date() { _year = 1; _month = 1; _day = 1; } // 调用:Date d1; // 不传参,直接调用
Date(int year, int month, int day) { _year = year; _month = month; _day = day; } // 调用:Date d2(2024,7,5); // 传参调用
Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
// 调用方式 1:不传参(当作无参构造) Date d1;
// 调用方式 2:传 1 个参 Date d2(2024);
// 调用方式 3:传 2 个参 Date d3(2024,7);
// 调用方式 4:传 3 个参 Date d4(2024,7,5);
误区 1:无参构造创建对象时,对象后面不能加括号!
Date d1; // 正确:调用无参/全缺省构造
Date d2(); // 错误:编译器会把这当成'函数声明'(声明一个叫 d2 的函数,返回值是 Date),不是创建对象!
误区 2:同时写无参和全缺省构造,编译报错!
Date(){} // 无参构造
Date(int year=1, int month=1, int day=1){} // 全缺省构造
// 调用 Date d1; 编译器报错:不知道调用哪个,产生歧义
构造函数是对象'出生'时的操作,析构函数就是对象'去世'时的操作。对象的生命周期结束时(比如局部对象出函数体),编译器会自动调用析构函数,核心作用是清理对象申请的堆资源(比如 malloc/new 的空间),替代 C 语言里的 Destroy 函数。
用 Stack 类(栈类)举例,栈需要申请堆空间存数据,必须写析构函数清理:
typedef int STDataType;
class Stack {
public:
// 构造函数:申请堆空间
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType)*n);
_capacity = n;
_top = 0;
}
// 析构函数:类名前加~,无参无返回值
~Stack() {
free(_a); // 释放堆空间
_a = nullptr; // 置空,避免野指针
_capacity = _top = 0;
}
private:
STDataType* _a; // 指向堆空间的指针
size_t _capacity; // 容量
size_t _top; // 栈顶
};
Stack,析构函数就是 ~Stack;_a 不会自动 free,这是核心坑点);一句话总结:类中申请了堆资源(malloc/new),就必须自己写析构函数;没申请堆资源,直接用编译器默认的就行。
Date 类(只有 int 成员,无堆资源)、MyQueue 类(只有 Stack 成员,Stack 自己写了析构,编译器会自动调用);Stack 类(用 malloc 申请了堆空间,默认析构不会 free,会造成内存泄漏)。C 语言实现栈,必须手动调用 Init 初始化、Destroy 清理,忘记调用就会出问题:
// C 语言实现栈
typedef struct Stack {
int* a;
int capacity;
int top;
}Stack;
void StackInit(Stack* st); // 初始化
void StackDestroy(Stack* st); // 清理
int main() {
Stack st;
StackInit(&st); // 手动调初始化,忘写就错
// ...使用栈
StackDestroy(&st); // 手动调清理,忘写就内存泄漏
return 0;
}
C++ 用构造和析构,自动完成初始化和清理,不用手动调用,更安全:
// C++ 实现栈
class Stack {
public:
Stack(){} // 构造:自动初始化
~Stack(){} // 析构:自动清理
};
int main() {
Stack st; // 创建对象,自动调用构造
// ...使用栈
return 0; // 出函数体,自动调用析构
}
简单理解:拷贝构造是'克隆'对象,用已经存在的 A 对象,创建一个全新的 B 对象,B 和 A 的初始值完全一样。比如 Date d2(d1);,用 d1 克隆出 d2,这就是拷贝构造的调用。
拷贝构造也是构造函数,所以满足构造函数的所有特点(函数名和类名相同、无返回值),只是参数有特殊要求。
class Date {
public:
Date(int year=1, int month=1, int day=1) // 普通构造
{
_year = year;
_month = month;
_day = day;
}
// 正确:拷贝构造,参数是 const Date& d(加 const 更安全,避免修改原对象)
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year, _month, _day;
};
错误写法:参数传值(Date(Date d)),编译器直接报错,会引发无穷递归!
Date(Date d) // 错误:传值参数,引发无穷递归
{
_year = d._year;
}
为什么会无穷递归?:C++ 规定,自定义类型传值传参时,必须调用拷贝构造函数。当拷贝构造的参数是 Date d 时,调用 Date d2(d1) 会先把 d1 传值给 d,这一步又要调用拷贝构造,无限循环下去,直到栈溢出。
_year=2024,d2 的 _year 也变成 2024);只要是用一个对象创建新对象,都会调用拷贝构造:
Date d2(d1);、Date d2 = d1;(注意:这是拷贝构造,不是赋值,因为 d2 是新对象);void Func(Date d),调用 Func(d1) 时,d1 传值给 d,调用拷贝构造;Date Func(),函数返回一个对象时,会创建临时对象,调用拷贝构造。定义:编译器自动生成的拷贝构造,对内置类型按字节复制,简单理解就是**'把对象的所有成员值直接复制一份'**。适用场景:类中无堆资源(比如 Date 类,只有 int 成员),浅拷贝完全够用,不用自己写拷贝构造。
Date d1(2024,7,5);
Date d2(d1); // 浅拷贝,d2 的_year/_month/_day 和 d1 完全一样,没问题
定义:不仅复制对象的成员值,还会为新对象重新申请独立的堆资源,并把原对象堆资源的内容复制过去,简单理解就是**'不仅克隆外表,还克隆内部的资源'**。适用场景:类中有堆资源(比如 Stack 类,_a 指针指向堆空间),必须自己写深拷贝,否则会出大问题!
用 Stack 类举例,看看浅拷贝为什么会崩溃:
// 未写深拷贝,用编译器默认的浅拷贝
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2 = st1; // 浅拷贝:st2._a = st1._a(两个指针指向同一块堆空间)
此时 st1 和 st2 的 _a 指针指向同一块堆空间,就像两个人共用一个钱包:
free(_a) 释放了堆空间;free(_a),对已经释放的堆空间再次释放,程序直接崩溃!// Stack 类的深拷贝构造
Stack(const Stack& st) {
// 1. 为新对象申请和原对象一样大的堆空间
_a = (STDataType*)malloc(sizeof(STDataType)*st._capacity);
if (_a == nullptr) // 判空,避免申请失败
{
perror("malloc fail");
return;
}
// 2. 把原对象堆空间的内容复制到新空间
memcpy(_a, st._a, sizeof(STDataType)*st._top);
// 3. 复制其他成员值
_top = st._top;
_capacity = st._capacity;
}
深拷贝后,st1 和 st2 的 _a 指针指向不同的堆空间,各自独立,析构时互不影响,完美解决问题。
一句话:如果一个类需要自己写析构函数(申请了堆资源),就一定需要自己写拷贝构造函数(深拷贝)。
赋值运算符重载和拷贝构造很像,极易混淆,核心区别就是:是否创建新对象。先记住这个结论,再看细节:
C++ 的运算符(+/-/=/==/++ 等)默认只能用于内置类型(比如 int a=1, b=2; a+b;),不能直接用于自定义类型(比如 Date d1, d2; d1 == d2;)。
运算符重载就是:为自定义类型重新定义运算符的含义,让运算符能用于我们自己写的类。
operator + 要重载的运算符(比如重载 == 就是 operator==,重载 = 就是 operator=);operator@);.*、::、sizeof、?:、.;this 指针,代表左操作数)。赋值运算符 = 的重载是默认成员函数,如果我们没写,编译器会自动生成,核心特点如下:
C++ 硬性规定,赋值运算符重载不能写在全局,只能作为类的成员函数。
class Date {
public:
Date(int year=1, int month=1, int day=1) {
_year = year;
_month = month;
_day = day;
}
// 赋值运算符重载:推荐写法
Date& operator=(const Date& d) {
// 防止自赋值:d1 = d1; 没必要执行后续代码
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 返回左操作数,支持连续赋值
}
private:
int _year, _month, _day;
};
const Date& d(加 const 避免修改原对象,加引用避免传值拷贝的开销);Date&(加引用避免返回值拷贝,支持连续赋值,比如 d1 = d2 = d3;);if (this != &d),如果自己赋值给自己(d1=d1),直接返回,避免无意义操作。和拷贝构造完全一样:
和拷贝构造一致:类中有堆资源(比如 Stack),必须自己写赋值重载的深拷贝,否则会出现浅拷贝的坑(堆资源共用、二次释放)。
用代码举例:
Date d1(2024,7,5); // 调用普通构造
Date d2(d1); // 拷贝构造:d2 是新对象,用 d1 造
Date d3 = d1; // 拷贝构造:看似是赋值,实则 d3 是新对象,编译器会优化为拷贝构造
Date d4; // 调用普通构造,d4 已存在
d4 = d1; // 赋值重载:d4 已存在,把 d1 赋值给 d4
核心判断:看等号左边的对象是否已经存在:
这两部分内容属于'锦上添花',知道是什么、有什么用就行,几乎不需要自己写代码实现。
用 const 修饰的类成员函数,const 写在函数参数列表的后面,比如:
class Date {
public:
void Print() const // const 成员函数
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year, _month, _day;
};
修饰隐含的 this 指针,把 this 指针从 Date* const this(指针本身不可改)变成 const Date* const this(指针本身和指向的内容都不可改),意味着在这个函数里,不能修改类的任何成员变量。
const 对象:只能调用 const 成员函数(不能调用普通成员函数,避免修改成员);简单理解:const 对象是'只读对象',只能调用'只读函数'(const 成员函数)。
取地址重载分为两种:普通对象取地址、const 对象取地址,编译器会自动生成,默认行为就是返回对象的地址(return this;)。
class Date {
public:
// 普通对象取地址重载
Date* operator&() {
return this; // 编译器默认行为
}
// const 对象取地址重载
const Date* operator&() const {
return this; // 编译器默认行为
}
private:
int _year, _month, _day;
};
什么时候需要自己写?:特殊场景,比如不想让别人取到对象的地址,可以手动返回 nullptr(空指针),一般开发中几乎用不到。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online