重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成。

基本概念

重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成。

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

只能重载大多数的运算符,而不能发明新的运算符号。重载运算符的优先级和结合律跟对应的内置运算符保持一致。

1
//非成员运算符函数
2
data1 + data2;
3
operator+(data1, data2);
4
//成员运算符函数
5
data1 += data2;
6
data1.operator+=(data2);

将运算符是否定义为成员函数的一些判断:

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
  • 复合赋值运算符一般来说是成员,但并非是必须的。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符通常是成员,如递增、解引用。
  • 具有对称性的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符等,通常是非成员函数。
可以被重载 不可以被重载
+, -, *, /, %, ^ ::, .*, ., ? :,
&, ` ,~,!,,,=`
<, >, <=, >=, ++, --
<<, >>, ==, !=, &&, `
+=, -=, /=, %=, ^=, &=
|=, *=, <<=, >>=, [], ()
->, ->*, new, new[], delete, delete[]

输入和输出运算符

重载输出运算符<<

输出运算符的第一个形参通常是一个非常量ostream对象的引用。第二个形参一般是一个常量的引用。operator<<一般返回它的ostream形参。

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

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

重载输入运算符>>

输入运算符的第一个形参通常是运算符将要读取的流的引用,第二个形参是将要读取到的(非常量)对象的引用,通常返回某个给的流的引用。

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

1
istream &operator>>(istream &is, Sales &item)
2
{
3
    double price;
4
    is >> item.bookNo >> item.units_sold >> price;
5
    if(is)
6
        item.revenue = item.units_sold * price;
7
    else
8
        item = Sales(); //输入失败,对象被赋予默认状态
9
    return is;
10
}

算数和关系运算符

算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。这些运算符一般不需要改变运算对象的状态,所有形参都是常量的引用。

如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。

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

相等运算符

  • 如果定义了operator==,则这个类也应该定义operator!=
  • 相等运算符和不等运算符的一个应该把工作委托给另一个。
  • 相等运算符应该具有传递性。
  • 如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使用户更容易使用标准库算法来处理这个类。

关系运算符

如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果同时还包含==,则当且晋档<的定义和++产生的结果一直时才定义<运算符。

赋值运算符

和拷贝赋值及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,在创建一片新的空间。

1
class StrVec{
2
public:
3
    StrVec &operator=(std::initializer_list<std::string>);
4
    //...
5
};
6
7
StrVec &StrVec::operator=(initializer_list<string> il)
8
{
9
    auto data = alloc_n_copy(il.begin(), il.end());
10
    free();
11
    elements = data.first;
12
    first_free = cap = data.second;
13
    return *this;
14
}

我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。

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

下标运算符

下标运算符必须是成员函数。

一般会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。

递增和递减运算符

定义递增和递减运算符的类应该同时定义前置版本和后置版本。通常应该被定义成类的成员。

为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。

1
class StrBlobPtr{
2
public:
3
    StrBlobPtr& operator++();
4
    StrBlobPtr& operator--();
5
    //...
6
};
7
8
StrBlobPtr& StrBlobPtr:operator++()
9
{
10
    check(curr, "increment past end of StrBlobPtr");
11
    ++curr;
12
    return *this;
13
}
14
15
StrBlobPtr& StrBlobPtr:operator--()
16
{
17
    --curr;
18
    check(curr, "decrement past begin of StrBlobPtr");
19
    return *this;
20
}

同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。

后置版本接受一个额外的,不被使用的int类型的形参。因为不会用到,所以无需命名。

1
class StrBlob{
2
public:
3
    StrBlob operator++(int);
4
    StrBlob operator--(int);
5
    //...
6
};
7
8
StrBlob StrBlob::operator++(int)
9
{
10
    StrBlob ret = *this; //记录当前值
11
    ++*this;    //向前移动一个元素,前置++需要检查递增的有效性
12
    return ret;  //返回之前记录的状态
13
}
14
15
StrBlob StrBlob::operator--(int)
16
{
17
  StrBlob ret = *this;
18
  --*this;
19
  return ret;
20
}
21
22
StrBlob p(a1);
23
p.operator++(0);    //显示调用后置运算符
24
p.operator++();     //调用前置运算符

成员访问运算符

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

1
class StrBlob{
2
public:
3
    std::string& operator*() const{
4
        auto p = check(curr, "dereference past end");
5
        return (*p)[curr];
6
    }
7
    std::string* operator->() const{
8
        //实际工作委托给解引用运算符
9
        return &this->operator*();
10
    }
11
}

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

解引用和乘法的区别是一个是一元运算符,一个是二元运算符。

函数调用运算符

可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。

如果类定义了调用运算符,则该类的对象称作函数对象(function object)。因为可以调用这种对象,所有这种对象的行为像函数一样

1
class PrintString{
2
public:
3
    PrintString(ostream &o = cout, char c = ' '):
4
            os(o), sep(c) { }
5
    void operator()(const string &s) const {
6
        os << s << sep;
7
    }
8
private:
9
    ostream &os;
10
    char sep;
11
};
12
13
PrintString printer;
14
printer(s); //在conut种打印s,后面跟一个空格
15
PrintString errors(cerr, '\n');
16
errors(s);  //在cerr中打印s,后面跟一个换行符

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

lambda是函数对象

当编写一个lambda时,编译器将该表达式翻译成一个未命名类的未命名对象,该类中包含一个重载的函数调用运算符。

1
auto wc = find_if(words.begin(), words.end(),
2
            [sz](const string &a)
3
                    { return a.size() >= sz; });
4
5
class SizeComp{
6
    SizeComp(size_t n): sz(n) { }
7
    bool operator()(const string &s) const
8
        { return s.size() >= sz; }
9
private:
10
    size_t sz;
11
};
12
13
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));

lambda捕获变量:lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。

lambda表达式产生的类不含默认构造函数,赋值运算符及默认析构函数,是否含有默认的拷贝/移动构造函数则通常视捕获的数据成员类型而定。

标准库定义的函数对象

算术 关系 逻辑
plus equal_to logical_and
minus not_equal_to logical_or
multiplies greater logical_not
divides greater_equal
modulus less
negate less_equal

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

可调用对象与function

C++中的可调用对象有:函数,函数指针,lambda表达式,bind创建的对象以及重载了函数调用运算符的类。

可调用的对象也有自己的类型,例如每个lambda有它自己唯一的(未命名)类类型,函数及函数指针的类型则由其返回值类型和实参类型决定。

调用形式(call signature)指明了调用返回的类型以及传递给调用的实参类型,一种调用形式对应了一个函数类型,不同类型的可调用对象可能共享同一种调用形式(int(int, int))。

function类型可以用来为不同类型的可调用对象共享同一种调用形式。

1
int add(int i, int j) { return i+j; }
2
auto mod = [](int i, int j) { return i % j; }
3
struct divide{
4
    int operator()(int denominator, int divisor){
5
        return denominator / divisor;
6
    }
7
};
8
9
map<string, function<int(int, int)>> binops = {
10
    {"+", add},
11
    {"-", std::minus<int>()},
12
    {"/", divide()},
13
    {"*", [](int i, int j){ return i * j; }},
14
    {"%", mod} };
15
16
binops["+"](10, 5);
17
binops["-"](10, 5);
18
binops["/"](10, 5);
19
binops["*"](10, 5);
20
binops["%"](10, 5);
操作 解释
function f; f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与类型T相同。
function f(nullptr); 显式地构造一个空function
function f(obj) f中存储可调用对象obj的副本
f f作为条件:当f含有一个可调用对象时为真;否则为假。
定义为function<T>的成员的类型
result_type function类型的可调用对象返回的类型
argument_type T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type
first_argument_type 第一个实参的类型
second_argument_type 第二个实参的类型

不能将重载函数的名字存入function类型的对象中,可以通关存储函数指针或者使用lambda来消除二义性。

1
int add(int i, int j){ return i+j; }
2
Sales add(const Sales&, const Sales&);
3
map<string, function<int(int, int)>> binops;
4
binops.insert( {"+", add} );    //error
5
int (*fp)(int, int) = add;  //指针所指的add是接受两个int的版本
6
binops.insert( {"+", fp} );
7
binops.insert( {"+", [](int a, int b){ return add(a,b); } });

重载、类型转换、运算符

类型转换运算符

转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversions)。

类型转换运算符(conversion operator)是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const

1
operator type() const; //将类类型转换为type类型

避免过度使用类型转换函数。

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

1
class SmallInt{
2
public:
3
    explicit operator int() const { return val; }
4
    //...
5
};
6
7
SmallInt si = 3; //隐式的转换为SmallInt,在调用SmallInt::operator=
8
si + 3; //error
9
static_cast<int>(si) + 3;   //显式请求类型转换成int

bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。

避免有二义性的类型转换

通常,不要为类定义相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。

在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。

函数匹配与重载运算符

如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。