前言

接着上一章类对象与类,在这一章里将介绍运算符重载、友元与类的自动转换和强制类型转换。这是类专题的其二,其他专题内容如下:

  1. (其一):对象与类
  2. (其二):使用类
  3. (其三):类和动态内存分配
  4. (其四):类继承

运算符重载

运算符重载是一种形式的C++多态。我们之前介绍过了函数重载,我们能够定义多个名称相同但特征标(参数列表)不同的函数的。运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多种含义。

举一个简单的运算符重载的例子:将*运算符用于地址,将得到存储在这个地址中的值;但将它用于两个数字时,得到的将是它们的乘积,C++根据操作数的数目和类型来决定采用哪种操作。

这是编译器已经实现的内容,我们要做到什么呢,来看一个例子:

for(int i=1;i<=10;i++)
{
    a[i]=b[i]+c[i];//add element by element
}

而在OPP中,我们想做到的是a=b+c这样的语句,这时候就需要把“+”运算符进行重载了。要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:

<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}

接下来用一个例子来看如何使用运算符重载:

首先在一个mytime0.h文件中定义一个类:

#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);
    const Time Sum(const Time & t) const;
    void Show() const;
};
#endif

Time类提供了用于调整和重新设置时间、显示时间、将两个时间相加的方法。

在mytime0.cpp中提供方法:

#include <iostream>
#include "mytime0.h"

Time::Time()
{
    hours = minutes = 0;
}

Time::Time(int h, int m )
{
    hours = h;
    minutes = m;
}

void Time::AddMin(int m)
{
    minutes += m;
    hours += minutes / 60;
    minutes %= 60;
}

void Time::AddHr(int h)
{
    hours += h;
}

void Time::Reset(int h, int m)
{
    hours = h;
    minutes = m;
}

const 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;
}

void Time::Show() const
{
    std::cout << hours << " hours, " << minutes << " minutes";
}

Sum( )函数的代码。注意参数是引用,但返回类型却不是引用。将参数声明为引用的目的是为了提高效率。如果按值传递Time对象,代码的功能将相同,但传递引用,速度将更快,使用的内存将更少。

最后使用主函数调用,在文件usetime0.cpp中:

#include <iostream>
#include "mytime0.h"

int main()
{
    using std::cout;
    using std::endl;
    Time planning;
    Time coding(2, 40);
    Time fixing(5, 55);
    Time total;

    cout << "planning time = ";
    planning.Show();
    cout << endl;
 
    cout << "coding time = ";
    coding.Show();
    cout << endl;
    
    cout << "fixing time = ";
    fixing.Show();
    cout << endl;

    total = coding.Sum(fixing);
    cout << "coding.Sum(fixing) = ";
  	total.Show();
    cout << endl;
    // std::cin.get();
    return 0;
}

这样的组合是一个非常常规的类的使用,我们接下来对它就行修改进行运算符重载:

我们把Sum( )的名称改为operator +( )即可

//declear
Time operator+(const Time & t) const;

//define
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;
}

//use
total = coding + fixing;
//alse use by this
total = morefixing.operator+(total);

我们看到,operator +( )函数的名称使得可以使用函数表示法或运算符表示法来调用它。和Sum( )一样,operator +( )也是由Time对象调用的,它将第二个Time对象作为参数,并返回一个Time对象。因此,可以像调用Sum( )那样来调用operator +( )方法:如果我们有多个对象需要相加,可以直接写成:t1=t2+t3+t4;因为它们是函数的嵌套关系

这样的重载称为类内重载,是把运算符重载函数作为类的成员函数。

重载运算符也拥有它自己的规则:

  • 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。即不能把"-"重载为加合。
  • 使用运算符时不能违反运算符原来的句法规则(几元运算),不能修改运算符的优先级。
  • 不能创建新运算符,因为重载的是原来有的运算符。
  • 不能重载下面的运算符。
    sizeof:sizeof运算符。
    .:成员运算符。
    . *:成员指针运算符。
    :::作用域解析运算符。
    ?::条件运算符。
    typeid:一个RTTI运算符。
    const_cast:强制类型转换运算符。
    dynamic_cast:强制类型转换运算符。
    reinterpret_cast:强制类型转换运算符。
    static_cast:强制类型转换运算符。
  • 大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符只能通
    过成员函数进行重载。
    =:赋值运算符。
    ( ):函数调用运算符。
    [ ]:下标运算符。
    ->:通过指针访问类成员的运算符。

我们知道把运算符重载函数作为类的成员函数这样的方式重载叫类内重载,那么有没有类外重载呢?当时是有的,但在这之前还需要介绍友元先。


友元

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

在为类重载二元运算符时(带两个参数的运算符)常常需要友元。例如重载一个运算符*,表示一个Time值与一个double值相乘。与前面不同的是,它只有一个参数是Time类型,当我们写成例如:

A=3.5*B;这样的形式的时候,根据之前的相当于是A=3.5.operator*(B);但是这个写法是错的,因为3.5不是对象,不能用于成员函数。

这种情况的最好解决方法就是不写成这样😂,或者非成员函数。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。

那这个函数的原型就应该是这样的:

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

对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。

但是,非成员函数不能直接访问类的私有数据。所以,就有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数

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

friend operator*(double m, const Time &t);
  • 虽然operator *( )函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
  • 虽然operator *( )函数不是成员函数,但它与成员函数的访问权限相同。
  • 注意声明放在放在类声明中!而且是在public里。

它的定义应该如下:

Time operator*(double m, const Time &t)
{
...
}
  • 定义的时候不需要用成员函数的定义法,即定义的时候不要使用Time::限定符
  • 定义中不需要使用friend关键字

有了上述声明和定义后,我们就可以写这样的语句了:A=3.5*B;它相当于:A=operator*(3.5,B);它是类的友元函数,不是非成员函数,其访问权限与成员函数相同

我们常用的重载一般是<<运算符的重载,因为我们时常需要使用cout来显示对象的内容。

假设trip是一个Time对象。为显示Time的值,前面使用的是Show( )。如果我们想用传统的cout进行输出,能不能通过重载让以下语句成立?

cout<<trip;

要使Time类知道使用cout,我们可以使用友元函数。因为如果使用一个Time成员函数来重载<<,Time对象将是第一个操作数,那就需要这样写:trip<<cout;这样写不符合我们的习惯。

我们接下来用友元函数对它进行重载:

void operator<<(ostream &os, const Time &t)
{
    os<<t.hours<<" hours "<<t.minutes<<" minutes";
}

此时cout作为ostream类的对象传入,就可以使用cout<<trip;了。调用cout << trip应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。Time对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少

我们实现了简单的重载使输出可用了,但是我们无法做到这样的语句,例如:

cout<<"Trip time is: "<<trip<<endl;

正如iosream中定义的那样,<<运算符要求左边是一个ostream对象。因此,ostream类将operator<<( )函数实现为返回一个指向ostream对象的引用,即它返回一个指向调用对象(这里是cout)的引用。所以我们可以对友元函数采用相同的方法。只要修改operator<<( )函数,让它返回ostream对象的引用即可。

ostream & operator<<(ostream &os, const Time &t)
{
    os<<t.hours<<" hours "<<t.minutes<<" minutes";
    return os;//传递ostream对象到下一位,这里可能是cout
}

当我们连续调用的时候,例如上面的语句,cout和字符串调用ostream中定义好的函数,第一步就是输出Trip time is:然后返回了cout对象,接下来是cout对象和Time对象trip作为参数调用友元函数。注意此时第一段并不是调用友元函数,因为字符串不是Time类的对象,同理后面的endl也是。

当然输出能这样重载的对象不止cout,输出到文件的fout,输出标准流对象cerr也可以。如果友元函数很短的时候,我们可以使用内联函数。那么在头文件中就应该这样写:

#ifndef MYTIME3_H_
#define MYTIME3_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 m, const Time & t)
        { return t * m; }   // inline definition
    friend std::ostream & operator<<(std::ostream & os, const Time & t);

};
#endif

定义中关于友元函数的部分:

std::ostream & operator<<(std::ostream & os, const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os; 
}

非成员函数重载

对于很多运算符来说,可以选择类内重载(使用成员函数)或类外重载(使用非成员函数)来实现运算符重载。一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。

对于某些运算符来说(如+),成员函数是唯一合法的选择。在其他情况下,这两种格式没有太大的区别。有时,根据类设计,使用非成员函数版本可能更好(尤其是为类定义类型转换时)。


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

类型转换

我们知道,当我们使用类型不同进行赋值的时候,需要类型转换。类型转换可能是编译器的自动转换,例如:int a=3.33;或者是我们的强制类型转换,例如:double b=(double)5;

那在类的初始化中是否能这样做呢,可以,需要我们修改构造函数。例如在声明中:

#ifndef MYTIME_H_
#define MYTIME_H_
#include <iostream> 

class Time
{
private:
    int hours;
    int minutes;
public:
    Time();
    Time(int h, int m = 0);
    Time(double dv);//input double 
    Time(int iv);//input int
    void AddMin(int m);
    void AddHr(int h);
    void Reset(int h = 0, int m = 0);
};
#endif

在定义中可以这样写:

Time::Time(double dv)
{
    minutes=dv; //double to double
}

那么这样我们就可以使用这样的语句初始化了,例如:

Time t=19.6;

程序将使用构造函数Time(double)来创建一个临时的Time对象,并将19.6作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到t中。这一过程称为隐式转换因为它是自动进行的,而不需要显式强制类型转换。所以,只有接受一个参数的构造函数才能作为转换函数。

将构造函数用作自动类型转换函数似乎是一项不错的特性,但会导致意外的类型转换。因此,C++新增了关键字explicit,用于关闭这种自动特性。也就是说,可以这样声明构造函数:

explicit Time(double dv);

这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换:

Time t1=19.6;//错误
Time t2=Time(19.6);//OK
Time t3=(Time)19.6;//OK

这样的强制类型转化就可以不拘束于初始化一个数值,并转化一个数值了,只要我们修改构造函数就可以一次性转换多个值。

如果我们在运算符重载的时候,运算符两侧一个为类参数,一个为普通参数时,例如+,定义为友元可以让程序更容易适应自动类型转换,两个操作数都成为函数参数,因此与函数原型匹配。或者我们还可以重载为一个显式使用相应类型参数的函数。


转换函数

我们前面将数字转换为Time对象,那可以做相反的转换吗?也就是说,是否可以将Time对象转换为double值。可以这样做,但不是使用构造函数。构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的C++运算符函数——转换函数。
转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。接下来创建转换函数:
operator typeName ();
  • 转换函数必须是类方法;
  • 转换函数不能指定返回类型;
  • 转换函数不能有参数。

例如,转换为double类型的函数的原型:

operator double() const;//const可选

typeName(这里为double)指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。当然这里的类方法即把声明放在public里。

当然我们还需要给转换函数定义,例如:

Time::operator double() const
{
    return minutes;
}

最后使用的时候,例如:

Time t(19.6);
double times=t;//转换函数

当然显示的情况也可以,即:

double t1=double(t);
double t2=(double)t;

在C++11中,可将转换运算符声明为显式的,即使用关键字explicit,C++98及之前不能用于转换函数。

所以总结一下,C++为类提供了下面的类型转换。

  • 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将int值赋给Time对象时,接受int参数的Time类构造函数将自动被调用。Time(int var);在构造函数声明中使用explicit可防止隐式转换,而只允许显式转换。
  • 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成
    员,没有返回类型、没有参数、名为operator typeName( ),其中,typeName是对象将被转换
    成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自
    动被调用。

总结

这一章介绍了定义和使用类的许多重要方面。

  • 一般来说,访问私有类成员的唯一方法是使用类方法。C++使用友元函数来避开这种限制。要让函数成为友元,需要在类声明中声明该函数,并在声明前加上关键字friend。
  • C++扩展了对运算符的重载,允许自定义特殊的运算符函数,这种函数描述了特定的运算符与类之间的关系。运算符函数可以是类成员函数,也可以是友元函数。要调用运算符函数,可以直接调用该函数(显式),也可以以通常(隐式)的句法使用被重载的运算符。
  • C++允许指定在类和基本类型之间进行转换的方式。首先,任何接受唯一一个参数的构造函数都可被用作转换函数,将类型与该参数相同的值转换为类。如果将类型与该参数相同的值赋给对象,则C++将自动调用该构造函数。
  • 如果在该构造函数的声明前加上了关键字explicit,则该构造函数将只能用于显式转换。
  • 要将类对象转换为其他类型,必须定义转换函数,指出如何进行这种转换。转换函数必须是成员函数。转换函数没有返回类型、没有参数,但必须返回转换后的值(虽然没有声明返回类型)。
  • 最好不要依赖于隐式转换函数

后记

好了,至此C++类专题中第而节,使用类就结束了。下一章 类和动态内存分配 见~

Reference:《C++ Primer Plus 6th》第11章

这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2024-01-14