苏州市规划建设局网站,网站备案接入商,石家庄市住房城乡建设局网站,网站建设工作室是干嘛的前言 本章我们接替前一章继续深入理解类的默认成员函数#xff0c;赋值重载#xff0c;取地址重载#xff0c;及const取地址操作符重载 但是在讲剩下的三个默认成员函数之前#xff0c;我们要先来了解运算符重载#xff0c;因为赋值重载#xff0c;取地址重载#xff0c…前言 本章我们接替前一章继续深入理解类的默认成员函数赋值重载取地址重载及const取地址操作符重载 但是在讲剩下的三个默认成员函数之前我们要先来了解运算符重载因为赋值重载取地址重载及const取地址操作符重载其实是属于运算符重载的一部分。 类与对象三一、运算符重载1、运算符重载的概念2、运算符重载的注意事项二、运算符重载的特例1、前置和后置类型2、流插入 流提取运算符四、赋值运算符重载默认成员函数1、引入2、特性1. 用户没有显式实现时编译器会生成一个默认赋值运算符重载以值的方式逐字节拷贝。2. 赋值运算符重载格式3. 赋值运算符只能重载成类的成员函数不能重载成全局函数4. 如果类中未涉及到资源管理赋值运算符是否实现都可以一旦涉及到资源管理则必须要实现。五、取地址及const取地址操作符重载1、取地址操作符重载默认成员函数取地址重载的手动实现2、const取地址操作符重载默认成员函数const取地址重载手动实现一、运算符重载
1、运算符重载的概念
对于C的内置类型我们有许多运算符可以使用但是这些运算符却无法对自定义类型进行使用我们只能写一个与运算符功能类似的函数让自定义类型去调用。 例如
#includeiostream
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);//d110; //想写成这样这样更直观方便可是编译器不允许啊啊啊d1.AddEqual(10);//d110; //想写成这样这样更直观方便可是编译器不允许啊啊啊return 0;
}C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似。 函数名字为关键字operator后面接需要重载的运算符符号。
函数原型返回值类型 operator操作符(参数列表)
只看定义不太好理解运算符重载我们还是直接先看代码结合代码边分析边理解定义与注意要点。
// 全局的operator
#includeiostream
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无法提前预知我们要写什么自定义类型然后给我们的自定义类型进行运算符重载所以我们想让我们的自定义类型也能用流提取必须我们手动实现自定义类型的的运算符重载。 我们先来看看第一种写法
// 运算符重载
#includeiostream
using namespace std;
class Date
{
public:Date(int year 1900, int month 1, int day 1){_year year;_month month;_day day;}ostream operator (ostreamout){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指针我们永远达不到目的。 于是我们定义在外面
#includeiostream
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、引入
我们首先来看一个使用场景我们想要把一个已经初始化的自定义类型的数据赋值给另一个已经初始化的自定义类型不是对象初始化时赋值对象初始化时赋值用的是拷贝构造该怎么办 看看下面的代码
//赋值重载
#includeiostream
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 要符合连续赋值的含义
#includeiostream
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. 赋值运算符只能重载成类的成员函数不能重载成全局函数
#includeiostream
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也是我们写与不写复制重载函数的判断条件 例如
// 这里会发现下面的程序会崩溃掉,编译器生成的是浅拷贝导致我们析构了两次空间
//这里就需要我们以后讲的深拷贝去解决。
#includeiostream
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、取地址操作符重载默认成员函数
我们还是先看代码再思考
#includeiostream
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 使用了取地址运算符,而我们并没有实现的运算符重载结果我们却可以使用,而且结果很对。为什么呢
这是因为第五个默认成员函数取地址操作符重载即我们不写编译器会帮我们自动生成它的作用就是帮我们实现自定义类型对象的取地址。
取地址重载的手动实现
通常情况下我们一般自己不写此函数让编译器自动生成。那假设我们自己实现此函数该怎么办呢
实现代码如下
//取地址重载函数
#includeiostream
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会发生什么 那我们再看一段代码
#includeiostream
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指针进行修饰
正确代码
#includeiostream
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取地址重载函数
#includeiostream
using namespace std;
class Date
{
public:Date(int year0, int month0, int day0){_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;
}这两个运算符一般不需要重载使用编译器生成的默认取地址的重载即可只有特殊情况才需要重载比如想让别人获取到指定的内容