

🔥@雾忱星: 个人主页
👀专栏:《数据结构与算法入门指南》、《C++学习之旅》
💪学习阶段:C/C++、数据结构与算法
⏳"人理解迭代,神理解递归。"
文章目录
- 引言
- 一、拷贝构造函数
-
- [1.1 解析:拷贝构造特点](#1.1 解析:拷贝构造特点)
- [1.2 关键:拷贝构造的调用](#1.2 关键:拷贝构造的调用)
- 二、赋值运算符重载
-
- [2.1 铺垫:运算符重载特点](#2.1 铺垫:运算符重载特点)
-
- [2.1.1 核心:理解运算符重载](#2.1.1 核心:理解运算符重载)
- [2.2 进阶:赋值运算符重载特点](#2.2 进阶:赋值运算符重载特点)
-
- [2.2 核心:理解赋值运算符重载](#2.2 核心:理解赋值运算符重载)
- 总结
引言
在C++面向对象编程中,对象的复制操作无处不在。无论是函数传参、返回值传递,还是对象间的赋值,都需要精确控制数据的复制行为。
C++通过拷贝构造函数和赋值运算符重载两套机制,为开发者提供了对象复制的完整解决方案。
本文将从基础概念出发,深入解析这两种复制机制的实现细节与应用技巧。
一、拷贝构造函数
如果一个构造函数 的第一个参数 是**自身类型的引用**,且 其他所有参数都有默认值(如果有) ,就叫做 拷贝构造,是特殊的构造函数。
- 基本形式:
cpp
#include <iostream>
using namespace std;
//基本形式
class Example
{
public:
Example(Example& d)
{
//...
}
};
1.1 解析:拷贝构造特点
(部分规则与构造函数相同)
- 拷贝构造函数是构造函数的一个重载;
- 拷贝构造函数的第一个参数必须是自身类类型的引用:类名& 或 const 类名&(最好加
const)。 如果使用传值的方式,在逻辑上会引发无穷递归调用; - 拷贝构造函数可以有多个参数,第一个为引用,其他必须有缺省值;
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成;
- 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。默认生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝 (⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造;
- 类似前面说的
Date类,成员变量全是内置类型且不指向资源,编译器默认生成的拷贝构造就够了。类似Stack类,虽然也都是内置类型,但是指针指向资源,那么编译器默认生成的浅拷贝/值拷贝就不太够,需要显式定义深拷贝。再对于MyQueue类,自定义类型Stack变量成员就直接调用它的拷贝构造。
【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。
- 传值返回会产生一个临时对象来调用拷贝构造;而传引用返回,返回的是对象的别名,不会产生拷贝,但是返回的对象为一个当前函数局部域的局部对象,函数结束就会销毁,这时传引用返回是有问题的,类似于野指针。
(传引用返回会减少拷贝,但是要确保返回对象在函数结束时不会被销毁)
- 解释特点第2条:

看图也可以明白,当==拷贝构造函数传值传参==时,函数的形参是实参拷贝出来的新对象,要调用拷贝构造,但是拷贝构造函数也是传值传参就又要调用拷贝构造,这样无限循环下去......
其次,在引用传参最好加上const,因为将对象传过来,也不会将对象进行改变操作,那么const就方便了传参(权限缩小)。当然,这时候传const对象也是可以的(权限平移)。
-
- 特点第2条拓展:既然要引用传参,那么指针可以吗?
先说,传指针是可以的,但是函数就变成普通的构造函数,不是拷贝构造函数。
- 特点第2条拓展:既然要引用传参,那么指针可以吗?
cpp
#include <iostream>
using namespace std;
//传指针
class Date
{
public:
//构造函数:全缺省
Date(int day = 8, int month = 1, int year = 2026)
{
_day = day;
_month = month;
_year = year;
}
//指针传参
Date(Date* d)
{
_day = d->_day;
_month = d->_month;
_year = d->_year;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
//调用构造函数初始化d1
Date d1;
d1.Print();
//传地址
Date d2(&d1);
d2.Print();
return 0;
}

- 解释特点第5条:
拷贝构造函数就和构造、析构有点不同。它会对内置类型的成员变量进行处理。
类似Date类这样全是内置类型的变量,编译器默认生成的就够用;对于复杂结构的类Stack,就要自定义深拷贝;对于MyQueue这样的类,不显式定义拷贝构造,编译器就会调用成员变量对应类的拷贝构造。
- 解释特点第6条: 通过实现栈来观察
-
-
有指向的资源,浅拷贝的后果:
- 一个对象改变会影响另一个对象;
- 析构时,同一块空间会释放两次空间;
-
cpp
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// Stack st2(st1);
Stack(const Stack& s)
{
_a = s._a;
_capacity = s._capacity;
_top = s._top;
}
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 s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}

(会产生错误,通常是内存相关的问题。)

这样浅拷贝会使两个对象的指针变量都指向同一块空间 ,最后的两次析构就导致第二次析构对已经释放完的空间再次释放,发生错误。
-
-
有指向的资源,自定义深拷贝:(先简单了解)
- 不仅仅对成员拷贝,还对指向的资源空间数据进行处理。(开空间)
-
cpp
Stack(const Stack& s)
{
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
if (_a == NULL)
{
perror("realloc fail");
return;
}
memcpy(_a, s._a, s._top * sizeof(STDataType));
_capacity = s._capacity;
_top = s._top;
}

调试程序发现:不指向同一空间。
- 解释特点第7条:
在上面栈的基础上
cpp
int& func1()
{
int ret = 1;
return ret;//返回的是ret的别名
}
Stack& func2()
{
Stack st;
return st;
}
int main()
{
int ret1 = func1();//ret在函数结束时就销毁了,所以这里存在错误
cout << ret1 << '\n';//可能是1或者随机值
Stack ret2 = func2();//调拷贝构造,但是st不存在
return 0;
}

根据特点7,函数传值返回是会调用拷贝构造的,但是传引用返回不会。对于st这里,函数就是进行析构(成员函数),那么在通过返回的别名来访问st肯定是错的。
【所以,在传引用返回是一定要注意返回对象是否还存在!】
1.2 关键:拷贝构造的调用
- 用一个对象初始化另一个同类的对象(在创建的同时初始化)
基本形式:
cpp
Example a;
Example b(a); // 调用拷贝构造函数
Example c = a; // 调用拷贝构造函数
cpp
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数:全缺省
Date(int day = 8, int month = 1, int year = 2026)
{
_day = day;
_month = month;
_year = year;
}
//拷贝构造函数
Date(Date& d)
{
_day = d._day;
_month = d._month;
_year = d._year;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
//调用构造函数初始化d1
Date d1;//不要写成Date d1();
d1.Print();
//创建对象的同时,调用拷贝构造进行初始化
Date d2(d1);
d2.Print();
return 0;
}

- 函数参数按值传递该类的对象(传值传参)
基本形式:
cpp
void func(Example obj) { ... }
Example a;
func(a); // 调用拷贝构造函数
注意: 调用函数,形参是用实参拷贝构造出来的新对象 ,将实参传递就符合调用拷贝构造的规则。(函数形参也是一个需要被创建的对象。)
cpp
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数:全缺省
Date(int day = 8, int month = 1, int year = 2026)
{
_day = day;
_month = month;
_year = year;
}
//拷贝构造函数
Date(Date& d)
{
_day = d._day;
_month = d._month;
_year = d._year;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
void func(Date d)
{
d.Print();
}
int main()
{
//调用构造函数初始化d1
Date d1;
d1.Print();
func(d1);//调用拷贝函数
return 0;
}

在VS2022上调试程序发现,调用
func()函数时,将对象传参,会先进行拷贝构造,然后进入func()函数内。(函数进行传值传参,会先完成传参)
二、赋值运算符重载
2.1 铺垫:运算符重载特点
- 当运算符被用于类类型的对象 时,C++允许通过运算符重载的形式指定新的含义。C++规定,类类型对象使用运算符时 ,需要转换成调用相应的运算符重载,没有会报错;
- 运算符重载式具有特殊名字的函数,名称由
operator和后面的运算符构成,与普通函数一样,具有返回值、返回类型、参数、函数体等; - 重载运算符函数的参数个数和运算符的操作对象数量相同:一元运算符一个参数,二元运算符两个参数。对于二元:左侧运算对象传给第一个参数,右侧传给第二个参数;
- 如果一个重载运算符函数是成员函数 ,则它的第一个运算对象默认传隐式的
this指针,因此运算符重载作为成员函数时,参数比运算对象少一个; - 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致;
- 不能通过连接语法中没有的符号来创建新的操作符:比如operator@;
.*、::、sizeof、? :、.以上五个运算符不能重载;- 重载运算符至少有一个类类型的参数 ,不能通过运算符重载改变内置类型对象的含义,如:
operator+(int x, int y); - 一个类需要哪些运算符重载。是看那些有实际意义,比如:
Date类的operatoe-有意义,operatoe*没有意义; - 重载 ++ 运算符时,有 前置 ++ 和 后置 ++ ,运算符重载函数名都是
operator++,无法很好的区分。C++规定,后置 ++ 重载时,增加一个int形参,跟前置 ++ 构成函数重载,方便区分; - 重载
<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象 ,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。
2.1.1 核心:理解运算符重载
【搭配举例】:
cpp
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int day = 8, int month = 1, int year = 2026)
{
_day = day;
_month = month;
_year = year;
}
//如果运算符重载载在类外实现,解决变量私有化一个方法:
int Getyear()
{
return _year;
}
//或者在类内定义
//比较Date类对象是否相等
bool operator==(const Date& d)//this占第一个,显式第一个参数为左侧运算对象
//第4条:默认第一个参数为this指针
{
return _day == d._day && _month == d._month &&
_year == d._year;
}
private:
int _day;
int _month;
int _year;
};
//类外定义
//bool operator==(const Date& x1, const Date& x2) //第2条
//{
// return x1.day == x2._day && x1._month == x2._month &&
// x1._year == x2._year;
//}
int main()
{
Date d1(1,1,1);
Date d2;
cout << (d1 == d2) << endl; //第3条
}
【额外注意】:
-
当在类外定义运算符重载时,因为成员变量私有无法访问,可采取:
- 将成员变量访问改为公有,但是太极端危险;
- 在类内定义
int Getyear();之类函数,获取成员变量; - 直接在类内定义运算符重载成为成员函数(推荐),但是要注意参数的改变(第4条)。
- 友元函数(后面会有)。
-
在最后输出结构,注意优先级。
<< / >>优先级较高 ,所以... == ...要加括号。
【介绍.*运算符】:C++不常用,了解
cpp
#include<iostream>
using namespace std;
void func1()
{
cout << "void func()" << endl;
}
class A
{
public:
void func2()
{
cout << "A::func()" << endl;
}
};
int main()
{
// 普通函数指针
void(*pf1)() = func1;
(*pf1)();
// A类型成员函数的指针
void(A:: * pf2)() = &A::func2;
A aa;
(aa.*pf2)();//这里就是使用的.*
//(aa.*pf2)(&aa);错误,this指针不能显式出现参数。
return 0;
}
2.2 进阶:赋值运算符重载特点
赋值运算符重载是一个默认成员函数,用于完成两个已存在的对象直接的拷贝复制,要和拷贝构造区分开。
- 赋值运算复是一个运算符重载,C++规定必须为成员函数。参数建议写成
const当前类类型引用传参,当然传值传参会调用拷贝构造; - 有返回值,建议写成当前类类型引用,传引用返回可以提高效率,有返回值就可以连续赋值;
- 当没有显式实现,编译器会默认生成,其行为和默认生成的拷贝构造类似,对内置类型成员变量会先完成值拷贝,对自定义类型会调用相应的赋值重载函数;
- 类似
Date类,为内置类型成员且不指向任何资源,编译器默认生成的浅拷贝就够了。但是类似Stack类,有指向的资源,就需要自定义深拷贝。
(这里和拷贝构造类似)
【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。
2.2 核心:理解赋值运算符重载
cpp
#include<iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int day = 8, int month = 1, int year = 2026)
{
_day = day;
_month = month;
_year = year;
}
//拷贝构造
Date(const Date& d)
{
_day = d._day;
_month = d._month;
_year = d._year;
}
//赋值重载
//d1 = d2
Date& operator=(const Date& d)
{
if (this != &d)
{
_day = d._day;
_month = d._month;
_year = d._year;
}
return *this;//返回d1别名,不拷贝
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Date d1(1,1,1);
Date d2(d1);//拷贝构造
Date d3 = d1;//拷贝构造
d2.Print();
d3.Print();
Date d4;
d5 = d4 = d1;//赋值重载
d4.Print();
return 0;
}
- 【说明】 :
Date& operator=(const Date& d);为什么可以传引用返回?
在拷贝构造部分,有过说明"传值返回会发生拷贝",但是this不是这个函数的局部对象,不会销毁,额外的拷贝就很麻烦,没必要。
总结
html
🍓 我是晨非辰Tong!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:
拷贝构造函数与赋值运算符重载构成了C++对象复制机制的核心支柱。它们分别负责对象初始化和对象赋值两种不同场景的复制需求。
掌握这些复制控制机制,不仅能写出更安全的代码,更能深入理解C++对象生命周期的管理哲学。这是从C++使用者迈向C++设计者的重要一步。
