🎉作者简介:👓 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢 c + + , g o , p y t h o n , 目前熟悉 c + + , g o 语言,数据库,网络编程,了解分布式等相关内容 \textcolor{orange}{博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容} 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容
📃 个人主页: \textcolor{gray}{个人主页:} 个人主页: 小呆鸟_coding
🔎 支持 : \textcolor{gray}{支持:} 支持: 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦 \textcolor{green}{如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦} 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦👍 就是给予我最大的支持! \textcolor{green}{就是给予我最大的支持!} 就是给予我最大的支持!🎁
💛本文摘要💛
本专栏主要是对c++ primer这本圣经的总结,以及每章的相关笔记。目前正在复习这本书。同时希望能够帮助大家一起,学完这本书。 本文主要讲解第14章 重载运算符
c++ primer 第五版 系列文章:可面试可复习
第2章 变量和基本类型
第3章 字符串、向量和数组
第4章 表达式
第5章 语句
第6章 函数
第8章 IO库
第9章 顺序容器
第10章 泛型算法
第11章 关联容器
第12章 动态内存
第13章 拷贝控制
第 14章 重载运算符
第15章 面向对象程序设计
第 16章 模板与泛型编程
名字由关键字 operator 和要定义的运算符号组成
。返回类型
、参数列表
和函数体
。(左侧)运算对象绑定到隐式的 this 指针上
,因此定义成员运算符函数时的参数数量比运算符的运算对象少一个。某个类的成员或至少拥有一个类类型的运算对象。
除了重载的函数调用运算符 operator() 之外,其他重载运算符不能含有默认实参。
错误的做法
'错误:不能为int重定义内置的运算符'
int operator+(int, int)
重载的俩种方法
直接调用
:如 data1+data2;data1+=data2。+ 是非成员函数,+= 是类的成员函数,两种都可以直接使用。函数调用
:如 operator+(data1,data2);data1.operator+=(data2)。重载运算符的规则
如果定义了 ==,一般也应该定义 !=
。如果定义了算术运算符或位运算符,也应该有对应的复合赋值运算符,如有 +,也应该有 +=。
当运算符定义为成员函数,它的左侧运算对象必须是所属类的一个对象。
运算符应该定义成成员还是非成员的规则
赋值、下标([ ])、调用(( ))、成员访问箭头(->)
运算符都必须是成员。递增、递减和解引用
等运算符应该是成员。IO 运算符
应该是非成员,但是应该声明为类的友元。具有对称性的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符等,应该是非成员。
string s = "world";
string t = s + '!'; //正确,等价于s.operator("!"),可以把一个cosnt char* 加到一个string对象中
string u = "hi" + s; //如果+是string成员则错误,如果是非成员则正确
'+是成员函数'
'hi' + s等价于hi.operator+(s),而hi的类型是const char* 这是内置类型,没有成员函数
'+是非成员函数'
"hi" + s等价于operator+("hi", s)
第一个形参是一个非常量 ostream 对象的引用(因为 IO 类型不能拷贝,所以必须是引用,因为要通过向流写入来输出,这会改变流的状态,所以必须是非常量),第二个形参一般是常量引用(为了避免复制实参,所以应为引用)。重载的 << 应该返回它的 ostream 形参。
例子
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << item.price << item.revenue;
return os;
}
非成员函数
,IO运算符通常需要读写类的非公有数据成员,所以应该声明为类的友元
。第一个形参是一个非常量 istream 对象的引用,第二个形参是要读入的非常量对象的引用
。返回 istream 对象的引用
。例子
istream& operator>>(istream& is,Sales_data& item)
{
double price;
is >> item.bookNp >> item.units_sold >> price;
if (is)
item.revenue = item.units_sold * price;
else
item = Sales_data(); //错误,对象被赋予默认状态
return is;
}
输入错误
处理办法
输入运算符必须检查是否输入成功,并处理输入失败的情况,而输出运算符不需要。
标示错误
failbit
。而eofbit
表示文件耗尽,badbit
表示流被破坏算术和关系运算符通常定义为非成员函数以允许对左侧或右侧的运算对象进行转换
。(如果定义成成员函数,就会出现限制,符号的左边就必须是this指针指向的对象0)形参都应该是常量引用
。Sales_data operator+ (const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
sum += rhs; //把rhs加到sum中
return sum;
}
注意:
一般都是先定义复合赋值运算符,而后使用复合赋值来实现算术运算符(复合赋值运算符一般为成员,而算术为非成员)。
一般将算术运算得到的值存放在局部变量中,操作完后返回该变量的副本
。使用规则
定义了 ==,也应该定义 !=。一般通过 == 来实现 !=
。通常相等运算符应该具有传递性。
operator==
,这样做可以使用户更容易使用标准库算法来处理这个类。bool operator== (const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() && lhs.revenue == rhs.revenue
}
bool operator != (const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs)
}
定义了 == 的类,经常也会包含关系运算符,尤其是 <,因为关联容器和一些算法都要用到 <
。使用规则
如果同时定义了 == 和 !=,则关系运算符应该与其保持一致,比如如果两个对象是 != 的,那么一个对象应该 < 另一个
。类的成员函数
。左侧运算对象的引用
。类中一般已经定义了拷贝赋值和移动赋值运算符。如果需要时也可以继续重载赋值运算符以使用别的类型作为右侧运算对象
。复合赋值运算符
一般也应该是成员
。其左侧运算对象绑定到隐式的 this 指针
。复合赋值运算符也返回左侧运算对象的引用
。'左侧运算对象绑定到隐式的this指针'
' *this += rhs '
Sales_data & Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
下标运算符必须是成员函数
通常以所访问元素的引用作为返回值,这样下标可以出现在赋值运算符的任意一端。
非常量版本
:返回普通引用。常量版本
:是类的常量成员并返回常量引用。常量版本取得的元素不能放在赋值运算符的左侧。定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常被定义成类的成员
前置版本返回递增或递减后的引用,后置版本返回修改前的副本(也就是拷贝)
。
前置和后置的区分
后置版本接受一个额外的不被使用的 int 类型的形参,当使用后置版本时,编译器为这个形参提供一个值为 0 的实参
。这个形参的唯一作用就是区分前置和后置。因为不会用到,所以该形参无需命名。
Student& operator++();//前置版本
Student& operator++(int);//后置版本
箭头运算符必须是类的成员,箭头运算符一般通过调用解引用运算符来实现。解引用运算符通常也是类的成员,但不必须。
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象
。对箭头运算符返回值的限定
难点
例子
如果类重载了函数调用运算符,就可以像使用函数一样使用该类的对象 (被称为函数对象)。
函数调用运算符必须是成员函数,一个类可以定义多个版本的调用运算符。不同版本的参数应有所区别。
理解
struct AbsInt{
int operator()(int val) const{ //该函数调用运算符返回一个整数的绝对值
return val<0 ? -val : val;
}
};
'应用'
int i = 42
AbsInt absObj; //定义了一个对象
int ui = absObj(i); //使用重载的函数调用运算符
含有状态的函数对象
struct PrintString{
public:
PrintString(ostream &_os = cout, char _sep = ' ') : os(os), sep(sep) {}
void operator()(const string &s) const { os<<s<<sep; }
};
'应用'
PrintString printString; //采用默认实参,输出到 cout 中,以空格为间隔符
'等价于cout << ss << '';'
printString(ss); //打印 string 类对象 ss。
'等价于cerr << s << '\n';'
PrintString errors(cerr, '\n');
errors(s);
函数对象常作为泛型算法的实参
for-each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
会将 lambda 翻译成一个未命名类的未命名对象,在 lambda 表达式产生的类中含有一个重载的函数调用运算符。
'lambda表达式'
stable_sort(word.begin(), word.end(),[](const string &a,const string &b){ return a.size() < b.size();})
'上面的lambda表达式类似这个类的未命名对象'
class ShorterString{
public:
bool operator()(const string &s1, const string &s2) const { return s1.size() < s2.size(); }
}
stable_sort(word.begin(), word.end(), ShortString);
lambda及相应捕获行为的类
当 lambda 通过引用捕获变量时,由程序确保 lambda 执行时所引用的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员
当 lambda 通过值捕获变量时,捕获的变量被拷贝到 lambda 中,此时产生的类中会建立对应的数据成员并创建构造函数来初始化数据成员。
'lambda表达式'
auto wc = find_if(word.begin(), word.end(),[sz](const string &a){ return a.size() >= sz})
'用类表示lambda捕获行为'
struct SizeComp{
SizeComp(size_t n):sz(n){} //将捕获的变量初始化
//该调用运算符返回类型、形参、函数体都与lambda一致
bool operator()(const string &s) const { return s.size() < sz; }
privatr:
size_t sz; //必须将捕获的变量建立对应的数据成员,同时创建构造函数
}
auto wc = find_if((word.begin(), word.end(), SizeComp(sz));
一组表示算数运算符、关系运算符和逻辑运算符的模板类,每个类分别定义了一个执行命名操作的调用运算符。
functional头文件中
plus<int> intAdd; //实例化了一个可执行 int 加法的函数对象
int sum = intAdd(10, 20); //调用 intAdd 来执行 int 加法
标准库函数对象
'算术' '关系' '逻辑'
plus<Type> '+' equal_to<Type> '=' logical_and<Type> '与'
minus<Type> '-' not_equal_to<Type> '!=' logical_or<Type> '或'
multiplies<Type> '*' greater<Type> '>' logical_not<Type> '非'
divides<Type> '/' greater_equal<Type> '>='
modulus<Type> '%' less<Type> '<'
negate<Type> '取反' less_equal<Type> '<='
在算法中使用标准库函数对象
表示运算符的函数对象类常用来替换算法中的默认运算符。
//传入临时对象用于执行俩个string对象的>比较运算
sort(svec.begin(), vec.end(), greater<string>());//使用 greater 对 svec 进行降序排列
因为关联容器使用 less 来对元素排序,如果将指针作为 set 或 map 的关键字,元素将自动按地址进行排序。
注意函数对象其实是一个函数对象类。
vectro<string *>nameTable;
//错误:nameTable中的指针彼此没有关系,所以<将产生未定义行为
sort(nameTable.begin(), nameTable.end(),[](string *a, string *b){return a < b;});
//正确:标准库规定指针的less是定义的
sort(nameTable.begin(), nameTable.end(),less<string *>());
函数、函数指针、lambda表达式、bind创建的对象、重载了调用运算符的类
。调用形式指明调用返回的类型以及传递给调用的实参类型
int(int, int); //是一个函数类型,它接受两个 int,返回一个 int
不同类型的可调用函数对象可能具有相同的调用形式
int add(int i, int j) { return i + j; } //普通函数
auto mod = [](int i, int j) { return i % j }; //lambda 产生一个未命名的函数对象类,mod 是这个类的一个实例
struct divide{ //一个函数对象类
int operator()(int i, int j) { return i / j; }
};
标准库function类型
'构建从运算符到函数指针的映射关系,其中函数接收俩个int、返回一个int'
map<string, int(*)(int, int)> b;
'add是一个指向正确类型函数的指针,{'+',add}是一个pair'
'add本身就是函数当传到int(*)(int, int)会被解析成指针'
b.insert({"+", add}); //正确
b.insert({"%", mod}); //错误:lambda表达式不是指针
functional 头文件中
。function 是一个模板
,创建具体的 function 类型时要指明具体的调用形式。'function操作'
function<retType(args)> f; //f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与类型T相同。
function<retType(args)> f(nullptr); //显式地构造一个空function
function<retType(args)> f(obj) //在f中存储可调用对象obj的副本
f //将f作为条件:当f含有一个可调用对象时为真;否则为假。
f(args) //通过 f 调用 f 中的对象
'function中定义的类型'
result_type //该 funciton 类型的可调用对象返回的类型 retType
argument_type //retType(args) 中的参数类型
first_argument_type //retType(args) 中第一个参数的类型
second_argument_type //retType(args) 中第二个参数的类型
function
int
,返回一个int
的可调用对象。因此可以用这个新生命的类型表示任意一种桌面计算机用到的类型map<string, int(*)(int, int)> binops;//这种方式只能存储函数和函数指针,不能存储函数对象类和 lambda 表达式
binops.insert("+",add);//正确
map<string, function<int(int, int)>> binops = { //可以存储相同调用形式的各种可调用对象
{"-", std::minus<int>()}, //标准库函数对象
{"/", divide()}, //用户定义的函数对象
{"%", mod} //命名了的 lambda 对象
}
重载的函数与function
不能直接将重载函数的名字存入 function 类型的对象中,但是可以存储指向确定重载版本的函数指针。
int add(int i, int j){return i + j;}
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> b;
b.insert({"+", add}); //错误:哪一个add
'1. 存储函数指针消除二义性'
int(*fp)(int, int) = add;
b.insert({"+", fp});
'2. 使用lambda消除二义性'
b.insert({"+", [](int a, int b){return add(a, b);}});//因为lambda内部传入俩个int,因此调用只能匹配接收俩个int的add版本
转换构造函数和类型转换运算符共同定义了类类型转换,这是用户定义的类型转换。
构造函数会实现从其他类型向类类型的转换,类型转换运算符实现从类类型向其他类型的转换。
一个类类型的值转换成其他类型
。包括引用、指针,但是不可以转换为数组或者函数类型
operator type()cosnt;
注意
类型转换函数必须是类的成员函数,不能声明返回类型,形参列表也必须为空,且函数一般是 const 的。虽然不指定返回类型,但是函数会返回一个对应类型的值。
class SmallInt {
public:
SmallInt(int i=0): val(i) {}
operator int() const { return val}; //类型转换函数
private:
int val;
}
'类型转换运算符不需要显式调用,在执行运算时会隐式的执行。'
SmallInt si;
si = 4; //将 4 隐式地转换为 SmallInt,然后调用 SmallInt::operator
si + 3; //将 si 隐式地转换为 int,然后执行整数的加法
'也可以使用类型转换运算符将一个SmallInt对象转换为int''然后再将所得的int转换成任何其他算术类型'
SmallInt si = 3.24; //调用SmallInt(int)构造函数,将si转换为int
si + 3.14 //内置类型转换将int转换为double
几种错误写法
class B;
operator int(&B); //错误:不是成员函数
class B{
public:
int operator int() cosnt; //错误:指定了返回类型
operator int(int = 0) const; //错误:参数列表不为空
operator int*() const (return 6); //错误:6不是一个指针
};
但是向 bool 的类型转换比较常见。
//当istream含有向bool的类中转换时
int i = 42;
cin << i; //如果向bool的类型转换不是显示的,则编译器看来是合法的
//提升后的bool值(1或0)最终被左移42个位置
显示的类型转换运算符
用关键字 explicit 来将运算符指定为显式的,调用时需使用 static_cast 来强制显式转换。
注意
则显式的类型转换会被隐式地执行。如用在 if 语句的条件部分。
class SmallInt {
public:
explicit operator int() const { return val; }
// 类的其他部分省略。
}
SmallInt si = 4; //正确:构造函数不是显式的,可以隐式将int类型转换为类类型
si + 3; //错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3; //正确:显式地请求类型转换(强制转换)
转换为bool类型
因此 operator bool() 一般定义成 explicit 的
。while(std::cin >> value)
它负责将数据读入到value
并返回cin.cin
被istream operator bool
类型转换函数隐式的执行了转换,转换为bool
类型
要确保类类型和目标类型之间只存在唯一的转换方式。
两个类提供相同的类型转换。A 类定义了一个接受 B 类对象的转换构造函数,同时 B 类定义了一个转换目标是 A 类的类型转换运算符。
通常情况下,不要为类定义相同的类型转换,也不要在类中定义俩个及俩个以上转换源或转换目标是算术类型的转换
转换目标为内置类型的多重类型转换
重载函数与转换构造函数
重载函数与用户定义的类型转换