C++ primer plus 第11章使用类

1 运算符重载

operatorop(argumnt-list)

例如,operator+()重载+运算符。

2 计算时间:一个运算符重载示例

如果要计算两个时间之和,如2小时40分与1小时30分之和,可以重载运算符+,在类中的成员函数为:

Time operator+(const Time & t) const;

2.1 重载限制

  • 重载后的运算符至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将减法运算符(-)重载为两个double值的和。
  • 使用运算符不能违反运算符原来的句法规则,例如不能将求模运算符(%)重载成使用一个操作数。
  • 不能创建新运算符。例如operator**()
  • 不能从在下面的运算符:
    1. sizeof:sizeof 运算符
    2. .: 成员运算符
    3. .*: 成员指针运算符
    4. ::: 作用域解析运算符
    5. ?:: 条件运算符
    6. typeid: 一个RTTI运算符
    7. 强制转换类型运算符
  • 大多数运算符都可以通过成员或非成员函数进行重载,但是下面运算符只能通过成员函数进行重载
    1. =: 赋值运算符
    2. (): 函数调用运算符
    3. []: 下表运算符
    4. ->: 通过指针访问类成员运算符

3 友元

C++控制对类对象私有部分的访问时,通常共有类方法是访问的唯一途径,但是C++还提供了另外一种形式的访问权限:友元。友元有三种:

  • 友元函数
  • 友元类
  • 友元成员函数

创建友元函数的第一步是将其原型放在类声明总,并在原型声明前加上关键字friend

friend Time operator*(double m, const Time & t);

该原型意味着下面两点:

  • 虽然operator*()函数是在类声明中声明的,但是它不是成员函数,因此不能使用成员运算符进行调用。
  • 虽然operator*()函数不是成员函数,但是它与成员函数的访问权限相同。

在编写函数定义时,因为它不是成员函数,所以不要使用::限定符。另外,不要再定义中用关键字 friend,定义应如下:

Time operator*(double m, const Time & t)
{
    ...
    ...
    ...
}

提示:如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以使用友元函数来反转操作数的顺序。

4 重载运算符:作为成员函数或非成员函数

对于很多运算符,可以选择使用成员函数和非成员函数来实现运算符重载。一般来说非成员函数应该是友元函数,这样才可以访问类的私有数据。

5 再谈重载:一个矢量类

可以为Vector类重载运算符,实现矢量的加法和乘法,同时还可以使用Vector类来模拟随机游走

// randwalk.cpp -- using the Vector class
// compile with the vect.cpp file
#include <iostream>
#include <cstdlib>      // rand(), srand() prototypes
#include <ctime>        // time() prototype
#include "vect.h"
int main()
{
    using namespace std;
    using VECTOR::Vector;
    srand(time(0));     // seed random-number generator
    double direction;
    Vector step;
    Vector result(0.0, 0.0);
    unsigned long steps = 0;
    double target;
    double dstep;
    cout << "Enter target distance (q to quit): ";
    while (cin >> target)
    {
        cout << "Enter step length: ";
        if (!(cin >> dstep))
            break;

        while (result.magval() < target)
        {
            direction = rand() % 360;
            step.reset(dstep, direction, Vector::POL);
            result = result + step;
            steps++;
        }
        cout << "After " << steps << " steps, the subject "
            "has the following location:\n";
        cout << result << endl;
        result.polar_mode();
        cout << " or\n" << result << endl;
        cout << "Average outward distance per step = "
            << result.magval()/steps << endl;
        steps = 0;
        result.reset(0.0, 0.0);
        cout << "Enter target distance (q to quit): ";
    }
    cout << "Bye!\n";
/* keep window open
    cin.clear();
    while (cin.get() != '\n')
        continue;
    cin.get();
*/
    return 0; 
}

6类的自动转换和强制类型转换

分为三种情况:

(1)如果要进行的转换之间是兼容的,C++自动将值转换为接收变量的类型:

int count = 8.8;
double time= 3; 
long day = 8;

(2)C++不自动转换不兼容的类型:

int * pr = 10;

(3)强制类型转换:

int *pr = (int *) 10;

这条语句通过类型的强制转换的结果是这条语句可以正常进行。表示:定义一个地址为10 的指针。(这种赋值是没有什么意义的)

6.1 将别的数据类型转换为类对象

将类定义成与基本类型相关或者是与另一个类相关,使得从一个类到另一个类的转换时有意义的。在这种情况下可以指示C++如何进行自动类型转换,或者通过强制类型转换来完成:

下面我们通过一个例子来详细介绍:

需求:计量单位磅转换为英石:

// stonewt.h -- definition for the Stonewt class
#ifndef STONEWT_H_
#define STONEWT_H_
class Stonewt
{
private:
    enum {Lbs_per_stn = 14};      // pounds per stone
    int stone;                    // whole stones
    double pds_left;              // fractional pounds
    double pounds;                // entire weight in pounds
public:
    Stonewt(double lbs);          // constructor for double pounds
    Stonewt(int stn, double lbs); // constructor for stone, lbs
    Stonewt();                    // default constructor
    ~Stonewt();
    void show_lbs() const;        // show weight in pounds format
    void show_stn() const;        // show weight in stone format
};
#endif
// stonewt.cpp -- Stonewt methods
#include <iostream>
using std::cout;
#include "stonewt.h"

// construct Stonewt object from double value
Stonewt::Stonewt(double lbs)
{
    stone = int (lbs) / Lbs_per_stn;    // integer division
    pds_left = int (lbs) % Lbs_per_stn + lbs - int(lbs);
    pounds = lbs;
}

// construct Stonewt object from stone, double values
Stonewt::Stonewt(int stn, double lbs)
{
    stone = stn;
    pds_left = lbs;
    pounds =  stn * Lbs_per_stn +lbs;
}

Stonewt::Stonewt()          // default constructor, wt = 0
{
    stone = pounds = pds_left = 0;
}

Stonewt::~Stonewt()         // destructor
{
}

// show weight in stones
void Stonewt::show_stn() const
{
    cout << stone << " stone, " << pds_left << " pounds\n";
}

// show weight in pounds
void Stonewt::show_lbs() const
{
    cout << pounds << " pounds\n";
}

Stonewt类有三个构造函数,可以允许将Stonewt对象初始化为一个浮点数或者两个浮点数,也可以不进行初始化。

6.1.1 隐式的自动类型转换

下面我们先来看一下这个构造函数:

Stonewt(double lbs);

这个构造函数允许将double类型的值转化为Stonewt类型,因此可以编写:

CStonewt myCat;
myCat = 19.6;

原理:程序使用Stonewt(double lbs),来创建一个临时的Stonewt对象,然后将19.6赋值给它,随后,采用逐成员赋值的方式将该对象的内容赋值到myCat中,这一过程成为隐式转换,因为他是自动进行的。而不需要显示强制类型转换。

只有接受一个参数的构造函数才能作为转换函数,例如:Stonewt(int stn, double lbs);不能作为转换类型。但是如果给第二个参数提供默认值,它便可以转换int

Stonewt(int stn,double lbs = 0);

6.1.2 显式类型强制转换

将构造函数用作自动类型转换函数似乎是一种非常好的特性,但是这种自动类型转换并不是总是合乎需要的,因为这可能导致意外的类型转换:

解决方法:C++新增了 关键字explicit用于关闭这种自动特性,也就是说可以这样声明构造函数:

explicit Stonewt(double lbs);

这将关闭隐式类型转换,但仍然允许显示类型转换,即显式类型转换:

Stonewt myCat;
myCat = 19.6;  //(这种方式是错的)
myCat = Stonewt(19.6);     //(允许的)
myCat = (Stonewt) 19.6;   //(允许的)

那么问题来了,编译器都在什么时候调用Stonewt(double lbs)函数呢?(如果声明为explict,则只能显式类型转换,不支持以下几点):

  • 将Stonewt对象初始化为double值时;
  • 将double值传递给接受Stonewt参数的函数时;
  • 返回值被声明为Stonewt得到函数试图返回double值时;
  • 将double值赋给Stonewt对象时;
  • 在上述任一一种情况下,使用可转换为double类型的内置类型时;

最重要的一点

函数原型化提供的参数匹配过程中,允许使用Stonewt(double)构造函数来转换其他数值类型。

stonewt  Jumb(7000);
Jumb = 7300;

这两条语句都是先将int 转化为double,然后使用Stonewt(double)构造函数。然而这是有前提性的:

即不存在二义性。如果还存在Stonewt(long),则编译器将拒绝执行这些语句,因为int可以转为long 或者double,一次调用会出现二义性

6.2 将类类型转换为别的类型(转换函数)

构造函数只用于从某种类型到类类型的转换,要进行相应的反转,必须使用特殊的C++运算符函数–转换函数

转换函数是用户自定义的强制类型转换,可以像使用强制类型转换那样使用它们。

Stonewt wolfe =(285.7); 
double host = double (wolfe); 
double thinker = (double) wolfe; 

注意点:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数

要将类类型转换为double和int类型,则:

1.在Stonewt.h文件中声明类方法:

operator double () const;  
operator int () const;  

2.在cpp文件中完成定义:

Stonewt::operator int() const  
{  
  erturn pounds;  
}  
Stonewt::operator double () const  
{  
  cout << int(pounds + 0.5);  
}  

3.此时,我们就可以在main函数中将Stonewt赋值给int、double 了

CStonewt poppins(9, 2.8);  
double p_wt = poppins;  
cout <<(int) poppins<<endl;

还有一种情况需要注意:

long temp = popins; 这条语句是不正确的,因为存在二义性,因为int、double都可以转换为long 类型。如果删除一个int或者double的转换函数,则这条语句将可以执行。

6.3 总结

1.要谨慎的使用隐式转换函数,通常最好的选择是仅在被显示的调用时才会执行。

2.只有一个参数的类构造函数将用于类型与该参数相同的值转换为类类型。 例如:将int类型转换为CStonewt对象是,接受int 参数的CStonewt类构造函数将自动被调用,然而在构造函数声明中使用explicit可防止隐式转换,而只允许显式转换。

3.被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。

转换函数是类成员,没有返回类型,没有参数名为 operator typeName();

其中 typeName是对象将被转换为的类型,将类对象赋给typeName变量或者将其强制转换为typeName类型时,该转换函数将会被自动调用。

4.尤其要注意过多的转换函数将导致出现二义性的几率变大,要谨慎防止二义性的出现。

6.4 转换函数和友元函数

普通成员函数和友元函数都能实现操作符的重载,如重载+运算符实现两个类对象的相加。有的时候你声明的操作符重载函数的形参是两个对象,但是实参中有一个类型兼容类型变量,这个时候如果你的类中存在类型转换函数,它就可能把原来是对象的那个实参通过类型转换函数转换成类型兼容类型。

成员函数和友元函数都可以实现:

Stonewt jennySt(9, 12);
double kennyD = 146.0;
Stonewt total;
total = jennySt + kennyD;
转换为:
total = operator+(jennySt + kennyD);
或
total = jennySt.operator+(kennyD);

但是只有友元函数才能实现:

Stonewt jennySt(9, 12);
double pennyD = 146.0;
Stonewt total;
total = pennyD + jennySt;
转换为:
total = operator+(pennyD, jennySt);
不能转换为
total = pennyD.operator+(jennySt);

这是因为当你使用一个成员函数版的操作符重载函数时,原本的调用者应该是该类的对象,但实际上调用者是一个类型兼容类型的变量,它显然不能成为成员重载函数的调用者,于是调用失败了。而一个友元函数却不会出现此状况,并且友元函数在接受实参时会使用构造函数将一个类型兼容类型转换为类类型。