C++ 编程/运算符/运算符重载
运算符重载(不太常见的术语为特设多态)是多态性(语言的面向对象特性的组成部分)的一种特殊情况,其中一些或所有运算符,如+, =或==被视为多态函数,因此根据其参数的类型表现出不同的行为。运算符重载通常只是语法糖。它可以很容易地通过函数调用来模拟。
考虑此操作
add (a, multiply (b,c))
使用运算符重载允许以更简洁的方式编写它,如下所示
a + b * c
(假设该*运算符的优先级高于+.)
运算符重载不仅可以提供美学上的优势,因为语言允许在某些情况下隐式调用运算符。使用运算符重载的问题和批评在于,它允许程序员为运算符赋予完全自由的功能,而没有施加一致性,而一致性可以始终满足用户/阅读者的期望。使用<<
运算符就是这个问题的一个例子。
// The expression
a << 1;
如果a是整型变量,则会返回a的值的两倍,但如果a是输出流,则会将“1”写入它。由于运算符重载允许程序员更改运算符的通常语义,因此通常被认为是谨慎使用运算符重载的良好做法。
重载运算符就是为用户定义类型赋予新的意义。这与定义函数的方式相同。基本语法如下(其中 @ 表示有效运算符)
return_type operator@(parameter_list)
{
// ... definition
}
并非所有运算符都可以重载,不能创建新的运算符,也不能更改运算符的优先级、结合性和元数(例如,! 不能重载为二元运算符)。大多数运算符可以重载为成员函数或非成员函数,但有些运算符必须定义为成员函数。运算符应仅在使用自然且明确无误的情况下重载,并且应按预期执行。例如,将 + 重载为添加两个复数是一个很好的用法,而将 * 重载为将对象推入向量则不被视为好的风格。
- 一个简单的消息头
// sample of Operator Overloading
#include <string>
class PlMessageHeader
{
std::string m_ThreadSender;
std::string m_ThreadReceiver;
//return true if the messages are equal, false otherwise
inline bool operator == (const PlMessageHeader &b) const
{
return ( (b.m_ThreadSender==m_ThreadSender) &&
(b.m_ThreadReceiver==m_ThreadReceiver) );
}
//return true if the message is for name
inline bool isFor (const std::string &name) const
{
return (m_ThreadReceiver==name);
}
//return true if the message is for name
inline bool isFor (const char *name) const
{
return (m_ThreadReceiver==name);// since name type is std::string, it becomes unsafe if name == NULL
}
};
除了必须是成员的运算符之外,运算符可以重载为成员函数或非成员函数。是否将运算符重载为成员由程序员决定。运算符通常在以下情况下重载为成员
- 更改左侧操作数,或者
- 需要直接访问对象的非公共部分。
当运算符被定义为成员时,显式参数的数量减少了一个,因为调用对象被隐式地提供为操作数。因此,二元运算符接受一个显式参数,而一元运算符不接受任何参数。对于二元运算符,左侧操作数是调用对象,并且不会对其进行任何类型的强制转换。这与非成员运算符形成对比,在非成员运算符中,左侧操作数可以被强制转换。
// binary operator as member function
//Vector2D Vector2D::operator+(const Vector2D& right)const [...]
// binary operator as non-member function
//Vector2D operator+(const Vector2D& left, const Vector2D& right)[...]
// binary operator as non-member function with 2 arguments
//friend Vector2D operator+(const Vector2D& left, const Vector2D& right) [...]
// unary operator as member function
//Vector2D Vector2D::operator-()const {...}
// unary operator as non-member function[...]
//Vector2D operator-(const Vector2D& vec) [...]
- +(加法)
- -(减法)
- *(乘法)
- /(除法)
- %(模运算)
作为二元运算符,它们涉及两个参数,这两个参数不必是相同类型。这些运算符可以定义为成员函数或非成员函数。下面是一个示例,说明了对 2D 数学向量类型的加法进行重载。
Vector2D Vector2D::operator+(const Vector2D& right)
{
Vector2D result;
result.set_x(x() + right.x());
result.set_y(y() + right.y());
return result;
}
良好的风格是仅重载这些运算符以执行其惯用的算术运算。由于运算符已被重载为成员函数,因此它可以访问私有字段。
- ^(异或)
- |(或)
- &(与)
- ~(补码)
- <<(左移,插入到流中)
- >>(右移,从流中提取)
所有位运算符都是二元运算符,除了补码运算符,它是单目运算符。应该注意,这些运算符的优先级低于算术运算符,因此如果 ^ 被重载为求幂运算,则 x ^ y + z 可能不会按预期工作。值得特别注意的是移位运算符 << 和 >>。它们已在标准库中重载以与流交互。在将这些运算符重载为与流一起工作时,应遵循以下规则
- 将 << 和 >> 重载为友元(以便它可以访问流的私有变量,该变量按引用传递)
- (输入/输出会修改流,并且不允许复制)
- 运算符应返回接收到的流的引用(以允许链式操作,例如 cout << 3 << 4 << 5)
- 使用 2D 向量的一个例子
friend ostream& operator<<(ostream& out, const Vector2D& vec) // output
{
out << "(" << vec.x() << ", " << vec.y() << ")";
return out;
}
friend istream& operator>>(istream& in, Vector2D& vec) // input
{
double x, y;
// skip opening paranthesis
in.ignore(1);
// read x
in >> x;
vec.set_x(x);
// skip delimiter
in.ignore(2);
// read y
in >> y;
vec.set_y(y);
// skip closing paranthesis
in.ignore(1);
return in;
}
赋值运算符 = 必须是成员函数,并且编译器为用户定义的类提供默认行为,使用其赋值运算符对每个成员进行赋值。对于仅包含变量的简单类来说,这种行为通常是可以接受的。但是,当类包含对外部资源的引用或指针时,应重载赋值运算符(作为一般规则,只要需要析构函数和复制构造函数,就需要赋值运算符),否则,例如,两个字符串将共享相同的缓冲区,更改其中一个将更改另一个。
在这种情况下,赋值运算符应执行两个职责
- 清理对象的旧内容
- 复制另一个对象的资源
对于包含原始指针的类,在进行赋值之前,赋值运算符应该检查自赋值,自赋值通常不会生效(因为当对象的旧内容被擦除时,它们不能被复制回来重新填充对象)。自赋值通常是编码错误的标志,因此对于没有原始指针的类,这种检查通常被省略,因为虽然这个操作会浪费 CPU 周期,但它不会对代码产生其他影响。
- 示例
class BuggyRawPointer { // example of super-common mistake
T *m_ptr;
public:
BuggyRawPointer(T *ptr) : m_ptr(ptr) {}
BuggyRawPointer& operator=(BuggyRawPointer const &rhs) {
delete m_ptr; // free resource; // Problem here!
m_ptr = 0;
m_ptr = rhs.m_ptr;
return *this;
};
};
BuggyRawPointer x(new T);
x = x; // We might expect this to keep x the same. This sets x.m_ptr == 0. Oops!
// The above problem can be fixed like so:
class WithRawPointer2 {
T *m_ptr;
public:
WithRawPointer2(T *ptr) : m_ptr(ptr) {}
WithRawPointer2& operator=(WithRawPointer2 const &rhs) {
if (this != &rhs) {
delete m_ptr; // free resource;
m_ptr = 0;
m_ptr = rhs.m_ptr;
}
return *this;
};
};
WithRawPointer2 x2(new T);
x2 = x2; // x2.m_ptr unchanged.
赋值运算符重载的另一个常见用法是在类的私有部分声明重载,而不定义它。因此任何尝试进行赋值的代码都会在两个方面失败,首先是引用私有成员函数,其次是由于没有有效的定义而无法链接。这适用于需要阻止复制的类,并且通常是在添加私有声明的复制构造函数的情况下完成的。
- 示例
class DoNotCopyOrAssign {
public:
DoNotCopyOrAssign() {};
private:
DoNotCopyOrAssign(DoNotCopyOrAssign const&);
DoNotCopyOrAssign &operator=(DoNotCopyOrAssign const &);
};
class MyClass : public DoNotCopyOrAssign {
public:
MyClass();
};
MyClass x, y;
x = y; // Fails to compile due to private assignment operator;
MyClass z(x); // Fails to compile due to private copy constructor.
关系运算符
[edit | edit source]- == (相等)
- != (不相等)
- > (大于)
- < (小于)
- >= (大于或等于)
- <= (小于或等于)
所有关系运算符都是二元的,应该返回真或假。通常,所有六个运算符都可以基于比较函数或彼此,尽管这永远不会自动完成(例如,重载 > 不会自动重载 < 以给出相反的结果)。但是,在头文件 <utility> 中定义了一些模板;如果包含此头文件,则只需重载运算符 == 和运算符 <,其他运算符将由 STL 提供。
逻辑运算符
[edit | edit source]- ! (非)
- && (与)
- || (或)
逻辑运算符 AND 用于评估两个表达式以获得单个关系结果。该运算符对应于布尔逻辑运算 AND,如果操作数为真,则结果为真,否则为假。以下面板显示了运算符评估表达式的结果。
运算符 ! 是单目运算符,&& 和 || 是二目运算符。需要注意的是,在正常使用中,&& 和 || 具有“短路”行为,其中可能不会评估右操作数,具体取决于左操作数。当重载时,这些运算符获得函数调用优先级,并且此短路行为会丢失。最好不要更改这些运算符。
- 示例
bool Function1();
bool Function2();
Function1() && Function2();
如果 Function1() 的结果为假,则不会调用 Function2()。
MyBool Function3();
MyBool Function4();
bool operator&&(MyBool const &, MyBool const &);
Function3() && Function4()
无论 Function3() 的调用结果如何,Function3() 和 Function4() 都会被调用。这会浪费 CPU 处理能力,更糟糕的是,与默认运算符的预期“短路”行为相比,它可能会产生令人惊讶的意外后果。考虑以下情况
extern MyObject * ObjectPointer;
bool Function1() { return ObjectPointer != null; }
bool Function2() { return ObjectPointer->MyMethod(); }
MyBool Function3() { return ObjectPointer != null; }
MyBool Function4() { return ObjectPointer->MyMethod(); }
bool operator&&(MyBool const &, MyBool const &);
Function1() && Function2(); // Does not execute Function2() when pointer is null
Function3() && Function4(); // Executes Function4() when pointer is null
复合赋值运算符
[edit | edit source]- += (加法赋值)
- -= (减法赋值)
- *= (乘法赋值)
- /= (除法赋值)
- %= (模赋值)
- &= (AND 赋值)
- |= (OR 赋值)
- ^= (XOR 赋值)
- <<= (左移赋值)
- >>= (右移赋值)
复合赋值运算符应该重载为成员函数,因为它们会更改左侧操作数。与所有其他运算符(除基本赋值运算符外)一样,复合赋值运算符必须显式定义,它们不会自动定义(例如,重载 = 和 + 不会自动重载 +=)。复合赋值运算符应该按预期工作:A @= B 应该等效于 A = A @ B。以下是二维数学向量类型的 += 的示例。
Vector2D& Vector2D::operator+=(const Vector2D& right)
{
this->x += right.x;
this->y += right.y;
return *this;
}
自增和自减运算符
[edit | edit source]- ++ (自增)
- -- (自减)
自增和自减有两种形式,前缀 (++i) 和后缀 (i++)。为了区分,后缀版本接受一个哑整数。自增和自减运算符最常被用作成员函数,因为它们通常需要访问类中的私有成员数据。前缀版本通常应该返回对已更改对象的引用。后缀版本应该只返回原始值的副本。在理想情况下,A += 1、A = A + 1、A++、++A 应该都使 A 具有相同的值。
- 示例
SomeValue& SomeValue::operator++() // prefix
{
++data;
return *this;
}
SomeValue SomeValue::operator++(int unused) // postfix
{
SomeValue result = *this;
++data;
return result;
}
通常,为了方便维护,一个运算符是在另一个运算符的基础上定义的,尤其是在函数调用很复杂的情况下。
SomeValue SomeValue::operator++(int unused) // postfix
{
SomeValue result = *this;
++(*this); // call SomeValue::operator++()
return result;
}
下标运算符
[edit | edit source]下标运算符 [ ] 是一个运算符,它可以接受任意数量的参数(与函数调用非常类似),但通常只有一个。它也必须是成员函数(因此它最多只能接受一个显式参数,即索引)。下标运算符并不局限于接受整数索引。例如,std::map 模板的下标运算符的索引与键的类型相同,因此它可能是字符串等。下标运算符通常重载两次;作为非常量函数(用于更改元素时),以及作为常量函数(用于仅访问元素时)。
函数调用运算符
[edit | edit source]函数调用运算符 ( ) 通常被重载以创建行为类似于函数的对象,或者用于具有主要操作的类。函数调用运算符必须是成员函数,但没有其他限制 - 它可以与任何数量的任何类型的参数进行重载,并且可以返回任何类型。一个类也可以对函数调用运算符进行多个定义。
地址运算符、引用运算符和指针运算符
[edit | edit source]这三个运算符,operator&()、operator*() 和 operator->() 可以被重载。通常,这些运算符只针对智能指针或试图模拟原始指针行为的类进行重载。指针运算符 operator->() 有一个额外的要求,即对该运算符的调用的结果必须返回一个指针,或者是一个具有重载 operator->() 的类。通常情况下,A == *&A 应该为真。
注意重载 operator& 会导致未定义的行为
- ISO/IEC 14882:2003,第 5.3.1 节
- 可以获取不完整类型的对象的地址,但如果该对象的完整类型是声明 operator&() 作为成员函数的类类型,则行为未定义(并且不需要诊断)。
- 示例
class T {
public:
const memberFunction() const;
};
// forward declaration
class DullSmartReference;
class DullSmartPointer {
private:
T *m_ptr;
public:
DullSmartPointer(T *rhs) : m_ptr(rhs) {};
DullSmartReference operator*() const {
return DullSmartReference(*m_ptr);
}
T *operator->() const {
return m_ptr;
}
};
class DullSmartReference {
private:
T *m_ptr;
public:
DullSmartReference (T &rhs) : m_ptr(&rhs) {}
DullSmartPointer operator&() const {
return DullSmartPointer(m_ptr);
}
// conversion operator
operator T() { return *m_ptr; }
};
DullSmartPointer dsp(new T);
dsp->memberFunction(); // calls T::memberFunction
T t;
DullSmartReference dsr(t);
dsp = &dsr;
t = dsr; // calls the conversion operator
这些都是非常简化的示例,旨在展示如何重载运算符,而不是 SmartPointer 或 SmartReference 类的完整细节。通常,你不会想在同一个类中重载所有这三个运算符。
逗号运算符
[edit | edit source]逗号运算符() , 可以被重载。语言逗号运算符具有从左到右的优先级,运算符() 具有函数调用优先级,因此请注意,重载逗号运算符有很多陷阱。
- 示例
MyClass operator,(MyClass const &, MyClass const &);
MyClass Function1();
MyClass Function2();
MyClass x = Function1(), Function2();
对于非重载的逗号运算符,执行顺序将是 Function1()、Function2();使用重载的逗号运算符,编译器可以先调用 Function1() 或 Function2()。
成员引用运算符
[edit | edit source]这两个成员访问运算符,operator->() 和 operator->*() 可以被重载。重载这些运算符最常见的用法是定义表达式模板类,这并不是一种常见的编程技巧。显然,通过重载这些运算符,你可以创建一些非常难以维护的代码,因此只有在非常小心的时候才重载这些运算符。
当 -> 运算符应用于类型为 (T *) 的指针值时,语言会解除指针的引用并应用 . 成员访问运算符(因此 x->m 等效于 (*x).m)。但是,当 -> 运算符应用于类实例时,它被调用为单目后缀运算符;它应该返回一个值,该值可以再次应用 -> 运算符。通常,这将是一个类型为 (T *) 的值,如上面 地址运算符、引用运算符和指针运算符 下面的示例所示,但也可能是一个定义了 operator->() 的类实例;语言将根据需要调用 operator->(),直到它得到一个类型为 (T *) 的值。
- new (为对象分配内存)
- new[ ] (为数组分配内存)
- delete (释放对象内存)
- delete[ ] (释放数组内存)
内存管理运算符可以被重载以自定义分配和释放(例如,插入相关的内存头)。它们应该按预期行为,new 应该返回指向堆上新分配对象的指针,delete 应该释放内存,忽略空参数。要重载 new,必须遵循以下规则:
- new 必须是成员函数
- 返回值类型必须是 void*
- 第一个显式参数必须是 size_t 值
要重载 delete,也有一些条件:
- delete 必须是成员函数(并且不能是虚函数)
- 返回值类型必须是 void
- 参数列表只有两种形式可用,并且在一个类中只允许出现其中一种形式:
- void*
- void*, size_t
转换运算符允许类的对象被隐式(强制转换)或显式(类型转换)转换为另一种类型。转换运算符必须是成员函数,并且不应改变被转换的对象,因此应该被标记为常量函数。转换运算符声明的基本语法,以及用于 int 转换运算符的声明如下所示。
operator ''type''() const; // const is not necessary, but is good style
operator int() const;
请注意,函数没有声明返回值类型,可以从转换类型轻松推断出来。在转换运算符的函数头中包含返回值类型是一个语法错误。
double operator double() const; // error - return type included
- ?: (条件运算符)
- . (成员选择运算符)
- .* (指向成员的指针的成员选择运算符)
- :: (作用域解析运算符)
sizeof
(对象大小信息)- typeid (对象类型信息)
- static_cast (类型转换运算符)
- const_cast (类型转换运算符)
- reinterpret_cast (类型转换运算符)
- dynamic_cast (类型转换运算符)
要了解为什么语言不允许重载这些运算符,请阅读 Bjarne Stroustrup 的 C++ 样式和技术常见问题解答中的 "为什么我不能重载点,::,sizeof
等?" ( http://www.stroustrup.com/bs_faq2.html#overload-dot )。