赋值运算符重载
运算符重载
运算符重载的本质 : 它是 C++ 赋予程序员的一种语法糖,让用户自定义类型能够像内置类型一样,使用 + - * / == != < > << >> 等运算符进行操作,从而大大提升代码的可读性和表达力
为什么有运算符重载 ,如果你写了一个日期类 Date,现在想比较两个日期是否相等
-
没有重载时 :你可能得写
if (date1.isEqual(date2)) -
重载之后 :你可以直接写
if (date1 == date2)
是不是这么一对比就显得很简洁
核心语法:operator 关键字
运算符重载的本质是函数调用, 它的函数名格式非常固定:返回值类型 operator运算符(参数列表)
operator使用
cpp
#include <iostream>
class Date
{
public:
// 构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 3, 28);
Date d2(2024, 3, 28);
Date d3(2026, 1, 1);
std::cout << "d1: " << std::endl;
d1.Print();
std::cout << "d2: " << std::endl;
d2.Print();
std::cout << "d3: " << std::endl;
d3.Print();
if (d1 == d2)//隐式类型转换
{
std::cout << "d1 != d2 " << std::endl;
}
else
{
std::cout << "d1 == d2 " << std::endl;
}
return 0;
}
先单独来看这段代码
cpp
bool operator==(const Date& d)const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
operator 是关键字operator== 连在一起就是这个函数的函数名,当写 d1 == d2 时,编译器其实是在偷偷调用 d1.operator==(d2)
这里为什么要用 const 和 &
-
&(引用) :如果不加&,程序会把整个d2对象拷贝一份传进来,浪费内存和时间,用引用相当于传了个"别名",效率最高 -
const:由于我们是传引用,如果不加const,函数内部万一不小心改了d._year或者月或日,外面的d2也会跟着变,加上const是为了承诺:"我只看你的数据,绝不乱动"
为什么函数右边还有一个const?
-
它的含义 :这个
const修饰的是隐藏的this指针,它告诉编译器:"这个函数保证不会修改调用它的对象(即d1)本身"。 -
为什么必须加 :在 C++20 标准下,编译器会尝试对
==进行对称匹配,如果你的函数不加const,编译器会觉得这个函数"不够安全",在匹配时会产生歧义,加上它,代码的健壮性直接拉满 -
只要一个成员函数不需要修改成员变量,就建议加上
const

既然能判断相等,按不相等是不是也可以试一下
cpp
//#include <iostream>
//using namespace std;
//class Date
//{
//public:
// // 构造函数
// Date(int year = 1900, int month = 1, int day = 1)
// {
// _year = year;
// _month = month;
// _day = day;
// }
// bool operator=(const Date& d)const
// {
// if (*this != &d)
// {
// _year = d._year
// _month = d._month
// _day = d._day;
// }
// }
// void Print()
// {
// std::cout << _year << "-" << _month << "-" << _day << std::endl;
// }
//private:
// int _year;
// int _month;
// int _day;
//};
//
//int main()
//{
// Date d1(2024, 3, 28);
// Date d2(2024, 3, 28);
// Date d3(2026, 1, 1);
//
// std::cout << "d1: " << std::endl;
// d1.Print();
// std::cout << "d2: " << std::endl;
// d2.Print();
// std::cout << "d3: " << std::endl;
// d3.Print();
// if (d2 != d3)
// {
// cout << "d2 != d3" << endl;
// }
// else
// {
// cout << "other" << endl;
// }
// return 0;
//}
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool operator!=(const Date& d)const
{
return _year != d._year &&
_month != d._month &&
_day != d._day;
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 3, 28);
Date d2(2024, 3, 28);
Date d3(2026, 1, 1);
std::cout << "d1: " << std::endl;
d1.Print();
std::cout << "d2: " << std::endl;
d2.Print();
std::cout << "d3: " << std::endl;
d3.Print();
if (d2.operator!=(d3))//显示类型转换
{
cout << "d2 != d3" << endl;
}
else
{
cout << "other" << endl;
}
return 0;
}
其实我们就可以把刚才的情况取反就可以了

但是我们还可以这样写
cpp
//部分代码,省略部分和上面代码一致
bool operator==(const Date& d)const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool operator!=(const Date& d)const
{
return !(*this == d);
}
这个叫做代码复用
什么是代码复用
代码复用就是"不重复造轮子"
在 Date 类里,判断"不相等"逻辑其实就是"相等"逻辑的对立面
-
常规写法(不推荐) :在
!=里再写一遍_year != d._year ......这种写法一旦你以后想增加一个"时分秒"字段,你得改两个地方,漏改一个就是 Bug -
代码复用(推荐) :
!=直接去问==:"兄弟,你俩相等吗?"然后把结果取反
核心思想: 只维护一套核心逻辑(==),其他相关逻辑(!=)都基于核心逻辑构建
编译器看到 *this == d,发现左边是一个 Date 对象,右边也是一个 Date 对象,于是它会去类里找 operator==
赋值运算符重载
赋值运算符重载,是指将一个已存在的对象赋值给另一个已经存在的对象
-
拷贝构造:对象还没出生,用别人来"初始化"它
-
赋值重载:对象已经出生了,现在要"更新"它的值
cpp
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数
Date(int year = 1900, 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;
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 3, 28);
Date d2(2024, 3, 28);
Date d3(2026, 1, 1);
std::cout << "d1: " << std::endl;
d1.Print();
std::cout << "d2: " << std::endl;
d2.Print();
d3.operator=(d1);
std::cout << "d3: " << std::endl;
d3.Print();
return 0;
}
我们先来看一下这一段代码
cpp
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
这里传入和返回都用的是引用,所以就不存在产生**临时对象,**把改变的日期会引用回去

初始化列表
初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成 员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地方
引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始 化,否则会编译报错

代码示例
cpp
#include<iostream>
using namespace std;
class date
{
public:
date(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;
};
int main()
{
int x = 0;
date d1(2020, 1, 1);
d1.Print();
return 0;
}

如果声明中的成员变量被const修饰 或者定义的是引用的话,就必须要走初始化列表
cpp
#include<iostream>
using namespace std;
class date
{
public:
date(int& x)//初始化列表
: _n(1)
{}
void Print() {
cout << _n << endl;
}
private:
//必须在初始化列表中初始化
const int _n;
};
int main()
{
int x = 0;
date d1(x);
d1.Print();
return 0;
}

cpp
#include<iostream>
using namespace std;
class date
{
public:
date(int& x)//初始化列表
: _ret(x)
{}
void Print() {
cout << _ret << endl;
}
private:
int& _ret;
};
int main()
{
int x = 100;
date d1(x);
d1.Print();
return 0;
}

cpp
#include<iostream>
class date
{
public:
date(int year, int month, int day)
:_year(year)//尽可能走初始化列表
, _month(month)
{
}
void Print() {
std::cout << _year << "/" << _month << "/" << _day << std::endl;
}
private:
//声明,缺省值->初始化列表
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
date d1(2020, 1, 10);
d1.Print();
return 0;
}
这里就算我们没有写"日"的初始化列表,但是在声明中我们给了初始值,就会调用初始值

题目
下面程序的运行结果是什么()
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{
}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}
成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是按照初始化列表中写的顺序
初始化过程(构造 A aa(1)):
- 先初始化 _a2 (因为它先声明):
- 初始化列表中写了 _a2(_a1),但此时 _a1还没有被初始化。
- _a1 仍然处于"未初始化"状态(虽然类中写了默认值 =2,但在初始化列表阶段,默认值不会覆盖初始化列表)。
- 所以 _a2 被初始化为 _a1 当前的值 → 未定义行为(通常是垃圾值)
- 再初始化 _a1 :
- 初始化列表中写了 _a1(a),即 _a1 = 1

类型转换
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
• 构造函数前面加explicit就不再支持隐式类型转换
• 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
cpp
int i = 1;
double j = i;
在这段代码中,我是先让i初始化为1,然后用i去初始化j,但是i和j不是相同的数据类型,所以在这个过程中会产生**临时变量,**先让i的值给这个编译器生成的临时变量,然后让j去接收这个临时变量
cpp
#include <iostream>
using namespace std;
class A
{
public:
//explicit A(int a = 0)
A(int a)
:_a1(a)
{ }
void print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
//类型转换
A aa2 = 2;
aa2.print();
return 0;
}
这里我把整型类型的值初始化给类类型,就会产生临时对象,这个临时对象再给类类型,而这一步会被编译器优化掉,但从语法上是会产生临时对象
我们再来看一个新东西

这里的raa1初始化的值是aa2,但是raa2不能初始化为2,因为类型不同会产生临时变量,而临时对象具有常性,需要用const修饰
cpp
int main()
{
A aa2 = 2;
A& raa1 = aa2;
const A& raa2 = 2;
return 0;
}
接下来在看一下这段代码
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
{
_a1 = a;
}
A(const A& aa)
{
_a1 = aa._a1;
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
}
private:
int _a1;
int _a2;
};
class Stack
{
public:
void push(const A& aa)
{
}
private:
A _arr[10];
int _top;
};
int main()
{
A aa2 = 2;
A& raa1 = aa2;
const A& raa2 = 2;
Stack st;
A aa3(3);
st.push(aa3);
return 0;
}
首先我把aa3初始化为3,然后把aa3给push,直接将 aa3 的地址绑定到引用 aa,"st.push(aa3)"不会产生临时变量,但整个过程较为麻烦,那我们也可以这样写
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
{
_a1 = a;
}
A(const A& aa)
{
_a1 = aa._a1;
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
}
private:
int _a1;
int _a2;
};
class Stack
{
public:
void push(const A& aa)
{
}
private:
A _arr[10];
int _top;
};
int main()
{
Stack st;
st.push(3);
return 0;
}
这样我直接push3会产生临时变量,再把临时变量给push函数,所以我们需要加const修饰
友元函数
类的私有成员(private) 是被严格保护的,外部函数或普通函数无法直接访问
但在实际开发中,我们有时候希望某个外部函数能够直接访问类的私有成员,这时就需要用到 友元函数
cpp
#include<iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
