C++中参数需要类型转换,请不要用成员函数

简介: C++中参数需要类型转换,请不要用成员函数

1.问题的引入


C++支持隐式类型转换,但通常情况下是不好的。然而,本这条规定也有例外。最常见的例外情况发生在建立数值类型时,假设你开始设计如下有理数类Rational:


class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);   // 注意:此处的构造函数为隐式的,因为没有使用explicit关键字修饰
    int numerator() const;
    int denominator() const;
private:
    // ...
};

你想支持加减乘除等算术运算,但你不确定是否该有成员函数、non-member函数、non-member non-friend函数来实现它们。直觉告诉我们,当我你犹豫不决时,请从面向对象的角度去思考。因为有理数相乘与Rational类有关,因此很自然地想应该在Rational类的内部为有理数实现重载*运算符函数opeator*()。下面,先考虑将operator*()写成Rational成员函数的写法:


class Rational {
public:
    // ...
    // 成员函数
    const Rational operator* (const Rational& rhs) const;   // 注意:成员函数的形参中有一个默认参数this,指向的是调用该成员函数的对象!
private:
    // ...
};

假设你不明白为啥上面的成员函数被声明为此种形式,即它返回一个const by-value结果,但接收的参数是一个reference-to-const实参,请先学习前面的文章尽量以const、enum、inline替换#defineC++中多用引用传递方式替换值传递方式C++中不要随便返回对象的引用。上面的成员函数能够让你将两个有理数以最普通的方式进行相乘。如下所示:


Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;

但是,你以为这样就结束啦?不不不,你还希望支持混合运算即有理数与int类型之间的相乘。但是,当你尝试混合运算的时候,不幸发生了,你发现只有一半行得通。如下所示:


Rational oneHalf(1, 2);
Rational result = oneHalf * 2;  // 正确
result = 2 * oneHalf;  // 错误


出问题了哦,乘法不是应该满足交换律嘛。如果你对上面的代码段进行重写,如下所示:


Rational oneHalf(1, 2);
Rational result = oneHalf.operator*(2);
result = 2.operator*(oneHalf);

2.分析问题,追根溯源


正如你所思考的那样,oneHalf是一个内含operator*成员函数的类的对象,所以编译器调用该函数。但是,整数2并没有相应的类,也就没有operator*成员函数。编译器也会试图寻找(即在命名空间或global作用域内查找)可以被以下调这样调用的non-member函数operator*


result = operator*(2, oneHalf);  // 错误

但是,本例中并不存在这样接收int和Rational作为参数的non-member函数operator*,因此查找失败再回过头看看先前成功的那个调用,注意它的第二个参数是int类型2,但是Rational::operator*需要的实参可是Rational对象哦。这是咋回事呢?为啥2在这里就可以接收,而在另一个调用中就不可以呢?


原因:因为这里发生了所谓的隐式类型转换。编译器知道你正在传递一个int,而函数需要的却是Rational,但它也知道只要调用Rational构造函数并赋予你所提供的int,就可以变出一个适当的Rational,于是编译器就那样做了。换句话来说,调用动作在编译器眼中有点像下面的代码:


const Rational temp(2);   // 根据整型2建立创建一个临时的 Rational对象
result = oneHalf * temp;  // 等价于oneHalf.operator*(temp)

当然,只因为构造函数是隐式的,即non-explicit构造函数,编译器才会去这样做。如果Rational的构造函数是explicit,下面的代码没有一个是正确的。


result = oneHalf * 2;  // 错误! (在explicit显式构造函数的情况下,无法将2转换为Rational)
result = 2 * oneHalf;  // 仍然错误!


如果你就是希望能希望支持有理数的混合算术运算,即希望有方法将以上的两个语句都可以通过编译。现在,我们继续探究为什么即使Rational构造函数不是显示explicit的,仍然只有一个可以通过编译呢?


result = oneHalf * 2;  // 正确! (在non-explicit构造函数的情况下,可以将2转换为Rational)
result = 2 * oneHalf;  // 错误!

根本原因:只有当参数出现在构造函数的初始化列表中,这个参数才是隐式类型转换的合格参与者。这是因为成员函数本身就自带了一个隐藏参数this指针,它指向调用成员函数的那个对象。因此,第一次调用可以通过,第二次调用是失败的。因为第一次调用伴随一个放在构造函数初始化列表中的参数,第二次调用则不是。



3.柳暗花明又一村


最终解决方法让operator*成为一个non-member函数,允许编译器在每个实参身上执行隐式类型转换如下所示:


class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);   // 注意,此处的构造函数为隐式的,因为没有使用explicit关键字修饰
    int numerator() const;
    int denomiator() const;
private:
    // ...
};
// non-member函数
const Rational operator* (const Rational& lhs, const Rational& rhs){
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denomiator() * rhs.denomiator());
}
int main(){
    Rational oneFourth(1, 4);
    Rational result;
    result = oneFourth * 2;
    // result = 2 * oneFourth; 也通过了编译!
    return 0;
}

此外,还有一点需要考虑:operator*是否应该成为Rational类的友元函数呢?答案是否定的。因为operator*可以完全通过Rational的public接口完成任务,上面的代码段就是这样做的。因此,可以得到一个结论:成员函数的对立面是non-member函数,而不是友元函数。现实中太多的程序员假设,如果一个与某类相关的函数不是一个它的成员函数,就应该是该类的友元函数。这实际上是错误的观点,上面的代码段就可以证明这个观点太过牵强。记住:无论何时如果你可以避免友元函数就去避免它,因为不像现实生活中“多个朋友多条路”。在C++程序中,友元函数带来的麻烦往往多于其产生的价值。


4.总结


(1) 如果你需要为某个函数的所有参数(包括this指针所指向的那个隐藏参数)进行类型转换,那么这个函数必须是non-member。

相关文章
|
22天前
|
编译器 C++ 容器
【C++】String常见函数用法
【C++】String常见函数用法
14 1
|
30天前
|
C++
c++常见函数及技巧
C++编程中的一些常见函数和技巧,包括生成随机数的方法、制表技巧、获取数字的个位、十位、百位数的方法、字符串命名技巧、避免代码修改错误的技巧、暂停和等待用户信号的技巧、清屏命令、以及避免编译错误和逻辑错误的建议。
18 6
|
5天前
|
程序员 C++ 容器
C++编程基础:命名空间、输入输出与默认参数
命名空间、输入输出和函数默认参数是C++编程中的基础概念。合理地使用这些特性能够使代码更加清晰、模块化和易于管理。理解并掌握这些基础知识,对于每一个C++程序员来说都是非常重要的。通过上述介绍和示例,希望能够帮助你更好地理解和运用这些C++的基础特性。
19 0
|
1月前
|
编译器 C++ 索引
C++虚拟成员-虚函数
C++虚拟成员-虚函数
|
1月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
30天前
|
存储 C++
c++学习笔记05 函数
C++函数使用的详细学习笔记05,包括函数的基本格式、值传递、函数声明、以及如何在不同文件中组织函数代码的示例和技巧。
27 0
c++学习笔记05 函数
|
29天前
|
存储 C++
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
30 0
|
1月前
|
Dart 编译器 API
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
|
1天前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
41 30
|
15天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)