文章目录
  • 基本概念
  • 为什么使用操作重载?
  • 注意事项:
  • 输入和输出运算符
  • 重载输出运算符 `<<`
  • 重载输入运算符 `>>`
  • 算术和关系运算符
  • 算术运算符
  • 相等运算符
  • 关系运算符
  • 赋值运算符
  • 基本原则:
  • 示例:
  • 注意事项:
  • 下标运算符
  • 实现原则:
  • 示例:
  • 注意事项:
  • 递增和递减运算符
  • 两种形式:
  • 示例:
  • 注意事项:
  • 成员访问运算符
  • 点运算符 `.`
  • 箭头运算符 `->`
  • 示例:
  • 注意事项:
  • 函数调用运算符
  • Lambda是函数对象
  • 标准库定义的函数对象
  • 可调用对象与 `std::function`
  • 注意事项:
  • 重载、类型转换与运算符
  • 类型转换运算符
  • 避免有二义性的类型转换
  • 函数匹配与重载运算符
  • 注意事项:


基本概念

在C++中,操作重载(Operator Overloading)允许对已有的操作符赋予额外的含义,使得它们可以用于自定义的数据类型。比如,我们可以定义两个对象的加法操作。

类型转换是指将一个类型的实例转换为另一个类型。在C++中,类型转换可以是显式的或隐式的,而类型转换运算符允许控制这个转换过程。

为什么使用操作重载?
  • 可读性:使代码更易读,更接近自然语言。
  • 直观性:对象操作更直观、更符合对象的自然属性。
  • 重用性:在不同上下文中重用相同的符号,但有不同的实现。
注意事项:
  • 重载的操作符应该与原有操作符的语义相似。
  • 不能改变操作符的优先级。
  • 不能创建新的操作符。
  • 某些操作符,如 ::, .*, .?:,不能被重载。

输入和输出运算符

输入(>>)和输出(<<)运算符在C++中用于数据的输入和输出。通常情况下,这些操作符被用于标准的输入输出流(如 std::cinstd::cout)。但是,也可以重载这些运算符以使它们适用于自定义的数据类型。

重载输出运算符 <<

为了使自定义类型能够通过 std::cout 输出,需要重载 << 运算符。这通常通过编写一个非成员函数来实现,该函数接受一个输出流(如 std::ostream)和要输出的对象作为参数。

示例

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}
};

std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
    os << obj.value;
    return os;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在这个例子中,使用 std::cout << myObject; 时,就会调用我们为 MyClass 类型重载的 << 运算符。

重载输入运算符 >>

类似地,为了使自定义类型可以从 std::cin 或其他输入流中接收输入,需要重载 >> 运算符。这也是通过编写一个非成员函数来实现的。

示例

std::istream& operator>>(std::istream& is, MyClass& obj) {
    is >> obj.value;
    return is;
}
  • 1.
  • 2.
  • 3.
  • 4.

通过这种方式,可以使用 std::cin >> myObject; 来给 MyClass 类型的对象输入值。

算术和关系运算符

在C++中,不仅可以重载输入输出运算符,还可以重载各种算术和关系运算符,以便在自定义数据类型上实现特定的操作。

算术运算符

算术运算符包括 +, -, *, /, % 等。通过重载这些运算符,可以定义自定义类型对象间的加法、减法、乘法等操作。

示例

class Complex {
public:
    double real;
    double imag;
    Complex(double r, double i) : real(r), imag(i) {}

    // 重载加法运算符
    Complex operator+(const Complex& rhs) const {
        return Complex(real + rhs.real, imag + rhs.imag);
    }
    // 其他运算符...
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

在这个例子中,定义了一个复数类 Complex 并重载了 + 运算符,使其能够实现两个复数的加法。

相等运算符

相等运算符(==)和不等运算符(!=)用于比较两个对象是否相等或不等。重载这些运算符时,通常需要确保它们的行为符合逻辑和直觉。

示例

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}

    bool operator==(const MyClass& rhs) const {
        return value == rhs.value;
    }
    bool operator!=(const MyClass& rhs) const {
        return !(*this == rhs);
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

在这个例子中,MyClass 类型的对象通过比较它们的 value 成员来判断是否相等。

关系运算符

关系运算符包括 <, >, <=, >= 等。它们用于定义对象间的大小比较逻辑。

示例

bool operator<(const MyClass& lhs, const MyClass& rhs) {
    return lhs.value < rhs.value;
}
// 其他关系运算符...
  • 1.
  • 2.
  • 3.
  • 4.

在这个例子中,< 运算符被重载以比较两个 MyClass 类型对象的 value

赋值运算符

赋值运算符(=)是C++中非常重要的一个运算符,它用于将一个对象的值赋给另一个对象。对于自定义的数据类型,通常需要重载赋值运算符,以确保对象之间的赋值行为正确。

基本原则:
  • 自赋值安全:确保对象自赋值时不会出现问题。
  • 返回引用:通常赋值运算符返回一个指向其左侧操作数的引用。
  • 释放资源:在覆盖旧值之前,释放对象当前占用的资源。
示例:
class MyClass {
public:
    int* data;
    MyClass(int d) : data(new int(d)) {}
    ~MyClass() { delete data; }

    // 重载赋值运算符
    MyClass& operator=(const MyClass& rhs) {
        if (this != &rhs) { // 自赋值检查
            delete data; // 释放现有资源
            data = new int(*rhs.data); // 分配新资源
        }
        return *this;
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

在这个例子中,定义了一个类 MyClass,它有一个动态分配的整数成员。赋值运算符首先检查自赋值,然后释放当前对象所持有的资源,并从右侧操作数复制数据。

注意事项:
  • 在处理动态资源或复杂的内部结构时,正确重载赋值运算符尤为重要。
  • 必须考虑异常安全性和资源泄露的问题。

下标运算符

下标运算符([])在C++中用于访问对象中的元素,如数组或容器类的元素。对于自定义数据类型,可以重载下标运算符,以提供类似数组一样的访问方式。

实现原则:
  • 直观性:确保下标运算符的使用和普通数组或标准库容器的使用方式一致。
  • 支持常量和非常量版本:通常需要提供两个版本的下标运算符——一个用于常量对象,一个用于非常量对象。
示例:
class MyArray {
    int* array;
    int size;
public:
    MyArray(int sz) : size(sz), array(new int[sz]) {}
    ~MyArray() { delete[] array; }

    // 非常量版本
    int& operator[](int index) {
        return array[index];
    }

    // 常量版本
    const int& operator[](int index) const {
        return array[index];
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

在这个例子中,MyArray 类重载了下标运算符,允许用户通过索引访问数组元素,就像使用普通数组一样。

注意事项:
  • 通常需要对索引值进行范围检查,以避免越界错误。
  • 下标运算符应该快速且高效。

递增和递减运算符

递增(++)和递减(--)运算符在C++中用于增加或减少对象的值。这些运算符可以被重载以适用于自定义数据类型,允许对象以特定方式响应递增或递减操作。

两种形式:
  • 前缀版本++obj--obj,先改变对象的值,然后返回改变后的对象。
  • 后缀版本obj++obj--,先返回对象当前的值,然后改变对象的值。
示例:
class Counter {
private:
    int value;
public:
    Counter(int v = 0) : value(v) {}

    // 前缀递增
    Counter& operator++() {
        ++value;
        return *this;
    }

    // 后缀递增
    Counter operator++(int) {
        Counter temp = *this;
        ++(*this);
        return temp;
    }

    // 类似地,可以实现递减运算符
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

在这个例子中,Counter 类重载了递增运算符。前缀版本直接增加值并返回,而后缀版本则首先创建一个当前状态的副本,然后增加值,最后返回副本。

注意事项:
  • 后缀版本通常需要返回对象的副本,这可能涉及额外的开销。
  • 适当重载递增和递减运算符可以提高代码的可读性和直观性。

成员访问运算符

在C++中,成员访问运算符包括两种:点运算符(.)和箭头运算符(->)。点运算符用于访问对象的成员,而箭头运算符用于通过指针访问对象的成员。对于自定义数据类型,尤其是实现了指针类行为的类型,可以重载箭头运算符。

点运算符 .

点运算符用于直接访问对象的成员。这个运算符不能被重载,因为它总是需要直接访问对象的实际成员。

箭头运算符 ->

箭头运算符用于通过对象的指针来访问其成员。如果我们创建了类似指针的对象,比如智能指针,就需要重载这个运算符。

示例:
template <typename T>
class SmartPointer {
private:
    T* ptr;
public:
    SmartPointer(T* p = nullptr) : ptr(p) {}
    ~SmartPointer() { delete ptr; }

    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

在这个例子中,定义了一个简单的智能指针类 SmartPointer。通过重载 -> 运算符,可以通过智能指针访问其指向对象的成员,就像使用普通指针一样。

注意事项:
  • 重载的 -> 运算符必须返回一个对象的指针,或者是另一个定义了 -> 操作的对象。
  • 这种重载通常用于实现智能指针、迭代器等类似指针的对象。

函数调用运算符

函数调用运算符 () 在C++中可以被重载,使得一个对象能像函数一样被调用。这种特性主要用于创建可调用的对象,例如函数对象(functors)或者Lambda表达式。

Lambda是函数对象

Lambda表达式是C++11引入的一个特性,它允许我们定义匿名函数对象。Lambda可以捕获作用域中的变量,并且可以像普通函数一样被调用。

示例

auto sum = [](int a, int b) { return a + b; };
std::cout << sum(3, 4); // 输出 7
  • 1.
  • 2.

在这个例子中,我们定义了一个Lambda表达式来求两个数的和。

标准库定义的函数对象

标准库中提供了许多预定义的函数对象,如 std::plusstd::minus 等,这些都是重载了函数调用运算符的类。

示例

std::plus<int> add;
std::cout << add(3, 4); // 输出 7
  • 1.
  • 2.

在这个例子中,我们使用了标准库中的 std::plus 类来进行加法操作。

可调用对象与 std::function

std::function 是一个模板类,它可以包裹任何可调用对象,比如普通函数、Lambda表达式、函数对象等。

示例

std::function<int(int, int)> add = [](int a, int b) { return a + b; };
std::cout << add(3, 4); // 输出 7
  • 1.
  • 2.

在这个例子中,使用 std::function 来存储一个Lambda表达式。

注意事项:
  • 通过重载函数调用运算符,可以让对象行为类似于函数。
  • Lambda表达式和 std::function 提供了灵活的方式来处理可调用对象。

重载、类型转换与运算符

在C++中,除了重载常用的算术和逻辑运算符外,还可以重载类型转换运算符。这使得我们可以定义对象如何从一种类型转换为另一种类型。

类型转换运算符

类型转换运算符用于将一个对象隐式或显式地转换为另一种类型。这些运算符的重载可以提供更多的控制,确保类型转换按预期进行。

示例

class MyClass {
    int value;
public:
    MyClass(int v) : value(v) {}

    // 转换为int类型的运算符
    operator int() const {
        return value;
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在这个例子中,MyClass 对象可以隐式地转换为 int 类型。

避免有二义性的类型转换

类型转换应该明确无误。如果一个类提供了多个可能的类型转换,可能会导致二义性,这应该尽量避免。

函数匹配与重载运算符

在重载运算符时,应该注意函数匹配的问题。重载应该清晰而明确,不应该引起调用者的困惑或误解。

注意事项:
  • 类型转换运算符应该谨慎使用,以避免引入错误或二义性。
  • 显式关键字(explicit)可以用来防止隐式类型转换,确保转换行为明确。
  • 重载运算符应该保持一致性和直观性,避免过度使用导致代码难以理解。