blueyi's notes

Follow Excellence,Success will chase you!

0%

C++学习笔记之重载运算符与类型转换

重载运算符的定义方式通常为(以+运算符举例)type operator+(argu1, argu2,..) {}

1.如果一个重载的运算符是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上。成员运算符函数的(显式)参数数量比运算对象的数量少一个。
运算符的调用通常是调用重载的运算符函数,所以可以像调用普通函数一样直接调用运算符函数。eg:

1
2
3
4
5
6
7
//一个非成员运算符函数的等价调用
data1 + data2; //普通表达式
operator+(data1, data2); //等价的函数调用

//成员运算符的等价调用,函数参数比实际少一个
data1 += data2; //普通表达式
data1.operator+=(data2); //对成员运算符函数的等价调用

2.运算符重载时,具有关联的运算符应该通过调用进行重载。例如在定义==!=时,可以先定义==,然后定义!=时调用==的重载运算符。

3.赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员函数。复合赋值运算符通常应该是成员,改变对象状态的运算符如递增、递减和解引用通常也应该是成员。而具有对称性的运算符有可能在调用时需要转换任意一端的运算对象,如算术、相等性、关系和位运算等通常应该为非成员函数。

4.不能对两个基本的内置变量进行运算符重载,如int operator+(int, int)是错误的。

输入(>>)输出(<<)运算符

5.重载输出运算符时应尽可以的减少对格式控制的操作。输入输出运算符必须是非成员函数,且IO对象必须是引用,通常返回也是引用,输出对象应该是const引用,因为不应该修改输出对象。输入运算符还应该检查并处理可能的输入出错情况,如有效性。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//重载输出运算符
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << item.price();
return os;
}

//重载输入运算符
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNo >> item.units_sold >> price;
if (is) //检查输入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); //输入失败,则将对象赋予默认的状态
}

关系运算符

关系运算符通常应该定义一个同时也定义关联和其他运算符,如定义了<,应该也定义>
6.相等和不等运算符举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
//先重载==运算符
bool operator==(const Sales_data &item1, const Sales_data &item2)
{
return item1.isbn() == item2.isbn() &&
item1.units_sold == item2.units_sold &&
item1.revenue == item2.revenue;
}

bool operator!=(const Sales_data &item1, const Sales_data &item2)
{
return !(item1 == item2);
}
//利用已经重载过的==运算符来定义!=运算符

7.赋值运算=的重载方式与拷贝赋值运算符类似,定义时也必须先释放当前的内存空间,再创建新的空间,以解决自赋值的问题,且必须是成员函数。

下标运算符

如果一个类定义了下标运算符,则它通常应该有两版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。eg:

1
2
3
4
5
6
7
8
9
class strVec {
public:
std::string& operator[](std::size_t n)
{ return elements[n]; }
const std::string& operator[](std::size_t n) const
{ return elements[n]; }
private:
std::string *elements; //指向数组首元素的指针
};

递增递减运算符

定义递增和递减运算符的类应该同时定义其前置和后置版本,operator++()默认情况下为前置版本(++obj),区分后置版本则需要额外提供一个不被使用的int类型的形参,编译器会为该形参提供一个值为0的实参。递增递减应该检查索引值的有效性,当定义了前置版本之后,就可以利用前置版本来完成后置版本的实际工作。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//StrBolbPtr类是一个指针类,其成员curr保存当前对象所表示的元素的下标,check成员函数用来检查解引用StrBlobPtr是否安全
class StrBolbptr {
public:
//前置递增递减运算符
StrBlobPtr& operator++();
StrBlobPtr& operator--();
//后置递增递减运算符
StrBlobPtr& operator++(int);
StrBlobPtr& operator--(int);

}

//相应的定义
StrBlobPtr& StrBlobPtr::operator++()
{
//如果curr已经指向了容器的尾后位置,则无法递增它
check(curr, "increment past end of StrBolbPtr");
++curr; //将curr在当前状态下向前移动一个位置
return *this;
}

StrBlobPtr& StrBlobPtr::operator--()
{
//如果curr是0,则继续递减它将产生一个无效的下标
--curr; //将curr的当前状态向后移动一个位置
check(curr, "decrment past begin of StrBlobPtr");
return *this;
}

//后置版本需要返回原值,所以调用之前需要先记录对象的原值
//由于不需要用到int形参,所以不需要为其命名
StrBlobPtr& StrBlobPtr::operator++(int)
{
//此处无需检查有效性,因为实际递增工作将由前置++完成,而前置运算中将会检查有效性
StrBlobPtr ret = *this;
++*this; //使用已经定义的前置版本的++运算符来完成工作
return ret; //返回之前的记录
}

//后置版本需要返回原值,所以调用之前需要先记录对象的原值
StrBlobPtr& StrBlobPtr::operator--(int)
{
//此处无需检查有效性,因为实际递增工作将由前置--完成,而前置运算中将会检查有效性
StrBlobPtr ret = *this;
--*this; //使用已经定义的前置版本的++运算符来完成工作
return ret; //返回之前的记录
}

显式调用前置和后置递增运算符的方式:

1
2
3
StrBlobPtr p(a1);  //p指向a1中的vector
p.operator++(0); //调用后置版本的operator++
p.operator++(); //调用前置版本的operator++

重载成员访问运算符*和->

箭头运算符必须是类的成员,解引用运算符通常也是类的成员。可以先定义解引用运算符,然后箭头运算符由解引用来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class StrBlobPtr {
public:
std::string& operator*() const
{
auto p = check(curr, "dereference past end"); //检查有效性,并返回元素索引
return (*p)[curr]; //返回的是一个引用,(*p)是对角所指的元素
}

std::string* operator->() const //返回对象本身,即指针,因为该类是指针类
{
//实际工作由解引用运算符操作
return & this->operator*();
}
}

函数调用运算符()

如果类定义了调用运算符,则该类的对象就称作函数对象。一个类可以定义多个函数调用运算符,只需要在参数数量和类型上有区别。eg:

1
2
3
4
5
6
7
//absInt的函数对象将返回实参的绝对值
struct absInt {
int operator()(int val) const
{
return val < 0 ? -val : val;
}
};

调用:

1
2
3
int i = -42;
absInt absobj; //absobj为含有函数调用运算符的对象
int ui = absobj(i); //将i传递给absobj.operator()

如果函数对象类中含有一些数据成员,且这些数据成员被用于定制函数调用运算中的操作,这样的类称为含有状态的函数对象类,也就是说这种类的函数对象可以带有状态。eg:

1
2
3
4
5
6
7
8
9
//PrintString将打印string,每个string之间使用空格隔开
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' ') : os(o), sep(c) {} //默认构造函数
void operator()(const string &s) const { os << s << sep; } //重载函数调用运算符
private:
ostream &os; //用于写入的目的流
char sep; //分隔符
};

调用:

1
2
3
4
PrintString printer;  //调用默认构造函数,目的流为cout,分隔符为空格
printer(s); //向cout打印s,后面跟一个空格
PrintString errors(cerr, '\n');
errors(s); //向cerr中打印s,后面跟一个换行符

lambda是函数对象

lambda表达式实际是一个含有重载函数调用运算符的类,该类只含有重载的函数调用运算符,不含默认构造函数、赋值运算符及默认析构函数。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获得第一个指向满足条件元素的迭代器,该元素满足size() 是否 >= sz
auto wc = find_if(words.begin(), words.end(), [sz](const string &s) { return s.size() >= sz; })
//上面的lambda表达式产生的类形如:
class SizeCmp {
SizeCmp(size_t n) : sz(n) {} //该形参对应捕获的变量
bool operator()(const string &s) const
{
return s.size() >= sz;
}
private:
size_t sz;
};
auto wc = find_if(words.begin(), words.end(), SizeCmp(sz));
上述调用可改写为:

标准库定义的函数对象

标准库的头文件functional中定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命命操作的调用运算符。例如plus<Type>类定义了一个函数调用运算符用于对一对运算对象执行+操作。还有minus、equal_to、greater等。用法:

1
2
3
4
5
plus<int> intAdd;  //可以执行int加法的函数对
int sum = intAdd(10, 20);

//传入一个临时的函数对象用于执行两个string对象的>比较运算
sort(svec.begin(), svec.end(), greater<string>());

对两个无关指针的比较将产生未定义的行为,但标准库的函数对象也可以用于对指针进行比较。eg:

1
sort(nameTab.begin(), nameTab.end(), less<string*>());

标准库的function类型

C++11中新增的function是一个模板类,利用该模板,可以将不同类型但具有相同调用形式的函数放到同一个函数指针变量中。例如定义接受两个int参数,返回一个int参数的计算函数(加,减,乘,除),声明为function<int(int, int)> f = add; ,此时f可以指向任意符合接受两个int返回一个int的函数。
C++中的可调用对象有:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
function举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//计算器程序,其中map中存放函数列表
#include <iostream>
#include <string>
#include <map>
#include <functional>

int add(int i, int j){ return i + j; } //普通函数
auto mod = [](int i, int j){ return i % j; }; //lambda表达式
struct Div{ int operator ()(int i, int j) const { return i / j; } }; //函数对象类

auto binops = std::map<std::string, std::function<int(int, int)>>
{
{ "+", add }, // 函数指针
{ "-", std::minus<int>() }, // 标准库函数对象
{ "/", Div() }, // 用户定义的函数对象
{ "*", [](int i, int j) { return i*j; } }, // 未命名的lambda
{ "%", mod } // 命名了的lambda
};

int main()
{
while ( std::cout << "Pls enter as: num operator num :\n", true )
{
int lhs, rhs; std::string op;
std::cin >> lhs >> op >> rhs;
std::cout << binops[op](lhs, rhs) << std::endl; //函数调用形式形如binops["+"](10, 5);将调用add(10, 5);
}
return 0;
}

当使用重载的函数与function时,应该使用函数指针,以免由于同名函数名引起二义性。eg:

1
2
3
4
5
6
7
8
9
10
int add(int i, int j) {return i + j}
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); //错误,因为无法区别是哪个add

//可以使用函数指针
int (*fp)(int, int) = add; //指针指向的add是接受两个int的版本
binops.insert({"+", fp});
//或者使用lambda表达式
binops.insert({"+", [](int a, int b){ return add(a, b); } });

类型转换运算符

类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。形如:operator type() const;
类型转换运算符可以面向任意类型(void除外)进行定义,只要该类型能作为函数的返回类型,所以不允许转换成数组或者函数类型。但允许转换成指针(如数组指针和函数指针)或者引用类型。
一个类型转换函数必须是类的成员函数,它不能声明返回类型,形参列表也必须为空,类型转换函数通常应该是const。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//SmallInt表示0-255之间的整数类
class SmallInt {
public:
SmallInt(int i = 0) : val(i) //构造函数将int转换为SmallInt对象
{
if (i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const {return val;} //重载类型转换运算符,将SmallInt向int转换,注意实际val必须可以隐式转换为int类型

//错误示例
int operator int() const; //错误:指定了返回类型
operator int(int = 0) const; //错误:参数列表不为空
operator int*() const { return 42; } //错误:42不是指针

private:
std::size_t val;
}

该SmallInt类即定义了向类类型的转换,也定义了从类类型向其他类型的转换。

1
2
3
SmallInt si;
si = 4; //首先将4隐式转换为SmallInt,然后调用SmallInt::operator=
si + 3; //首先将si隐式转换成int,然后执行整数的加法

C++11中引入了显式的类型转换运算符(explicit conversion operator):

1
2
3
4
5
6
7
8
class SmallInt {
public:
//编译器不会自动执行这一类型转换
explicit operator int() const { return val; }
}
SmallInt si = 3; //正确:SmallInt的构造函数不是显示的,即没有使用explicit修饰
si + 3; //错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3; //正确:显式地强制类型转换

Welcome to my other publishing channels