C++运算符重载

C++运算符重载

简介

重载运算符实质是编写一个执行相应操作的函数,当运算符被使用时,编译器就会调用相应的函数去完成操作。重载的运算符函数,都有个特殊的函数名:

operator 运算符,operator为关键字,表明这个函数是运算符函数。

如      加法运算符时的函数名:        operator+

         乘法运算符时的函数名:        operator*

C++支持运算符重载,可以让类的设计者自定义 当运算符操作在对象上时,发生的逻辑,这使得类被封装得更加完美。

如下是一个简单的例子

class Font
{
private:
    std::string _name;
    std::size_t _size;
public:
    Font(const std::string& name, std::size_t size):_name(name),_size(size){}

    Font& operator ++()   //增加字体对象 的大小(运算符重载)
    {
        ++_size;
        return *this;
    }

//    Font& bigger()      //增加字体对象 的大小(普通函数)
//    {
//        ++_size;
//        return *this;
//    }

    bool operator <(std::size_t size) const  //比较字体的大小(运算符重载)
    {
        return _size < size;
    }

//    bool small_than(std::size_t size)const  //比较字体的大小(普通函数)
//    {
//        return _size < size;
//    }

};



int main(int argc, char *argv[])
{

    Font font("Microsoft YaHei",20);

    if(font<20)
        ++font;

    return 0;
}

可以发现,使用运算符重载,写出来的代码干净清晰。但是对类的设计者要求就比较高,这需要熟练掌握运算符重载。

重载的运算符的注意点

 重载运算符有一些限制,我们不能打破,否则适得其反。

1、不能创建新的运算符,只能重载C++内置的运算符,见下表。

与比较相关,最好成对重载,或者全部重载。返回bool,或者int > < >= <= == !=    

与赋值相关,重载的函数要返回当前对象的 非const引用

= += -= /= *= %=

&=

|=

^=

>>=

<<=

需要区分前后缀,前缀,返回 非const引用,后缀,返回const对象值 ++ --            
只能重载为对象的成员的运算符(还有=) [] () ->          
逻辑相关,不建议重载. && ||          
加和减的 一元版本,前缀。很少使用 + -            
二元常规运算符,返回运算生成的临时const对象作为结果返回 + - * / %      
位运算相关 & | ~ ^ >> <<    
其他运算符 -> new new[] delete delete[]    

2、运算符重载只改变逻辑,不会改变优先级。重载后的运算符的优先级和内置运算符的优先级一样。

3、重载的运算符,无论是一元还是二元运算符,必须至少有一个操作数是本类(当前正在编写的类)对象

     比如: std::string 类重载了 + 运算符, 可以使用 strObj + "abc"  ,但是 "abc"+"def"  是不对的。这里的运算符+ 将2个地址相加,所以出错。

3、不要改变运算符原有的意义,这样使用起来会很别扭。有些运算符是不建议重载的。如  &    和   *   的 一  元  版  本,他们对于所有的数据对象都有固有的操作语义:取地址 和 解地址。不要打破这个根深蒂固的操作符的语义,所以不建议重载。

类成员函数,还是类外的辅助函数?

下面一元运算符,他们必须重载为对象的成员函数。原因是:这些运算符只有一个操作数,且这个操作数就是本类对象。

(什么,你说赋值运算符是2个操作数?NONONO,被赋值的那个对象还没完全诞生呢!)

[  ]  下标运算符 ,一般用于容器或者序列。

( )   函数调用运算符 ,一般用于自定义函数类对象。这些对象是可调用的。

    赋值运算符,大多数类都会实现。

->   通过指针访问对象的成员的运算符。一般用于 自定义指 针 类对象。

 除此之外,其它的运算符既可以定义为成员函数,也可以定义为类外的辅助函数(全局函数),该如何选择呢?

一般来说,如+   -    *   /  %  这样的二元运算符,不会改变对象,则一般定义为 类外的辅助函数。而 ++  -- 操作会改变对象状态,就定义为成员函数。

如果是类外的辅助函数,一般会声明为类的友元函数: friend ,便于访问类的成员。

对于二元运算符,它有2个操作数。如果重载为成员函数,则第一个操作数作为当前对象隐式传递。即:  a @ b   实质是    a.operator@(b)

当一个二元运算符 定义为类的辅助函数时,必须指明2个操作数参数。即:   a @ b    实质是    operator@(a,b)

运算符重载代码例子

重载 >>   << 运算符,让对象更方便的输入 , 输出

/*studnet.h*/
#ifndef _STUDENT_H__
#define _STUDENT_H__
#include<string>
#include<iostream>
class Student
{
private: std::string _name; int _age; double _score; public: Student(); Student(const std::string &name, const int &age, const double&score);

     /*一般将非成员运算符重载 声明为类的友元,这样方便访问对象的数据*/
    friend std::ostream& operator<<(std::ostream& out, const Student& stu);
    friend std::istream& operator>>(std::istream& in, Student& stu);

};

std::ostream& operator<<(std::ostream& out, const Student& stu);
std::istream& operator>>(std::ostream& in,        Student& stu);
/*studnet.cpp*/
#include"student.h"

Student::Student():_name(""),_score(0),_age(0)
{
}
 
Student::Student(const std::string &name, const int &age, const double&score) 
                            :_name(name), _age(age), _score(score)
{
    
}


//参数 out不能修饰为const,因为通过out流对象 输出 stu时,就是更改out流状态的过程。
//参数stu被输出,不会改变对象状态,修饰为const最好
//返回out本身,以便连续输出
std::ostream& operator<<(std::ostream& out, const Student& stu)
{
    out << "age:" << stu._age << '
'
        << "name:" << stu._name << '
'
        << "score:" << stu._score;

    return out;

}

std::istream& operator>>(std::istream& in, Student& stu)
{
    in >> stu._name >> stu._age >> stu._score;

    return in;

}

重载 ++   --  运算符

以 ++ 运算符为例,-- 运算与之符同理。

++  有前缀和后缀版本。当仅仅使用 ++ 的副作用,使操作对象自增1时,++a 和 a++都可以达到相同的效果,但是优选使用 ++ a,为什么,请往后看。

++ a   整个表达式的值是 a +1 之后的值, a++ 整个表达式的值是 a原本的值,这是二者表明上的区别。

下面将一个Student类对象重载 ++ 运算符,表示增加对象的_age属性。

/*studnet.h*/
#ifndef _STUDENT_H__
#define _STUDENT_H__
#include<string>
#include<iostream>
class Student { private: std::string _name; int _age; double _score; public: Student(); Student(const std::string &name, const int &age, const double&score); Student& operator++(); //前缀版本 Student operator++(int); //后缀版本

    friend std::ostream& operator<<(std::ostream& out, const Student& stu);

}; std::ostream& operator<<(std::ostream& out, const Student& stu);
/*studnet.cpp*/
#include"student.h"
Student::Student():_name(""),_score(0),_age(0)
{
}
Student::Student(const std::string &name, const int &age, const double&score) 
:_name(name), _age(age), _score(score)
{

}

/*使用一个int参数类型占位符来区别 前缀 和后缀版本,它只用来占位,区分,并无它用*/

/*后缀版本需要临时保存对象增1前的状态,以便返回,这就是我为什么说优先使用前缀版本的缘故了*/
 //前缀,返回值是增1后的 值,返回的是当前对象 
Student
& Student::operator++()
{
++_age;
return *this;
}

//后缀,返回的值当前对象增1 前 的值。 
//由于返回的是局部对象,所以函数的返回类型不能是引用类型。 
const Student Student::
operator++(int)
{
Student re = *this;
++_age;
return re;
}

std::ostream
& operator<<(std::ostream& out, const Student& stu)
{
out << "age:" << stu._age << ' '
<< "name:" << stu._name << ' '
<< "score:" << stu._score;
return out;
}

赋值运算符

默认,编译器会帮我们提供一个默认的赋值算 符 函 数 ,其默认的行为是:

对对象字段做如下操作:

  字段是class类型,struct,则调用字段的赋值运算符。

  字段是基本类型则直接赋值。

  字段 是数组,则一 一 执行数组元素的赋值运算符,复制到另一个数组中。

很多时候这样并不能正确的执行我们需要的效果。所以需要自定义。

固定格式形如:Student & operator=(cosnt Student& other);

注意点:

1、如果赋值参数是同类对象,则应该有防止自赋值代码,以提高函数效率。

2、所有的赋值运算符,组合赋值运算符都应该返回当前对的引用。

3、由于赋值是原有数据的覆盖,所以应在赋值数据前,做必要的清理工作,如delete原对象申请的内存。

    总结就是4个步骤:  

    ①如果参数是同类对象,则要防止自赋值

    ②清理当前对象原有的资源

    ③ 一 一拷贝数据

    ④返回当前对象引用

下面是一个简单的 存储int类型元素的Stack 的赋值运算符。

C++运算符重载
    Stack& Stack::operator = (const Stack& that)
    {
        if (&that == this)     //防止自赋值
            return *this;

        delete[] _innerArr;   //清理源有内存

        _len = that._len;
        _innerArr = new int[len]; //分配新内存
        for (std::size_t i = 0; i < _len; ++i)
        {
            _innerArr[i] = that._innerArr[i];
        }


        return *this;   //返回当前对象
    
    }
C++运算符重载

注意重载二元运算符的对称性

下面,为Student重载 + 运算符,表示返回一个_age 加上 某个参数后的 新 的Student类对象。

/*student.h(部分)*/
class
Student { //... public: //... //返回一个student镀锡 的_age 加上 add岁后的新的student对象 Student operator+(int add) const; };
/*student.cpp中的实现(部分)*/
Student Student::operator+(int add) const { Student re = *this; re._age += add; return re; }
/*main.cpp*/
int
main() { Student s("Bob", 19, 90.0); cout << s+3 << endl; //OK cout << 3+s<< endl; //error 匹配不到相应的运算符,因为我们没有考虑到前操作数是int 的版本 system("pause"); return 0; }

巧妙的补救

/*student.h(部分)*/
class
Student {
//...
public: Student operator+(int add) const;
/***/
friend Student operator+(int add, const Student& stu);
}; Student
operator+(int add, const Student& stu); //通过全局辅助函数来完成另一个重载。
/*student.cpp中的实现(部分)*/
Student Student::operator+(int add) const { Student re = *this; re._age += add; return re; } Student operator+(int add, const Student& stu) { return stu + 3; }

对容器类型重载 索引运算符 [ ]

一些容器(Java中叫集合)类型,很多时候需要获取容器中的第 xx个元素,这个时候重载 下标运算符[ ] 再合适不过了。

注意:由于[ ] 实质是函数调用,意味着 [ index] 索引可以是任何类型。当然不要乱用。

        如果[ index] 索引的的值是整型的,最好使用 无符号类型   std::size_t 。

        请考虑重载2和个版本:分别用于容器本身是 常量 / 非常量 的情况。当容器本身就是常量时,[]运算符取得的元素是const类型,这样避免了修改容器中的元素。

#ifndef _MSTRING_H__
#define _MSTRING_H__

#include<cstring>

class MString
{

public:
    MString(const char* cs);
    ~MString();

    const char& operator[](std::size_t index) const;
    char& operator[](std::size_t index);

private:
    char*  pstr;
    size_t len;
};

#endif
#include"mstring.h"


MString::MString(const char* cs)
{
    len = std::strlen(cs);
    pstr = new char[len + 1];
    std::strcpy(pstr,cs);
pstr[len] = ' ';
} MString::~MString() { delete[] pstr; }
/*用于读容器中的元素,则元素是不应该被修改的,容器也不应该被修改*/
const char& MString::operator[](std::size_t index) const { //if (index >= len || index < 0) //注意越界检查,这里没写出来了 return pstr[index]; }

//用于给容器中的元素写入新的值
char& MString::operator[](std::size_t index) { //if (index >= len || index < 0) //注意越界检查,这里没写出来了 return pstr[index]; }

类型转换运算符

除了可以自定义运算符的逻辑,还可以自定义类型转化时的逻辑,他们可以发生在 显示的或者隐式的类型转换时。严格说,这不属于运算符重载,但语法很相似,所以我一并写出来了。

格式:   operator TypeName() const

要求

1、必须是成员函数

2、没有返回类型,没有参数。

3、类型转化不会改变对象状态,所以定义的转化函数应该是const 函数。虽然它不是必须的。

玩过Arduino 的朋友都知道Serial类,代表了开发板的串口对象,我们将串口对象用于条件时,是判断它是否成功开启。下面来模拟一个。

class Serial
{

private:
    bool _is_opened;

public:
    Serial():_is_opened(false) {}

    operator bool() const   //定义对象转化为bool 时的操作逻辑
    {
        return _is_opened;
    }

};

int main(int argc, char *argv[])
{
    Serial serial;
    if(serial)      //隐式的类型转化
    {
        //deal with serial
    }
    return 0;

}