运算符重载
C++允许将运算符重载扩展到用户定义的类型。定义方法类似下面的例子:
operator+()重载+运算符,operator*()重载*运算符。下面讲一个运算符重载的示例:
定义一个类:
// mytime0.h -- Time class before operator overloading
#ifndef MYTIME0_H_
#define MYTIME0_H_
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
Time Sum(const Time & t) const;
//或者使用下面的运算符重载函数
Time operator+(const Time & t) const;
void Show() const;
}
如果不定义一个运算符重载函数,那么就需要上面的那个Sum()函数,Sum函数实现如下:
Time Time::Sum(const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
如果定义运算符重载函数,则其实现如下:
Time Time::operator+(const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
这样就可以使用+运算符直接对对象进行操作,例如:
total = coding + fixing;
这里需要注意的是,在运算符表示法中,运算符左侧的对象(这里是coding)是调用对象,运算符右侧的对象(这里为fixing)是作为参数被传递的对象。而且可以连加。
重载的限制
- 重载后的运算符必须至少有一个操作数是用户定义的类型(防止用户为标准类型重载运算符)。
- 使用运算符时不能违反原来的句法规则。例如不能将求模运算符(%)重载成使用一个操作数。同样,也不能修改运算符的优先级。
- 不能创建新的运算符。例如,不能定义operator**()函数来表示求幂。
- 不能重载下面的运算符:
- sizeof运算符
- ‘.’ 成员运算符
- ‘.*’ 成员指针运算符
- ‘::’ 作用域解析运算符
- ‘?:’ 条件运算符
- typeid:一个RTTI运算符
- const_cast :强制类型转换运算符
- dynamic_cast:强制类型转换运算符
- reinterpret_cast:强制类型转换运算符
- static_cast:强制类型转换运算符
- 只能通过成员函数重载的运算符:
- =: 赋值运算符
- (): 函数调用运算符
- []: 下标运算符
- ->: 通过指针访问类成员的运算符
成员函数和非成员函数均可重载运算符:
+ | - | * | / | % | ^ |
---|---|---|---|---|---|
& | | | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= |
<<= | == | != | <= | >= | && |
|| | ++ | – | , | ->* | -> |
() | [] | new | delete | new[] | delete[] |
友元
C++控制类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以至于不适合特定的编程问题。这种情况下,C++提供了另外一种形式的访问权限:友元。友元有三种:
- 友元函数;
- 友元类;
- 友元成员函数。
通过让函数成为类的友元,可以赋予该函数域类的成员函数相同的访问权限。
成员函数重载运算符的时候,只需要传递右操作数,这样产生了一些限制。非成员函数重载运算符时,可以将符号两端的操作数作为第一个和第二个参数传入函数中。所以需要友元定义一个非成员函数,其目的是让非成员函数也可以访问类的私有数据。
创建友元
创建友元函数的第一步是将其原型放在类声明中,并在原型前面加上关键字friend:
friend Time operator*(double m, const Time & t); // goes in class declaration
该原型意味着下面两点:
- 虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用。
- 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
第二步是编写定义。因为它不是成员函数,所以不需要使用Time::限定符。另外,也不需要在定义中使用关键字friend。定义应该如下:
Time operator*(double mult, const Time & t) // friend not used int definition
{
Time result;
long totalminutes = t.hours * mult * 60 + t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
常用的友元:重载<<运算符
重载<<运算符可以使自己定义的类直接使用<<运算符进行打印输出。重载形式如下:
ostream & operator<<(ostream & os, const Time & t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
return os;
}
这样就可以直接如下使用:
cout << "Trip time: " << trip << " (Tuesday)\n";
而且这个版本的定义还可以用于将输出写入文件中:
#include <fstream>
...
ofstream fout;
fout.open("savetime.txt");
Time trip(12, 40);
fout << trip;
最后一条语句将被转换成这样:
operator<<(fout, trip);
由于类继承的属性让ostream引用能够指向ostream对象和ofstream对象。(ofsream是派生类,ostream是基类,基类引用可以指向派生类对象)
下面是一个Time类的完整声明:
#ifndef MYTIME_H_
#define MYTIME_H_
#include <iostream>
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
Time operator+(const Time & t) const;
Time operator-(const Time & t) const;
Time operator*(double n) const;
friend Time operator*(double n, const Time & t)
{ return t * m; }
friend std::ostream & operator<<(std::ostream & os, const Time & t);
}
最后一个函数的定义:
std::ostream & operator<<(std::ostream & os, const Time & t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
return os;
}
类的自动转换和强制类型转换
标准类型变量都是可以进行自动的类型转换的,我们希望类也可以与某些基本类型或者其它的类进行类型转换,一般来说是用构造函数进行类型转换的。比如有一个类Stonewt,其构造函数是Stonewt(double lbs),那么就可以利用这个构造函数将double类型的数转换为Stonewt类型,而且C++中可以隐式的使用这个方法,也就是说,可以这么写代码:
Stonewt myCat; // creat a Stonewt object
myCat = 19.6; //use Stonewt(double) to convert 19.6 to Stonewt
程序将使用构造函数Stonewt(double)来创建一个临时的Stonwt对象,并将19.6作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到myCat中。这一过程称为隐式转换。
只有接受一个参数的构造函数才能作为转换函数。然而如果给第二个参数提供默认值,它就也可以作为转化函数。
这种自动类型转换特性有时会带来问题,所以是可以关闭的,使用explicit关键字在声明构造函数前可以关闭这个特性:
explicit Stonewt(double lbs); // no implicit conversions allowed
但是还是可以显式转换:
Stonewt myCat; // creat a Stonewt object
myCat = 19.6; // not valid if Stonewt(double) is declared as explicit
myCat = Stonewt(19.6); // ok, an explicit conversion
myCat = (Stonewt) 19.6; // ok, old form for explicit typecast
注意: 只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。
隐式转换发生的一些情况:
- 将Stonewt对象初始化为double值时。
- 将double值赋给Stonewt对象时。
- 将double值传递给接受Stonewt参数的函数时。
- 返回值被声明为Stonewt的函数试图返回double值时。
- 在上述任意一种情况下,使用可转换为double类型的内置类型时。
最后一句话很重要,就是说传入的值不是double,但是可以转换为double,那么就会先转换为double然后再转换为Stonewt类。然而,当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了函数Stonewt(long),则编译器将拒绝这些语句,可能指出:int可被转换为long或者double,因此调用存在二义性。
转换函数
如果想做相反的变换必须使用特殊的C++运算符函数——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。例如,如果定义了从Stonewt到double的转换函数,可以使用下面的转换:
Stonewt wolfe(285.7);
double host = double(wolfe); // syntax #1
double thinker = (double)wolfe; //syntax #2
也可以让编译器来决定如何做:
Stonewt wells(20, 3);
double star = wells; // implicit use of conversion function
编译器发现,右侧是Stonewt类型,而左侧是double类型,因此它将查看程序员是否定义了与此匹配的转换函数。(如果没有找到这样的定义,编译器将生成错误消息,指出无法将Stonewt赋给double。)
创建转换函数:要转换为typeName类型,需要使用这种形式的转换函数:
operator typeName();
注意:
- 转换函数必须是类方法;
- 转换函数不能指定返回类型;
- 转换函数不能有参数。
例如,转换为double类型的函数的原型如下:
operator double();
示例如下:
class Stone
{
...
public:
...
// conversion functions
operator int() const;
operator double() const;
}
//conversion functions
Stonewt::operator int() const
{
return int(pounds + 0.5);
}
Stonewt::operator double() const
{
return pounds;
}
如果担心隐式转换出问题,那么也可以声明成显式的:
class Stone
{
...
public:
...
// conversion functions
explicit operator int() const;
explicit operator double() const;
}
这样就必须显式的调用转换函数了,应该谨慎的使用隐式转换函数,通常最好选择仅在被显式地调用时才会执行的函数。