本章节包括:运算符重载、友元函数、重载<<运算符,以便用于输出、状态成员、使用rand()生成随机值、类的自动转换和强制类型转换、类转换函数。
11.1 运算符重载
运算符重载是一种形式的C++多态。要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:
operator op(argument-list)
例如,operator +( )重载+运算符,operator * ( )重载 * 运算符。op必须是有效的C++运算符,不能虚构一个新的符号。例如,不能有operator@( )这样的函数,因为C++中没有@运算符。
district2 = sid + sara;
district2 = sid.operator+(sara);
//operator +( )函数的名称使得可以使用函数表示法或运算符表示法来调用它。编译器将根据操作数的类型来确定如何做
该函数将隐式地使用sid(因为它调用了方法),而显式地使用sara对象(因为它被作为参数传递),来计算总和,并返回这个值。当然最重要的是,可以使用简便的+运算符表示法,而不必使用笨拙的函数表示法。
11.2 计算时间:一个运算符重载示例
程序清单11.1,11.2,11.3; · 0879e92 · Kite/C和C++ - Gitee.com
这里上传的是一个压缩包,下面 还有代码补充是c文件等。
这里要注意一下为什么sum函数,参数使用引用是速度更快,使用内存将更少;返回值不使用引用。
不要返回指向局部变量或临时对象的引用。函数执行完毕时,局部变量和临时对象将消失,引用将指向不存在的数据。
使用返回类型Time意味着程序将在删除之前构造它的拷贝,调用函数将得到该拷贝。
输出是和书里面的一样的。
11.2.1 添加加法运算符
Time Sum(const Time & t) const;//这是最初的求和函数
Time operator+(const Time & t) const;//这是重载的加法运算符
程序清单11.4; · 356827a · Kite/C和C++ - Gitee.com
书中的11.4,11.5和11.6都在上面链接所在的文件夹下。
新的运行输出结果;
这里有个重要说明,小伙伴应该都知道=运算符的结合方向是从右到左,但是+运算符的结合方向是从左到右的。
例如:
int a,b,c,d;
a = b = c = d;//d赋值给c再赋值给b再赋值给a
a = b + c + d;//b加c再加d,然后赋值给a
带入本章节的内容,如果我们重载了运算符+且是应用于对象;Time是前面程序里定义的类;
Time t1,t2,t3,t4;
t4 = t1 + t2 + t3;
实际步骤是:
t4 = t1.operator + (t2 + t3);
然后
t4 = t1.operator + (t2.operator + (t3));
11.2.2 重载的限制
1.重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将减法运算符(−)重载为计算两个double值的和,而不是它们的差。虽然这种限制将对创造性有所影响,但可以确保程序正常运行。
2.使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符(%)重载成只使用一个操作数。同样,不能修改运算符的优先级。因此,如果将加号运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级。
3.不能创建新运算符。例如,不能定义operator **( )函数来表示求幂。
4.不能重载下面的运算符。
sizeof :sizeof运算符。
. :成员运算符。
. * :成员指针运算符。
:: :作用域解析运算符。
?: :条件运算符。
typeid :一个RTTI运算符。
const_cast :强制类型转换运算符。
dynamic_cast :强制类型转换运算符。
reinterpret_cast :强制类型转换运算符。
static_cast :强制类型转换运算符。5.表11.1中的大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载。
= :赋值运算符。
( ):函数调用运算符。
[ ]:下标运算符。
-> :通过指针访问类成员的运算符。
+ | - | * | / | % | ^ |
---|---|---|---|---|---|
& | | | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= |
<<= | == | != | <= | >= | && |
|| | ++ | – | , | ->* | -> |
() | [] | new | delete | new[] | delete[] |
除了这些正式限制之外,还应在重载运算符时遵循一些明智的限制,尽量符合常识,见闻知意。
这一块知识书里论述的没有侯捷的教学视频全面。
11.2.3 其它重载运算符
程序清单11.7,11.8,11.9;新添加了重载运算符-和*; · 5c1876b · Kite/C和C++ - Gitee.com
运行输出结果如下;
11.3 友元
C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以致于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元。
友元有3种:
- 友元函数;
- 友元类;
- 友元成员函数。
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
为什么需要友元?
在为类重载二元运算符时(带两个参数的运算符),常常需要友元。例如将TIme对象乘以实数。
例如,我们有一个时间类,并且运用上面的知识,实现了对运算符的重载(左侧的操作数是调用对象,也就是Time对象):
A = B * 2.75;//相当于 A = B.operator*(2.75);
但是这种调用就不适合:
A = 2.75 * B;
从概念上说,2.75 * B应与B *2.75相同,但代码实现并不符合这种常识。
解决方案:
一,告诉用户只能使用第一种格式,其他的均不适用。这是一种对服务器友好,客户警惕的解决方案,与OOP无关。
二,非成员函数。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。
Time operator*(double m, const Time& t);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,至于为什么可以参考侯捷视频中关于这一点的探讨。
我们通过非成员函数确实可以实现我们的目的,但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。
11.3.1 创建友元
创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend:
friend Time operator* (double m, const Time & t);
该原型意味着以下两点:
- 虽然operator *( )函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
- 虽然operator *( )函数不是成员函数,但它与成员函数的访问权限相同。
第二步是编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend。
有了以上步骤,下面的语句才成立:
A = 2.75 * B;//相当于 A = operator*(2.75,B);
总之,类的友元函数是非成员函数,其访问权限与成员函数相同。
友元是否有悖于OOP?
乍一看,你可能会认为友元违反了OOP数据隐藏的原则,因为友元机制允许非成员函数访问私有数据。然而,这个观点太片面了。相反,应将友元函数看作类的扩展接口的组成部分。例如,从概念上看,double乘以Time和Time乘以double是完全相同的。也就是说,前一个要求有友元函数,后一个使用成员函数,这是C++句法的结果,而不是概念上的差别。通过使用友元函数和类方法,可以用同一个用户接口表达这两种操作。另外请记住,只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据。总之,类方法和友元只是表达类接口的两种不同机制。
实际上,按下面的定义进行修改,可以将友元函数编写为非友元函数:
Time operator*(double m,const Time & t){
return t*m;//use t.operator*(m)
}
如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
11.3.2 常用的友元:重载<<运算符
cout是一个ostream对象,能够识别所有的C++基本类型。这是因为对于每种基本类型,ostream类声明中都包含了相应的重载的operator<<( )定义。因此,要使cout能够识别Time对象,一种方法是将一个新的函数运算符定义添加到ostream类声明中。但修改iostream文件是个危险的主意,这样做会在标准接口上浪费时间。相反,通过Time类声明来让Time类知道如何使用cout。
1.<<的第一种重载版本
要使Time类知道使用cout,必须使用友元函数。
cout是ostream类的对象,还有一个对象是cerr,它将输出发送到标准输出流----默认为显示器。
调用cout << trip应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式cout << trip将导致os成为cout的一个别名;
而表达式cerr << trip将导致os成为cerr的一个别名。Time对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少。
2.<<的第二种重载版本
前面介绍的实现有一个问题;
cout << trip;//可以正常工作
cout << "Trip Time: " << trip << " (Tuesday)\n";//不允许的
下面说一下如何修改;对友元函数采用这种方式;
ostream& operator<< (ostream& os,const Time & t)
{
os. t.hours << " hours," << t.minutes << " minutes";
return os;
}
cout<<trip;//实际上就相当于
operator<<(cout,trip);
然后这种语句就可以正常工作了;
cout << "Trip Time: " << trip << " (Tuesday)\n";
实话说,书看到这里我是没搞明白的;还好有逐步介绍;
cout << "Trip Time: "
看这一部分,这是调用ostream中的<<定义,显示字符串并且返回cout对象;
cout << trip << " (Tuesday)\n";
这是后半部分,这里就用到Time中声明的<<的意义了,(友元函数进行重载的作用),显示trip值(小时和分钟);然后返回了cout对象;
最后打印最后的字符串。
这样做的结果就是:函数的返回值就是传递给它的对象。有趣的是,这个operator版本还可以将输出写入到文件中。
#include<fstream>
...
ofstream fout;
fout.open("savetime.txt");
Time trip(12,40);
fout<<trip;//这条语句将会被转换为这样:operator<<(fout,trip);
类继承属性让ostream引用能够指向ostream对象和ofstream对象。
一般来说,要重载<<运算符来显示c_name的对象,可使用一个友元函数,其定义如下:
ostream & operator << (ostream & os , sonst c_name & obj){
os << ...;
return os;
}
只有在类声明中的原型中才能使用friend关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。
程序清单11.10,11.11,11.12;新修改的,*和<<的两个友元函数; · 8f8ec4d · Kite/C和C++ - Gitee.com
再次修改后的;下图圈住的函数是一个友元函数同时也作为了内联函数(代码很短);
再次修改后的输出结果;
11.4 重载运算符:作为成员函数还是非成员函数
对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。
Time operator+(const Time & T) const;
//成员函数版本 T1 = T2.operator+(T3);
friend Time operator+(const Time & t1 , const Time & t2);
//非成员函数版本 T1 = operator+(T2,T3);
//加法运算符需要两个操作数。对于成员函数版本来说,
//一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;
//对于友元版本来说,两个操作数都作为参数来传递。
在定义运算符时,必须选择其中的一种格式,而不能同时选择这两种格式。因为这两种格式都与同一个表达式匹配,同时定义这两种格式将被视为二义性错误,导致编译错误。
11.5 再谈重载:一个矢量类
有大小,有方向的量叫做矢量;关于矢量的表示方法:
- 可以用大小(长度)和方向(角度)描述矢量;
- 可以用分量x和y表示矢量。
程序清单11.13,11.14;矢量; · 311f87e · Kite/C和C++ - Gitee.com
代码想要了解一下可以去看下,由于没有实现内容,所以我没运行成功。
11.5.1使用状态成员
程序清单11.13,11.14;矢量; · 311f87e · Kite/C和C++ - Gitee.com
上面这个程序里,有REST和POL两个枚举量,其中REST表示直角坐标模式(也是默认值),POL表示极坐标模式。这样的成员称为状态成员,因为这种成员描述的是对象所处的状态。
11.5.2 为Vector类重载算术运算符
这里重载了加减乘运算符。
减运算符有两种含义,第一是使用两个操作数,它是减法运算符,它是一个二元运算符;一个操作数时,它是负号运算符,它是一元运算符。对于矢量这两种含义同样是有意义的。
11.5.3 对实现的说明
将接口和实现分离是OOP的目标之一。
11.5.4 使用Vector类来模拟随机漫步
code_c++/randwalk.cpp · Kite/C和C++ - 码云 - 开源中国 (gitee.com)
每次运行的结果都不同。
11.6 类的自动转换和强制类型转换
C++中的类型转换;
lone count = 8; //int 8 被强制转换成了long
double time = 11; //int 11 被强制转换成了double
int side = 3.3 //double 被强转换成了int
C++不自动转换不兼容的类型,但可以强制转换。
int*p = 10; //不允许
int*p = (int*) 10;//允许,将指针设置为地址10,但这种赋值是否有意义是另外一回事。
可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。在这种情况下,程序员可以指示C++如何自动进行转换,或通过强制类型转换来完成。但只有接受一个参数的构造函数才能作为转换函数。(注意:这里指的是接收一个参数,不是只有一个参数)
Stonewt(double lbs);//这样一个构造函数将有能力实现double到Stonewt的强转
Stonewt myCat;
myCat = 19.6;
Stonewt(int stn , double lbs = 0);//如果给第二个参数提供默认值,将有能力转换int。
如何关闭这种特性?
将构造函数用作自动类型转换函数似乎是一项不错的特性。然而,当程序员拥有更丰富的C++经验时,将发现这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。因此,C++新增了关键字explicit,用于关闭这种自动特性。也就是说,可以这样声明构造函数:
explicit Stonewt(double lbs);//这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换.
Stonewt myCat; //定义一个对象
myCat = 19.6; //not ok
mycat = Stonewt(19.6); //ok
myCat = (Stonewt)19.6; //ok
如果使用了关键字explicit限定了Stonewt(double)将只用于显式强制类型转换,否则还可以用于下面的隐式转换;
将Stonewt对象初始化为double值时;
将double值赋给Stonewt对象时;
将double值传递给接受Stonewt参数的函数时;
返回值被声明为Stonewt的函数试图返回double值时;
在上述任意一种情况下,使用可转换为double类型的内置类型时。
函数原型化提供的参数匹配过程:
//下面两条语句都首先将int转换为double,然后使用Stonewt(double)构造函数。
Stonewt Jumbo(7000);
Jumbo = 7300;
然而,当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造函数Stonewt(long),则编译器将拒绝这些语句,可能指出:int可被转换为long或double,因此调用存在二义性。
程序清单11.16,11.17,11.18;类的类型转换; · 3284ea1 · Kite/C和C++ - Gitee.com
Stonewt incognito = 275;
//等价于下面两种
Stonewt incognito(275);
Stonewt incognito = Stonewt 275;
11.6.1 转换函数
前面是将数字转换为Stonewt对象,那么是否可以将Stonewt对象转换为double值?
Stonewt wolfe(285.7);
double host = wolfe;
可以这样做,但不是使用构造函数。构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的C++运算符函数——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。
Stonewt wolfe(285.7);
double host = double(wolfe);
double thinker = (double)wolfe;
double thinker = wolfe;
那么,如何创建转换函数呢?要转换为typeName类型,需要使用这种形式的转换函数:
operator typeName();
//例如,转换为double类型的函数的原型如下:
operator double();
请注意以下几点:
转换函数必须是类方法;
转换函数不能指定返回类型;
转换函数不能有参数。
typeName(这里为double)指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。
程序清单11.19,11.20,11.21;转换函数; · f582e00 · Kite/C和C++ - Gitee.com
例如:
operator int() const;
operator double() const;
Stonewt::operator int() const{
return int(pounds+0.5);
}
Stonewt::operator double() const{
return pounds;
}
注意,虽然没有声明返回类型,这两个函数也将返回所需的值。另外,int转换将待转换的值四舍五入为最接近的整数,而不是去掉小数部分。例如,如果pounds为114.4,则pounds +0.5等于114.9,int(114.9)等于114。但是如果pounds为114.6,则pounds + 0.5是115.1,而int(115.1)为115。
原则上说,最好使用显式转换,而避免隐式转换。C++11中关键字explicit也同样适用于转换函数。
explicit operator int() const;
explicit operator double() const;
有了这些声明后,需要强制转换时将调用这些运算符。
和转换构造函数一样,转换函数也有其优缺点。提供执行自动、隐式转换的函数所存在的问题是:在用户不希望进行转换时,转换函数也可能进行转换。
另一种方法是,用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数才会执行。
Stonewt::operator int(){return int (pounds + 0.5);}//可以替换为
int Stonewt::Stone_to_int(){return int(pounds+0.5);}
int plb = poppins;//Not ok替换之后这就是非法的
int plb = poppins.Stone_to_int();//ok但是可以这么做
总之,C++为类提供了下面的类型转换:
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将int值赋给Stonewt对象时,接受int参数的Stonewt类构造函数将自动被调用。然而,在构造函数声明中使用explicit可防止隐式转换,而只允许显式转换。
- 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员,没有返回类型、没有参数、名为operator typeName( ),其中,typeName是对象将被转换成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自动被调用。
11.6.2 转换函数与友元函数
使用成员函数与友元函数都可以实现操作符的重载,如果是用成员函数实现的函数重载,则其第一个操作数必定是函数对象;如果是用友元函数实现的操作符重载,如果提供了Stonewt(double)构造函数,则其第一个参数可以为double类型。因为会首先将第一个double值强制转换为Stonewt对象。
但是在这种情况下,如果定义了operator double()成员函数,将造成混乱,因为该函数将提供另一种解释方式。编译器不是将kennyD转换为double并执行Stonewt加法,而是将jennySt转换为double并执行double加法。过多的转换函数将导致二义性。
total = jennySt + KennyD;//将转换为
total = operator+(jennySt, KennyD);
实现加法时的选择
要将double量和Stonewt量相加,有两种选择。第一种方法是将下面的函数定义为友元函数,让Stonewt(double)构造函数将double类型的参数转换为Stonewt类型的参数:
operator+(const Stonewt & , const Stonewt &);
第二种方法是,将加法运算符重载为一个显式使用double类型参数的函数:
Stonewt operator + (double x);//成员函数
friend Stonewt operator + (double x, Stonewt& t);
这样,下面的语句将与成员函数operator + (double x)完全匹配:
total = jennySt + kennyD;
而下面的语句将与友元函数operator + (double x, Stonewt &s)完全匹配:
total = pennyD + jennySt;
每一种方法都有其优点。第一种方法(依赖于隐式转换)使程序更简短,因为定义的函数较少。这也意味程序员需要完成的工作较少,出错的机会较小。这种方法的缺点是,每次需要转换时,都将调用转换构造函数,这增加时间和内存开销。第二种方法(增加一个显式地匹配类型的函数)则正好相反。它使程序较长,程序员需要完成的工作更多,但运行速度较快。
如果程序经常需要将double值与Stonewt对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了更保险,可以使用显式转换。
11.7 总结
一般来说,访问私有类成员的唯一方法是使用类方法。C++使用友元函数来避开这种限制。要让函数成为友元,需要在类声明中声明该函数,并在声明前加上关键字friend。
C++扩展了对运算符的重载,允许自定义特殊的运算符函数,这种函数描述了特定的运算符与类之间的关系。运算符函数可以是类成员函数,也可以是友元函数(有一些运算符函数只能是类成员函数)。要调用运算符函数,可以直接调用该函数,也可以以通常的句法使用被重载的运算符。对于运算符op,其运算符函数的格式如下:
operator op (argument list)
argument-list表示该运算符的操作数。如果运算符函数是类成员函数,则第一个操作数是调用对象,它不在argument-list中。例如,本章通过为Vector类定义operator +( )成员函数重载了加法。如果up、right和result都是Vector对象,则可以使用下面的任何一条语句来调用矢量加法:
result = up.operator + right;
result = up + right;
在第二条语句中,由于操作数up和right的类型都是Vector,因此C++将使用Vector的加法定义。
当运算符函数是成员函数时,则第一个操作数将是调用该函数的对象。例如,在前面的语句中,up对象是调用函数的对象。定义运算符函数时,如果要使其第一个操作数不是类对象,则必须使用友元函数。这样就可以将操作数按所需的顺序传递给函数了。
最常见的运算符重载任务之一是定义<<运算符,使之可与cout一起使用,来显示对象的内容。要让ostream对象成为第一个操作数,需要将运算符函数定义为友元;要使重新定义的运算符能与其自身拼接,需要将返回类型声明为ostream &。下面的通用格式能够满足这种要求:
ostream & operator << (ostream & os , sonst c_name & obj){
os << ...;
return os;
}
然而,如果类包含这样的方法,它返回需要显示的数据成员的值,则可以使用这些方法,无需在operator<<( )中直接访问这些成员。在这种情况下,函数不必(也不应当)是友元。
C++允许指定在类和基本类型之间进行转换的方式。首先,任何接受唯一一个参数的构造函数都可被用作转换函数,将类型与该参数相同的值转换为类。如果将类型与该参数相同的值赋给对象,则C++将自动调用该构造函数。例如,假设有一个String类,它包含一个将char *值作为其唯一参数的构造函数,那么如果bean是String对象,则可以使用下面的语句:
bean = "pinto";
然而,如果在该构造函数的声明前加上了关键字explicit,则该构造函数将只能用于显式转换:
bean = String("pinto");
要将类对象转换为其他类型,必须定义转换函数,指出如何进行这种转换。转换函数必须是成员函数。将类对象转换为typeName类型的转换函数的原型如下:
operator typeName();
注意,转换函数没有返回类型、没有参数,但必须返回转换后的值(虽然没有声明返回类型)。例如,下面是将Vector转换为double类型的函数:
Vector::operator double(){
...
return a_double_value;
}
经验表明,最好不要依赖于这种隐式转换函数。