
| 🍕阿i索 | 个人主页 |
|---|---|
| 《C语言专栏》 | 《C++专栏》 |
| 《数据结构专栏》 | 《LaTeX专栏》 |
| 待更新... |
本章节核心讲解类的 6 个默认成员函数,重点掌握构造、析构、拷贝构造、赋值运算符重载四大核心函数的特性、编译器默认行为及手动实现方式,同时理解运算符重载、const 成员函数等关键语法,是 C++ 面向对象封装特性的核心延伸。
一、类的默认成员函数
默认成员函数:用户未显式实现,编译器会自动生成的成员函数。一个空类编译器默认生成 6 个,C++11 后新增移动构造、移动赋值,核心重点为前 4 个:
| 默认成员函数 | 核心功能 | 关键说明 |
|---|---|---|
| 构造函数 | 完成对象的初始化 | 替代手动写的 Init 函数,自动调用 |
| 析构函数 | 完成对象的资源清理 | 替代手动写的 Destroy 函数,自动调用 |
| 拷贝构造函数 | 用同类对象初始化新对象 | 自定义类型拷贝行为必调用 |
| 赋值运算符重载 | 两个已存在对象间的拷贝赋值 | 区分拷贝构造,完成赋值行为 |
| 普通取地址运算符重载 | 返回普通对象的地址 | 编译器默认生成即可,极少手动实现 |
| const 取地址运算符重载 | 返回 const 对象的地址 | 编译器默认生成即可,极少手动实现 |
学习核心🙂:
- 未手动实现时,编译器默认生成的函数行为是什么,是否满足需求;
- 若默认行为不满足,如何手动实现(重点为深拷贝场景)。
二、构造函数
构造函数的核心任务不是开空间创建对象 (对象空间在实例化时已分配),而是在对象实例化时自动完成初始化,替代 C 语言中手动调用的 Init 函数。
2.1 构造函数的特性
基础规则 :函数名与类名一致,无返回值(无需写void);对象实例化时自动调用,且仅调用一次,支持函数重载。
编译器生成逻辑:未显式定义时,编译器自动生成无参默认构造;一旦手动定义任意构造,编译器不再生成。
默认构造与初始化:无需传参即可调用的默认构造包含无参、全缺省、编译器生成的无参构造(三者不可共存,否则调用歧义);编译器默认构造对内置类型成员初始化行为不确定,对自定义类型成员会自动调用其默认构造(无则编译报错)
类型区分:
- 内置类型:语言原生类型,如 int/char/double/ 指针等;
- 自定义类型:用 class/struct 定义的类型,如 Stack/Date/MyQueue 等。
2.2 构造函数的实现示例
#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)
{
_year = year;
_month = month;
_day = day;
}*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用无参/默认构造,不能加括号(Date d1()会被识别为函数声明)
Date d2(2025, 7, 31); // 调用带参构造
d1.Print();
d2.Print();
return 0;
}
2.3 自定义类型的构造调用
编译器默认生成的构造函数对自定义类型成员的初始化,会自动调用其默认构造:
class Stack
{
public:
// Stack的默认构造(全缺省)
Stack(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
// 自定义类型MyQueue,无显式构造
class MyQueue
{
private:
Stack _pushst; // 自定义类型,自动调用Stack的默认构造
Stack _popst; // 自定义类型,自动调用Stack的默认构造
};
int main()
{
MyQueue q; // 编译器生成MyQueue的默认构造,完成两个Stack成员的初始化
return 0;
}
2.4 注意
- 无参构造创建对象时,对象后不能加括号 (
Date d1()会被编译器识别为函数声明); - 全缺省构造和无参构造不能同时存在,否则调用时产生歧义;
- 绝大多数场景需要手动实现构造函数,保证内置类型成员的确定性初始化。
三、析构函数
析构函数的核心任务不是销毁对象本身 (对象空间在生命周期结束时自动释放),而是在对象销毁时自动完成资源清理,替代 C 语言中手动调用的 Destroy 函数。
3.1 析构函数的特性
基础规则 :函数名是类名前加~,无参数、无返回值(无需写void);一个类仅能有一个析构函数,不支持重载;对象生命周期结束时自动调用,且仅调用一次。
编译器生成与成员处理:未显式定义时,编译器自动生成默认析构;默认析构对内置类型成员不处理,对自定义类型成员自动调用其析构(自定义类型成员无论是否手动实现类的析构,都会触发自身析构)。
调用与实现规则:局部域多个对象遵循 "后定义先析构"(栈后进先出);无堆资源的类(如 Date)可用默认析构,有资源申请的类(如 Stack)必须手动实现析构,否则导致内存泄漏
3.2 析构函数的实现示例
#include<iostream>
#include<cstdlib>
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
{
private:
Stack _pushst; // 自定义类型,自动调用Stack的析构
Stack _popst; // 自定义类型,自动调用Stack的析构
};
int main()
{
Stack st; // 生命周期结束时调用~Stack()
MyQueue mq; // 生命周期结束时,编译器生成的~MyQueue()调用两个Stack的析构
return 0;
}
3.3 析构函数对比 C 语言
C++ 的构造 + 析构自动调用,避免了 C 语言中忘记调用 Init/Destroy 的问题:
// C++版Stack实现括号匹配(自动初始化、自动清理)
bool isValid(const char* s) {
Stack st; // 实例化时调用构造,自动初始化
while (*s)
{
if (*s == '[' || *s == '(' || *s == '{')
st.Push(*s);
else
{
if (st.Empty()) return false;
char top = st.Top();
st.Pop();
if ((*s == ']' && top != '[') || (*s == '}' && top != '{') || (*s == ')' && top != '('))
return false;
}
s++;
}
return st.Empty();
// 函数结束,st生命周期结束,自动调用析构释放资源
}
// C语言版Stack实现括号匹配(手动初始化、手动清理,易遗漏)
bool isValid(const char* s) {
ST st;
STInit(&st); // 手动调用初始化
while (*s)
{
// 业务逻辑与C++一致
}
bool ret = STEmpty(&st);
STDestroy(&st); // 手动调用清理,忘记则内存泄漏
return ret;
}
四、拷贝构造函数
拷贝构造函数是特殊的构造函数 ,用于用同类已存在的对象,初始化另一个即将创建的对象 ,C++ 规定自定义类型的拷贝行为必须调用拷贝构造。
4.1 拷贝构造函数的特性
基础规则 :作为构造函数的重载,函数名与类名一致;首个参数必须是const 类名&(传值会引发无穷递归),其余参数若有则需带缺省值;未显式定义时,编译器自动生成默认版本。
默认拷贝逻辑:编译器生成的拷贝构造对内置类型做浅拷贝(按字节拷贝),对自定义类型自动调用其拷贝构造;但类含堆资源指针时,浅拷贝会导致多对象共享堆空间,引发数据混乱、重复释放资源等问题,需手动实现深拷贝(重新申请堆空间并拷贝数据)。
调用与注意事项:自定义类型传值传参 / 返回时会触发拷贝构造(引用方式则不会);局部对象的引用返回会产生野引用,需避免
4.2 拷贝构造的无穷递归原因
若拷贝构造的第一个参数为传值(Date d),则调用拷贝构造时,需要先将实参拷贝给形参,而拷贝实参又需要调用拷贝构造,形成无限递归:
// 错误写法:传值参数,编译器直接报错
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 正确写法:const引用参数,避免拷贝
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
4.3 拷贝构造的实现示例
4.3.1 浅拷贝(适用于无堆资源的类,如 Date)
#include<iostream>
using namespace std;
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;
}
void Print() { cout << _year << "/" << _month << "/" << _day << endl; }
private:
int _year;
int _month;
int _day; // 无堆资源,浅拷贝足够
};
int main()
{
Date d1(2025, 8, 1);
Date d2(d1); // 调用拷贝构造,用d1初始化d2
Date d3 = d1; // 等价于d3(d1),**拷贝构造**(非赋值重载)
d2.Print();
d3.Print();
return 0;
}
4.3.2 深拷贝(适用于有堆资源的类,如 Stack)
#include<iostream>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
// 手动实现深拷贝:为新对象重新申请资源
Stack(const Stack& s)
{
// 1. 申请与原对象相同大小的堆空间
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
// 2. 拷贝原对象的资源数据
memcpy(_a, s._a, sizeof(STDataType) * s._top);
// 3. 拷贝普通成员
_capacity = s._capacity;
_top = s._top;
}
// 析构:释放资源
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
void Push(STDataType x) { /* 入栈逻辑 */ }
private:
STDataType* _a; // 堆资源指针,必须深拷贝
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(st1); // 深拷贝,st1和st2的_a指向不同堆空间
return 0;
}
4.4 拷贝构造的调用场景
- 用已存在对象初始化新对象:
Date d2(d1);、Date d3 = d1;; - 自定义类型传值传参 :
void Func(Date d)(d 为形参,由实参拷贝构造); - 自定义类型传值返回 :
Date Func()(返回值为临时对象,由函数内局部对象拷贝构造); - 容器存储自定义类型:
vector<Date> v; v.push_back(d1);(底层调用拷贝构造)。
4.5 引用传参 / 返回的注意
-
引用传参 :
void Func(const Date& d),不会调用拷贝构造,减少开销,推荐使用; -
引用返回 :若返回的是函数内的局部对象 ,函数结束后局部对象销毁,引用变为野引用 ,必须用传值返回;
// 错误:返回局部对象的引用,野引用
Stack& Func3()
{
Stack st; // 局部对象,函数结束销毁
return st;
}
// 正确:传值返回,调用拷贝构造生成临时对象
Stack Func4()
{
Stack st;
return st;
}
五、运算符重载
C++ 允许为自定义类型重载运算符 ,赋予运算符新的含义,使自定义类型能像内置类型一样使用运算符,运算符重载是具有特殊名字的函数。
5.1 运算符重载的基本语法
核心格式 :函数名固定为operator+要重载的运算符(如operator==、operator+),具备返回值、参数列表和函数体;参数个数与运算对象数一致(一元运算符 1 个,二元运算符 2 个),若为类成员函数则首个运算对象由 this 指针隐式传递,参数个数比运算对象少 1。
重载规则 :优先级和结合性与内置类型一致,不可修改;必须包含至少一个类 / 枚举类型参数,无法重载内置类型运算符,也不能创建新运算符(如operator@);以下5个运算符禁止重载:.*、::、sizeof、?:、.
使用原则 :仅为有实际业务意义的运算符重载(如 Date 类重载-合理,重载+无意义)。
5.2 运算符重载的实现方式
5.2.1 全局函数重载
需解决类私有成员的访问问题,解决方案:①成员设为 public;②提供 getter 函数;③声明为友元函数;④改为成员函数重载(推荐)。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}
int _year;
int _month;
int _day; // 设为public,方便全局函数访问
};
// 全局函数重载==:二元运算符,两个参数
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2025,8,1);
Date d2(2025,8,2);
cout << (d1 == d2) << endl; // 编译器转换为operator==(d1, d2)
return 0;
}
5.2.2 类的成员函数重载
推荐方式,利用 this 指针隐式传递第一个运算对象,参数个数少 1,且可直接访问类的私有成员。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}
// 成员函数重载==:this指针指向d1,参数为d2
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025,8,1);
Date d2(2025,8,2);
cout << (d1 == d2) << endl; // 编译器转换为d1.operator==(d2)
cout << d1.operator==(d2) << endl; // 显式调用,与上等价
return 0;
}
5.3 前置 / 后置 ++ 的重载区分
++ 有前置(++d)和后置(d++),函数名均为operator++,C++ 规定后置 ++ 重载时增加一个 int 形参(仅作区分,无需使用),与前置 ++ 构成重载。
class Date
{
public:
// 前置++:成员函数,返回引用(支持连续++)
Date& operator++()
{
_day++;
// 处理日期进位逻辑...
return *this;
}
// 后置++:增加int形参,返回值(不支持连续++)
Date operator++(int)
{
Date tmp = *this; // 先保存原对象
_day++;
// 处理日期进位逻辑...
return tmp; // 返回原对象值
}
private:
int _year, _month, _day;
};
int main()
{
Date d(2025,8,1);
++d; // 调用d.operator++()
d++; // 调用d.operator++(0)(int形参传任意值均可)
return 0;
}
5.4 流插入 / 流提取的重载
<<(流插入)、>>(流提取)需重载为全局函数 ,若重载为成员函数,this 指针会抢占第一个参数,导致调用形式变为d << cout(不符合使用习惯)。
#include<iostream>
using namespace std;
class Date
{
// 声明为友元,允许全局函数访问私有成员
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
private:
int _year, _month, _day;
};
// 流插入重载:cout << d
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日";
return out; // 返回out,支持连续输出:cout << d1 << d2
}
// 流提取重载:cin >> d
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in; // 返回in,支持连续输入:cin >> d1 >> d2
}
int main()
{
Date d1;
cin >> d1;
cout << d1 << endl;
return 0;
}
六、赋值运算符重载
赋值运算符重载是特殊的运算符重载 ,属于默认成员函数,用于两个已存在的对象之间的拷贝赋值 ,必须重载为类的成员函数。
6.1 区分拷贝构造与赋值重载
| 场景 | 调用函数 | 核心差异 |
|---|---|---|
Date d2(d1); |
拷贝构造 | 用 d1初始化新对象 d2(d2 未存在) |
Date d2; d2 = d1; |
赋值运算符重载 | 用 d1赋值给已存在的 d2(d2 已存在) |
6.2 赋值运算符重载的特性
基础规则:必须重载为类的成员函数(禁止全局重载);未显式定义时,编译器自动生成默认版本,其对内置类型做浅拷贝、对自定义类型自动调用对应赋值重载。
参数与返回值 :参数建议为const 类名&(减少拷贝开销且保护原对象);返回值建议为类类型引用(返回*this),支持连续赋值。
深拷贝要求:函数体需判断自赋值(避免深拷贝场景的资源泄漏);含堆资源的类(如 Stack)必须手动实现深拷贝版重载,否则会出现浅拷贝导致的资源问题。
6.3 赋值运算符重载的实现示例
6.3.1 浅拷贝(适用于无堆资源的类,如 Date)
#include<iostream>
using namespace std;
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) {}
// 赋值运算符重载:浅拷贝
Date& operator=(const Date& d)
{
// 自赋值判断
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 返回自身,支持连续赋值
}
void Print() { cout << _year << "/" << _month << "/" << _day << endl; }
private:
int _year, _month, _day;
};
int main()
{
Date d1(2025,8,1);
Date d2(2025,8,2);
d2 = d1; // 调用赋值重载,d2已存在
d2.Print();
Date d3;
d3 = d2 = d1; // 连续赋值,支持
d3.Print();
return 0;
}
6.3.2 深拷贝(适用于有堆资源的类,如 Stack)
#include<iostream>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
Stack(const Stack& s) // 拷贝构造(深拷贝)
{
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
memcpy(_a, s._a, sizeof(STDataType) * s._top);
_capacity = s._capacity;
_top = s._top;
}
// 赋值运算符重载(深拷贝)
Stack& operator=(const Stack& s)
{
if (this != &s) // 自赋值判断
{
// 1. 释放当前对象的原有资源
free(_a);
// 2. 重新申请资源,拷贝数据
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return *this;
}
memcpy(_a, s._a, sizeof(STDataType) * s._top);
// 3. 拷贝普通成员
_capacity = s._capacity;
_top = s._top;
}
return *this;
}
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1, st2;
st1.Push(1);
st1.Push(2);
st2 = st1; // 深拷贝,st1和st2的_a指向不同堆空间
return 0;
}
七、const 成员函数
7.1 概念与语法
const 成员函数 :用const修饰的类成员函数,const写在函数参数列表的后面,是 C++ 中保证对象只读性的重要语法。
// 语法格式
class Date
{
public:
void Print() const // const成员函数
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year, _month, _day;
};
7.2 特性
本质作用 :const修饰成员函数的隐式this指针,将其类型从类名* const this(仅指向不可改)变为const 类名* const this(指向和指向的内容均不可改),因此 const 成员函数内无法修改类的任何成员变量。
调用规则:const 对象仅能调用 const 成员函数,普通对象可调用 const / 普通成员函数(权限缩小允许);仅读取成员的函数建议定义为 const,提升代码兼容性。
特殊限制:构造、析构函数因需修改对象(初始化 / 清理资源),无法定义为 const 成员函数。
7.3 实现示例
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
// const成员函数:仅读取,不修改
void Print() const
{
// _year = 2025; // 编译报错:const成员函数不能修改成员
cout << _year << "/" << _month << "/" << _day << endl;
}
// 普通成员函数:可修改成员
void SetDay(int day)
{
_day = day;
}
private:
int _year, _month, _day;
};
int main()
{
const Date d1(2025,8,1); // const对象
d1.Print(); // 正确:const对象调用const成员函数
// d1.SetDay(2); // 编译报错:const对象不能调用普通成员函数
Date d2(2025,8,2); // 普通对象
d2.Print(); // 正确:普通对象调用const成员函数
d2.SetDay(3); // 正确:普通对象调用普通成员函数
return 0;
}
八、取地址运算符重载
取地址运算符重载包含普通取地址 和const 取地址 两个版本,属于默认成员函数,编译器自动生成的版本完全满足需求,极少需要手动实现。
8.1 语法格式
class Date
{
public:
// 普通取地址运算符重载:返回普通对象的地址
Date* operator&()
{
return this; // 编译器默认行为,返回对象自身地址
// 特殊场景:return nullptr; // 故意返回空地址,禁止外部取地址
}
// const取地址运算符重载:返回const对象的地址
const Date* operator&() const
{
return this; // 编译器默认行为
}
private:
int _year, _month, _day;
};
8.2 注意
- 手动实现的唯一场景:禁止外部获取对象的真实地址(如故意返回 nullptr);
- 普通对象调用
operator&(),const 对象调用operator&() const(权限匹配); - 日常开发中,直接使用编译器默认生成的版本,无需手动实现。
核心总结
- 默认成员函数是 C++ 封装的核心,编译器会为空类生成 6 个,重点掌握前 4 个(构造、析构、拷贝构造、赋值重载);
- 构造 / 析构:自动完成初始化 / 资源清理,替代 C 语言的手动 Init/Destroy,有堆资源的类必须手动实现析构;
- 浅拷贝 / 深拷贝:无堆资源的类(如 Date)用编译器默认的浅拷贝即可;有堆资源的类(如 Stack)必须手动实现深拷贝的拷贝构造和赋值重载,避免浅拷贝的问题;
- 运算符重载 :让自定义类型支持内置类型的运算符,必须至少有一个类类型参数,5 个运算符不能重载,
<</>>建议重载为全局函数,++/--通过 int 形参区分前置 / 后置; - const 成员函数:修饰 this 指针,禁止修改成员变量,const 对象只能调用 const 成员函数,只读函数建议定义为 const;
- 拷贝构造与赋值重载 :核心区分是对象是否已存在,初始化新对象用拷贝构造,已存在对象赋值用赋值重载。
下一篇再见🤠