简介
重载运算符实质是编写一个执行相应操作的函数,当运算符被使用时,编译器就会调用相应的函数去完成操作。重载的运算符函数,都有个特殊的函数名:
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#include 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 << '\n' << "name:" << stu._name << '\n' << "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#include 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 << '\n' << "name:" << stu._name << '\n' << "score:" << stu._score; return out; }
赋值运算符
默认,编译器会帮我们提供一个默认的赋值算 符 函 数 ,其默认的行为是:
对对象字段做如下操作:
字段是class类型,struct,则调用字段的赋值运算符。
字段是基本类型则直接赋值。
字段 是数组,则一 一 执行数组元素的赋值运算符,复制到另一个数组中。
很多时候这样并不能正确的执行我们需要的效果。所以需要自定义。
固定格式形如:Student & operator=(cosnt Student& other);
注意点:
1、如果赋值参数是同类对象,则应该有防止自赋值代码,以提高函数效率。
2、所有的赋值运算符,组合赋值运算符都应该返回当前对的引用。
3、由于赋值是原有数据的覆盖,所以应在赋值数据前,做必要的清理工作,如delete原对象申请的内存。
总结就是4个步骤:
①如果参数是同类对象,则要防止自赋值
②清理当前对象原有的资源
③ 一 一拷贝数据
④返回当前对象引用
下面是一个简单的 存储int类型元素的Stack 的赋值运算符。
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; //返回当前对象 }
注意重载二元运算符的对称性
下面,为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__#includeclass 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] = '\0'; }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;}