C++的重要性质!

来源:互联网 发布:win7红警2点网络进不去 编辑:程序博客网 时间:2024/04/29 19:28

C++是一种扭转程序员思维模式的语言,一个人思维模式的扭转,不可能轻而易举一蹴而就。

C++是最重要的面向对象语言,因为它站在C语言的肩膀上,而C语言拥有绝对多数的使用者,C++并非纯粹的面向对象程序语言,但有时候混血并不是坏事,纯种不见得就有多好。(所谓纯面向对象语言,是指不管什么东西,都应该存在于对象之中,java就是纯面向对象语言)。C++语言范围何其广大,这部分主题的挑选完全是以MFC Programming所需技术为前提。

1、类及其成员----谈封装

      让我们把世界看成是一个由对象(object)所组成的大环境。对象是什么?说白了,“东西”是也!任何实际的物体你都可以说他是对象。为了描述对象,我们应该先把对象的属性描述出来,好,给“对象的属性”一个比较学术的名词,就是“类”(class)。

      对象的属性有2大成员,一是属性,一是方法。在面向对象的术语中,前者常被称为property,后者常被称为method。另有一种比较像程序设计领域的术语,名为member variable(或data member)和member function。为求统一,本部分使用第二组术语,也就是member variable(或data member)和member function。一般而言,成员变量通常由成员函数处理。

      如果我们以CSquare代表“正方形”这种类。正方形有color,正方形可以display。好,color就是一种成员变量,display就是一种成员函数:

[cpp] view plaincopy
  1. //下面是C++语言对CSquare的描述:  
  2. class CSquare  
  3. {  
  4. private:  
  5.       int m_color;  
  6. public:  
  7.       void display();  
  8.       void setcolor(int color){m_color=color;}  
  9. };  
  10. void main()  
  11. {  
  12.       CSquare square;//声明一个CSquare类的对象  
  13.       square.setcolor(RED);//设定成员变量  
  14.       square.display();//调用成员函数  
  15. }  
 

      成员变量可以只在类内部被处理,也可以开放给外界处理。以数据封住的目的而言,自然是前者较为妥当,但有时候也不得不放开。为此,C++提供了private、public、protected三种修饰词。一般而言,成员变量尽量声明为private,成员函数通常声明为public。上例中的m_color既然声明为private,我们势必得准备一个成员函数setcolor,供外界设定颜色用。

      把数据声明为private,不允许外界随意存取,只能通过特定的接口来操作,这就是面向对象的封住特性。

2、基类与派生类:谈继承(inheritance)

      其它语言欲完成封装性质,并不太难。以C为例,在结构中放置资料以及处理资料的函数的指针,就可以得到某种程度的封装。C++神秘而特有的性质其实在于继承。矩形是形、椭圆形也是形、三角形也是形。苍蝇是昆虫、蜜蜂是昆虫、蚂蚁也是昆虫。是的,人类习惯把相同的性质抽取出来,成立一个基类(base class),再从中演化出派生类(derived class)。所以,关于形状,我们就有了这样的类层次结构。

[cpp] view plaincopy
  1. class CShape  
  2. {  
  3. private:  
  4.       int m_color;  
  5. public:  
  6.       void setcolor(int color){m_color=color;}  
  7. };  
  8. //-------------------------------------------------  
  9. class CRect : public CShape//矩形是一种形状  
  10. {                          //它会继承m_color和setcolor()  
  11. public:  
  12.       void display(){...}  
  13. };  
  14. //-------------------------------------------------  
  15. class CEllipse : public CShape//椭圆是一种形状  
  16. {                             //它会继承m_color和setcolor()  
  17. public:  
  18.       void display(){...}  
  19. };  
  20. //-------------------------------------------------  
  21. class CTriangle : public CShape//三角形是一种形状  
  22. {                             //它会继承m_color和setcolor()  
  23. public:  
  24.       void display(){...}  
  25. };  
  26. //-------------------------------------------------  
  27. class CSquare : public CRect//正方形是一种矩形  
  28. {                             //它会继承m_color和setcolor()  
  29. public:  
  30.       void display(){...}  
  31. };  
  32. //-------------------------------------------------  
  33. class CCircle : public CEllipse//圆形是一种椭圆  
  34. {                             //它会继承m_color和setcolor()  
  35. public:  
  36.       void display(){...}  
  37. };  
  38. //于是你可以这么操作:  
  39. CSquare square;  
  40. CRect rect1,rect2;  
  41. CCircle circle;  
  42. square.setcolor(1);  
  43. square.display();  
  44. rect1.setcolor(2);  
  45. rect1.display();  
  46. rect2.setcolor(3);  
  47. rect2.display();  
  48. circle.setcolor(4);  
  49. circle.display();  
 

注意以下这些问题与事实:

1)所有类都是由CShape派生下来的,所以它们都自然而然继承了CShape的成员,包括变量和函数。也就是说,所有的形状都暗自具备了m_color变量和setcolor函数。所谓的暗自(implicit),意思是无法从各派生类的声明中直接看出来。

2)两个矩形对象rect1和rect2各有自己的m_color,但关于setcolor函数却是共享相同的CRect::setcolor(其实更应该说是CShape::setcolr)。想一下,同一个函数如何处理不同的数据,为什么rect1.setcolor和rect2.setcolor明明都是调用CRect::setcolor(),却能够有条不紊的处理rect1.m_color和rect2.m_color?答案在于所谓的this指针,下一节我们就会提到它。

3)既然所有类都有display操作,那么把它提升到老祖宗CShape去,然后再继承它好吗?不好,因为display函数应该因不同的形状而操作不同。

4)如果display不能提升到基类去,我们就不能够以一个for循环或while循环干净漂亮的完成下列操作(此操作模式在面向对象方法中重要无比):

CShape shapes[5];

//...令5个shape各为矩形、正方形、椭圆、圆形、三角形

for(int i=0;i<5;i++)

      shapes[i].display();

5)Shape只是一种抽象概念,世界上并没有“形状”这种东西,你可以在一个C++程序中做以下操作,但是不符合生活法则:

CShape shape;//世界上没有“形状”这种东西

shape.setcolor();//所以这个操作就有点古怪

这同时也说出了第三点的另一个否定理由:按理你不能把一个抽象的“形状”显示出来,不是吗?

3、this指针

      刚才讲过,两个矩形对象rect1和rect2各有自己的m_color成员变量,但rect1.setcolor()和rect2.setcolor()却都通往唯一的CRect::setcolor()成员函数,那么CRect::setcolor()如何处理不同对象中的m_color?答案是:成员函数有一个隐藏参数,名为this指针,但你调用:

rect1.setcolor(2);

rect2.setcolor(3);

时,编译器实际上为你做出来的代码是:

CRect::setcolor(2,CRect*(&rect1));

CRect::setcolor(3,CRect*(&rect2));

不过,由于CRect本身并没有setcolor,它是从CShape继承来的,所以编译器实际上产生的代码是:

CShape::setcolor(2,CRect*(&rect1));

CShape::setcolor(3,CRect*(&rect2));

多出来的参数就是所谓的this指针,至于类之中成员函数的定义:

[cpp] view plaincopy
  1. class CShape  
  2. {  
  3. ......  
  4. public:  
  5.       void setcolor(int color){n_color=color}  
  6. };  
 

被编译过后,其实是:

[cpp] view plaincopy
  1. class CShape  
  2. {  
  3. ......  
  4. public:  
  5.       void setcolor(int color,(CShape*)this){this->n_color=color}  
  6. };  
 

4、虚函数与多态(polymorphism)

      前面曾经提到过,前一个例子不能完成这样的操作:

CShape shape[5];

//...令5个shape各为矩形、正方形、椭圆、圆形、三角形

for(int i=0;i<5;i++)

      shapes[i].display();

但是这种所谓“对象操作的一般化操作”在application framework中非常重要。我们希望display函数能根据CShape派生出来的类的不同,只需调用display就能显示自己的形状特性。

      为了支持这种能力,C++提供了所谓的虚函数(virtual function)。

虚拟+函数?!听起来很恐怖的样子,如果你了解汽车的离合器踩下去 代表 汽车空挡,空挡表示失去引擎本身的牵制力,你就会了解“高速行驶时刹车决不能踩离合器”的道理并矢志遵守它。好,如果你真的了解为什么需要虚拟函数 以及 什么情况下需要它,你就能够掌握它的灵魂与内涵,真正了解它的设计原理,并且发现它非常合乎人性。并且,真正知道怎么用它。

看下面的一个例子:

[cpp] view plaincopy
  1. #include <string.h>  
  2. //-----------------------------------------------  
  3. class CEmployee//职员  
  4. {  
  5. private:  
  6.     char m_name[30];  
  7. public:  
  8.     CEmployee();  
  9.     CEmployee(const char* nm){strcpy(m_name,nm);}  
  10. };  
  11. //------------------------------------------------  
  12. class CWage : public CEmployee //时薪职员是一种职员  
  13. {  
  14. private:  
  15.     float m_wage;  
  16.     float m_hours;  
  17. public:  
  18.     CWage(const char* nm):CEmployee(nm)  
  19.     {  
  20.         m_wage=250.0;  
  21.         m_hours=40.0;  
  22.     }  
  23.     void setWage(float wg)  
  24.     {  
  25.         m_wage=wg;  
  26.     }  
  27.     void setHours(float hrs)  
  28.     {  
  29.         m_hours=hrs;  
  30.     }  
  31.     float computePay()  
  32.     {  
  33.         return (m_wage*m_hours);//时薪职员以 钟点费*每周工时 计薪  
  34.     }  
  35. };  
  36. //------------------------------------------------  
  37. class CSales : public CWage //销售员是一种时薪职员  
  38. {  
  39. private:  
  40.     float m_comm;  
  41.     float m_sale;  
  42. public:  
  43.     CSales(const char* nm):CWage(nm)  
  44.     {  
  45.         m_comm=m_sale=0;  
  46.     }  
  47.     void setCommission(float comm)  
  48.     {  
  49.         m_comm=comm;  
  50.     }  
  51.     void setSales(float sale)  
  52.     {  
  53.         m_sale=sale;  
  54.     }  
  55.     float computepay()  
  56.     {  
  57.         return  CWage::computePay() + m_comm*m_sale;//销售员以 钟点费*每周工时+佣金*销售额 计薪  
  58.     }  
  59. };  
  60. //------------------------------------------------  
  61. class CManager : public CEmployee //经理也是一种职员  
  62. {  
  63. private:  
  64.     float m_salary;  
  65. public:  
  66.     CManager(const char* nm):CEmployee(nm)  
  67.     {  
  68.         m_salary=15000;  
  69.     }  
  70.     void setSalary(float salary)  
  71.     {  
  72.         m_salary=salary;  
  73.     }  
  74.     float computePay()  
  75.     {  
  76.         return m_salary;//经理以 固定周薪 计薪  
  77.     }  
  78. };  
 

虚函数的故事要从薪水的计算说起,根据不同职员的计薪方式,设计了computePay()函数。看上面代码中computePay函数的实现,注意代码中的作用域操作符(::)。

接下来我们要触及对象类型的转换,这关系到指针的运用,更直接关系到为什么需要虚函数。了解它,对application framework如MFC者的运用十分十分重要。

假设我们有2个对象:

CWage aWager;

CSales aSales("张三");

销售员是时薪员之一,因此这样做是合理的:

aWager=aSales;//合理,销售员必定是时薪员

这样就不合理:

aSales=aWager;//错误,时薪员未必是销售员

如果你一定要转换,必须使用指针,并且明显的做类型转换(cast)操作:

[cpp] view plaincopy
  1. CWage* pWager;  
  2. CSales* pSales;  
  3. CSales aSales("张三");  
  4. pWager=&aSales;//把一个基类指针 指向 派生类对象 合理且自然  
  5. pSales=(CSales*)pWager;//强迫转换,语句上可以,但不符合实际  
 

为了某种便利(这个便利稍候即可看到),我们也会想以“一个通用的指针”表示所有可能的职员类型。无论如何,销售员、时薪职员、经理都是职员,所以下面的操作合情合理:

[cpp] view plaincopy
  1. CEmployee* pEmployee;  
  2. CWage aWager("john");  
  3. CSales aSales("jack");  
  4. CManager aManager("mary");  
  5. pEmployee=&aWager;//合理,因为时薪员必是职员  
  6. pEmployee=&aSales;//合理,因为销售员必是职员  
  7. pEmployee=&aManager;//合理,因为经理必是职员  
 

也就是说,可以把一个“职员指针”指向任何一种职员。这带来的好处就是程序设计的巨大弹性,譬如说你设计一个链表,各个元素都是职员,你的add函数可能因此希望有一个“职员指针”作为参数:add(CEmployee* pEmp);//pEmp可以指向任何一种职员

5、晴天霹雳

      我们渐渐接触问题的核心,上述C++性质使真实生活经验 的确在 计算机语言中仿真了出来,但是万里无云的日子里却出现了一个晴天霹雳:如果你以一个“基类之指针”指向一个“派生类之对象”,那么经由此指针,你就只能够调用基类(而不是派生类)所定义的函数。

[cpp] view plaincopy
  1. CSales aSales("Jack");  
  2. CSales* pSales;  
  3. CWage* pWager;  
  4. pSales=&aSales;  
  5. pWager=&aSales;//以“基类指针”指向“派生类对象”  
  6. pWager->setSales(800.0);//错误(编译器会检测出来),因为CWage并没有定义setSales()函数  
  7. pSales->setSales(800.0);//正确,调用的是CSales::setSales();函数  
 

虽然pSales和pWager指向同一个对象,但却因指针的原始类型不同而使2者之间有了差异,,延续此例,我们看另一种情况:

pWager->computePay();//调用CWage::computePay()

pSales->computePay();//调用CSales::computePay()

虽然pWager和pSales指向同一个对象CSales,但两者调用的computePay却不相同。到底该调用哪个函数,必须视指针的原始类型而定,与指针实际所指对象无关。

三个结论:

1)如果你以一个“基类指针”指向“派生类对象”,那么经由该指针你只能够调用基类所定义的函数。

2)如果你以一个“派生类之指针”指向“基类对象”,你必须先做明星的类型转换。这种做法很危险,不符合真实生活经验,在程序设计上也会给程序员带来困惑。

3)如果基类和派生类都定义了“相同名称的成员函数”,那么通过对象指针调用成员函数时,到底调用哪一个函数,必须视指针的原始类型而定,而不是视指针实际所指的对象类型而定,这与第一点的意义相通。

      得到这些结论以后,看看什么事情会困扰我们,前面我曾提到一个由职员组成的链表,如果我想写一个printNames函数遍历链表中的每一个元素并打印出职员的名字,我们可以在CEmployee(最基类)中多加一个getName函数,然后再设计一个While循环,以此打印职员链表中职员的名字(遍历链表的每个元素,调用其getName函数即可)。

      但是函数的调用是依赖指针的原始类型而不管它实际上指向何方(何种对象),所以,上面所想象的循环都执行的是同一条语句。即基类CEmployee的getName函数,不能达到我们预期的目的。

6、虚函数与一般化

      你可以体会,上述的while循环其实就是把操作“一般化”。“一般化”之所以重要,在于它可以把现在的、未来的统统纳入考虑。将来即使有另一种名曰“顾问”的职员,上述计薪循环应该仍然能够正常运行。当然了,“顾问”的computePay必须设计好。

      “一般化”如此重要,解决上述问题因此也就迫切起来,我们需要的是什么呢?是能够“依旧以基类指针代表每一种职员”,而又能够在“实际指向不同种类之职员”时,调用到不同版本只computePay的能力。这种性质就是多态(polymorphism),靠虚函数来完成。

      从 操作性 定义来看,什么是虚函数呢?如果你预期派生类有可能重新定义某一个成员函数,那么你就在基类中把此函数设为virtual。MFC有2个非常重要的虚函数,与document有关的Serialize函数和与View有关的OnDraw函数。你应该在自己的CMyDoc和CMyView中改写这两个函数。

7、多态(polymorphism)

      你看,我们以相同的指令却调用了不同的函数,这种性质称为多态,编译器无法再编译时判断到底该调用哪一个函数,必须在执行时才能判断,这成为后期绑定或动态绑定。至于C函数或C++的non-virtual函数,在编译时期就转化为一个固定地址的调用了,这成为前期绑定或静态绑定。

      多态的目的,就是要让处理“基类之对象”的程序代码,能够完全无碍的继续适当处理“派生类之对象”。可以说,虚函数是多态以及动态绑定的关键,同时,它也是了解如何使用MFC的关键。

      再次回到前面CShape的例子。我们说CShape是抽象的,所有它根本不应该有display这个操作,但为了在各具体派生类中绘图,我们又不得不在基类CShape中加上display虚函数,你可以定义它什么也不做:

[cpp] view plaincopy
  1. class CShape  
  2. {  
  3. public:  
  4.       virtual void display(){}  
  5. };  
  6. 或只是给个消息:  
  7. class CShape  
  8. {  
  9. public:  
  10.       virtual void display(){cout<<"Shape"<<endl;}  
  11. };  
 

 

这两种做法都不高明,因为这个函数根本就不应该被调用,我们根本就不该定义它,步定义但又必须保留一块空间给它,于是C++提供了所谓的纯虚函数:

 

[cpp] view plaincopy
  1. class CShape  
  2. {  
  3. public:  
  4.       virtual void display()=0;  
  5. };  
 

 

纯虚函数不需要定义实际操作,它的存在只是为了在派生类中被重新定义,只是为了提供一个多态接口。只要是拥有纯虚函数的类,就是一种抽象类,它是不能够被实例化的,也就是说,你不能根据它产生一个对象,如果硬要用抽象类来产生一个对象,那么会换来这样的编译消息:error:illegal attempt to instantiate abstract class.

关于抽象类还有一点需要补充,CCircle继承了CShape之后,如果没有改写CShape中的纯虚函数,那么CCircle本身也就成为一个拥有纯虚函数的类,于是它也是一个抽象类。

对虚函数的总结:

1)如果你期望 派生类 重新定义 一个成员函数,那么你应该在基类中把此函数设为virtual。

2)以单一指令调用不同函数,这种性质成为多态

3)虚函数是C++语言多态以及动态绑定的关键

4)既然抽象类中的虚函数不打算被调用,我们就不应该定义它,应该把他设为纯虚函数(在函数声明之后加上“=0”即可)

5)我们可以说,拥有纯虚函数者为抽象类,以别于所谓的具体类

6)抽象类不能产生出对象实例,但我们可以拥有指向抽象类的指针,以便于操作抽象类的各个派生类

7)虚函数派生下去仍为虚函数,而且可以省略virtual关键词

 

原创粉丝点击