跳到主要内容
C++ 类和对象(中):默认成员函数详解 | 极客日志
C++ 算法
C++ 类和对象(中):默认成员函数详解 综述由AI生成 介绍 C++ 类中的六个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址重载及 const 成员函数。重点讲解了构造函数的初始化作用、析构函数的资源清理机制,以及浅拷贝与深拷贝的区别。通过 Date 和 Stack 类的示例,阐述了何时需要自定义这些函数以避免内存泄漏或逻辑错误,帮助初学者掌握面向对象编程的核心基础。
魔尊 发布于 2026/3/27 更新于 2026/6/1 26 浏览C++ 类和对象(中):默认成员函数详解
作为 C++ 初学者,类和对象的默认成员函数是入门的核心难点,也是理解 C++ 面向对象的关键。这部分内容的核心是编译器会为我们自动生成一些函数,帮我们完成对象的初始化、清理、拷贝等工作,我们要做的就是搞懂'编译器默认帮我们做了什么''什么时候默认的不够用,需要自己写''自己该怎么写'。
前置基础:2 个核心概念
在学默认成员函数前,必须先分清这两个概念,否则后面会越学越乱:
1. 内置类型 vs 自定义类型
内置类型 :C++ 语言自带的'原生类型',不用我们自己定义,比如 int/char/double/指针,简单理解就是'基础数据类型'。
自定义类型 :我们用 class/struct 关键字自己写的类型,比如 Date(日期类)、Stack(栈类)、Queue(队列类),简单理解就是'自己造的类型'。
核心结论 :编译器对这两种类型的处理逻辑完全不同,后面所有默认成员函数的讲解,都会围绕这个区别展开。
2. 默认成员函数
我们写一个空类(比如 class A{}),看似里面什么都没有,但 C++ 编译器会偷偷为我们自动生成 6 个函数,这些函数就叫默认成员函数。
class A {};
这 6 个函数分为 3 类,重点掌握前 4 个,最后 2 个几乎不用自己写,仅作了解:
分类 函数名称 核心作用 初始化 & 清理 构造函数 给对象'初始化赋值' 析构函数 给对象'清理资源' 拷贝 & 赋值 拷贝构造函数 用一个对象'造一个新对象' 赋值运算符重载 把一个对象'赋值给已存在对象' 取地址重载 普通对象取地址重载 取普通对象的地址 const 对象取地址重载 取 const 对象的地址
C++11 后还新增了移动构造、移动赋值,暂时不用管,先把基础 6 个学透。
一、构造函数:给对象'出生就赋值',替代手动 Init
我们之前写 C 语言时,定义一个对象后,还要手动调用 Init 函数初始化,比如 Stack st; st.Init();,很麻烦还容易忘。构造函数就是用来替代 Init 的,对象一创建,编译器自动调用它完成初始化 。
1. 构造函数的核心特点
用 Date 类(日期类)举例,快速理解构造函数的 7 个核心特点,前 6 个是重点:
class Date {
public :
Date ( year, month, day) {
_year = year;
_month = month;
_day = day;
}
:
_year;
_month;
_day;
};
int
int
int
private
int
int
int
函数名和类名完全相同 :比如类叫 Date,构造函数也叫 Date;
无返回值 :不用写 void,也不用 return 任何东西,C++ 硬性规定;
自动调用 :对象一创建就执行,不用手动调,比如 Date d1(2024,7,5);,创建 d1 时自动调用构造函数;
可以重载 :一个类可以有多个构造函数,参数列表不同即可(比如无参、带参);
编译器默认生成规则 :如果我们自己写了构造函数 ,编译器就不再自动生成 ;如果我们没写,编译器才会生成一个'无参的默认构造函数';
什么是默认构造函数 :不传参数就能调用的构造函数 ,包含 3 种:
我们没写时,编译器自动生成的无参默认构造 ;
我们自己写的无参构造函数 (Date(){});
我们自己写的全缺省构造函数 (Date(int year=1, int month=1, int day=1){})。
关键注意 :这 3 种默认构造只能存在一个 ,否则编译器会报'调用歧义'(不知道该调用哪个);
编译器默认构造的行为 :
对内置类型成员 :不做任何初始化 (值是随机的,比如 _year 可能是一个垃圾值);
对自定义类型成员 :自动调用该成员的默认构造函数 。
2. 3 种常见的构造函数写法(推荐用全缺省)
(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 ) { _year = year; _month = month; _day = day; }
3. 初学者常见误区 误区 1 :无参构造创建对象时,对象后面不能加括号 !
Date (){}
Date (int year=1 , int month=1 , int day=1 ){}
二、析构函数:给对象'去世前清资源',替代手动 Destroy 构造函数是对象'出生'时的操作,析构函数就是对象'去世'时的操作 。对象的生命周期结束时(比如局部对象出函数体),编译器会自动调用析构函数,核心作用是清理对象申请的堆资源 (比如 malloc/new 的空间),替代 C 语言里的 Destroy 函数。
1. 析构函数的核心特点 用 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;
无参无返回值 :和构造函数一样,不用写 void,也不用 return;
一个类只能有一个析构函数 :不能重载,编译器自动生成的析构函数是唯一的;
自动调用 :对象生命周期结束时自动执行,比如局部对象出函数体、全局对象程序结束时;
编译器默认析构的行为 :
对内置类型成员 :不做任何处理 (比如指针 _a 不会自动 free,这是核心坑点);
对自定义类型成员 :自动调用该成员的析构函数 ;
析构顺序 :局部域的多个对象,后定义的先析构 (比如先创建 d1,再创建 d2,先析构 d2,再析构 d1);
自定义类型成员必析构 :不管我们写不写析构函数,类中的自定义类型成员,都会自动调用自身的析构函数,编译器会帮我们处理。
2. 什么时候需要自己写析构函数?(核心判断准则) 一句话总结 :类中申请了堆资源(malloc/new),就必须自己写析构函数;没申请堆资源,直接用编译器默认的就行 。
不用自己写 :Date 类(只有 int 成员,无堆资源)、MyQueue 类(只有 Stack 成员,Stack 自己写了析构,编译器会自动调用);
必须自己写 :Stack 类(用 malloc 申请了堆空间,默认析构不会 free,会造成内存泄漏 )。
3. 构造和析构的优势:对比 C 语言 C 语言实现栈,必须手动调用 Init 初始化、Destroy 清理,忘记调用就会出问题:
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++ 用构造和析构,自动完成初始化和清理,不用手动调用,更安全:
class Stack {
public :
Stack (){}
~Stack (){}
};
int main () {
Stack st;
return 0 ;
}
三、拷贝构造函数:用'一个对象'造'另一个新对象' 简单理解:拷贝构造是'克隆'对象,用已经存在的 A 对象,创建一个全新的 B 对象,B 和 A 的初始值完全一样 。比如 Date d2(d1);,用 d1 克隆出 d2,这就是拷贝构造的调用。
1. 拷贝构造的核心特点
(1)本质是构造函数的重载 拷贝构造也是构造函数,所以满足构造函数的所有特点(函数名和类名相同、无返回值),只是参数有特殊要求。
(2)参数的硬性规定:第一个参数必须是自身类类型的引用 class Date {
public :
Date (int year=1 , int month=1 , int day=1 )
{
_year = year;
_month = month;
_day = day;
}
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,这一步又要调用拷贝构造,无限循环下去,直到栈溢出。
(3)编译器默认生成的拷贝构造行为
对内置类型成员 :值拷贝 / 浅拷贝 (按字节复制,比如 d1 的 _year=2024,d2 的 _year 也变成 2024);
对自定义类型成员 :自动调用该成员的拷贝构造函数 。
(4)拷贝构造的调用场景(3 种常见场景,必须记住)
直接用对象初始化新对象:Date d2(d1);、Date d2 = d1;(注意:这是拷贝构造,不是赋值,因为 d2 是新对象);
自定义类型传值传参 :比如 void Func(Date d),调用 Func(d1) 时,d1 传值给 d,调用拷贝构造;
自定义类型传值返回 :比如 Date Func(),函数返回一个对象时,会创建临时对象,调用拷贝构造。
2. 浅拷贝 vs 深拷贝
(1)浅拷贝:编译器默认的'表面拷贝' 定义 :编译器自动生成的拷贝构造,对内置类型按字节复制,简单理解就是**'把对象的所有成员值直接复制一份'**。适用场景 :类中无堆资源 (比如 Date 类,只有 int 成员),浅拷贝完全够用,不用自己写拷贝构造。
Date d1 (2024 ,7 ,5 ) ;
Date d2 (d1) ;
(2)深拷贝:需要自己写的'深度克隆' 定义 :不仅复制对象的成员值,还会为新对象重新申请独立的堆资源 ,并把原对象堆资源的内容复制过去,简单理解就是**'不仅克隆外表,还克隆内部的资源'**。适用场景 :类中有堆资源 (比如 Stack 类,_a 指针指向堆空间),必须自己写深拷贝 ,否则会出大问题!
(3)浅拷贝的坑:堆资源被多个对象共用,析构时二次释放
Stack st1;
st1. Push (1 );
st1. Push (2 );
Stack st2 = st1;
此时 st1 和 st2 的 _a 指针指向同一块堆空间 ,就像两个人共用一个钱包:
当对象生命周期结束时,st1 先析构,free(_a) 释放了堆空间;
接着 st2 析构,又 free(_a),对已经释放的堆空间再次释放 ,程序直接崩溃!
(4)深拷贝的实现:为新对象重新申请堆资源
Stack (const Stack& st) {
_a = (STDataType*)malloc (sizeof (STDataType)*st._capacity);
if (_a == nullptr )
{
perror ("malloc fail" );
return ;
}
memcpy (_a, st._a, sizeof (STDataType)*st._top);
_top = st._top;
_capacity = st._capacity;
}
深拷贝后,st1 和 st2 的 _a 指针指向不同的堆空间 ,各自独立,析构时互不影响,完美解决问题。
3. 实用判断准则 一句话 :如果一个类需要自己写析构函数(申请了堆资源),就一定需要自己写拷贝构造函数(深拷贝) 。
四、赋值运算符重载:把'一个对象'赋值给'已存在的另一个对象' 赋值运算符重载和拷贝构造很像,极易混淆,核心区别就是:是否创建新对象 。先记住这个结论,再看细节:
拷贝构造:创建新对象 ,用 A 造 B(B 之前不存在);
赋值重载:不创建新对象 ,把 A 赋值给 B(B 之前已经存在,有自己的初始值)。
1. 先搞懂:什么是运算符重载? C++ 的运算符(+/-/=/==/++ 等)默认只能用于内置类型(比如 int a=1, b=2; a+b;),不能直接用于自定义类型(比如 Date d1, d2; d1 == d2;)。
运算符重载 就是:为自定义类型重新定义运算符的含义 ,让运算符能用于我们自己写的类。
函数名固定:operator + 要重载的运算符(比如重载 == 就是 operator==,重载 = 就是 operator=);
本质是函数:有参数、有返回值,调用时编译器会自动转换成函数调用。
运算符重载的 3 个核心规则(简单记,不深究)
不能创造新运算符(比如不能重载 operator@);
有 5 个运算符绝对不能重载 :.*、::、sizeof、?:、.;
若重载为类的成员函数 ,则参数数比运算符的运算对象少 1 个(因为隐含了 this 指针,代表左操作数)。
2. 赋值运算符重载的核心特点(重点) 赋值运算符 = 的重载是默认成员函数 ,如果我们没写,编译器会自动生成,核心特点如下:
(1)必须重载为类的成员函数 C++ 硬性规定,赋值运算符重载不能写在全局 ,只能作为类的成员函数。
(2)参数和返回值的推荐写法 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 ;
}
private :
int _year, _month, _day;
};
参数 :const Date& d(加 const 避免修改原对象,加引用避免传值拷贝的开销);
返回值 :Date&(加引用避免返回值拷贝,支持连续赋值 ,比如 d1 = d2 = d3;);
自赋值判断 :if (this != &d),如果自己赋值给自己(d1=d1),直接返回,避免无意义操作。
(3)编译器默认生成的赋值重载行为
对内置类型成员 :值拷贝 / 浅拷贝 ;
对自定义类型成员 :自动调用该成员的赋值运算符重载 。
(4)深拷贝的要求 和拷贝构造一致:类中有堆资源(比如 Stack),必须自己写赋值重载的深拷贝 ,否则会出现浅拷贝的坑(堆资源共用、二次释放)。
3. 拷贝构造 vs 赋值重载:终极区分 Date d1 (2024 ,7 ,5 ) ;
Date d2 (d1) ;
Date d3 = d1;
Date d4;
d4 = d1;
五、const 成员函数 & 取地址重载:简单了解,几乎不用写 这两部分内容属于'锦上添花',知道是什么、有什么用就行,几乎不需要自己写代码实现。
1. const 成员函数:让 const 对象能调用类的成员函数
(1)定义 用 const 修饰的类成员函数,const 写在函数参数列表的后面 ,比如:
class Date {
public :
void Print () const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private :
int _year, _month, _day;
};
(2)核心作用 修饰隐含的 this 指针,把 this 指针从 Date* const this(指针本身不可改)变成 const Date* const this(指针本身和指向的内容都不可改),意味着在这个函数里,不能修改类的任何成员变量 。
(3)调用规则(权限匹配)
const 对象:只能调用 const 成员函数 (不能调用普通成员函数,避免修改成员);
普通对象:可以调用 const 成员函数 (权限缩小,C++ 允许)。
简单理解 :const 对象是'只读对象',只能调用'只读函数'(const 成员函数)。
2. 取地址重载:取对象的地址,编译器默认实现足够用 取地址重载分为两种:普通对象取地址 、const 对象取地址 ,编译器会自动生成,默认行为就是返回对象的地址 (return this;)。
class Date {
public :
Date* operator &() {
return this ;
}
const Date* operator &() const {
return this ;
}
private :
int _year, _month, _day;
};
什么时候需要自己写? :特殊场景,比如不想让别人取到对象的地址 ,可以手动返回 nullptr(空指针),一般开发中几乎用不到。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online