C++ Primer 笔记——重载运算

1.对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载元素运算符不能含有默认实参。

class test
{
    void operator() (int i = 0) {};    // 正确, 但是operator()只能是成员函数
};

int operator+ (test t, int j = 0);    // 错误,不可以有默认实参

 

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

class test
{
public:
    int operator+ (int i) { return i + 1; };
};

test t;
int i = t + 1;

3.对一个运算符函数来说,它或是类的成员,或者至少含有一个类类型或枚举类型的参数,所以我们无法改变内置类型的运算符的含义。

class test
{
};

int operator- (int i, test t);    // 正确
int operator+ (int i, int j);    // 错误,没有类类型或枚举的参数

4.以下是可重载和不可重载运算符的列表:

  • 有四个符号(+-*&)既是一元运算符,也是二元运算符,从参数的数量我们可以推断到底定义的是哪种运算符。
  • 对于一个重载的运算符来说,其优先级和结合律与内置运算符保持一致。
  • 赋值(=),下标([ ]),调用(( ))和成员访问箭头(->)运算符必须是成员

5.重载运算符函数的调用方法:

class test
{
public:
    int operator+=(int i) { return 2 + i; }
};

int operator- (int i, test t)
{
    return  i - 1;
}

test t;
// 以下两种调用是等价的
int i = 3 - t;
i = operator-(3, t);

// 以下两种调用是等价的
i = t += 3;
i = t.operator+=(3);

 

 6.与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。否则,它的左侧运算对象将是我们的类的一个对象。

 

7.operator<<一般要返回它的ostream形参,operator>>一般要返回它的istream形参,输入运算符必须处理输入可能失败的情况。

8.定义递增和递减运算符的类应该同时定义前置版本和后置版本。为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。后置运算符应该返回对象的原值,返回的形式是一个值而非引用。为了区分前置和后置运算符,后置版本接受一个额外的(不被使用)int型形参,当我们使用后置运算符的时候,编译器为这个形参提供一个值为0的实参,尽管从语法上来说后置函数可以使用这个额外的形参,但是实际过程中我们通常不会这么做。

class test
{
public:
    test(int i) :m_count(i) {}

    test& operator++()    // 前置运算符
    {
        ++m_count;
        return *this;
    }

    test operator--(int)    // 后置运算符
    {
        test tmp = *this;
        --m_count;
        return tmp;
    }

    int m_count;
};

test t(1);
test t1 = ++t;    // t1.m_count = 2
test t2 = t--;    // t2.m_count = 2

                // 显示调用
test t3 = t.operator++();    // t3.m_count = 2
test t4 = t.operator--(0);    // t4.m_count = 2

 9.我们能令operator*完成任何我们指定的操作,比如返回一个固定的值等等,但是operator->获取成员的事实则永远不变,我们可以改变箭头是从哪个对象中获取成员,例如如果箭头的对象是string类型那么箭头获取的成员一定有size(),这个是无法改变的。重载的箭头运算必须返回类的指针或者自定义了箭头运算符的某个类的对象。

class test
{
public:
    test(std::string name) :m_name(name) {}

    std::string* operator->()
    {
        return &(this->m_name);
    }

    
    std::string m_name;
};

test t("test");
std::size_t i = t->size();    // i=4

10.函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,互相之间应该在参数或类型上有所区别。如果类定义了调用运算符,则该类的对象称作函数对象。

class test
{
public:
    int operator()(int i)
    {
        return i > 0 ? i : -i;
    }

};

test abs;
int i = abs(-1);    // i=1

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

std::plus<int> test;
int i = test(10, 20);    // i=30

12.我们可以将标准库函数对象运用在算法中,而且标准库规定其函数对象对于指针同样适用。

std::vector<std::string> vec = { "1","3","2" };
std::sort(vec.begin(), vec.end(), std::greater<std::string>());    // 将默认的升序排列改为了降序排列

std::vector<std::string*> vecp = { new std::string("1"), new std::string("3"), new std::string("2") };
// 错误,以下比较指针的大小是未定义的
std::sort(vecp.begin(), vecp.end(), [](std::string *a, std::string *b) { return a < b; });
    
// 正确,标准库规定指针的less是定义良好的
std::sort(vecp.begin(), vecp.end(), std::less<std::string*>());


13.我们可以使用一个名为function的新标准库类型来定义函数类型。function也是一个模板,function类型重载了调用运算符,该运算符接受它自己的实参然后将其传递给存好的可调用对象。

 

int add(int a, int b)
{
    return a + b;
}

auto mod = [](int a, int b) {return a % b; };

struct divide 
{
    int operator() (int a, int b) { return a / b; }
};

int main()
{
    std::map<std::string, int(*)(int, int)> mapFunc =
    {
        { "+", add },        // 正确,add是一个函数指针
        { "/", divide() },    // 错误,不是一个函数指针
        { "*", mod}            // 错误,lambda是个类类型
    };

    std::function<int(int, int)> f1 = add;        // 函数指针
    std::function<int(int, int)> f2 = divide();    // 函数对象类的对象
    std::function<int(int, int)> f3 = mod;        // lambda

    int ret;
    ret = f3(1, 3);        // ret=1

    std::map<std::string, std::function<int(int, int)>> mapFuncs = 
    {
        {"+", add},                    // 函数指针
        {"-", std::minus<int>()},    // 标准库函数对象
        {"/", divide()},            // 影狐定义的函数对象
        {"*", [](int a, int b) { return a * b; }},    // 未命名的lambda
        {"%",mod}                    // 命名了的lambda对象
    };

    ret = mapFuncs["+"](1, 2);        // ret = 3

    return 0;
}


14.我们不能(直接)将重载函数的名字存入function类型的对象中,但是可以通过存储函数指针的方法。

int add(int a, int b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}

std::map<std::string, std::function<int(int, int)>> mapFunc =
{
    { "+", add },    // 错误,分不清哪个add
};

int(*fp)(int, int) = add;
std::map<std::string, std::function<int(int, int)>> mapFuncs =
{
    { "+", fp },    // 正确,是int型的add
};

15.类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型,一般形式如下:

operator type() const;

  • 其中type表示某种类型,类型转换运算符可以面向任意类型(void除外)进行定义,只要该类型能作为函数的返回类型。因此我们不允许转换成数组或函数类型,但允许转换成指针或引用类型。
  •  类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成类的成员函数。通常不应该改变转换对象的内容,因此一般被定义成const成员。
  • 因为类型转换运算符是隐式执行的,所以无法给这些函数传递实参,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都i会返回一个对应类型的值。
  • 尽管编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准类型转换之后或之前。
class test
{
public:
    test(std::size_t size) : m_size(size) {}
    operator std::size_t() const { return m_size; }

private:
    std::size_t m_size;
};

test t = 3;                // 首先将3隐式的转换成test,然后调用赋值运算符
std::size_t i = t + 1;    // 首先将t隐式的转换为size_t,然后执行加法
double d = t + 3.14;    // t先被转换成了size_t,后又被转换成double
test t1 = 3.14;            // double被转换成了size_t


16.为了防止不必要的隐式自动类型转换,C++11引入了显示的类型转换运算符。

class test
{
public:
    test(std::size_t size) : m_size(size) {}
    explicit operator std::size_t() const { return m_size; }

private:
    std::size_t m_size;
};

test t(3);
std::size_t s = t + 1;        // 错误,运算符是显示的
std::size_t s1 = static_cast<std::size_t>(t) + 1;    //    正确 

该规定存在一个例外,即如果表达式被用作条件,则编译器会将显示的类型转换自动应用于它:

  • if , while及do语句的条件部分
  • for语句头的条件表达式
  • 逻辑与或非运算符的运算对象
  • 条件运算符(? :)的条件表达式

 

17.如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则会产生二义性,只能通过显示调用转换运算符或转换构造函数来解决,强制类型转换也无法解决二义性问题。

class testex;
class test
{
public:
    test() {}
    test(const testex& t);
};
class testex { public: operator test() const; }; test func(const test&); testex tex; test t1 = func(tex); // 二义性错误,我们想把tex转成test类型,但是有两种方法 test t2 = func(tex.operator test()); // 正确 test t3 = func(test(tex)); // 正确

18.当我们使用两个用户定义的类型转换时,如果转换函数之前或者之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。

class test
{
public:
    test(int i = 0) {}
    test(double d) {}

    operator int() const {}
    operator double() const {}
};

void func(long double);
test t;
func(t);    // 二义性错误,不知道调用哪个operator

long l;
test t1(l);    // 理论上也会产生二义性,此时标准类型转换将决定最佳匹配是哪个


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

class test
{
    friend test operator+(const test& t1, const test& t2);
public:
    test(int i = 0):m_count(i) {}
    operator int() const { return m_count; }

    int m_count;
};

test operator+(const test& t1, const test& t2)
{
    return test(t1.m_count + t2.m_count);
}

test t1, t2;
test t3 = t1 + t2;    // 正确
int i = t3 + 1;        // 二义性错误

 

原文地址:https://www.cnblogs.com/zoneofmine/p/7308267.html