跳转到内容

C++ 编程/运算符/运算符重载

来自 Wikibooks,开放世界中的开放书籍

运算符重载

[编辑 | 编辑源代码]

运算符重载(不太常见的术语为特设多态)是多态性(语言的面向对象特性的组成部分)的一种特殊情况,其中一些或所有运算符,如+, ===被视为多态函数,因此根据其参数的类型表现出不同的行为。运算符重载通常只是语法糖。它可以很容易地通过函数调用来模拟。

考虑此操作

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
    }
};

注意
使用inline关键字在上面的示例中在技术上是多余的,因为在类定义中定义的像这样的函数是隐式内联的。

运算符作为成员函数

[编辑 | 编辑源代码]

除了必须是成员的运算符之外,运算符可以重载为成员函数或非成员函数。是否将运算符重载为成员由程序员决定。运算符通常在以下情况下重载为成员

  1. 更改左侧操作数,或者
  2. 需要直接访问对象的非公共部分。

当运算符被定义为成员时,显式参数的数量减少了一个,因为调用对象被隐式地提供为操作数。因此,二元运算符接受一个显式参数,而一元运算符不接受任何参数。对于二元运算符,左侧操作数是调用对象,并且不会对其进行任何类型的强制转换。这与非成员运算符形成对比,在非成员运算符中,左侧操作数可以被强制转换。

    // 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 可能不会按预期工作。值得特别注意的是移位运算符 << 和 >>。它们已在标准库中重载以与流交互。在将这些运算符重载为与流一起工作时,应遵循以下规则

  1. 将 << 和 >> 重载为友元(以便它可以访问流的私有变量,该变量按引用传递)
  2. (输入/输出会修改流,并且不允许复制)
  3. 运算符应返回接收到的流的引用(以允许链式操作,例如 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;
}
赋值运算符
[编辑 | 编辑源代码]

赋值运算符 = 必须是成员函数,并且编译器为用户定义的类提供默认行为,使用其赋值运算符对每个成员进行赋值。对于仅包含变量的简单类来说,这种行为通常是可以接受的。但是,当类包含对外部资源的引用或指针时,应重载赋值运算符(作为一般规则,只要需要析构函数和复制构造函数,就需要赋值运算符),否则,例如,两个字符串将共享相同的缓冲区,更改其中一个将更改另一个。

在这种情况下,赋值运算符应执行两个职责

  1. 清理对象的旧内容
  2. 复制另一个对象的资源

对于包含原始指针的类,在进行赋值之前,赋值运算符应该检查自赋值,自赋值通常不会生效(因为当对象的旧内容被擦除时,它们不能被复制回来重新填充对象)。自赋值通常是编码错误的标志,因此对于没有原始指针的类,这种检查通常被省略,因为虽然这个操作会浪费 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 )。

华夏公益教科书