条款5:对定制的“类型转换函数”保持警觉

版权声明:版权归作者所有,转发请标明 https://blog.csdn.net/weixin_40087851/article/details/82587646

不同类型之间的隐式转换(基础类型)

  • C++允许编译器在不同类型之间执行隐式转换。继承了C的伟大传统,允许默默地将char转换为int,将short转换为double(上转型)。这便是为什么你可以将一个short交给一个“期望获得double”的函数而仍能成功的原因。C++还存在更令人害怕的转型(遗失信息),包括将int转换为short,以及将double转换为char(下转型)。

  • 基础类型的转换是语言提供的,我们对这类转型无能为力。

函数实现隐式转换

  • 两种函数允许编译器执行这样的转换:单自变量构造函数(类中只含有一个数据成员)和隐式类型转换操作符

  • 单自变量构造函数是指能够以单一变量成功调用的构造函数。如此的构造函数可能声明拥有单一参数,也可能声明拥有多个参数,并且除了第一个参数之外都有默认值。

#include<iostream>
#include<string>

using namespace std;

class Name {
public:
    Name(const string& s):str(s){}
    string getStr() const{
        return str;
    }
private:
    string str;
};

class Rational {
public:
    Rational(int numerator = 0, int denominator = 2) {
        rat = static_cast<double>(numerator) / denominator;
    }
    double getRat() const{
        return rat;
    }
private:
    double rat;
};

int main(int arrgc, char* argv[]) {
    Name name = "can't!!!";            //隐式转换
    cout << name.getStr() << endl;     //can't!!!
    Rational rational = 1;             //隐式转换
    cout << rational.getRat() << endl; //0.5
    getchar();
    return 0;
}
  • 隐式类型转换操作符,是一个拥有奇怪名称的成员函数:关键字operator之后加上一个类型名称。你不能为此函数指定返回值类型,因为其返回值类型基本上已经表现于函数名称上。例如,为了让Rational对象能够被隐式转换为double,定义Rational类如下:
#include<iostream>
#include<string>

using namespace std;

class Rational {
public:
    Rational(int numerator = 0, int denominator = 2) {
        rat = static_cast<double>(numerator) / denominator;
    }
    double getRat() const{
        return rat;
    }
    operator double()const {   //隐式类型转换操作符
        return rat;
    }
private:
    double rat;
};

int main(int arrgc, char* argv[]) {
    Rational rational = 1;             //隐式转换
    cout << rational.getRat() << endl; //0.5

    Rational rational2(1, 2);
    double d = 0.5*rational2;          //隐式类型转换操作符
    cout << d << endl;                 //0.25
    getchar();
    return 0;
}

为什么最好不要提供任何类型转换函数

  • 在你从未打算也未预期的情况下,此类函数可能会被调用,而其结果可能是不正确、不直观的程序行为,很难调试。

处理隐式类型转换操作符

  • 假设你有一个类用来表示分数。你希望像内建类型一样地输出Rational对象内容,即:
    Rational r(1, 2);
    cout << r;
  • 假设你忘记了为Rational写一个operator<<,那么你或许会认为上述打印不会成功,因为没有适当的operator<<可以调用。但是你错了,你的编译器面对上述动作,发现不存在任何operator<<可以接受一个Rational,但它会想尽各种办法(包括找出一系列可接受的隐式类型转换)让函数调用动作成功,“可被接受的转换程序”定义十分复杂,但本例中你的编译器发现,只要调用Rational::operator double,将r隐式转换为double,调用动作便能成功于是上述代码将r以浮点数而非分数的形式输出。。这虽然不至于造成灾难,却显示了隐式类型转换操作符的缺点:它们的出现可能导致错误(非预期)的函数被调用。

  • 方法一:重载“<<”运算符:

#include<iostream>
#include<string>

using namespace std;

class Rational {
public:
    Rational(int numerator = 0, int denominator = 2) {
        rat = static_cast<double>(numerator) / denominator;
    }
    double getRat() const{
        return rat;
    }
    //友元函数
    friend ostream& operator<< (ostream& cout, const Rational& rational);
private:
    double rat;
};

ostream& operator<< (ostream& cout, const Rational& rational) {
    cout << rational.rat;
    return cout;
}

int main(int arrgc, char* argv[]) {
    Rational r(1, 2);
    cout << r << endl;               //正确
    return 0;
}
  • 方法二:以功能对等的另一个函数取代类型转换操作符。
#include<iostream>
#include<string>

using namespace std;

class Rational {
public:
    Rational(int numerator = 0, int denominator = 2) {
        rat = static_cast<double>(numerator) / denominator;
    }
    double getRat() const{
        return rat;
    }
    /*
    operator double()const {   //隐式类型转换操作符
        return rat;
    }
    */
    double asDouble() const {
        return rat;
    }
private:
    double rat;
};

int main(int arrgc, char* argv[]) {
    Rational rational = 1;             //隐式转换
    cout << rational.getRat() << endl; //0.5

    Rational rational2(1, 2);
    double d = 0.5*rational2.asDouble();         //隐式类型转换操作符
    cout << d << endl;

    Rational r(1, 2);
    //cout << r << endl;               //错误:没有与这些操作符匹配的“<<”运算符
    cout << r.asDouble() << endl;      //正确:函数调用转换为double
    getchar();
    return 0;
}

处理单自变量构造函数的隐式转换

  • 通过单自变量构造函数完成的隐式转换,较难消除。此外,这些函数造成的问题在许多方面比隐式类型转换操作符的情况更不好对付。

  • 举个例子,考虑一个针对数组结构而写的class template。这些数组允许用户指定索引的上限和下限:

template<class T>
class Array {
public:
    Array(int lowBound, int highBound);
    Array(int size);

    T& operator[] (int index);
    ...
};
  • 第二个构造函数允许用户只指定数组的元素个数,便可定义出Array对象。它可以被用来作为一个类型转换函数,结果导致无尽苦恼。

  • 例如,考虑一个用来对Array对象进行比较动作的函数,以及一小段代码:

bool operator==(const Array<int>& lhs, const Array<int>& rhs);

Array<int> a(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i) {
    if (a == b[i]) {  //此处应为a[i]
        ...
    }
    else {
        ...
    }
}
  • 我们试图将a的每一个元素拿来和b的对应元素相比,但当我们键入a时却意外地遗漏了下标语法。我们希望我们的编译器发挥挑错功能,将它挑出来,但它却一声也不吭。因为他看到的是一个operator==函数调用,夹带着类型为Array的自变量a和类型int的自变量b[i],虽然没有这样的operator==函数可被调用,我们的编译器却会注意到,只要调用Array构造函数,它就可以将int转为Arrry对象。于是会产生类似这样的代码:
for (int i = 0; i < 10; ++i) {
    if (a == static_cast< Array<int> >b[i]) {  //重点!!!(生成b[i]大小的数组)
        ...
    }
    else {
        ...
    }
}
  • 于是,循环的每一次迭代都拿a的内容和一个大小为b[i]的临时数组(其内容想必未定义)做比较。此种行为不仅不令人满意,而且非常没有效率。因为每次走过这个循环,我们必须产生和销毁一个临时的Array对象

  • 只要不声明隐式类型转换操作符,便可将它所带来的害处避免。但是单自变量构造函数却没有那么容易去除,毕竟你可能真的需要提供一个单自变量构造函数给你的客户使用。此时有两种做法可以消除。

  • 简易法是最新使用的C++特性:关键字explicit

  • 这个特性之所以被导入,就是为了解决隐私类型转换带来的问题。用法非常简单,只要将构造函数声明为explicit,编译器便不能因隐式类型转换的需要而调用它们。

Array<int> a(10);
Array<int> b(10);

if (a == b[i]) {  //错误:不能隐式转换
    ...
}

if (a == Array<int>(b[i])) {  //没问题,但用意是什么?
    ...
}

if (a == static_cast< Array<int> >(b[i])) {  //此处注意空格!!!没问题,但用意是什么?
    ...
}

if (a == (Array<int>)b[i]) {  //没问题,但用意是什么?
    ...
}
  • 上述使用static_cast的那一行中,两个“>”字符之间的空格是必要的。如果那行写成这样:
if (a == static_cast<Array<int>>(b[i])) {  //此处注意空格!!!没问题,但用意是什么?
    ...
}
  • 它就有了不同的意义,因为C++编译器将“>>”视为单一的词元。如果没有加空格,将出现语法错误。

  • 另一种方法:在没有关键字explicit时,我们在类内产生一个新类,完成构造

  • 以上例为例,产生一个新类,名为ArraySize。此型对象只有一个母的:用来表现即将被产生的数组的大小。

template<class T>
class Array {
public:
    class ArraySize {
    public:
        ArraySize(int size):theSize(size){}
        int getSize() const {
            return theSize;
        }
    private:
        int theSize;
    };
    Array(int lowBound, int highBound);
    explicit Array(ArraySize size);  //可以进行一次隐式转换
    ...
};
  • 构造函数可以进行一次隐式转换。但考虑下列代码:
bool operator==(const Array<int>& lhs, const Array<int>& rhs);

Array<int> a(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i) {
    if (a == b[i]) {  //此处应为a[i]
        ...
    }
    else {
        ...
    }
}
  • 编译不能考虑将int转换为一个临时性的ArraySize对象,然后再根据这个临时对象产生必要的Array对象,因为那将调用两个用户制转换行为,一个将int转换为ArraySize,另一个将ArraySize转换为Array。如此的转换程序是禁止的,所以编译器对以上代码发出错误消息。

猜你喜欢

转载自blog.csdn.net/weixin_40087851/article/details/82587646