前言
本章我们接替前一章继续深入理解类的默认成员函数,赋值重载,取地址重载,及const取地址操作符重载
但是在讲剩下的三个默认成员函数之前,我们要先来了解运算符重载,因为赋值重载,取地址重载,及const取地址操作符重载其实是属于运算符重载的一部分。
类与对象(三)
- 一、运算符重载
- 1、运算符重载的概念
- 2、运算符重载的注意事项
- 二、运算符重载的特例
- 1、前置++和后置++类型
- 2、流插入<< 流提取>>运算符
- 四、赋值运算符重载(默认成员函数)
- 1、引入
- 2、特性
- 1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
- 2. 赋值运算符重载格式:
- 3. 赋值运算符只能重载成类的成员函数不能重载成全局函数
- 4. 如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
- 五、取地址及const取地址操作符重载
- 1、取地址操作符重载(默认成员函数)
- 取地址重载的手动实现
- 2、const取地址操作符重载(默认成员函数)
- const取地址重载手动实现
一、运算符重载
1、运算符重载的概念
对于C++的内置类型,我们有许多运算符可以使用,但是这些运算符却无法对自定义类型进行使用,我们只能写一个与运算符功能类似的函数,让自定义类型去调用。
例如:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//给一个给日期加num天,不修改原始值
Date Add(int num)
{
//......
}
//给一个日期加num天,并修改原始值
Date AddEqual(int num)
{
//.....
}
private:
int _year;
int _month;
int _day;
};
int main()
{
int a = 10;
a + 10;
a += 10;
Date d1;
d1.Add(10);
//d1+10; //想写成这样,这样更直观方便,可是编译器不允许啊啊啊
d1.AddEqual(10);
//d1+=10; //想写成这样,这样更直观方便,可是编译器不允许啊啊啊
return 0;
}
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
只看定义不太好理解运算符重载,我们还是直接先看代码,结合代码边分析边理解定义与注意要点。
// 全局的operator==
#include<iostream>
using namespace std;
class Date
{
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;
}
void Test()
{
Date d1(2023, 2, 12);
Date d2(2023, 2, 12);
cout << operator==(d1, d2) << endl;//判断两个对象是否相同,第一种使用方法,直接调用函数
cout << (d1 == d2) << endl;//判断两个对象是否相同,第二种使用方法,使用重载后的运算符
//此处必须加括号,运算符优先级:<< 大于 ==
}
相信仔细看完这个代码后你已经对运算符重载有了一定的了解,就是大概相当于自定义类型的运算符其实是一个我们手动写函数,但是呢经过运算符重载以后我们可以像使用内置类型运算符那样去使用函数。
2、运算符重载的注意事项
- 不能通过连接其他符号(C++中不存在的运算符)来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数(因为运算符重载主要是为了让自定义类型能够像内置类型那样去使用运算符)
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义(因为运算符重载主要是为了让自定义类型能够像内置类型那样去使用运算符,内置类型不需要运算符重载)
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的
this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
二、运算符重载的特例
1、前置++和后置++类型
经过上面的运算符重载的讲解相信你对于Date类中的一些其他运算符也能写出它的运算符重载如:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator+(int num)
{
//......
}
Date& operator-(Date& d)
{
//......
}
Date& operator+=(int num)
{
//......
}
//......
//其实这么多运算符重载我们可以一一实现,也可以实现一到两个,然后让其他运算符重载
private:
int _year;
int _month;
int _day;
};
但是我们在学习C语言时学习过 前置++,后置++,前置- -,后置- -, 对于这种运算符重载我们应该怎么做呢?
我们先写一下函数外部的参数列表:
//前置++
Date& operator++()
{
}
//后置++
Date operator++()
{
}
我们发现它们的函数外部参数列表一模一样,这样根本无法构成函数重载,我们也只能实现 前置++,后置++中的一个。
为了解决这个问题,我们C++给这种前置++,后置++,前置- -,后置- -,这种运算符重载时进行了特殊化处理,规则是:
- 前置++正常实现
- 后置++在运算符重载时多传一个int型参数(此参数只是为了占位,实现函数重载,不起其他作用,也不必真的传递参数)
(- -与以上规则类似)
所以正确实现我们应该这样实现:
//前置++
Date& operator++()
{
*this += 1;//假设 +=我们已经运算符重载过了
return this;
}
//后置++
Date operator++(int a)//形参名可以不写,直接写成 int,调用时也不需要传递参数
{
Date tmp(this); //会去调用拷贝构造
*this += 1; //假设 +=我们已经运算符重载过了
return tmp;
}
2、流插入<< 流提取>>运算符
在前面讲解运算符重载时我们说过,<<其实是移位运算符,但是呢在C++中被重载为了流插入运算符,那具体是怎么做的呢?我们现在学习了运算符重载已经可以去讨论这个问题了。
首先我们经常使用的cout
和cin
,其实分别是一个ostream
类型对象,一个是istream
类型的对象,这两个类型又分别在<ostream>
和<istream>
两个C++标准库的std
标准命名空间内,然后我们经常使用的C++标准库<iostream>
又包含了<istream>
和<ostream>
头文件,所以我们使用cin
或cout
的时即要包含头文件<iostream>
又要使用using namespeace std;
将标准命名空间里面的内容展开到全局中。
我们再来看看流提取<<
运算符重载
可以看到在C++对流提取<<
运算符进行了许多运算符重载+函数重载,我们对于内置类型可以随意使用<<
,但是自定义类型我们却没有办法使用,因为自定义类型是我们自定义的,C++无法提前预知我们要写什么自定义类型,然后给我们的自定义类型进行运算符重载,所以我们想让我们的自定义类型也能用流提取<<
,必须我们手动实现自定义类型的<<
的运算符重载。
我们先来看看第一种写法
//<< 运算符重载
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
ostream& operator<< (ostream&out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
int a = 10;
cout << a << endl;//C++对自定义类型已经实现了 << 的运算符重载
cout << d1;
return 0;
}
编译失败了,我们仔细检查检查,会发现是67行我们写反了!应该写成
d1 << cout; //第一种写法
d1.operator<<(cout); //第二种写法
因为运算符重载时,第一个参数是左操作数,第二个操作数是右操作数(注意,第一个参数是隐藏的this
指针)
写正确后我们运行一下:
没有问题,但是这样写也太变态了,违法我们的使用直觉使用啊,直觉告诉我们,我们应该这样使用:
cout << d1;
那么我们就应该把这个流插入<<
重载定义到函数外面,因为定义在类的内部默认传递的第一个参数就是this
指针,我们永远达不到目的。
于是我们定义在外面:
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, Date& d);//友元,允许我们在类外部使用成员变量。
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
int main()
{
Date d1;
int a = 10;
cout << a << endl;//C++对自定义类型已经实现了 << 的运算符重载
cout << d1;
return 0;
}
还又一个问题我们写<<
运算符重载时为什么要用引用呢?
注意:<<
是左结合性!
返回值用引用了我们就可以实现连续多个自定义类型打印了!
//假如d1 d2 d3 都是Date类型
cout << d1 << d2 << d3 << endl;
四、赋值运算符重载(默认成员函数)
1、引入
我们首先来看一个使用场景,我们想要把一个已经初始化的自定义类型的数据赋值给另一个已经初始化的自定义类型(不是对象初始化时赋值,对象初始化时赋值用的是拷贝构造)该怎么办?
看看下面的代码:
//赋值重载
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _month << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2 = d1;//或者Date d2(d1) 会调用默认生成的拷贝构造,对象初始化时赋值用的是拷贝构造
Date d3;
d3 = d1;//我们没有实现Date类的运算符 = 的赋值重载,所以会调用默认生成的赋值重载
//最后d3里面的数据与d1一样
}
2、特性
1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值(=)运算符重载完成赋值。
实例代码:
如上面的代码
2. 赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
返回*this :要符合连续赋值的含义
#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(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//自己写的 赋值重载
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,2,12);
Date d2;
d2 = d1;
return 0;
}
3. 赋值运算符只能重载成类的成员函数不能重载成全局函数
#include<iostream>
using namespace std;
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 =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。
此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
4. 如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
和拷贝构造函数一样我们继续思考:既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,对于内置类型还需要自己实现吗?
和拷贝构造一样,特性4也是我们写与不写复制重载函数的判断条件!
例如:
// 这里会发现下面的程序会崩溃掉,编译器生成的是浅拷贝,导致我们析构了两次空间,
//这里就需要我们以后讲的深拷贝去解决。
#include<iostream>
using namespace std;
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;
}
到这里我们就把六个默认成员函数中的第四个:复制重载给讲完了。
复制重载其实是运算符重载的一部分!
五、取地址及const取地址操作符重载
1、取地址操作符重载(默认成员函数)
我们还是先看代码再思考:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
cout << &d1 << endl;
}
结果符合我们的预期,你可能觉得没有什么值得思考的点。
但是我们说过:对于自定义类型我们不能对他们像对内置类型那样使用运算符,但是我们对Date类的对象 d1 使用了取地址运算符&
,而我们并没有实现&
的运算符重载,结果我们却可以使用&
,而且结果很对。为什么呢?
这是因为第五个默认成员函数:取地址操作符重载,即我们不写,编译器会帮我们自动生成,它的作用就是帮我们实现自定义类型对象的取地址。
取地址重载的手动实现
通常情况下我们一般自己不写此函数,让编译器自动生成。那假设我们自己实现此函数该怎么办呢?
实现代码如下:
//取地址重载函数
#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&()
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
cout << &d1 << endl;
}
2、const取地址操作符重载(默认成员函数)
我们定义对象时一般都不会加const
,那我们如果给对象加const
会发生什么?
那我们再看一段代码:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
return 0;
}
我们发现无法编译通过
为什么给对象加了const
后我们调用函数就失败了呢?按照加const
报错的常见原因,不难想应该是权限被放大了。
还记得this
指针的类型是什么吗?答案是:* const
类型。这里应该是Date * const
我们用const
修饰的对象取地址后应该是什么类型?答案是:const *
。这里应该是const Date*
两个类型不匹配,const
修饰对象后内容不能被更改,所以我们的this
指针要改变类型,在*
前加一个const
。
但是呢 this
指针是编译器传递的,我们无法加const
,这该怎么办呢?
这里C++编译器又做了特殊化处理我们需要加const
在函数括号后面,才能对this
指针进行修饰
正确代码:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
return 0;
}
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
答案:不可以,传递this
指针时权限会放大 - 非const对象可以调用const成员函数吗?
答案:可以,传递this
指针时权限缩小 - const成员函数内可以调用其它的非const成员函数吗?
答案:不可以,传递this
指针时权限会放大 - 非const成员函数内可以调用其它的
const
成员函数吗?
答案:可以,传递this
指针时权限缩小
const取地址重载手动实现
同理在前面的代码中我们取const
类型的地址时没有对&
进行重载,但我们却可以使用,同样是因为编译器自动帮我们实现了const取地址重载。
注意两个不太一样,两个函数构成函数重载!
取地址操作符重载
Date* operator&() //对非 const 对象取地址
const取地址重载
const Date* operator&()const //对 const 对象取地址
手动实现:
//const取地址重载函数
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=0, int month=0, int day=0)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
const Date* operator&()const //返回值const Date * 是为了与this 指针保持一致
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
const Date d1;
cout << &d1 << endl;
return 0;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!