11.1 运算符重载

C++ 允许将运算符(如 +, -, *, /, <<, >>, == 等)应用于类对象,就像它们是内置类型(如 intdouble)一样。这种特性称为**运算符重载 (Operator Overloading)**。

目的:

运算符重载的主要目的是提高代码的可读性和直观性。通过重载运算符,我们可以让我们自己定义的类(比如表示复数、向量、时间或字符串的类)能够使用熟悉的运算符进行操作,使得代码更接近自然语言或数学表示法。

例如,假设你有一个表示二维向量的类 Vector。如果没有运算符重载,你可能需要这样写代码来计算两个向量的和:

1
2
3
Vector v1(1.0, 2.0);
Vector v2(3.0, 4.0);
Vector sum = v1.add(v2); // 使用名为 add 的成员函数

通过重载 + 运算符,你可以写出更自然、更简洁的代码:

1
2
3
Vector v1(1.0, 2.0);
Vector v2(3.0, 4.0);
Vector sum = v1 + v2; // 使用重载的 + 运算符

如何实现:

运算符重载是通过编写特殊的运算符函数 (Operator Functions) 来实现的。运算符函数的名称格式为 operator 关键字后跟要重载的运算符符号。例如,重载加法运算符 + 的函数名为 operator+,重载小于运算符 < 的函数名为 operator<

运算符函数可以被定义为:

  1. 类的成员函数 (Member Function):

    • 当运算符函数是成员函数时,它的第一个操作数隐式地是调用该函数的对象(通过 this 指针访问)。
    • 对于二元运算符(如 +, -, *),成员函数只需要一个显式参数(代表右操作数)。
    • 对于一元运算符(如 - (负号), ++ (前缀)),成员函数没有显式参数
  2. 非成员函数 (Non-member Function):

    • 通常将非成员运算符函数声明为类的**友元 (friend)**,以便它可以访问类的私有成员。
    • 对于二元运算符,非成员函数需要两个显式参数(分别代表左操作数和右操作数)。
    • 对于一元运算符,非成员函数需要一个显式参数

基本语法 (以二元运算符 + 为例):

作为成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
public:
// ... 其他成员 ...

// 声明重载的 + 运算符 (成员函数)
// other 是右操作数对象
MyClass operator+(const MyClass& other) const;
};

// 定义
MyClass MyClass::operator+(const MyClass& other) const {
MyClass result;
// 实现加法逻辑,通常涉及 this 对象和 other 对象
// result.data = this->data + other.data; // 假设有 data 成员
return result;
}

// 使用
MyClass obj1, obj2;
MyClass sum = obj1 + obj2; // 实际调用 obj1.operator+(obj2)

作为非成员函数 (通常是友元):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
// ... 私有成员 ...
public:
// ... 其他公有成员 ...

// 声明友元函数来重载 + 运算符
friend MyClass operator+(const MyClass& left, const MyClass& right);
};

// 定义 (非成员函数,不需要 MyClass::)
MyClass operator+(const MyClass& left, const MyClass& right) {
MyClass result;
// 实现加法逻辑,访问 left 和 right 对象的成员 (需要友元权限访问私有成员)
// result.data = left.data + right.data; // 假设有 data 成员
return result;
}

// 使用
MyClass obj1, obj2;
MyClass sum = obj1 + obj2; // 实际调用 operator+(obj1, obj2)

注意事项:

  • 不能创建新的运算符: 你只能重载 C++ 中已有的运算符,不能发明新的运算符(比如 **##)。
  • 不能改变运算符的优先级或结合性: 重载 +* 后,* 的优先级仍然高于 +
  • 不能改变运算符的操作数个数: 不能将二元运算符重载为一元运算符,反之亦然。
  • 至少一个操作数是用户定义类型: 不能为两个内置类型(如 intint)重载运算符。运算符重载必须涉及至少一个类对象(或枚举类型)。
  • 某些运算符不能重载:
    • . (成员访问运算符)
    • .* (成员指针访问运算符)
    • :: (作用域解析运算符)
    • sizeof (大小运算符)
    • ?: (条件运算符)
    • typeid (运行时类型识别运算符)
    • static_cast, dynamic_cast, const_cast, reinterpret_cast (类型转换运算符,虽然 operator type() 形式的转换函数可以定义)

运算符重载是 C++ 提供的一个强大工具,可以使自定义类的使用更加自然和方便。然而,也应谨慎使用,避免过度或不直观的重载,以免降低代码的可读性。接下来的章节将通过具体的例子来演示如何重载不同的运算符。

11.2 计算时间:一个运算符重载示例

为了具体说明运算符重载的过程和用法,让我们创建一个简单的 Time 类来表示时间(小时和分钟),并为其重载一些运算符。

基础 Time 类:

首先,我们定义一个基础的 Time 类,包含小时和分钟,一个构造函数来初始化时间,以及一个 show() 方法来显示时间。

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
// time.h -- Time 类定义
#ifndef TIME_H_
#define TIME_H_

#include <iostream>

class Time {
private:
int hours;
int minutes;

public:
// 构造函数,默认值为 0 时 0 分
Time(int h = 0, int m = 0);
// 添加分钟数的方法 (内部会处理进位)
void AddMin(int m);
// 添加小时数的方法
void AddHr(int h);
// 重置时间为 0 时 0 分
void Reset(int h = 0, int m = 0);
// 显示时间
void Show() const;
};

#endif // TIME_H_
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
// time.cpp -- Time 类方法实现
#include "time.h"

Time::Time(int h, int m) {
hours = h;
minutes = m;
// 可以添加一些验证逻辑,例如确保分钟数小于60
}

void Time::AddMin(int m) {
minutes += m;
hours += minutes / 60; // 整数除法,计算增加的小时数
minutes %= 60; // 取模,得到剩余的分钟数
}

void Time::AddHr(int h) {
hours += h;
}

void Time::Reset(int h, int m) {
hours = h;
minutes = m;
}

void Time::Show() const {
std::cout << hours << " hours, " << minutes << " minutes";
}

这个基础类允许我们创建 Time 对象并对其进行一些基本操作,但还不能直接使用 + 等运算符。

11.2.1 添加加法运算符

我们希望能够像 total = time1 + time2; 这样将两个 Time 对象相加。为此,我们需要重载 + 运算符。我们将它实现为 Time 类的成员函数

time.h 中添加声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// filepath: d:\ProgramData\files_Cpp\250424\time.h
// ... (包含 guards 和 iostream) ...

class Time {
private:
int hours;
int minutes;

public:
Time(int h = 0, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
void Show() const;

// 重载加法运算符 (+) 作为成员函数
// 参数 t 代表加号右边的 Time 对象
// const 关键字表示这个函数不会修改调用它的对象 (加号左边的对象)
// 返回一个新的 Time 对象作为结果
Time operator+(const Time & t) const;
};

// ... (endif) ...

time.cpp 中添加定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
// filepath: d:\ProgramData\files_Cpp\250424\time.cpp
#include "time.h"

// ... (构造函数和其他方法的定义) ...

// 定义重载的加法运算符
Time Time::operator+(const Time & t) const {
Time sum; // 创建一个临时的 Time 对象来存储结果
sum.minutes = minutes + t.minutes; // 将两个对象的分钟数相加
sum.hours = hours + t.hours + sum.minutes / 60; // 将小时数相加,并加上分钟进位的小时
sum.minutes %= 60; // 调整结果的分钟数
return sum; // 返回结果对象
}

使用重载的 + 运算符:

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
// main.cpp -- 使用 Time 类和重载的 +
#include <iostream>
#include "time.h"

int main() {
Time planning(2, 40); // 2 小时 40 分钟
Time coding(5, 55); // 5 小时 55 分钟
Time fixing(1, 30); // 1 小时 30 分钟
Time total;

std::cout << "Planning time = ";
planning.Show();
std::cout << std::endl;

std::cout << "Coding time = ";
coding.Show();
std::cout << std::endl;

// 使用重载的 + 运算符
std::cout << "Calculating total time...\n";
total = planning + coding + fixing; // 相当于 (planning.operator+(coding)).operator+(fixing)

// 显示总时间
std::cout << "Total time = ";
total.Show();
std::cout << std::endl;

return 0;
}

编译和运行:
(需要将 main.cpptime.cpp 一起编译链接)

输出:

1
2
3
4
Planning time = 2 hours, 40 minutes
Coding time = 5 hours, 55 minutes
Calculating total time...
Total time = 10 hours, 5 minutes

可以看到,通过重载 + 运算符,我们可以用非常自然的方式将 Time 对象相加。

11.2.2 重载限制

重载运算符时必须遵守一些规则和限制:

  1. 不能创建新运算符: 只能重载 C++ 已有的运算符。
  2. 不能改变运算符优先级和结合性: * 总是优先于 +
  3. 不能改变运算符操作数个数: 二元运算符(如 +)必须接受两个操作数,一元运算符(如 - (负号))必须接受一个操作数。
  4. 至少一个操作数是用户定义类型: 不能为两个 int 重载 + 运算符。重载必须涉及至少一个类对象(或枚举)。
  5. 特定运算符不能重载:.::sizeof?: 等。
  6. 保持直观性: 重载应该符合运算符的通常含义。例如,用 + 来表示两个对象的相减会非常令人困惑。虽然语法上允许,但这是不良实践。

11.2.3 其他重载运算符

我们可以为 Time 类重载更多的运算符,使其功能更完善。

重载减法运算符 (-) 作为成员函数:

1
2
3
4
5
6
7
8
9
10
11
// filepath: d:\ProgramData\files_Cpp\250424\time.h
// ... existing code ...
class Time {
// ... existing members ...
public:
// ... existing methods ...
Time operator+(const Time & t) const;
// 声明重载减法运算符 (-)
Time operator-(const Time & t) const;
};
// ... existing code ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// filepath: d:\ProgramData\files_Cpp\250424\time.cpp
// ... existing code ...

// 定义重载的减法运算符
Time Time::operator-(const Time & t) const {
Time diff;
int tot1, tot2;
// 将时间都转换为总分钟数进行计算
tot1 = t.minutes + 60 * t.hours;
tot2 = minutes + 60 * hours;
// 确保结果非负 (简单处理,实际可能需要更复杂的逻辑)
if (tot2 < tot1) {
std::cerr << "Warning: subtraction result is negative. Resetting to 0.\n";
diff.hours = diff.minutes = 0;
} else {
diff.minutes = (tot2 - tot1) % 60;
diff.hours = (tot2 - tot1) / 60;
}
return diff;
}

重载乘法运算符 (*) - 时间乘以一个因子:

假设我们想计算 Time 对象乘以一个 double 因子(例如,将时间放大 1.5 倍)。这个运算符的操作数类型不同(Timedouble),可以作为成员函数或非成员函数实现。这里我们作为成员函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
// filepath: d:\ProgramData\files_Cpp\250424\time.h
// ... existing code ...
class Time {
// ... existing members ...
public:
// ... existing methods ...
Time operator+(const Time & t) const;
Time operator-(const Time & t) const;
// 声明重载乘法运算符 (*)
Time operator*(double mult) const;
};
// ... existing code ...
1
2
3
4
5
6
7
8
9
10
11
12
13
// filepath: d:\ProgramData\files_Cpp\250424\time.cpp
// ... existing code ...

// 定义重载的乘法运算符
Time Time::operator*(double mult) const {
Time result;
// 将时间转换为总分钟数,乘以因子,然后转换回小时和分钟
long totalminutes = hours * 60 + minutes;
totalminutes *= mult; // 乘以因子
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.cpp (续)
#include <iostream>
#include "time.h"

int main() {
Time t1(2, 30); // 2 hours, 30 minutes
Time t2(1, 45); // 1 hours, 45 minutes
Time diff, product;
double factor = 1.5;

std::cout << "t1 = "; t1.Show(); std::cout << std::endl;
std::cout << "t2 = "; t2.Show(); std::cout << std::endl;

diff = t1 - t2; // 使用重载的 -
std::cout << "t1 - t2 = "; diff.Show(); std::cout << std::endl;

product = t1 * factor; // 使用重载的 *
std::cout << "t1 * " << factor << " = "; product.Show(); std::cout << std::endl;

// 注意:我们没有定义 double * Time,所以下面的写法会报错
// product = factor * t1; // 错误! 需要非成员函数或转换函数

return 0;
}

输出 (续):

1
2
3
4
t1 = 2 hours, 30 minutes
t2 = 1 hours, 45 minutes
t1 - t2 = 0 hours, 45 minutes
t1 * 1.5 = 3 hours, 45 minutes

这个例子展示了如何通过重载运算符,让自定义的 Time 类能够以更自然、更符合数学直觉的方式进行运算。下一节将讨论友元函数,特别是如何使用友元函数来重载像 << 这样的输出运算符,以及处理像 double * Time 这样的运算顺序问题。

11.3 友元

通常,类的私有成员(private)只能被该类的成员函数访问。这是 C++ 实现封装和数据隐藏的关键机制。然而,在某些特殊情况下,允许特定的非成员函数其他类访问一个类的私有成员会非常方便。C++ 提供了友元 (friend) 机制来实现这种受控的访问。

什么是友元?

友元是 C++ 中的一种机制,它允许一个类授予非成员函数另一个类访问其 privateprotected 成员的权限。被授予权限的函数或类被称为该类的友元

注意: 友元关系是单向的,并且不能被继承。如果类 A 将函数 func() 声明为友元,func() 可以访问 A 的私有成员,但这并不意味着 A 可以访问 func() 的内部(如果 func 是另一个类的成员),也不意味着 A 的派生类会自动将 func() 视为友元。

11.3.1 创建友元

要将一个函数或另一个类声明为当前类的友元,需要在当前类的定义内部使用 friend 关键字进行声明。

1. 友元函数 (Friend Function):

友元函数可以是普通的非成员函数,也可以是另一个类的成员函数。

  • 声明普通非成员函数为友元:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class MyClass {
    private:
    int secret;
    public:
    MyClass(int s = 0) : secret(s) {}
    // 在类定义内部声明友元函数
    friend void showSecret(const MyClass& obj);
    };

    // 定义友元函数 (注意:它不是成员函数,没有 MyClass::)
    void showSecret(const MyClass& obj) {
    // 因为是 MyClass 的友元,所以可以访问其私有成员 secret
    std::cout << "The secret is: " << obj.secret << std::endl;
    }

    int main() {
    MyClass myObj(42);
    showSecret(myObj); // 调用友元函数
    return 0;
    }
  • 声明另一个类的成员函数为友元: (需要注意声明顺序和前向声明)

    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
    // 前向声明 ClassB,因为 ClassA 的友元声明中用到了它
    class ClassB;

    class ClassA {
    private:
    int dataA;
    public:
    ClassA(int d = 0) : dataA(d) {}
    // 声明 ClassB 的成员函数 memberB 为友元
    friend void ClassB::memberB(const ClassA& a);
    };

    class ClassB {
    public:
    // ClassB 的成员函数,它将是 ClassA 的友元
    void memberB(const ClassA& a);
    };

    // 定义 ClassB::memberB
    void ClassB::memberB(const ClassA& a) {
    // 可以访问 ClassA 的私有成员 dataA
    std::cout << "Accessing ClassA data from ClassB: " << a.dataA << std::endl;
    }

    int main() {
    ClassA objA(10);
    ClassB objB;
    objB.memberB(objA); // 调用 ClassB 的成员函数,该函数是 ClassA 的友元
    return 0;
    }

2. 友元类 (Friend Class):

一个类可以将另一个整个类声明为友元。这样,友元类的所有成员函数都可以访问声明它为友元的那个类的 privateprotected 成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Storage {
private:
int value;
friend class Controller; // 声明 Controller 为友元类
public:
Storage(int v = 0) : value(v) {}
};

class Controller {
public:
void manipulateStorage(Storage& s, int newValue) {
// 因为 Controller 是 Storage 的友元,可以访问其私有成员 value
s.value = newValue;
std::cout << "Storage value changed to: " << s.value << std::endl;
}
};

int main() {
Storage myStorage(5);
Controller myController;
myController.manipulateStorage(myStorage, 99); // Controller 的方法可以修改 Storage 的私有数据
return 0;
}

11.3.2 常用的友元:重载 << 运算符

运算符重载最常见的应用之一就是重载**输出运算符 <<**,以便能够直接使用 cout 来打印对象的信息,例如 cout << myTimeObject;

为什么通常需要友元?

考虑 cout << myTimeObject; 这个表达式。

  • 它实际上是调用一个形式为 operator<<(cout, myTimeObject) 的函数。
  • 这个运算符的左操作数cout(一个 ostream 类型的对象),右操作数是我们要打印的对象(比如 Time 类型的对象)。

如果我们尝试将 operator<< 定义为 Time 类的成员函数,那么它的调用形式会是 myTimeObject.operator<<(cout)。这意味着 myTimeObject 必须是左操作数,而 cout 是右操作数,即 myTimeObject << cout。这显然不符合我们习惯的用法。

因此,operator<< 必须被重载为非成员函数。但是,这个非成员函数通常需要访问类的私有数据成员(如 Time 类的 hoursminutes)来打印它们。这就使得将 operator<< 声明为类的友元函数成为最自然、最常用的解决方案。

Time 类重载 <<:

  1. time.h 中声明友元函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // filepath: d:\ProgramData\files_Cpp\250424\time.h
    // ... (包含 guards 和 iostream) ...

    class Time {
    // ... private members ...
    public:
    // ... constructors and other methods ...
    Time operator+(const Time & t) const;
    Time operator-(const Time & t) const;
    Time operator*(double mult) const;

    // 声明友元函数 operator<<
    // 第一个参数是 ostream 对象的引用 (如 cout)
    // 第二个参数是要打印的 Time 对象的 const 引用
    // 返回 ostream 对象的引用,以支持链式输出 (cout << t1 << t2)
    friend std::ostream & operator<<(std::ostream & os, const Time & t);

    // 不再需要 Show() 方法,因为 << 提供了更好的方式
    // void Show() const;
    };

    // ... (endif) ...
  2. time.cpp 中定义友元函数: (注意:没有 Time:: 前缀)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // filepath: d:\ProgramData\files_Cpp\250424\time.cpp
    #include "time.h" // 确保包含了 time.h

    // ... (构造函数和其他方法的定义) ...

    // 定义友元函数 operator<<
    std::ostream & operator<<(std::ostream & os, const Time & t) {
    // 因为是友元,可以访问 t 的私有成员 hours 和 minutes
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os; // 返回 ostream 引用
    }

    // 如果删除了 Show(),需要移除它的定义
    // void Time::Show() const { ... }
  3. 使用重载的 <<:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // main.cpp -- 使用重载的 <<
    #include <iostream>
    #include "time.h"

    int main() {
    Time t1(3, 45);
    Time t2(2, 15);
    Time sum = t1 + t2;

    // 使用重载的 << 运算符输出
    std::cout << "Time t1: " << t1 << std::endl;
    std::cout << "Time t2: " << t2 << std::endl;
    std::cout << "Sum: " << sum << std::endl;

    // 链式输出
    std::cout << "t1 + t2 = " << t1 + t2 << std::endl;

    return 0;
    }

输出:

1
2
3
4
Time t1: 3 hours, 45 minutes
Time t2: 2 hours, 15 minutes
Sum: 6 hours, 0 minutes
t1 + t2 = 6 hours, 0 minutes

处理 double * Time (友元函数方式):

在上一节中,我们定义了 Time operator*(double mult) const; 作为成员函数,可以处理 time * double 的情况,但不能处理 double * time。我们可以通过添加一个非成员友元函数来解决这个问题。

  1. time.h 中添加友元声明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // filepath: d:\ProgramData\files_Cpp\250424\time.h
    // ... existing code ...
    class Time {
    // ... existing members ...
    public:
    // ... existing methods ...
    Time operator*(double mult) const; // 处理 time * double

    // 友元函数处理 double * time
    friend Time operator*(double m, const Time & t);
    friend std::ostream & operator<<(std::ostream & os, const Time & t);
    };
    // ... existing code ...
  2. time.cpp 中添加友元定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // filepath: d:\ProgramData\files_Cpp\250424\time.cpp
    // ... existing code ...

    // 定义友元函数 operator* (double * Time)
    // 通常可以简单地调用已有的成员函数版本
    Time operator*(double m, const Time & t) {
    // return t.operator*(m); // 直接调用成员函数
    return t * m; // 或者更简洁地使用已重载的 Time * double 运算符
    }

    std::ostream & operator<<(std::ostream & os, const Time & t) {
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os;
    }

现在,double * time 的运算也可以正常工作了:

1
2
3
4
5
6
7
8
// main.cpp
Time t1(2, 0);
double factor = 2.5;
Time product1 = t1 * factor; // 调用成员函数 t1.operator*(factor)
Time product2 = factor * t1; // 调用友元函数 operator*(factor, t1)

std::cout << product1 << std::endl; // 输出: 5 hours, 0 minutes
std::cout << product2 << std::endl; // 输出: 5 hours, 0 minutes

总结:

  • 友元(函数或类)被授予访问声明其为友元的类的私有和保护成员的权限。
  • 使用 friend 关键字在类定义内部声明友元。
  • 友元对于重载某些运算符(尤其是需要访问私有成员且不能作为成员函数的运算符,如 <<)非常有用。
  • 友元也用于实现需要紧密协作但又不适合放在同一个类中的功能。
  • 虽然友元打破了纯粹的封装,但它提供了一种受控的、明确的方式来在必要时绕过访问限制。应谨慎使用,避免滥用。

11.4 重载运算符:作为成员函数还是非成员函数

在重载运算符时,一个关键的决定是将其实现为类的成员函数还是非成员函数(通常是友元)。这个选择会影响函数的调用方式、参数列表以及某些情况下的行为。

核心区别回顾:

  • 成员函数:

    • 通过类的对象调用。
    • 左操作数隐式地是调用该函数的对象(通过 this 指针访问)。
    • 对于二元运算符,它只需要一个显式参数(右操作数)。
    • 对于一元运算符,它没有显式参数。
    • 可以直接访问类的 privateprotected 成员。
    • 示例:obj1 + obj2 调用 obj1.operator+(obj2)
  • 非成员函数:

    • 独立于类定义之外(但通常在同一个头文件或源文件中)。
    • 所有操作数都必须作为显式参数传递。
    • 对于二元运算符,它需要两个显式参数(左、右操作数)。
    • 对于一元运算符,它需要一个显式参数。
    • 如果需要访问类的 privateprotected 成员,必须被声明为该类的**友元 (friend)**。
    • 示例:obj1 + obj2 调用 operator+(obj1, obj2)

选择指南:

以下是一些通用指南和特定运算符的惯例:

  1. 必须是成员函数的运算符:
    以下运算符只能通过成员函数进行重载:

    • = (赋值运算符)
    • [] (下标运算符)
    • () (函数调用运算符)
    • -> (成员访问运算符)
    • 任何类型转换运算符 (如 operator int())
  2. 通常作为成员函数的运算符 (修改对象状态):
    对于那些通常会修改其左操作数对象状态的运算符,将它们实现为成员函数通常更自然。这包括:

    • 复合赋值运算符: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

      • 例如,time1 += time2; 直观地表示修改 time1。实现为 time1.operator+=(time2) 很合适。
    • 递增/递减运算符: ++, -- (前缀和后缀)

      • 例如,++myCounter;myCounter++; 修改 myCounter 对象。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 示例:重载 += 作为成员函数
      class Time {
      // ... private members ...
      public:
      // ... other methods ...
      Time& operator+=(const Time& t); // 返回引用以支持链式赋值
      };

      Time& Time::operator+=(const Time& t) {
      // 将 t 加到 *this 对象上
      this->minutes += t.minutes;
      this->hours += t.hours + this->minutes / 60;
      this->minutes %= 60;
      return *this; // 返回修改后的对象自身的引用
      }
  3. 通常作为非成员函数 (通常是友元) 的运算符:
    对于那些不修改操作数状态(而是创建一个新值)或者需要对称处理操作数(允许不同类型的左操作数)的运算符,通常将它们实现为非成员函数(通常是友元)。

    • 二元算术运算符: +, -, *, /, %

      • 虽然 + 也可以作为成员函数(如 11.2 节所示),但作为非成员友元函数可以提供更好的对称性。例如,如果类允许从 doubleTime 的隐式转换(通常不推荐,但可能存在),那么友元函数 operator+(const Time&, const Time&) 可以处理 time + timetime + doubledouble + time(通过转换),而成员函数只能处理 time + timetime + double
      • 对于像 double * Time 这样的混合类型运算,如果想让 double 作为左操作数,则必须使用非成员函数(如 11.3 节所示)。
    • 关系运算符: ==, !=, <, >, <=, >=

      • 将它们实现为非成员友元可以更容易地处理对称性,并可能允许混合类型的比较(如果定义了适当的转换或重载版本)。
    • 位运算符: &, |, ^

    • 逻辑运算符: &&, || (较少重载)

    • 输入/输出运算符: <<, >>

      • 必须是非成员函数,因为左操作数是 ostreamistream 对象,而不是你的类的对象。它们几乎总是需要成为友元以访问私有数据进行读写。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 示例:重载 == 作为非成员友元函数
      class Time {
      // ... private members ...
      public:
      // ... methods ...
      friend bool operator==(const Time& t1, const Time& t2);
      // ... other friends ...
      };

      bool operator==(const Time& t1, const Time& t2) {
      return (t1.hours == t2.hours && t1.minutes == t2.minutes);
      }

总结:

运算符类型 推荐实现方式 原因
=, [], (), ->, type() 必须是成员函数 C++ 语言规定
+=, -=, *=, etc. 成员函数 通常修改左操作数对象的状态
++, -- 成员函数 修改对象状态
+, -, *, /, % 非成员友元 (或成员) 对称性,允许混合类型左操作数 (如果需要),通常不修改操作数
==, !=, <, >, <=, >= 非成员友元 (或成员) 对称性
<<, >> 必须是非成员友元 左操作数是流对象 (ostream, istream),需要访问私有成员
一元 +, -, *, &, !, ~ 成员或非成员友元 成员函数无参数,非成员函数一个参数

选择依据:

  • 强制要求: 某些运算符必须是成员函数。
  • 左操作数类型: 如果左操作数必须是特定类型(如 ostream),则必须使用非成员函数。
  • 状态修改: 如果运算符主要目的是修改左操作数的状态,成员函数通常更自然。
  • 对称性/类型转换: 如果希望运算符对操作数顺序具有对称性,或者希望允许对左操作数进行隐式类型转换(需谨慎),非成员函数(通常是友元)是更好的选择。

在实践中,对于常见的二元运算符如 +, -, *, /,一种常见的模式是:

  1. 复合赋值运算符(如 +=, -=)实现为成员函数
  2. 将对应的二元运算符(如 +, -)实现为非成员友元函数,并在其内部调用复合赋值运算符来完成实际工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 示例模式
class Time {
// ...
public:
Time& operator+=(const Time& t); // 成员函数
friend Time operator+(const Time& t1, const Time& t2); // 非成员友元
// ...
};

Time& Time::operator+=(const Time& t) {
// ... 实现 += 逻辑 ...
return *this;
}

// operator+ 调用 operator+=
Time operator+(const Time& t1, const Time& t2) {
Time sum = t1; // 创建一个副本
sum += t2; // 使用成员函数 +=
return sum; // 返回结果
}

这种模式可以减少代码重复。

11.5 再读重载:一个矢量类

本节我们将通过定义一个表示二维矢量(或向量)的类,进一步探讨运算符重载的应用。矢量既可以用直角坐标 (x, y) 表示,也可以用极坐标 (大小/模, 角度) 表示。我们的 Vector 类将能够存储这两种表示,并允许用户在它们之间切换。

11.5.1 使用状态成员

为了同时支持直角坐标和极坐标,我们的 Vector 类需要包含相应的成员:

  • x: x 分量
  • y: y 分量
  • mag: 矢量的大小(模)
  • ang: 矢量的角度

同时,我们需要一个状态成员来指示当前对象是以哪种模式表示的(直角坐标或极坐标),以及一些方法来根据一种表示计算另一种表示。

vector.h (类定义)

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
// filepath: d:\ProgramData\files_Cpp\250424\vector.h
#ifndef VECTOR_H_
#define VECTOR_H_

#include <iostream>

namespace VECTOR { // 将类放入命名空间

class Vector {
public:
// 枚举类型,用于表示模式:直角坐标(RECT) 或 极坐标(POL)
enum Mode {RECT, POL};
private:
double x; // x 分量
double y; // y 分量
double mag; // 矢量的大小 (模)
double ang; // 矢量的角度 (弧度)
Mode mode; // 当前模式 (RECT 或 POL)

// 私有辅助函数,用于根据一种表示计算另一种表示
void set_mag(); // 根据 x, y 计算 mag
void set_ang(); // 根据 x, y 计算 ang (弧度)
void set_x(); // 根据 mag, ang 计算 x
void set_y(); // 根据 mag, ang 计算 y

public:
// 构造函数
Vector(); // 默认构造函数
// 根据指定模式和值构造
Vector(double n1, double n2, Mode form = RECT);
// 重置矢量
void reset(double n1, double n2, Mode form = RECT);
~Vector(); // 析构函数

// 获取各分量的值
double xval() const { return x; }
double yval() const { return y; }
double magval() const { return mag; }
double angval() const { // 返回角度 (度)
// 将弧度转换为度
if (ang == 0.0 && x == 0.0 && y == 0.0) return 0.0; // 处理零向量
else if (ang == 0.0 && x > 0.0) return 0.0; // x轴正方向
else if (ang == 0.0 && x < 0.0) return 180.0; // x轴负方向 (atan2会处理)
else return ang * 180.0 / 3.141592653589793; // 假设 M_PI 不可用
}

// 设置模式
void polar_mode(); // 设置为极坐标模式
void rect_mode(); // 设置为直角坐标模式

// --- 运算符重载 ---
// 加法: Vector + Vector
Vector operator+(const Vector & b) const;
// 减法: Vector - Vector
Vector operator-(const Vector & b) const;
// 取反: -Vector
Vector operator-() const;
// 乘法: double * Vector (友元) 或 Vector * double (成员或友元)
Vector operator*(double n) const;

// --- 友元函数 ---
// 乘法: double * Vector
friend Vector operator*(double n, const Vector & a);
// 输出: cout << Vector
friend std::ostream & operator<<(std::ostream & os, const Vector & v);
};

} // namespace VECTOR
#endif // VECTOR_H_

vector.cpp (类实现)

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// filepath: d:\ProgramData\files_Cpp\250424\vector.cpp
#include <cmath> // 为了使用 sqrt, atan, atan2, sin, cos
#include "vector.h" // 包含类定义

namespace VECTOR {

// 角度转换常量 (如果 cmath 没有定义 M_PI)
const double Rad_to_deg = 180.0 / 3.141592653589793;

// --- 私有辅助函数 ---
void Vector::set_mag() {
mag = std::sqrt(x * x + y * y);
}

void Vector::set_ang() {
if (x == 0.0 && y == 0.0)
ang = 0.0;
else
ang = std::atan2(y, x); // atan2 处理所有象限
}

void Vector::set_x() {
x = mag * std::cos(ang);
}

void Vector::set_y() {
y = mag * std::sin(ang);
}

// --- 公有方法 ---
Vector::Vector() { // 默认构造函数
x = y = mag = ang = 0.0;
mode = RECT;
}

// 根据模式构造
Vector::Vector(double n1, double n2, Mode form) {
mode = form;
if (form == RECT) {
x = n1;
y = n2;
set_mag();
set_ang();
} else if (form == POL) {
mag = n1;
ang = n2 / Rad_to_deg; // 将角度转换为弧度
set_x();
set_y();
} else {
std::cerr << "Incorrect 3rd argument to Vector() -- ";
std::cerr << "vector set to 0\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}

// 重置矢量
void Vector::reset(double n1, double n2, Mode form) {
mode = form;
if (form == RECT) {
x = n1;
y = n2;
set_mag();
set_ang();
} else if (form == POL) {
mag = n1;
ang = n2 / Rad_to_deg; // 角度转弧度
set_x();
set_y();
} else {
std::cerr << "Incorrect 3rd argument to Vector::reset() -- ";
std::cerr << "vector set to 0\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}

Vector::~Vector() { // 析构函数
}

void Vector::polar_mode() {
mode = POL;
}

void Vector::rect_mode() {
mode = RECT;
}

// --- 运算符重载实现 ---
// 加法 (通常在直角坐标下计算)
Vector Vector::operator+(const Vector & b) const {
return Vector(x + b.x, y + b.y); // 返回一个新的 Vector 对象
}

// 减法
Vector Vector::operator-(const Vector & b) const {
return Vector(x - b.x, y - b.y);
}

// 取反
Vector Vector::operator-() const {
return Vector(-x, -y);
}

// 乘法 (Vector * double)
Vector Vector::operator*(double n) const {
return Vector(x * n, y * n);
}

// --- 友元函数实现 ---
// 乘法 (double * Vector)
Vector operator*(double n, const Vector & a) {
return a * n; // 调用成员函数 a.operator*(n)
}

// 输出 (根据模式选择输出格式)
std::ostream & operator<<(std::ostream & os, const Vector & v) {
if (v.mode == Vector::RECT) {
os << "(x,y) = (" << v.x << ", " << v.y << ")";
} else if (v.mode == Vector::POL) {
os << "(m,a) = (" << v.mag << ", "
<< v.ang * Rad_to_deg << ")"; // 输出角度
} else {
os << "Vector object mode is invalid";
}
return os;
}

} // end namespace VECTOR

11.5.2 为 Vector 类重载算术运算符

我们在上面的代码中已经实现了几个运算符的重载:

  • operator+: 两个 Vector 相加,返回一个新的 Vector。计算在直角坐标下进行。
  • operator-: 两个 Vector 相减,返回一个新的 Vector
  • operator- (一元): 对 Vector 取反,返回一个新的 Vector
  • operator*: Vector 乘以一个 double,返回一个新的 Vector
  • operator* (友元): double 乘以一个 Vector,通过调用成员函数版本实现。
  • operator<< (友元): 将 Vector 输出到 ostream,根据当前模式选择输出格式。

11.5.3 对实现的说明

  • 命名空间:Vector 类及其相关函数放入 VECTOR 命名空间,以避免潜在的名称冲突。
  • 坐标计算: 算术运算(加、减、乘)通常在直角坐标下执行更简单。即使对象是以极坐标模式创建或设置的,内部的 xy 值也会被计算出来,运算基于这些值进行。结果对象默认以直角坐标模式创建,但其 magang 也会被计算。
  • 角度单位: 内部计算(如 sin, cos, atan2)使用弧度。构造函数和 reset 接受角度值(如果是极坐标模式),并将其转换为弧度存储。angval()operator<< 在显示时将内部的弧度转换回角度
  • 友元函数: operator* (double * Vector) 和 operator<< 被实现为友元函数,原因与 Time 类中的类似:operator* 需要处理 double 作为左操作数的情况,operator<< 需要 ostream 作为左操作数,并且它们都需要访问 Vector 的私有成员。

11.5.4 使用 Vector 类来模拟随机漫步

现在我们可以使用 Vector 类来模拟一个简单的物理过程:随机漫步。假设一个人从原点出发,每次随机选择一个方向行走固定的一段距离,重复多次,我们想知道他最终的位置。

randwalk.cpp (主程序)

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
// filepath: d:\ProgramData\files_Cpp\250424\randwalk.cpp
#include <iostream>
#include <cstdlib> // 提供 rand(), srand() 原型
#include <ctime> // 提供 time() 原型
#include "vector.h" // 包含 Vector 类定义 (假设在同一目录或包含路径中)

int main() {
using namespace std;
using VECTOR::Vector; // 使用 VECTOR 命名空间中的 Vector

srand(time(0)); // 初始化随机数生成器

double direction;
Vector step; // 当前一步的矢量
Vector result(0.0, 0.0); // 从原点开始的总位移
unsigned long steps = 0; // 总步数
double target; // 目标距离
double dstep; // 每步的距离

cout << "Enter target distance (q to quit): ";
while (cin >> target) { // 循环直到输入非数字
cout << "Enter step length: ";
if (!(cin >> dstep))
break; // 如果步长输入无效则退出

cout << "Target Distance: " << target << ", Step Size: " << dstep << endl;
cout << steps << ": " << result << endl; // 输出初始位置

// 开始随机漫步
while (result.magval() < target) { // 只要还没达到目标距离
direction = rand() % 360; // 随机生成 0-359 度的方向
step.reset(dstep, direction, Vector::POL); // 设置当前步的矢量 (极坐标)
result = result + step; // 将当前步加到总位移上
steps++;
cout << steps << ": " << result << endl; // 输出当前位置
}

// 输出最终结果
cout << "After " << steps << " steps, the subject "
"has the following location:\n";
cout << result << endl; // 输出最终位置 (默认直角坐标)
result.polar_mode(); // 切换到极坐标模式
cout << " or\n" << result << endl; // 输出最终位置 (极坐标)
cout << "Average outward distance per step = "
<< result.magval() / steps << endl; // 计算平均每步移动的直线距离

// 重置以便下一次模拟
steps = 0;
result.reset(0.0, 0.0);
cout << "\nEnter target distance (q to quit): ";
}

cout << "Bye!\n";
cin.clear(); // 清除可能的错误状态
while (cin.get() != '\n') // 清空输入缓冲区
continue;

return 0;
}

编译和运行:

你需要将 vector.cpprandwalk.cpp 一起编译链接。

1
2
g++ randwalk.cpp vector.cpp -o randwalk -lm # 可能需要链接数学库 (-lm)
./randwalk

程序会提示你输入目标距离和每步的长度,然后模拟随机漫步过程,打印每一步后的位置,直到达到目标距离,最后输出总结信息。这个例子很好地展示了如何利用运算符重载使类的使用(如 result = result + step;)变得直观和方便。

11.6 类的自动转换和强制类型转换

C++ 是一种强类型语言,但它也允许在不同类型之间进行转换。我们已经熟悉了内置类型之间的转换(例如 intdouble)。C++ 也允许定义类类型与其它类型(包括内置类型和其他类类型)之间的转换规则。这种转换可以是自动(隐式)发生的,也可以是需要显式请求的(强制类型转换)。

11.6.1 隐式转换:使用构造函数

如果一个类的构造函数可以用单个参数来调用(要么它只有一个参数,要么它有多个参数但第一个参数之后的所有参数都有默认值),那么这个构造函数就定义了一个从其参数类型到该类类型的隐式转换规则

示例:

假设我们有一个简单的 Stones 类,表示英石(一种重量单位),并且我们想允许从 double(表示磅)自动转换为 Stones

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
75
76
77
78
79
80
81
82
83
// stones.h
#ifndef STONES_H_
#define STONES_H_

class Stones {
private:
enum { Lbs_per_stn = 14 }; // 每英石包含的磅数
int stone; // 整数部分的英石
double pds_left; // 剩余的小数磅数
double pounds; // 总磅数
public:
// 构造函数:接受总磅数 (double)
Stones(double lbs);
// 默认构造函数
Stones();
~Stones();
void show_lbs() const; // 以磅显示
void show_stn() const; // 以英石格式显示
};
#endif // STONES_H_

// stones.cpp
#include <iostream>
#include "stones.h"

Stones::Stones(double lbs) {
pounds = lbs;
stone = int(lbs) / Lbs_per_stn; // 整数英石
pds_left = int(lbs) % Lbs_per_stn + lbs - int(lbs); // 剩余磅数
}

Stones::Stones() {
stone = pounds = pds_left = 0;
}

Stones::~Stones() {
}

void Stones::show_lbs() const {
std::cout << pounds << " pounds\n";
}

void Stones::show_stn() const {
std::cout << stone << " stone, " << pds_left << " pounds\n";
}

// main.cpp
#include <iostream>
#include "stones.h"

void display(const Stones & st, int n); // 函数接受 Stones 对象

int main() {
Stones incognito = 275; // 隐式转换: 调用 Stones(275.0)
Stones wolfe(285.7); // 显式调用构造函数
Stones taft = 325; // 隐式转换: 调用 Stones(325.0)

std::cout << "Incognito weighs ";
incognito.show_stn();
std::cout << "Wolfe weighs ";
wolfe.show_stn();
std::cout << "Taft weighs ";
taft.show_stn();

incognito = 276.8; // 隐式转换: 调用 Stones(276.8), 然后赋值
taft = Stones(330); // 显式转换 (调用构造函数)

std::cout << "After dinner, Incognito weighs ";
incognito.show_stn();
std::cout << "After dinner, Taft weighs ";
taft.show_stn();

display(19.99, 2); // 隐式转换: 19.99 转换为临时 Stones 对象传递给函数

return 0;
}

void display(const Stones & st, int n) {
for (int i = 0; i < n; i++) {
std::cout << "Wow! ";
st.show_stn();
}
}

main 函数中:

  • Stones incognito = 275;Stones taft = 325;:整数 275325 首先被提升为 double,然后编译器使用 Stones(double) 构造函数创建了一个临时的 Stones 对象,并用这个临时对象来初始化 incognitotaft(在现代 C++ 中,这通常会被优化,直接在目标对象上构造,称为复制省略)。
  • incognito = 276.8;double276.8 被用来创建一个临时的 Stones 对象,然后该临时对象通过赋值运算符(这里是默认的赋值运算符)赋给 incognito
  • display(19.99, 2);double19.99 被用来创建一个临时的 Stones 对象,这个临时对象作为参数传递给 display 函数。

这种隐式转换有时很方便,但也可能导致意想不到的行为或错误,特别是当转换不是程序员的本意时。

使用 explicit 关键字

为了禁止构造函数用于隐式转换,可以在构造函数声明前加上 explicit 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
// stones.h (修改后)
// ...
class Stones {
// ...
public:
// 使用 explicit 禁止隐式转换
explicit Stones(double lbs);
Stones();
~Stones();
// ...
};
// ...

如果使用了 explicit,那么之前的隐式转换代码将不再合法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.cpp (使用 explicit Stones 后)
int main() {
// Stones incognito = 275; // 错误!不能隐式转换
// Stones taft = 325; // 错误!不能隐式转换

Stones wolfe(285.7); // OK: 显式调用构造函数
Stones incognito = Stones(275); // OK: 显式转换 (调用构造函数)
Stones taft(325); // OK: 显式调用构造函数

// incognito = 276.8; // 错误!不能隐式转换后赋值
incognito = Stones(276.8); // OK: 显式转换后赋值

// display(19.99, 2); // 错误!不能隐式转换参数
display(Stones(19.99), 2); // OK: 显式转换参数

// 强制类型转换也允许
display((Stones)20.5, 1); // C 风格强制转换
display(static_cast<Stones>(21.2), 1); // C++ 风格强制转换

return 0;
}

建议: 通常建议将只接受一个参数的构造函数声明为 explicit,除非你确实希望进行该类型的隐式转换。这可以防止许多潜在的错误。

11.6.2 转换函数:operator typeName()

构造函数提供了从其他类型到类类型的转换。如果我们想定义从类类型转换到其他类型(如 doubleint)的规则,我们需要使用**转换函数 (Conversion Function)**。

转换函数是一种特殊的类成员函数,形式为 operator typeName()

特点:

  1. 名称: operator 关键字后跟目标类型名 typeName
  2. 无返回类型声明: 函数头不能指定返回类型(即使它确实会返回一个 typeName 类型的值)。
  3. 无参数: 转换函数必须是没有参数的成员函数。
  4. 必须是成员函数: 不能是静态成员函数或友元函数。

示例:为 Stones 添加转换为 doubleint 的函数

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
// stones.h (添加转换函数)
// ...
class Stones {
// ...
public:
explicit Stones(double lbs);
Stones();
~Stones();
void show_lbs() const;
void show_stn() const;

// 转换函数:转换为 double (总磅数)
operator double() const;
// 转换函数:转换为 int (四舍五入的总磅数)
operator int() const;
};
// ...

// stones.cpp (添加转换函数定义)
// ...
Stones::operator double() const {
return pounds; // 返回总磅数
}

Stones::operator int() const {
// 四舍五入到最近的整数磅
return int(pounds + 0.5);
}
// ...

// main.cpp (使用转换函数)
#include <iostream>
#include "stones.h"

int main() {
Stones poppins(9, 2.8); // 9 stone, 2.8 pounds
double p_wt = poppins; // 隐式转换: 调用 poppins.operator double()
int int_wt = poppins; // 隐式转换: 调用 poppins.operator int()

std::cout << "Poppins weighs " << p_wt << " pounds.\n";
std::cout << "Poppins weighs " << int_wt << " pounds (rounded).\n";

// 也可以显式调用
double p_wt_explicit = double(poppins);
int int_wt_explicit = int(poppins);
std::cout << "Explicit double: " << p_wt_explicit << std::endl;
std::cout << "Explicit int: " << int_wt_explicit << std::endl;

return 0;
}

输出:

1
2
3
4
Poppins weighs 128.8 pounds.
Poppins weighs 129 pounds (rounded).
Explicit double: 128.8
Explicit int: 129

explicit 转换函数 (C++11)

与构造函数类似,C++11 允许将转换函数声明为 explicit,以防止它们被用于隐式转换。如果转换函数是 explicit 的,那么只能在需要显式转换的上下文中使用(例如,使用 static_cast 或直接初始化)。

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
// stones.h (使用 explicit 转换函数)
class Stones {
// ...
public:
// ...
explicit operator double() const;
explicit operator int() const;
};

// main.cpp (使用 explicit 转换函数后)
int main() {
Stones poppins(9, 2.8);
// double p_wt = poppins; // 错误!不能隐式转换
// int int_wt = poppins; // 错误!不能隐式转换

// 需要显式转换
double p_wt = static_cast<double>(poppins);
int int_wt = static_cast<int>(poppins);

std::cout << "Poppins weighs " << p_wt << " pounds.\n";
std::cout << "Poppins weighs " << int_wt << " pounds (rounded).\n";

// if (poppins) { ... } // 如果 operator bool() 是 explicit,这里也会报错
if (static_cast<bool>(poppins)) { /* ... */ } // 需要显式转换

return 0;
}

将转换函数设为 explicit 通常也是一个好主意,除非隐式转换确实是你想要的行为(例如 operator bool() 在条件语句中的使用,但在 C++11 后也推荐 explicit operator bool())。

11.6.3 转换函数和友元函数

转换函数和接受单参数的构造函数都可能导致二义性问题,尤其是在与重载运算符(特别是友元函数)一起使用时。

二义性示例:

考虑之前的 Vector 类和 Vector operator*(double n, const Vector & a); 友元函数。如果我们为 Vector 添加一个接受 double 的构造函数(可能表示创建一个大小为 double、角度为 0 的向量)和一个转换为 double 的函数(可能返回向量的大小 magval()),会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vector.h (假设添加了转换构造函数和函数)
namespace VECTOR {
class Vector {
// ...
public:
Vector(double val); // 转换构造函数 (可能不 explicit)
operator double() const; // 转换函数 (可能不 explicit)
// ...
friend Vector operator*(double n, const Vector & a);
};
}

// main.cpp
VECTOR::Vector result;
VECTOR::Vector vec(10.0, 20.0);
double factor = 2.0;
result = factor * vec; // 潜在的二义性!

编译器在处理 factor * vec 时,可能会有两种解释:

  1. 调用友元函数: operator*(factor, vec),这是我们原本期望的。
  2. 使用转换函数:vec 转换为 double(通过 vec.operator double()),然后执行 double * double 的内置乘法。

如果 Vector(double)operator double() 都存在且都不是 explicit,编译器通常会因为无法确定使用哪个转换路径而报告**二义性错误 (ambiguity error)**。

解决方法:

  1. 使用 explicit: 将转换构造函数和转换函数声明为 explicit,可以消除隐式转换带来的二义性。用户必须显式指定转换。
  2. 提供精确匹配的函数: 如果只提供了 operator*(double, const Vector&),而没有提供 operator double(),那么编译器会优先选择精确匹配的友元函数,不会产生二义性。
  3. 避免定义相互冲突的转换: 在设计类时,仔细考虑可能需要的转换,避免同时提供从类型 A 到类型 B 以及从类型 B 到类型 A 的隐式转换,或者提供多个可能导致相同结果的转换路径。

总结:

  • 接受单个参数的构造函数定义了从参数类型到类类型的隐式转换,除非使用 explicit 阻止。
  • 转换函数 operator typeName() 定义了从类类型到 typeName 类型的转换,可以是隐式的,除非使用 explicit (C++11) 阻止。
  • 隐式转换虽然方便,但可能导致意外行为和二义性。优先使用 explicit 来控制转换。
  • 当存在多个转换路径(通过构造函数或转换函数)可以使表达式合法时,编译器可能会遇到二义性问题。
  • 在设计类时,应谨慎考虑转换需求,避免引入不必要的或有歧义的转换规则。

11.7 总结

本章重点介绍了如何通过运算符重载、友元和类型转换等特性来扩展类的功能,使其使用起来更自然、更强大。

主要内容回顾:

  1. 运算符重载 (Operator Overloading):

    • 允许为类定义标准 C++ 运算符(如 +, -, *, <<)的行为。
    • 目的是提高代码的可读性和直观性,使类对象的运算类似于内置类型。
    • 通过定义运算符函数operator+, operator<< 等)来实现。
    • 运算符函数可以是成员函数非成员函数(通常是友元)。
    • 重载不能改变运算符的优先级、结合性或操作数个数,也不能创建新运算符。某些运算符(如 .::sizeof)不能被重载。
    • Time 类的示例演示了如何重载 +, -, * 等算术运算符。
  2. 友元 (Friends):

    • 允许非成员函数或整个类访问另一个类的 privateprotected 成员。
    • 通过在类定义内部使用 friend 关键字声明。
    • 友元函数常用于重载那些不能作为成员函数(如 <<, >>)或需要对称处理操作数(如 double * Vector)的运算符。
    • 友元类允许一个类的所有成员函数访问另一个类的私有部分,适用于需要紧密协作的类。
  3. 成员函数 vs. 非成员函数 (Operator Overloading Choice):

    • 赋值 (=)、下标 ([])、函数调用 (())、成员访问 (->) 运算符必须是成员函数。
    • 修改对象状态的运算符(如 +=, ++)通常实现为成员函数
    • 需要对称性或允许左操作数进行类型转换的运算符(如 +, *, ==)以及输入/输出运算符 (<<, >>) 通常实现为非成员友元函数
  4. Vector 类示例:

    • 演示了如何设计一个包含状态(直角/极坐标模式)的类。
    • 进一步展示了算术运算符 (+, -, unary -, *) 和输出运算符 (<<) 的重载。
    • 通过随机漫步模拟展示了 Vector 类的实际应用。
  5. 类的类型转换:

    • 构造函数转换: 接受单个参数的构造函数(除非声明为 explicit)定义了从参数类型到类类型的隐式转换
    • 转换函数: operator typeName() 形式的成员函数(除非声明为 explicit)定义了从类类型到 typeName 类型的隐式转换
    • explicit 关键字: 用于阻止构造函数和转换函数进行隐式转换,提高代码安全性,避免意外转换和二义性。
    • 二义性: 当存在多个转换路径(通过构造函数或转换函数)时,可能导致编译错误。应谨慎设计转换规则或使用 explicit

通过合理运用运算符重载、友元和类型转换,可以创建出功能丰富、易于使用且表达力强的 C++ 类。然而,这些特性也需要谨慎使用,以避免引入不必要的复杂性或潜在的错误。

评论