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。

相关文章
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
1月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
48 6
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
25 0
C++ 多线程之线程管理函数
|
1月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
191 1
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
46 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
1月前
|
C++
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
21 0
|
2天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
14 2
|
14天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
46 4
|
15天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
43 4