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。

相关文章
|
3月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
116 0
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
84 0
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
161 12
|
6月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
324 6
|
10月前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
322 4
|
10月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
261 5
|
11月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
3月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
164 0
|
6月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
124 16