分清成员函数非成员函数和友元函数

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

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

class rational {
public:

  rational(int numerator = 0, int denominator = 1);

  int numerator() const;

  int denominator() const;

private:

  ...

};

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

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

class rational {
public:
  ...
  const rational operator*(const rational& rhs) const;
};

(如果你不明白为什么这个函数以这种方式声明——返回一个const值而取一个const的引用作为它的参数——尽可能使用const

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

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,所以才有上面成功的调用。换句话说,编译器处理这个调用时的情形类似下面这样:

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指针所对应的对象)进行转换。这就是为什么这个语句可以工作:

C++

result = onehalf.operator*(2);      // converts int -> rational

而这个语句不行:

result = 2.operator*(onehalf);      // 不会转换

                                    // int -> rational

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

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

class rational {

  ...                               // contains no operator*

};
// 在全局或某一名字空间声明,
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的成员函数。

相关文章:

  1. 提高对C++的认识

    C++中有很多 "东西":C,重载,面向对象,模板,例外,名字空间。这么多东西,有时让人感到不知所措。怎么弄懂所有这些东西呢? C++之所以发展到现在这个样子,在于它有自己的设计目标。理解了这些设计目标,就不难弄懂所有这些东西了。C++最首要的目标在于:...

  2. 尽量用const和inline而不用#define

    这个条款最好称为:“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。这是问题之一。再看下面的语句: #define ASPECT_RATIO 1.653...

  3. 写operator new和operator delete时要遵循常规

    但事情也不是那么简单。因为operator new实际上会不只一次地尝试着去分配内存,它要在每次失败后调用出错处理函数,还期望出错处理函数能想办法释放别处的内存。只有在指向出错处理函数的指针为空的情况下,operator new才抛出异常。...

  4. 避免隐藏标准形式的new

    因为内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部 和全局声明的两个相同名字的函数f来说,类的成员函数会隐藏掉全局函数:...

  5. 在operator=中对所有数据成员赋值

    说明了如果没写赋值运算符的话,编译器就会为你生成一个,条款11则说明了为什么你会经常不喜欢编译器为你生成的这个赋值运算符,所以你会想能否有个两全其美的办法,让编译器生成一个缺省的赋值运算符,然后可以有选择地重写不喜欢的部分。这是不可能的!只要想对赋值过程的某一个部分进行控制,就必须负责做赋值过程中所有的事。...

  6. 在operator=中检查给自己赋值的情况

    如果b是a的另一个名字(例如,已被初始化为a的引用),那这也是对自己赋值, 虽然表面上看起来不象。这是别名的一个例子:同一个对象有两个以上的名字。在 本条款的最后将会看到,别名可以以大量任意形式的伪装出现,所以在写函数时一 定要时时考虑到它。...

  7. 避免对指针和数字类型重载

    答案是,0是一个int——准确地说,一个字面上的整数常量——所以,“总是”f(int)被调用。这就是问题所在:因为不是所有的人总是希望它这样执行。这是c++世界中特有的一种情况:当人们认为某个调用应该具有多义性时,编译器却不这么干。...

  8. 明智地使用内联

    明智地使用内联,也就是在函数体并不大的时候选择用内联,如是函数体很大选 用内联,并不会变快,有可能会更慢...

  9. 区分继承和模板

    · 作为一位爱猫的宠物迷,你想设计一个类来表示猫。这也将需要多个不同的类,因为每个品种的猫都会有点不同。和所有对象一样,猫可以被创建和销毁,但,正如所有猫迷所知道的,猫所做的其它事不外乎吃和睡。然而,每一种猫吃和睡都有各自惹人喜爱的方式。...

  10. 熟悉标准库好的方法

    C++标准库很大。非常大。难以置信的大。怎么个大法?这么说吧:在C++标准中,关于标准库的规格说明占了密密麻麻300多页,这还不包括标准C库,后者只是 "作为参考"(老实说,原文就是用的这个词)包含在C++库中。...

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值