博主头像
小雨淅沥

Some things were meant to be.

OOP课程笔记:第六章 运算符重载

第六章 运算符重载

1.函数的重载

所谓函数的重载是指完成不同功能的函数可以具有相同的函数名

C++的编译器是根据 函数的实参 来确定应该调用哪一个函数,仅返回值不同时,不能定义为重载函数

2.运算符的重载

重载的概念

在下例中,编译系统中的运算符本身不能做这种运算

若使下式可以运算,必须重新定义运算符,这种重新定义的过程成为运算符的重载

char str[4], c1[2]="a", c2[2]="b";
str= c1 + c2;
class A{
    float x, y;
public:
    A(float a = 0, float b = 0){
        x = a;
        y = b;
    }
};
void main(void){
    A a(2, 3), b(3, 4), c;
    c = a + b; // 两对象不能使用+,必须重新定义+
}

运算符重载就是赋予已有的运算符多重含义

C++ 通过重新定义运算符,使它能够用于特定类的对象执行特定的功能


多态性的体现

运算符的重载从另一个方面体现了OOP 技术的多态性, 且同一运算符根据不同的运算对象可以完成不同的操作。

为了重载运算符,必须定义一个函数,并告诉编译器, 遇到这个 重载运算符 就调用该函数,由这个函数来完成该运算符应该完成的操作。

这种函数称为运算符重载函数,它通常是 类的成员函数或者是 友元函数运算符的操作数通常也应该是类的对象

ClassName operator <operator> (<args>){;}
A operator + (A&); //重载了类 A 的 + 运算符

operator 是定义运算符重载函数的关键字,与其后的运算符一起构成函数名

#include <iostream>
using namespace std;
class A{
    int i;
public:
    A(int a=0) { i = a; }
    void Show(void) { cout << "i=" << i << endl; }
    void AddA(A &a, A &b) // 利用函数进行类之间的运算
    { i = a.i + b.i; }
    A operator+(A &a) // 重载运算符+
    { A t; t.i = i + a.i; return t; }
};
int main(){
    A a1(10), a2(20), a3;
    a1.Show();
    a2.Show();
    a3 = a1 + a2; // 重新解释了加法,可以直接进行类的运算
    a3.AddA(a1, a2); // 调用专门的功能函数
    a3.Show();
    return 0;
}

重载运算符限制

重载运算符只能是两种参数情况:没有参数带有一个参数

对于只有一个操作数的运算符 如++ 在重载这种运算符时通常不能有参数;

而对于有二个操作数的运算符只能带有一个参数 。 这参数可以是对象对象的引用 或其它类型的参数 。

在 C++ 中不允许重载有三个操作数的运算符

只能对 C++ 中已定义了的运算符进行重载,当重载一个运算符时该运算符的优先级和结合律是不能改变的

不能进行重载的运算符有6个,见下表

名称符号
成员访问运算符.
成员指针访问运算符.*->*
作用域运算符::
大小运算符sizeof
三目运算符? :

3.前置后置运算符重载

只具有一个操作数的运算符为单目运算符,最常用的为 ++--

可以看出,虽然运算后对象的值一致,但先自加后自加的重载运算符函数的返回值不一致, 必须在重载时予以区分

<type> operator ++() {;}         // 前置运算
<type> operator ++(int) {;}     // 后置运算

下面是一段可以直接运行的代码

注意返回 *this 会返回一个对象的副本,并非原来的对象

根据 return 返回的值可以区分前置和后置运算符

#include <iostream>
using namespace std;

class incount
{
    int c1, c2;
public:
    incount(int a = 0, int b = 0)
    {
        c1 = a;
        c2 = b;
    }
    void Show(void) { cout << "c1=" << c1 << '\t' << "c2=" << c2 << endl; }
    incount operator++()
    {
        c1++;
        c2++;
        return *this; // 返回拷贝的副本,并非本身
    } // 前置
    incount operator++(int) // 后置
    {
        incount c;
        c.c1 = c1;
        c.c2 = c2;
        c1++;
        c2++;
        return c;
    }
};
int main()
{
    incount ic1(10, 20), ic2(100, 200), ic3, ic4;
    ic1.Show();
    ic2.Show();
    ic3 = ++ic1; // ic3=ic1.operator++()
    ic4 = ic2++; // ic4=ic4.operator++(3)
    ic3.Show();
    ic4.Show();
    return 0;
}

4.运算符重载为友元函数

如果运算符重载为成员函数时,是由一个操作数调用另一个操作数

用成员函数实现运算符的重载时运算符的左操作数为当前对象,并且要用到隐含的 this 指针

运算符重载函数不能定义为静态的成员函数,因为静态的成员函数中没有 this 指针

c = a + b;                 // 左操作数是a
c = a.operator+(b);     // 本质上是这样

c = ++a;
c = a.operator++();

c += a;
c = a.operator++(a);

友元函数是在类外的普通函数,与一般函数的区别是可以调用类中的私有或保护数据

将运算符的重载函数定义为友元函数,参与运算的对象全部成为函数参数

c = a + b     // 实际上是 c = operator+(a, b)
c = ++a;     // 实际上是c = operator++(a)
c += a;        // 实际上是 operator+=(c, a);

对双目运算符,友元函数有 2 个参数;对单目运算符,友元函数有 1 个参数。

有些运算符不能重载为友元函数: , ()[]

使用格式如下

friend <type> operator<operator> (<args>) {;}
c = a + b;    // c = operator+(a, b);
friend A operator+ (A &a, A &b) {;}
class A{
    int i;
public:
    A(int a=0) { i = a; }
    void Show(void) { cout << "i=" << i << endl; }
    friend A operator+(A &, A &); // 友元函数,两个参数,为引用
};
A operator+(A &a, A &b){
    A t;
    t.i = a.i + b.i;
    return t;
}
int main(){
    A a1(10), a2(20), a3;
    a1.Show(); // 相当于 a3=operator+(a1,a2)
    a2.Show();
    a3 = a1 + a2; // 重新解释了加法,可以直接进行类的运算
    a3.Show();
    return 0;
}

前后单目运算符

// ++为前置运算时 它的运算符重载函数的一般格式为:
A operator ++(A &a) {;}
//++为后置运算时 它的运算符重载函数的一般格式为:
A operator ++(A & a,int) {;}

对双目运算符,重载为成员函数时,仅一个参数,另一个被隐含;重载为友元函数时,有两个参数,没有隐含参数。

一般来说,单目运算符最好被重载为成员函数双目运算符最好被重载友元函数

5.赋值运算符的重载

同类型的对象间可以相互赋值,等同于对象的各个成员的一一赋值。

A a(2,3), b;
b = a;

但当对象的成员中使用了动态的数据类型时(用 new 开辟空间),不能直接相互赋值,否则在程序的执行期间会出现运行错误

class A {
    char *ps;
public: 
    A() { ps = 0; }
    A(char *s) { 
        ps = new char[strlen(s) + 1]; 
        strcpy(ps, s); 
    }
    ~A() { 
        if (ps) delete ps; 
    }
    void Show(void) { 
        cout << ps << endl; 
    }
};
void main(void) {
    A s1("China!"), s2("Computer!"); 
    s1.Show(); 
    s2.Show();
    s2 = s1;  //相当于 s2.ps = s1.ps
    s1.Show(); 
    s2.Show();
    // 析构会出现问题
}

这个需要使用到重载赋值运算符

class A{
    char *ps;
public:
    A() { ps = 0; }
    A(char *s){
        ps = new char[strlen(s) + 1];
        strcpy(ps, s);
    }
    ~A() { if (ps) delete ps;}
    void Show(void) { cout << ps << endl; }
    A &operator=(A &b);        // 声明重载运算符
};
A &A::operator=(A &b) // 重载赋值运算符
{
    if (ps) delete[] ps;    // 先删除左操作的指针
    if (b.ps){
        ps = new char[strlen(b.ps) + 1];    // 创建一个新长度的空间
        strcpy(ps, b.ps);
    }
    else
        ps = 0;
    return *this;    // 返回这个类本身
}


void main(void){
    A s1("China!"), s2("Computer!");
    s1.Show();
    s2.Show();
    s2 = s1;
    s1.Show();
    s2.Show();
}

6.选择是否成员重载

赋值 = 、下标 [ ]、函数调用 ( ) , 成员函数访问箭头运算符 ->必须是成员函数

复合赋值运算符 += 一般应该是成员,但并非必须

改变对象状态的运算符或者与给定类型密切相关的运算符,如自增、自减和解引用运算符 *,通常应该是成员

具有对称性的运算符可能转换两个操作数中的任何一个,如算术、关系和位运算符等,通常应该是友元函数

重载移位运算符和用于对象的 I/O 操作时,左操作数是标准库流对象,右操作数才是类类型的对象, 只能用友元函数

如果左操作数不是对象时,只能通过友元函数来实现

class Complex{
public:
    Complex operator+(double);
    Complex operator=(Complex);
    friend Complex operator +(double,Complex); // 友元重载,左操作数是一个double类型 
};
int main(){
    Complex c1,c2,c3;
    c3 = c1 + 1.1;    // 先调用成员函数+,再调用成员函数=
    c3 = 1.1 + c1;    // 先调用友元函数+,再调用成员函数=
}

7.用户定义的类型转换

补充:static_cast 运算符

在C++中,static_cast 是一种显式类型转换运算符,用于在编译时进行类型转换(相对于 dynamic_cast的运行时类型转换)。它是C++中最常用的类型转换方式之一,比C风格的强制类型转换更安全,因为它会进行一些编译时检查。

主要作用:

  1. 基本数据类型之间的转换(如 intdoublefloatint 等)
  2. 指针/引用在类层次结构中的向上转换(派生类指针/引用 → 基类指针/引用)
  3. void指针与其他指针类型的互转
  4. 显式调用构造函数或转换函数

其他类型转换为本类对象

带单个实参的构造函数提供了参数类型的对象到本类型对象的转换

下例中的构造函数提供了从 int 类型转为 A 类型的转换

class A{
int x;
public:
    A(int b): x(b){;} 
};

本类的对象转换为其他类型

类型转换运算符 operator <type> ()

只能用成员函数重载,不带参数,不必指定返回类型

对当前对象实施类型转换操作,产生 <type> 类型的新对象

#include <iostream>
#include <cassert>
using namespace std;

class MinInt {
    char m;
public:
    MinInt(int val = 0) {    // int类型转换为MinInt,也是构造函数
        assert(val >= 0 && val <= 100); // 要求取值范围在0~100之间
        m = static_cast<char>(val);
    }
    operator int() {        // MinInt转为int
        return static_cast<int>(m);
    }
};

int main()
{
    MinInt mi(10), num;
    num = mi + 20;
    int val = num;   // 将num自动转换为int,并赋值给val
    cout << mi << "\t" << num << "\t" << val;
}

显式类型转换运算符

C++11 标准引入了显式类型转换运算符 explicit ,编译器不会将显式类型转换运算符用于隐式类型转换

class SmallInt {
    char val = 0;
public:
    SmallInt(char v) : val(v) {}
    explicit operator int() const { return val; }
};

SmallInt si = 3;  // 隐式构造函数调用
si + 3;          // 错误:需要隐式类型转换(但operator int是explicit的)
static_cast<int>(si) + 3;  // 正确:显式类型转换

8.函数调用运算符

如果类重载了函数调用运算符 operator(),就可以像用函数一样使用该类的对象

函数调用运算符是唯一不限制操作数个数的运算符

函数调用运算符必须是成员函数

一个类可以定义多个不同版本的调用运算符,但是必须在参数个数或类型上有所区别


函数对象

如果一个类定义了 函数调用运算符 ,那么该类的对象称为函数对象 ,或者仿函数 (functor)

因为可以调用这种对象,在代码层面感觉跟函数的使用一样,但本质上并非函数

函数对象类除了 operator() 之外还可以包含其他成员,其中的数据成员可以用于定制调用运算符中的操作

在定义对象时, 初始化对象的数据成员,这些数据成员的状态就成为函数对象的初始状态 。因此,可以借由函数对象产生多个功能类似却不相同的仿函数实例

使用函数调用运算符的方式是令对象作用于一个实参列表 ,形式类似于函数调用 struct


函数对象示例1

通过带状态的函数对象,设定不同税率的计算

在下例子中,创建了两个对象用于计算税,但具体的税率并不相同,这是通过创建对象时决定的

class Tax {
    double rate;
    int base;
public:
    Tax(double r, int b) : rate(r), base(b) {}
    double operator()(double money) {
        return (money - base) * rate;
    }
};

int main() {
    Tax high(0.4, 30000);        // 第一种税率计算
    Tax middle(0.25, 20000);      // 第二种税率计算
    cout << "tax over 3w: " << high(38900) << endl;
    cout << "tax over 2w: " << middle(27500) << endl;
    return 0;
}

函数对象示例2

带状态的函数对象,设定不同的输出流对象和分隔符

#include <iostream>
#include <string>
using namespace std;
class PrintString {
    ostream &os;  // 输出流的引用(可以是 cout、cerr 或任何 ostream)
    char sep;     // 分隔符
public:
    // 构造函数,默认使用 cout 和空格分隔符
    PrintString(ostream &o = cout, char c = ' ') : os(o), sep(c) {}

    // 重载 () 运算符,使其可以像函数一样调用
    void operator()(const string& str) { 
        os << str << sep;  // 输出字符串 + 分隔符
    }
};
int main() {
    PrintString welcomeMsg;       // 默认使用 cout 和 ' ' 分隔符
    welcomeMsg("Welcome!");       // 输出 "Welcome! "

    PrintString errMsg(cerr, '\n'); // 使用 cerr 和换行符 '\n'
    errMsg("Error!");             // 输出 "Error!\n"

    return 0;
}
OOP课程笔记:第六章 运算符重载
https://rainerseventeen.cn/index.php/Code-Basic/17.html
本文作者 Rainer
发布时间 2025-10-18
许可协议 CC BY-NC-SA 4.0

评论已关闭