第13讲——使用类
前面我们学过了使用类定义用户的类型以及类的两个特殊的成员函数——构造函数和析构函数,在这一讲中我们将学习啥呢?往下看吧!!
在这一讲中,我们将学习运算符重载、友元函数以及类的自动转换和强制转换类型。
【运算符重载】
- 【概述】
我们之前学过了函数重载(或函数多态),旨在让我们能够用同名函数来完成相同的基本操作,即使这种操作被用于不同的数据类型。
如今,运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多种含义。实际上,很多运算符已经被重载,比如*运算符,将它用于地址,将得到存储在这个地址中的值,但将它用于两个数字时,得到的是它们的乘积。C++根据操作数的数目和类型来决定采用哪种操作。
运算符重载的革命性意义在于它被扩展到用户定义的类型,例如可以使用+将两个对象相加。
要重载运算符,需使用一种特殊函数形式,它被称为运算符函数。运算符函数的格式如下:
operator op (参数列表) //注:应该没有空格,此处只为了表达清楚
例如,operator*()重载*运算符,operator-()重载-运算符,operator[]()将重载[]运算符。op必须是有效的C++运算符,不能虚构一个新的符号(如@)。
- 【示例】
下面我们看一个运算符重载示例,我们将通过这个示例解读运算符重载的“神秘”:
如果今天早上在Tom的账户上花费了2小时35分钟,下午又花费了2小时40分钟,则总共花了多长时间呢?这个示例与加法概念很吻合,但要相加的单位(小时与分钟的混合)与内置类型不匹配。我们将采用一个使用成员函数来处理加法的Time类。首先使用一个名为Sum()的成员函数,然后介绍如何将其转换为重载运算符。
首先是这个类的声明:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime0.h -- Time class before operator overloading 2 #ifndef MYTIME0_H_ 3 #define MYTIME0_H_ 4 5 class Time 6 { 7 private: 8 int hours; 9 int minutes; 10 public: 11 Time(); 12 Time(int h, int m = 0); 13 void AddMin(int m); 14 void AddHr(int h); 15 void Reset(int h = 0, int m = 0); 16 const Time Sum(const Time & t) const; 17 void Show() const; 18 }; 19 #endif
由Time类声明,我们知道Time类提供了用于调整和重新设置时间、显示时间、将两个时间相加的方法。
接下来是这个类的方法定义:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime0.cpp -- implementing Time methods 2 #include <iostream> 3 #include "mytime0.h" 4 5 Time::Time() 6 { 7 hours = minutes = 0; 8 } 9 10 Time::Time(int h, int m ) 11 { 12 hours = h; 13 minutes = m; 14 } 15 16 void Time::AddMin(int m) 17 { 18 minutes += m; 19 hours += minutes / 60; 20 minutes %= 60; 21 } 22 23 void Time::AddHr(int h) 24 { 25 hours += h; 26 } 27 28 void Time::Reset(int h, int m) 29 { 30 hours = h; 31 minutes = m; 32 } 33 34 const Time Time::Sum(const Time & t) const 35 { 36 Time sum; 37 sum.minutes = minutes + t.minutes; 38 sum.hours = hours + t.hours + sum.minutes / 60; 39 sum.minutes %= 60; 40 return sum; 41 } 42 43 void Time::Show() const 44 { 45 std::cout << hours << " hours, " << minutes << " minutes"; 46 }
在Sum()函数中,我们注意到返回类型不是引用。因为函数将创建一个新的Time对象(sum),来表示另外两个Time对象的和。返回对象(如代码所做的那样)将创建对象的副本,而调用函数可以使用它。然而,如果返回类型为Time&,则引用的将是sum对象。但由于sum对象是局部变量,在函数结束时将被删除,因此引用将指向一个不存在的对象。使用返回类型Time意味着程序将在删除sum之前构造它的拷贝,调用函数将得到该拷贝。即不要返回指向局部变量或临时对象的引用。
最后是测试程序:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // usetime0.cpp -- using the first draft of the Time class 2 // compile usetime0.cpp and mytime0.cpp together 3 #include <iostream> 4 #include "mytime0.h" 5 6 int main() 7 { 8 using std::cout; 9 using std::endl; 10 Time planning; 11 Time coding(2, 40); 12 Time fixing(5, 55); 13 Time total; 14 15 cout << "planning time = "; 16 planning.Show(); 17 cout << endl; 18 19 cout << "coding time = "; 20 coding.Show(); 21 cout << endl; 22 23 cout << "fixing time = "; 24 fixing.Show(); 25 cout << endl; 26 27 total = coding.Sum(fixing); 28 cout << "coding.Sum(fixing) = "; 29 total.Show(); 30 cout << endl; 31 return 0; 32 }
上面就是一套完整的程序,接下来我们要展示如何进行运算符重载了。
首先是添加加法运算符:
将Time类转换为重载的加法运算符很容易,只要将Sum()的名称改为operator+()即可(此时的成员函数名变为operator+)。这样做是对的,只要把运算符(这里为+)放到operator的后面,并将结果(operator+)用作方法名即可。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime1.h -- Time class before operator overloading 2 #ifndef MYTIME1_H_ 3 #define MYTIME1_H_ 4 5 class Time 6 { 7 private: 8 int hours; 9 int minutes; 10 public: 11 Time(); 12 Time(int h, int m = 0); 13 void AddMin(int m); 14 void AddHr(int h); 15 void Reset(int h = 0, int m = 0); 16 Time operator+(const Time & t) const; 17 void Show() const; 18 }; 19 #endif
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime1.cpp -- implementing Time methods 2 #include <iostream> 3 #include "mytime1.h" 4 5 Time::Time() 6 { 7 hours = minutes = 0; 8 } 9 10 Time::Time(int h, int m ) 11 { 12 hours = h; 13 minutes = m; 14 } 15 16 void Time::AddMin(int m) 17 { 18 minutes += m; 19 hours += minutes / 60; 20 minutes %= 60; 21 } 22 23 void Time::AddHr(int h) 24 { 25 hours += h; 26 } 27 28 void Time::Reset(int h, int m) 29 { 30 hours = h; 31 minutes = m; 32 } 33 34 Time Time::operator+(const Time & t) const 35 { 36 Time sum; 37 sum.minutes = minutes + t.minutes; 38 sum.hours = hours + t.hours + sum.minutes / 60; 39 sum.minutes %= 60; 40 return sum; 41 } 42 43 void Time::Show() const 44 { 45 std::cout << hours << " hours, " << minutes << " minutes"; 46 }
和Sum()一样,operator+()也是由Time对象调用的,它将第二个Time对象作为参数,并返回一个Time对象。因此可以像调用Sum()那样来调用operator+()方法:
total = coding.operator+(fixing); //函数表示法
但将该方法命令为operator+()后,也可以使用运算符表示法:
total = coding + fixing; //运算符表示法
这两种方法都将调用operator+()方法。注意,在运算符表示法中,运算符左侧的对象(这里为coding)是调用对象,运算符右边的对象(这里为fixing)是作为参数被传递的对象。下面这段程序将说明这一点:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // usetime1.cpp -- using the second draft of the Time class 2 // compile usetime1.cpp and mytime1.cpp together 3 #include <iostream> 4 #include "mytime1.h" 5 6 int main() 7 { 8 using std::cout; 9 using std::endl; 10 Time planning; 11 Time coding(2, 40); 12 Time fixing(5, 55); 13 Time total; 14 15 cout << "planning time = "; 16 planning.Show(); 17 cout << endl; 18 19 cout << "coding time = "; 20 coding.Show(); 21 cout << endl; 22 23 cout << "fixing time = "; 24 fixing.Show(); 25 cout << endl; 26 27 total = coding + fixing; 28 // operator notation 29 cout << "coding + fixing = "; 30 total.Show(); 31 cout << endl; 32 33 Time morefixing(3, 28); 34 cout << "more fixing time = "; 35 morefixing.Show(); 36 cout << endl; 37 total = morefixing.operator+(total); 38 // function notation 39 cout << "morefixing.operator+(total) = "; 40 total.Show(); 41 cout << endl; 42 return 0; 43 }
总之,operator+()函数的名称使得可以使用函数表示法或运算符表示法来调用它。编译器将根据操作数的类型来确定如何做:
Time t1, t2, t3, t4;
t3 = t1 + t2; //编译器根据t1、t2的类型决定+运算符的用法 t4 = t1 + t2 + t3; //valid?
我们知道t3 = t1 + t2是合法的,那么t4 = t1 + t2 + t3是否合法呢?
为回答这个问题,我们来看一下上述语句将被如何转换为函数调用。由于+是从左向右结合的运算符,因此上述语句首先被转换成下面这样:
t4 = t1.operator+(t2 + t3); //valid?
然后,函数参数本身被转换成一个函数调用,结果如下:
t4 = t1.operator+(t2.operator+(t3)); //valid? YES
上述语句是合法的,因为函数调用t2.operator+(t3)返回一个Time对象(t2和t3的和)。然后,该对象成为函数调用t1.operator+()的参数,该调用返回t1与表示t2和t3之和的Time对象的和。总之,最后的返回值正是t1、t2、t3之和。
然后我们介绍运算符重载的限制:
大多数的运算符(+ - * / % ^ & | ~= != < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- , -> () [] new delete new [] delete [])都可以使用上面的方式重载。
重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。
下面就是C++对用户定义的运算符重载的限制:
- 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符;(因此不能将减号运算符-重载为计算两个double值的和,而不是它们的差。虽然这种限制将对创造性有所影响,但可以确保程序正常运行)
- 使用运算符时不能违反运算符原来的句法规则;例如不能将求模运算符%重载成使用一个操作数:
int x; Time shiva; % x; //invalid for modulus operator % shiva; //invalid for overloaded operator
- 不能修改运算符的优先级;(如果将加号运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级)
- 不能创建新运算符;(例如,不能定义operator**()函数来表示求幂)
- 不能重载以下运算符(sizeof . .* :: ?: typeid static_cast);
- 下面的运算符只能通过成员函数进行重载(== () [] ->)。
最后我们介绍一些其他的重载运算符:
还有一些其他的操作对Time类来说是有意义的。例如,可能要将两个时间相减或将时间乘以一个因子,这需要重载减法和乘法运算符。这和重载加法运算符采用的技术相同,即创建operator-()和operator*()方法。
也就是说,可以将下面的原型添加到类声明中:
Time operator-(const time& t) const;
Time operator*(double n) const;
于是可以得到新的头文件:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime2.h -- Time class after operator overloading 2 #ifndef MYTIME2_H_ 3 #define MYTIME2_H_ 4 5 class Time 6 { 7 private: 8 int hours; 9 int minutes; 10 public: 11 Time(); 12 Time(int h, int m = 0); 13 void AddMin(int m); 14 void AddHr(int h); 15 void Reset(int h = 0, int m = 0); 16 Time operator+(const Time & t) const; 17 Time operator-(const Time & t) const; 18 Time operator*(double n) const; 19 void Show() const; 20 }; 21 #endif
然后将新增方法的定义添加到实现文件中:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime2.cpp -- implementing Time methods 2 #include <iostream> 3 #include "mytime2.h" 4 5 Time::Time() 6 { 7 hours = minutes = 0; 8 } 9 10 Time::Time(int h, int m ) 11 { 12 hours = h; 13 minutes = m; 14 } 15 16 void Time::AddMin(int m) 17 { 18 minutes += m; 19 hours += minutes / 60; 20 minutes %= 60; 21 } 22 void Time::AddHr(int h) 23 { 24 hours += h; 25 } 26 27 void Time::Reset(int h, int m) 28 { 29 hours = h; 30 minutes = m; 31 } 32 33 Time Time::operator+(const Time & t) const 34 { 35 Time sum; 36 sum.minutes = minutes + t.minutes; 37 sum.hours = hours + t.hours + sum.minutes / 60; 38 sum.minutes %= 60; 39 return sum; 40 } 41 42 Time Time::operator-(const Time & t) const 43 { 44 Time diff; 45 int tot1, tot2; 46 tot1 = t.minutes + 60 * t.hours; 47 tot2 = minutes + 60 * hours; 48 diff.minutes = (tot2 - tot1) % 60; 49 diff.hours = (tot2 - tot1) / 60; 50 return diff; 51 } 52 53 Time Time::operator*(double mult) const 54 { 55 Time result; 56 long totalminutes = hours * mult * 60 + minutes * mult; 57 result.hours = totalminutes / 60; 58 result.minutes = totalminutes % 60; 59 return result; 60 } 61 62 void Time::Show() const 63 { 64 std::cout << hours << " hours, " << minutes << " minutes"; 65 }
最后是测试程序:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // usetime2.cpp -- using the third draft of the Time class 2 // compile usetime2.cpp and mytime2.cpp together 3 #include <iostream> 4 #include "mytime2.h" 5 6 int main() 7 { 8 using std::cout; 9 using std::endl; 10 Time weeding(4, 35); 11 Time waxing(2, 47); 12 Time total; 13 Time diff; 14 Time adjusted; 15 16 cout << "weeding time = "; 17 weeding.Show(); 18 cout << endl; 19 20 cout << "waxing time = "; 21 waxing.Show(); 22 cout << endl; 23 24 cout << "total work time = "; 25 total = weeding + waxing; // use operator+() 26 total.Show(); 27 cout << endl; 28 29 diff = weeding - waxing; // use operator-() 30 cout << "weeding time - waxing time = "; 31 diff.Show(); 32 cout << endl; 33 34 adjusted = total * 1.5; // use operator+() 35 cout << "adjusted work time = "; 36 adjusted.Show(); 37 cout << endl; 38 // std::cin.get(); 39 return 0; 40 }
重温这些知识,我做一点读书笔记:运算符重载,在这儿运用到用户定义的类型(类),那么将会在类声明、类实现文件中有相关的表示(譬如,将sum()修改为operator-()),这些表示尽管停留在表象,但就足以给我们足够的提示——关键字operator的重要性。
【友元】
对于类的私有成员,我们现在只知道使用类的公有成员函数来访问,这种限制太严格,以致于不适合特定的编程。在这种情况下,C++提供了另外一种形式的访问权限:友元。
友元有三种:友元函数、友元类、友元成员函数。
我们现在只介绍友元函数,其他两种友元请转至。
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
在介绍如何成为友元之前,我们先学习一下为何需要友元。
在为类重载二元运算符时,常常需要友元。将Time对象乘以实数就属于这种情况:
在前面的Time类示例中,重载的乘法运算符与其他两种重载运算符的差别在于——它使用了两种类型。即加法和减法运算符都结合两个Time值,而乘法运算符将一个Time值与一个double值结合在一起。这限制了该运算符的使用方式。记住,左侧的操作数是调用对象(这个对象调用类成员函数)。
也就是说,下面的语句:
A = B * 2.75;
将被转换为下面的成员函数调用:
A = B.operator*(2.75);
但下面的语句又如何呢?
A = 2.75 * B;
从概念上说,B*2.75应与2.75*B相同,但第二个表达式不对应于成员函数,因为2.75不是Time类型的对象。记住,左侧的操作数应是调用对象,但2.75不是对象。因此,编译器不能使用成员函数调用来替换该表达式。
解决这个难题的一种方式是——非成员函数(记住,大多数运算符都可以通过成员或非成员函数来重载)。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。这样,编译器能够将下面的表达式:
A = 2.75 * B;
与下面的非成员函数调用匹配:
A = operator*(2.75, B);
该函数的原型如下:
Time operator*(double m, const Time& t);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,也就是说,double值乘以Time值。
使用非成员函数可以按所需的顺序获得操作数(先是double,然后是Time),但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。
然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。
2.1创建友元函数
(1)将友元函数的原型放在类声明中,并在原型声明前加上关键字friend:
friend Time operator*(double m, const Time& t);
该原型意味着:①虽然operator*()函数实在类声明中声明的,但它不是成员函数;②虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
(2)编写函数定义。
因为友元函数不是类的成员函数,所以不要使用Time::限定符,而且在函数定义中也不需要使用friend关键字:
Time operator*(double m, const Time& t)
{
Time result;
long totalminutes = t.hours * mult * 60 + t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
有了上述声明和定以后,下面的语句:
A = 2.75 * B;
将转换为如下语句,从而调用刚才定义的非成员友元函数:
A = operator*(2.75, B);
总之,类的友元函数是非成员函数,其访问权限与成员函数相同。
重温笔记1:友元函数能访问类的私有成员,而且它可以将形式“对象.成员函数”改为“友元函数(对象)”,并由此打破了语法上的一些限制。
重温笔记2:如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
实际上,按下面的方式对定义进行修改(交换乘法操作数的顺序),可以将这个友元函数编写为非友元函数:
Time operator*(double m, const Time& t) { return t * m; }
原来的版本显式地访问t.minutes和t.hours,所以它必须是友元。这个版本将Time对象t作为一个整体使用,让成员函数来处理私有值,因此不必是友元。然而,将该版本作为友元也是一个好主意。最重要的是,它将作为正式类接口的组成部分。其次,如果以后发现需要函数直接访问私有数据,则只要修改函数定义即可,而不必修改类原型。
2.2 常用的友元:重载<<运算符
我们学习了运算符重载后,便很从容地使用运算符<<与cout一起显示对象的内容。哈哈,这个运算符重载为何要放在友元的大标题下面写啊,因为这个运算符重载的实现过程涉及到了友元。下面我们一起来看看吧!
假设trip是一个Time对象,为显示Time值,我们可以“cout<<trip;”。ostream类对运算符<<进行了重载,将其转换为一个输出工具。前面讲过,cout是一个ostream对象,它能识别所有的C++基本类型,这是因为对于每种基本类型,ostream类都包含了相应的重载的operator<<()定义。也就是说,一个定义使用int参数,一个定义使用double参数,等等。因此,要使cout能够识别Time对象,一种方式是将一个新的函数运算符定义添加到ostream类声明中,但修改iostream文件是个危险的主意,这样做会在标准接口上浪费时间。而我们的第二种方式就是——通过Time类声明来让Time类知道如何使用cout。
要让Time类知道使用cout,就必须使用友元函数。假设我们使用一个Time成员函数来重载<<,Time对象将是第一个操作数,也就是会这样使用<<:
trip << cout;
而我们理想中是这样使用<<运算符的:
cout << trip; //包含两个对象,第一个是ostream类对象(cout)
所以我们只能使用友元函数,于是可以像下面这样重载运算符:
void operator<<(ostream & os, const Time & t) //接受一个ostream参数和一个Time参数 { os<<t.hours<<" hours, "<<t.minutes<<" minutes"; }
新的Time类声明使operator<<()函数成为Time类的一个友元函数。但要知道,该函数并不是ostream类的友元(这样就不要再修改iostream文件),即使operator<<()函数看上去必须是ostream类的友元(因为它接受一个ostream参数)。因为该函数从始至终都将ostream对象作为一个整体使用,即它并不直接访问ostream对象的私有成员,所以并不一定必须是ostream类的友元。而之所以该函数是Time类的友元,是因为它直接访问Time类的私有成员,所以它必须是Time类的友元。
重温笔记:上面这段话,已经将区别友元函数的方法讲的很透彻了。
新的operator<<()定义使用ostream引用os作为它的第一个参数。之所以用引用,是因为调用cout<<trip应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式cout<<trip将导致os成为cout的一个别名。而Time对象既可以按值来传递,也可以按引用来传递,因为这两种形式都使函数能够使用对象的值。此处我们选择按引用传递,是因为按引用传递使用的内存和时间都比按值传递少。
上面这段函数定义在具体使用<<运算符时存在一个问题。
像下面这样的语句可以正常工作:
cout << trip;
但像下面这样的语句就不能使用:
cout<<"Trip time: "<<trip<<" (Tuesday)\n"; //can't do
要知道背后的原因,就必须了解关于cout操作的一点知识。
请看下面的语句:
int x = 5, y = 8; cout<<x<<y;C++从左至右读取输出语句,意味着它等同于:
cout<<x)<<y;iostream中定义,<<运算符要求左边是一个ostream对象。显然,因为cout是ostream对象,所以表达式cout<<x满足这种要求。所以当表达式cout<<x位于<<运算符的左侧时,输出语句也要求该表达式是一个ostream类的对象。因此,ostream类将operator<<()函数实现为返回一个指向ostream对象的引用。具体地说,它返回一个指向调用对象(这里是cout)的引用。因此,表达式(cout<<x)本身就是ostream对象cout,从而可以位于<<运算符的左侧。
所以我们可以对友元函数采用相同的方法,只要让它返回ostream对象的引用即可:
ostream & operator<<(ostream & os, const Time & t) { os<<t.hours<<" hours, "<<t.minutes<<" minutes"; return os; }
于是,该函数开始执行时,程序传递一个对象引用(os)给它,最后,函数的返回值就是传递给它的对象。
即语句“cout<<trip;”将被转换为如此调用:“operator<<(cout, trip);”,并且该调用将返回cout对象。因此,下面的语句可以正常工作:
cout<<"Trip time: "<<trip<<" (Tuesday)\n"; //can do
为了加深印象,我们再来看看这条语句是如何工作的:
首先,下面的代码调用ostream中的<<定义,它显示字符串并返回cout对象:
cout<<"Trip time: "
因此表达式cout<<"Trip time: "将显示字符串,然后被它的返回值cout所替代。
于是,原来的语句被简化为下面的形式:
cout<<trip<<" (Tuesday)\n";
接下来,程序使用<<的Time声明显示trip值,并再次返回cout对象。
于是,语句又被简化为:
cout<<" (Tuesday)\n";
最后,程序使用ostream中用于字符串的<<定义,来显示最后一个字符串,并结束运行。
有趣的是,这个operator<<()版本还可用于将输出写入到文件中:
#include <fstream> ... ofstream fout; fout.open("savetime.txt"); Time trip(12,40); fout<<trip;其中最后一条语句将被转换为:operator<<(fout, trip);
另外,类继承属性让ostream引用能够指向ostream对象和ofstream对象。
重温笔记:要重载运算符<<来显示某类的对象,可使用友元函数。
下面是修改后的类定义:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime3.h -- Time class with friends 2 #ifndef MYTIME3_H_ 3 #define MYTIME3_H_ 4 #include <iostream> 5 6 class Time 7 { 8 private: 9 int hours; 10 int minutes; 11 public: 12 Time(); 13 Time(int h, int m = 0); 14 void AddMin(int m); 15 void AddHr(int h); 16 void Reset(int h = 0, int m = 0); 17 Time operator+(const Time & t) const; 18 Time operator-(const Time & t) const; 19 Time operator*(double n) const; 20 friend Time operator*(double m, const Time & t) 21 { return t * m; } // inline definition 22 friend std::ostream & operator<<(std::ostream & os, const Time & t); 23 24 }; 25 #endif
这段代码里包含了operator*()和operator<<()这两个友元函数。它将第一个友元函数作为内联函数,因为其代码很短。
当友元函数定义同时也是原型时,要使用friend关键字作为前缀。
再给出类实现和示例程序:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // mytime3.cpp -- implementing Time methods 2 #include "mytime3.h" 3 4 Time::Time() 5 { 6 hours = minutes = 0; 7 } 8 9 Time::Time(int h, int m ) 10 { 11 hours = h; 12 minutes = m; 13 } 14 15 void Time::AddMin(int m) 16 { 17 minutes += m; 18 hours += minutes / 60; 19 minutes %= 60; 20 } 21 22 void Time::AddHr(int h) 23 { 24 hours += h; 25 } 26 27 void Time::Reset(int h, int m) 28 { 29 hours = h; 30 minutes = m; 31 } 32 33 Time Time::operator+(const Time & t) const 34 { 35 Time sum; 36 sum.minutes = minutes + t.minutes; 37 sum.hours = hours + t.hours + sum.minutes / 60; 38 sum.minutes %= 60; 39 return sum; 40 } 41 42 Time Time::operator-(const Time & t) const 43 { 44 Time diff; 45 int tot1, tot2; 46 tot1 = t.minutes + 60 * t.hours; 47 tot2 = minutes + 60 * hours; 48 diff.minutes = (tot2 - tot1) % 60; 49 diff.hours = (tot2 - tot1) / 60; 50 return diff; 51 } 52 53 Time Time::operator*(double mult) const 54 { 55 Time result; 56 long totalminutes = hours * mult * 60 + minutes * mult; 57 result.hours = totalminutes / 60; 58 result.minutes = totalminutes % 60; 59 return result; 60 } 61 62 std::ostream & operator<<(std::ostream & os, const Time & t) 63 { 64 os << t.hours << " hours, " << t.minutes << " minutes"; 65 return os; 66 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //usetime3.cpp -- using the fourth draft of the Time class 2 // compile usetime3.cpp and mytime3.cpp together 3 #include <iostream> 4 #include "mytime3.h" 5 6 int main() 7 { 8 using std::cout; 9 using std::endl; 10 Time aida(3, 35); 11 Time tosca(2, 48); 12 Time temp; 13 14 cout << "Aida and Tosca:\n"; 15 cout << aida<<"; " << tosca << endl; 16 temp = aida + tosca; // operator+() 17 cout << "Aida + Tosca: " << temp << endl; 18 temp = aida* 1.17; // member operator*() 19 cout << "Aida * 1.17: " << temp << endl; 20 cout << "10.0 * Tosca: " << 10.0 * tosca << endl; 21 // std::cin.get(); 22 return 0; 23 }
友元函数到此正式告一段落!!
学会了友元函数,我们再来看重载运算符的一个问题——作为成员函数还是非成员函数?
对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应是友元函数(这就是我们将这块知识放在友元函数之后讲的原因),这样它才能直接访问类的私有数据。
所以,Time类的加法运算符在Time类声明中的原型有如下两个版本:
Time operator+(const Time & t) const; //成员函数版本 friend Time operator+(const Time & t1, const Time & t2); //非成员函数版本
加法运算符需要两个操作数。对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显示地传递;对于友元版本来说,两个操作数都作为参数来传递。
即,非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。
这两个原型都与表达式T2 + T3匹配,其中T2和T3都是Time类型对象。也就是说,编译器将下面的语句:
T1 = T2 + T3;
转换为下面两个的任何一个:
T1 = T2.operator+(T3); //成员函数 T1 = operator+(T2, T3); //非成员函数
所以,为了避免二义性,我们在定义运算符时,只能选择其中的一种格式。
那么选择哪种格式最好呢???对于某些运算符来说(如前所述),成员函数是唯一合法的选择。在其他情况下,这两种格式没有太大的区别。有时,根据类设计,使用非成员函数版本可能更好(尤其是为类定义类型转换时),后面我们将谈到这种情形。
下面我们来谈一种使用了运算符重载和友元的类设计——一个表示矢量的类。
这个类还说明了类设计的其他方面,例如,在同一个对象中包含两种描述同一样东西的不同方式等。即使并不关心矢量,也可以在其他情况下使用这里介绍的很多新技术。我们接下来好好欣赏矢量与C++的结合带来的视觉盛宴吧!
显然,我们无法用任何内置类型来表示矢量,所以我们应创建一个类来表示矢量。而且矢量虽然不能用普通数学运算(如加法、减法)进行操作,但可以重载这些运算符(使之能用于矢量)。
描述一个二维矢量,我们使用下面两种方法:
- 用大小(长度)和方向(角度)描述;
- 用分量x和y表示矢量(两个分量分别是水平矢量和垂直矢量,将其相加可以得到最终的矢量)。
例如,我们这样描述点的运动:向右移动30个单位,再向上移动40个单位。这将把该点沿与水平方向呈53.1度的方向移动50个单位,因此,水平分量为30个单位、垂直分量为40个单位的矢量,与长度为50个单位、方向为53.1度的矢量相同。位移矢量指的是从何处开始、到何处结束,而不是经过的路线。
我们设计的矢量类将这两种表示形式都包含进去(因为有时一种表示形式不如另一种方便),此外,当用户修改了矢量的一种表示后,对象将自动更新另一种表示。
下面是类的声明:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // vect.h -- Vector class with <<, mode state 2 #ifndef VECTOR_H_ 3 #define VECTOR_H_ 4 #include <iostream> 5 namespace VECTOR 6 { 7 class Vector 8 { 9 public: 10 enum Mode {RECT, POL}; 11 // RECT for rectangular, POL for Polar modes 12 private: 13 double x; // horizontal value 14 double y; // vertical value 15 double mag; // length of vector 16 double ang; // direction of vector in degrees 17 Mode mode; // RECT or POL 18 // private methods for setting values 19 void set_mag(); 20 void set_ang(); 21 void set_x(); 22 void set_y(); 23 public: 24 Vector(); 25 Vector(double n1, double n2, Mode form = RECT); 26 void reset(double n1, double n2, Mode form = RECT); 27 ~Vector(); 28 double xval() const {return x;} // report x value 29 double yval() const {return y;} // report y value 30 double magval() const {return mag;} // report magnitude 31 double angval() const {return ang;} // report angle 32 void polar_mode(); // set mode to POL 33 void rect_mode(); // set mode to RECT 34 // operator overloading 35 Vector operator+(const Vector & b) const; 36 Vector operator-(const Vector & b) const; 37 Vector operator-() const; 38 Vector operator*(double n) const; 39 // friends 40 friend Vector operator*(double n, const Vector & a); 41 friend std::ostream & operator<<(std::ostream & os, const Vector & v); 42 }; 43 44 } // end namespace VECTOR 45 #endif
上述程序中4个报告分量值的函数是在类声明中定义的,因此将自动成为内联函数。这些函数非常短,因此适于声明为内联函数。因为它们都不会修改对象数据,所以声明时使用了const限定符。前面讲过,这种句法用于声明那些不会对其显式访问的对象进行修改的函数。此外,为复习名称空间,我们将类声明放在VECTOR名称空间中。我们还使用枚举创建了两个常量(RECT和POL)用于标识两种表示法。
接下来是类的实现:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // vect.cpp -- methods for the Vector class 2 #include <cmath> 3 #include "vect.h" // includes <iostream> 4 using std::sqrt; 5 using std::sin; 6 using std::cos; 7 using std::atan; 8 using std::atan2; 9 using std::cout; 10 11 namespace VECTOR 12 { 13 // compute degrees in one radian 14 const double Rad_to_deg = 45.0 / atan(1.0); 15 // should be about 57.2957795130823 16 17 // private methods 18 // calculates magnitude from x and y 19 void Vector::set_mag() 20 { 21 mag = sqrt(x * x + y * y); 22 } 23 24 void Vector::set_ang() 25 { 26 if (x == 0.0 && y == 0.0) 27 ang = 0.0; 28 else 29 ang = atan2(y, x); 30 } 31 32 // set x from polar coordinate 33 void Vector::set_x() 34 { 35 x = mag * cos(ang); 36 } 37 38 // set y from polar coordinate 39 void Vector::set_y() 40 { 41 y = mag * sin(ang); 42 } 43 44 // public methods 45 Vector::Vector() // default constructor 46 { 47 x = y = mag = ang = 0.0; 48 mode = RECT; 49 } 50 51 // construct vector from rectangular coordinates if form is r 52 // (the default) or else from polar coordinates if form is p 53 Vector::Vector(double n1, double n2, Mode form) 54 { 55 mode = form; 56 if (form == RECT) 57 { 58 x = n1; 59 y = n2; 60 set_mag(); 61 set_ang(); 62 } 63 else if (form == POL) 64 { 65 mag = n1; 66 ang = n2 / Rad_to_deg; 67 set_x(); 68 set_y(); 69 } 70 else 71 { 72 cout << "Incorrect 3rd argument to Vector() -- "; 73 cout << "vector set to 0\n"; 74 x = y = mag = ang = 0.0; 75 mode = RECT; 76 } 77 } 78 79 // reset vector from rectangular coordinates if form is 80 // RECT (the default) or else from polar coordinates if 81 // form is POL 82 void Vector:: reset(double n1, double n2, Mode form) 83 { 84 mode = form; 85 if (form == RECT) 86 { 87 x = n1; 88 y = n2; 89 set_mag(); 90 set_ang(); 91 } 92 else if (form == POL) 93 { 94 mag = n1; 95 ang = n2 / Rad_to_deg; 96 set_x(); 97 set_y(); 98 } 99 else 100 { 101 cout << "Incorrect 3rd argument to Vector() -- "; 102 cout << "vector set to 0\n"; 103 x = y = mag = ang = 0.0; 104 mode = RECT; 105 } 106 } 107 108 Vector::~Vector() // destructor 109 { 110 } 111 112 void Vector::polar_mode() // set to polar mode 113 { 114 mode = POL; 115 } 116 117 void Vector::rect_mode() // set to rectangular mode 118 { 119 mode = RECT; 120 } 121 122 // operator overloading 123 // add two Vectors 124 Vector Vector::operator+(const Vector & b) const 125 { 126 return Vector(x + b.x, y + b.y); 127 } 128 129 // subtract Vector b from a 130 Vector Vector::operator-(const Vector & b) const 131 { 132 return Vector(x - b.x, y - b.y); 133 } 134 135 // reverse sign of Vector 136 Vector Vector::operator-() const 137 { 138 return Vector(-x, -y); 139 } 140 141 // multiply vector by n 142 Vector Vector::operator*(double n) const 143 { 144 return Vector(n * x, n * y); 145 } 146 147 // friend methods 148 // multiply n by Vector a 149 Vector operator*(double n, const Vector & a) 150 { 151 return a * n; 152 } 153 154 // display rectangular coordinates if mode is RECT, 155 // else display polar coordinates if mode is POL 156 std::ostream & operator<<(std::ostream & os, const Vector & v) 157 { 158 if (v.mode == Vector::RECT) 159 os << "(x,y) = (" << v.x << ", " << v.y << ")"; 160 else if (v.mode == Vector::POL) 161 { 162 os << "(m,a) = (" << v.mag << ", " 163 << v.ang * Rad_to_deg << ")"; 164 } 165 else 166 os << "Vector object mode is invalid"; 167 return os; 168 } 169 170 } // end namespace VECTOR
该程序利用了名称空间的开放性,将方法定义添加到VECTOR名称空间中。而且构造函数和reset()函数都设置了矢量的直角坐标和极坐标表示,因此需要这些值时,可直接使用而无需进行计算。此外,C++的内置数学函数在使用角度时以弧度为单位,所以函数在度和弧度之间进行转换。该Vector类实现对用户隐藏了极坐标和直角坐标之间的转换以及弧度和度之间的转换等内容。用户只需知道:类在使用角度时以度为单位,可以使用两种等价的形式来表示矢量。
当然,我们也可以以另一种方式来设计这个类。例如,在对象中存储直角坐标而不是极坐标,并使用方法magval()和angval()来计算极坐标。对于很好进行坐标转换的应用来说,这将是一种效率更高的设计。另外,方法reset()并非必不可少的。
假设shove是一个Vector对象,而我们编写了如下代码:
shove.reset(100,300);
可以使用构造函数来得到相同的结果:
shove = Vector(100,300);
然而,方法set()直接修改shove的内容,而使用构造函数将创建一个临时对象,然后将其赋给shove。
重温笔记:这些设计决策遵守了OOP传统,即将类接口的重点放在其本质上(抽象模型),而隐藏细节。这样,当用户使用Vector类时,只需要考虑矢量的通用特性,例如,矢量可以表示位移,可以将两个矢量相加等。使用哪种方式表示矢量已无关紧要,因为程序员可以设置矢量的值,并选择最方便的格式来显示它们。
下面我们将更详细地介绍Vector类的一些特性。
【使用状态成员】
何为状态成员?首先必须是成员,然后这种成员描述的是对象所处的状态。
我们知道Vector类存储了矢量的直角坐标和极坐标。它使用名为mode的成员来控制使用构造函数、reset()方法和重载的operator<<()函数使用哪种形式,其中枚举RECT表示直角坐标形式(默认值)、POL表示极坐标模式。这样的成员就是状态成员。要知道具体的含义,我们先看构造函数的代码:
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 { cout << "Incorrect 3rd argument to Vector() -- "; cout << "vector set to 0\n"; x = y = mag = ang = 0.0; mode = RECT; } }
如果第三个参数时RECT或省略了(原型将默认值设置为RECT),则将输入解释为直角坐标;如果为POL,则将输入解释为极坐标。
Vector folly(3.0,4.0); //set x = 3, y = 4 Vector foolery(2.5,4.0,Vector::Vector::POL); //set mag = 2.5, ang = 4
标识符POL的作用域为类,因此类定义可使用未限定的名称。但全限定名为Vector::Vector::POL,因为POL是在Vector类中定义的,而Vector是在名称空间VECTOR中定义的。注意,如果用户提供的是x值和y值,则构造函数将使用私有方法set_mag()和set_ang()来设置距离和角度值;如果提供的是距离和角度值,则构造函数将使用set_x()和set_y()方法来设置x值和y值。另外,如果用户指定的不是RECT或POL,则构造函数将显示一条警告消息,并将状态设置为RECT。
【为Vector类重载算术运算符】
在使用x、y坐标时,将两个矢量相加将非常简单,只要将两个x分量相加,得到最终的x分量,将两个y分量相加,得到最终的y分量即可。根据这种描述,可能使用下面的代码:
Vector Vector::operator+(const Vector & b) const { Vector sum; sum.x = x + b.x; sum.y = y + b.y; return sum; //incomplete version }
如果对象只存储x和y分量,则这很好。遗憾的是,上述代码无法设置极坐标值。我们可以通过添加另外一些代码来解决这种问题:
Vector Vector::operator+(const Vector & b) const { Vector sum; sum.x = x + b.x; sum.y = y + b.y; sum.set_ang(sum.x,sum.y); sum.set_mag(sum.x,sum.y); return sum; //version duplicates needlessly }
然而,使用构造函数来完成这种工作将更简单可靠:
Vector Vector::operator+(const Vector & b) const { return Vector(x + b.x + b.y); //return the constructed Vector }
上述代码将新的x分量和y分量传递给Vector构造函数,而后者将使用这些值来创建无名的新对象,并返回该对象的副本。这确保了新的Vector对象是根据构造函数制定的标准规则创建的。
重温笔记:如果方法通过计算得到一个新的类对象,则应考虑是否可以使用类构造函数来完成这种工作。这样做不仅可以避免麻烦,而且可以确保新的对象是按照正确的方式创建的。
1. 乘法
将矢量与一个数相乘,将使该矢量加长或缩短(取决于这个数)。因此,将矢量乘以3得到的矢量的长度为原来的3倍,而方向不变。要在Vector类中实现矢量的这种行为很容易。对于极坐标,只要将长度进行伸缩,并保持角度不变即可;对于直角坐标,只需将x和y分量进行伸缩即可。也就是说,如果矢量的分量为5和12,则将其乘以3后,分量将分别是15和36。这正是重载的乘法运算符要完成的工作:
Vector Vector::operator*(double n) const { return Vector(n*x, n*y); }
和重载加法一样,上述代码允许构造函数使用新的x和y分量来创建正确的Vector对象。上述函数用于处理Vector值和double值相乘。可以像Time示例那样,使用一个内联友元函数来处理double与Vector相乘:
Vector operator*(double n, const Vector & a) { return a*n; }
2.对已重载的运算符进行重载
在C++中,-运算符已经有两种含义(即被重载过)。首先,使用两个操作数,它是减法运算符;其次,使用一个操作数时,它是负号运算符。对于矢量来说,这两种操作(减法和符号反转)都是有意义的,因此Vector类有这两种操作。
要从矢量A中减去矢量B,只要将分量相减即可,因此,重载减法与重载加法相似:
Vector operator-(const Vector & b) const; //原型声明 Vector Vector::operator-(const Vector & b) const //函数定义 { return Vector(x - b.x - b.y); //return the constructed Vector }
操作数的顺序非常重要,下面的语句:diff = v1 - v2; 将被转换为下面的成员函数调用:diff = v1.operator-(v2);
这意味着将从隐式矢量参数减去以显式参数传递的矢量,所以应使用x - b.x,而不是b.x - x。
接下来,来看一元负号运算符,它只使用一个操作数。将这个运算符用于数字(如-x)时,将改变它的符号。因此,将这个运算符用于矢量时,将反转矢量的每个分类的符号。更准确地说,函数应返回一个与原来的矢量相反的矢量(对于极坐标,长度不变,但方向相反)。下面是重载负号的原型和定义:
Vector operator-() const; //原型声明 Vector Vector::operator-() const //函数定义 { return Vector(-x,-y); }
现在,operator-()有两种不同的定义。这是可行的,因为它们的特征标不同。可以定义-运算符的一元和二元版本,因为C++提供了该运算符的一元和二元版本,对于只有二元形式的运算符(如除法运算符),只能将其重载为二元运算符。
重温笔记:因为运算符重载是通过函数来实现的,所以只要运算符函数的特征标不同,使用的运算符数量与相应的内置C++运算符相同,就可以多次重载同一个运算符。
3.对实现的说明
前几节介绍的Vector对象中存储了矢量的直角坐标和极坐标,但公有接口并不依赖于这一事实。所有接口都只要求能够显示这两种表示,并可以返回各个值。内部实现方式可以完全不同。正如前面指出的,对象可以只存储x和y分量,而返回矢量长度的magval()方法可以根据x和y的值来计算出长度,而不是查找对象中存储的这个值。这种方法改变了实现,但用户接口不变。将接口与实现分离是OOP的目标之一,这样允许对实现进行调整,而无需修改使用这个类的程序中的代码。
这两种实现各有利弊。存储数据意味着对象将占据更多的内存,每次Vector对象被修改时,都需要更新直角坐标和极坐标表示;但查找数据的速度比较快。如果应用程序经常访问矢量的这两种表示,则这个例子采用的实现比较合适;如果只是偶尔需要使用极坐标,则另一种实现更好。可以在一个程序中使用一种实现,而在另一个程序中使用另一种实现,但它们的用户接口相同。
4. 使用Vector类来模拟随机漫步
下面是一个模拟了著名的醉鬼走路问题的程序。其意思是,将一个人领到街灯柱下。这个人开始走动,但每一步的方向都是随机的(与前一步不同)。这个问题的一种表述是,这个人走到离灯柱50英尺处需要多少步。从矢量的角度看,这相当于不断将方向随机的矢量相加,直到长度超过50英尺。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // randwalk.cpp -- using the Vector class 2 // compile with the vect.cpp file 3 #include <iostream> 4 #include <cstdlib> // rand(), srand() prototypes 5 #include <ctime> // time() prototype 6 #include "vect.h" 7 int main() 8 { 9 using namespace std; 10 using VECTOR::Vector; 11 srand(time(0)); // seed random-number generator 12 double direction; 13 Vector step; 14 Vector result(0.0, 0.0); 15 unsigned long steps = 0; 16 double target; 17 double dstep; 18 cout << "Enter target distance (q to quit): "; 19 while (cin >> target) 20 { 21 cout << "Enter step length: "; 22 if (!(cin >> dstep)) 23 break; 24 25 while (result.magval() < target) 26 { 27 direction = rand() % 360; 28 step.reset(dstep, direction, POL); 29 result = result + step; 30 steps++; 31 } 32 cout << "After " << steps << " steps, the subject " 33 "has the following location:\n"; 34 cout << result << endl; 35 result.polar_mode(); 36 cout << " or\n" << result << endl; 37 cout << "Average outward distance per step = " 38 << result.magval()/steps << endl; 39 steps = 0; 40 result.reset(0.0, 0.0); 41 cout << "Enter target distance (q to quit): "; 42 } 43 cout << "Bye!\n"; 44 /* keep window open 45 cin.clear(); 46 while (cin.get() != '\n') 47 continue; 48 cin.get(); 49 */ 50 return 0; 51 }
上述程序允许用户选择行走距离和步长。该程序用一个变量来表示位置(一个矢量),并报告到达指定距离处(用两种格式表示)所需的步数。可以看到,行走者前进得相当慢。虽然走了1000步,每步的距离为2英尺,但离起点可能只有20英尺。这个程序将行走者所走的净距离(这里为50英尺)除以步数,来指出这种行走方式的低效性。随即改变方向使得该平均值远远小于步长。为了随机选择方向,该程序使用了标准库函数rand()、srand()和time()。
需要说明的是,程序中使用VECTOR名称空间非常方便。下面的using声明使Vector类的声明可用:
using VECTOR::vector;
因为所有的Vector类方法的作用域都为整个类,所以导入类名后,无需提供其他声明,就可以使用Vector的方法。即该程序可使用Vector::POL,而不必使用VECTOR::Vector::POL。
【类的自动转换和强制转换类型】
在这一小节里,我们将介绍类的另一个主题——类型转换(处理用户定义类型的转换)。
首先,我们回顾一下C++是如何处理内置类型转换的:
将一个标准类型变量的值赋给另一种标准类型的变量时,
- 如果这两种类型兼容,则C++自动将这个值转换为接收变量的类型;
- 如果这两种类型不兼容,则C++将不自动转换(此时可以使用强制类型转换)。
我们要使得从一个类型转换到另一种类型是有意义的,可以将类定义成与基本类型或另一个类相关。在这种情况下,我们可以指示C++如何自动进行转换或进行强制类型转换。
首先,我们应设计一种合适的类型。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // stonewt.h -- definition for the Stonewt class 2 #ifndef STONEWT_H_ 3 #define STONEWT_H_ 4 class Stonewt 5 { 6 private: 7 enum {Lbs_per_stn = 14}; // pounds per stone 8 int stone; // whole stones 9 double pds_left; // fractional pounds 10 double pounds; // entire weight in pounds 11 public: 12 Stonewt(double lbs); // constructor for double pounds 13 Stonewt(int stn, double lbs); // constructor for stone, lbs 14 Stonewt(); // default constructor 15 ~Stonewt(); 16 void show_lbs() const; // show weight in pounds format 17 void show_stn() const; // show weight in stone format 18 }; 19 #endif
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // stonewt.cpp -- Stonewt methods 2 #include <iostream> 3 using std::cout; 4 #include "stonewt.h" 5 6 // construct Stonewt object from double value 7 Stonewt::Stonewt(double lbs) 8 { 9 stone = int (lbs) / Lbs_per_stn; // integer division 10 pds_left = int (lbs) % Lbs_per_stn + lbs - int(lbs); 11 pounds = lbs; 12 } 13 14 // construct Stonewt object from stone, double values 15 Stonewt::Stonewt(int stn, double lbs) 16 { 17 stone = stn; 18 pds_left = lbs; 19 pounds = stn * Lbs_per_stn +lbs; 20 } 21 22 Stonewt::Stonewt() // default constructor, wt = 0 23 { 24 stone = pounds = pds_left = 0; 25 } 26 27 Stonewt::~Stonewt() // destructor 28 { 29 } 30 31 // show weight in stones 32 void Stonewt::show_stn() const 33 { 34 cout << stone << " stone, " << pds_left << " pounds\n"; 35 } 36 37 // show weight in pounds 38 void Stonewt::show_lbs() const 39 { 40 cout << pounds << " pounds\n"; 41 }
因为Stonewt对象表示一个重量,所以可以提供一些将整数或浮点值转换为Stonewt对象的方法。
我们已经这样做了!在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。
因此,下面的构造函数用于将double类型的值转换为Stonewt类型:
Stonewt(double lbs); //模板 for double-to-Stonewt转换
也就是说,可以编写这样的代码:
Stonewt myCat; //创建一个Stonewt对象 myCat = 19.6; //使用Stone(double)将19.6转换为Stonewt类型
程序将使用构造函数Stonewt(double)来创建一个临时的Stonewt对象,并将19.6作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到myCat中。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换。
将构造函数用作自动类型转换似乎是一项不错的特性。然而,当程序员拥有更丰富的C++经验时,将发现这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。因此C++新增了关键字explicit,用于关闭这种自动特性。也就是说,可以这样声明构造函数:
explicit Stonewt(double lbs);
这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换:
Stonewt myCat; myCat = 19.6; //不合法 myCat = Stonewt(19.6); //ok,强制类型转换 myCat = (Stonewt)19.6; //ok,旧版强制类型转换
重温笔记:只接受一个参数的构造函数定义了从参数类型到类类型的转换(如果想反过来可使用转换函数)。如果使用了关键字explict限定了这种构造函数,则它只能用于显式转换,否则也可以用于隐式转换。
当构造函数声明中没有关键字explicit时,可以用于下面的隐式转换:
- 将Stonewt对象初始化为double值时;
- 将double值赋给Stonewt对象时;
- 将double值传递给接受Stonewt参数的函数时;
- 将返回值被声明为Stonewt的函数试图返回double值时;
- 在上述任意一种情况下,使用可转换为double类型的内置类型时。·
后面还有一些关于转换函数和友元函数在此的应用,我就不展开了,上面的知识点足以应付平时的使用需求了!
【温故而知新】
1.使用成员函数为Stonewt类重载乘法运算符,该运算符将数据成员与double类型的值相乘。注意用英石和磅表示时,需要进位。也就是说,将10英石8磅乘以2等于21英石2磅。
答:
Stonewt Stonewt::operator*(double a) { Stonewt q; q.stone=stone*a+pounds*a/14; q.pounds=(pounds*a)%14; }
2.友元函数和成员函数之间的区别是什么?
答:
友元函数:①函数原型需要使用friend;②调用的对象一般是非类对象;③在运算符重载函数里,有两个参数,并且往往一个是类对象、一个是非类对象;④在函数定义里,若不是内联函数,无需使用类作用域解析运算符在函数头中;
成员函数:①函数原型无需使用friend;②调用的对象是类对象;③在运算符重载函数里,类对象被隐式的传递给了函数;④若不是内联函数,那么在函数定义的函数头,需要使用作用域解析运算符。
成员函数是类定义的一部分,通过特定的对象来调用,可以直接调用对象的成员,而无需使用成员运算符(那个句号)。
友元函数不是类的组成部分,不是隐式的访问对象的成员,而是通过参数传递的对象来访问,并且需要使用成员运算符。
3.非成员函数必须是友元才能访问类成员吗?
答:
一般是,因为友元函数才能获得和成员函数一样的权限来访问类成员。
如果不使用友元函数,那么若要访问类成员,则只能访问类的公有成员,而无法访问私有成员。或者,类方法里有返回私有成员的值的公有成员函数。
4.使用友元函数为Stonewt类重载乘法运算符,该运算符将double值与Stone值相乘。
答:
friend Stonewt operator * (double a,const Stone& b); //函数原型,位于Stonewt类公有成员区域 Stonewt operator * (double a,const Stone& b) //函数定义 { Stonewt q; q.Stone = b.Stone*a; return q; }
5.哪些运算符不能重载?
答:
(1)sizeof sizeof运算符
(2). ←这是一个句号,成员运算符,比如结构名.结构内变量
(3).* ←句号和乘号,成员指针运算符(没见过)
(4):: ←两个冒号,作用域解析运算符
(5)?: 条件运算符,比如: a>b?c:d
(6)typeid ←一个RTTI运算符(没见过)
(7)const_cast ←强制类型转换运算符(没见过这种用法)
(8)dynamic_cast ←强制类型转换运算符(没见过这种用法)
(9)reinterpret_cast ←强制类型转换运算符(没见过这种用法)
(10)static_cast ←强制类型转换运算符(没见过这种用法)
6.在重载运算符=、()、[]和->时,有什么限制?
答:
只能通过类成员函数来进行重载(but,我没试过这个)。
7.为Vector类定义一个转换函数,将Vector类转换为一个double类型的值,后者表示矢量的长度。
答:
以下这个函数直接放在public区域成为内联函数。
operator double() { return mag; }