CSDN博客

img zosatapo

理解C++面向对象编程[多态性部分]

发表于2002/2/28 15:16:00  1230人阅读

分类: C/C++

[面向对象编程]

初次写文章,错误一定百出不止。我只是希望我学习面向对象编程的一些理解看法
能对有关有共同爱好的人有一些小小用处,还谈不上帮助。

[文本涉及称谓说明]
1.变量:这个称谓包括两个部分。
内建类型实例(比如int,float,char等)
用户自定义类型实例(比如类(class),联合(union),结构(struct)等)

同时本文有时候也泛泛的称变量和函数为程序成员.
2.父类/子类: 相对于继承层次而言,被继承的为父类;继承的为子类.
3.对象: 本文指类类型的实例(an instance of a class)

************************************基础部分**************************
[内存对象模型概要]
因为本文主要是讨论多态性在语言层面上的表现,所以我这里不打算做太深入的
讨论,只是为了本文的阐释目的,做一些针对性的说明,概念性解释一下内存对象模型。

C++类类型的成员函数,我们可以把他们看成C语言中函数。它只是被编译器进行
一定的相关处理防止命名冲突而已。同时除了类中静态成员函数以外,这些函数
比普通的C语言函数增加了一个参数(this指针),因为可以处理对象中成员变量。
所以我们并没有在class的对象内存布局中看到任何与成员函数有关的东西。

当然要是类声明中或者类的继承层次中出现virtual函数的话,你会在内存布局中看到
有一个叫“虚拟函数指针表”的东东的存在。当我们不是使用单一继承的话,情况变得
更加复杂了,正如前面所讲的那样,本文主要在解释如何在语言使用层面上使用语言,
因此我不会在这里谈论更多关于内存布局的内容,需要了解的同行,请自行参阅有关
面向对象编程的书籍。

[程序成员生命周期概要]
存储空间:定义程序成员在程序中(或者翻译单元中)的存储形式.正如我们经常遇到的那样,
           有很多程序全局变量,程序局部变量等等概念.同时这个概念决定了变量的生命
           周期.这个概念主要指出了变量放在程序运行的哪个内存部分,比如说程序数据段,
           程序栈等等.
生命周期:程序成员在那个时间段是可以利用的,简单一点讲就是程序运行中有一段时间某个成员
          是可以引用的,我们就把这段时间称为这个成员的生命周期.
可见性:哪些界面(类,函数)可以引用程序成员
链接方式:概念是指一个翻译单元中声明定义成员是否可以被程序的其他的翻译单元使用.

************************************理论部分**************************
[重要的三个结论]
为了说明结论引入类:
class CBase
{
public:
  BaseFunc(){}
};
class CDerived :public CBase
{
public: 
    DeriFunc(){}
};

☆结论一☆.如果我们以一个[父类指针]pointer指向一个[子类对象],那么通过pointer我们
只能呼叫引用父类类型(指类定义)中所定义的函数.
我们这样写:
CBase* pBase;
虽然我们可以用指针pBase指向CDerived对象,但是由于
pBase是一个CBase类型指针,所以我们只能呼叫(或引用)
BaseFunc(),不能DeriFunc()。

☆结论二☆. 如果我们以一个[子类指针]指向一个[父类对象],我们引用函数前,必须通过RTTI来
确定指针指向对象的具体类型.也就是说要做显式的造型转换.这种做法应该是我们做
程序员深恶痛绝的做法.

CDerived* pDeri;
CDerived *pDeri;
CBase aBase("Jason");
pDeri = &aBase;

☆结论三☆. 如果父类和子类都定义了[相同名称的成员函数],我们这个时候通过指针呼叫
引用成员函数的决议,就必须根据指针类型(注意不是指针实际指向对象的类型)
来确定我们引用的是哪个函数.实际上这个结论就是函数屏蔽(mask)功能,这个结论很重要的,
因为这里不管是不是虚拟函数,统统管用的,所以这个结论杀伤力很大的,后面我重点
解释这个结论。


************************************解释部分**************************

☆结论一解释☆:

为什么是这样的,我们下面看一下一个例子来了解为什么语言设计者采用这样的策略呢?

#include <iostream.h>
 #include <stdio.h>

class ClassA
{
 public:
 int m_data1;
 void func1() { }
 void func2() { }
  virtual void vfunc1() { }
  virtual void vfunc2() { }
 };

 class ClassB : public ClassA
 {
 public:
  int m_data2;
  void func2() { }
 void func3(){}
  virtual void vfunc1() { }
 };

当我们写这样的一下函数;
看看这个函数实现方面有那些问题呢?

void change(ClassA& ca)
{
 ca.func1();
 ca.func2();
 //ca.func3();
 ca.vfunc1();
 ca.vfunc2();
}
当我们这样调用函数:
ClassA ca;
ClassB cb;
/*
调用说明,当我们这样调用的时候,大家应该都很明白的发现
那个func3()函数是不能调用的,因为这个函数在ClassA中根本
就是不存在的。很简单吧,但是复杂的问题本身就是很简单的,
我们继续下面一个例子
*/
change(ca);
/*
你也许说这样应该可以调用func3()函数了吧,不错ClassB中的确是
存在一个名字叫func3()的函数,但是我们仍然不能通过上面那个函数
调用他的,为什么呢?请接着看我下面的解释
*/
change(cb);

/*
因为我们是用一个父类的引用引用一个子类的类型的。
从函数的实现细节,我们的目的很明确的,就是要实现
多态性,因为我们在函数使用的时候才能知道函数参数
引用指向对象的真正类型,可能是ClassA也可能是ClassB的,
当然我们就不能贸然的调用func3()那个函数了。我们我们
那样实现函数在编译器的时候就会被挡住了。

☆结论二☆
这个结论属于程序员编程的能力问题,因为语言提供了灵活性,但是
如何使用是程序员本身的事情,如果你没有很好的理解语言本身,你
写的代码就是经常会出现问题,这个只能怪我们自己,要进修,绝对
不要动辄就说这个语言不好。

*********************本人废话集***************************
很多语言都讲什么简单简洁的,我在这里讲一个不是很生动的例子,
看过这个例子以后,我不知道你对于那些所谓简洁的语言的看法会
有什么改变。呵呵~~~~

经常同行人讲某某企业公司太没有自由了,严重影响自己创造性之类的话。
你想过这些话没有?我来说我的理解,如果这位同行换一家比较自由的公司,
按理讲,他的创造性应该可以得到一定程序的释放(就是发挥了)。
这个就是自由度从小到大给我带来的好处。

同时由于我们这个同行还是以为严格要求自己的人,所以他即使在自由度很大的
空间中也可以很好的把握自己的方向。

好了,中常休息的时间差不多结束了。现在回到正题,一个给程序员自由度很大的
语言,我们称为灵活性比较高的语言。在这样的语言环境中,我们并不是每一个都
可以很好利用这个灵活性的,有时候造成错误发生,但是我们不应该责怪语言本身,
我们需要做的只有一件事情,就是努力提高自己的水平。

而自由度很低的语言,我们就丧失了这种灵活性,我们再想扩大自由度的时候已经
有些不太可能的。但是我们为了获得在自由度大的环境下自由毒比较小的时候,我们
只要严格要求自己就行了。

上面的废话我是建立在程序员就是程序员本身,程序员不是编程机器的基础之上的。
无论如何软件生产不会(至少说几使年之内),象传统生产行业那样的,何况现在
已经进入“人管理“(知识管理)时代。
**********************************************************


☆结论三☆
说到这个结论,实际上就是C++中与override ,overload齐名的mask问题了。
借助上面的分析我们一口气把这个问题也弄明白。
我这个例子和解释是借用<<effective C++>>,特此说明。
不过这里也加入我的一些说明,这样我觉得很容易理解。

下面看一个可能使你会发疯的例子。

class Base {
public:
  void f(const Base& base){}
  virtual void f(int x){}
};

class Derived: public Base {
public:
  virtual void f(double *pd);
};

Derived *pd = new Derived;
pd->f(10);                            // 错误!

这里就发生了函数屏蔽作用,你要是不清楚的话,请你再次看一下上面的结论三。

这不很合理,但ARM对这种行为提供了解释。假设调用f时,你真的是想调用Derived中的版本,但不小心用错了参数类型。进一步假设Derived是在继承层次结构的下层,你不知道Derived间接继承了某个基类BaseClass,而且BaseClass中声明了一个带int参数的虚函数f。这种情况下,你就会无意中调用了BaseClass::f,一个你甚至不知道它存在的函数!在使用大型类层次结构的情况下,这种错误会时常发生;所以,为了防患于未然,Stroustrup决定让派生类成员按名字隐藏掉基类成员。

顺便指出,如果想让Derived的用户可以访问Base::f,可以很容易地通过一个using声明来完成:

class Derived: public Base {
public:
  using Base::f;                   // 将Base::f引入到
                                   // Derived的空间范围
  virtual void f(double *pd);
};

Derived *pd = new Derived;
pd->f(10);                         // 正确,调用Base::f

对于尚不支持using声明的编译器,另一个选择是采用内联函数:

class Derived: public Base {
public:
  virtual void f(int x) { Base::f(x); }
  virtual void f(double *pd);
};

Derived *pd = new Derived;
pd->f(10);                 // 正确,调用Derived::f(int),
                           // 间接调用了Base::f(int)

 

 

0 0

相关博文

我的热门文章

img
取 消
img