11.1、运算符重载
运算符重载是指在C++中,可以对运算符进行重载来定义类的行为,从而使得类的对象可以像基本数据类型一样进行运算。通常情况下,运算符重载是通过类的成员函数或全局函数来实现的。
运算符重载的语法格式为:
返回类型 operator 运算符 (参数列表) {
// 运算符重载函数的具体实现
}
其中,operator
是关键字,用于声明运算符名称。在参数列表中可以定义运算符的几个操作数,如果是成员函数,则第一个参数为隐式的this
指针。
例如,假设定义了一个名为Vector
的向量类,可以重载加号运算符+
,使得两个向量对象可以相加。代码示例如下:
class Vector {
public:
Vector(int x, int y) : x_(x), y_(y) {}
Vector operator+(const Vector& other) const {
return Vector(x_ + other.x_, y_ + other.y_);
}
private:
int x_;
int y_;
};
在上述代码中,Vector
类定义了一个公有成员函数operator+()
,用于重载加号运算符+
。当对Vector
对象执行加号运算时,将会调用该函数,从而实现向量的相加操作。
可以通过以下方式使用向量类对象执行加法操作:
Vector v1(1, 2);
Vector v2(3, 4);
Vector v3 = v1 + v2;
在上述代码中,v1 + v2
执行向量的相加操作,并将结果存储在v3
中。
需要注意的是,在运算符重载时,需要遵循运算符的本意和常规规则,同时需要保证运算符的语义和类型匹配。运算符重载还应该有明确的含义,避免产生二义性,从而降低程序的可读性和可维护性。
11.2、计算时间:一个运算符重载示例
下面以一个计算时间的例子来说明运算符重载的具体实现。
假设有一个表示时间的类Time
,其中包含小时、分钟、秒等属性。现在希望实现以下运算符:
+
:将两个Time
对象相加得到一个新的Time
对象。-
:将两个Time
对象相减得到一个新的Time
对象。+=
:将当前的Time
对象增加一个指定的时间,并返回当前对象的引用。-=
:将当前的Time
对象减少一个指定的时间,并返回当前对象的引用。<
:比较两个Time
对象的时间大小。
可以通过运算符重载来实现这些运算符的功能。
首先,需要在Time
类中添加以下成员变量和成员函数:
class Time {
private:
int hour_;
int minute_;
int second_;
public:
Time(int hour, int minute, int second)
: hour_(hour), minute_(minute), second_(second) {}
Time operator+(const Time& other) const;
Time operator-(const Time& other) const;
Time& operator+=(const Time& other);
Time& operator-=(const Time& other);
bool operator<(const Time& other) const;
};
在上述代码中,定义了成员变量hour_
、minute_
和second_
用于表示时间,同时定义了以上需要实现的运算符。
可以通过以下方式实现+
和-
运算符重载:
Time Time::operator+(const Time& other) const {
int hour = hour_ + other.hour_;
int minute = minute_ + other.minute_;
int second = second_ + other.second_;
if (second >= 60) {
second -= 60;
minute++;
}
if (minute >= 60) {
minute -= 60;
hour++;
}
hour %= 24;
return Time(hour, minute, second);
}
Time Time::operator-(const Time& other) const {
int hour = hour_ - other.hour_;
int minute = minute_ - other.minute_;
int second = second_ - other.second_;
if (second < 0) {
second += 60;
minute--;
}
if (minute < 0) {
minute += 60;
hour--;
}
if (hour < 0) {
hour += 24;
}
return Time(hour, minute, second);
}
在代码中,+
运算符将两个Time
对象的时间相加,返回一个新的Time
对象。-
运算符将两个Time
对象的时间相减,返回一个新的Time
对象。需要注意的是,在进行时间相加和相减时,需要对时间进行进位和借位的处理。
接下来,可以实现+=
和-=
运算符重载:
Time& Time::operator+=(const Time& other) {
hour_ = (hour_ + other.hour_) % 24;
minute_ = (minute_ + other.minute_) % 60;
second_ = (second_ + other.second_) % 60;
return *this;
}
Time& Time::operator-=(const Time& other) {
hour_ = (hour_ - other.hour_ + 24) % 24;
minute_ = (minute_ - other.minute_ + 60) % 60;
second_ = (second_ - other.second_ + 60) % 60;
return *this;
}
在代码中,+=
运算符将当前的Time
对象增加一个指定的时间,并返回当前对象的引用。-=
运算符将当前的Time
对象减少一个指定的时间,并返回当前对象的引用。需要注意的是,在进行时间相加和相减时,同样需要对时间进行进位和借位的处理。
最后,可以实现<
运算符重载:
bool Time::operator<(const Time& other) const {
if (hour_ != other.hour_) {
return hour_ < other.hour_;
}
if (minute_ != other.minute_) {
return minute_ < other.minute_;
}
return second_ < other.second_;
}
在代码中,<
运算符比较两个Time
对象的时间大小。首先比较小时数,如果小时数相同,则比较分钟数,如果分钟数也相同,则比较秒数。
通过以上实现,可以使用运算符对Time
对象进行相应的操作。例如,可以使用以下方式执行时间相加操作:
Time t1(9, 40, 30);
Time t2(1, 20, 50);
Time t3 = t1 + t2;
在代码中,t1 + t2
执行时间相加操作,并将结果存储在t3
中。
总之,在C++中,运算符重载是一种非常有用的特性,它允许用户通过自定义运算符的行为来扩展类的功能,使得类对象的操作更加直观和方便。但是,在运算符重载时,需要保证运算符的语义和类型匹配,避免产生二义性,从而降低程序的可读性和可维护性。
11.2.1、添加加法运算符
为了添加加法运算符 +
,需要在类定义中声明一个 operator+
函数,该函数需要有以下特征:
- 返回类型:返回值类型应该与操作数类型相同。
- 函数名:函数名必须是
operator+
。 - 参数:通常将函数参数声明为常量引用类型,并命名为
other
或其他合适的名称。 - 实现:根据上下文和数据成员的类型,使用普通加法操作将成员变量组合起来。
下面是一个使用友元函数实现重载加法运算符的示例:
#include<iostream>
using namespace std;
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
friend MyClass operator+(const MyClass& lhs, const MyClass& rhs);
};
MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
return MyClass(lhs.value + rhs.value);
}
int main() {
MyClass a(10);
MyClass b(20);
MyClass c = a + b;
cout << c.value << endl;
return 0;
}
在此示例中,operator+
定义为一个友元函数,可以在类的外部定义。该函数将两个 MyClass
对象值相加,并返回一个新对象。
在 main
中,创建了两个 MyClass
对象 a
和 b
,然后将它们相加,并将结果存储在 c
中,最后在控制台打印了 c
对象的 value
值。
输出结果为 30
,表示成功地重载了加法运算符。
需要注意的是,如果定义为类的成员函数,那么运算符只能左操作数是当前类对象。如果需要允许右操作数为其他类型,则必须定义为友元函数。此外,如果对于已有的运算符使用了运算符重载,则必须确保所定义的操作与原始习惯相匹配,以免混淆使用该运算符的人。
11.2.2、重载限制
重载运算符需要遵守以下限制:
1、必须具有至少一个自定义类型的操作数。这意味着不能仅重载原始类型的运算符,例如 int
、double
等。
2、不能更改运算符的基本含义。重载运算符时需要保持运算符的基本含义与习惯相匹配。例如,+
运算符通常用于对数字进行加法运算,而不是连接两个字符串。
3、不能制定新的运算符。只能重载现有 C++ 运算符。
4、不能重载的运算符包括: ?:
、.*
、::
、.
,以及作用于基本类型的运算符:sizeof
、typeid
、static_cast
、dynamic_cast
、const_cast
、reinterpret_cast
。
5、您无法创建新的运算符,但您可以重载这些运算符:( )
、[ ]
、->
、 以及逗号运算符 ,
。
6、以下运算符必须定义为成员函数:=、[]、()、->
。这些运算符要求左操作数为类对象。
7、不能重载的运算符为 &&
、||
和 ,
运算符。这是因为它们具有短路求值和顺序点的特性。
总之,重载运算符是 C++ 面向对象编程的一个重要特性,但需要仔细考虑重新定义运算符的含义和适用环境。只有在非常必要的情况下,才应使用运算符重载技术来使代码更具可读性和易用性。
11.2.3、其他重载运算符
除了常见的运算符,C++ 中还有许多其他可以重载的运算符。下面列出了一些常见的例子:
-
new
和delete
运算符:可以重载全局的new
和delete
运算符来实现自定义的内存管理机制。这在使用自定义内存池等高效的内存分配技术时非常有用。 -
->*
二元运算符:用于解引用类成员指针。可以使用->*
运算符来重载类的成员指针操作。 -
typeid
运算符:用于获取一个对象的类型信息。可以重载typeid
运算符来改变它的行为,例如返回一个自定义的类型信息。 -
->
一元运算符:用于访问指针类型的成员。可以重载->
运算符来实现一个自己的指针类型,以实现类似智能指针的功能。 -
,
运算符:用于连续执行多个表达式,并返回最后一个表达式的值。可以重载,
运算符来实现更加复杂的表达式。 -
()
运算符:用于将一个对象当作函数进行调用。可以重载()
运算符来让类的对象可以像函数一样被调用。
总之,C++ 中的运算符重载提供了很多强大的编程功能,可以让开发者定制自己的类型,实现更加自然和优雅的代码。但是,在使用运算符重载时也要注意遵守一些规则,以保证代码的正确性和可读性。
11.3、友元
在 C++ 中可以使用关键字 friend
声明一个函数或类为友元(friend),从而使得这个函数或类可以访问当前类的私有成员和受保护成员。
这里所谓的友元可以是一个普通函数、一个类的成员函数,也可以是另一个类本身。当我们将一个函数或类声明为另一个类的友元之后,它就可以在不使用任何访问控制符(public、protected、private)的情况下来访问当前类的成员。
在使用友元时需要注意以下几点:
-
友元关系不能继承:如果类
A
是类B
的友元,类C
继承自类B
,则类C
不能自动成为类A
的友元。 -
友元关系不具有传递性:如果类
A
是类B
的友元,类B
是类C
的友元,那么类A
不能自动成为类C
的友元。 -
友元关系是单向的:如果类
A
是类B
的友元,那么类B
不一定是类A
的友元。 -
友元关系是一种破坏封装性的技术,应该慎重使用。
友元可以提高程序的灵活性和效率,但是可能会破坏封装性,使得程序难以维护。因此,在使用友元时需要慎重考虑其必要性和安全性。
11.3.1、创建友元
在 C++ 中,要声明一个函数或类为另一个类的友元,需要在类的定义中使用 friend
关键字进行声明。下面是一个使用友元函数访问类私有成员的简单例子:
#include <iostream>
using namespace std;
class MyClass {
private:
int num;
public:
MyClass(int n): num(n) {}
friend void friendFunction(MyClass& obj); // 声明友元函数
};
void friendFunction(MyClass& obj) { // 定义友元函数
obj.num++; // 可以访问私有成员 num
cout << "friendFunction: " << obj.num << endl;
}
int main() {
MyClass obj(10);
friendFunction(obj);
cout << "main: " << obj.num << endl;
return 0;
}
在上面的例子中,类 MyClass
中声明了一个友元函数 friendFunction
,并在 main 函数中调用了该函数来访问类的私有成员变量 num
。friendFunction
函数的定义体内可以直接访问 MyClass
类的私有成员变量。
输出结果为:
friendFunction: 11
main: 11
从输出结果可以看出,friendFunction
函数已经成功地访问了 MyClass
类声明为私有的成员变量 num
。
我们也可以将另一个类声明为友元来访问当前类的私有成员。下面是一个使用友元类访问当前类私有成员的简单示例:
#include<iostream>
using namespace std;
class MyClass {
private:
int num;
friend class FriendClass; // 声明友元类
};
class FriendClass {
public:
void modify(MyClass &obj) {
obj.num++; // 可以访问私有成员 num
cout << "FriendClass: " << obj.num << endl;
}
};
int main() {
MyClass obj;
FriendClass fc;
fc.modify(obj);
return 0;
}
在上面的例子中,类 FriendClass
声明为 MyClass
的友元类,可以直接访问 MyClass
类的私有成员变量。FriendClass
中的 modify
方法可以直接修改 MyClass
类对象的 num
私有成员变量。
输出结果为:
FriendClass: 1
这个例子中,FriendClass
类成功访问了 MyClass
类中声明为私有的成员变量 num
。
11.3.2、常用的友元:重载<<运算符
在 C++ 中,ostream
类提供了一种非常方便的机制来向流中插入数据。对于自定义类,如果想要将对象插入到 ostream
流中,可以通过重载 <<
运算符来实现。
通常情况下,为了使重载 <<
运算符能够直接访问类的私有成员,我们需要将 <<
运算符重载函数声明为友元函数。下面是一个简单的例子:
#include <iostream>
using namespace std;
class MyClass {
private:
int num;
public:
MyClass(int n): num(n) {}
friend ostream& operator<<(ostream& os, const MyClass& obj); // 声明友元函数
};
ostream& operator<<(ostream& os, const MyClass& obj) { // 定义友元函数
os << "MyClass(num = " << obj.num << ")";
return os;
}
int main() {
MyClass obj(10);
cout << obj << endl; // 输出 MyClass(num = 10)
return 0;
}
在上面的例子中,operator<<
函数被重载为友元函数,使它可以直接访问 MyClass
类的私有成员 num
。在 main
函数中,我们使用 cout
流和插入运算符 <<
将 MyClass
对象 obj
插入到输出流中,输出对象的值。
输出结果为:
MyClass(num = 10)
可以看到,成功地输出了 MyClass
对象的私有成员 num
值。要注意的是,对于插入运算符重载函数,通常应该返回一个 ostream&
类型的引用,以支持链式编程风格。
11.4、重载运算符:作为成员函数还是非成员函数
在 C++ 中,运算符重载既可以作为类的成员函数,也可以作为非成员函数。那么如何选择哪种方式呢?需要考虑以下几个因素:
-
操作数数量:对于双目运算符,如果将运算符重载作为成员函数,只有一个参数是该类的对象,另一个参数可以是其他类型的对象或数据。如果将运算符重载作为非成员函数,需要将两个操作数作为参数传递进来。对于一元运算符,如果将其重载作为成员函数,只需要一个参数,即该类的对象;如果将其重载作为非成员函数,则需要将该类的对象作为参数传递进来。
-
操作数的属性:如果操作数全是该类的对象,那么将运算符重载作为成员函数更为自然。如果操作数包括其他类型的对象或数据,那么更倾向于将运算符重载作为非成员函数。
-
类型转换:如果将运算符重载作为成员函数,那么可以隐式地将当前对象转换为其他类型。如果将其作为非成员函数,则需要显式地进行类型转换。
-
对称性:运算符应该是具有对称性的。例如,当我们重载加法运算符时,对于
a + b
和b + a
两种操作应该得到相同的结果。如果将运算符重载作为成员函数,则只能支持a + b
操作;如果将其作为非成员函数,则可以支持a + b
和b + a
两种操作。
综上所述,如果运算符涉及到当前类对象的属性,可以将其重载作为成员函数。否则,可以将其重载作为非成员函数。下面是一个使用成员函数和非成员函数分别重载加法运算符的例子:
#include<iostream>
using namespace std;
class MyClass {
private:
int num;
public:
MyClass(int n): num(n) {}
MyClass operator+(const MyClass& obj) const { // 成员函数重载加法运算符
MyClass result(num + obj.num);
return result;
}
friend MyClass operator+(const MyClass& obj1, const MyClass& obj2); // 非成员函数重载加法运算符
};
MyClass operator+(const MyClass& obj1, const MyClass& obj2) { // 定义非成员函数
MyClass result(obj1.num + obj2.num);
return result;
}
int main() {
MyClass obj1(10), obj2(5);
MyClass obj3 = obj1 + obj2; // 成员函数重载
MyClass obj4 = obj1 + obj2; // 非成员函数重载
return 0;
}
在上面的例子中,首先定义了一个 MyClass
类,然后分别定义了使用成员函数和非成员函数重载加法运算符的方式。在 main
函数中定义了两个 MyClass
类的对象 obj1
和 obj2
,然后使用已重载的加法运算符分别得到了两个新对象 obj3
和 obj4
。
需要注意的是,非成员函数版本的重载函数必须在类定义外定义,并且必须声明为 friend
。在上面的例子中,将 operator+
函数声明为了友元函数,以便它可以访问 MyClass
类的私有成员。
总之,选择将运算符重载作为成员函数还是非成员函数取决于上述几个因素,开发者需要根据自己的需求进行选择。
11.5、再谈重载:一个矢量类
矢量类是计算机图形学和物理学中常用的一种数据结构,用于表示二维或三维空间中的向量。下面我们可以通过一个例子来展示如何设计一个矢量类,并重载运算符来简化其使用。
#include <iostream>
#include <cmath>
using namespace std;
class Vector {
public:
float x, y, z;
Vector(float x=0.0f, float y=0.0f, float z=0.0f): x(x), y(y), z(z) {}
// 重载运算符,用于计算两个向量的加减乘除
Vector operator+(const Vector &v) const { return Vector(x+v.x, y+v.y, z+v.z); }
Vector operator-(const Vector &v) const { return Vector(x-v.x, y-v.y, z-v.z); }
Vector operator*(float f) const { return Vector(x*f, y*f, z*f); }
Vector operator/(float f) const { return Vector(x/f, y/f, z/f); }
friend Vector operator*(float f, const Vector &v) { return Vector(f*v.x, f*v.y, f*v.z); }
// 重载运算符,用于计算两个向量的叉积、点积、判断是否相等
Vector operator^(const Vector &v) const { return Vector(y*v.z-z*v.y, z*v.x-x*v.z, x*v.y-y*v.x); }
float operator|(const Vector &v) const { return x*v.x+y*v.y+z*v.z; }
bool operator==(const Vector &v) const { return fabs(x-v.x)<1e-5 && fabs(y-v.y)<1e-5 && fabs(z-v.z)<1e-5; }
// 重载运算符,用于修改向量的值
Vector& operator+=(const Vector &v) { x+=v.x; y+=v.y; z+=v.z; return *this; }
Vector& operator-=(const Vector &v) { x-=v.x; y-=v.y; z-=v.z; return *this; }
Vector& operator*=(float f) { x*=f; y*=f; z*=f; return *this; }
Vector& operator/=(float f) { x/=f; y/=f; z/=f; return *this; }
// 重载运算符,用于求向量的长度、单位向量以及向量的输出
float operator!() const { return sqrt(x*x+y*y+z*z); }
Vector operator~() const { float len = !(*this); return Vector(x/len, y/len, z/len); }
friend ostream& operator<<(ostream &out, const Vector &v) { out<<"("<<v.x<<","<<v.y<<","<<v.z<<")"; return out; }
};
int main() {
Vector v1(1, 2, 3);
Vector v2(2, 3, 4);
Vector v3 = v1 + v2;
Vector v4 = v1 - v2;
Vector v5 = v1 * 2;
Vector v6 = v2 / 2;
Vector v7 = 2 * v1;
Vector v8 = v1 ^ v2;
float dot = v1 | v2;
bool isEqual = (v1 == v2);
cout << "v1: " << v1 << endl;
cout << "v2: " << v2 << endl;
cout << "v3 = v1+v2: " << v3 << endl;
cout << "v4 = v1-v2: " << v4 << endl;
cout << "v5 = v1*2: " << v5 << endl;
cout << "v6 = v2/2: " << v6 << endl;
cout << "v7 = 2*v1: " << v7 << endl;
cout << "v8 = v1^v2: " << v8 << endl;
cout << "dot = v1|v2: " << dot << endl;
cout << "isEqual = (v1==v2): " << isEqual << endl;
return 0;
}
在上面的代码中,我们定义了一个 Vector
类,并且重载了加减乘除、叉积、点积、判断相等、修改、长度和单位向量等运算符。我们通过这些运算符来实现了向量之间的加减乘除运算、长度和单位向量的计算、判断向量是否相等等操作。我们还同时定义了自己的输出运算符,用于实现向量的输出。
在 main
函数中,我们定义了两个向量 v1
和 v2
,并使用重载的运算符进行了一些操作。最后我们将结果输出到屏幕上。
需要注意的是,本例中的运算符重载函数都是定义为了类的成员函数。另外,我们还定义了一个友元函数 operator*
,用于将一个浮点数和一个向量相乘。
11.5.1、使用状态成员
在前文中我们提到了状态成员,其实可以在矢量类中使用状态成员来记录某些状态信息,例如当前向量是否被归一化过。
下面是一个修改后的 Vector
类的实现,其中增加了一个布尔类型的 normalized
状态成员,用于记录当前向量是否被归一化。
#include <iostream>
#include <cmath>
using namespace std;
class Vector {
public:
float x, y, z;
bool normalized;
Vector(float x=0.0f, float y=0.0f, float z=0.0f): x(x), y(y), z(z), normalized(false) {}
// 重载运算符,用于计算两个向量的加减乘除
Vector operator+(const Vector &v) const { return Vector(x+v.x, y+v.y, z+v.z); }
Vector operator-(const Vector &v) const { return Vector(x-v.x, y-v.y, z-v.z); }
Vector operator*(float f) const { return Vector(x*f, y*f, z*f); }
Vector operator/(float f) const { return Vector(x/f, y/f, z/f); }
friend Vector operator*(float f, const Vector &v) { return Vector(f*v.x, f*v.y, f*v.z); }
// 重载运算符,用于计算两个向量的叉积、点积、判断是否相等
Vector operator^(const Vector &v) const { return Vector(y*v.z-z*v.y, z*v.x-x*v.z, x*v.y-y*v.x); }
float operator|(const Vector &v) const { return x*v.x+y*v.y+z*v.z; }
bool operator==(const Vector &v) const { return fabs(x-v.x)<1e-5 && fabs(y-v.y)<1e-5 && fabs(z-v.z)<1e-5; }
// 重载运算符,用于修改向量的值
Vector& operator+=(const Vector &v) { x+=v.x; y+=v.y; z+=v.z; normalized = false; return *this; }
Vector& operator-=(const Vector &v) { x-=v.x; y-=v.y; z-=v.z; normalized = false; return *this; }
Vector& operator*=(float f) { x*=f; y*=f; z*=f; normalized = false; return *this; }
Vector& operator/=(float f) { x/=f; y/=f; z/=f; normalized = false; return *this; }
// 重载运算符,用于求向量的长度、单位向量以及向量的输出
float operator!() const {
if(normalized) return 1.0f;
return sqrt(x*x+y*y+z*z);
}
Vector operator~() const {
if(normalized) return *this;
float len = !(*this);
return Vector(x/len, y/len, z/len);
}
friend ostream& operator<<(ostream &out, const Vector &v) { out<<"("<<v.x<<","<<v.y<<","<<v.z<<")"; return out; }
// 归一化向量
void normalize() {
if(normalized) return;
float len = !(*this);
x /= len; y /= len; z /= len;
normalized = true;
}
};
int main() {
Vector v1(1, 2, 3);
Vector v2(2, 3, 4);
Vector v3 = v1 + v2;
Vector v4 = v1 - v2;
Vector v5 = v1 * 2;
Vector v6 = v2 / 2;
Vector v7 = 2 * v1;
Vector v8 = v1 ^ v2;
float dot = v1 | v2;
bool isEqual = (v1 == v2);
cout << "v1: " << v1 << endl;
cout << "v2: " << v2 << endl;
cout << "v3 = v1+v2: " << v3 << endl;
cout << "v4 = v1-v2: " << v4 << endl;
cout << "v5 = v1*2: " << v5 << endl;
cout << "v6 = v2/2: " << v6 << endl;
cout << "v7 = 2*v1: " << v7 << endl;
cout << "v8 = v1^v2: " << v8 << endl;
cout << "dot = v1|v2: " << dot << endl;
cout << "isEqual = (v1==v2): " << isEqual << endl;
v1.normalize();
Vector v9 = ~v1;
cout << "v1 normalized: " << v1 << endl;
cout << "v1 length: " << !v1 << endl;
cout << "v1 unit vector: " << v9 << endl;
return 0;
}
在上面的代码中,我们增加了一个布尔类型的 normalized
状态成员,初始化为 false
,用于记录当前向量是否被归一化过。在 normalize
函数中,我们首先判断当前向量是否被归一化过,如果已经被归一化过,则不需要再次执行计算。否则,我们计算出向量的长度并用其来进行归一化,将 normalized
修改为 true
。
在修改运算符(如 operator+=
)时,我们需要增加一个语句将 normalized
修改为 false
,因为当向量发生了改变时,其归一化状态也需要重新计算。
最后,在使用 !
操作符(用于计算向量长度)和 ~
操作符(用于计算单位向量)时,我们需要先判断当前向量是否被归一化过,如果已经被归一化过,则直接返回结果,否则,我们需要先计算向量的长度,再用其来进行归一化。
11.5.2、为Vector类重载算术运算符
在前面示例代码中,我们已经成功地为 Vector
类重载了一些运算符,例如加减乘除、叉积、点积等。现在让我们来看看如何为 Vector
类重载其他算术运算符。
首先我们介绍一下 C++ 语言中的算术运算符,包括加、减、乘、除以及求模等。C++ 支持对这些运算符进行重载,从而可以使得用户自定义类型的对象也支持这些运算符。
在下面的示例代码中,我们将会使用已经重载好的 operator+
来演示如何重载其他算术运算符。
#include <iostream>
using namespace std;
class Vector {
public:
float x, y, z;
Vector(float x=0.0f, float y=0.0f, float z=0.0f): x(x), y(y), z(z) {}
// 重载运算符,用于计算两个向量的加减乘除
Vector operator+(const Vector &v) const { return Vector(x+v.x, y+v.y, z+v.z); }
Vector operator-(const Vector &v) const { return Vector(x-v.x, y-v.y, z-v.z); }
Vector operator*(float f) const { return Vector(x*f, y*f, z*f); }
Vector operator/(float f) const { return Vector(x/f, y/f, z/f); }
friend Vector operator*(float f, const Vector &v) { return Vector(f*v.x, f*v.y, f*v.z); }
// 重载运算符,用于计算两个向量的叉积、点积、判断是否相等
Vector operator^(const Vector &v) const { return Vector(y*v.z-z*v.y, z*v.x-x*v.z, x*v.y-y*v.x); }
float operator|(const Vector &v) const { return x*v.x+y*v.y+z*v.z; }
bool operator==(const Vector &v) const { return x==v.x && y==v.y && z==v.z; }
// 重载运算符,用于修改向量的值
Vector& operator+=(const Vector &v) { x+=v.x; y+=v.y; z+=v.z; return *this; }
Vector& operator-=(const Vector &v) { x-=v.x; y-=v.y; z-=v.z; return *this; }
Vector& operator*=(float f) { x*=f; y*=f; z*=f; return *this; }
Vector& operator/=(float f) { x/=f; y/=f; z/=f; return *this; }
// 重载运算符,用于求向量的长度、单位向量以及向量的输出
float operator!() const { return sqrt(x*x+y*y+z*z); }
Vector operator~() const {
float len = !(*this);
return Vector(x/len, y/len, z/len);
}
friend ostream& operator<<(ostream &out, const Vector &v) { out<<"("<<v.x<<","<<v.y<<","<<v.z<<")"; return out; }
};
// 重载运算符,用于计算两个向量的乘积
Vector operator*(const Vector& v1, const Vector& v2) {
return Vector(v1.x * v2.x, v1.y * v2.y, v1.z * v2.z);
}
int main() {
Vector v1(1, 2, 3);
Vector v2(4, 5, 6);
Vector v3 = v1 + v2;
Vector v4 = v1 - v2;
Vector v5 = v1 * 2;
Vector v6 = v2 / 2;
Vector v7 = 2 * v1;
Vector v8 = v1 ^ v2;
float dot = v1 | v2;
bool isEqual = (v1 == v2);
Vector v9 = v1 * v2;
cout << "v1: " << v1 << endl;
cout << "v2: " << v2 << endl;
cout << "v3 = v1+v2: " << v3 << endl;
cout << "v4 = v1-v2: " << v4 << endl;
cout << "v5 = v1*2: " << v5 << endl;
cout << "v6 = v2/2: " << v6 << endl;
cout << "v7 = 2*v1: " << v7 << endl;
cout << "v8 = v1^v2: " << v8 << endl;
cout << "dot = v1|v2: " << dot << endl;
cout << "isEqual = (v1==v2): " << isEqual << endl;
cout << "v9 = v1*v2: " << v9 << endl;
return 0;
}
在上面的代码中,我们为 Vector
类新增了一个函数 operator*
,用于计算两个向量各分量的积,从而得到新的向量。
在 operator*
函数中,我们直接将两个向量 v1
和 v2
的各分量相乘,得到的结果就是一个新的向量。这里有一个要注意的地方,我们需要将该函数声明为全局函数(没有 Vector
类的作用域),才能正确地进行重载。
在主函数中,我们首先定义了两个 Vector
类对象 v1
和 v2
,然后演示了几个用 Vector
类对象进行的运算,包括加减乘除、叉积、点积、判断是否相等、求长、求单位向量以及重载的 operator*
运算符等。
注意,在实际开发中,尽量不要重载过多的运算符,以避免增加代码的复杂度和维护成本。
11.5.3、对实现的说明
在这个示例中,我们演示了如何在 C++ 中使用运算符重载来实现一个名为 Vector
的向量类。这个类实现了向量的加减乘除、叉积、点积、长度、单位化,以及用户定义的运算符 operator*
。
首先,在 Vector
类的定义中,我们定义了向量的三个成员变量 x、y 和 z,它们代表了向量在三个维度上的分量。
在 Vector
类中,我们重载了多个运算符,包括加减乘除、叉积、点积、判断相等等。对于每一个运算符,我们都实现了对应的成员函数,例如重载加法运算符 +
,我们定义了一个成员函数 operator+
,它接受一个参数,并返回一个新的 Vector
对象,表示当前向量加上参数表示的另一个向量。
在实现 operator*
这个用户定义的运算符时,我们采用了全局函数的方式进行重载,我们定义了一个名为 operator*
的函数,接受两个 Vector
对象作为参数,并返回一个新的 Vector
,其中每个分量都是两个向量相应分量的乘积。
对于一些运算符,例如加减乘除,我们还提供了相应的缩写方式,例如定义 operator+=
表示向量的原地加法。
最后,我们还定义了打印向量的操作,即 operator<<
,用于将向量的三个分量打印出来。
在实际应用中,向量类通常是数学库或者计算机图形学中最常用的类之一。运算符重载可以极大地简化向量类的使用,使其更接近数学公式的书写形式。在实现向量类时,我们还需考虑向量的精度问题,例如使用浮点数不同的取值范围、进行加减乘除时的精度损失等。为了更好的扩展性和兼容性,我们还应该考虑定义向量类时的坐标系问题,例如局部坐标系还是全局坐标系等。
11.5.4、使用Vector类来模拟随机漫步
随机漫步是一种基本的随机模型,可以用于模拟许多自然界和社会现象。为了模拟随机漫步,我们可以使用 Vector
类来表示漫步的方向和距离。
首先,我们定义一个 Walker
类,它包含一个 position
成员变量和一个 step_size
成员变量,分别表示漫步者的当前位置和每一步的长度。
我们在 Walker
类中实现一个名为 step
的成员函数,它利用向量相加来模拟一次随机漫步,即漫步者随机选择一个方向,向这个方向前进一个步长。具体的实现过程如下:
-
首先,我们使用随机数生成器来随机选择一个方向,即生成一个矢量,其分量随机取值在 [−1,1][−1,1] 之间;
-
接着,我们对这个向量进行长度归一化(即转换为单位向量),然后与当前位置向量相加,得到下一步的位置向量。
-
最后,我们将漫步者的位置更新为下一步的位置向量,完成一次随机漫步。
我们可以多次调用 step
函数,来模拟多步的随机漫步。
下面是一个简单的示例代码:
#include <iostream>
#include <random>
#include "vector.h" // Vector 类的头文件
using namespace std;
class Walker {
public:
Walker(double x, double y, double z, double step_size)
: position_(x, y, z), step_size_(step_size) {}
void step() {
// 生成随机方向
random_device rd;
default_random_engine eng(rd());
uniform_real_distribution<double> distr(-1.0, 1.0);
Vector direction(distr(eng), distr(eng), distr(eng));
// 将方向向量归一化为单位向量
direction.normalize();
// 计算下一步的位置
auto next_position = position_ + direction * step_size_;
// 更新漫步者的位置
position_ = next_position;
}
void print_position() const {
cout << "(" << position_.x() << ", " << position_.y() << ", "
<< position_.z() << ")" << endl;
}
private:
Vector position_;
double step_size_;
};
int main() {
Walker w(0, 0, 0, 0.1);
for (int i = 0; i < 100; ++i) {
w.step();
w.print_position();
}
return 0;
}
上述代码中,我们使用 default_random_engine
和 uniform_real_distribution
来实现了一个能够生成在 [−1,1][−1,1] 之间均匀分布的随机数的随机发生器。在 step
函数中,我们使用 normalize
函数将随机生成的方向向量归一化为单位向量。最后,我们通过多次调用 step
函数来模拟多步随机漫步,并通过 print_position
函数输出漫步者的位置。
需要注意的是,这个实现只是模拟了一个简单的三维随机漫步,并没有考虑障碍物的阻挡、漫步的边界等问题。实际应用中,我们需要根据具体的应用需求,对随机漫步算法进行进一步的优化和改进。
11.6、类的自动转换和强制类型转换
在 C++ 中,一个类可以通过类型转换方式转换为另一个类,这种转换可以分为自动转换和强制类型转换。
1、自动转换
自动转换指的是当表达式中包含不同类型的类对象时,编译器会自动将其中一个对象的类型转换为另一个对象的类型。这种转换是隐式进行的,无需进行任何显式的类型转换。
常见的自动转换方式有:
从派生类转换为基类,如:
class Base {};
class Derived : public Base {};
Derived d;
Base b = d; // 自动从 Derived 类型转换为 Base 类型
从数字类型转换为类类型,如:
class Vector {
...
};
double x = 1.0, y = 2.0, z = 3.0;
Vector v = { x, y, z }; // 自动从数字类型转换为 Vector 类型
从类类型转换为数字类型或字符类型,如:
class Vector {
...
};
Vector v(1.0, 2.0, 3.0);
double length = v.length(); // 自动将 Vector 类型转换为 double 类型
char ch = v[0]; // 自动将 Vector 类型转换为字符类型
2、强制类型转换
当需要进行类型转换时,可以使用强制类型转换。这种转换需要通过显式的强制类型转换语法来实现,可以将任意类型的对象转换为另一个类型。
C++ 提供了四种强制类型转换方式:
-
static_cast
:用于进行用于非多态类型的转换,在编译时检查类型安全。 -
dynamic_cast
:用于进行运行时多态类型的转换,会进行类型检查,如果转换失败则返回空指针。 -
const_cast
:用于去除const
和volatile
限定符。 -
reinterpret_cast
:用于进行底层的强制类型转换,可能会导致未定义行为。
使用强制类型转换时需要非常小心,不当的类型转换可能会导致程序出现不可预期的行为,而且强制类型转换也可能会破坏代码的类型安全性。在进行强制类型转换时,需要尽可能地保证转换的正确性,并检查转换后的分配对象是否满足要求。
11.6.1、转换函数
C++ 中,我们还可以通过定义转换函数来实现类的自动类型转换。转换函数是一种特殊的成员函数,它可以将当前类对象转换为指定类型的对象。
转换函数是一种非常强大的类型转换方式,但也需要小心使用。因为转换函数会将类对象自动转换为其他类型的对象,所以它容易引起类型转换的意外和混淆。
下面是一个实现 Vector
向 double
的自动类型转换的示例:
#include <iostream>
#include <cmath>
class Vector {
public:
Vector(double x = 0.0, double y = 0.0, double z = 0.0)
: x_(x), y_(y), z_(z) {}
double x() const { return x_; }
double y() const { return y_; }
double z() const { return z_; }
double length() const { return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_); }
operator double() const { return length(); } // 转换函数
private:
double x_, y_, z_;
};
int main() {
Vector v(1.0, 2.0, 3.0);
double length = v; // 将 Vector 对象自动转换为 double 类型
std::cout << length << std::endl; // 输出 Vector 的长度
return 0;
}
上述代码中,我们在 Vector
类中定义了一个名为 operator double
的转换函数,它将 Vector
类型自动转换为 double
类型,这里的实现方式是返回向量的长度。
在 main
函数中,我们将一个 Vector
对象赋值给一个 double
类型的变量,此时会自动调用 Vector
类的转换函数,将 Vector
对象转换为 double
类型的对象,输出向量的长度。而这个转换函数的定义使得这个转换过程变得非常自然和便利。
需要注意的是,转换函数虽然方便,但也会使得代码的意图变得更加隐蔽和难以理解,因此在使用时需要谨慎,建议只在一些比较常见和易于理解的情况下使用。此外,在使用转换函数时,我们还应该遵循一些良好的实践和设计原则,例如尽可能保持转换函数的简单性和透明性,并使用注释或者命名来明确转换函数的语义和行为。
11.6.2、转换函数和友元函数
在 C++ 中,我们还可以通过友元函数来实现类的类型转换。具体来说,如果我们希望让某个非类类型通过调用类的成员函数来进行转换,但是又不希望将转换函数作为类的成员函数来定义,那么可以定义一个友元函数来实现这个功能。
下面是一个示例代码,演示了如何通过友元函数的方式实现类的类型转换:
#include <iostream>
#include <string>
class Date {
public:
Date(int year, int month, int day)
: year_(year), month_(month), day_(day) {}
friend std::ostream& operator<<(std::ostream& os, const Date& date);
operator std::string() const; // 转换函数声明
private:
int year_, month_, day_;
};
std::ostream& operator<<(std::ostream& os, const Date& date) {
os << date.year_ << "-" << date.month_ << "-" << date.day_;
return os;
}
Date::operator std::string() const { // 转换函数定义
return std::to_string(year_) + "-" +
std::to_string(month_) + "-" +
std::to_string(day_);
}
int main() {
Date date(2023, 5, 8);
std::cout << date << std::endl; // 输出 Date 对象
std::string str = date; // 使用友元函数将 Date 对象转换为字符串
std::cout << str << std::endl; // 输出字符串
return 0;
}
上述代码中,我们在 Date
类中定义了一个名为 operator std::string
的转换函数,并通过 friend
关键字将 <<
运算符重载函数定义为 Date
类的友元函数。这意味着在定义 <<
运算符重载函数时,可以访问 Date
类的私有数据成员。
在 main
函数中,我们通过 <<
运算符在控制台输出一个 Date
对象,然后使用转换函数将 Date
对象转换为一个字符串,最后在控制台输出这个字符串。
需要注意的是,虽然使用友元函数可以实现类的类型转换,但这并不意味着我们应该经常使用它。友元函数的使用应该基于明确的设计考虑,并尽可能避免过度使用,以免引起代码的混淆和不必要的复杂性。