C++ Primer 学习笔记_63_重载操作符与转换 --转换与类类型【上】

重载操作符与转换

--转换与类类型【上】

引言:

在前面我们提到过:可以用一个实参调用的非explicit构造函数定义一个隐式转换。当提供了实参类型的对象需要一个类类型的对象时,编译器将使用该转换。于是:这种构造函数定义了到类类型的转换。

除了定义到类类型的转换之外,还可以定义从类类型到其他类型的转换。即:我们可以定义转换操作符,给定类类型的对象,操作符将产生其他类型的对象。和其他转换一样,编译器将自动应用这个转换。

一、转换为什么有用?

定义一个SmallInt的类,该类实现安全小整数,这个类将使我们能够定义对象以保存与 8位 unsignedchar 同样范围的值,即:0到255。这个类可以捕获下溢和上溢错误,因此使用起来比内置unsignedchar 更安全

我们希望这个类定义unsignedchar 支持的所有操作。具体而言,我们想定义5个算术操作符(+、-、*、/、%)及其对应的复合赋值操作符,4个关系操作符(<、<=、>、>=),以及相等操作符(==、!=)。显然,需要定义16个操作符%>_<%。

1、支持混合类型表达式

而且,我们希望可以在混合模式表达式中使用这些操作符。例如,应该可以将两个SmallInt对象相加,也可以将任意算术类型加到SmallInt。通过为每个操作符定义三个实例来达到目标:

int operator+(int,const SmallInt &);
int operator+(const SmallInt &,int);
SmallInt operator+(const SmallInt &,const SmallInt &);

但是,这个设计仅仅接近内置整数运算的行为,它不能适用于处理浮点类型的混合模式,也不能适当支持longunsignedintunsignedlong的加运算

2、转换减少所需操作符的数目

C++提供了一种机制:一个类可以定义自己的转换,应用于其类类型对象。对SmallInt而言,可以定义一个从SmallInt到 int类型的转换。如果定义了该转换,则无须再定义任何算术、关系或相等操作符(不然要定义48个!。给定到int的转换,SmallInt对象可以用在任何可用int值的地方。

如果存在一个到int的转换,则以下代码:

    SmallInt si(3);
    /**可以存在这样的转换:
    *1. 将si转换成为int值
    *2. 将所得 int 结果转换为 double 值并与双精度字面值常量 3.14159 相加,
    *   得到 double 值
    */
    si + 3.1415926;

二、转换操作符

转换操作符是一种特殊的类成员函数((⊙o⊙)真的很特殊!):它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字operator之后紧跟着转换的目标类型:

class SmallInt
{
public:
    SmallInt(int i = 0):val(i)
    {
        if (i < 0 || i > 255)
        {
            throw std::out_of_range("Bad SmallInt initializer");
        }
    }

    operator int() const
    {
        return val;
    }

private:
    std::size_t val;
};

转换函数采用如下通用形式:

    operator type();

这里,type表示内置类型名、类类型名或由类型别名所定义的名字。对任何可作为函数返回类型的类型(除了void之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。

【注意】

转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。

虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。例如,operatorint 返回一个int值;如果定义operatorSales_item,它将返回一个Sales_item对象,诸如此类。

【最佳实践】

转换函数一般不应该改变被转换的对象。因此,转换操作符通常定义为const成员!

1、使用类类型转换

只要存在转换,编译器将在可以使用内置转换的地方自动调用它

1)在表达式中:

    SmallInt si;
    double dval;
    si >= dval; //si转换为int,然后它们转换成为double

2)在条件中:

    if (si) //si转换为int,然后它们转换成为bool
    {
        //...
    }

3)将实参传给函数或从函数返回值:

    int calc(int);
    SmallInt si;
    calc(si);   //si转换为int,然后调用函数calc

4)作为重载操作符的操作数:

    cout << si << endl;   //si转换为int,然后调用opreator<<

5)在显式类型转换中:

    int ival;
    SmallInt si = 3.14;
    //显式将si转换成为int
    ival = static_cast<int>(si) + 3;

2、类类型转换和标准转换

使用转换函数时,被转换的类型不必与所需要的类型完全匹配。必要时可以在类类型转换之跟上标准转换以获得想要的类型。

    SmallInt si;
    double dval;
    si >= dval; //si转换成为int然后标准转换成为double

3、只能应用一个类类型转换

类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码出错!

class Intergral
{
public:
    Intergral(int i):val(i){}
    operator SmallInt () const
    {
        return val % 256;
    }

private:
    std::size_t val;
};

可以在需要SmallInt的地方使用Intergral,但不能在需要int的地方使用Intergral:

    int calc(int);
    Intergral intVal;
    SmallInt si(intVal);    //OK:intVal转换成为SmallInt
    int i = calc(si);       //OK:si转换成为int
    int j = call(intVal);   //Error

在最后的 calc调用中:没有从Integral到 int的直接转换。int需要两次类类型转换:首先从Integral到 SmallInt,然后从SmallInt到int。但是,语言只允许一次类类型转换,所以该调用出错

4、标准转换可放在类类型转换之前

使用构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配。

    void calc(SmallInt);
    short sobj;
    /*
    *调用 SmallInt类中定义的构造函数(SmallInt(int)),
    *将 sobj 转换为 SmallInt 类型
    */
    calc(sobj);

如果需要,在调用构造函数执行类类型转换之前,可将一个标准转换序列应用于实参。为了调用函数calc(),应用标准转换将dobj从 double类型转换为int类型,然后调用构造函数SmallInt(int)将转换结果转换为SmallInt类型。

    void calc(SmallInt);
    double dval;
    calc(dval);
    //作用等同于
    //calc(static_cast<int>(dval));
//P457 习题14.40
class Sales_item
{
public:
    Sales_item(const std::string &book = ""):
        isbn(book), units_sold(0), revenue(0.0) {}

    /**
    *其实定义string和double的转换操作符并不是一个好办法
    *因为一般不必在需要string和double的地方使用Sales_item对象
    */

    operator string () const
    {
        return isbn;
    }
    operator double () const
    {
        return revenue;
    }
//As before...

private:
    std::string isbn;
    unsigned units_sold;
    double revenue;

};

//习题14.42
class CheckoutRecord
{
public:
    typedef unsigned Date;

    operator bool () const
    {
        return wait_list.empty();
    }
    //As Before...

private:
    //As Before...
    vector< pair<string,string> * > wait_list;
};

三、实参匹配和转换

虽然类类型转换可能是实现和使用类的一个好处,但类类型转换也可能是编译时错误的一大来源!当从一个类型转换到另一个类型有多种方式时,如果有几个类类型转换可以使用,编译器必须决定对给定表达式使用哪一个

【小心地雷】

如果小心使用,类类型转换可以大大简化类代码和用户代码。如果使用得太过自由随意,类类型转换会产生令人迷惑的编译时错误,这些错误难以理解而且难以避免!

1、实参匹配和多个转换操作符

class SmallInt
{
public:
    //从int/double转换到SmallInt
    SmallInt(int = 0);
    SmallInt(double);

    //从SmallInt转换到int/double
    operator int() const
    {
        return val;
    }
    operator double () const
    {
        return val;
    }

private:
    std::size_t val;
};

【小心地雷】

一般而言,给出一个类两个内置类型之间的转换是不好的做法!

考虑最简单的调用非重载函数的情况:

    void compute(int);
    void fp_compute(double);
    void extended_compute(long double);

    SmallInt si;
    compute(si);            //OK
    fp_compute(si);         //OK
    extended_compute(si);   //Error

在这段程序中,任一转换操作符都可用于compute调用中:

1)operatorint 产生对形参类型的完全匹配。

2)首先调用operatordouble 进行转换,后跟从double到 int的标准转换与形参类型匹配。

由于完全匹配转换比需要标准转换的其他转换更好,因此,第一个转换序列更好,选择转换函数SmallInt::operatorint()来转换实参。

在第二个调用中,可用任一转换调用fp_compute。但是,到double的转换是一个完全匹配,不需要额外的标准转换。

最后一个对extended_compute的调用有二义性。可以使用任一转换函数,但每个都必须跟上一个标准转换来获得longdouble,因此,没有一个转换比其他的更好,调用具有二义性

【小结】

如果两个转换操作符都可用在一个调用中,而且在转换函数之后存在标准转换,则根据该标准转换的类别选择最佳匹配!

2、实参匹配和构造函数转换

正如可能存在两个转换操作符,也可能存在两个构造函数可以用来将一个值转换为目标类型。

    void manip(const SmallInt &);

    double d;
    int i;
    long l;
    manip(d);   //OK
    manip(i);   //OK
    manip(l);   //Error

在第一个调用中,可以用任一构造函数将d转换为 SmallInt类型的值:int构造函数需要对d的标准转换,double构造函数完全匹配。因为完全匹配比标准转换更好,所以用构造函数SmallInt(double)进行转换。

在第二个调用中,构造函数SmallInt(int)提供完全匹配,调用接受一个double参数的 SmallInt构造函数需要首先将i转换为 double类型。对于这个调用,则编译器更喜欢使用int构造函数转换实参。

第三个调用具有二义性。没有构造函数完全匹配于long。使用每一个构造函数之前都需要对实参进行转换:

1)标准转换(从long到 double)后跟SmallInt(double)。

2)标准转换(从long到 int)后跟SmallInt(int)。

这些转换序列是不能区别的,所以该调用具有二义性。

【小结】

当两个构造函数定义的转换都可以使用时,如果存在构造函数实参所需的标准转换,就用标准转换的类型选择最佳匹配

3、当两个类定义了转换时的二义性

当两个类定义了相互转换时,很可能存在二义性:

class Integral;
class SmallInt
{
public:
    SmallInt(Integral);
    //...
};

class Integral
{
public:
    operator SmallInt() const;
    //...
};

    void compute(SmallInt);

    Integral int_val;
    /**
    *Error:调用有二义性
    *但是有些编译器还是检测不出来的,比如我所测试的g++编译器
    */
    compute(int_val);

实参int_val可以用两种不同的方式转换成为SmallInt对象,编译器可以接受SmallInt对象的构造函数(此处中文版翻译有误,原文为:...Thecompiler could use the SmallInt constructor that takes and Integralobject or it could use the Integral conversion operation thatconverts an Integral to aSmallInt...,所以应该翻译为SmallInt对象的构造函数,而不是Integral对象的构造函数),也可以使用Integral对象转换为SmallInt对象的Integral转换操作。因为这两个函数没有高下之分,所以这个调用会出错

在这种情况下,不能用显式类型转换来解决二义性—— 显式类型转换本身既可以使用转换操作又可以使用构造函数,相反,需要显式调用转换操作符或构造函数:

    compute(SmallInt(int_val)); //OK
    compute(int_val.operator SmallInt());   //OK

而且,由于某些似乎微不足道的原因,我们认为可能有二义性的转换是合法的。例如,SmallInt类构造函数复制它的Integral实参,如果改变构造函数以接受constIntegral 引用:

class SmallInt
{
public:
    SmallInt(const Integral &);
    //...
};

则对 compute(int_val)的调用不再有二义性!原因在于使用SmallInt构造函数需要将一个引用绑定到int_val,而使用Integral类的转换操作符可以避免这个额外的步骤。这一小小区别足以使我们倾向于使用转换操作符。

【最佳实践】

避免二义性最好的方法是避免编写互相提供隐式转换成对的类

【警告:避免转换函数的过度使用 P460非常精彩的讲述!】

避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型。做到这点,最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。

C++ Primer 学习笔记_63_重载操作符与转换 --转换与类类型【上】

时间: 2024-10-08 04:15:43

C++ Primer 学习笔记_63_重载操作符与转换 --转换与类类型【上】的相关文章

C++ Primer 学习笔记_64_重载操作符与转换 --转换与类类型【下】

重载操作符与转换 --转换与类类型[下] 四.重载确定和类的实参 在需要转换函数的实参时,编译器自动应用类的转换操作符或构造函数.因此,应该在函数确定期间考虑类转换操作符.函数重载确定由三步组成: 1)确定候选函数集合:这些是与被调用函数同名的函数. 2)选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数.选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参. 3)选择最佳匹配的函数.为了确定最佳匹配,对将实参转换为对应形参所需的类型转换进行分类.对于

C++ Primer 学习笔记_58_重载操作符与转换 --重载操作符的定义

重载操作符与转换 --重载操作符的定义 引言: 明智地使用操作符重载可以使类类型的使用像内置类型一样直观! 重载操作符的定义 重载操作符是具有特殊名称的函数:保留字operator后接定义的操作符符号.如: Sales_item operator+(const Sales_item &,const Sales_item &); 除了函数调用操作符之外,重载操作符的形参数目(包括成员函数的隐式this指针)与操作符的操作数数目相同.函数调用操作符可以接受任意数目的操作数. 1.重载的操作符名

C++ Primer 学习笔记_59_重载操作符与转换 --输入/输出、算术/关系操作符

重载操作符与转换 --输入/输出.算术/关系操作符 支持I/O操作的类所提供的I/O操作接口,一般应该与标准库iostream为内置类型定义的接口相同,因此,许多类都需要重载输入和输出操作符. 一.输出操作符<<的重载 为了与IO标准库一致,操作符应接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回ostream形参的引用! ostream &operator<<(ostream &os,const ClassType &

C++ Primer 学习笔记_60_重载操作符与转换 --赋值、下标、成员訪问操作符

重载操作符与转换 --赋值.下标.成员訪问操作符 一.赋值操作符 类赋值操作符接受类类型形參,通常该形參是对类类型的const引用,但也能够是类类型或对类类型的非const引用.假设未定义这个操作符,则编译器将合成它.类赋值操作符必须是类的成员,以便编译器能够知道是否须要合成一个.并且还能够为一个类定义很多附加的赋值操作符,这些赋值操作符会由于右操作数的不同而构成重载!如string类型: string car("Volks"); car = "Studebaker"

C++ Primer 学习笔记_61_重载操作符与转换 --自增/自减操作符

重载操作符与转换 --自增/自减操作符 引言: 自增,自减操作符常常由诸如迭代器这种类实现,这种类提供相似于指针的行为来訪问序列中的元素.比如,能够定义一个类,该类指向一个数组并为该数组中的元素提供訪问检查: class CheckedPtr { public: //这个类没有默认构造函数,必须提供指向数组的指针. /**构造函数的參数是两个指针:一个指向数组的開始,还有一个指向数组的末端. *构造函数用这两个指针初始化 beg 和 end *并将 curr 初始化为指向第一个元素 */ Che

C++ Primer 学习笔记_62_重载操作符与转换 --调用操作符和函数对象

重载操作符与转换 --调用操作符和函数对象 引言: 能够为类类型的对象重载函数调用操作符:一般为表示操作的类重载调用操作符! struct absInt { int operator() (int val) { return val > 0 ? val : -val; } }; 通过为类类型的对象提供一个实參表而使用调用操作符,所用的方式看起来系那个一个函数调用: absInt absObj; int i = -1; cout << absObj(i) << endl; 虽然

C++ Primer 学习笔记_63_重载运算符和转换 --转换和类类型【上】

重载运算符和转换 --转换与类类型[上] 引言: 在前面我们提到过:能够用一个实參调用的非explicit构造函数定义一个隐式转换.当提供了实參类型的对象须要一个类类型的对象时.编译器将使用该转换. 于是:这样的构造函数定义了到类类型的转换. 除了定义到类类型的转换之外,还能够定义从类类型到其它类型的转换.即:我们能够定义转换操作符,给定类类型的对象.该操作符将产生其它类型的对象.和其它转换一样,编译器将自己主动应用这个转换. 一.转换为什么实用? 定义一个SmallInt的类,该类实现安全小整

C++ Primer 学习笔记_83_模板与泛型编程 --一个泛型句柄类

模板与泛型编程 --一个泛型句柄类 引言: [小心地雷] 这个例子体现了C++相当复杂的语言应用,理解它需要很好地理解继承和模板.在熟悉了这些特性之后再研究这个例子也许会帮助.另一方面,这个例子还能很好地测试你对这些特性的理解程度. 前面示例的Sales_item和Query两个类的使用计数的实现是相同的.这类问题非常适合于泛型编程:可以定义类模板管理指针和进行使用计数.原本不相关的Sales_item类型和 Query类型,可通过使用该模板进行公共的使用计数工作而得以简化.至于是公开还是隐藏下

C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样: Bulk_item bulk; cout << bulk.book() << endl; 名字book的使用将这样确定[先派生->后基类]: 1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名