C++运算符重载

时间:2021-01-10 08:07:08

C++运算符重载

基本知识

重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和其后要定义的运算符号共同组成。

运算符可以重载为成员函数和非成员函数。当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数比运算对象的数量少一个。

调用重载运算符函数

//非成员函数的等价调用
data1 + data2;//normal expression
operator+(data1,data2); // equal function call
//成员函数的等价调用
data1 += data2;//normal expression
data1.operator+(data2); // equal function call

可重载的运算符:+ - * / % ˆ & | ~ ! = < > += -= = /= %= ˆ= &= |= << >> >>= <<= == != <= >= && || ++ -- , -> -> ( ) [ ]

无法被重载的运算符有: :: .* . ?:

Restrictions

  • The operators :: (scope resolution), . (member access), .* (member access through pointer to member), and ?: (ternary conditional) cannot be overloaded
  • New operators such as **, <>, or &| cannot be created
  • The overloads of operators &&, ||, and , (comma) lose their special properties: short-circuit evaluation and sequencing.
  • The overload of operator -> must either return a raw pointer or return an object (by reference or by value), for which operator -> is in turn overloaded.
  • It is not possible to change the precedence, grouping, or number of operands of operators.

选择作为成员或非成员的依据(引用C++ Primer)

  1. 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
  2. 复合赋值运算符一般来说应该是成员,但并非必须。
  3. 改变对象状态的运算符或者与给定类型密切相关的运算符,如++、--和解引用运算符,通常应为成员。
  4. 具有对称性的运算符可能转换任意一端的运算对象,例如算数、相等性、关系和位运算等,通常应为普通的非成员函数。便于自动类型转换
  5. IO运算符必须是非成员函数(友元)。目的是为了与iostream标准库的输入输出兼容,否则左值为类的对象。

运算符实例类

class Sales_data{
public:
Sales_data(): units_sold(0), revenue(0.0) {}
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0.0) {}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
std::string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data&);
double avg_price() const; //运算符重载函数申明
friend ostream& operator<<(ostream& os,const Sales_data& item);
friend istream& operator>>(istream& is,Sales_data& item); private:
string bookNo;
unsigned units_sold;
double revenue;
};
inline double Sales_data:: avg_price() const{
if(units_sold)
return revenue / units_sold;
else
return 0;
}

输入和输出运算符

重载输出运算符<<

通常情况下,第一个形参是一个非const的ostream对象的引用。ostream是非常量是因为写入流会改变其状态;而该形参是引用类型是因为无法直接复制一个ostream对象。

第二个形参一般是const&。因为我们希望避免复制实参,并且被打印的内用一般不会改变其内容。

为了与其他运算符保持一致,operator<<一般返回它的ostream形参。

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

输出运算符尽量减少格式化操作

输出运算符主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

输入输出运算符必须是非成员函数,一般申明为友元

与iostream标准库兼容。

重载输入运算符>>

通常,第一个形参是流的引用,第二个是将要读到的(非const)对象引用。函数返回给定流的引用。

istream& operator>>(istream& is,Sales_data& item)
{
double price;
is>> item.bookNo >> item.units_sold >> price;
if(is) //检查输入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); //输入失败,对象被赋予默认的状态
}

当读取操作发生错误时,输入运算符应该负责从错误中恢复。

算术和关系运算符

通常情况下,我们把算术和关系运算符定义成非成员函数以允许对左侧或者右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是const&。

Sales_data operator+(const Sales_data& lhs,const Sales_data& rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}

如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合运算符来实现算术运算符。

相等运算符

用来检验两个对象是否相等。

inline bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
inline bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}

相等运算符设计准则:

  • 如果一个类包含判断两个对象是否相等的操作,显然定义成operator而非一个普通命名函数。这使得用户不必记忆新的函数名,同时定义了运算符之后也更容易使用标准库容器和算法。
  • 如果类定义了operator==,则该运算符应该能判断一组给定对象中是否含有重复数据。
  • 通常情况下,==运算符应该具有传递性。
  • 当类定义了==,应当也为类定义!=。
  • ==运算符和!=运算符中的一个应该把工作委托给另一个。意味着其中一个是负责实际比较对象的工作,另一个负责调用。

关系运算符

定义了相等运算符的类也常常(但不总是)包含关系运算符。特别的是,因为关联容器和一些算法要用到<,所以定义operator<会比较有用。

通常情况下关系运算符应该

  1. 定义顺序关系,令其与关联容器对关键字的要求一致(与考试内容无关,详情见C++ Primer);并且
  2. 如果类同时有运算符时,则定义一种关系令其与保持一致,特别的两个对象!=时,那么一个对象应该<另外一个。

如果存在唯一一种逻辑可靠的<定义,则应该考虑为类定义<运算符。如果类同时存在运算符,当且仅当<的定义和产生的结果一致时才定义<

此处的实例不存在逻辑可靠的关系运算,故引用其他例子。

关系运算符一般定义为非成员函数。

Typically, once operator< is provided, the other relational operators are implemented in terms of operator<。

inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return rhs < lhs;}
inline bool operator<=(const X& lhs, const X& rhs){return !(lhs > rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !(lhs < rhs);}

赋值运算符

T& operator=(const T& other) // copy assignment
{
if (this != &other) { // self-assignment check expected
if (/* storage cannot be reused (e.g. different sizes) */)
{
delete[] mArray; // destroy storage in this
/* reset size to zero and mArray to null, in case allocation throws */
mArray = new int[/*size*/]; // create storage in this
}
/* copy data from other's storage to this storage */
}
return *this;
}

复合赋值运算

一般倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部。为了与内置类型的复合赋值保持一致,一般返回左侧对象的引用。

Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

下标运算符

表示容器的类通常可以通过元素在容器中的位置访问元素,一般会定义operator[]。

下标运算符必须是成员函数

一般会定义两个版本,一个返回普通引用,另一个是类的常成员并返回常量引用。

class  StrVec
{
public :
string& operator[](size_t n)
{ return elements[n];}
const string& operator[](size_t n) const
{ return elements[n];}
private :
string *elements;
};

递增和递减运算符

递增和递减运算符改变的是操作对象的状态,建议设定为成员函数。

应该同时定义前置和后置版本的递增和递减运算符

定义前置递增/递减运算符

template<class T>
class T{
public :
T& operator++();
T& operator--();
private:
int x;
};
template<class T>
T& T::operator++()
{
++x;// actual increment takes place here
return *this;
}
template<class T>
T& T::operator--()
{
--x;// actual decrement takes place here
return *this;
}

为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。

区分前置和后置运算符

后置版本接受一个额外的(不被使用)int类型的形参。

template<class T>
class T{
public :
T operator++(int);
T operator--(int);
private:
int x;
};
template<class T>
T T::operator++()
{
T tmp = *this; // copy
++(*this); // pre-increment
return *tmp; // return old value
}
template<class T>
T T::operator--()
{
T tmp = *this; // copy
--(*this); // pre-increment
return *tmp; // return old value
}

为了与内置版本保持一致,后置运算符应该返回对象的原值,返回的是一个值而非引用。

参考资料: