第14章 重载运算与类型转换
介绍
内置类型运算都有基本的运算符来支持,而我们想要实现类类型的一些运算,就需要自己重载运算符。
基本概念:重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和后面要定义的运算符号共同组成。
和其他函数一样,也有返回类型,参数列表和函数体。
注意:
当一个重载的运算符是成员函数时,this绑定到左侧的对象,成员运算符函数的参数比运算对象的参数少一个.
比如+重载成成员函数,那么它的参数只能有一个,默认类对象this本身是一个参数。
不能重载的运算符 : :: .* . ?:
不应该被重载的运算符: 逻辑运算符,逗号运算符,取地址运算符
逻辑运算符不支持短路求值和求值顺序的属性,逗号和取地址本身有内置含义
我们重载运算符时最好和内置的形成映射最好,也就是类似程度。比如IO运算符,==运算符(若定义也该定义!=)等等
!选择作为成员函数还是非成员函数
成员函数:
=,[ ],( ), ->,
符合赋值+=,-=.. ,
++,--,*(解引用) 改变对象的状态或与给定类型密切相关的运算符
非成员函数:
算数+,-,*,/...
相等性== ,!=
关系 >,<,>=,<=..
位运算符 ^, |, &
方便记忆:可以看一个运算符左右两侧的对象是否可以互换位置,不能互换位置则一般为成员函数,可以互换位置则一般为非成员函数
比如=赋值运算符,右边的值给左边的值赋值,若互换位置则完全改变了意思,所以为成员函数
+加号运算符,左右位置无所谓3+5和 5+3都相同,所以为非成员函数。
1.输入输出运算符
<1.重载输出运算符<<
输出运算符应定义为非成员函数,因为要读写类的非共有数据,所以要定义为友元。
且返回值为std::ostream&, 参数为std::ostream&和类对象的引用。
注意:通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
我们应减少对输出格式的控制。
<2.重载输入运算符>>
输入运算符和输出运算符格式上类似,也是非成员函数,返回输入流引用(流不能拷贝),参数是输入流引用和类对象
和输出运算符不同的是,输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
当流含有错误类型的数据时读取操作可能失败,读取数据到文件末尾或遇到其他流错误也会失败。
不需要逐个检查,只在末尾检查即可,当读取操作发生错误时,输入运算符应该负责从错误中恢复。
一些输入运算符可能需要更多的数据验证工作。
2.算数和关系运算符
我们把算数和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换,一般不需要改变参数状态,所以都是常量引用。
如果定义了算数运算符,则他一般也会定义一个对应的复合赋值运算符,最有效的方式是使用复合赋值来定义算数运算符。
如果类同时定义了算数运算符和相关的赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符
3.相等运算符
依次比较每个成员是否相等。
注意:
如果类定义了operator==,那么类也应该定义operator!=。
相等运算符和不相等运算符中的一个应该把工作委托给另外一个。
如果某个类逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使得用户更加容易的使用标准库算法(部分标准库算法必须要有==支持)。
比较智能指针问题
shared_ptr 和 unique_ptr 都用get( )返回的指针来比较是否指向同一个对象
weak_ptr 要先使用lock( )获取shared_ptr 然后在用get( )来比较地址从而判定是否指向同一个对象。
本来不想敲这个课后题,看着比较简单,最后过意不去,结果发现智能指针比较相等还是有问题的 - -。So 在简单也要敲代码测验啊,真理。
4.关系运算符
定义了相等运算符的类通常也应该定义关系运算符,因为关联容器和一些算法要用到小于运算符。所以operator<会比较有用。
注意:
在定义关系运算符我们应当考虑一些问题,类的成员一般不是仅有一个,我们定义相等运算符时,比较的是全部的成员,但是在关系运算符可能不需要比较这么多。
primer上说:
如果存在唯一一种逻辑可靠的<定义,则应该为这个类定义<运算符,如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。
意味这不要轻易定义<运算符,如果<和==比较的逻辑相同(也就是比较的成员相同)才定义<运算符。
注意:一些情况我们必须定义<运算符,比如类对象需要存在map或set中的时候等等。
5.赋值运算符
类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。
和拷贝赋值运算符及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,不同之处是无需检查自赋值。
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
复合赋值运算符
复合赋值运算符不非得是类的成员,不过我们还是倾向于把包括复合赋值在内的所有赋值运算符都定义在类的内部。
为了与内置类型的复合赋值保持一直,类中的复合赋值运算符也要返回其左侧运算对象的引用。
赋值运算符必须定义成类的成员,复合赋值运算符通常也应该这样做,这两类运算符都应该返回对象的引用。
6.下标运算符
表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[ ]。
下标运算符必须是成员函数。
下标运算符通常以所访问元素的引用作为返回值。可以作为左值或右值
我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会修改返回值。
如果一个类包含下标运算符,则它通常会有两个版本:一个返回普通引用,另一个是类的常量成员并返回常量引用。
7.递增递减运算符
定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符应该被定义为类的成员。
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
区分前置运算符和后置运算符:后置版本提供一个额外的不被使用的int类型的参数,使用后置运算符时,编译器为这个形参提供一个值为0的实参。
这个形参的唯一作用就是区分前置版本和后置版本。
为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非一个引用。对于后置版本来说,在
递增或递减之前首先需要记录对象的状态。
注意:
后置版本里可以调用前置版本。
前置版本在递增之前要判断是否到达末尾,前置版本递减要在递减之后判断是否出界。
如果我们要通过函数调用的方式调用后置版本,则必须为他整型参数传递一个值,尽管我们不使用这个值。
8.成员访问运算符
解引用运算符检查是否在范围内,然后返回所指元素的一个引用,箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果的地址。
箭头运算符必须是类的成员,解引用运算符通常也是类的成员,尽管并非必须如此。
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
简单的实现string类
#ifndef _STRING_H_
#define _STRING_H_
#include <memory>
#include <iostream>
#include <cstring>
class String
{
friend std::ostream& operator<<(std::ostream &os, const String &s);
friend bool operator<(const String &lhs, const String &rhs);
friend bool operator==(const String &lhs, const String &rhs);
public:
String():st(nullptr) { }
String(const char *s);
String(const String &s);
String& operator=(const String &s);
char& operator[](std::size_t n);
const char& operator[](std::size_t n)const;
~String();
private:
char* alloc_n_copy(const char *s);
void free();
static std::allocator<char>alloc;
char *st;
};
#endif
#include "string.h"
char* String::alloc_n_copy(const char *s)
{
st = alloc.allocate(strlen(s)+1);
char *p = st;
int i = 0;
while(s[i] != '\0')
{
alloc.construct(p++, s[i++]);
}
alloc.construct(p, s[i]);
return st;
}
void String::free()
{
char *p = st;
size_t num = strlen(p)+1;
while(*p != '\0')
{
alloc.destroy(p++);
}
alloc.destroy(p);
alloc.deallocate(st, num);
}
String::String(const char *s)
{
st = alloc_n_copy(s);
}
String::String(const String &s)
{
st = alloc_n_copy(s.st);
}
String& String::operator=(const String &s)
{
if(st != s.st)
{
if(st)
{
free();
}
alloc_n_copy(s.st);
}
return *this;
}
String::~String()
{
if(st)
{
free();
}
}
std::ostream& operator<<(std::ostream &os, const String &s)
{
os << s.st;
return os;
}
bool operator<(const String &lhs, const String &rhs)
{
std::cout << __func__ << std::endl;
if(strcmp(lhs.st, rhs.st) < 0)
return true;
else
return false;
}
bool operator==(const String &lhs, const String &rhs)
{
std::cout << __func__ << std::endl;
if(strcmp(lhs.st, rhs.st) == 0)
return true;
else
return false;
}
char& String::operator[](std::size_t n)
{
return st[n];
}
const char& String::operator[](std::size_t n)const
{
return st[n];
}
#include "string.h"
#include <set>
std::allocator<char> String::alloc;
int main()
{
String s1("123");
String s2("456");
String s3("789");
String s4("123");
std::set<String>st;
st.insert(s2);
st.insert(s1);
st.insert(s3);
st.insert(s4);
for(const String &s : st)
{
std::cout << s << std::endl;
}
std::cout << s1[1] << std::endl;
}
9.重点:
函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象,因为这个类同时也能存储状态,所以与普通函数相比更加具有灵活性。
#include <iostream>
struct absInt
{
int operator()(int val)const
{
return val < 0 ? -val : val;
}
};
int main()
{
int val = -42;
absInt t;
std::cout << t(val) << std::endl; //t是一个对象而非函数
}
函数调用运算符必须是成函数。一个类可以员定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
如果类定义了调用运算符,则该类被称为函数对象,因为可以调用这种对象,所以我们说这些对象的行为像函数一样。
函数对象类除了operator()之外也可以包含其他成员,通常函数对象类包含一些数据成员,这些成员用来定制调用运算符中的操作。
例子:
#include <iostream>
#include <string>
class PrintString
{
public:
PrintString(std::ostream &o = std::cout, char t = ' '):
os(o), c(t) { }
void operator()(const std::string &s)const //借用辅助工具来完成函数的操作
{
os << s << c;
}
private: //private成员可以用来保存“辅助”工具
std::ostream &os;
char c;
};
int main()
{
PrintString ps;
std::string s = "abc";
ps(s);
}
函数对象比一般函数灵活就是它可以让
另完成函数所需要的辅助成员成为自己的类成员。和lambda类似 ^ _^
函数对象常常作为泛型算法的实参。
14.37 编写一个类令其检查两个值是否相等,令其替换某序列中给定值的所有实例
#include <iostream>
#include <vector>
#include <algorithm>
class cmp
{
public:
cmp(int i = 0):
t(i) { }
bool operator()(int a)
{
return a == t;
}
private:
int t;
};
int main()
{
std::vector<int>ivec = {1,2,1,2,1,2,1,2,1,2};
cmp cp(2);
//最好不要在for_each里面使用类成员函数!
std::for_each(ivec.begin(), ivec.end(), [&](int &i){ if(cp(i)) i = 100; });
for(const int i : ivec)
std::cout << i << std::endl;
}
<1.lambda是函数对象
回忆一下泛型算法里面解释的lambda:当定义一个lambda时,编译器生成一个与lambda对应的新的类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和
该类型的一个对象
其实当我们编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符。
也就是
[](const string &lhs, const string &rhs)
{ return lhs.size() < rhs.size(); }
编译器翻译成也就是等价于
class shrotstring
{
public:
bool operator()(const string &lhs, const string &rhs)const
{ return lhs.size() < rhs.size(); }
};
表示lambda及相应捕获行为的类当一个lambda通过 引用捕获变量时,将由程序负责确保lambda执行时引用所引用的对象的确存在,因此,编译器可以直接使用该引用而无需
在lambda产生的类中将其存储为数据成员
当一个lambad通过值将变量拷贝到lambda时,lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用
捕获的变量来初始化数据成员。
lambda产生的类不含默认构造函数,赋值运算符及默认析构函数:它是否含有默认的拷贝/移动构造函数则通常视捕获的数据成员类型而定。
<2.标准库定义的函数对象
头文件 #include <functional>
#include <functional>
#include <iostream>
int main()
{
//算数
std::cout << "算数" << std::endl;
int a = 10, b = 20;
int ret;
std::plus<int>add; //加法
ret = add(a, b);
std::cout << ret << std::endl;
std::minus<int>min; //减法
ret = min(a, b);
std::cout << ret << std::endl;
std::multiplies<int>mp; //乘法
ret = mp(a, b);
std::cout << ret << std::endl;
std::divides<int>divi; //除法
ret = divi(a, b);
std::cout << ret << std::endl;
std::modulus<int>mod; //求余
ret = mod(a, b);
std::cout << ret << std::endl;
std::negate<int>neg; //相反数
ret = neg(a);
std::cout << ret << std::endl;
//关系
std::cout << "关系" << std::endl;
int a2 = 10, b2 = 20;
int ret2;
std::equal_to<int>et; //是否相等
ret2 = et(a2, b2);
std::cout << ret2 << std::endl;
std::not_equal_to<int>net; //不等
ret2 = net(a2, b2);
std::cout << ret2 << std::endl;
std::greater<int>gt; //大于
ret2 = gt(a2, b2);
std::cout << ret2 << std::endl;
std::greater_equal<int>gte; //大于等于
ret2 = gte(a2, b2);
std::cout << ret2 << std::endl;
std::less<int>ls; //小于
ret2 = ls(a2, b2);
std::cout << ret2 << std::endl;
std::less_equal<int>lel; //小于等于
ret2 = lel(a2, b2);
std::cout << ret2 << std::endl;
//逻辑
int a3 = 10, b3 = 20;
int ret3;
std::cout << "逻辑" << std::endl;
std::logical_and<int>la; //and
ret3 = la(a3, b3);
std::cout << ret3 << std::endl;
std::logical_or<int>lo; //or
ret3 = lo(a3, b3);
std::cout << ret3 << std::endl;
std::logical_not<int>ln; //not
ret3 = ln(a3);
std::cout << ret3 << std::endl;
}
我们也可以在算法中使用标准库函数对象
sort(svec.begin(), svec.end(), std::greater<std::string>());
而且标准库规定其函数对象对于指针同样适用。排序的同样是string.
sort(svec.begin(), svec.end(), std::less<std::string *>());
510页课后几个题我觉得很好,贴上来,顺便复习了泛型算法和bind等。
<1.统计大于10的值有多少个
#include <vector>
#include <iostream>
#include <functional>
#include <algorithm>
int main()
{
std::vector<int>ivec = {1,22,3,44,5,66,7,88,9,100};
sort(ivec.begin(), ivec.end(), std::greater<int>()); //排序
for(int i : ivec)
std::cout << i << " ";
std::cout << std::endl;
//auto p = find_if(ivec.begin(), ivec.end(), [](int &a) { return a < 10; }); //查找第一个满足条件的下标
//std::cout << "result:" << p - ivec.begin() << std::endl;
int i = 10;
//auto num = std::count_if(ivec.begin(), ivec.end(), bind2nd(std::greater<int>(), i)); //通过bind1st和bind2nd处理
//auto num = std::count_if(ivec.begin(), ivec.end(), bind1st(std::greater<int>(), i));
//auto num = std::count_if(ivec.begin(), ivec.end(), bind(std::greater<int>(), std::placeholders::_1, i)); //通过bind来处理
//不管绑定第一个参数还是第二个参数都是绑定一个,所以是_1
auto num = std::count_if(ivec.begin(), ivec.end(), bind(std::greater<int>(), i, std::placeholders::_1));
std::cout << num << std::endl;
}
注意:
bind1st是绑定第一个参数
bind2nd是绑定第二个参数
bind1st和bind2nd函数参数必须是一个函数对象,如果是普通函数要用ptr_fun转换为函子.
bind是c++11新加入,可以绑定1-9个参数
为什么bind和lambda会在《c++ primer》泛型算法章节出现? 最好的答案就是配合泛型算法,这几个例子很好的表现出了。
因为有些谓词函数规定是一个参数,而我们有时需要传递两个或多个参数这时就可以使用bind或lambda。
<2.找到第一个不等于pooh的字符串
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
#include <string>
int main()
{
std::string s = "pooh";
std::vector<std::string>svec = {"pooh","pooh","pooh","p","pooh"};
//不管绑定第一个还是第二个参数,s的位置都在那
//auto p = std::find_if(svec.begin(), svec.end(), std::bind1st(s,std::not_equal_to<std::string>()));//error s在后面位置,1st只是表名绑定的是第一个参数
auto p = std::find_if(svec.begin(), svec.end(), std::bind2nd(std::not_equal_to<std::string>(), s)); //绑定了第二个参数
std::cout << *p << std::endl;
}
<3.将所有值乘2
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
using namespace std::placeholders;
int main()
{
std::vector<int>ivec = {1,2,3,4,5,6,7,8,9,0};
std::vector<int>ivec2(ivec.size());
std::transform(ivec.begin(), ivec.end(), ivec2.begin(), bind(std::multiplies<int>(), _1, 2)); //transform 谓词函数处理后放到新的位置
for(const int i : ivec2)
std::cout << i << std::endl;
}
<4.使用标准库函数对象判定一个给定的int值是否能被int容器中的所有元素整除
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int>ivec = {2,4,6,8};
std::vector<int>ivec2(ivec.size());
std::transform(ivec.begin(), ivec.end(), ivec2.begin(), bind(std::modulus<int>(), std::placeholders::_1, 2));
if(std::count(ivec2.begin(), ivec2.end(), 0) == ivec2.size())
std::cout << "yes" << std::endl;
else
std::cout << "no" << std::endl;
}
<3.可调用对象与function
c++中有几种可调用的对象:函数,函数指针,lambda表达式,bind创建的对象,以及函数对象(重载了函数调用运算符的类)。
和其他对象一样,可调用的对象也有类型。
然而两个不同的可调用对象确有可能享用同一种调用形式。
int add(int a, int b) { return a+b; }
auto t = [](int a, int b) { return a+b; }
class A
{
int operator()(int a, int b)
{
return a+b;
}
}
....
类型都是 int(int, int)
但实际操作比如我们想vector<int(*)(int, int)>来保存他们是不行的,只能保存第一个,因为他们毕竟是不同的对象!
标准库function类型
我们可以使用一个名为function的新的标准库类型来解决上面的问题。
头文件 #include <functional>
看个例子:
#include <iostream>
#include <functional>
#include <vector>
#include <map>
#include <string>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
class multip
{
public:
int operator()(int a, int b)
{
return a * b;
}
};
int fun(int a, int b, int c)
{
return a + b + c;
}
int main()
{
auto div = [](int a, int b) { return a / b; };
int (*p)(int, int) = sub;
//function<T>f;
//function<T>f(nullptr);
//function<T>f(obj);
//f 判断f里是否含有一个条件
//f(args) 调用
std::function<int(int, int)>func1 = add; //函数
std::function<int(int, int)>func2 = p; //函数指针
std::function<int(int, int)>func3 = multip();//函数对象
std::function<int(int, int)>func4 = div; //lambda
std::cout << div(1,2) << std::endl;
std::map<std::string, std::function<int(int, int)>>cal;
cal.insert({"+", func1});
cal.insert({"-", func2});
cal.insert({"*", func3});
cal.insert({"/", func4});
std::string s;
std::cin >> s;
std::cout << cal[s](10,5) << std::endl;
std::function<int(int, int, int)>func6 = fun;
}
从例子中可以很好的看出function的作用。
注意:当我们有函数重载对象时,不能直接将相同的函数名字放入function,必须通过函数指针或者lambda来消除二义性。
10.重载,类型转换与运算符
我们能定义类类型之间的转换,转换构造函数和类型转换运算符共同定义了类类型转换。
<1.类型转换运算符
是类的一种特殊的成员函数,负责将一个类类型的值转换成其他类型。
operator type( )const;
type表示某种类型。但是该类型必须能作为返回类型。类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成类的成员函数,类型转换通常不应该改变待
转换的对象。
一个类型转换函数必须是类的成员函数,它不能声明返回类型,也没有形参,类型转换函数通常为const
注意:应该避免过度的使用类型转换函数。
类型转换函数可能产生意外结果。
类通常很少定义类型转换运算符,但是定义像bool类型转换还是比较常见
c++11 显示类型转换运算符
explicit operator type( ) const { };
static_cast<type>(name);
当类型转换运算符是显式的我们才能只能类型转换。不过必须通过显式的强制类型转换才行。
但是存在一个例外:既当如果表达式被用作 条件,则编译器会将显示的类型转换自动应用于它。
包括while, if, do, for语句头条件表达式,(!, ||, &&)的运算对象, (? :)条件表达式
注意: 流对象转换bool也是因为标准库定义了流向bool显式类型转化
向bool的类型转换通常用在条件部分,因此operator bool 一般定义为explicit 的。
<2.避免有二义性的类型转换。
如果一个类中包含一个或多个类型转换,则必须确保在类类型和目标类型转换之间只存在唯一一种转换方式。否则我们的代码可能会存在二义性。
通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算数类型的转换。
我们无法用强制类型转换来解决二义性,因为强制类型转换也面临着二义性。
最好不要创建两个转换源都是算数类型的转换。
operator int( )const
operator double( )const
当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。
注意:
不要让两个类执行相同的类型转换,比如A转换B的同时B也转换为A。
避免转换目标是内置算数类型的类型转换。
如果我们对一个类既提供了转换目标是算数类型的类型转换,也提供了重载的运算符,则会遇到重载运算符与内置运算符二义性的问题。