本章内容一览:
1、基本概念 和 限制条件
- 只有重载的函数调用运算符
operator()
能有默认实参,其他重载运算符不能有默认实参。 - 一个重载的运算符,至少含有一个类类型的参数。
- 可被重载的运算符:
- 一般不重载
&&
||
,
&
这几个运算符 - 返回值类型一般与内置版本兼容:
-
- 逻辑和关系运算符返回
bool
- 逻辑和关系运算符返回
-
- 算术运算符返回类类型的值
-
- 赋值运算符 和 复合运算符 返回左侧运算对象的引用
- 若一个运算符是成员函数,则他的左侧运算对象绑定到隐式的
this
指针上。这就是为什么后面重载<<
>>
运算符不能是成员函数的原因。
2、定义为 成员 还是 非成员?
什么是对称性呢?举个例子:
计算一个int
和double
的和,他们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法就是对称的。如果想计算 含有类对象混合的表达式,则运算符必须定义成非成员函数。
当把运算符定义为成员函数时,他的左侧运算对象必须是运算符所属类的对象。
练习题:
说明g:==
具有对称性,所以定义为非成员
3、重载输入运算符
3.1 注意处理输入失败:
X& operator>>(istream& is, X& x)
{
is >> 输入巴拉巴拉.....
if (!is) 这个检查是一次性检查,没有逐个检查每个读取操作
x = X(); 输入失败,赋予对象默认状态
else
某些成员需要由刚刚出入的数据计算出来,则在确保输入正常的情况下进行
return is;
}
发生输入错误可能是如下原因:
- 输入的数据类型与代码要求的不符
- 读取到达文件末尾,或遇到其他错误
3.2 标示错误
4、算术运算符 和 复合赋值运算符
因为使用复合赋值运算符来实现算数运算符是很方便的。如下所示:
Sale_data operator+(const Sale_data& s1, const Sale_data& s2)
{
Sale_data sum = s1;
sum += s2; 直接使用复合赋值运算符就行了,而不用逐成员相加
return sum;
}
5、相等运算符
- 定义了
==
也应该定义!=
!=
==
其中一个定义好之后,直接用他去实现另一个
bool operator==(const Sale_data& s1, const Sale_data& s2)
{
return s1.isbn() == s2.isbn() &&
s1.units_sold == s2.units_sold &&
s1.reevnue == s2.revenue;
}
bool operator!=(const Sale_data& s1, const Sale_data& s2)
{
return !(s1 == s2); 就像这样,不要傻不拉几再逐个比一遍了
}
6、关系运算符
也就是说要有严密的逻辑自洽。当两个对象经!=
运算返回true
,那么必有一个是<
另一个的。
7、赋值运算符
之前学的拷贝赋值运算符只是赋值运算符重载中的一种。我们还可以用很多其他类型给目标赋值。
如下面的例子:
class StrVec
{
public:
StrVec& operator=(initializer_list<string> l)
{
auto data = alloc_n_copy(l.begin(), l.end());
free(); 不管什么样的赋值运算符都要先销毁掉原来的内存空间
elements = data.first;
first_free = cap = data.second;
return *this;
}
};
int main()
{
StrVec s;
s = { "hello", "world" };
return 0;
}
上例实现了自定义类型StrVec
的列表赋值。
8、下标运算符
- 下标运算符必须是成员函数
- 类的下标运算符通常定义一对,一个返回普通引用,一个返回常量引用。
如下例所示:
class StrVec
{
public:
string& operator[](size_t n) { return elements[n]; }
const string& operator[](size_t n) const { return elements[n]; } const的对象使用下标
运算符,就用这个版本
};
9、递增和递减运算符
- 通常定义为成员函数。
- 要同时定义前置 和 后置 版本
9.1 前置版本
class X
{
public:
X& operator++() { x++; return *this; }
X& operator--() { x--; return *this; }
private:
int x = 0;
};
注意:返回的是对象的引用。内置版本也是这样的。
9.2 后置版本
虽说是后置版本,但是名字完全一样,所以为了重载他们勉强加一个int
类型的参数进去。这个int
的作用只有区分重载版本:
class X
{
public:
X operator++(int); 这里只是声明
X operator--(int); 函数的定义和前置有所区别
private:
int x = 0;
};
注意:返回的是对象的原值(递增减之前的值)。内置版本也是这样的。
在递增减之前要首先记录对象的状态。
X operator++(int) 不需要用到int形参,无需为其命名
{
X x = *this; 记录原始状态,便于待会返回
++*this; 递增,直接用已实现的前置递增更方便,当然,是对于更复杂的类来说的,这个类太简单了
return x; 返回对象原始状态的副本(拷贝)
}
X operator--(int) 与后置递增同理
{
X x = *this;
--*this;
return x;
}
10、成员访问运算符*
和->
箭头运算符必须是类的成员,解引用运算符通常也是类的成员,但也可以不是。
重载 ->
- 箭头运算符的作用只能是获取成员。
- 对于形如
point->member
的表达式,point
只能是指向类对象的指针,或者是一个重载了operator->
的类的对象,绝无其它。
11、函数调用运算符
必须是成员函数。
11.1 函数对象
函数对象其实是类,这个类定义了函数调用运算符,当使用这个类的对象调用重载的函数调用运算符时,其形式和调用函数一毛一样,所以叫做函数对象。下面我们举个例子:
class X
{
public:
int operator()(int value)
{
return value > 0 ? value : -value;
}
};
int main()
{
X x;
int a = x(-42); a 的值是42
}
你看,a = x(-42)
,这多像函数调用,可以称X
为函数对象。
函数对象通常作为泛型算法的实参。
就类似lambda
表达式那个作用,还是举个例子,让我们把上面的类改一改。
class X
{
public:
X(ostream& o = cout, char c = ' ') : os(o), sep(c) { } 构造函数
void operator()(const int& x)
{
os << (x > 0 ? x : -x) << sep;
}
private:
ostream& os;
char sep;
};
int mina()
{
X x;
vector<int> v{ -1, 3, 5, -9, -54, 9, -1, -3, -2 };
for_each( v.begin(), v.end(), X(cout, '\n') );
}
修改后的类X
有两个成员;分别表示 输出流 和 间隔符。
他的函数调用运算符,能够向给定的输出流 并 以给定间隔符 输出传入的int
的绝对值。
在main
函数中我们使用for_each
对每个v
的元素调用X
的函数调用运算符,就实现了像标准输出流输出v
中所有元素的绝对值,并以换行符为间隔。
这就是上面说的:函数对象通常作为泛型算法的实参
11.2 深入剖析lambda
表达式
当我们编写一个lambda
表达式,编译器将该表达式翻译为一个未命名类的未命名对象,这个对象是一个函数调用运算符。
哇,原来如此,惊为天人,原来是这样。我们举个例子:
stable_sort(words.begin(), words.end(),
[](const string& a, const string& s2) { return a.size() < b.size(); }
上面实现了根据长度将string
排序。其lambda
表达式的行为类似于下面的函数对象:
class shorter
{
bool operator()(const string& s1, const string& s2)
{
return s1.size() < s2.size();
}
};
调用这个函数对象:
stable_sort( words.begin(), words.end(), shorter() );
通过这个例子可一目了然地看到,上面的 lambda
表达式和下面的函数对象是等价的。
那么 捕获 又是怎么一回事呢?
分两种情况:
- 当
lambda
通过引用捕获变量时,由程序确保引用的对象确实存在,故,编译器直接使用该引用, 不用在lambda
产生的类中将其存储为数据成员。 - 当
lambda
通过值捕获方式捕获变量时,变量是被拷贝到lambda
中的,故,lambda
产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,构造函数使用捕获到的值初始化数据成员。
焯!连起来的,知识连起来了!太妙了!!!还是举个例子:
auto first_iter = find_if(words.begin(), words.end(),
[size](const string& a) { return a.size() >= size; }
上面能够获得指向序列中第一个长度>= size
的迭代器。
该lambda
产生的类形如:
class size_cpomare
{
public:
size_compare(size_t n) : size(n) { } 这里得构造函数使用捕获到的值初始化 size 成员
bool operator(const string& s)
{
return s.size() >= size;
}
private:
size_t size; 这里有一个名为 size 的成员,准们保存捕获到的 size 的值
};
调用这个函数对象:
auto first_iter = find_if( words.begin(), words.end(), size_compare(size) );
lambda
表达式产生的类不含默认构造函数、赋值运算符 及 默认析构函数;是否含有默认的拷贝 / 移动构造函数 则通常要视捕获的数据成员类型而定。
这里有个好问题:
11.3 标准库定义的函数对象
下面是标准库定义的表示算术、关系 和 逻辑 运算符的类,每个类都定义了调用运算符,可以执行相应的运算。
标准库定义的这些类都非常好,可以进行我们做不到的操作。例如通过比较指针的地址来排序指针序列。
这些指针可以是毫无关系的指针,我们直接比较两个指针会产生未定义的行为,使用标准库的less
则不会。
vector<string*> v;
sort(v.begin(), v.end(), [](string* a, string* b) { return a < b; }
错误,v里面的指针彼此之间没有关系,<会产生未定义行为。
sort( v.begin(), v.end(), less<string*>() ); 正确,标准库定义的 less 是良好的
注意:
关联容器默认使用less<key_type>
对元素排序,因此可以定义一个指针的set
或在map
中使用指针作为关键字而无须声明less
。
精彩练习:
答案:这个办法就很妙!隐式的把取模的结果转换成bool
类型,正好可以用于判断是否能整除。
bool divided_by_all(vector<int>& vec, int dividend)
{
return count_if( vec.begin(), vec.end(), bind2nd(modulus<int>(), dividend) ) == 0;
}
11.4 可调用对象 和 function
类型
目前为止见过的可调用对象有:
- 函数
- 函数指针
lambda
表达式bind
创建的对象- 重载了函数调用运算符的类
- 标准库定义的函数对象(其实这个也算上一条里的)
和其它对象一样,可调用对象也有类型。例如,每个lambda
表达式都有自己唯一的(未命名)类类型。函数和函数指针也有类型,他们的类型由其返回值类型和实参类型决定。
但是,不同类型的可调用对象,可能共享同一种调用形式。
调用形式:是可调用对象的 返回值 和 参数列表 的 组合 。
举个例子,现有如下三种可调用对象:
auto add1 = [](int a, int b) { return a + b; } 这是lambda表达式,是 未命名 的类,
用编译器打印 typeid(add1).name() ,是下面这串东西
class <lambda_1df7cfe0482636e736c1805ed2e94511>
int add2(int a, int b) { return a + b; } 这是函数,是 int (int, int) 类,
它的类型其实就是调用形式
class add3 这是函数对象,是 add 类
{
public:
int operator()(int a, int b) { return a + b; }
};
嗯,他们都是可调用表达式,类都不相同。但是他们都有 共同的调用方式 。
那就是:
int(int, int) 返回值 和 参数列表的组合,
根据这一特性,C++推出一个 function
类,允许我们 用一个对象 存储 具有相同调用方式的 可调用对象 。
function
类
function
对象允许我们用一个对象,存储一类具有相同调用方式的可调用对象。
就用上面的三个可调用对象举个例子:
f1, f2, f3, 都具有相同的类型,都是 function<int(int, int)> 类
function<int(int, int)> f1 = add1; f1 存储了可调用对象 add1
function<int(int, int)> f2 = add2; f2 存储了可调用对象 add2
function<int(int, int)> f3 = add3(); f3 存储了可调用对象 add3
cout << f1(1, 2); 打印3
cout << f2(1, 2); 打印3
cout << f3(1, 3); 打印3
下面使用function
做一个简易计算器,阅读代码,体会一下function
的用处:
class mul 函数对象,实现乘法
{
public:
int operator()(int a, int b) { return a * b; }
};
int divi(int a, int b) 函数,实现除法
{
return a / b;
}
int main()
{
map<string, function<int(int, int)>> cal; 一个 map,运算符号为键,其实现作为值
auto mod = [](int a, int b) { return a % b; }; lambda表达式,实现取模
插入元素:
cal.insert({ "+", plus<int>() }); 标准库的加法函数对象
cal.insert({ "-", [](int a, int b) { return a - b; } }); 未命名的 lambda 对象,实现减法
cal.insert({ "*", mul() }); 函数指针
cal.insert({ "/", divi }); 用户定义的函数对象
cal.insert({ "%", mod }); 命名了的 lambda 表达式
int a, b;
string s;
while (cin >> a >> s >> b)
{
cout << a << s << b << " = " << cal[s](a, b) << endl;
}
}
上面代码中名为
cal
的map
是一种叫函数表的东西:
重载函数与function
我们不能直接将重载函数的名字存入function
类型的对象中。如下所示:
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
function(int(int, int)> cal = add; 错误,出现二义性,到底要存哪个 add
即便在声明cal
前面加上了调用方式,仍然不行。
这时应该使用函数指针,而非函数的名字。
int (*fun_ptr)(int, int) = add; 指针'fun_ptr'所指的'add'是接受两个 int 的版本
cal = fun_ptr; 这回对了
12、重载、类型转换 与 运算符
我们已经见过很多内置类型之间的类型转换了,下面将学习自定义类的类型转换。
这其中包含两个方向的转换,从其他类型转换到本类型,通过转换构造函数来完成。
以及从本类型转换成其他类型,通过类型转换运算符完成。
转换构造函数其实和普通的构造函数没啥区别,只不过他只有一个其它类型的参数,用这个参数来构造一个本类型的对象。
12.1 类型转换运算符
类型转换函数的一般形式:
operator type() const; type 是要转换成的类型
type
的类型要求是能作为函数的返回类型(void
除外)。因此不允许传换成数组或函数类型,但可以是数组指针、函数指针或者引用类型。
注意:
- 类型转换运算符必须是成员函数
- 不能声明返回类型
- 形参列表必须为空
- 函数应该用
const
修饰
下面举一个简单的例子:
class X
{
public:
X(int i = 0) : val(i) { } 转换构造函数, int 类型转 X 类型
operator int() { return val; } 类型转换运算符,X 类型转 int 类型
private:
size_t val;
};
X x;
x = 4; 4 先隐式转换成 X 类型,然后调用合成的赋值运算符
x + 3; x 隐式的转换成 int 类型,然后执行整数加法
x = 3.14; 3.14 转换成 int ,int 再转换成 X
x + 3.14; x 先转换成 int,int 再转换成 double
哎停停停停停!最后两行转换代码是不是有什么问题?怎么会自动执行两步类型转换呢???
之前学的分明是:编译器只会自动执行一步类型转换。
嗯,确实只会执行一步。不过这里出现了新的特性。
类型转换运算符的特性:
用户定义的隐式的类型转换可以置于一个内置类型转换之前或之后,并与之一起使用。 只有这种情况下,允许执行两步类型转换。
上面的最后两条语句都是有自定义的类型转换参与,所以可以进行。
因此,可以把任何算术类型传递给X
的构造函数,同理,也能使用类型转换运算符把一个 X
对象转换成int
,然后把得到的int
转换成任何其他算术类型。
不能滥用类型转换运算符:
直接上例子,在早期C++版本中,下面的代码是能编译通过的:
int i = 10;
cin << i; 这里 cin 后接左移运算符,显然是错的,但却能通过编译
istream
本身确实没定义<<
,能通过编译是因为,在早期C++版本的这种情况下,istream
能够隐式的转换成bool
类型,由于bool
是算术类型,所以能执行<<
,就是将bool
的值左移10位。这显然不是我们想要的结果,他应该报错才对。
现实的类型转换运算符:
为了避免上述情况,C++11引入显式类型转换运算符。
在转换运算符前用explicit
修饰,就不会自动执行这一类型转换。同时,(一般情况下)也不能用于隐式类型转换。只能显示的使用它。如下所示:
class X
{
public:
operator int() { return val; }
int val;
};
X x;
x + 3; 错误,隐式转化
static_cast<int>(x) + 3; 正确,显示强转转换
上面的规定存在 例外 :
如果表达式被用作条件,则编译器会将explicit
修饰的类型转换运算符自动应用于它。在下列位置,显式类型转换将被隐式的执行:
if
、while
及do
语句的条件部分for
语句的条件表达式!
、||
、&&
这些逻辑运算符? :
条件运算符的条件表达式
这已经是明示你了,C++就是想让你在自定义的类型转换运算符的时候,使用explicit
修饰,同时,要把类类型转换成bool
类型,专门用于这些判断是否为真的情况。不过这是我个人的理解,有时候肯定也是有其他应用情况的。
12.2 避免有二义性的类型转换
有四种由类型转换导致的二义性错误。
12.2.1 两个类定义了转换到同一个类型的成员
图示:
class A
{
public:
A(const B&); 转换构造函数,B 类型转 A 类型
A fun(const A&);
};
class B
{
public:
operator A() const; 转换运算符,B 类型转 A 类型
};
B b;
A a = fun(b); fun()里需要一个A类型对象,b需要转换,但是会产生二义性
是 fun(B::operator()) 呢? 还是 fun(A::A(const B&)) 呢?
你现在有两种由B
转换成A
的方法,那到底用哪个???这就产生了二义性。
想要区分两种方法,就必须显示的调用相关成员:
A a1 = f(b.operator A()); 用B的类型转换运算符
A a2 = f( A(b) ); 用A的构造函数
解决方法:
不要定义出两种转换成同一类型的方法噻。有一种方法不就够了吗。
12.2.2 涉及到内置类型的多重类型转换
图示:
直接上例子:
class A
{
public:
A(int = 0); int 转换成 A
A(double); double 转换成 A
operator int() const; A 转换成 int
operator double() const; A 转换成 double
};
void fun(long double); 函数,需要 long double 做成员
A a; a 到底是先转 int 再转 long double 呢?
fun(a); 还是先转 double 再转 long double 呢?
lone lg; lg 到底是先转 int 再转 A 呢?
A a2(lg); 还是先转 double 再转 A 呢?
解决方法:
少定义点类类型 和 算术类型之间的转换,很多算术类型间本来就能转换,你还定义这么多,不是添乱??
12.2.3 重载 和 转换构造函数
直接上例子:
class A
{
public:
A(int); int 转 A
};
class B
{
public:
B(int); int 转 B
};
void f1(const A&); 用 A 重载
void f1(const B&); 用 B 重载
f1(10); 显然,又二义性了
f1( A(10) ); 显式的用A,避免二义性
虽说可以通过显式的构造,但是存在这种操作,说明程序设计存在缺陷。
12.2.3 重载 和 转换构造函数(比上一个稍微复杂点)
这个和上一个很想,但要复杂一点:
class A
{
public:
A(int); int 转 A
};
class B
{
public:
B(double); double 转 B
};
void f1(const A&); 用 A 重载
void f1(const B&); 用 B 重载
f1(10); 哎!哎哎哎!!10是 int 欸!是不是不会二义性了?
不不不!很可惜,还是会二义性。
分析一波:
f1(const A&)
成立,int
可以转A
,而且是精确匹配f1(const B&)
成立,int
可先转double
,然后double
再转B
啊?这不是有一个可精确匹配吗?虽然但是,不可以!
编译器会报错,这是二义性。
就是因为你有两个类,如果只有一个类的话,里面有int
和 double
的构造函数就不会有二义性了。
这小节很绕,看看这个练习题:
14.50:
看好了LongDouble
并不是内置的long double
,不存在精确匹配一说。
14.51:
这一题告诉我们,内置类型转换优先于自定义的转换构造函数。
12.3 函数匹配 与 重载运算符
注意:本节的讨论的核心是重载运算符。
一言以蔽之,就是下面这句话:
说详细点就是:
当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。
也就是说,若a
是一种类类型,则表达式a sym b
的等价可能是:
a.operator(b); a 的成员函数
operator(a, b); 一个普通的非成员函数
产生二义性的例子:
class X
{
friend X operator+(const X&, const X&);
public:
X(int = 0);
operator int() const { return val; }
int val;
};
X x1, x2;
X x3 = x1 + x2; 使用重载的operator+成员
int i = s3 + 0; 产生二义性
例题:
这个例题算是您能遇见的最复杂的情况了,把他搞明白,就没问题了。
这是 上图缺少的LongDouble
和SmallInt
的定义:
对于第一个式子ld = si + ld;
:
对于第二个式子ld = ld +si
:仍然使用同样的策略
总结:
当两个类类型相加时,优先考虑一方进行类型之间的转换,再相加,如果不行,再考虑双方都转换成内置类型相加。
再来一个,这个题主要想让你知道,出现二义性,要怎么改:
SmallInt
的定义和上题一样
很明显会产生二义性
所以我们只需 显式的 让他走其中一条路即可。