跳到主要内容C++ 类和对象进阶:默认成员函数与运算符重载 | 极客日志C++算法
C++ 类和对象进阶:默认成员函数与运算符重载
综述由AI生成C++ 类机制涵盖构造函数、析构函数、拷贝构造及赋值运算符重载。重点在于资源管理(深拷贝)与规则三原则。日期类实现展示了运算符重载在业务逻辑中的应用,包括比较、算术及流操作符。Const 正确性确保对象状态安全。
猫巷少女15 浏览 一、类的默认成员函数
编译器会自动生成的成员函数称为默认成员函数。在 C++11 之前,一个类如果不写任何函数,编译器会默认生成以下 6 个:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址运算符重载(普通和 const)。C++11 之后又增加了移动构造和移动赋值。
学习默认成员函数主要关注两点:
- 不写时,编译器生成的行为是否符合预期?
- 如果不符合需求,如何自己实现?
通常编译器生成的默认版本对内置类型成员变量初始化没有要求(可能是随机值),对自定义类型成员变量则会调用其默认构造函数。如果自定义类型成员没有默认构造函数,编译就会报错,这时必须使用初始化列表来解决。
二、构造函数
构造函数的核心任务是对象实例化时的初始化。以前写栈或队列可能需要显式调用 StackInit(),有了构造函数就不需要这一步了。
构造函数的特点:
- 函数名与类名相同,无返回值(连 void 都不能写)。
- 对象实例化时系统自动调用。
- 支持重载。
- 若未显式定义,编译器生成无参默认构造函数;一旦用户定义了任意构造函数,编译器不再自动生成无参版本。
- 注意:"默认构造函数"不仅指编译器生成的那个,也包括无参构造函数和全缺省构造函数。这三者有且只有一个存在,不能同时存在。虽然它们构成重载,但调用时若无实参会有歧义。
1、构造函数的基本运用
#include <iostream>
using namespace std;
class Date {
public:
Date() : _year(1), _month(1), _day(1) {}
Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
_month;
_day;
};
{
Date d1;
d();
;
d();
;
}
int
int
int main()
1.
Print
Date d2(2025, 11, 25)
2.
Print
return
0
这里有个细节:通过无参构造函数创建对象时,对象后面不用跟括号,否则编译器无法区分是函数声明还是实例化对象。
2、特殊场景下的运用
在容器内部管理资源时,构造函数的初始化列表尤为重要。例如用两个 Stack 实现队列时,成员变量的初始化顺序决定了构造调用的时机。
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
public:
private:
Stack pushst;
Stack popst;
};
int main() {
MyQueue mq;
return 0;
}
三、析构函数
析构函数与构造函数功能相反,用于对象生命周期结束时清理资源。C++ 规定对象销毁时会自动调用析构函数。
析构函数的特点:
- 函数名是在类名前加
~,如 ~Stack()。
- 无参数、无返回值。
- 一个类只能有一个析构函数。
- 若未显式定义,系统自动生成默认析构函数。
关键点: 编译器生成的默认析构函数对内置类型不做处理,对自定义类型成员会调用其析构函数。如果类中申请了动态内存(如 malloc),必须手动编写析构函数释放资源,否则会造成泄漏。
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
~Stack() {
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
public:
private:
Stack pushst;
Stack popst;
};
int main() {
MyQueue mq;
return 0;
}
四、拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且后续参数都有默认值,则此构造函数为拷贝构造函数。
拷贝构造函数的特点:
- 是构造函数的重载。
- 第一个参数必须是类类型对象的引用(传值会导致无穷递归)。
- 若未显式定义,编译器生成默认拷贝构造(浅拷贝)。
- 默认拷贝构造对内置类型做值拷贝,对自定义类型调用其拷贝构造。
- 像
Date 这样全是内置类型且无资源管理的,默认拷贝构造够用。
- 像
Stack 这样包含指针指向堆资源的,默认浅拷贝会导致析构时重复释放,必须实现深拷贝。
- 像
MyQueue 这样内部主要是自定义类型成员,只要成员实现了正确的拷贝逻辑,外层可以依赖默认版本。
经验法则: 如果一个类显式实现了析构并释放资源,那么它通常需要显式实现拷贝构造和赋值运算符重载(Rule of Three)。
1、无穷递归问题
如果拷贝构造函数参数不是引用而是值,调用时会再次触发拷贝构造,导致死循环。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Func1(const Date& d) {
cout << &d << endl;
d.Print();
}
int main() {
Date d1(2025, 11, 16);
Func1(d1);
Date d2(d1);
d2.Print();
return 0;
}
2、浅拷贝与深拷贝
当类中包含指针成员时,浅拷贝会让两个对象指向同一块内存,析构时就会崩溃。
#include <iostream>
#include <cstring>
using namespace std;
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st) {
cout << "Stack(const Stack& st)" << endl;
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a) {
perror("malloc fail!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x) {
if (_top == _capacity) {
int newCapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newCapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newCapacity;
}
_a[_top++] = x;
}
~Stack() {
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(st1);
return 0;
}
3、传参与返回优化
- 传参: 尽量使用
const T&,避免拷贝开销。
- 返回: 传值返回会产生临时对象调用拷贝构造;传引用返回需确保对象生命周期有效(如静态对象)。
Stack& func2() {
static Stack st;
return st;
}
int main() {
Stack ret = func2();
return 0;
}
五、赋值运算符重载
赋值运算符用于两个已存在对象之间的拷贝,区别于拷贝构造(用于初始化新对象)。
赋值运算符重载的特点:
- 必须重载为成员函数。
- 参数建议为
const 当前类类型引用。
- 返回值建议为
当前类类型引用(支持连续赋值)。
- 默认版本行为类似拷贝构造(浅拷贝)。
总结: 如果显示了析构释放资源,就需要实现拷贝构造和赋值重载。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2025, 11, 26);
Date d2(2025, 11, 27);
d1 = d2;
Date d3 = d2;
d1 = d2 = d3;
return 0;
}
六、运算符重载实战:日期类
日期类是一个非常好的综合练习,涵盖了比较、算术、流操作符等。
1. 头文件设计
#pragma once
#include <assert.h>
#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 = 1990, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
if (!CheckDate()) {
cout << "非法日期";
Print();
}
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
inline int GetMonthDay(int year, int month) {
assert(month > 0 && month < 13);
static int MontDayArray[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 30, 31, 30, 31, 30};
if ((month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))) {
return 29;
}
return MontDayArray[month];
}
bool CheckDate();
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
Date operator++(int);
Date& operator++();
Date operator--(int);
Date& operator--();
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
2. 实现细节
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
bool Date::CheckDate() {
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)) {
return false;
}
return true;
}
Date& Date::operator+=(int day) {
if (day < 0) {
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) {
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator-=(int day) {
if (day < 0) {
return *this += (-day);
}
_day -= day;
while (_day <= 0) {
_month--;
if (_month == 0) {
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) {
Date tmp = *this;
tmp -= day;
return tmp;
}
bool Date::operator<(const Date& d) {
if (_year < d._year) return true;
else if (_year == d._year) {
if (_month < d._month) return true;
else if (_month == d._month) {
return _day < d._day;
}
}
return false;
}
bool Date::operator<=(const Date& d) {
return *this < d || *this == d;
}
bool Date::operator>(const Date& d) {
return !(*this <= d);
}
bool Date::operator>=(const Date& d) {
return !(*this < d);
}
bool Date::operator==(const Date& d) {
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator!=(const Date& d) {
return !(*this == d);
}
Date Date::operator++(int) {
Date tmp = *this;
*this += 1;
return tmp;
}
Date& Date::operator++() {
*this += 1;
return *this;
}
Date Date::operator--(int) {
Date tmp = *this;
*this -= 1;
return tmp;
}
Date& Date::operator--() {
*this -= 1;
return *this;
}
int Date::operator-(const Date& d) {
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max) {
++min;
++n;
}
return n * flag;
}
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d) {
cout << "请输入年月日: >";
in >> d._year >> d._month >> d._day;
return in;
}
七、Const 正确性与取地址重载
将 const 修饰的成员函数称为 const 成员函数。它修饰的是隐含的 this 指针,表明在该函数内不能修改成员变量。
class Date {
public:
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
};
void Test() {
const Date d1(2025, 11, 30);
d1.Print();
}
取地址运算符也可以重载,通常编译器生成的默认版本足够使用。但在特殊场景下(如单例模式或不想暴露地址),可以自行实现返回特定地址。
class Date {
public:
Date* operator&() {
return this;
}
const Date* operator&() const {
return this;
}
};
相关免费在线工具
- 加密/解密文本
使用加密算法(如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