C++运算符重载

运算符重载

当运算符作用域类类型对象时,可以通过运算符重载重新定义该运算符的含义,明智使用运算符重载能令我们的代码更易于编写和阅读。

重载的运算符是具有特殊名字的函数,它们的名字由关键字operator和其后要定义的运算符号共同组成,重载运算符函数的参数数量与该运算符的运算对象数量易于多,一元运算符有一个参数,二元运算符有两个参数,除了重载运算符operator()外,其它重载运算符不能有默认实参。

如果一个运算符函数是成员函数,则它的第一个运算对象绑定到隐式的this指针上,因为成员运算符函数的显式参数数量比运算符的运算对象少一个。

运算符函数至少含有一个类类型的参数,不能重定义内置类型的运算符。

//错误,不能重定义int类型的加法运算符
int operator+(int value1, int value2);

哪些运算符可以重载

大部分运算符都能被重载,有些运算符则不行,例如::,.*,.,? :

逻辑运算符&&,||会先求左侧运算对象的值,再求右侧对象的值,仅当左侧运算对象无法确定表达式的结果时才会再求右侧运算对象的值,这种策略称为短路求值

逗号运算符含有两个运算对象,按照从左向右的顺序依次求值,它会首先对左侧的表达式求值,然后将求值结果丢弃,逗号表达式的真正的结果是右侧表达式的值。

重载逻辑运算符&&,||和逗号运算符会破坏求值顺序或短路求值特性,所以不建议重载它们。

对于类类型对象,取地址运算符有内置的含义,一般也不应该被重载。

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

  • 如果类进行IO操作,则定义移位运算符使其与内置类型的IO保持一致。
  • 如果类的某个操作时检查想等性,则定义operator==,如果有了operator==则通常也应该有operator!=。
  • 如果类包含有单序比较操作,则定义operator<,如果有了operator<,则通常也应该有其它关系操作。
  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容,例如逻辑运算符&&应该返回bool,算符运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符应该返回左侧运算对象的一个引用。
  • 如果类重载了算术运算符或者位运算符,最好也提供对应的复合赋值运算符,例如+=,~=。
  • 明智使用运算符重载,不要滥用这一特性造成误解。

使用成员函数还是非成员函数

  • 赋值=,下标[],调用(),成员访问箭头->运算符必须是成员函数。
  • 复合赋值运算符通常应该是成员函数。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增,递减,解引用运算符,通常应该是成员函数。
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术,想等性,关系,位运算等,它们通常应该是普通的非成员函数。

输入和输出运算符

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

通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,因为向流写入内容会改变其状态,引用是因为我们无法直接复制一个ostream对象。第二个形参一般是一个常量的引用,引用是因为我们希望避免复制实参,之所以是常量是因为输出对象不会改变对象内容。返回值一般要返回它的ostream形参。

与输出运算符不同,输入运算符的第二个参数必须是非常量的,因为输入运算符会改变对象。

class Person
{
public:
    int height = 180;
};

std::ostream& operator<<(std::ostream& os, const Person& person)
{
    os << "person.height=" << person.height;
    return os;
}

std::istream& operator>>(std::istream& is, Person& person) 
{
    is >> person.height;
    if (!is) {
        person.height = 0;
    }
    return is;
}

int main(void)
{
    Person person;
    std::cin >> person;
    std::cout << person;
    system("pause");
    return 0;
}

算术运算符

算术运算符通常定义为非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参一般是常量的引用。算术运算符通常会计算它的两个运算对象并得到一个新值,所以算符运算符返回值通常是新值的副本。

class Money
{
public:
    int value = 0;
    Money(int value) 
    {
        this->value = value;
    };
};

Money operator+(const Money& money1, const Money& money2)
{
    return Money(money1.value+money2.value);
}


int main(void)
{
    Money money1{10};
    Money money2{ 50 };
    Money money3 = money1 + money2;
    std::cout << money3.value;
    system("pause");
    return 0;
}

相等运算符

相等运算符与算术运算符类似,通常也是非成员函数,形参为常量的引用,但是它的返回值是bool。

如果一个类含有判断两个对象是否相等的操作,最好把函数函数定义为operator==而不是一个普通的命名函数,这样更容易理解。

相等运算符应该有传递性,即a==b和b==c都为真,则a==c也为真。

如果类定义了operator==,则也应该定义operator!=,相等运算符和不相当运算符中的一个应该把工作委托给另外一个,这样可以复用代码。

class Money
{
public:
    int value = 0;
    Money(int value) 
    {
        this->value = value;
    };
};

bool operator==(const Money& money1, const Money& money2)
{
    return money1.value == money2.value;
}

bool operator!=(const Money& money1, const Money& money2)
{
    return !(money1==money2);
}

int main(void)
{
    Money money1{ 10 };
    Money money2{ 50 };
    Money money3{ 10 };
    std::cout << ((money1 == money2) ? "true" : "false") << std::endl;//false
    std::cout << ((money1 == money3) ? "true" : "false") << std::endl;//true
    std::cout << ((money1 != money2) ? "true" : "false") << std::endl;//true
    system("pause");
    return 0;
}

关系运算符(大于和小于)

当需要对某个类的对象进行排序时可以定义关系运算符,如果类同时包含相等运算符,则关系运算符的定义和相等运算符的结果应该是一致的。

如果类中有多个数据成员可以用来比较,例如Person类中有height,age等,这种类不定义关系运算符比较好,不然容易造成歧义。

对某个类定义了大于,小于,等于运算符后,不会自动定义小于等于和大于等于运算符。

class Money
{
public:
    int value = 0;
    Money(int value) 
    {
        this->value = value;
    };
};

bool operator==(const Money& money1, const Money& money2)
{
    return money1.value == money2.value;
}

bool operator!=(const Money& money1, const Money& money2)
{
    return !(money1==money2);
}

bool operator<(const Money& money1, const Money& money2)
{
    return money1.value < money2.value;
}

bool operator<=(const Money& money1, const Money& money2)
{
    return (money1 < money2 || money1 == money2);
}

bool operator>(const Money& money1, const Money& money2)
{
    return money1.value > money2.value;
}

bool operator>=(const Money& money1, const Money& money2)
{
    return (money1 > money2 || money1 == money2);
}

int main(void)
{
    Money money1{ 10 };
    Money money2{ 50 };
    Money money3{ 10 };
    std::cout << ((money1 > money2) ? "true" : "false") << std::endl;//false
    std::cout << ((money1 < money2) ? "true" : "false") << std::endl;//true
    std::cout << ((money1 <= money3) ? "true" : "false") << std::endl;//true
    std::cout << ((money2 >= money3) ? "true" : "false") << std::endl;//true
    system("pause");
    return 0;
}

赋值运算符

之前已经介绍过拷贝赋值和移动赋值运算符,它们可以把类的一个对象赋值给该类的另一个对象。除此之外,类还可以定义其他赋值运算符以使用别的类型作为右侧使运算对象。

下标运算符

下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标运算符可以出现在赋值运算符的任意一端,另外我们最好同时定义下标运算符的常量版本和非常量版本。

class Http
{
private:
    std::string headers_[3] = {"hello ","world","!"};
public:
    std::string& operator[](std::size_t n) 
    {
        return headers_[n];
    }
    const std::string& operator[](std::size_t n) const
    {
        return headers_[n];
    }
};


int main(void)
{
    Http http;
    std::cout << http[0]<

递增和递减运算符

由于递增和递减运算符会改变操作对象的状态,所以最好定义为成员函数。定义递增和递减运算符的类应该同时定义前置版本和后置版本。

为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用,后置运算符应该返回对象的原值(递增和递减之前),返回的形式是值而非引用。

由于前置版本和后置版本的递增或递减运算符使用的是同一个符号,为了区分它们,后置版本接受一个额外的不被使用的int类型的形参,这个形参的唯一作用就是区分前置版本和后置版本。

class Money
{
private:
    int value_ = 0;
public:
    int getValue() { return value_;}
    Money& operator++()
    {
        ++value_;
        return *this;
    }

    Money& operator--()
    {
        --value_;
        return *this;
    }

    Money operator++(int)
    {
        Money oldMoney = *this;
        ++*this;
        return oldMoney;
    }

    Money operator--(int)
    {
        Money oldMoney = *this;
        --*this;
        return oldMoney;
    }
    
};


int main(void)
{
    Money money;
    std::cout << money.getValue() << std::endl;//0
    std::cout << (++money).getValue()  << std::endl;//1
    std::cout << (--money).getValue() << std::endl;//0
    std::cout << (money++).getValue() << std::endl;//0
    std::cout << (money--).getValue() << std::endl;//1
    std::cout << money.getValue() << std::endl;//0
    system("pause");
    return 0;
}

成员访问运算符

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

我们可以重载解引用运算符完成任何想要的操作,但是箭头运算符永远不能丢掉成员访问这个基本的含义,重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。

对于形如object->member的表达式来说
1.如果object是一个对象的指针,则会执行(*object).member。
2.如果object是一个重载了箭头运算符的对象,则会调用重载的箭头运算符计算结果,如果结果依然是一个指针,则执行第一步,如果结果本身也重载了箭头运算符,则重复调用当前步骤。

class Money
{
private:
    int value_ = 0;
    std::string hello_ = "hello,world!";
public:
    std::string& operator*()
    {
        return hello_;
    }
    
    std::string* operator->() 
    {
        return &this->operator*();
    }
    
};


int main(void)
{
    Money money;
    std::cout << (*money).c_str() << std::endl;
    std::cout << money->c_str() << std::endl;
    system("pause");
    return 0;
}

你可能感兴趣的:(C++运算符重载)