C++ Primer 5th学习笔记13 操作重载与类型转换

操作重载与类型转换

1 基本概念

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

运算符
符号 符号 符号 符号 符号 符号
+ - * / % ^
& | ~ , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new[] delete delete[]

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

某些运算符不应该被重载
  由于某些运算符指定了运算对象求值的顺序,因此关于运算对象求值顺序的规则无法应用到重载的运算符上,特别是,逻辑与运算符、逻辑或运算符、逗号运算符的运算对象求值顺序规则无法保留下来,除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性。
即:通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符

赋值和复合赋值运算符
  赋值运算符的行为和复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回其左侧运算对象的一个引用。

2 输入和输出运算符

2.1 重载输出运算符<<

  一般情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,之所以ostream是非常量是因为向流写入内容会改变其状态;而第二个参数一般是常量引用,打印对象不会改变对象的内容。operator<<一般返回其ostream形参。
编写Sales_data的输出运算符,示例如下:

ostream &operator<<(ostream &os, const Sales_data &item)
{
    os << item.isbn() << " " << item.units_sold << " "
       << item.revenue << " " << item.avg_price();
       return os;
}

输出运算符主要负责打印对象内容而非控制格式,因此尽量减少格式化操作,如打印换行符等。

2.2 重载输入运算符>>

  一般情况下,输入运算符的第一个形参是运算符将要读取的流的引用;而第二个参数将要读入到的(非常量)对象的引用。
编写Sales_data的输入运算符,示例如下:

istream &operator>>(istream &is, Sales_data &item)
{
    double price;
    is >> item.bookNo >> itme.units_sold >> price; 
    if (is)    //检查输入是否成功
        item.revenue = item.units * price;
    else
        item = Sales_data();    //输入失败:对象被赋予默认的状态
    return is;
}

3 算术和关系运算符

3.1 相等运算符

  检查两个对象是否相等,编写两个Sales_data对象是否相等,示例如下:

bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
    return lhs.isbn() == rhs.isbn() &&
           lhs.units_sold == rhs.units_sold &&
           lhs.revenue == rhs.revenue;
}

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

设计准则:

  • 若判断一个类含有判断两个对象是否相等的操作,则定义成operator==比定义为一个普通的函数更好,也更容易使用标准库容器和算法。
  • 若类定义了operator==,则该运算符应能判断一组给定的对象中是否含有重复数据
  • 若类定义了operator==,则该类也应该定义operator!=

4 下标运算符

  表示容器的类通常可以通过元素在容器中的位置访问元素。下标运算符通常以访问元素的引用作为返回值,这样可以使下标出现在赋值运算符的任意一端。如果一个类包含下标运算符,则通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。示例如下:

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;    //指向数组首元素的指针
}

5 递增和递减运算符

定义前置递增/递减运算符
  在StrBlobPtr类中定义递增和递减运算符,示例如下:

//前置运算
class StrBlobPtr{
    public:
    //递增和递减运算符
    StrBlobPtr& operator++();    //前置运算符
    StrBlobPtr& operator--();
}

//前置版本:返回递增/递减对象的引用
StrBlobPtr& StrBlobPtr::operator++()
{
    check(curr, "increment past end of StrBlobPtr");
    ++curr;    //将curr在当前状态下向前移动一个元素
    return *this;
}

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

后置版本接受额外的(不被使用)一个int类型的形参,其作用是区分前置版本和后置版本的函数,示例如下:

//后置运算
class StrBlobPtr{
    public:
    //递增和递减运算符
    StrBlobPtr& operator++(int);    //后置运算符
    StrBlobPtr& operator--(int);
    
}

//后置版本:递增/递减对象的值但是返回原值
StrBlobPtr StrBlobPtr::operator++(int)
{
    StrBlobPtr ret = *this;    //记录当前的值
    ++*this;    //先前移动一个元素
    return ret;    //返回之前记录的状态
}

StrBlobPtr& StrBlobPtr::operator++()
{
    StrBlobPtr ret = *this;    //记录当前的值
    ++*this;    //向后移动一个元素
    return ret;    //返回之前记录的状态
}

6 成员访问运算符

  在迭代器及智能指针中常常用到解引用运算符(*)和箭头运算符(->)。在StrBlobPtr类添加这两种运算符:

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*();
    }
}

7 函数调用运算符

7.1 lambda是函数对象

表示lambda以及相应捕获行为的类
当一个lambda表达式通过引用捕获变量时,将由出席负责确保lambda执行时引用所引的对象确实存在。lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员,示例如下:

//lambda的作用是找到第一个长度不小于给定值的string对象
//获得第一个指向满足条件的迭代器,该元素满足size() is >=sz
auto wc = find_if(words.begin(), words.end(),
                 [sz](const string &a)
                 {return a.size() >= sz;});
                 
//该lambda表达式产生的类将形如:
class SizeComp {
    SizeComp(size_t n): sz(n) {}    //该形参对应捕获的变量
    //该调用运算符的返回类型、形参和函数体都与lambda一致
    bool operator()(const string &s) const
         { return s.size() >= sz; }
    private:
        size_t sz;    //该数据成员对应通过值捕获的变量
}

lambda表达式产生的类不包含默认构造函数、赋值运算符及默认析构函数

7.2 标准库定义的函数对象

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类。例如,plus类定义了一个函数调用运算符用于对一对运算对象执行+的操作;modulus类定义了 一个调用运算符执行二元的%操作;equal to类执行==,等等。下列类型定义在functional头文件中。

标准库函数对象
算术 关系 逻辑
plus<type> equal_to<type> logical_and<type>
minus<type> not_equal_to<type> logical_or<type>
multiplies<type> greater<type> logical_not<type>
devides<type> greater_equal<type>
modulus<type> less<type>
negate<type> less_equal<type>

使用示例如下:

plus<int> intAdd;    //可执行int加法的函数对象
negate<int> intNegate;    //可对int值取反的函数对象
int sum = intAdd(10, 20);    //等价于sum = 30
sum = inAdd(10, intNegate(10));   //sum = 0

在算法中使用标准库函数对象
==需要特别注意的是,标准库规定其函数对象对于指针同样适用。==通过比较指针的内存地址来sort指针的vector,使用标准库函数对象来实现,示例如下:

vector<string *> nameTable;    //指针的vector
//错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
     [](string *a, string *b) {return a < b; });
//正确:标准库规定指针的less是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());

关联容器使用less<key_type>对元素排序,因此可以调用一个指针的set或者再map中使用指针作为关键值而无须直接声明less。

7.3 可调用对象与function

C++语言中几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。定义一个函数表用于储存指向指向这些可调用对象的“指针”,当程序需要执行某个特定的操作时,从表中查找该调用的函数即可。在C++中函数表可以通过map来实现。例如使用一个表示运算符符号的string 对象作为关键字;使用实现运算符的函数作为值,当需要求给定运算符的值时,先通过运算符索引map,然后调用找到的那个元素。示例如下:

//构建从运算符到函数指针的映射关系,其中函数接受两个int,返回一个int
map<string, int(*)(int, int)> binops;
//将add的指针添加到binops中:
binops.insert({"+", add});    //{"+", add}是一个pair

标准库function类型

function的操作
操作 描述
function<T> f; f是一个用来储存可调用对象的空function,可调用对象的调用形式应该与函数类型T相同
function<T> f(nullptr); 显示地构造一个空function
function<T> f(obj); 在f中储存可调用对象obj的副本
f 将f作为条件:当f含有一个可调用对象时为真;否则为假
f(args) 调用f中的对象,参数是args

定义为function<T>的成员的类型

类型 描述
result_type 该function类型的可调用对象返回的类型
argument_type 当T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type是
first_argument_type` 该类型的同义词,如果T有两个实参,则两个实参的类型分别为
second_argument_type first_argument_type和second_argument_type

function使用示例如下:

function<int(int, int)> f1 = add;    //函数指针
function<int(int, int)> f2 = devide();    //函数对象类的对象
function<int(int, int)> f3 = [](int i, int j)    //lambda表达式
                             { return i * j; };

//使用function类型操作定义map:
map<string, function<int(int, int)>> binops;

接下来就可以把所有课调用的对象,包括函数指针、lambda或者函数对象在内,都添加到这个map中:

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

//使用示例:
binops["+"](10, 5);    //调用add(5, 5)
binops["-"](10, 5);    //调用minus<int>对象的调用运算符

重载的函数与function
==不能直接将重载函数的名字存入function中,==示例如下:

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来消除二义性

//使用lambda来指定使用的add版本
binops.insert({"+", [](int a, int b) {return add(a, b);}});

猜你喜欢

转载自blog.csdn.net/qq_18150255/article/details/89300930