C++面向对象整理(7)之运算符重载、operator关键字

注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构



提示:本文为 C++ 中 运算符重载 的写法和举例


一、运算符重载

1、运算符重载的定义

  运算符重载允许程序员为自定义数据类型(如类)重新定义或重载运算符的行为。通过这种方式,我们可以使自定义类型的对象像内置类型(如int、float等)一样使用运算符。

当我们定义了一个类,并希望这个类的对象能够使用某个运算符时,我们就可以在那个类中重载那个运算符。重载运算符本质上就是定义一个特殊的成员函数或友元函数,这个函数的名字就是运算符的符号。
运算符重载的格式基本上遵循了函数的定义格式,只不过函数名被替换成了operator后跟要重载的运算符。

ReturnType operator 运算符号(参数){ }

下面,我将以加号+和赋值号=为例,说明如何在C++中重载运算符。

2、加号的重载

加号+的重载
假设我们有一个简单的复数类Complex,我们想要重载加号运算符来让两个复数相加。

class Complex {  
public:  
    double real;  
    double imag;  
  
    // 构造函数  
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}  
  
    // 加号运算符重载,作为成员函数  
    Complex operator+(const Complex& rhs) const {  
        return Complex(real + rhs.real, imag + rhs.imag);  
    }  
  
    // 其他成员函数,如输出等...  
};  
  
int main() {  
    Complex c1(1, 2);  
    Complex c2(3, 4);  
    Complex sum = c1 + c2; // 使用重载的+运算符  
    // ...  
}

在这个例子中,operator+是一个成员函数,它接受一个类型为const Complex&的右操作数rhs。函数返回一个新的Complex对象,其实部和虚部分别是两个操作数的对应部分之和。

也可以将加号运算符+重载为非成员函数(并且作为友元),你需要修改Complex类,将operator+声明为友元,并在类外部定义它。以下是修改后的代码:

class Complex {  
public:  
    double real;  
    double imag;  
  
    // 构造函数  
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}  
  
    // 输出成员函数  
    void print() const {  
        std::cout << "(" << real << ", " << imag << ")" << std::endl;  
    }  
  
    // 声明+运算符重载为非成员函数且为友元  
    friend Complex operator+(const Complex& lhs, const Complex& rhs);  
};  
  
// 非成员函数形式的+运算符重载,作为Complex类的友元  
Complex operator+(const Complex& lhs, const Complex& rhs) {  
    return Complex(lhs.real + rhs.real, lhs.imag + rhs.imag);  
}  
  
int main() {  
    Complex c1(1, 2);  
    Complex c2(3, 4);  
    Complex sum = c1 + c2; // 使用重载的+运算符  
    sum.print(); // 输出结果  
    // ...  
    return 0;  
}

在本例中,operator+现在是一个非成员函数,它接受两个Complex类型的常量引用参数lhs和rhs,并返回一个新的Complex对象,该对象的实部和虚部分别是两个输入对象的实部和虚部之和。

由于operator+需要访问Complex类的私有成员real和imag,我们将其声明为Complex类的友元。这样,operator+就能够直接访问类的私有成员,而不需要通过类的公共接口。

在main函数中,我们可以像之前一样使用+运算符来相加两个Complex对象,并打印出结果。

3、赋值号的重载

赋值号=的重载,注意不写这个的话,编译器也会默认为一个类添加一个赋值运算符 operator= 的重载函数,这被称为合成赋值运算符(synthesized assignment operator)。这个合成的赋值运算符会逐个成员地将右侧对象的成员值赋给左侧对象的对应成员。下面给出对于赋值运算符=的重载的例子 :

class Complex {  
public:  
    double real;  
    double imag;  
  
    // ...其他成员函数...  
  
    // 赋值运算符重载  
    Complex& operator=(const Complex& rhs) {  
        if (this != &rhs) { // 检查自赋值  
            real = rhs.real;  
            imag = rhs.imag;  
        }  
        return *this; // 返回*this以支持链式赋值  
    }  
};  
  
int main() {  
    Complex c1(1, 2);  
    Complex c2(3, 4);  
    c1 = c2; // 使用重载的=运算符  
    // ...  
}

在这个例子中,operator=返回对当前对象的引用(Complex&),这使得我们可以进行链式赋值,例如c1 = c2 = c3;。同时,我们在函数体内首先检查自赋值的情况,以避免不必要的操作,并防止潜在的错误。

重载运算符时需要注意以下几点:

不是所有运算符都可以被重载。例如,.、.*、::、sizeof、?:运算符不能被重载
重载的运算符必须至少有一个操作数是用户自定义类型
重载运算符不能改变运算符的优先级和结合性。
重载运算符通常应该保持其原有的语义,避免引起混淆。
对于成员函数形式的重载,左操作数总是类的实例(*this),而右操作数作为参数传递。
对于非成员函数形式的重载(通常作为友元函数),可以自由地定义左右操作数。
通过合理地重载运算符,我们可以使自定义类型的使用更加直观和方便。

二、动态分配的内存时赋值=的运算符重载

如果类中有动态分配的内存(即使用 new 在堆上创建的对象),并且没有正确地管理这些动态内存(例如,在赋值运算符或析构函数中释放它们),那么使用编译器提供的默认赋值运算符可能会导致问题。

当两个对象它们都拥有指向相同堆内存区域的指针,那么析构函数可能会多次释放同一块内存,导致未定义行为(通常是程序崩溃)。如果在赋值操作中没有正确地复制堆内存(即只复制了指针而没有复制所指向的内容),那么当原始对象被销毁并释放其内存时,新对象将包含一个指向已经被释放的内存的悬垂指针(dangling pointer)。
为了避免这些问题,需要为包含动态分配内存的类提供自定义的赋值运算符。这个自定义的赋值运算符应该执行深拷贝(deep copy),即要复制指针所指向的内容而不是复制一个指针。

下面是一个简单的例子,展示了如何为包含动态分配内存的类提供自定义赋值运算符:

class MyClass {  
public:  
    MyClass(int size) {  
        data = new int[size];  
        dataSize = size;  
    }  
  
    ~MyClass() {  
        delete[] data;  
    }  
  
    MyClass& operator=(const MyClass& other) {  
        if (this != &other) { // 检查自赋值  
            delete[] data; // 释放当前对象的内存  
            dataSize = other.dataSize;  
            data = new int[dataSize]; // 分配新内存  
            std::copy(other.data, other.data + dataSize, data); // 复制数据  
        }  
        return *this; // 返回当前对象的引用以支持链式赋值  
    }  
  
private:  
    int* data;  
    int dataSize;  
};

在这个例子中,MyClass 的赋值运算符首先检查是否发生了自赋值,然后释放当前对象的内存,并根据 other 对象的大小分配新的内存。最后,它使用 std::copy 来复制数据。这样,每次赋值时都会创建新的堆内存区域,避免了重复释放和悬垂指针的问题。

总结可以重载的运算符

可以重载的运算符相当丰富,包括一元运算符、二元运算符以及某些特殊运算符。以下是一些常用的可重载的运算符:

一元运算符:
+(正号)
-(负号)
*(指针解引用)
&(取地址)
~(按位取反)
!(逻辑非)
++(自增)
--(自减)
->(指向成员的指针)
newdelete(尽管它们不是真正的运算符,但它们的行为可以通过重载操作符函数来定制)

二元运算符:
+(加法)
-(减法)
*(乘法)
/(除法)
%(取模)
+=(加等于)
-=(减等于)
*=(乘等于)
/=(除等于)
%=(模等于)
<<(左移)
>>(右移)
<<=(左移等于)
>>=(右移等于)
==(等于)
!=(不等于)
>(大于)
<(小于)
>=(大于等于)
<=(小于等于)
&(按位与)
|(按位或)
^(按位异或)
&&(逻辑与)
||(逻辑或)
=(赋值)
->*(成员访问)
,(逗号)

特殊运算符:
()(函数调用)
[](下标访问)
new[]delete[](数组形式的newdeleteoperator 关键字后的类型转换函数,例如 operator int()

需要注意的是,不是所有的运算符都可以被重载。C++标准规定了几种运算符不能重载,包括:

.(成员访问)
.*(成员指针访问)
::(作用域解析)
sizeof(获取对象或类型大小)
? :(条件运算符,即三目运算符)
03-25 04:51