c++重载运算符与类型转换

一、基本概念

由关键字operator和其后要定义的运算符号共同组成。也包含返回类型、参数列表以及函数体。

重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。

除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。

当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的参数数量比运算对象的数量少一个。

对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。

//错误:不能为int重定义内置的运算符
int operator+(int, int);

只能重载已有的运算符,无权发明新的运算符号。

有四个符号(+ - * &)既是一元运算符也是二元运算符,从参数的数量可以推断定义的是哪种运算符。

对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。

不能被重载的运算符:::        .*        .        ?:

//非成员运算符函数的等价调用
data1 + data2;                //普通的表达式
operator+(data1, data2);    //等价的函数调用

//成员运算符函数的等价调用
data1 += data2;                //基于“调用”的表达式
data1.operator+=(data2);    //对成员运算符函数的等价调用

通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。

使用与内置类型一致的含义

赋值和复合赋值运算符:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。

成员or非成员

= [] () ->运算符必须是成员
复合赋值运算符一般是成员
改变对象状态的运算符活着与给定类型密切相关的运算符,如递增、递减和解引用,通常是成员
具有对称性的运算符可能转换任意一段的运算对象,如算术、相等性、关系和位运算符等,它们通常是非成员函数。

运算符定义成成员函数时,左侧运算对象必须是运算符所属类的一个对象。

string s = "world";
string t = s + "!";        //正确:可以把一个const char*加到一个string对象中
string u = "hi" + s;    //如果+是string的成员,则会报错

string将+定义成普通的非成员函数,所以”hi”+s等价于operator+(“hi”,s)。和任何其他函数diaoyong一样,每个实参都能被转换成行参类型。唯一的要求是至少有一个运算类型是类类型,并且两个运算对象都能准确无误的准换成string。

二、输入和输出运算符

2.1 重载输出运算符<<
第一个形参是一个非常量ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。
第二个形参一般是常量的引用。形参是引用为了避免复制实参;之所以为常量是打印对象不会改变对象的内容。
为了与其他输出运算符保持一致,operator<<一般返回它的ostream形参。

输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

输入输出运算符必须是非成员函数。

IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。

2.2重载输入运算符>>
第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。

1
2
3
4
5
6
7
8
9
10
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNO>>item.units_sold>>price;
if(is) //判断是否发生IO错误
item.revenue = item.units_sold*price;
else
item = Sales_data();
return is;
}

输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

输入时的错误。

当流含有错误类型的数据时读取操作可能失败。

当流操作到达文件末尾或者遇到输入流的其他错误时也会失败。

当读取操作发生错误时,输入运算符应该负责从错误中恢复。

流的失败信息failbit。文件耗尽eofbit。流被破坏badbit。

三、算术和关系运算符

通常定义成非成员函数,以允许对左侧或右侧的元算对象进行转换。如果类定义了算数运算符,则它一般也会定义一个对应的复合赋值运算符。此时,最有效的方式是使用复合赋值来定义算术运算符。

1
2
3
4
5
6
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}

3.1相等运算符
通常,比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等。

1
2
3
4
5
6
7
8
9
Sales_data operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.bookNO_ == rhs.bookNO_ && lhs.units_sold_ == rhs.units_sold_ && lhs.revenue_ == rhs.revenue_;
}

Sales_data operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}

涉及准则:

使用operator==,而不是普通成员函数

operator==用来判断一组给定的对象中是否含有重复数据

相等运算符具有传递性

同样也要定义operator!=

==和!=中的一个应该把工作委托给另外一个

3.2关系运算符
如果存在唯一一种逻辑可靠的<定义,则应该为这个累定义<运算符。如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。

四、赋值运算符
除拷贝赋值和移动赋值运算符之外,还定义了接受花括号内元素列表为参数的。

vector<string> v;
v = {"A","an"};

StrVec &StrVec::operator=(initilizer_list<string> il)
{
    auto data = alloc_n_copy(il.begin(), il.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

赋值运算符必须定义成类的成员,复合赋值运算符通常也应该这样。都返回左侧运算对象的引用。

五、下标运算符
通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[]。
下标运算符必须是成员函数。返回值为所访问元素的引用,下标可以出现在赋值运算符的任意一端。最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
};

const StrVec cvec = svec;
if(svec.size() && svec[0].empty())
{
svec[0] = "zero";
cvec[0] = "zip"; //错误:对cvec取下标返回的是常量引用
}

六、递增和递减运算符
因为它们改变的正好是所操作对象的状态,所以建议设定为成员函数。
定义递增和递减运算符的类应该同时定义前置和后置版本。

前置,返回递增或递减后对象的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class StrBlobPtr
{
public:
StrBlobStr& operator++();
StrBlobStr& operator--();
};

StrBlobPtr& StrBlobPtr::operator++()
{
check(curr,"increment past end");
++curr;
return *this;
}

StrBlobPtr& StrBlobPtr::operator--()
{
--curr;
check(curr,"decrement past begin");
return *this;
}

后置,普通的重载形式无法区分这两个情况。为解决这个问题,后置版本接受一个额外的int类型的形参。使用后置运算符时,编译器为这个形参提供一个值为0的实参。
后置运算符返回对象的原值,返回值而不是引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class StrBlobPtr
{
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(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(a);
p.operator++(0); //后置
p.operator++(); //前置

七、成员访问运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
class StrBlobPtr
{
public:
std::string& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; //(*p)是对象所指的vector
}
std::string* operator->() const
{
return &this->operator*();
}
};

箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。

对箭头运算符的限定

point->mem

1.如果point是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序会发生错误。
2.如果point是定义了operator->的类的一个对象,则我们使用point.operator->()的结果来获取mem。如果该结果是一个指针,则制定第一步;如果该结果本身含有重载的operator->(),则重复调用当前步骤。

重载的箭头运算符必须返回累的指针或者自定义箭头运算符的某个类的对象。

八、函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。

1
2
3
4
5
6
7
8
9
10
struct absInt
{
int operator()(int val) const{
return val < 0 ? -val : val;
}
}

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

如果类定义了调用运算符,则该类的对象称作函数对象。

含有状态的函数对象类

函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符的操作。

1
2
3
4
5
6
7
8
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;
};

函数对象常常作为泛型算法的实参。

for_each(vs.begin(), vs.end(), PrintString(cerr,'\n'));

8.1 lambda是函数对象

1
2
3
4
5
6
7
8
9
10
11
stable_sort(words.begin(), words.end(), [](const string &a, const string &b){return a.size() < b.size();});

class ShorterString{
public:
bool operator()(const string &s1, const string &s2) const
{
return s1.size() < s2.size();
}
}

stable_sort(words.beign(), words.end(), ShorterString());

表示lambda及相应捕获行为的类

当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引用的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。

相反,通过值捕获得变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。

1
2
3
4
5
6
7
8
9
10
11
auto wc = find_if(words.begin(), words.end(), [sz](const string &a)const{return a.size() >= sz;});

class SizeComp
{
SizeComp(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(),SizeComp(sz));

8.2标准库定义的函数对象

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义一个执行命名操作的调用运算符。这些类都被定义成模版的形式。

1
2
3
4
5
6
7
plus<int> intAdd;
negate<int> intNegate;

int sum = intAdd(10,20);
sum = intNegate(intAdd(10,20));

sum = intAdd(10, intNegate(10));
算术 关系 逻辑
plus< Type > equal_to< Type > logical_and< Type >
minus< Type > not_euqal _to < Type > logical_or< Type >
multiplies< Type > greater< Type > logical_not< Type >
divides< Type > greater_equal< Type >
modulus< Type > less< Type >
negate< Type > less_euqal< Type >

在算法中使用标准库函数对象

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

vector<string *> nameTable;

sort(nameTable.begin(), nameTable.end(), [](string *a, stirng *b){return a < b;}); //错误:比较两个无关指针将产生未定义的行为

sort(nameTable.begin(), nameTable.end(), less<string*>());//正确:标准库规定指针的less是定义良好的

关联容器使用less对元素排序,因此我们可以直接定义一个指针的set或在map中使用指针作为关键值而无须直接声明less。

1
2
3
4
5
6
7
8
统计大于1024的值有多少个
std::count_if(ivec.cbegin(), ivec.cend(), std::bind(std::greater<int>(),_1,1024));

找到第一个不等于pooh的字符串
std::find_if(svec.cbegin(), svec.cend(),std::bind(std::not_euqal_to<std::string>(),_1,"pooh"));

将所有的值乘以2
std::transform(ivec.begin(),ivec.end(),ivec.begin(),std::bind(std::multiple<int>(),_1,2));

8.3 可调用对象与function

c++语言中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类。
可调用的对象也有类型。每个lambda有它自己唯一的类类型;函数及函数指针的类型由其返回值类型和实参类型决定。
两个不同类型的可调用对象却可能共享同一种调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,如int(int,int)。

不同类型可能具有相同的调用形式。

1
2
3
4
5
6
7
8
9
int add(int i, int j){return i+j;}

auto mod = [](int i, int j){return i % j;}

struct divide{
int operator()(int denominator, int divisor){
return denominator/divisor;
}
};

定义一个函数表(funciton table)用于存储指向这些可调用对象的“指针”。

1
2
3
4
5
map<string, int(*)(int, int)> binops;

binops.insert({"+",add}); //正确

binops.insert({"%",mod}); //错误:mod不是一个函数指针,mod是一个lambda表达式

标准库function类型

function定义在functional头文件中。是一个模版,需要提供该function类型能够表示的对象的调用形式。

1
2
3
4
5
6
7
8
9
10
11
function<int(int,int)>

function<int(int,int)> f1 = add;

function<int(int,int)> f2 = divide();

function<int(int,int)> f3 = [](int i, int j){return i*j;};

cout<<f1(4,2)<<endl;
cout<<f2(4,2)<<endl;
cout<<f3(4,2)<<endl;

使用function重新定义map

1
2
3
4
5
6
7
8
9
10
11
12
13
map<string,function<int(int,int)>> binops = {
{"+",add},
{"-",std::minus<int>()},
{"/",divide()},
{"*",[](int i, int j){return i * j;}},
{"%",mod}
};

binops["+"](10,3);
binops["-"](10,3);
binops["*"](10,3);
binops["/"](10,3);
binops["%"](10,3);

我们不能将重载函数的名字存入function类型的对象中。

1
2
3
4
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?

解决办法:存储函数指针

1
2
3
4
int (*fp)(int, int) = add;
binops.insert({"+",fp});

binops.insert({"+",[](int i, int j){return i+j;}});

九、重载、类型转换与运算符

类型转换运算符
operator type() const; type可作为函数的返回类型。

一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。通常是const。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SmallInt{
public:
SmallInt(int i = 0):val(i)
{
if(i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const{return val;}
private:
std::size_t val;
};

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

尽管编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准类型转换之前或之后,并与其一起使用。

1
2
3
SmallInt si = 3.14;		//内置类型转换将double实参转换成int,调用SmallInt(int)构造函数

si + 3.14; //SmallInt的类型转换运算符将si转换成int,内置类型转换将所得的int继续转换成double

无法给这些函数传递实参,不负责制定返回类型。

1
2
3
4
5
6
7
8
9
10
11
class SmallInt;

operator int(SmallInt&); //错误:不是成员函数

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

类型转换运算符可能 产生意外结果

1
2
int i = 42;
cin << i; //如果向bool的类型转换不是显式的,则该代码在编译器看来是合法的,把提升后的bool值左移42位

显式的类型转换运算符

1
2
3
4
5
6
7
8
9
10
11
class SmallInt
{
public:
explicit operator int() const {return val;}
}

SmallInt si = 3; //正确:SmallInt的构造函数不是显式的

si + 3; //错误:此处需要隐式的类型转换,但类的运算符是显式的

static_cast<int>(si) + 4; //正确:显式的请求类型转换

当类型转换运算符是显式时,也能执行类型转换,不过必须通过显式的强制类型转换才可以。

如果表达式被用作条件,则编译器会讲显式的类型转换自动应用于它。

if while do语句的条件部分

for语句头的条件表达式

逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象

条件运算符(? :)的条件运算符

转换为bool

c++11,IO标准库通过定义一个向bool的显式类型转换,避免发生意外结果

1
while(std::cin>>value)

将数据读入value,返回cin。为了对条件求值,cin被istream operator bool类型转换函数隐士地执行了转换。如果cin条件状态是good,则返回真;否则返回假。

类型转换与运算符

不要令两个类执行相同的类型转换。如果Foo类有一个接受Bar类对象的构造函数,则不要再Bar类中再定义转换目标是Foo类的类型转换运算符

避免转换目标是内置算术类型的类型转换。当已经定义一个转换成算术类型的类型转换时,接下来

    不要再定义接受算术类型的重载运算符

    不要定义转换到多种算术类型的类型转换

如果在调用重载函数时,我们需要使用构造函数或者强制类型转换来改变实参的类型,则这意味着程序的设计存在不足。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct C
{
C(int);
};
struct D{
D(int);
};

void manip(const C&);
void manip(const D&);

manip(10); //二义性错误:是manip(c(10)) 还是manip(D(10))


struct E
{
E(double);
};

void manip2(const C&);
void manip2(const E&);

manip2(10); //二义性错误:manip2(C(10)) 还是manip2(E(double(10)))

如果所需的用户定义的类型转换不止一个,则该调用具有二义性

1
2
3
4
5
6
7
8
9
10
11
12
13
class SmallInt
{
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); //转换源为int的类型转换
operator int() const{return val;} //转换目标为int的类型转换
private:
std::size_t val;
};

SmallInt s1, s2;
SmallInt s3 = s1 + s2; //使用重载的operator+
int i = s3+0; //二义性:可以把0转换成SmallInt,然后使用SmallInt的+;或者s3转换成int,然后对于两个int执行内置的加法运算