Effective C++ 2e Item19

原创 2001年07月10日 21:21:00

 

条款19: 分清成员函数,非成员函数和友元函数

成员函数和非成员函数最大的区别在于成员函数可以是虚拟的而非成员函数不行。所以,如果有个函数必须进行动态绑定(见条款38),就要采用虚拟函数,而虚拟函数必定是某个类的成员函数。关于这一点就这么简单。如果函数不必是虚拟的,情况就稍微复杂一点。

看下面表示有理数的一个类:

class Rational {
public:
  Rational(int numerator = 0, int denominator = 1);
  int numerator() const;
  int denominator() const;

private:
  ...
};

这是一个没有一点用处的类。(用条款18的术语来说,接口的确最小,但远不够完整。)所以,要对它增加加,减,乘等算术操作支持,但是,该用成员函数还是非成员函数,或者,非成员的友元函数来实现呢?

当拿不定主意的时候,用面向对象的方法来考虑!有理数的乘法是和Rational类相联系的,所以,写一个成员函数把这个操作包到类中。

class Rational {
public:

  ...

  const Rational operator*(const Rational& rhs) const;
};

(如果你不明白为什么这个函数以这种方式声明——返回一个const值而取一个const的引用作为它的参数——参考条款21-23。)

现在可以很容易地对有理数进行乘法操作:

Rational oneEighth(1, 8);
Rational oneHalf(1, 2);

Rational result = oneHalf * oneEighth;   // 运行良好

result = result * oneEighth;             // 运行良好

但不要满足,还要支持混合类型操作,比如,Rational要能和int相乘。但当写下下面的代码时,只有一半工作:

result = oneHalf * 2;      // 运行良好

result = 2 * oneHalf;      // 出错!

这是一个不好的苗头。记得吗?乘法要满足交换律。

如果用下面的等价函数形式重写上面的两个例子,问题的原因就很明显了:

result = oneHalf.operator*(2);      // 运行良好

result = 2.operator*(oneHalf);      // 出错!

对象oneHalf是一个包含operator*函数的类的实例,所以编译器调用了那个函数。而整数2没有相应的类,所以没有operator*成员函数。编译器还会去搜索一个可以象下面这样调用的非成员的operator*函数(即,在某个可见的名字空间里的operator*函数或全局的operator*函数):

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

但没有这样一个参数为int和Rational的非成员operator*函数,所以搜索失败。

再看看那个成功的调用。它的第二参数是整数2,然而Rational::operator*期望的参数却是Rational对象。怎么回事?为什么2在一个地方可以工作而另一个地方不行?

秘密在于隐式类型转换。编译器知道传的值是int而函数需要的是Rational,但它也同时知道调用Rational的构造函数将int转换成一个合适的Rational,所以才有上面成功的调用(见条款M19)。换句话说,编译器处理这个调用时的情形类似下面这样:

const Rational temp(2);      // 从2产生一个临时
                             // Rational对象

result = oneHalf * temp;     // 同oneHalf.operator*(temp);

当然,只有所涉及的构造函数没有声明为explicit的情况下才会这样,因为explicit构造函数不能用于隐式转换,这正是explicit的含义。如果Rational象下面这样定义:

class Rational {
public:
  explicit Rational(int numerator = 0,     // 此构造函数为
                    int denominator = 1);  // explicit
  ...

  const Rational operator*(const Rational& rhs) const;

  ...

};

那么,下面的语句都不能通过编译:

result = oneHalf * 2;             // 错误!
result = 2 * oneHalf;             // 错误!

这不会为混合运算提供支持,但至少两条语句的行为一致了。

然而,我们刚才研究的这个类是要设计成可以允许固定类型到Rational的隐式转换的——这就是为什么Rational的构造函数没有声明为explicit的原因。这样,编译器将执行必要的隐式转换使上面result的第一个赋值语句通过编译。实际上,如果需要的话,编译器会对每个函数的每个参数执行这种隐式类型转换。但它只对函数参数表中列出的参数进行转换,决不会对成员函数所在的对象(即,成员函数中的*this指针所对应的对象)进行转换。这就是为什么这个语句可以工作:

result = oneHalf.operator*(2);      // converts int -> Rational

而这个语句不行:

result = 2.operator*(oneHalf);      // 不会转换
                                    // int -> Rational

第一种情形操作的是列在函数声明中的一个参数,而第二种情形不是。

尽管如此,你可能还是想支持混合型的算术操作,而实现的方法现在应该清楚了:使operator*成为一个非成员函数,从而允许编译器对所有的参数执行隐式类型转换:

class Rational {

  ...                               // contains no operator*

};

// 在全局或某一名字空间声明,
// 参见条款M20了解为什么要这么做
const Rational operator*(const Rational& lhs,
                         const Rational& rhs)
{
  return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}

Rational oneFourth(1, 4);
Rational result;

result = oneFourth * 2;           // 工作良好
result = 2 * oneFourth;           // 万岁, 它也工作了!

这当然是一个完美的结局,但还有一个担心:operator*应该成为Rational类的友元吗?

这种情况下,答案是不必要。因为operator*可以完全通过类的公有(public)接口来实现。上面的代码就是这么做的。只要能避免使用友元函数就要避免,因为,和现实生活中差不多,友元(朋友)带来的麻烦往往比它(他/她)对你的帮助多。

然而,很多情况下,不是成员的函数从概念上说也可能是类接口的一部分,它们需要访问类的非公有成员的情况也不少。

让我们回头再来看看本书那个主要的例子,String类。如果想重载operator>>和operator<<来读写String对象,你会很快发现它们不能是成员函数。如果是成员函数的话,调用它们时就必须把String对象放在它们的左边:

// 一个不正确地将operator>>和
// operator<<作为成员函数的类
class String {
public:
  String(const char *value);

  ...

  istream& operator>>(istream& input);
  ostream& operator<<(ostream& output);

private:
  char *data;
};

String s;

s >> cin;                   // 合法, 但
                            // 有违常规

s << cout;                  // 同上

这会把别人弄糊涂。所以这些函数不能是成员函数。注意这种情况和前面的不同。这里的目标是自然的调用语法,前面关心的是隐式类型转换。

所以,如果来设计这些函数,就象这样:

istream& operator>>(istream& input, String& string)
{
  delete [] string.data;

  read from input into some memory, and make string.data
  point to it

  return input;
}

ostream& operator<<(ostream& output,
                    const String& string)
{
  return output << string.data;
}

注意上面两个函数都要访问String类的data成员,而这个成员是私有(private)的。但我们已经知道,这个函数一定要是非成员函数。这样,就别无选择了:需要访问非公有成员的非成员函数只能是类的友元函数。

本条款得出的结论如下。假设f是想正确声明的函数,C是和它相关的类:

·虚函数必须是成员函数。如果f必须是虚函数,就让它成为C的成员函数。

·operator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f成为非成员函数。如果f还需要访问C的非公有成员,让f成为C的友元函数。

·只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。如果f还需要访问C的非公有成员,让f成为C的友元函数。

·其它情况下都声明为成员函数。如果以上情况都不是,让f成为C的成员函数。

编写高质量的iOS代码--Effective Objective-C 2.0 读书笔记

编写高质量的iOS代码--Effective Objective-C 2.0 读书笔记 这本书年初刷完,感觉不错,介绍了很多小点,都是平日不怎么关注的. 第1章 熟悉Objective-C...
  • uxyheaven
  • uxyheaven
  • 2014年12月26日 23:56
  • 5027

读书笔记_Effective C++_习惯C++

这是一本非常经典C++书籍,也是我在工作中发现自己C++上还有很多薄弱点的时候经常拿来充电的。这本书内容很多,讲了很多如何高效地使用C++的方法,有些地方自己也没能啃透,读过一遍后很多知识点容易忘记,...
  • John_cdy
  • John_cdy
  • 2015年05月04日 09:51
  • 2154

《Effective C++》:条款44-条款45

条款44将与参数无关的代码抽离templates 条款45运用成员函数模板接受所有兼容类型...
  • KangRoger
  • KangRoger
  • 2015年03月12日 22:01
  • 1475

Item 19:把类的设计视作类型设计 Effective C++笔记

Item 19: Teat class design as type design. 在面向对象语言中,开发者的大部分时间都用在了增强你的类型系统。这意味着你不仅是类的设计者,更是类型设计者...
  • yangjvn
  • yangjvn
  • 2015年08月31日 18:20
  • 567

More Effective C++之Item M19:理解临时对象的来源

临时对象产生场景: 1.类型隐式转换; 2.函数值返回; 当程序员之间进行交谈时,他们经常把仅仅需要一小段时间的变量称为临时变量。例如在下面这段swap(交换)例程里: templat...
  • janeqi1987
  • janeqi1987
  • 2017年07月24日 11:16
  • 108

重读经典-《Effective C++》Item2:尽量以const,enum,inline替换#define

本博客(http://blog.csdn.net/livelylittlefish )贴出作者(三二一@小鱼)相关研究、学习内容所做的笔记,欢迎广大朋友指正!   1. 宏定义   ...
  • chenwenshi
  • chenwenshi
  • 2011年08月10日 09:54
  • 563

effective C++ Item 2: Prefer consts, enums, and inlines to #defines

前面一款基本上就是介绍性的内容,这一节进入主题。   条款二:尽量以const,enum,inline代替#define   1:使用const代替#define定义常量。     如果你定义圆...
  • manyouzhelilixi
  • manyouzhelilixi
  • 2011年08月22日 21:42
  • 265

Effective Modern C++ Item 2

如果你已经读过条款1关于模板类型的推导,你几乎已经知道所有关于auto类型推导的知识了,因为除了一种特殊情况,auto类型推导就是模板类型推导。但怎么会这样?模板类型推导涉及模板和函数还有参数,但au...
  • Love_3_9
  • Love_3_9
  • 2017年08月03日 19:55
  • 183

Effective C++----3rd Edition, Item 2:用consts,enums和inlines取代#define

Item 2: 用 consts, enums 和 inlines 取代 #defines 作者:Scott Meyers 译者:fatalerror99 (iTePub's Nirvana) ...
  • qianqin_2014
  • qianqin_2014
  • 2016年05月10日 21:25
  • 214

effective C++ (Item2) Prefer <iostream> to <stdio.h>

type safety and extensibility are cornerstones of the C++ way of life.  int i; Rational r; cin >>...
  • yqdm_zju
  • yqdm_zju
  • 2011年11月03日 23:52
  • 630
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Effective C++ 2e Item19
举报原因:
原因补充:

(最多只允许输入30个字)