11.1 运算符重载
C++ 允许将运算符(如 +
, -
, *
, /
, <<
, >>
, ==
等)应用于类对象,就像它们是内置类型(如 int
或 double
)一样。这种特性称为**运算符重载 (Operator Overloading)**。
目的:
运算符重载的主要目的是提高代码的可读性和直观性。通过重载运算符,我们可以让我们自己定义的类(比如表示复数、向量、时间或字符串的类)能够使用熟悉的运算符进行操作,使得代码更接近自然语言或数学表示法。
例如,假设你有一个表示二维向量的类 Vector
。如果没有运算符重载,你可能需要这样写代码来计算两个向量的和:
1 | Vector v1(1.0, 2.0); |
通过重载 +
运算符,你可以写出更自然、更简洁的代码:
1 | Vector v1(1.0, 2.0); |
如何实现:
运算符重载是通过编写特殊的运算符函数 (Operator Functions) 来实现的。运算符函数的名称格式为 operator
关键字后跟要重载的运算符符号。例如,重载加法运算符 +
的函数名为 operator+
,重载小于运算符 <
的函数名为 operator<
。
运算符函数可以被定义为:
类的成员函数 (Member Function):
- 当运算符函数是成员函数时,它的第一个操作数隐式地是调用该函数的对象(通过
this
指针访问)。 - 对于二元运算符(如
+
,-
,*
),成员函数只需要一个显式参数(代表右操作数)。 - 对于一元运算符(如
-
(负号),++
(前缀)),成员函数没有显式参数。
- 当运算符函数是成员函数时,它的第一个操作数隐式地是调用该函数的对象(通过
非成员函数 (Non-member Function):
- 通常将非成员运算符函数声明为类的**友元 (friend)**,以便它可以访问类的私有成员。
- 对于二元运算符,非成员函数需要两个显式参数(分别代表左操作数和右操作数)。
- 对于一元运算符,非成员函数需要一个显式参数。
基本语法 (以二元运算符 +
为例):
作为成员函数:
1 | class MyClass { |
作为非成员函数 (通常是友元):
1 | class MyClass { |
注意事项:
- 不能创建新的运算符: 你只能重载 C++ 中已有的运算符,不能发明新的运算符(比如
**
或##
)。 - 不能改变运算符的优先级或结合性: 重载
+
和*
后,*
的优先级仍然高于+
。 - 不能改变运算符的操作数个数: 不能将二元运算符重载为一元运算符,反之亦然。
- 至少一个操作数是用户定义类型: 不能为两个内置类型(如
int
和int
)重载运算符。运算符重载必须涉及至少一个类对象(或枚举类型)。 - 某些运算符不能重载:
-
.
(成员访问运算符) -
.*
(成员指针访问运算符) -
::
(作用域解析运算符) -
sizeof
(大小运算符) -
?:
(条件运算符) -
typeid
(运行时类型识别运算符) -
static_cast
,dynamic_cast
,const_cast
,reinterpret_cast
(类型转换运算符,虽然operator type()
形式的转换函数可以定义)
-
运算符重载是 C++ 提供的一个强大工具,可以使自定义类的使用更加自然和方便。然而,也应谨慎使用,避免过度或不直观的重载,以免降低代码的可读性。接下来的章节将通过具体的例子来演示如何重载不同的运算符。
11.2 计算时间:一个运算符重载示例
为了具体说明运算符重载的过程和用法,让我们创建一个简单的 Time
类来表示时间(小时和分钟),并为其重载一些运算符。
基础 Time
类:
首先,我们定义一个基础的 Time
类,包含小时和分钟,一个构造函数来初始化时间,以及一个 show()
方法来显示时间。
1 | // time.h -- Time 类定义 |
1 | // time.cpp -- Time 类方法实现 |
这个基础类允许我们创建 Time
对象并对其进行一些基本操作,但还不能直接使用 +
等运算符。
11.2.1 添加加法运算符
我们希望能够像 total = time1 + time2;
这样将两个 Time
对象相加。为此,我们需要重载 +
运算符。我们将它实现为 Time
类的成员函数。
在 time.h
中添加声明:
1 | // filepath: d:\ProgramData\files_Cpp\250424\time.h |
在 time.cpp
中添加定义:
1 | // filepath: d:\ProgramData\files_Cpp\250424\time.cpp |
使用重载的 +
运算符:
1 | // main.cpp -- 使用 Time 类和重载的 + |
编译和运行:
(需要将 main.cpp
和 time.cpp
一起编译链接)
输出:
1 | Planning time = 2 hours, 40 minutes |
可以看到,通过重载 +
运算符,我们可以用非常自然的方式将 Time
对象相加。
11.2.2 重载限制
重载运算符时必须遵守一些规则和限制:
- 不能创建新运算符: 只能重载 C++ 已有的运算符。
- 不能改变运算符优先级和结合性:
*
总是优先于+
。 - 不能改变运算符操作数个数: 二元运算符(如
+
)必须接受两个操作数,一元运算符(如-
(负号))必须接受一个操作数。 - 至少一个操作数是用户定义类型: 不能为两个
int
重载+
运算符。重载必须涉及至少一个类对象(或枚举)。 - 特定运算符不能重载: 如
.
、::
、sizeof
、?:
等。 - 保持直观性: 重载应该符合运算符的通常含义。例如,用
+
来表示两个对象的相减会非常令人困惑。虽然语法上允许,但这是不良实践。
11.2.3 其他重载运算符
我们可以为 Time
类重载更多的运算符,使其功能更完善。
重载减法运算符 (-
) 作为成员函数:
1 | // filepath: d:\ProgramData\files_Cpp\250424\time.h |
1 | // filepath: d:\ProgramData\files_Cpp\250424\time.cpp |
重载乘法运算符 (*
) - 时间乘以一个因子:
假设我们想计算 Time
对象乘以一个 double
因子(例如,将时间放大 1.5 倍)。这个运算符的操作数类型不同(Time
和 double
),可以作为成员函数或非成员函数实现。这里我们作为成员函数实现。
1 | // filepath: d:\ProgramData\files_Cpp\250424\time.h |
1 | // filepath: d:\ProgramData\files_Cpp\250424\time.cpp |
使用示例:
1 | // main.cpp (续) |
输出 (续):
1 | t1 = 2 hours, 30 minutes |
这个例子展示了如何通过重载运算符,让自定义的 Time
类能够以更自然、更符合数学直觉的方式进行运算。下一节将讨论友元函数,特别是如何使用友元函数来重载像 <<
这样的输出运算符,以及处理像 double * Time
这样的运算顺序问题。
11.3 友元
通常,类的私有成员(private
)只能被该类的成员函数访问。这是 C++ 实现封装和数据隐藏的关键机制。然而,在某些特殊情况下,允许特定的非成员函数或其他类访问一个类的私有成员会非常方便。C++ 提供了友元 (friend) 机制来实现这种受控的访问。
什么是友元?
友元是 C++ 中的一种机制,它允许一个类授予非成员函数或另一个类访问其 private
和 protected
成员的权限。被授予权限的函数或类被称为该类的友元。
注意: 友元关系是单向的,并且不能被继承。如果类 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
20class 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):
一个类可以将另一个整个类声明为友元。这样,友元类的所有成员函数都可以访问声明它为友元的那个类的 private
和 protected
成员。
1 | class Storage { |
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
类的 hours
和 minutes
)来打印它们。这就使得将 operator<<
声明为类的友元函数成为最自然、最常用的解决方案。
为 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// 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) ...在
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
// ... (构造函数和其他方法的定义) ...
// 定义友元函数 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 { ... }使用重载的
<<
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// main.cpp -- 使用重载的 <<
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 | Time t1: 3 hours, 45 minutes |
处理 double * Time
(友元函数方式):
在上一节中,我们定义了 Time operator*(double mult) const;
作为成员函数,可以处理 time * double
的情况,但不能处理 double * time
。我们可以通过添加一个非成员友元函数来解决这个问题。
在
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 ...在
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 | // main.cpp |
总结:
- 友元(函数或类)被授予访问声明其为友元的类的私有和保护成员的权限。
- 使用
friend
关键字在类定义内部声明友元。 - 友元对于重载某些运算符(尤其是需要访问私有成员且不能作为成员函数的运算符,如
<<
)非常有用。 - 友元也用于实现需要紧密协作但又不适合放在同一个类中的功能。
- 虽然友元打破了纯粹的封装,但它提供了一种受控的、明确的方式来在必要时绕过访问限制。应谨慎使用,避免滥用。
11.4 重载运算符:作为成员函数还是非成员函数
在重载运算符时,一个关键的决定是将其实现为类的成员函数还是非成员函数(通常是友元)。这个选择会影响函数的调用方式、参数列表以及某些情况下的行为。
核心区别回顾:
成员函数:
- 通过类的对象调用。
- 左操作数隐式地是调用该函数的对象(通过
this
指针访问)。 - 对于二元运算符,它只需要一个显式参数(右操作数)。
- 对于一元运算符,它没有显式参数。
- 可以直接访问类的
private
和protected
成员。 - 示例:
obj1 + obj2
调用obj1.operator+(obj2)
。
非成员函数:
- 独立于类定义之外(但通常在同一个头文件或源文件中)。
- 所有操作数都必须作为显式参数传递。
- 对于二元运算符,它需要两个显式参数(左、右操作数)。
- 对于一元运算符,它需要一个显式参数。
- 如果需要访问类的
private
或protected
成员,必须被声明为该类的**友元 (friend
)**。 - 示例:
obj1 + obj2
调用operator+(obj1, obj2)
。
选择指南:
以下是一些通用指南和特定运算符的惯例:
必须是成员函数的运算符:
以下运算符只能通过成员函数进行重载:-
=
(赋值运算符) -
[]
(下标运算符) -
()
(函数调用运算符) -
->
(成员访问运算符) - 任何类型转换运算符 (如
operator int()
)
-
通常作为成员函数的运算符 (修改对象状态):
对于那些通常会修改其左操作数对象状态的运算符,将它们实现为成员函数通常更自然。这包括:复合赋值运算符:
+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
,<<=
,>>=
- 例如,
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; // 返回修改后的对象自身的引用
}- 例如,
通常作为非成员函数 (通常是友元) 的运算符:
对于那些不修改操作数状态(而是创建一个新值)或者需要对称处理操作数(允许不同类型的左操作数)的运算符,通常将它们实现为非成员函数(通常是友元)。二元算术运算符:
+
,-
,*
,/
,%
- 虽然
+
也可以作为成员函数(如 11.2 节所示),但作为非成员友元函数可以提供更好的对称性。例如,如果类允许从double
到Time
的隐式转换(通常不推荐,但可能存在),那么友元函数operator+(const Time&, const Time&)
可以处理time + time
、time + double
和double + time
(通过转换),而成员函数只能处理time + time
和time + double
。 - 对于像
double * Time
这样的混合类型运算,如果想让double
作为左操作数,则必须使用非成员函数(如 11.3 节所示)。
- 虽然
关系运算符:
==
,!=
,<
,>
,<=
,>=
- 将它们实现为非成员友元可以更容易地处理对称性,并可能允许混合类型的比较(如果定义了适当的转换或重载版本)。
位运算符:
&
,|
,^
逻辑运算符:
&&
,||
(较少重载)输入/输出运算符:
<<
,>>
- 必须是非成员函数,因为左操作数是
ostream
或istream
对象,而不是你的类的对象。它们几乎总是需要成为友元以访问私有数据进行读写。
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 | // 示例模式 |
这种模式可以减少代码重复。
11.5 再读重载:一个矢量类
本节我们将通过定义一个表示二维矢量(或向量)的类,进一步探讨运算符重载的应用。矢量既可以用直角坐标 (x, y) 表示,也可以用极坐标 (大小/模, 角度) 表示。我们的 Vector
类将能够存储这两种表示,并允许用户在它们之间切换。
11.5.1 使用状态成员
为了同时支持直角坐标和极坐标,我们的 Vector
类需要包含相应的成员:
-
x
: x 分量 -
y
: y 分量 -
mag
: 矢量的大小(模) -
ang
: 矢量的角度
同时,我们需要一个状态成员来指示当前对象是以哪种模式表示的(直角坐标或极坐标),以及一些方法来根据一种表示计算另一种表示。
vector.h (类定义)
1 | // filepath: d:\ProgramData\files_Cpp\250424\vector.h |
vector.cpp (类实现)
1 | // filepath: d:\ProgramData\files_Cpp\250424\vector.cpp |
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
命名空间,以避免潜在的名称冲突。 - 坐标计算: 算术运算(加、减、乘)通常在直角坐标下执行更简单。即使对象是以极坐标模式创建或设置的,内部的
x
和y
值也会被计算出来,运算基于这些值进行。结果对象默认以直角坐标模式创建,但其mag
和ang
也会被计算。 - 角度单位: 内部计算(如
sin
,cos
,atan2
)使用弧度。构造函数和reset
接受角度值(如果是极坐标模式),并将其转换为弧度存储。angval()
和operator<<
在显示时将内部的弧度转换回角度。 - 友元函数:
operator*
(double * Vector) 和operator<<
被实现为友元函数,原因与Time
类中的类似:operator*
需要处理double
作为左操作数的情况,operator<<
需要ostream
作为左操作数,并且它们都需要访问Vector
的私有成员。
11.5.4 使用 Vector 类来模拟随机漫步
现在我们可以使用 Vector
类来模拟一个简单的物理过程:随机漫步。假设一个人从原点出发,每次随机选择一个方向行走固定的一段距离,重复多次,我们想知道他最终的位置。
randwalk.cpp (主程序)
1 | // filepath: d:\ProgramData\files_Cpp\250424\randwalk.cpp |
编译和运行:
你需要将 vector.cpp
和 randwalk.cpp
一起编译链接。
1 | g++ randwalk.cpp vector.cpp -o randwalk -lm # 可能需要链接数学库 (-lm) |
程序会提示你输入目标距离和每步的长度,然后模拟随机漫步过程,打印每一步后的位置,直到达到目标距离,最后输出总结信息。这个例子很好地展示了如何利用运算符重载使类的使用(如 result = result + step;
)变得直观和方便。
11.6 类的自动转换和强制类型转换
C++ 是一种强类型语言,但它也允许在不同类型之间进行转换。我们已经熟悉了内置类型之间的转换(例如 int
到 double
)。C++ 也允许定义类类型与其它类型(包括内置类型和其他类类型)之间的转换规则。这种转换可以是自动(隐式)发生的,也可以是需要显式请求的(强制类型转换)。
11.6.1 隐式转换:使用构造函数
如果一个类的构造函数可以用单个参数来调用(要么它只有一个参数,要么它有多个参数但第一个参数之后的所有参数都有默认值),那么这个构造函数就定义了一个从其参数类型到该类类型的隐式转换规则。
示例:
假设我们有一个简单的 Stones
类,表示英石(一种重量单位),并且我们想允许从 double
(表示磅)自动转换为 Stones
。
1 | // stones.h |
在 main
函数中:
-
Stones incognito = 275;
和Stones taft = 325;
:整数275
和325
首先被提升为double
,然后编译器使用Stones(double)
构造函数创建了一个临时的Stones
对象,并用这个临时对象来初始化incognito
和taft
(在现代 C++ 中,这通常会被优化,直接在目标对象上构造,称为复制省略)。 -
incognito = 276.8;
:double
值276.8
被用来创建一个临时的Stones
对象,然后该临时对象通过赋值运算符(这里是默认的赋值运算符)赋给incognito
。 -
display(19.99, 2);
:double
值19.99
被用来创建一个临时的Stones
对象,这个临时对象作为参数传递给display
函数。
这种隐式转换有时很方便,但也可能导致意想不到的行为或错误,特别是当转换不是程序员的本意时。
使用 explicit
关键字
为了禁止构造函数用于隐式转换,可以在构造函数声明前加上 explicit
关键字。
1 | // stones.h (修改后) |
如果使用了 explicit
,那么之前的隐式转换代码将不再合法:
1 | // main.cpp (使用 explicit Stones 后) |
建议: 通常建议将只接受一个参数的构造函数声明为 explicit
,除非你确实希望进行该类型的隐式转换。这可以防止许多潜在的错误。
11.6.2 转换函数:operator typeName()
构造函数提供了从其他类型到类类型的转换。如果我们想定义从类类型转换到其他类型(如 double
或 int
)的规则,我们需要使用**转换函数 (Conversion Function)**。
转换函数是一种特殊的类成员函数,形式为 operator typeName()
。
特点:
- 名称:
operator
关键字后跟目标类型名typeName
。 - 无返回类型声明: 函数头不能指定返回类型(即使它确实会返回一个
typeName
类型的值)。 - 无参数: 转换函数必须是没有参数的成员函数。
- 必须是成员函数: 不能是静态成员函数或友元函数。
示例:为 Stones
添加转换为 double
和 int
的函数
1 | // stones.h (添加转换函数) |
输出:
1 | Poppins weighs 128.8 pounds. |
explicit
转换函数 (C++11)
与构造函数类似,C++11 允许将转换函数声明为 explicit
,以防止它们被用于隐式转换。如果转换函数是 explicit
的,那么只能在需要显式转换的上下文中使用(例如,使用 static_cast
或直接初始化)。
1 | // stones.h (使用 explicit 转换函数) |
将转换函数设为 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 | // vector.h (假设添加了转换构造函数和函数) |
编译器在处理 factor * vec
时,可能会有两种解释:
- 调用友元函数:
operator*(factor, vec)
,这是我们原本期望的。 - 使用转换函数: 将
vec
转换为double
(通过vec.operator double()
),然后执行double * double
的内置乘法。
如果 Vector(double)
和 operator double()
都存在且都不是 explicit
,编译器通常会因为无法确定使用哪个转换路径而报告**二义性错误 (ambiguity error)**。
解决方法:
- 使用
explicit
: 将转换构造函数和转换函数声明为explicit
,可以消除隐式转换带来的二义性。用户必须显式指定转换。 - 提供精确匹配的函数: 如果只提供了
operator*(double, const Vector&)
,而没有提供operator double()
,那么编译器会优先选择精确匹配的友元函数,不会产生二义性。 - 避免定义相互冲突的转换: 在设计类时,仔细考虑可能需要的转换,避免同时提供从类型 A 到类型 B 以及从类型 B 到类型 A 的隐式转换,或者提供多个可能导致相同结果的转换路径。
总结:
- 接受单个参数的构造函数定义了从参数类型到类类型的隐式转换,除非使用
explicit
阻止。 - 转换函数
operator typeName()
定义了从类类型到typeName
类型的转换,可以是隐式的,除非使用explicit
(C++11) 阻止。 - 隐式转换虽然方便,但可能导致意外行为和二义性。优先使用
explicit
来控制转换。 - 当存在多个转换路径(通过构造函数或转换函数)可以使表达式合法时,编译器可能会遇到二义性问题。
- 在设计类时,应谨慎考虑转换需求,避免引入不必要的或有歧义的转换规则。
11.7 总结
本章重点介绍了如何通过运算符重载、友元和类型转换等特性来扩展类的功能,使其使用起来更自然、更强大。
主要内容回顾:
运算符重载 (Operator Overloading):
- 允许为类定义标准 C++ 运算符(如
+
,-
,*
,<<
)的行为。 - 目的是提高代码的可读性和直观性,使类对象的运算类似于内置类型。
- 通过定义运算符函数(
operator+
,operator<<
等)来实现。 - 运算符函数可以是成员函数或非成员函数(通常是友元)。
- 重载不能改变运算符的优先级、结合性或操作数个数,也不能创建新运算符。某些运算符(如
.
、::
、sizeof
)不能被重载。 -
Time
类的示例演示了如何重载+
,-
,*
等算术运算符。
- 允许为类定义标准 C++ 运算符(如
友元 (Friends):
- 允许非成员函数或整个类访问另一个类的
private
和protected
成员。 - 通过在类定义内部使用
friend
关键字声明。 - 友元函数常用于重载那些不能作为成员函数(如
<<
,>>
)或需要对称处理操作数(如double * Vector
)的运算符。 - 友元类允许一个类的所有成员函数访问另一个类的私有部分,适用于需要紧密协作的类。
- 允许非成员函数或整个类访问另一个类的
成员函数 vs. 非成员函数 (Operator Overloading Choice):
- 赋值 (
=
)、下标 ([]
)、函数调用 (()
)、成员访问 (->
) 运算符必须是成员函数。 - 修改对象状态的运算符(如
+=
,++
)通常实现为成员函数。 - 需要对称性或允许左操作数进行类型转换的运算符(如
+
,*
,==
)以及输入/输出运算符 (<<
,>>
) 通常实现为非成员友元函数。
- 赋值 (
Vector
类示例:- 演示了如何设计一个包含状态(直角/极坐标模式)的类。
- 进一步展示了算术运算符 (
+
,-
, unary-
,*
) 和输出运算符 (<<
) 的重载。 - 通过随机漫步模拟展示了
Vector
类的实际应用。
类的类型转换:
- 构造函数转换: 接受单个参数的构造函数(除非声明为
explicit
)定义了从参数类型到类类型的隐式转换。 - 转换函数:
operator typeName()
形式的成员函数(除非声明为explicit
)定义了从类类型到typeName
类型的隐式转换。 -
explicit
关键字: 用于阻止构造函数和转换函数进行隐式转换,提高代码安全性,避免意外转换和二义性。 - 二义性: 当存在多个转换路径(通过构造函数或转换函数)时,可能导致编译错误。应谨慎设计转换规则或使用
explicit
。
- 构造函数转换: 接受单个参数的构造函数(除非声明为
通过合理运用运算符重载、友元和类型转换,可以创建出功能丰富、易于使用且表达力强的 C++ 类。然而,这些特性也需要谨慎使用,以避免引入不必要的复杂性或潜在的错误。