第十一章目录
11.1-11.2 运算符重载
C++允许将运算符扩展到用户定义的类型。
语法格式:
// function prototype
Time operator+(const Time& t) const;
//function implementation
Time Time::operator+(const Time& t) const{
}
//two function calls
Time t1, t2, total;
total = t1 + t2; //operator notation
total = t1.operator+(t2); //function notation
可重载的操作符
+ | - | * | / | % | ^ |
---|---|---|---|---|---|
& | 或 | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | 或= | << | >> | >>= |
<<= | == | != | <= | >= | && |
并 | ++ | - - | , | ->* | -> |
() | [] | new | delete | new[] | delete[] |
重载限制
- 重载运算符必须至少有一个操作数是用户定义的类型,防止用户为标准类型重载运算符。(例如:不能重载(-)给double)
- 使用运算符时不能违反运算符原来的句法规则。(例如:不能将+重载成一个操作符,如:+a;)
- 不能创建新的运算符。
- 不能重载下面的运算符
【注意】:在类内重载操作符,只能有1个形参。友元可以解决。
11.3 友元
P390
通过让函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
为何需要友元?
在为类重载二元运算符时(带两个参数的运算符)常常需要友元。
例如:
A = B * 2;
// 转化为
A = B.operator*(2);
// 下面是否报错
A = 2 * B;// 报错,需要反转操作数的顺序
从实际意义上讲,B * 2 与 2 * B应该都是执行相乘。
但在类内重载操作符,只能有1个形参。非成员函数不是由对象调用,它使用的所有值(包括对象)都是显式参数。但是非成员函数不能访问类的私有变量。
因此,需要友元函数,类友元函数是非成员函数,但可以访问私有成员。
创建友元
//类内声明
friend Time operator*(double m, const Time & t);
//类外实现
Time operator*(double m, const Time & t){
}// 注意:没有friend
//调用
A = 2*B;
A = operator*(2, B);//等价于
友元函数是否有悖于OOP ?
乍一看,友元可能会违反了OOP数据隐藏的原则。但是这个观点太片面了。
相反,友元函数应看作类的扩展接口。要实现 B * 2 与 2 * B,前一个需要成员函数和后一个需要友元函数。
类方法和友元只是表达类接口的两种不同的机制。
B * 2 与 2 * B实现技巧:
//function prototype
// B*m
Time operator*(double m) const;
// m*B, inline definition
friend Time operator*(double m, const Time & t)
{
return t*m;}
【技巧】:如果要为类重载运算符,并将非类的项作为其第一操作符,则可以用友元函数来反转操作数的顺序。(重要)
重载<< 运算符
最初,<< 运算符是C和C++ 的位运算符,将值中的位左移。ostream类对该运算符进行重载,将其转化为一个输出工具。
一种方法是将一个新的函数运算符定义添加到ostream类声明中。但修改iostream文件是一个非常危险的主意,这样做会在标准接口上浪费时间。
第二种方法是用友元函数。
cout << times;
上面的语句将使用2个参数,第一个是ostream对象,第二个是Time对象,因此,需要通过友元函数进行<< 重载运算符。
代码如下:
//函数声明
friend std::ostream& operator<< (std::ostream& os,const Time& t);
//函数定义
std::ostream& operator<< (std::ostream& os,const Time& t){
os << t.hours << t.minutes;
return os;
}
-
问题1:为啥要友元Time,而不是友元ostream?
因为要访问Time的私有成员,同时ostream被看作对象整体使用。 -
问题2:为啥形参是 std::ostream&?
调用os << t.hours 应使用cout 对象本身,而不是它的拷贝,同时ostream流对象不允许拷贝,所以如果作为返回值或者形参时,只能用引用。因此,需要按引用传递该对象。 -
问题3:为啥要返回 std::ostream&?
首先为了实现cout<<连续输出,需要返回一个ostream对象,同时函数的形参是对象引用,函数返回值就是传递给它的对象。
11.4 重载运算符:成员函数?友元函数
Time operator+(const Time& t) const; //成员函数
friend Time operator+(const Time& t1, const Time& t2); // 友元函数
Time t1, t2, t3;
t1 = t2 + t3;
两个函数原型都与t1 = t2 + t3匹配。必须选择其中的一种格式,否则将产生二义性错误。
哪种格式最好呢?
对于某些运算符重载,成员函数是唯一的选择。在其他情况下,两种格式的差别不大。在为类定义类型转化时,使用非成员函数版本更好。
11.5 再谈重载:一个矢量类(技巧)
在同一个对象中包含两种描述同一样东西的不同方式。
- 使用枚举在类中创建两个常量,代表着两个不同的模式。
使用Mode来控制函数状态的成员被称为状态成员。
enum Mode{
RECT, POL};
- 在类声明中定义的,将自动成为内联函数。适合短小的函数
- 不会修改对象数据的方法,建议声明使用const限定
double xval() const {
return x;}
- 方法reset() 必不可少,虽然构造函数可以实现同样的功能,但会增加额外的步骤,创建一个临时对象。
shove.reset(100, 300); // reset() function
shove = Vector(100, 300); // create and assign a temporary object
- 使用构造函数实现通过计算得到一个新类对象的方法
优点:确保了新的对象是按照正确的方式创建的。
Vector Vector::operator+(const Vector& b) const
{
return Vector(x + b.x, y + y.b);
}
- 只要运算符函数的特征标不同,就可以多次重载同一运算符。
//-运算符有两种含义,一是减法运算符,二是负号运算符
//函数原型
Vector operator-(const Vector& b) const; //重载减法运算符
Vector operator-() const; //重载负号运算符
11.6 类的自动转换和强制转换
C++为类提供了两种类型转换。
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。(例如:int 转换为 类类型)
- 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。(例如:类类型 转换为 int) 格式:operator int()
隐式转换
例如
Stonewt(double lbs);
Stonewt myCat;
myCat = 19.6; //use Stonewt(double) to convert 19.6 to Stonewt
// 有默认参数的也可以隐式转换
Stonewt(double lbs, double stn = 1.2);
myCat = 19.6; //yes
关键字explicit
作用是:修饰的类构造函数为显示,关闭自动转换,只能显示强制类型转换。
用法
explicit Stonewt(double lbs);
myCat = 19.6; //not
myCat = (Stonewt) 19.6; // yes
下面的语句首先将int 转换为double,然后再使用Stonewt(double)构造。
然而,当且仅当转换不存在二义性,才会进行这种二步转换,如果还定义了Stonewt(int),则编译器拒绝下面转换。
myCat = 20;
转换函数(类对象 -> 其他数据类型)
转换函数注意事项:
- 转换函数必须是类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
//conversion functions prototype
operator int() const;
operator double() const;
double pounds;
// Function implementation
Stonewt::operator int() const{
return pounds;}
提供执行自动、隐式转换函数所存在的问题:
在用户不希望转换时,转换函数也也可能进行转换。
【注意】:最好使用显式转换,而避免隐式转换。