打开效率的大门—C++类和对象(中):运算符重载、赋值构造函数与拷贝构造函数区别
文章目录
前言
在写代码中我们可能需要对类中的某个变量进行运算操作,但还得依靠层层函数才能够实现,但C++给出了运算符重载,化简了问题,本节将带你了解运算符重载的原理与作用。
一、初识运算符重载
(1.1)本质
本质为函数重载,通过定义特殊名称的函数(格式为operator运算符),实现运算符对自定义类型的自定义操作行为
(1.2)作用与限制
让自定义类型支持运算符操作(如对象相加、对象比较、对象赋值等),使代码更简洁、更符合直觉。
C但是仅能重载 C++ 允许的运算符,无法创建新运算符;不能改变运算符对内置类型的操作行为,仅能扩展自定义类型的行为;不能改变运算符的优先级、结合性和操作数个数
(1.3)运算符中无法重载类型
成员访问符:.
作用域解析符:::
长度运算符:sizeof
三目条件运算符:?:
成员指针访问符:.*****
类型转换运算符:typeid
二、深入运算符重载
(1.1)规定与操作
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错
//错误使用
int operator+(int x,int y)
{
return x+y;
}
//正确使用
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
(1.2)格式与要求
格式: 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体
int operator+( 类型a ,类型b)
要求: 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数
(1.3)所属差异
- 当函数不是成员函数时,参数就需要严格要求(1.2)中所示,比如:operator==
- 但当时成员函数时,则它的第⼀个运算对象默认传给隐式的this指针,因此运算
符重载作为成员函数时,参数⽐运算对象少⼀个
如Date类中的operator==函数
bool operator==(const Date& other) const {
// 语义:年、月、日全部相等,两个Date对象才相等
// const修饰成员函数:保证不修改当前对象(this)和参数对象(other)
return (this->year == other.year)
&& (this->month == other.month)
&& (this->day == other.day);
}
(1.4)特点
-
优先所属
优先级 / 结合性固定:重载后运算符的优先级、结合性、操作数个数,与对应内置类型运算符完全一致(例如+仍优先于==,=仍为右结合),无法修改 -
定义判断
一个类需要重载哪些运算符,是要根据类的基本特性来的,如Date类中:operator-就有意义,但是operator+就没有意义。 -
区分运算
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,C++中规定,后置++重载时,增加一个int形参,跟前置做出区分。
Date& operator++ (int)
{
cout << "后置" << endl;
}
Date& operator++ ()
{
cout << "前置" << endl;
}
- 重载<<与>>的特殊性
- 成员函数构造的缺陷:重载为成员函数,this指针默认为形参位置,第一个形参位置时左侧运算对象,调用时就成 “对象<<cout ”
i/o stream类型分别有输入的cin 和 输出的cout,因此传参为这个
- 成员函数构造的缺陷:重载为成员函数,this指针默认为形参位置,第一个形参位置时左侧运算对象,调用时就成 “对象<<cout ”
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
void operator(ostream& out)
{
cout << _year << _month << _day << endl;
return;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1 << cout;
}
这样调用就非常反人性了,但想在类外直接调用私有变量是不现实的。
有四种解决方法:
1、成员放公有
2、Date提供get函数
3、友元函数
4、重载为成员函数
这里建议使用友元函数(后面知识)
//在类输入
friend ostream& operator<<(ostream& out, const Data& d);
//在类外构造函数
ostream& operator<<(ostream& out, const Data& d)
{
out << d._year << "\\" << d._month << "\\" << d._day << endl;
return out;
}
三、探究赋值运算符重载
(1.1)定义与差异
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
(1.2)特点
-
参数类型: 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉
-
返回值特点: 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。
Date& operator= (Date& Odate)
{
this->_day = Odate._day;
this->_month = Odate._month;
this->_year = Odate._year;
return *this;
}
- 浅拷贝: 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数 (同拷贝构造函数相似)
当然解决方法还是实现深拷贝:
class String {
private:
char* str; // 指向动态分配的字符数组
public:
// 构造函数
String(const char* s = "") {
str = new char[strlen(s) + 1];
strcpy(str, s);
}
// 析构函数:释放动态内存
~String() {
delete[] str;
}
// 赋值运算符重载(深拷贝)
String& operator=(const String& rhs) {
// 1. 防止自赋值(关键!避免释放自身内存后拷贝)
if (this == &rhs) {
return *this;
}
// 2. 释放当前对象的旧内存
delete[] str;
// 3. 分配新内存并拷贝数据(深拷贝)
str = new char[strlen(rhs.str) + 1];
strcpy(str, rhs.str);
// 4. 返回自身引用,支持链式赋值
return *this;
}
}
- 默认生成特性: 如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。可以说只要没有指向资源的变量就不需要显示赋值函数
四、赋值构造函数与拷贝构造函数的区别及调用
(1.1)区别
本质:拷贝构造是 “生新对象”,赋值重载是 “改老对象”
- 调用时机不同
前者为已有对象赋值时,后者为新建对象调用
赋值重载函数:已有对象赋值时调用:直接赋值、链式赋值
拷贝构造函数:对象诞生时的调用 :直接和拷贝初始化、传值返回 - 实现逻辑不同
前者为先释放旧资源,后者为直接分配新资源
MyString& MyString::operator=(const MyString& other) {
// 1. 自赋值判断(核心)
if (this == &other) return *this;
// 2. 释放当前对象旧资源(避免内存泄漏)
delete[] this->buf;
this->buf = nullptr;
this->len = 0;
// 3. 拷贝源对象资源(同拷贝构造逻辑)
this->len = other.len;
if (other.len > 0) {
this->buf = new char[other.len + 1];
strcpy(this->buf, other.buf);
}
MyString::MyString(const MyString& other) {
// 直接为新对象分配资源,拷贝源对象内容
this->len = other.len;
if (other.len > 0) {
this->buf = new char[other.len + 1];
strcpy(this->buf, other.buf);
} else {
this->buf = nullptr;
}
}
- 语法规则不同
- 返回值差异:前者无返回值,后者为实现连续赋值,需要返回类名&(上面已有解释)
- 自操作区别:
赋值重载: 必须判断自赋值(this == &other),否则a = a时会先释放 a 的资源,后续拷贝访问野指针导致崩溃
拷贝构造: 无法处理 “自拷贝”(如Date a(a);),编译器会直接报错(无意义且危险),因此无需额外判断 - 重载差异
赋值重载: 是运算符重载的一种,属于成员函数,可被重载(如移动赋值operator=)
拷贝构造: 是构造函数的重载,仅能有一个拷贝构造(参数固定为const 类名&)
(1.2)调用
继续根据上述的原则:赋值重载是对两个已初始化对象,拷贝构造则是用一个初始化对象同时定义与赋值。
int main()
{
Date d1(2025,12,22);
Date d2(2025,12,21);
// 赋值重载
d1 = d2;
// 拷贝构造
Date d3(d2);
Date d4 = d2;
}
同时,返回值:类型引用 允许连续赋值
int main()
{
Date d1(2025,12,22);
Date d2,d3,d4;
d2 = d3 =d4 = d1;
}

总结
运算符重载完美的解决了C语言不能对类对象进行运算操作的缺点,在今后的学习中将发挥更多的作用,不仅能够完成赋值还能完善各类运算符操作,弄清本节的赋值构造函数与拷贝构造函数的区别也将为未来代码的基石。
更多推荐


所有评论(0)