文章目录
- 一、前言:为什么学会了类,还要学习默认成员函数?
- 二、什么是类的默认成员函数?
- 2.1 默认成员函数的概念
- 2.2 编译器自动生成,不代表一定符合需求
- 三、构造函数:让对象出生时就拥有初始状态
- 3.1 为什么需要构造函数?
- 3.2 构造函数的基本写法
- 3.3 构造函数有哪些特点?
- 3.4 什么是默认构造函数?
- 3.5 编译器生成的构造函数会初始化什么?
- 四、析构函数:对象结束时,负责清理资源
- 4.1 为什么需要析构函数?
- 4.2 析构函数的特点
- 4.3 析构函数与构造函数的配合
- 五、拷贝构造函数:如何复制一个对象?
- 5.1 什么是拷贝构造函数?
- 5.2 为什么拷贝构造必须使用引用?
- 5.3 拷贝构造的几种常见调用方式
- 六、浅拷贝与深拷贝:指针复制为什么会出问题?
- 6.1 什么是浅拷贝?
- 浅拷贝的内存示意图
- 0x1000
- 0x1000
- 同一块动态内存
- 6.2 什么是深拷贝?
- 6.3 什么情况下需要深拷贝?
- 七、运算符重载:让自定义对象也能使用运算符
- 7.1 为什么需要运算符重载?
- 7.2 运算符重载的基本语法
- 7.3 运算符重载有哪些规则?
- 7.4 前置 `++` 和后置 `++`
- 7.5 为什么 `<<` 和 `>>` 通常重载为全局函数?
- 八、赋值运算符重载:两个已有对象之间的数据复制
- 8.1 赋值与拷贝构造有什么区别?
- 8.2 赋值运算符重载的实现
- 8.3 默认赋值运算符重载有什么问题?
- 九、综合实战:一步步理解日期类的设计
- 9.1 日期类需要实现哪些功能?
- 9.2 判断闰年与获取每月天数
- 9.3 判断日期是否合法
- 9.4 日期比较运算符
- 9.5 日期加减天数
- 十、`const` 成员函数:为什么函数末尾还有一个 const?
- 10.1 先看一个常见问题
- 10.2 const 成员函数的本质
- 10.3 为什么推荐给只读函数加 const?
- 十一、取地址运算符重载:了解即可
- 十二、本章知识点总结:零基础必须掌握的内容
- 十三、结语
- 结尾
一、前言:为什么学会了类,还要学习默认成员函数?
在上一篇 C++ 学习中,我们已经知道,类可以把数据和操作数据的函数组织在一起。
例如,我们可以定义一个日期类:
class Date { public: int _year; int _month; int _day; void Print() { cout << _year << "-" << _month << "-" << _day << endl; } };有了这个类,就可以创建日期对象:
Date d1; d1._year = 2026; d1._month = 10; d1._day = 10; d1.Print();但是,我们很快就会遇到一些问题:
- 每次创建对象,都要手动初始化成员变量吗?
- 对象使用完毕后,申请的内存应该如何释放?
- 如果想把一个对象复制给另一个对象,应该怎么办?
- 两个对象能不能直接使用
==、=、++等运算符?
C++ 为我们提供了一套机制来解决这些问题,这就是类的默认成员函数和运算符重载。
本篇博客将围绕以下内容展开:
- 默认成员函数的基本概念
- 构造函数与析构函数
- 拷贝构造函数与深浅拷贝
- 运算符重载与赋值运算符重载
- 日期类的综合实现思路
const成员函数与取地址运算符重载
二、什么是类的默认成员函数?
2.1 默认成员函数的概念
默认成员函数,就是用户没有显式定义时,编译器可以自动生成的特定成员函数。
传统 C++ 类的默认成员函数主要有六个:
| 默认成员函数 | 主要作用 |
|---|---|
| 构造函数 | 初始化对象 |
| 析构函数 | 清理对象持有的资源 |
| 拷贝构造函数 | 使用已有对象初始化新对象 |
| 赋值运算符重载 | 将一个已有对象的状态赋给另一个已有对象 |
| 普通取地址运算符重载 | 获取非 const 对象的地址 |
| const 取地址运算符重载 | 获取 const 对象的地址 |
其中,前四个是学习重点。C++11 后增加了移动构造函数和移动赋值运算符重载,后续可以进一步学习。
2.2 编译器自动生成,不代表一定符合需求
这是本章最重要的思想之一。
编译器能够自动生成函数,但自动生成的函数不一定适合所有类。
例如,日期类的成员变量只是三个整数,自动生成的拷贝构造通常就能满足需求。
但是,如果一个栈类内部通过指针申请了动态内存,编译器默认生成的拷贝构造就可能导致两个对象指向同一块内存,引发严重问题。
因此,学习默认成员函数时,要思考两个问题:
- 编译器自动生成的函数具体做了什么?
- 如果自动生成的行为不符合需求,我们应该如何自己实现?
带着这两个问题,我们开始逐个学习。
三、构造函数:让对象出生时就拥有初始状态
3.1 为什么需要构造函数?
先看下面的代码:
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };使用这个类时,我们需要先创建对象,再手动调用Init():
Date d1; d1.Init(2026, 10, 10); d1.Print();问题在于:如果忘记调用Init(),就可能使用尚未正确初始化的成员变量。
有没有办法让对象创建时自动完成初始化呢?
答案就是构造函数。
3.2 构造函数的基本写法
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };使用时:
Date d1; // 使用默认参数初始化 Date d2(2026, 10, 10); // 使用指定参数初始化 d1.Print(); d2.Print();运行结果:
1-1-1 2026-10-10这里的构造函数相当于给对象安排了一位“出生登记员”:对象创建时,它自动负责初始化。
3.3 构造函数有哪些特点?
需要重点记住以下几点:
- 函数名与类名相同。
- 没有返回值,也不需要写
void。 - 创建对象时会自动调用。
- 可以进行函数重载。
- 如果没有显式定义构造函数,编译器通常会尝试生成默认构造函数;如果显式定义了构造函数,编译器就不会再额外生成无参默认构造函数。
例如:
class Date { public: Date() { _year = 2026; _month = 1; _day = 1; } Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };这两个构造函数构成重载,可以根据传入的参数选择合适的版本。
3.4 什么是默认构造函数?
默认构造函数是指不传实参就可以调用的构造函数。
强调,以下三种形式都可以属于默认构造函数:
- 无参构造函数;
- 全缺省构造函数;
- 编译器自动生成的默认构造函数。
但一个类不能同时拥有多个在无参调用时产生歧义的默认构造函数。
例如:
class Date { public: Date() { _year = 1; } // 如果同时取消下面的注释,会产生无参调用歧义 // Date(int year = 1) // { // _year = year; // } private: int _year; };另外,下面这行代码很容易让初学者出错:
Date d1(); // 不是创建一个 Date 对象它会被解释为函数声明,而不是对象定义。
正确的写法是:
Date d1;初学者记忆口诀:创建无参对象,类名后面不要加括号。
3.5 编译器生成的构造函数会初始化什么?
这一点非常重要。
假设有如下代码:
class A { public: int _x; }; class B { public: A _a; int _y; };对于编译器自动生成的默认构造函数,需要区分两种成员:
- 内置类型成员,如
int、char、指针等:在传统默认构造场景下,不能指望编译器自动将它们初始化为零。 - 自定义类型成员,如
A _a:编译器会调用其默认构造函数进行初始化;如果该成员没有可用的默认构造函数,外层类的默认构造也可能无法生成或调用。
因此,编写类时应该明确对象的初始状态,不要依赖未初始化的内置类型值。
四、析构函数:对象结束时,负责清理资源
4.1 为什么需要析构函数?
构造函数解决了对象初始化的问题,那么对象使用完毕之后呢?
对于普通整数,我们通常不需要手动释放。但如果对象使用malloc()申请了动态内存,就必须在适当的时候释放,否则会发生内存泄漏。
例如,栈类会申请一块动态内存来保存数据。
#include <iostream> #include <cstdlib> using namespace std; class Stack { public: Stack(int n = 4) { _a = (int*)malloc(sizeof(int) * n); if (_a == nullptr) { perror("malloc failed"); return; } _capacity = n; _top = 0; } ~Stack() { free(_a); _a = nullptr; _capacity = 0; _top = 0; } private: int* _a; size_t _capacity; size_t _top; };这里的~Stack()就是析构函数。
当对象生命周期结束时,析构函数会自动调用,负责释放对象管理的资源。
4.2 析构函数的特点
- 函数名是在类名前添加
~。 - 没有参数,也没有返回值。
- 一个类只能有一个析构函数。
- 对象生命周期结束时,析构函数自动调用。
- 如果没有显式定义,编译器可以生成默认析构函数。
- 默认析构函数不会替你释放普通指针指向的动态内存,但会自动调用自定义类型成员的析构函数。
最后一点尤其容易出错。
假如栈类没有自己实现析构函数,那么编译器生成的析构函数不会自动执行free(_a)。指针变量本身虽然会随对象结束而消失,但它指向的动态内存可能仍然占用着,这就是内存泄漏。
4.3 析构函数与构造函数的配合
可以把两者理解为一对负责对象生命周期管理的函数:
构造函数Stack()
申请空间、设置容量、初始化栈顶位置。
析构函数~Stack()
释放动态内存,清理对象管理的资源。
需要注意:析构函数负责清理资源,并不是由它直接决定对象本身何时销毁。
五、拷贝构造函数:如何复制一个对象?
5.1 什么是拷贝构造函数?
假设我们已经创建了一个日期对象:
Date d1(2026, 10, 10);现在想创建另一个对象d2,并让它与d1拥有相同的初始数据。
可以这样写:
Date d2(d1);这就是拷贝构造的典型使用场景:使用一个已经存在的对象,初始化一个新对象。
拷贝构造函数是构造函数的一种特殊重载,常见写法如下:
Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }下面逐步理解这段代码:
Date:表示当前类的名称。const Date& d:接收一个已有的Date对象的引用。const:表示函数不应该通过这个引用修改原对象。&:引用可以避免为了传参而再次复制整个对象。
5.2 为什么拷贝构造必须使用引用?
新手可能会写出这样的代码:
Date(Date d) { _year = d._year; _month = d._month; _day = d._day; }这不是合法的拷贝构造函数写法。
原因是:如果拷贝构造的参数采用传值方式,那么调用拷贝构造时,为了把实参复制到形参d中,又需要调用拷贝构造,最终可能导致无限递归。
所以,拷贝构造的第一个参数必须是本类对象的引用,通常写成:
Date(const Date& d);5.3 拷贝构造的几种常见调用方式
Date d1(2026, 10, 10); // 写法一 Date d2(d1); // 写法二 Date d3 = d1;注意:Date d3 = d1;在这里是初始化一个新对象,因此调用的是拷贝构造,而不是赋值运算符重载。
如果类没有显式定义拷贝构造函数,编译器可以自动生成一个。对于普通成员变量,这种自动生成的拷贝通常就是逐成员复制。
但如果类管理着动态内存,仅仅复制指针值就可能产生问题。
接下来,我们学习本章最容易出错的知识:浅拷贝与深拷贝。
六、浅拷贝与深拷贝:指针复制为什么会出问题?
6.1 什么是浅拷贝?
假设我们有一个栈对象,它内部通过指针管理一块动态内存。
浅拷贝的内存示意图
栈对象 A
指针_a
0x1000
栈对象 B
指针_a
0x1000
同一块动态内存
两个指针指向相同地址
浅拷贝只复制指针的值,没有为新对象创建独立的内存资源。
例如:
Stack st1; Stack st2 = st1;如果Stack使用编译器自动生成的拷贝构造函数,那么st1._a和st2._a可能指向同一块动态内存。
当两个对象分别执行析构函数时,就可能对同一块内存重复释放,造成未定义行为,程序甚至会崩溃。
6.2 什么是深拷贝?
深拷贝的核心思想是:
不仅复制数据,还要为新对象创建属于它自己的资源。
例如:
#include <iostream> #include <cstdlib> #include <cstring> using namespace std; class Stack { public: Stack(int n = 4) : _a(nullptr), _capacity(n), _top(0) { _a = (int*)malloc(sizeof(int) * n); if (_a == nullptr) { _capacity = 0; } } Stack(const Stack& st) : _a(nullptr), _capacity(st._capacity), _top(st._top) { if (_capacity == 0) return; _a = (int*)malloc(sizeof(int) * _capacity); if (_a != nullptr) { memcpy(_a, st._a, sizeof(int) * _top); } else { _capacity = 0; _top = 0; } } ~Stack() { free(_a); } private: int* _a; size_t _capacity; size_t _top; };这段代码演示了深拷贝的基本思路:
- 为新对象申请一块独立内存。
- 将原对象中的有效数据复制过去。
- 让两个对象分别管理自己的内存。
这样,两个栈对象就不会因为共同管理同一块动态内存而发生重复释放。
注意:上面的代码主要用于解释深拷贝的思路。完整的资源管理类还需要妥善处理内存申请失败,以及赋值运算时原有资源的释放等问题。
6.3 什么情况下需要深拷贝?
可以这样判断:
| 类的情况 | 通常如何处理 |
|---|---|
Date只保存年、月、日 | 默认拷贝构造通常够用 |
Stack使用指针管理动态内存 | 需要考虑自行实现深拷贝 |
MyQueue内部由两个Stack对象组成 | 如果Stack的拷贝行为正确,队列通常可以使用编译器生成的拷贝构造 |
这里有一个实用的判断技巧:
如果一个类显式实现了析构函数,并且析构函数负责释放资源,就应该重点检查它的拷贝构造和赋值运算符重载是否也需要自定义实现。
这并不是说所有自定义析构函数都必然要求手写拷贝函数,而是说资源所有权需要保持一致。
七、运算符重载:让自定义对象也能使用运算符
7.1 为什么需要运算符重载?
对于内置类型,下面的写法非常自然:
int a = 10; int b = 20; cout << (a + b) << endl;但如果我们定义了一个Date类,下面的表达式是什么意思呢?
Date d1(2026, 10, 10); Date d2(2026, 10, 11); // d1 == d2编译器并不知道如何直接比较两个Date对象的年月日。
这时,就可以通过运算符重载,为类类型定义合理的运算行为。
7.2 运算符重载的基本语法
运算符重载本质上是一个特殊命名的函数,其名称由operator和对应的运算符组成。
例如,定义日期相等比较:
bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; }这是成员函数形式的运算符重载。
使用时:
Date d1(2026, 10, 10); Date d2(2026, 10, 10); cout << (d1 == d2) << endl;如果对象日期相同,表达式的结果就是true。
对于成员函数形式的运算符重载:
d1 == d2;可以理解为:
d1.operator==(d2);其中,左侧对象d1通过隐含的this指针传入,右侧对象d2则作为显式参数。
7.3 运算符重载有哪些规则?
初学者需要掌握以下规则:
- 运算符重载不能凭空创造新的运算符,例如
operator@。 - 至少有一个操作数必须是类类型或枚举类型,不能改变两个内置类型之间的运算含义。
- 运算符重载不会改变原运算符的优先级和结合性。
- 赋值运算符
operator=必须重载为非静态成员函数。 .*、::、sizeof、?:、.等运算符不能重载。
7.4 前置++和后置++
这是 C++ 面试和基础考试中比较常见的知识点。
前置递增:
++d1;后置递增:
d1++;为了区分这两种操作,C++ 规定后置++重载时,需要额外增加一个int形参。
// 前置++ Date& operator++() { *this += 1; return *this; } // 后置++ Date operator++(int) { Date tmp(*this); *this += 1; return tmp; }两者的区别是:
- 前置
++:先修改对象,再返回修改后的对象引用。 - 后置
++:先保存修改前的状态,再修改对象,最后返回旧状态的副本。
例如,假设d1最初是2026-10-10:
Date d2 = ++d1;那么d1和d2都表示递增后的日期。
而:
Date d3 = d1++;则d3保存递增前的日期,d1自身已经递增。
7.5 为什么<<和>>通常重载为全局函数?
我们经常使用:
cout << 100; cin >> value;如果想让日期类支持类似的写法:
Date d(2026, 10, 10); cout << d;通常会把流插入运算符<<重载为非成员函数:
ostream& operator<<(ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day; return out; }不过,上述示例假设该函数可以访问日期类的私有成员。实际实现时,可以将它声明为友元函数,或者通过公有接口获取日期数据。
为什么不直接把它写成成员函数?
因为成员函数隐含的this会占据第一个操作数的位置。如果写成成员函数,使用方式会变成:
d.operator<<(cout);而不是我们熟悉的:
cout << d;所以,为了保持流操作的使用习惯,通常选择全局函数,并在需要时通过friend授予访问权限。
八、赋值运算符重载:两个已有对象之间的数据复制
8.1 赋值与拷贝构造有什么区别?
这是本章必须掌握的知识点。
先看两段代码:
Date d1(2026, 10, 10); // 拷贝构造 Date d2 = d1;这里的d2是一个新创建的对象,因此调用拷贝构造函数。
再看:
Date d1(2026, 10, 10); Date d2(2026, 10, 11); // 赋值 d1 = d2;这里d1和d2都已经存在,d1 = d2是把d2的状态赋给d1,通常调用赋值运算符重载。
拷贝构造
创建新对象时使用已有对象初始化。
Date d2 = d1;
赋值运算符重载
两个已有对象之间进行状态赋值。
d1 = d2;
8.2 赋值运算符重载的实现
以日期类为例:
Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }逐句理解:
const Date& d:通过引用接收右侧对象,避免不必要的拷贝。this != &d:检查是否发生自己给自己赋值。return *this:返回当前对象的引用。
为什么要返回引用?
因为这样可以支持连续赋值:
d1 = d2 = d3;赋值运算符返回当前对象的引用后,整个表达式就可以继续完成下一次赋值。
8.3 默认赋值运算符重载有什么问题?
如果一个类只包含整数、字符等普通成员变量,编译器自动生成的赋值运算符通常就够用了。
但如果类中含有指针并管理动态内存,默认赋值可能只复制指针值,导致两个对象共享同一块内存。
因此,对于前面介绍的Stack,我们不仅要考虑深拷贝构造,还要考虑深拷贝赋值。
需要注意:赋值运算符重载不能只申请新内存,还必须妥善处理当前对象原来管理的资源,并保证异常或申请失败时不会破坏对象状态。
这也是资源管理类比普通数据类复杂得多的原因。
九、综合实战:一步步理解日期类的设计
前面学习的构造函数、拷贝构造、运算符重载等知识,并不是彼此孤立的。它们最终被应用到了一个完整的Date日期类中。
我们以日期类为例,看看如何把这些知识联系起来。
9.1 日期类需要实现哪些功能?
一个实用的日期类,至少需要考虑以下问题:
- 如何创建一个日期对象?
- 如何判断输入的日期是否合法?
- 如何比较两个日期的先后?
- 如何计算日期加减若干天?
- 如何实现前置和后置
++? - 如何计算两个日期之间相差多少天?
- 如何支持
cout << d和cin >> d?
9.2 判断闰年与获取每月天数
首先,需要解决一个基础问题:每个月有多少天?
2 月比较特殊,闰年有 29 天,平年有 28 天。
判断闰年的规则是:
- 能被 4 整除,但不能被 100 整除;
- 或者能被 400 整除。
代码如下:
bool IsLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } int GetMonthDay(int year, int month) { int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month == 2 && IsLeapYear(year)) { return 29; } return days[month]; }这里使用了一个包含 13 个元素的数组,将下标 1 到 12 分别对应 1 月到 12 月。
这种写法可以让我们直接通过days[month]获取某个月的天数。
9.3 判断日期是否合法
有了每月天数的计算方法,就可以检查日期是否合法。
bool CheckDate(int year, int month, int day) { if (month < 1 || month > 12) return false; if (day < 1 || day > GetMonthDay(year, month)) return false; return true; }例如:
CheckDate(2024, 2, 29); // true CheckDate(2025, 2, 29); // false CheckDate(2026, 13, 1); // false注意:这个简单示例主要检查月份和日期范围。如果项目还要求限制年份范围,可以继续增加年份合法性判断。
9.4 日期比较运算符
假设需要判断d1是否早于d2。
比较顺序应该是:
- 先比较年份;
- 年份相同,再比较月份;
- 年月相同,最后比较日期。
这就是日期类operator<的基本逻辑。
bool operator<(const Date& d) const { if (_year != d._year) return _year < d._year; if (_month != d._month) return _month < d._month; return _day < d._day; }这里的const有两个用途:
const Date& d:不允许通过参数修改右侧对象。- 函数末尾的
const:表示这个成员函数不会修改当前对象的成员状态。
如果已经实现了<和==,其他比较运算符就可以复用已有逻辑。
例如:
bool operator<=(const Date& d) const { return *this < d || *this == d; } bool operator!=(const Date& d) const { return !(*this == d); }这种写法能够减少重复代码,让类更容易维护。
9.5 日期加减天数
例如:
Date d1(2026, 10, 10); Date d2 = d1 + 5;我们希望d2表示 2026 年 10 月 15 日,同时d1保持不变。
一种常见的设计是:
operator+=:直接修改当前对象;operator+:复制当前对象,再在副本上执行+=。
示意代码:
Date& operator+=(int day) { // 这里实现日期向后增加 day 天的逻辑 // 需要考虑跨月、跨年以及闰年 return *this; } Date operator+(int day) const { Date tmp = *this; tmp += day; return tmp; }为什么operator+返回对象,而operator+=返回引用?
因为两者的语义不同:
d1 += 5:修改d1本身。d1 + 5:计算一个新结果,不应该改变d1。
这也体现了运算符重载的一个基本原则:尽量保持与内置类型操作相似的使用习惯。
上面的代码只是接口和设计思路的演示,并不是完整的日期加减实现。课件中的完整实现还需要通过循环处理跨月、跨年以及负数天数等情况。
十、const成员函数:为什么函数末尾还有一个 const?
10.1 先看一个常见问题
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };创建一个 const 对象:
const Date d1; d1.Print();这时可能出现编译错误。
原因是:Print()没有声明为 const 成员函数,不能直接通过 const 对象调用。
如果函数的职责只是读取数据并打印,就应该将它声明为 const 成员函数。
void Print() const { cout << _year << "-" << _month << "-" << _day << endl; }注意,const是写在参数列表后面的,而不是函数名前面。
10.2 const 成员函数的本质
可以把它理解为向编译器作出承诺:
这个成员函数不会通过当前对象修改普通成员变量。
从this指针的角度理解:
普通成员函数中的隐含this可以理解为指向当前对象的常量指针;而 const 成员函数中的this则指向 const 对象。
简化表示如下:
// 普通成员函数中的 this Date* const this; // const 成员函数中的 this const Date* const this;这里需要区分两个 const:
Date* const:指针本身不能改为指向其他地址。const Date*:不能通过该指针修改所指对象的普通成员。
10.3 为什么推荐给只读函数加 const?
因为这样可以同时满足两种调用需求:
Date d1; const Date d2; // const 成员函数可以由普通对象调用 d1.Print(); // const 成员函数也可以由 const 对象调用 d2.Print();因此,对于不需要修改对象状态的成员函数,应该优先考虑添加const。
十一、取地址运算符重载:了解即可
C++ 还允许我们对取地址运算符&进行重载。
普通对象可以通过:
Date d1; Date* p = &d1;获取地址。
对于 const 对象:
const Date d2; const Date* p = &d2;也可以正常获取地址。
编译器能够自动生成普通取地址运算符重载和 const 取地址运算符重载,通常不需要自己编写。
手动重载的形式:
Date* operator&() { return this; } const Date* operator&() const { return this; }一般情况下,我们不建议随意改变取地址运算符的正常行为。对于初学者,先理解编译器可以自动生成这两个函数即可。
十二、本章知识点总结:零基础必须掌握的内容
学完这一章,不要只记住代码,更要理解每个函数解决了什么问题。
| 知识点 | 核心作用 | 最容易犯的错误 |
|---|---|---|
| 构造函数 | 初始化对象 | 误以为负责分配对象本身的空间 |
| 析构函数 | 清理对象管理的资源 | 认为默认析构会自动释放所有动态内存 |
| 拷贝构造 | 初始化新对象 | 把Date d2 = d1误认为赋值 |
| 深浅拷贝 | 管理指针与资源 | 忘记默认拷贝可能只复制指针 |
| 运算符重载 | 定义对象的运算行为 | 忘记重载规则和成员函数的this |
| 赋值运算符重载 | 处理已有对象之间的赋值 | 没有处理自赋值或资源所有权 |
| const 成员函数 | 保护只读操作 | 把函数末尾的const忘掉 |
| 取地址运算符重载 | 自定义取地址行为 | 没有必要时也进行重载 |
十三、结语
C++ 的类和对象,是从基础语法走向真正面向对象编程的重要一步。
刚开始学习时,你可能觉得构造函数、析构函数、拷贝构造、赋值运算符重载这些概念比较多,而且容易混淆。这很正常。
建议你先抓住三条主线:
- 对象的生命周期: 构造函数负责初始化,析构函数负责清理资源。
- 对象的复制: 拷贝构造用于创建新对象,赋值运算符用于处理已经存在的对象。
- 对象的行为: 通过运算符重载和 const 成员函数,让类的使用更加自然、安全。
特别是深浅拷贝,一定要亲自动手写一个管理动态内存的类,观察指针地址、资源释放和拷贝结果。只有真正理解了这些问题,后续学习智能指针、移动语义和 STL 容器时才会更加轻松。
学习 C++ 不需要一口气记住所有知识。每理解一个概念,就用一段小代码验证它,逐步积累,你会发现原本复杂的面向对象编程也能变得清晰起来。
结尾
uu们,本文的内容到这里就全部结束了,道尔在这里再次感谢您的阅读!
👀【关注】跟随我一起深耕技术领域,见证每一次成长。 ❤️【点赞】让优质内容被更多人看见,让知识传递更有力量。 ⭐【收藏】把核心知识点存好,在需要时随时查、随时用。 💬【评论】分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主“一键四连”哦! |
结语:希望对学习C++相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!