读书笔记 effective c++ Item 24 如果函数的所有参数都需要类型转换,将其声明成非成员函数

简介: 1. 将需要隐式类型转换的函数声明为成员函数会出现问题 使类支持隐式转换是一个坏的想法。当然也有例外的情况,最常见的一个例子就是数值类型。举个例子,如果你设计一个表示有理数的类,允许从整型到有理数的隐式转换应该是合理的。

1. 将需要隐式类型转换的函数声明为成员函数会出现问题

使类支持隐式转换是一个坏的想法。当然也有例外的情况,最常见的一个例子就是数值类型。举个例子,如果你设计一个表示有理数的类,允许从整型到有理数的隐式转换应该是合理的。在C++内建类型中,从int转换到double也是再合理不过的了(比从double转换到int更加合理)。看下面的例子:

 1 class Rational {
 2 
 3 public:
 4 
 5 Rational(int numerator = 0, // ctor is deliberately not explicit;
 6 
 7 int denominator = 1); // allows implicit int-to-Rational
 8 
 9 // conversions
10 
11 int numerator() const; // accessors for numerator and
12 
13 int denominator() const; // denominator — see Item 22
14 
15 private:
16 
17 ...
18 
19 };

 

你想支持有理数的算术运算,比如加法,乘法等等,但是你不知道是通过成员函数还是非成员函数,或者非成员友元函数来实现。你的直觉会告诉你当你犹豫不决的时候,你应该使用面向对象的特性。有理数的乘积和有理数类相关,所有将有理数的operator*实现放在Rationl类中看上去是很自然的事。但违反直觉的是,Item 23已经论证过了将函数放在类中的方法有时候会违背面向对象法则,现在我们将其放到一边,研究一下将operator*实现为成员函数的做法:

1 class Rational {
2 
3 public:
4 
5 ...
6 
7 const Rational operator*(const Rational& rhs) const;
8 
9 };

 

(如果你不明白为什么函数声明成上面的样子——返回一个const value值,参数为const引用,参考Item 3,Item 20Item21

这个设计让你极为方便的执行有理数的乘法:

1 Rational oneEighth(1, 8);
2 
3 Rational oneHalf(1, 2);
4 
5 Rational result = oneHalf * oneEighth; // fine
6 
7 result = result * oneEighth; // fine

 

但是你不满足。你希望可以支持混合模式的操作,例如可以支持int类型和Rational类型之间的乘法。这种不同类型之间的乘法也是很自然的事情。

当你尝试这种混合模式的运算的时候,你会发现只有一半的操作是对的:

1 result = oneHalf * 2; // fine
2 
3 result = 2 * oneHalf; // error!

 

这就不太好了,乘法是支持交换律的。

2. 问题出在哪里?

将上面的例子用等价的函数形式写出来,你就会知道问题出在哪里:

1 result = oneHalf.operator*(2); // fine
2 
3 result = 2.operator*(oneHalf ); // error!

 

oneHalf对象是Rational类的一个实例,而Rational支持operator*操作,所以编译器能调用这个函数。然而,整型2却没有关联的类,也就没有operator*成员函数。编译器同时会去寻找非成员operator*函数(也就是命名空间或者全局范围内的函数):

1 result = operator*(2, oneHalf ); // error!

 

但是在这个例子中,没有带int和Rational类型参数的非成员函数,所以搜索会失败。

再看一眼调用成功的那个函数。你会发现第二个参数是整型2,但是Rational::operator*使用Rational对象作为参数。这里发生了什么?为什么都是2,一个可以另一个却不行?

没错,这里发生了隐式类型转换。编译器知道函数需要Rational类型,但你传递了int类型的实参,它们也同样知道通过调用Rational的构造函数,可以将你提供的int实参转换成一个Rational类型实参,这就是编译器所做的。它们的做法就像下面这样调用:

1 const Rational temp(2); // create a temporary
2 
3 // Rational object from 2
4 
5 result = oneHalf * temp; // same as oneHalf.operator*(temp);

 

当然,编译器能这么做仅仅因为类提供了non-explicit构造函数。如果Rational类的构造函数是explicit的,下面的两个句子都会出错:

1 result = oneHalf * 2; // error! (with explicit ctor);
2 
3 // can’t convert 2 to Rational
4 
5 result = 2 * oneHalf; // same error, same problem

 

这样就不能支持混合模式的运算了,但是至少两个句子的行为现在一致了。

然而你的目标是既能支持混合模式的运算又要满足一致性,也就是,你需要一个设计使得上面的两个句子都能通过编译。回到上面的例子,当Rational的构造函数是non-explicit的时候,为什么一个能编译通过另外一个不行呢?

看上去是这样的,只有参数列表中的参数才有资格进行隐式类型转换。而调用成员函数的隐式参数——this指针指向的那个——绝没有资格进行隐式类型转换。这就是为什么第一个调用成功而第二个调用失败的原因。

3. 解决方法是什么?

然而你仍然希望支持混合模式的算术运行,但是方法现在可能比较明了了:使operator*成为一个非成员函数,这样就允许编译器在所有的参数上面执行隐式类型转换了:

 1 class Rational {
 2 
 3 ... // contains no operator*
 4 
 5 };
 6 
 7 const Rational operator*(const Rational& lhs, // now a non-member
 8 
 9 const Rational& rhs) // function
10 
11 {
12 
13 return Rational(lhs.numerator() * rhs.numerator(),
14 
15 lhs.denominator() * rhs.denominator());
16 
17 }
18 
19 Rational oneFourth(1, 4);
20 
21 Rational result;
22 
23 result = oneFourth * 2; // fine
24 
25 result = 2 * oneFourth; // hooray, it works!

 

4. Operator*应该被实现为友元函数么?

故事有了一个完美的结局,但是还有一个挥之不去的担心。Operator*应该被实现为Rational类的友元么?

在这种情况下,答案是No。因为operator*可以完全依靠Rational的public接口来实现。上面的代码就是一种实现方式。我们能得到一个很重要的结论:成员函数的反义词是非成员函数而不是友元函数。太多的c++程序员认为一个类中的函数如果不是一个成员函数(举个例子,需要为所有参数做类型转换),那么他就应该是一个友元函数。上面的例子表明这样的推理是有缺陷的。尽量避免使用友元函数,就像生活中的例子,朋友带来的麻烦可能比从它们身上得到的帮助要多。

5. 其他问题

如果你从面向对象C++转换到template C++,将Rational实现成一个类模版,会有新的问题需要考虑,并且有新的方法来解决它们。这些问题,方法和设计参考Item 46。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
打赏
0
0
0
0
33
分享
相关文章
|
1月前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
119 6
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
187 5
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
225 6
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
43 12
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
52 16
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。

热门文章

最新文章