C++中成员函数,非成员函数和友元函数

转自:http://blog.chinaunix.net/uid-10673338-id-2936852.html

转自:http://www.cnblogs.com/carsonzhu/p/5024585.html

 

对以上两篇文章,我添加了自己已有的部分知识,并重新地汇总整理

转载请注明:http://blog.csdn.Net/lee_shuai

 

从函数定义的位置来粗略理解:

成员函数:函数声明在类的定义中,函数定义在类的实现文件中

非成员函数:函数声明在类的定义下面,函数定义时的函数头前面不能添加类对象名称作为作用域限定。


一 成员函数


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

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

class rational {

public:

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

  int numerator() const;

  int denominator() const;

private:

  ...

};

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


1.1 添加成员函数


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

class rational {

public:

  ...

  const rationaloperator*(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在一个地方可以工作而另一个地方不行?


1.2 隐式类型转换


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

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

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


1.3 在声明explicit的情况下


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

class rational {

public:

  explicit rational(intnumerator = 0, int denominator = 1);//此构造函数为explicit

  ...

  const rationaloperator*(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*成为一个非成员函数,从而允许编译器对所有的参数执行隐式类型转换。

这段解释也可以在《C++类和数据结构》重载运算符一节中找到类似的注明:当左操作数不是对象,而右操作数是对象时,最好将函数定义直接放在结构定义(类定义)的下方。

class rational {

  ...                               // contains nooperator*

};

// 在全局或某一名字空间声明,

// 参见条款m20了解为什么要这么做

const rational operator*(const rational& lhs, constrational& rhs)

{

  returnrational(lhs.numerator() * rhs.numerator(),

                 lhs.denominator() * rhs.denominator());

}

rational onefourth(1, 4);

rational result;

result = onefourth * 2;          // 工作良好

result = 2 * onefourth;          // 万岁, 它也工作了!


2.1 类中的重载运算符函数


在为类编写重载运算符函数时,也采用这种方式编写。唯一的区别就是函数原型位于类说明文件中,函数定义位于类实现文件中,像类的其他函数一样。当左操作数是类的对象时,函数原型位于类定义的公用部分。函数定义在类实现文件中的编写方式与类的其它函数定义的编写方式相似。

当左操作数不是类的对象,但右操作数是类的对象时,函数原型则直接位于类定义的下面。函数定义位于类实现文件中,但是函数头中不能使用类名称。

例如,假设为Checkbook类编写一个重载运算符>=函数,它的左操作数是整型,那么我们需要将函数原型放在类定义下面,而且类实现文件(注意是文件)中的函数头要采用下述形式:

bool operater >= (int num , Checkbook cd)

而不是下述形式:

bool Checkbook::operater >= (int num , Checkbook cd)

因为表达式“Checkbook::”用来告诉编译器所定义的函数原型位于Checkbook类的定义中。


三 友元函数


这当然是一个完美的结局,但还有一个担心: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; //readfrom 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是和它相关的类:

1.       虚函数必须是成员函数。如果f必须是虚函数,就让它成为c的成员函数。

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

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

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


3.1 创建友元函数

1)       将函数原型(声明)放在类声明中,并在函数原型(声明)前加上关键字friend

2)       编写函数定义。因为它不是成员函数,所以不要使用类的作用域。另外,不要在定义中使用关键字friend。 

 

一个普通函数可以是多个类的友元函数。

一个类的成员函数也可以是另一个类的友元,从而可以使得一个类的成员函数可以操作另一个类的数据成员。

 

定义方法是在友元类中定义类的作用域,如在类B中定义A的成员函数test为类B友元函数: friend void A::test(int &temp)

友元函数作为多个类的友元函数例:

class Country;

class Internet {

 public:

  Internet(char *name,char *address)// 改为:internet(constchar *name , const char *address)  

  {

  strcpy(Internet::name,name);        

  strcpy(Internet::address,address); 

   }

friend void ShowN(Internet &obj,Country &cn);//注意这里

public:

  char name[20];   

   char address[20];

};

class Country {

public:

  Country()   

   {

        strcpy(cname,"中国");  

   }

  friend void ShowN(Internet&obj,Country &cn);//注意这里

protected:

  char cname[30];

}; 

void ShowN(Internet &obj,Country &cn) {

  cout<<cn.cname<<"|"<<obj.name<<endl;

 }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值