我的C++primer长征之路:重载运算与类型转换

重载运算与类型转换

基本概念

基本格式

返回类型 operator 运算符 (参数列表){
    
    

}

重载运算符参数数目与该运算符作用的运算对象个数一样,例如重载=运算符,其参数列表中的参数数目应该为2。
但是,如果重载运算符为类的成员函数,那么其左侧的运算对象会绑定到隐式的this指针上,所以成员函数运算符的参数个数比运算符操作对象个数少1个。

重载的运算符的优先级与内置类型的对应运算符的优先级一致。

定义为成员函数OR非成员函数?

  • 赋值=、下标[]、函数调用()、成员访问运算符->必须是成员函数。
  • 符合赋值运算符(+= -=…)一般是成员函数,但非必须。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,递增++、递减–、解引用*等一般应该是成员运算符。
  • 具有对称性的运算符、如算术运算符、关系运算符、位运算符等应该是非成员运算符。

重载的例子

输入输出运算符>> 和 <<

一般来说,输入输出运算符的重载形式为:stream& operator 输入输出流运算符 (流普通引用, 操作对象);

输入输出运算符必须是非成员函数。因为它的返回对象是一个流的引用而非自定义的类对象引用。

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

istream &operator (istream& is, Sales_data &item){
    
     //与>>不同,这里的第二个参数必须为非const得,因为读入会改变操作对象。
    double price;
    is >> item.bookNo >> item.units_sold >> price;
    if(is){
    
                                     //检查输入是否成功
        item.revenue = item.units.sold * price;
    }
    else{
    
    
        item = Sales_data();    //输入失败,赋予默认构造得值
    }
    return is;
}

重载输入运算符与输出运算符的一个不同就是输入运算符需要确保输入操作是否成功。

算数和关系运算符

相等运算符

//还是以Sales_data为例
bool operator == (const Sales_data &rhs, const Sales_data &lhs){
    
    
    return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue;
}
//一般情况下定义了==也应该定义!=
bool operator != (const Sales_data &rhs, const Sales_data &lhs){
    
    
    return !(lhs == rhs);
}

关系运算符

#include<string>
#include<iostream>

using namespace std;

class Person{
    
    
    public:
    Person(string name, int age) : name(name), age(age) {
    
    }
    bool operator < ( const Person& rhs);
    private:
    string name;
    int age;
};

bool Person::operator < (const Person& rhs){
    
    
    return this->age < rhs.age;
}

int main(){
    
    
    Person a("Jack", 23);
    Person b("Rose", 22);
    cout << (a < b);
    return 0;
}

定义一个关系运算符 < 会使自定义类对象在调用标准库算法时很便利。

赋值运算符

赋值运算符必须是成员函数。复合赋值运算符不一定必须是成员函数,但是为了与内置类型的复合赋值运算符保持一致,一般定义为成员函数。

//从给定的初始化列表赋值
Person& Person::operator = (initializer_list<string> il){
    
    
    //alloc_n_copy分配内存空间和拷贝给定范围内的元素
    auto data = alloc_n_copy(il.begin(), il.end());

    //如果对象之前分配有内存,先释放它,Person类没有,所以不释放。

    name = data.first;
    age = data.second;
    return *this;
}

initializer_list是一种模板类型,其中 的元素永远是常量值,无法改变。

il.begin();  //初始化列表首元素
il.end();
il.size();

下标运算符

下标运算符必须是成员函数。为与原始下标保持一致,一般返回所访问元素的引用。

class StrVec{
    
    
    public:
    std::string& operator [](std::size_t n){
    
    
        return element[n];
    }
    //一般来说定义常量版本和非常量版本
    const std::string& operator [](std::size_t n){
    
    
        return element[n];
    }
    private:
    std::string* element;
};

递增递减运算符

  1. 前置运算符与后置运算符的区别是,后置运算符的参数列表需要有一个int形参。

  2. 一般先定义前置运算符,之后在定义后置运算符时可以利用定义好的前置运算符进行递增或递减。

  3. 前置运算符返回该类型的引用,而后置运算符返回一个临时对象。

    class MyNumber{
          
          
        public:
        MyNumber(int n) :num(n) {
          
          }
        //前置返回类型引用
        MyNumber& operator ++(){
          
          
            ++num;
            return *this;
        }
        MyNumber& operator --(){
          
          
            --num;
            return *this;
        }
        //后置返回该类型的临时对象,参数列表有int形参。
        MyNumber operator ++(int){
          
          
            MyNumber tmp = *this;
            ++num;
            return tmp;
        }
        MyNumber operator --(int){
          
          
            MyNumber tmp = *this;
            --num;
            return tmp;
        }
        public:
        int get_num(){
          
          
            return num;
        }
        private:
        int num = 0;
    };
    

其实重载的运算符可以显式地调用,也就是以一般的函数形式进行调用,但是一般不这样用。

成员访问运算符

  • 箭头运算符->必须是成员函数,解引用运算符一般也是成员函数。
  • 重载的箭头运算符永远都是执行访问成员的操作,而重载的其他运算符可以执行与默认运算符不一样的操作。

重载函数调用运算符

作用就是让类的对象像函数一样进行调用。
必须是成员函数。

struct AbsInt{
    
    
    int operator() (int val){
    
    
        return val < 0 ? -val : val;
    }
};

int i = -100;
AbsInt abs;
int a = abs(i);

如果一个类定义了函数调用运算符,那么这个类的对象一般称为函数对象。而函数对象通常可以作为泛型算法的实参。

vector<string> vs = {
    
    "Jack", "Rose"};
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;
};
PrintString printer;
printer(s);  //cout中打印s,以空格为分隔符

//调用for_each算法,将vs中的字符逐一输出到cerr中,以换行符为分隔符。
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

lambda表达式是函数对象

定义一个lambda表达式后,编译器其实将该表达式翻译成了一个未命名类的未命名对象。也就是说该未命名的类重载了函数调用运算符 () 。

stable_sort(words.begin(), words.end(), 
            [](const string &a, const string &b) {
    
    return a.size() < b.size();});
//默认情况下lambda表达式不能该变它捕获的变量。

//这里的lambda表达式类似于下面:
class ShorterString{
    
    
    public:
    bool operator ()(const string &s1, const string &s2){
    
    
        return s1.size() < s2.size();
    }
};

//上面的stable_sort相当于:
stable_sort(words.begin(), words.end(), ShorterString());
//stable_sort每次比较两个字符串时,会“调用”这个函数对象。

还有,lambda表达式以引用捕获变量时,其产生的类中不会生成对应的数据成员。若是以值捕获方式捕获变量时,其生成的类中会生成对应的数据成员,同时创建构造函数以拷贝的值来初始化该数据成员。生成的类不含默认构造函数、赋值运算符以及默认析构函数,默认的拷贝构造函数/移动构造函数视捕获的数据成员类型而定。书本P574.

标准库定义的函数对象

定义在functional头文件中。
一般情况下能使用标准库的函数对象就尽量使用标准库的函数对象。

vector<string*> 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。

可调用对象与function

可调用对象包含:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类。
不同的类型可能具有相同的调用形式。例如

int add(int i, int j) {
    
    return i + j;}

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

struct divide{
    
    
    int operator() (int i, int j){
    
    
        return i / j;
    }
};
//他们的调用形式都是
int (int, int)

//用一个map来存储函数指针
map<string, int(*)(int, int)> binops;
binops.insert({
    
    "+", add});    //正确,add是一个正确类型的函数指针。
//错误, divide和mod的调用形式与add一样,但类型不是int(*)(int, int)
binops.insert({
    
    "/", divide})

解决方法就是用functional头文件中的function。

map<string, function<int(int, int)>> binops;
binops.insert({
    
    "+", add});
binops.insert({
    
    "/", divide()});  //重载了()的函数对象
binops.insert({
    
    "%", mod});

//可以这样调用,function类型重载了调用运算符
binops["+"](1, 2);  ///调用add函数,输出3。

重载的函数不能直接放入function中。

//重载了add,无法区分
int add(int i, int j);
Sales_data add(const Sales_data&, const Sales_data&);

map<string, function<int(int, int)>> binops;
binops.insert({
    
    "+", add}); //错误,不能确定是那个add

解决方法,可以用:

  1. 函数指针。
  2. lambda表达式
int (*fp)(int, int) = add;  //指向的是第一个add。
binops.insert({
    
    "+", fp});

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

重载、类型转换与运算符

一个形参的非explicit的构造函数隐式地定义了一种从形参类型到该类类型的隐式转换规则。

类型转换运算符

  • 必须定义为成员函数。
  • 没有返回类型。
  • 形参列表必须为空。
  • 通常应该是const成员函数。
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 value;}  //定义了转换为int的类型转换运算符。

    private:
    std::size_t val;
};

SmallInt si;
si = 3.14;   //先内置类型double转换成int,之后调用SmallInt(int)构造函数。
si + 3.14;   //int()先将si转换成int型,之后内置的Int型转换成double与3.14相加。

因为类型转换是***隐式***进行的,无法给函数传递实参,所以也就形参列表就为空。
注意避免过度使用类型转换运算符。一般来说,类很少定义类型转换运算符,除了bool之外。

隐式的类型转换会存在意想不到的问题,所以引入了显式的类型转换运算符。也就是在转换运算符前加上explicit。

class SmallInt{
    
    
    public:
    explicit operator int() const {
    
    return val;}  //显式类型转换
};

SmallInt si;
//这种情况下,需要显式地声明类型转换才会进行类型的转换。
int ans = static_cast<int>(si) + 3;  //显式地调用operator int()

一般来说,显式声明的类型转换运算符需要显式地调用,只有一个例外:表达式被用作条件时,显式的类型转换将会被隐式地执行。

  1. 在if、while以及do语句的条件部分。
  2. for语句的条件表达式。
  3. 逻辑与或非(&&,||,!)。
  4. 条件运算符(? : )的条件部分。
if (si > 10){
    
      //si被隐式地转换成int型。
    //...
}

对于IO流,无论什么时候在条件中使用流,都会使用流定义的operator bool类型转换运算符。

while(std::cin >> i){
    
     ... } //cin将数据读入i并返回cin。这个时候cin就会隐式地执行operator bool将返回的cin转换成bool用于条件判断。

而且一般来说, bool类型转换应该定义成explicit的,因为它一般只用在条件语句中。

避免有二义性的类型转换

不要定义多种从类型A到类型B的转换路径,一般只要一对一的转换,否则在类型转换时,编译器不知道调用哪种转换方式。(书本P584)

总的来说,除了显式地定义向bool的转换之外,尽量避免定义类型转换函数和非显式的单形参的构造函数。

猜你喜欢

转载自blog.csdn.net/weixin_40313940/article/details/107325443