Cpp学习24 运算符重载

铁名_IronName Lv5

在 C++ 中,运算符以函数的形式实现。通过对运算符函数进行函数重载,您可以定义自定义的运算符版本,使其能够处理不同的数据类型(包括您自己编写的类)。使用函数重载来重载运算符的过程称为运算符重载 。

你的重载运算符至少应操作一种程序自定义类型。这样可以确保未来的语言标准不会破坏你的程序。

最佳实践

不修改操作数的运算符(例如算术运算符)通常应该按值返回结果。

修改最左边操作数的运算符(例如前置递增运算符、任何赋值运算符)通常应该按引用返回最左边的操作数。

运算符重载有三种不同的方式:成员函数方式、友元函数方式和普通函数方式。

使用友元函数重载算术运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Cents
{
private:
int m_cents {};

public:
Cents(int cents) : m_cents{ cents } { }

// add Cents + Cents using a friend function
friend Cents operator+(const Cents& c1, const Cents& c2);

int getCents() const { return m_cents; }
};

// note: this function is not a member function!
Cents operator+(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return c1.m_cents + c2.m_cents;
}

友元函数的定义放到类的内部也行。

链式:每个连续的操作都会返回一个 MinMax 对象,该对象将成为下一个运算符的左侧操作数。

使用普通函数重载运算符

由于普通函数和友元函数的工作方式几乎完全相同(只是对私有成员的访问权限级别不同),我们通常不会区分它们。唯一的区别在于,类内部的友元函数声明同时也是一个函数原型。而对于普通函数,你需要提供自己的函数原型(就是说,记得写到头文件里)。

最佳实践

如果可以在不添加额外函数的情况下重载运算符,请优先选择普通函数而不是友元函数。

I/O 操作符重载

1
2
3
4
5
6
7
8
//在类中已声明为友元函数
std::ostream& operator<< (std::ostream& out, const Point& point)
{
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')'; // actual output done here

return out; // return std::ostream so we can chain calls to operator<<
}

任何时候,当我们希望重载的二元运算符能够以这种方式链式调用时,都应该返回左操作数(通过引用)。

在上面的例子中, operator<< 是一个友元,因为它需要直接访问 Point 的成员。但是,如果可以通过 getter 方法访问成员,那么 operator<< 就可以实现为非友元。

1
2
3
4
5
6
7
8
// note that point must be non-const so we can modify the object
std::istream& operator>> (std::istream& in, Point& point)
{
// This version subject to partial extraction issues (see below)
in >> point.m_x >> point.m_y >> point.m_z;

return in;
}

(这个输入有问题!在输入多个值的时候。)
那么我们该如何避免这种情况呢?一种方法是将操作设为事务性的。 事务性操作要么完全成功,要么完全失败——不允许部分成功或部分失败。

以下是 operator>> 的一个替代版本,它会在任何提取失败时将 Point 重置为其默认状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
// note that point must be non-const so we can modify the object
// note that this implementation is a non-friend
std::istream& operator>> (std::istream& in, Point& point)
{
double x{};
double y{};
double z{};

in >> x >> y >> z;
point = in ? Point{x, y, z} : Point{};

return in;
}

处理语义无效的输入,前面有提到。但我不觉得游戏客户端用得到这个。

使用成员函数重载运算符

使用成员函数重载运算符时:左操作数隐式地变为 *this 对象。

如果我们可以将一个运算符重载为友元或成员,我们应该使用哪一个呢?
赋值运算符 (=)、下标运算符 ([])、函数调用运算符 (()) 和成员选择运算符 (->) 必须重载为成员函数,因为该语言要求它们必须如此。
并非所有内容都可以重载为成员函数。我们无法将 operator<< 重载为成员函数,因为重载的运算符必须作为左操作数的成员添加,而 operator<<必须返回std::ostream

经验法则

  • 如果要重载赋值(=)、下标([])、函数调用(())或成员选择(->),请将其作为成员函数。
  • 如果要重载一元运算符,请将其作为成员函数来实现
  • 如果要重载一个不修改其左操作数的二元运算符(例如 operator+),请将其作为普通函数(首选)或友元函数来执行。
  • 如果你要重载一个修改其左操作数的二元运算符,但你不能向左操作数的类定义中添加成员(例如 operator<<,它的左操作数类型为 ostream),那么请将其作为普通函数(首选)或友元函数来执行。
  • 如果你要重载一个修改其左操作数的二元运算符(例如 operator+=),并且你可以修改左操作数的定义,那么请将其作为成员函数来执行。

重载一元运算符 +、- 和 !

由于它们只作用于它们所应用的对象,因此通常一元运算符的重载实现为成员函数。

重载比较运算符

因为比较运算符都是不修改其左操作数的二元运算符,所以我们将把重载的比较运算符设为友元函数。

1
2
3
4
5
6
7
8
9
    friend bool operator== (const Car& c1, const Car& c2);
friend bool operator!= (const Car& c1, const Car& c2);
};

bool operator== (const Car& c1, const Car& c2)
{
return (c1.m_make == c2.m_make &&
c1.m_model == c2.m_model);
}

重载递增和递减运算符

由于递增和递减运算符都是一元运算符,并且它们都会修改操作数,因此最好将它们重载为成员函数。

1
2
3
4
5
6
7
8
9
10
11
Digit& Digit::operator++()
{
// If our number is already at 9, wrap around to 0
if (m_digit == 9)
m_digit = 0;
// otherwise just increment to next number
else
++m_digit;

return *this;
}

在重载时如何区分这两种运算符呢?
编译器会检查重载运算符是否带有 int 类型的参数。如果带有 int 类型的参数,则该运算符是后缀重载;如果没有参数,则该运算符是前缀重载。

1
2
3
4
5
6
7
8
9
10
11
12
// int parameter means this is postfix operator++
Digit Digit::operator++(int)
{
// Create a temporary variable with our current digit
Digit temp{*this};

// Use prefix operator to increment this digit
++(*this); // apply operator

// return temporary result
return temp; // return saved state
}

下标运算符重载

1
2
3
4
5
6
7
8
9
10
11
class IntList
{
private:
int m_list[10]{};

public:
int& operator[] (int index)
{
return m_list[index];
}
};

由于 operator[] 的结果可以用于赋值语句的左侧(例如 list[2] = 3 ),因此 operator[] 的返回值必须是左值。引用始终是左值。

分别定义运算符[]的非 const 版本和 const 版本。非 const 版本用于非常量对象,const 版本用于 const 对象。

实现该函数 const 版本的逻辑。让非常量函数调用常量函数,并使用 const_cast 去掉 const 符号。

1
2
3
4
5
6
7
8
9
10
11
12
13
public:
int& operator[] (int index)
{
// use std::as_const to get a const version of `this` (as a reference)
// so we can call the const version of operator[]
// then const_cast to discard the const on the returned reference
return const_cast<int&>(std::as_const(*this)[index]);
}

const int& operator[] (int index) const
{
return m_list[index];
}

指向对象的指针和重载的运算符[]不要混用。

实际上,您可以定义重载的 operator[] 接受任何您想要的类型值。

重载括号运算符

可以是可以,但是意义不明,连函数名都没有。

玩转函子 functors

Operator() 函数通常也被重载以实现仿函数 (或函数对象 ),仿函数是一种像函数一样工作的类。仿函数相对于普通函数的优势在于,仿函数可以将数据存储在成员变量中(因为它们是类)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

class Accumulator
{
private:
int m_counter{ 0 };

public:
int operator() (int i) { return (m_counter += i); }

void reset() { m_counter = 0; } // optional
};

int main()
{
Accumulator acc{};
std::cout << acc(1) << '\n'; // prints 1
std::cout << acc(3) << '\n'; // prints 4

Accumulator acc2{};
std::cout << acc2(10) << '\n'; // prints 10
std::cout << acc2(20) << '\n'; // prints 30

return 0;
}

函子的优点在于我们可以根据需要实例化任意多个独立的函子对象,并同时使用它们。函子还可以拥有其他成员函数(例如 reset() ),这些函数可以执行一些便捷的操作。

类型转换过载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cents
{
private:
int m_cents{};
public:
Cents(int cents=0)
: m_cents{ cents }
{
}

// Overloaded int cast
operator int() const { return m_cents; }

int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};

重载类型转换必须是非静态成员,并且应该是 const ,以便可以与 const 对象一起使用。

也可以 explicit来禁止隐式调用,类型转换通常应该标记为显式。

最佳实践

尽可能优先使用 转换构造函数,避免重载类型转换。

避免为同一转换同时定义重载类型转换函数和转换构造函数,因为结果不明确。

最佳实践

当您需要定义如何将类型 A 转换为类型 B 时:

  • 如果 B 是可以修改的类类型,最好使用转换构造函数从 A 创建 B。
  • 否则,如果 A 是可修改的类类型,请使用重载类型转换将 A 转换为 B。
  • 否则,请使用非成员函数将 A 转换为 B。

赋值运算符重载

复制赋值运算符 (运算符=)用于将值从一个对象复制到另一个 已存在的对象 。
如果在复制发生之前必须创建一个新对象,则使用复制构造函数(注意:这包括按值传递或返回对象)。如果复制之前不需要创建新对象,则可以使用赋值运算符。

1
Fraction& operator= (const Fraction& fraction);
1
2
3
4
5
6
7
8
9
10
// A simplistic implementation of operator= (see better implementation below)
Fraction& Fraction::operator= (const Fraction& fraction)
{
// do the copy
m_numerator = fraction.m_numerator;
m_denominator = fraction.m_denominator;

// return the existing object so we can chain this operator
return *this;
}

检测和处理自分配

与其他构造函数和运算符一样,您可以通过将复制赋值运算符设为私有或使用 delete 关键字来阻止赋值操作。Fraction(const Fraction &copy) = delete;

如果你想让一个包含 const 成员的类可以赋值(对于所有非 const 成员),你需要显式地重载 operator= 并手动为每个非常量成员赋值

浅拷贝与深拷贝

提供的默认复制构造函数和默认赋值运算符使用一种称为成员级复制(也称为浅拷贝 )的复制方法。这意味着 C++ 会单独复制类的每个成员(使用重载的赋值运算符,以及直接初始化复制构造函数)。当类很简单(例如,不包含任何动态分配的内存)时,这种方法非常有效。

在设计处理动态分配内存的类时,逐成员(浅)复制会给我们带来很多麻烦!这是因为指针的浅复制仅仅复制了指针的地址——它不会分配任何内存,也不会复制指针指向的内容!

解决这个问题的一个方法是对所有非空指针进行深拷贝。 深拷贝会为副本分配内存,然后复制实际值,使副本与源指针位于不同的内存空间。这样,副本和源指针就完全独立,互不影响。要实现深拷贝,我们需要编写自定义的拷贝构造函数和重载赋值运算符。

运算符和函数模板的重载

从头开始编写一个名为 IntArray 整数数组类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <cassert> // for assert

class IntArray
{
private:
int m_length{ 0 };
int* m_array{ nullptr };

public:
explicit IntArray(int length)
: m_length{ length }
{
assert(length > 0 && "IntArray length should be a positive integer");

m_array = new int[static_cast<std::size_t>(m_length)] {};
}

// Copy constructor that does a deep copy
IntArray(const IntArray& array)
: m_length{ array.m_length }
{
// Allocate a new array
m_array = new int[static_cast<std::size_t>(m_length)] {};

// Copy elements from original array to new array
for (int count{ 0 }; count < array.m_length; ++count)
m_array[count] = array.m_array[count];
}

~IntArray()
{
delete[] m_array;
}

// If you're getting crazy values here you probably forgot to do a deep copy in your copy constructor
friend std::ostream& operator<<(std::ostream& out, const IntArray& array)
{
for (int count{ 0 }; count < array.m_length; ++count)
{
out << array.m_array[count] << ' ';
}
return out;
}

int& operator[] (const int index)
{
assert(index >= 0);
assert(index < m_length);
return m_array[index];
}

// Assignment operator that does a deep copy
IntArray& operator= (const IntArray& array)
{
// self-assignment guard
if (this == &array)
return *this;

// If this array already exists, delete it so we don't leak memory
delete[] m_array;

m_length = array.m_length;

// Allocate a new array
m_array = new int[static_cast<std::size_t>(m_length)] {};

// Copy elements from original array to new array
for (int count{ 0 }; count < array.m_length; ++count)
m_array[count] = array.m_array[count];

return *this;
}

};
  • 标题: Cpp学习24 运算符重载
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-14 11:14:08
  • 更新于 : 2026-08-14 12:49:46
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习24/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论