文章目录
- 📝赋值运算符重载
- 🌠 运算符重载
- 🌉特性
- 🌠 赋值运算符重载
- 🌠传值返回:
- 🌠传引用赋值:
- 🌉两种返回选择
- 🌉赋值运算符只能重载成类的成员函数不能重载成全局函数
- 🚩总结
📝赋值运算符重载
🌠 运算符重载
运算符重载是C++中的一个重要特性,他允许我们为自定义的类型定义自己的运算符行为。通过运算符重载,我们可以使用与内置数据类型相同的语法来操作自定义类型,从而提高代码的可读性和可维护性。
还是我们熟悉的日期函数:
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 = 1;
int _month = 1;
int _day = 1;
};
然后我们定义两个日期对象d1和d2:
int main()
{
Date d1(2024, 2, 17);
Date d2(2024, 6, 27);
return 0;
}
当你想要比较两个对象d1和d2的数据是否一样,这是通常的比较方法:
创建一个专门的比较函数来比较两个Date
对象是否相同。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool isSame(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 2, 17);
Date d2(2024, 6, 27);
if (d1.isSame(d2))
{
cout << "d1 and d2 are the same date" << endl;
}
else
{
cout << "d1 and d2 are different dates" << endl;
}
return 0;
}
很明显的可以看出这是个比较函数,能不能直接通过像内置类型那样d1==d2
来比较相同呀,因此运算符重载就来了:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通函数类似。
函数名字为:关键字operator
后面接需要重载的运算符号。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool isSame(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//private:
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1,const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2024, 2, 17);
Date d2(2024, 6, 27);
//d1.isSame(d2)
if (d1 == d2)
{
cout << "d1 and d2 are the same date" << endl;
}
else
{
cout << "d1 and d2 are different dates" << endl;
}
return 0;
}
这样使用运算符重载是不是直接可以使用d1==d2
,方便了,但是这里有个注意点:此时bool
operator==(const Date& d1,const Date& d2)
这个函数我是写在全局变量中,因此private
也去掉,才能够访问。
这样一来,安全性降低了,可读性升高了,有点得不偿失,运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
补充:两种调用方式直接写和显示调用是一样的:
int main()
{
Date d1(2024, 2, 17);
Date d2(2024, 6, 27);
// 显式调用
operator==(d1, d2);
// 直接写,装换调用,编译会转换成operator==(d1, d2);
d1 == d2;
return 0;
}
两者的call指令是一样的:
bool operator==(const Date& d1, const Date& d2)
重载成全局,无法访问私有成员,解决办法有三种:
- 使用 getter 和 setter 函数的方案:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int GetYear() { return _year; }
int GetMonth() { return _month; }
int GetDay() { return _day; }
void SetYear(int year) { _year = year; }
void SetMonth(int month) { _month = month; }
void SetDay(int day) { _day = day; }
bool operator==(const Date& d) const
{
return _year == d._year && _month == d._month && _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
- 使用友元函数的方案:
- 这种方式可以直接访问私有成员变量,不需要额外的 getter 和 setter 函数。
- 但是,将
operator==
函数声明为友元函数会破坏类的封装性,需要谨慎使用。
class Date
{
friend bool operator==(const Date& d1, const Date& d2);
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
- 重载为成员函数的方案:
- 这是最常见和推荐的方式。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
这里我们本节主要学习第三种重载为成员函数的方案:bool operator==(const Date& d)
这里需要注意的是,左操作数是this
,指向调用函数的对象。
这里的参数使用const修饰,确保传入的原对象不被修改。
函数的调用方法:
int main()
{
Date d1(2024, 2, 17);
Date d2(2024, 6, 27);
//显示调用
d1.operator==(d2);
//转换调用,等价与d1.operator==(d2);
d1 == d2;
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
//注意这里的()括号,因为符号优先级问题
return 0;
}
对于自定义对象我们可以使用运算符,返回值是根据运算符来决定,加减返回int
类型,判断大小,使用bool
类型:一个类要重载哪些运算符是看需求,看重载有没有价值和意义
🌉特性
- 不能通过连接其他符号来创建新的操作符:比如
operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.*
::
sizeof
?:
.
注意以上5个运算符不能重载。这个经常在笔试选择题中出
现。
这里主要注意的是.*
,看看这个代码:
class OB
{
public:
void func()
{
cout << "void func" << endl;
}
};
typedef void(OB::*ptrFunc)();//成员函数指针类型
int main()
{
//函数指针
//void(*ptr)();
//成员函数要加&才能取到函数指针
ptrFunc fp = &OB::func;//定义函数指针fp指向func
OB temp;//定义OB类对象temp
(temp.*fp)();
return 0;
}
首先这是普通函数指针的定义:
//函数指针
void(*ptr)();
其次是成员函数指针类型:
typedef void(OB::*ptrFunc)();//成员函数指针类型
在这个代码中,typedef void(OB::*ptrFunc)()
定义了一个新的类型 ptrFunc
,它是一个指向 OB
类的成员函数的指针类型。
-
void(OB::*)()
- 这是一个函数指针类型,它指向一个返回值为
void
且没有参数的成员函数。 OB::*
表示这个函数指针是指向OB
类的成员函数。
- 这是一个函数指针类型,它指向一个返回值为
-
typedef void(OB::*ptrFunc)();
- 使用
typedef
关键字定义了一个新的类型ptrFunc
。 ptrFunc
就是这个指向OB
类成员函数的指针类型的别名。
- 使用
这样定义之后,我们就可以使用 ptrFunc
这个类型来声明指向 OB
类成员函数的指针变量了。
//成员函数要加&才能取到函数指针
ptrFunc fp = &OB::func;//定义函数指针fp指向func
在 main()
函数中,我们使用 &OB::func
获取了 OB
类的 func()
成员函数的地址,并将其赋值给 ptrFunc
类型的变量 fp
。
OB temp;//定义OB类对象temp
(temp.*fp)();
然后,我们创建了一个 OB
类的对象 temp
。最后,使用 (temp.*fp)();
语法调用了 temp
对象的 func()
成员函数。这里的 .*
运算符用于通过成员函数指针调用成员函数。
🌠 赋值运算符重载
上节我们学了拷贝构造来进行数据的复制:一个已经存在的对象,拷贝给另一个要创建初始化的对象
Date d1(2024, 4, 20);
// 拷贝构造
// 一个已经存在的对象,拷贝给另一个要创建初始化的对象
Date d2(d1);
Date d3 = d1;
当然那还有赋值拷贝/赋值运算符重载也可以进行复制:
一个已经存在的对象,拷贝赋值给另一个已经存在的对象
Date d1(2024, 4, 20);
d1 = d4;
这里是单个赋值,能不能连续像内置类型那样赋值?
int i, j, k;
i = j = k = 1;
连续赋值的本质是:从右向左开始,1赋值给k,k=1表达式返回值为左操作数k,接着j赋值给k,j=k表达式返回值为左操作数,再接着i就拿到了1,连续赋值完毕。
同理,自定义类型也是一样的,有两种方式传值和引用:
🌠传值返回:
Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
由于每次返回的都是左操作数,我们需要this,但是这是传值返回,返回的是this的临时拷贝,不是这个函数里局部变量*this
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2 = d4;
// ->d2 = d4
// ->d1 = d2;
Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 14);
// 拷贝构造
// 一个已经存在的对象,拷贝给另一个要创建初始化的对象
Date d2(d1);
Date d3 = d1;
Date d4(2024, 5, 1);
// 赋值拷贝/赋值重载
// 一个已经存在的对象,拷贝赋值给另一个已经存在的对象
d1 = d4;
d1 = d2 = d4;
return 0;
}
当从右向左开始,d4
赋值给d2
,d2=d4
表达式返回值为左操作数d2
,这里的d2
是*this
,但是传值返回,会生成一个临时的拷贝,返回的是Date *this
,此时此刻,如果你一步一步调试,他会跳转到Date
的构造函数里Date(const Date& d)
,然后刷新private的值,然后进行第二步的连续赋值d2
赋值给d1
,和上面的步骤是一样的。
🌠传引用赋值:
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
这里按引用传递,无需再创造临时变量拷贝,再调用拷贝构造函数,当从右向左开始,d4
赋值给d2
,d2=d4
表达式返回值为左操作数d2
,这里的d2
是*this
,*this直接返回的是左操作数的d2的别名。
这里还需注意的一点是,我们可能会出现写错,避免不必要的操作:比如自赋值
d1=d1
处理自赋值问题主要有以下几个原因:
- 如果不检查自赋值的情况,当执行
d1 = d1
时,会进行不必要的赋值操作。这可能会造成性能的浪费,尤其是对于大型对象而言。 - 某些情况下,对象的成员变量可能依赖于其他成员变量的值。如果在赋值过程中,这些成员变量的值被覆盖,可能会导致对象处于不一致的状态,从而引发错误。
修改自赋值问题:使用地址来判断
Date& operator=(const Date& d)
{
if (this != &d)//使用的是地址判断
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
因此我们总结一下赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
🌉两种返回选择
- 传值返回
Date func()
{
Date d(2024, 4, 14);
return d;
}
int main()
{
const Date& ref = func();
return 0;
}
选择传值,还是传引用呢?
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2 = d4;
// d2 = d4
// d1 = d2
// Date operator=(const Date& d)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
cout << "~Date()" << endl;
_year = -1;
_month = -1;
_day = -1;
}
private:
int _year;
int _month;
int _day;
};
Date func()
{
Date d(2024, 4, 14);
return d;
}
int main()
{
const Date ref = func();
return 0;
}
- 传引用返回
Date& func()
{
Date d(2024, 4, 14);
return d;
}
int main()
{
const Date& ref = func();
return 0;
}
总结一下:返回对象是一个局部对象或者临时对象,出了当前
func
函数作用域,就析构销毁了,那么不能用引用返回用引用返回是存在风险的,因为引用对象在func
函数栈帧已经销毁了
虽然引用返回可以减少一次拷贝,但是出了函数作用,返回对象还在,才能用引用返回
理解了func
函数,那么operator=
重载赋值函数返回选择哪种方式也是同样的方法:
*this
是d2
,在main
函数传参的时候,this
指针是存放栈空间的,当operator
函数生命周期结束时,*this
回到的是回到的是main
函数的,也就是*this
离开operator
时生命周期未到,不会析构,因此按引用返回。
🌉赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
编译失败:
error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
直接无法加载发生异常!
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。