运算符重载是C++的一项强大特性,为自定义类型(如类或结构体)赋予了与内置类型相似的运算符行为。

通过运算符重载,可以使自定义类对象的操作更加直观与简洁,从而提高代码的可读性与可维护性。

本文将系统介绍C++运算符重载的概念、规则、分类及实现方法,并给出相应示例。

1. 运算符重载简介

1.1 概念说明

在C++中,运算符重载(Operator Overloading)允许程序员针对特定类型重新定义已有运算符的行为。例如,通过对“+”号的重载,我们可以直接使用 a + b 来表示两个用户自定义类型对象的相加操作,而不必调用函数 a.add(b)

1.2 重载的目的

重载运算符的主要目的是提高代码的自然度和可读性。对自定义类型而言,使用运算符进行操作往往比调用类成员函数更直观,有助于编写简洁易懂的代码。

2. 重载运算符的基本规则

2.1 可重载与不可重载运算符

几乎所有C++运算符都可被重载,如 +-*/%[]() <<>> 等。但以下运算符不可重载:

  • 作用域解析运算符 ::
  • 成员访问运算符 ..*
  • 条件运算符 ?:
  • sizeof
  • 类型信息运算符(如 typeid

2.2 成员函数与非成员函数重载

运算符重载既可通过类的成员函数实现,也可通过非成员函数(如全局友元函数)实现。通常:

  • 当需要访问类的私有成员或以类对象为左操作数时,使用成员函数更为方便。
  • 当需要对称性(如 a + bb + a)或隐式类型转换时,使用非成员函数往往更灵活。

2.3 运算符优先级与结合性

重载运算符并不会改变该运算符原有的优先级与结合性。也就是说,即使重载了 “+” 运算符,它仍保持与内置类型加法相同的优先级和结合性。

3. 运算符重载的常见分类与示例

3.1 算术运算符

可重载 +-*/% 等用于数学运算的运算符。

示例(复数相加):

class Complex {
public:
    double real, imag;
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // 重载 + 运算符(成员函数版本)
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
};

3.2 关系与比较运算符

可重载 ==!=<><=>= 等,以比较自定义对象的大小或等价性。

示例(复数相等性判断):

class Complex {
public:
    double real, imag;
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    friend bool operator==(const Complex& lhs, const Complex& rhs) {
        return (lhs.real == rhs.real) && (lhs.imag == rhs.imag);
    }
    
    friend bool operator!=(const Complex& lhs, const Complex& rhs) {
        return !(lhs == rhs);
    }
};

3.3 赋值及复合赋值运算符

可重载 =+=-=*=/=%= 等。重载赋值运算符时应小心内存管理,避免浅拷贝问题。

示例(字符串类赋值):

#include <cstring>

class String {
public:
    char* data;
    
    // 构造函数
    String(const char* str = "") {
        data = new char[std::strlen(str) + 1];
        std::strcpy(data, str);
    }
    
    // 拷贝构造函数
    String(const String& other) {
        data = new char[std::strlen(other.data) + 1];
        std::strcpy(data, other.data);
    }
    
    // 赋值运算符
    String& operator=(const String& other) {
        if (this != &other) {
            delete[] data;
            data = new char[std::strlen(other.data) + 1];
            std::strcpy(data, other.data);
        }
        return *this;
    }
    
    // 析构函数
    ~String() {
        delete[] data;
    }
};

3.4 自增自减运算符

包括前置和后置的 ++-- 运算符。前置运算符(如 ++obj)在操作前修改对象,后置运算符(如 obj++)在操作后修改对象。

示例(计数器):

class Counter {
public:
    int count;
    Counter(int c = 0) : count(c) {}
    
    // 前置自增
    Counter& operator++() {
        ++count;
        return *this;
    }
    
    // 后置自增
    Counter operator++(int) {
        Counter temp = *this;
        ++count;
        return temp;
    }
    
    // 前置自减
    Counter& operator--() {
        --count;
        return *this;
    }
    
    // 后置自减
    Counter operator--(int) {
        Counter temp = *this;
        --count;
        return temp;
    }
};

3.5 逻辑与位运算符

可重载 !&&|| 以及 &|^~<<>> 等。请谨慎重载逻辑运算符,因为短路特性难以完全保留。

示例(位域操作):

class BitField {
public:
    unsigned int bits;
    BitField(unsigned int b = 0) : bits(b) {}
    
    BitField operator&(const BitField& other) const {
        return BitField(bits & other.bits);
    }
    
    BitField operator|(const BitField& other) const {
        return BitField(bits | other.bits);
    }
    
    BitField operator^(const BitField& other) const {
        return BitField(bits ^ other.bits);
    }
    
    BitField operator~() const {
        return BitField(~bits);
    }
    
    BitField operator<<(int shift) const {
        return BitField(bits << shift);
    }
    
    BitField operator>>(int shift) const {
        return BitField(bits >> shift);
    }
};

3.6 流输入输出运算符

重载 <<>> 可以方便地输出和输入自定义类型的数据。通常实现为友元函数。

示例(复数的输入输出):

#include <iostream>

class Complex {
public:
    double real, imag;
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
        os << c.real;
        if (c.imag >= 0)
            os << " + " << c.imag << "i";
        else
            os << " - " << -c.imag << "i";
        return os;
    }
    
    friend std::istream& operator>>(std::istream& is, Complex& c) {
        is >> c.real >> c.imag;
        return is;
    }
};

3.7 下标与函数调用运算符

[] 可用于实现类似数组的访问,() 可用于实现函数对象(仿函数)。

示例(下标运算符):

#include <vector>
#include <stdexcept>

class MyArray {
private:
    std::vector<int> data;
public:
    MyArray(int size) : data(size) {}
    
    int& operator[](int index) {
        if (index < 0 || index >= (int)data.size())
            throw std::out_of_range("Index out of range");
        return data[index];
    }
    
    const int& operator[](int index) const {
        if (index < 0 || index >= (int)data.size())
            throw std::out_of_range("Index out of range");
        return data[index];
    }
};

示例(函数调用运算符):

class Adder {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

3.8 成员访问与其他运算符

-> 可被重载以返回指针或代理对象。逗号运算符、newdelete 也可被重载,但不常见且需谨慎使用。

示例(智能指针):

#include <iostream>

class Proxy {
public:
    void display() const {
        std::cout << "Proxy display" << std::endl;
    }
};

class SmartPointer {
private:
    Proxy* ptr;
public:
    SmartPointer() : ptr(new Proxy()) {}
    
    // 拷贝构造函数
    SmartPointer(const SmartPointer& other) : ptr(new Proxy(*other.ptr)) {}
    
    // 赋值运算符
    SmartPointer& operator=(const SmartPointer& other) {
        if (this != &other) {
            delete ptr;
            ptr = new Proxy(*other.ptr);
        }
        return *this;
    }
    
    ~SmartPointer() { delete ptr; }
    
    Proxy* operator->() const {
        return ptr;
    }
};

4. 实现细节与注意事项

4.1 参数传递与返回类型

为提高效率,二元运算符的参数应尽量使用常量引用传递。对于返回类型,除赋值类运算符外,多数运算符返回新对象的值。

4.2 友元函数的使用

当需要访问类的私有成员,或保持操作数对称性时,可将运算符函数声明为类的友元,这样无需通过类的公共接口访问成员数据。

4.3 链式调用

运算符常通过返回引用来支持链式调用。以 a += b += c; 为例,通过让 += 返回引用,可实现连续赋值操作。

4.4 不改变运算符含义与优先级

应保持重载运算符的语义与原有运算符的内在逻辑相一致,不要让 + 用于非加法语义的行为。同时,重载不会改变运算符的优先级和结合性。

5. 示例综合

以下以复数类为例,展示典型运算符重载的实现和使用。

5.1 Complex 类定义

#include <iostream>

class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // 拷贝构造函数
    Complex(const Complex& other) : real(other.real), imag(other.imag) {}
    
    // 赋值运算符
    Complex& operator=(const Complex& other) {
        if (this != &other) {
            real = other.real;
            imag = other.imag;
        }
        return *this;
    }
    
    // 算术运算符重载
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imag - other.imag);
    }
    
    // 非成员友元函数形式的运算符重载(如乘法、比较、输入输出)
    friend Complex operator*(const Complex& lhs, const Complex& rhs) {
        return Complex(lhs.real * rhs.real - lhs.imag * rhs.imag,
                       lhs.real * rhs.imag + lhs.imag * rhs.real);
    }
    friend bool operator==(const Complex& lhs, const Complex& rhs) {
        return lhs.real == rhs.real && lhs.imag == rhs.imag;
    }
    friend bool operator!=(const Complex& lhs, const Complex& rhs) {
        return !(lhs == rhs);
    }
    friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
        os << c.real;
        if (c.imag >= 0)
            os << " + " << c.imag << "i";
        else
            os << " - " << -c.imag << "i";
        return os;
    }
    friend std::istream& operator>>(std::istream& is, Complex& c) {
        is >> c.real >> c.imag;
        return is;
    }
};

5.2 使用示例

#include <iostream>

int main() {
    Complex c1(3.0, 4.0);
    Complex c2(1.5, -2.5);
    
    Complex c3 = c1 + c2; // 使用重载的 + 运算符
    Complex c4 = c1 - c2; // 使用重载的 - 运算符
    Complex c5 = c1 * c2; // 使用重载的 * 运算符
    
    std::cout << "c1: " << c1 << "\n";
    std::cout << "c2: " << c2 << "\n";
    std::cout << "c3: " << c3 << "\n";
    std::cout << "c4: " << c4 << "\n";
    std::cout << "c5: " << c5 << "\n";
    
    if (c1 == c2)
        std::cout << "c1 和 c2 相等。\n";
    else
        std::cout << "c1 和 c2 不相等。\n";
        
    if (c1 != c2)
        std::cout << "c1 和 c2 不相等。\n";
    else
        std::cout << "c1 和 c2 相等。\n";
        
    std::cout << "请输入复数 c6 的实部和虚部(空格分隔): ";
    Complex c6;
    std::cin >> c6;
    std::cout << "您输入的 c6 为: " << c6 << "\n";
    
    return 0;
}

输出示例:

c1: 3 + 4i
c2: 1.5 - 2.5i
c3: 4.5 + 1.5i
c4: 1.5 + 6.5i
c5: 10.5 + -1.5i
c1 和 c2 不相等。
c1 和 c2 不相等。
请输入复数 c6 的实部和虚部(空格分隔): 2.5 3.5
您输入的 c6 为: 2.5 + 3.5i

6. 高级话题

6.1 运算符的优先级和结合性

重载运算符不会改变其原有的优先级和结合性。例如,+ 的优先级高于 =,即使重载了 + 运算符,表达式 c1 = c2 + c3 依然会先执行 c2 + c3,然后将结果赋值给 c1

6.2 运算符链式调用

通过适当的返回类型,可以实现运算符的链式调用。例如,赋值运算符通常返回 *this 的引用,以允许连续赋值操作。

示例:

#include <iostream>

class Number {
public:
    int value;
    Number(int v = 0) : value(v) {}
    
    // 重载 += 运算符
    Number& operator+=(const Number& other) {
        value += other.value;
        return *this;
    }
    
    // 重载 << 运算符以便输出 Number 对象
    friend std::ostream& operator<<(std::ostream& os, const Number& n) {
        os << n.value;
        return os;
    }
};

int main() {
    Number a(1), b(2), c(3);
    a += b += c; // a = a + (b += c)
    std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
    return 0;
}

输出:

a: 4, b: 5, c: 3

6.3 运算符重载与继承

在继承体系中,运算符重载可以与多态性结合使用,但需要注意运算符函数的作用域和访问权限。需要避免尝试将运算符重载为虚函数,因为C++不支持运算符重载的虚拟化。

示例:

#include <iostream>

class Base {
public:
    virtual void print() const {
        std::cout << "Base" << std::endl;
    }
    
    // 基类的 + 运算符
    Base operator+(const Base& other) const {
        std::cout << "Base + Base" << std::endl;
        return Base();
    }
};

class Derived : public Base {
public:
    void print() const override {
        std::cout << "Derived" << std::endl;
    }
    
    // 派生类的 + 运算符
    Derived operator+(const Derived& other) const {
        std::cout << "Derived + Derived" << std::endl;
        return Derived();
    }
};

int main() {
    Base b1, b2;
    Base b3 = b1 + b2;
    b3.print(); // 输出 "Base"
    
    Derived d1, d2;
    Derived d3 = d1 + d2;
    d3.print(); // 输出 "Derived"
    
    return 0;
}

输出示例:

Base + Base
Base
Derived + Derived
Derived

说明:

  1. 运算符重载不能作为虚函数:C++不允许将运算符重载声明为虚函数,因此在基类和派生类中分别实现各自的运算符。
  2. 不同类型的运算符:基类和派生类的 operator+ 是独立的,不存在多态性。

7. 常见问题与注意事项

7.1 避免不必要的重载

并非所有运算符都需要重载。在某些情况下,重载运算符可能会使代码变得复杂或难以理解。只有在运算符的重载能够增加代码的清晰度和可读性时,才应考虑进行重载。

7.2 保持运算符的一致性

重载运算符时,应尽量保持其与内置类型运算符的行为一致。例如,重载 + 运算符应表示“相加”的意思,避免引入与预期行为不符的逻辑。

7.3 实现对称性

对于二元运算符,如果选择使用非成员函数实现,应确保运算符在操作数顺序上的对称性。例如,a + bb + a 应该都能正常工作。

示例:

#include <iostream>

class Number {
public:
    int value;
    Number(int v = 0) : value(v) {}
    
    // 重载 + 运算符(成员函数)
    Number operator+(const Number& other) const {
        return Number(value + other.value);
    }
    
    // 重载 + 运算符(非成员函数)
    friend Number operator+(int lhs, const Number& rhs) {
        return Number(lhs + rhs.value);
    }
    
    // 重载 << 运算符以便输出 Number 对象
    friend std::ostream& operator<<(std::ostream& os, const Number& n) {
        os << n.value;
        return os;
    }
};

int main() {
    Number a(5);
    Number b = a + 10; // 使用成员函数
    Number c = 10 + a; // 使用非成员函数
    std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
    return 0;
}

输出:

a: 5, b: 15, c: 15

7.4 高效的参数传递

对于需要传递对象的运算符重载,应尽量使用常量引用传递,避免不必要的对象拷贝,提高性能。

示例:

class Complex {
public:
    double real, imag;
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // 重载 + 运算符(使用常量引用)
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
};

7.5 注意内存管理

在重载赋值运算符、拷贝构造函数等操作时,必须妥善管理内存,避免内存泄漏或浅拷贝问题。遵循三大法则(拷贝构造函数、拷贝赋值运算符、析构函数)以确保类的正确行为。

示例:

#include <cstring>

class String {
public:
    char* data;
    
    // 构造函数
    String(const char* str = "") {
        data = new char[std::strlen(str) + 1];
        std::strcpy(data, str);
    }
    
    // 拷贝构造函数
    String(const String& other) {
        data = new char[std::strlen(other.data) + 1];
        std::strcpy(data, other.data);
    }
    
    // 赋值运算符
    String& operator=(const String& other) {
        if (this != &other) {
            delete[] data;
            data = new char[std::strlen(other.data) + 1];
            std::strcpy(data, other.data);
        }
        return *this;
    }
    
    // 析构函数
    ~String() {
        delete[] data;
    }
};

7.6 不要改变运算符的优先级和结合性

运算符的优先级和结合性由C++语法定义,重载运算符不会改变这些规则。因此,应设计运算符重载的行为以符合其原有的优先级和结合性,避免引入混淆和错误。

7.7 避免过度重载

过度重载运算符可能会使代码难以理解和维护。应在必要时进行重载,并确保重载后的运算符具有清晰明确的语义。

8. 总结与建议

通过运算符重载,可以使用户自定义类型的使用方式更贴近内置类型,使代码更加直观。编写运算符重载时应注意以下几点:

  • 不要滥用重载,保持逻辑清晰,避免误导。
  • 遵循运算符原有的含义和使用习惯,例如,+ 表示加法。
  • 尽量使用常量引用参数传递,提高性能。
  • 对需要内存管理的类(如动态分配内存)格外谨慎,确保深拷贝与正确的析构。
  • 保持运算符的优先级和结合性,不改变其内在逻辑。
  • 避免过度重载,确保重载后的运算符具有清晰明确的语义。

掌握运算符重载将有助于编写更加优雅、易读且扩展性良好的C++程序。

12-16 04:04