
个人主页:
wengqidaifeng
✨ 永远在路上,永远向前走
个人专栏:
数据结构
文章目录
-
- 前言
- 一、拷贝构造函数
-
- [1.1 什么是拷贝构造函数](#1.1 什么是拷贝构造函数)
- [1.2 拷贝构造函数的格式](#1.2 拷贝构造函数的格式)
- [1.3 拷贝构造函数的调用场景](#1.3 拷贝构造函数的调用场景)
- [1.4 编译器自动生成的拷贝构造](#1.4 编译器自动生成的拷贝构造)
- [二、浅拷贝 vs 深拷贝 ------ 拷贝构造的核心难点](#二、浅拷贝 vs 深拷贝 —— 拷贝构造的核心难点)
-
- [2.1 浅拷贝的问题](#2.1 浅拷贝的问题)
- [2.2 深拷贝的解决方案](#2.2 深拷贝的解决方案)
- [2.3 判断是否需要深拷贝的黄金法则](#2.3 判断是否需要深拷贝的黄金法则)
- [2.4 两个有趣的例子](#2.4 两个有趣的例子)
- 三、赋值运算符重载
-
- [3.1 拷贝构造 vs 赋值](#3.1 拷贝构造 vs 赋值)
- [3.2 赋值运算符重载的格式](#3.2 赋值运算符重载的格式)
- [3.3 编译器自动生成的赋值运算符](#3.3 编译器自动生成的赋值运算符)
- 四、运算符重载深入
-
- [4.1 operator== 的实现](#4.1 operator== 的实现)
- [4.2 成员函数 vs 全局函数](#4.2 成员函数 vs 全局函数)
- [4.3 日期类的 operator+ 和 operator+=](#4.3 日期类的 operator+ 和 operator+=)
- [4.4 头文件声明](#4.4 头文件声明)
- [4.5 一些复用的技巧](#4.5 一些复用的技巧)
- [五、const 成员函数](#五、const 成员函数)
-
- [5.1 为什么要 const 成员函数](#5.1 为什么要 const 成员函数)
- [5.2 const 成员函数的语法](#5.2 const 成员函数的语法)
- [5.3 const 对象 vs 非 const 对象](#5.3 const 对象 vs 非 const 对象)
- [5.4 函数重载中的 const](#5.4 函数重载中的 const)
- 六、取地址操作符重载(选读)
- 七、成员函数指针(了解)
- 总结
前言
在上一篇中,我们学习了类和对象的基础知识------类的定义、this 指针、构造函数和析构函数。本篇我们将深入几个 C++ 中最容易踩坑的核心概念:拷贝构造函数 、深拷贝与浅拷贝 、赋值运算符重载 ,以及运算符重载的详细实践。
这些内容直接关系到程序的正确性和内存安全,掌握它们才能真正写出健壮的 C++ 代码。
一、拷贝构造函数
1.1 什么是拷贝构造函数
当你用一个已存在的对象去初始化另一个同类对象时,会调用拷贝构造函数。
cpp
Date d1(2024, 7, 12);
Date d2(d1); // 拷贝构造:用 d1 初始化 d2
Date d3 = d1; // 拷贝构造:注意,这是初始化,不是赋值!
1.2 拷贝构造函数的格式
cpp
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;
};
几个关键点:
- 参数必须是
const T&:引用传递避免无限递归(如果传值,传值本身又会调用拷贝构造,形成死循环) - 建议加 const:保护源对象不被修改
如果写成
Date(Date d)会发生什么?编译器直接报错------传值调用本身需要先拷贝参数,而拷贝参数又要调用拷贝构造函数......无限递归。
cpp
// 错误示范
Date(const Date d) // error C2652: "Date": 非法的复制构造函数:
{ // 第一个参数不应是"Date"
// ...
}
1.3 拷贝构造函数的调用场景
三种情况会调用拷贝构造:
场景3:函数返回对象(可能优化)
Date Func2() { Date d; return d; }
Date ret = Func2(); // 可能调用拷贝构造
场景2:函数传参(传值)
void Func1(Date d) { }
Func1(d1); // d1 → d,调用拷贝构造
场景1:用已有对象初始化新对象
Date d1(2024,7,12);
Date d2(d1); // 拷贝构造
Date d3 = d1; // 拷贝构造(不是赋值!)
📦 拷贝构造函数触发条件
C++ 规定传值 调用函数参数时,需要调用拷贝构造函数。因此实际编程中,建议用
const T&引用传参来避免不必要的拷贝。
1.4 编译器自动生成的拷贝构造
如果你没有显式定义拷贝构造函数,编译器会自动生成一个。这个默认的拷贝构造函数执行的是浅拷贝 (shallow copy)------将源对象的所有成员变量按字节复制到新对象。
二、浅拷贝 vs 深拷贝 ------ 拷贝构造的核心难点
2.1 浅拷贝的问题
对于没有动态资源的类(如 Date),默认的浅拷贝完全没问题:
cpp
// Date 只有 int 成员,浅拷贝 OK
Date d1(2024, 7, 12);
Date d2(d1); // 默认拷贝构造,逐字节复制 int,一切正常
但对于持有动态内存的类(如 Stack),浅拷贝会导致严重问题:
浅拷贝 vs 深拷贝 ------ 本系列最重要的图解:
✅ 深拷贝 Deep Copy(手动实现)
st2 (拷贝构造)
st1
_a = 0x2000 ← 独立地址!
_a = 0x1000
堆内存 0x1000
1, 2, ?, ?
堆内存 0x2000
1, 2, ?, ?
✅ st2析构 → free(0x2000) ✓
✅ st1析构 → free(0x1000) ✓
_top = 2
_capacity = 4
_top = 2
_capacity = 4
❌ 浅拷贝 Shallow Copy(编译器默认)
st2 = st1
st1
_a = 0x1000 ← 同一个地址!
_a = 0x1000
堆内存 0x1000
1, 2, ?, ?
💥 st2析构 → free(0x1000)
💥 st1析构 → free(0x1000) ← 重复释放!
_top = 2
_capacity = 4
_top = 2
_capacity = 4
cpp
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;
};
int main() {
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(st1); // 使用默认拷贝构造------浅拷贝!
// st1._a 和 st2._a 都指向同一块 malloc 出来的内存
// 函数结束,st2 析构 → free(_a)
// 函数结束,st1 析构 → free(_a) ← 重复释放!崩溃!
}
图解:
浅拷贝后:
st1._a ──→ [malloc堆内存: 1, 2, ?, ?]
st2._a ──→ [同一块堆内存 ↑]
重复释放 → 程序崩溃
2.2 深拷贝的解决方案
我们需要在拷贝构造函数中为副本也分配一块独立的内存,然后把数据复制过去。
cpp
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
// 深拷贝------手动实现
Stack(const Stack& st) {
cout << "Stack(const Stack& st)" << endl;
// 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;
}
~Stack() {
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
图解:
深拷贝后:
st1._a ──→ [malloc堆内存1: 1, 2, ?, ?]
st2._a ──→ [malloc堆内存2: 1, 2, ?, ?] ← 独立的内存!
析构时各自释放各自的内存,互不影响
2.3 判断是否需要深拷贝的黄金法则
如果类中持有指向动态资源的指针(malloc/new 出来的),就需要自己实现深拷贝的拷贝构造函数和赋值运算符。
如果类的成员都是基本类型(int, double 等)或者有深拷贝能力的类,就可以用编译器自动生成的。
2.4 两个有趣的例子
例1:一个含成员有深拷贝能力的类
cpp
class MyQueue {
public:
// 不需要写拷贝构造函数!
// 编译器自动生成的会对 pushst 和 popst 调用 Stack 的拷贝构造,
// 而 Stack 的拷贝构造已经实现了深拷贝
private:
Stack pushst;
Stack popst;
};
例2:拷贝构造与取地址构造的区别
cpp
class Date {
public:
Date(const Date& d) { // 拷贝构造:参数是引用
_year = d._year;
_month = d._month;
_day = d._day;
}
Date(const Date* d) { // 普通构造:参数是指针,不是拷贝构造!
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
private:
int _year, _month, _day;
};
三、赋值运算符重载
3.1 拷贝构造 vs 赋值
这两个经常被混淆,但它们有本质区别:
左值正在创建 →
左值已存在 →
赋值 ------ 已存在对象间
d1 = d2;
d1 已经存在
只是修改 d1 的值
拷贝构造 ------ 创建新对象
Date d3(d2);
d3 是全新对象
Date d4 = d2;
d4 是全新对象
(这是初始化,不是赋值!)
🔑 关键判断规则
cpp
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
Date d3(d2); // 拷贝构造------用 d2 初始化 d3(d3 是新对象)
Date d4 = d2; // 拷贝构造------同样是在初始化新对象
d1 = d2; // 赋值------d1 已经存在,把 d2 的值赋给 d1
关键判断:左值已经存在 → 赋值;左值正在创建 → 拷贝构造。
3.2 赋值运算符重载的格式
cpp
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 赋值运算符重载
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this; // 返回 *this 是为了支持连续赋值
}
private:
int _year, _month, _day;
};
int main() {
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
Date d3, d4;
d3 = d1; // 赋值
d4 = d3 = d1; // 连续赋值:需要 operator= 返回引用
int i, j, k;
i = j = k = 1; // 内置类型的连续赋值也是如此
}
为什么返回 Date& 而不是 Date?
- 返回引用可以避免额外的拷贝,提高效率
- 返回引用支持
d4 = d3 = d1这样的连续赋值
为什么返回 *this?
*this就是当前对象自身,返回它让赋值表达式的结果仍然是这个对象
3.3 编译器自动生成的赋值运算符
和拷贝构造一样,编译器自动生成的赋值运算符也是浅拷贝:
cpp
// 对于 Stack 来说,默认的赋值运算符:
// _a = d._a; ← 两个对象指向同一块内存!
// _top = d._top;
// _capacity = d._capacity;
如果类持有动态资源,必须自己实现深拷贝的赋值运算符。通常还需要考虑"自我赋值"和"释放旧资源"的问题(属于进阶内容,下篇会提到)。
四、运算符重载深入
在上一篇中我们简单介绍了运算符重载,现在来深入学习。
4.1 operator== 的实现
cpp
class Date {
public:
// 成员函数版本
bool operator==(const Date& d) {
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year, _month, _day;
};
调用方式:
cpp
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 三种写法等价:
d1.operator==(d2); // 显式调用
d1 == d2; // 语法糖,编译器转换为上面的形式
operator==(d1, d2); // 如果是全局函数版本
4.2 成员函数 vs 全局函数
运算符可以在类内(成员函数)或类外(全局函数)重载:
| 运算符类型 | 推荐形式 | 原因 |
|---|---|---|
= () [] -> |
必须成员函数 | C++ 标准规定 |
比较运算符(< == 等) |
建议成员函数 | 左操作数固定为 *this |
算术运算符(+ - 等) |
建议成员函数 | + 复用 += 更方便 |
<< >>(流) |
必须全局函数 | 左操作数是 cout/cin |
+ 复用 += 的设计模式 --- 一图看懂:
渲染错误: Mermaid 渲染失败: Parse error on line 10: ...dd --> tmp --> call --> ret op_addeq... -----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'CALLBACKNAME'
4.3 日期类的 operator+ 和 operator+=
以下是一个日期类的完整示例。思路:operator+ 不应改变左操作数,返回一个新的 Date 对象;operator+= 改变自身,返回引用。
cpp
#include"Date.h"
// d1 + 100 ------ 不改变 d1,返回新对象
Date Date::operator+(int day) {
Date tmp = *this; // 拷贝一份
tmp += day; // 复用 += 的逻辑
return tmp;
}
// d1 += 100 ------ 改变 d1 自身
Date& Date::operator+=(int day) {
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
设计技巧 :operator+ 调用 operator+=(复用逻辑,避免代码重复)。反过来(+= 调用 +)效率更低,因为多了一次拷贝。
4.4 头文件声明
cpp
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print();
// 获取某月的天数
int GetMonthDay(int year, int month) {
static int monthDayArray[13] =
{ -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0)
|| (year % 400 == 0))) {
return 29;
}
return monthDayArray[month];
}
// 比较运算符
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);
private:
int _year;
int _month;
int _day;
};
4.5 一些复用的技巧
比较运算符之间有逻辑关系,可以互相复用:
cpp
bool Date::operator<(const Date& d) const {
if (_year < d._year) return true;
if (_year == d._year && _month < d._month) return true;
if (_year == d._year && _month == d._month && _day < d._day) return true;
return false;
}
bool Date::operator==(const Date& d) const {
return _year == d._year && _month == d._month && _day == d._day;
}
// 其他只需复用一个已实现的:
bool Date::operator<=(const Date& d) const { return *this < d || *this == d; }
bool Date::operator>(const Date& d) const { return !(*this <= d); }
bool Date::operator>=(const Date& d) const { return !(*this < d); }
bool Date::operator!=(const Date& d) const { return !(*this == d); }
五、const 成员函数
5.1 为什么要 const 成员函数
cpp
const Date d1(2024, 7, 13);
// d1 是 const 对象,内容不可修改
// d1.Print(); // 错误!非 const 成员函数不能用于 const 对象
// 因为 Print 可能修改成员变量
d1.Print(); // 只有在 Print 后面加上 const 才能被 const 对象调用
5.2 const 成员函数的语法
cpp
class Date {
public:
// const 修饰的是 *this,即 this 指向的内容不可修改
void Print() const {
// _year++; // 错误!const 成员函数不能修改成员变量
cout << _year << "/" << _month << "/" << _day << endl;
}
};
const 成员函数的本质:
const 移到 this 上
调用规则
const 对象 → 只能调用 const 成员函数 ✅
非 const 对象 → const 和非 const 都能调用 ✅
const 成员函数内 → 不能修改成员变量 ❌
编译器眼中的等价代码
void Print(const Date* const this) { ... }
你写的代码
void Print() const { ... }
cpp
// 你写的:
void Print() const { /* ... */ }
// 编译器眼中:
void Print(const Date* const this) { /* ... */ }
// ↑ const 修饰 this 指向的内容,即 *this 不可改
5.3 const 对象 vs 非 const 对象
cpp
int main() {
const Date d1(2024, 7, 13);
d1.Print(); // OK,Print() const
Date d2(2024, 7, 13);
d2.Print(); // OK,非 const 对象也可以调用 const 成员函数
}
const 成员函数可以被 const 对象和非 const 对象调用。非 const 成员函数只能被非 const 对象调用。
5.4 函数重载中的 const
const 可以作为函数重载的区分:
cpp
class Date {
public:
void Print() { cout << "非 const" << endl; }
void Print() const { cout << "const" << endl; }
};
int main() {
Date d1;
d1.Print(); // 输出:"非 const"(优先匹配非 const)
const Date d2;
d2.Print(); // 输出:"const"(只能匹配 const 版本)
}
六、取地址操作符重载(选读)
正常情况下你可能永远不需要重载取地址运算符,但它属于 6 个默认成员函数之一,了解一下即可。
cpp
class Date {
public:
Date* operator&() {
return this; // 正常返回地址
// return nullptr; // 也可以"骗"调用者
}
const Date* operator&() const {
return this;
}
};
七、成员函数指针(了解)
cpp
class A {
public:
void func() { cout << "A::func()" << endl; }
};
typedef void (A::*PF)(); // 成员函数指针类型
int main() {
PF pf = nullptr;
pf = &A::func; // C++ 规定成员函数必须用 & 取地址
A aa;
(aa.*pf)(); // 通过对象和指针调用
}
成员函数指针在实际开发中用得不多,但理解它有助于理解 this 指针的工作原理。
总结
中篇知识体系总览:
C++类和对象 中篇
拷贝构造函数
const T& d
参数必须是引用否则无限递归
三种调用场景
初始化新对象
函数传值参数
函数返回值
编译器默认生成的是浅拷贝
浅拷贝 vs 深拷贝
浅拷贝
逐字节复制
指针成员指向同一地址
析构时重复释放 → 崩溃
深拷贝
手动为新对象分配内存
复制内容到新内存
各自独立释放
黄金法则
持有动态资源 → 必须深拷贝
无动态资源 → 默认即可
赋值运算符重载
const T& d
返回 *this 支持连续赋值
拷贝构造 vs 赋值
左值正在创建 → 拷贝构造
左值已存在 → 赋值
运算符重载深入
成员函数 vs 全局函数
- 复用 += 的设计模式 比较运算符互相复用
const 成员函数
本质: const Date* const this
const 对象只能调 const 函数
const 成员不能修改成员变量
取地址运算符重载
了解即可
本篇重点内容:
- 拷贝构造函数 :
T(const T& d),在初始化新对象、传值、返回时调用 - 浅拷贝 vs 深拷贝:持有动态资源的类必须实现深拷贝,否则重复释放导致崩溃
- 赋值运算符重载 :
T& operator=(const T& d),返回*this支持连续赋值 - 运算符重载 :让自定义类型使用运算符如内置类型,
+复用+=,>复用< - const 成员函数 :
void func() const,const 对象只能调用 const 成员函数 - 取地址运算符重载:了解即可,一般不需要重载
下一篇(下篇),将介绍初始化列表、explicit 关键字、static 成员、友元、内部类、匿名对象、流插入/流提取重载等进阶特性,并给出一个功能完备的日期类。