C++ 运算符重载详解

1. 运算符重载简介

所谓重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。同样运算符重载(Operator Overloading)可以让同一个运算符可以有不同的功能。

  • 可以对 int、float、string 等不同类型数据进行操作
    << 既是位移运算符,又可以配合 cout 向控制台输出数据

也可以自定义运算符重载:

class Complex
{
public:
    Complex();
    Complex(double real, double imag);
    Complex operator+(const Complex &a) const;
    void display() const; 
private:
    double m_real;
    double m_imag;
};

// ...

// 实现运算符重载
Complex Complex::operator+(const Complex &A) const{
    Complex B;
    B.m_real = this->m_real + A.m_real;
    B.m_imag = this -> m_imag + A.m_imag;
    return B;
    // return Complex(this->m_real + A.m_real, this->m_imag + A.m_imag);
}

int main(){
    Complex c1(4.3, 5.8);
    Complex c2(2.7, 3.7);
    Complex c3;
    c3 = c1 + c2;   // 运算符重载
    c3.display();
    return 0;
}
运算结果
7 + 9.5i

运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数,它本质上是函数重载。

c3 = c1 + c2;
实际上通过调用成员函数 operator+(),会转换为下面的形式:
c3 = c1.operator+(c2);

全局范围内重载运算符

运算符重载函数不仅可以作为类的成员函数,还可以作为全局函数。

复数加法运算通过全局范围内重载 + 实现:

class Complex{
    // ...
    friend complex operator+(const complex &A, const complex &B);
};

// 全局函数
Complex operator+(const Complex &A, const Complex &B)
{
    return Complex(A.m_real + B.m_real, A.m_imag + B.m_imag);   // 访问了Complex 的 private 成员变量,需要声明为友元函数
}

2. 运算符重载时要遵循的规则

  • 能够重载的运算符

    + - * / % ^ & | ~ ! = < > += = = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , -> - > () [] new new[] delete delete[]

    长度运算符 sizeof、条件运算符: ?、成员选择符. 和域解析运算符::不能被重载。

  • 重载不能改变运算符的优先级和结合律

  • 重载不会改变运算符的用法,即操作数个数、位置都不会改变

  • 运算符重载函数不能有默认的参数,因为这改变了运算符操作数个数

  • 运算符重载函数既可作为类成员函数,也可为全局函数,注意全局函数如何要访问类对象的私有成员,需要声明为类的友元

  • 箭头运算符->、下标运算符[]、函数调用运算符()、赋值运算符 =,只能以成员函数的形式重载。

3. 重载运算符实现形式的选择

重载运算符可以通过成员函数和全局函数(友元)来实现

转换构造函数

Complex c1(25, 25);
Complex c2 = c1 + 15.6;
Complex c3 = 28.23 + c1;    // 要以全局函数实现重载

这几行代码都可以顺利运行,说明 Complex 对象可以和 double 类型对象相加。其实,编译器在检测到 Complex 和 double(小数默认为 double 类型)相加时,会先尝试将 double 转换为 Complex,或者反过来将 Complex 转换为 double(只有类型相同的数据才能进行 + 运算),如果都转换失
败,或者都转换成功(产生了二义性),才报错。实际上,上述两行加法代码会转换为:

Complex c2 = operator+(c1,Complex(15.6));
Complex c3 = operator+(Complex(28.23),c1);  

Complex(double real)在作为普通构造函数的同时,还能将 double 类型转换为 Complex 类型,集合了“构造函数”和“类型转换”的功能,所以被称为「转换构造函数」。换句话说,转换构造函数用来将其它类型(可以是 bool、int、double等基本类型,也可以是数组、指针、结构体、类等构造类型)转换为当前类类型。

以全局函数形式重载

以全局函数的形式重载了 +、-、*、/、==、!=,这样做是为了保证这些运算符能够被对称的处理。

如果将 operator+定义为成员函数,根据“+ 运算符具有左结合性”这条原则,Complex c2 = c1 + 15.6;会被转换为下面的形式:

Complex c2 = c1.operator+(Complex(15.6));

但是对于 Complex c3 = 28.23 + c1,编译器会尝试转换为不同形式:

Complex c3 = (28.23).operator+(c1);

显然这是错误的。

以成员函数的形式重载

以成员函数的形式重载了 +=、-=、 *=、/=。

运算符重载的初衷是给类添加新的功能,方便类的运算,它作为类的成员函数是理所应当的, 是首选的。不过类的成员函数不能对称处理数据,运算符的第一个运算对象不会出现类型转换(要类的对象才能调用类的成员函数)。

C++ 规定,箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。

4. 重载 >> 和 <<(输入输出运算符)详解

C++ 标准库已对 左移运算符 << 和 >> 右移运算符进行了承载,如果我们定义新的类型需要输入输出运算符去处理,需要再进行重载。

重载运算符 >>

以全局函数的形式重载>>,使它能够读入两个 double 类型的数据,并分别赋值给复数的实部和虚部:

istream & operator>>(istream &in, Complex &A)   // 友元
{
    in >> A.m_real >> A.m_image;
    return in;
}

之所以返回 istream 类 对象的引用,是为了能够连续读取复数,让代码书写更加漂亮,例如:

Complex c1,c2;
cin >> c1 >> c2;

如果不返回引用,需要一个一个读取:

cin >> c1;
cin >> c2;

实际上,上述 >> 运算符会被转换成如下形式:

operator<<(cin, c);

重载运算符 <<

ostream & operator<<(ostream &out, complex &A){
    out << A.m_real <<" + "<< A.m_imag <<" i ";
}

5. 重载 [] 下标运算符

下标运算符 [] 必须以成员函数的形式进行重载。

int& Array::operator[](int i){
    return m_p[i];
}

const int & Array::operator[](int i) const{
    return m_p[i];
}

当 Array 是 const 对象时,如果没有提供 const 版本的 operator[],会报错。

6. 重载 ++ 和 -- 自增自减运算符

class Stopwatch{
    // ... 秒表类
public:
    Stopwatch operator++();     // ++i,前置形式 
    Stopwatch operator++(int);  // i++,后置形式
    Stopwatch run();    // 运行
private:
    int m_min;  // 分钟
    int m_sec;  // 秒钟
};

Stopwatch Stopwatch::run(){
    ++m_sec; 
    if(m_sec == 60){
        m_min++;
        m_sec = 0;
    } 
    return *this;
}

Stopwatch Stopwatch::operator++(){
    return run();
}

Stopwatch Stopwatch::operator++(int n){
    Stopwatch s = *this;    // i++, 先返回对象,再对象自增,所以需要将对象保存
    run();
    return s;
}

函数中参数 n 是没有任何意义的,它的存在只是为了区分是前置形式还是后置形式。

7. 重载 new 和 delete 运算符

内存管理运算符 new、new[]、delete 和 delete[] 也可以进行重载,其重载形式既可以是类的成员函数,也可以是全局函数。一般情况下,内建的内存管理运算符就够用了,只有在需要自己管理内存时才会重载。

// 成员函数
void * className::operator new( size_t size ){ 
    //TODO:
}

void className::operator delete( void *ptr){ 
    //TODO:
}

// 全局函数
void * operator new( size_t size ){ 
    //TODO:
}

void operator delete( void *ptr){ 
    //TODO:
}

在重载 new 或 new[] 时,无论是作为成员函数还是作为全局函数,它的第一个参数必须是 size_t 类型。size_t 表示的是要分配空间的大小,对于 new[] 的重载函数而言,size_t 则表示所需要分配的所有空间的总和。

size_t 在头文件 中被定义为 typedef unsigned int size_t;,也就是无符号整型。

当然,重载函数也可以有其他参数,但都必须有默认值,并且第一个参数的类型必须是 size_t。

如果类中没有定义 new 和 delete 的重载函数,那么会自动调用内建的 new 和 delete 运算符。

8. 重载()(强制类型转换运算符)

类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当 重载后,(类型名)对象这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名(),即变成对运算符函数的调用。

class Complex{
public:
    // ...
    operator double()
    {
        return real;
    }
private:
    double m_real;
    double m_imag;
};

int main()
{
    Complex c(1.2,3.4);
    cout << (double)c << endl;  // 1.2
    double n = 2 + c;   // 等价于 double n = 2 + c.operator double()
}

9. 运算符重载总结

注意事项:

  • 重载后运算符的含义应该符合原有用法习惯。例如重载 + 运算符,完成的功能就应该类似于做加法,在重载的+ 运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
  • C++ 规定,运算符重载不改变运算符的优先级。
  • 以下运算符不能被重载:.、.*、::、? :、sizeof。
  • 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。
  • 运算符的实质是将运算符重载为一个函数,使用运算符的表达式就会被解释为对重载函数的调用。

  • 运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。(友元)

  • 运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。

  • 必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。

  • <<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。

  • 类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。

  • 自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。

  • 运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。

上一篇:Python基于socket模块实现UDP通信功能示例


下一篇:小米官网的css3导航菜单