面向對象程序設計

類的四個主要特徵:抽象、封裝、繼承、多態    

封裝性:一、將有關的數據和操作代碼封裝在一個對象中;二、隱蔽內部細節,只留少量接口,用以接收外部消息

繼承 inheritance:利用一個類建立一個新的類,解決了軟件重用問題,是類最重要的一個特徵

多態:由繼承而產生的相關的不同的類,其對象對同一消息會作出不同反應,增加了程序的靈活性。(例狗和魚是動物的子類,狗的呼吸事件使用肺,魚的是用肺)


類的private私有成員只能被成員函數訪問,而不能被外界直接訪問(類名.變量名)。


類是對象的抽象,對象是類的具體實例(instance)

抽象的作用是表示同一類事物的本質。

面向對象開發的五個過程:

OOA    Object Oriented Analysis   面向對象分析

OOD   Object Oriented Design     面向對象設計

OOP    Object Oriented Programming   面向對象編程

OOT    Object Oriented  Test    面向對象測試

OOSM   Object Oriented Soft Maintenance  面向對象維護


類的大小:

對象的大小等於數據成員所佔空間,而不包含函數代碼。函數代碼是對象公用的。

class Time
{
     int hour,minute,sec;
     void set()
     {
            cin>>hour,minute,sec;
     }
}
cout<<sizeof(Time);

結果應爲12       12 = 3*4


類的實現、信息隱蔽

公用程序函數是用戶使用類的公用接口,通過成員函數對數據成員進行操作稱爲類的實現,類的公用接口與私有實現的分離形成了信息隱蔽。


類庫

類的聲明與定義往往放在兩個文件中,類聲明放在頭文件中,成員函數定義放在主模塊中。

實際工作中,並不是把一個類聲明爲一個頭文件,而是將若干常用的功能相近的類聲明集中在一起,形成類庫。

類庫包括兩部分:

(1)類聲明頭文件

(2)已經編譯過的成員函數的定義,它是目標文件,這樣用戶能看到頭文件中類的聲明和成員函數的原型聲明,卻看不到定義成員函數的源代碼,保護了軟件開發商的利益。


構造函數

構造函數在建立對象時自動執行,名字與類名同,用來處理對象的初始化。

類的數據成員是不能在聲明時初始化的,如下面的寫法是錯誤的

class T
{
    int a = 5;    //錯誤
}
數據成員的賦值

(1)定義對象時賦值: Time t1 = {14,20,56};

(2)建立對象後,用成員函數賦值

(3)構造函數賦值

(4)構造函數的參數初始化表(在構造函數聲明後面)

Box::Box(int h, int w, int len):height(h),width(w),length(len){}

相當於

Box::Box(int h, int w, int len):
{
    height = h;
    width  = w;
    length = len;
}

析構函數

類名前加 ~ 符號,當對象生命期結束時在,自動執行析構函數。具體情況

(1)函數中局部自動對象,在函數調用結束時,析構。如果函數被多次調用,則每次調用時都調用構造函數。

(2)static局部對象,在函數調用結束時對象不釋放,因此也不調用析構函數,只在main函數結束或調用exit結束函數時,才調用static局部對象的析構函數。

(3)如果定義了一個全局對象,在程序流程離開其作用域時(main結束或exit退出),調用其析構函數。

(4)如果用new運算符動態建立了一個對象,則在delete釋放對象時,調用析構函數

析構函數的作用

(1)並不是刪除對象,而是在撤銷對象佔用的內存之前完成一些清理工作,使這部分內存可以被程序分配給新的對象。

(2)執行用戶希望在最後一次使用對象之後所執行的任何操作。

析構函數沒有函數參數,沒有返回值值,沒有函數類型,故也不能被重載。

先構造的後析構,後構造的先析構。


對象數組

定義:Student Stu[3] = {Student(1001,18,87),  Student(1002,19,76), Student(1003,18,72)}

使用:cout << Stu[0].num;


常對象與常成員函數:

Time const t1(12,34,56);

常對象的數據成員都不能被改變,注意:常對象不能調用非const的成員函數。

常數據成員的值不能被改變,故只能利用構造函數的參數初始化表對其進行初始化

常成員函數用const修飾,只能引用本類中的數據成員,而不能修改它。


靜態數據成員、成員函數

靜態數據成員、靜態成員函數屬於類,而不是具體的對象。

靜態數據成員在所有對象中的值相等,只佔一份內存單元。

靜態成員函數只能操作靜態數據成員。


友元函數

如果在本類以外的其他地方定義了一個函數,(該函數可以是或不是某類的成員函數),在類體中用friend對其進行聲明,此函數即爲本類的友元函數。

友元函數可以訪問本類的私有成員。

例1:普通函數作爲友元函數

#include <iostream>
using namespace std;
class Time
{
  public:
      Time(int,int,int); //構造函數
      friend void display(Time &); //友元函數,引用形參
  private:
      int hour,minute,sec;
}

Time::Time(int h,int m,int n):hour(h),minute(m),sec(n)); //構造函數 參數初始化表

void display(Time &t)  //定義友元函數
{
     cout<<t.hour<<t.minute<<t.sec;
}

int main()
{
   Time t(10,23,45);
   display(t);
   return 0;
}
例2:友元成員函數,另一個類的成員函數作爲本類的友元函數

class Date;   //提前引用聲明
class Time
{
   Time(int,int,int);
   display(Data &);
}
class Date
{
    Date(int,int,int);
    friend void Time::display(Date &);  //display是Time類的成員函數,也是本類的友元函數
}

兩個類互相用到對方,故需要進行提前引用聲明,否則編譯報錯。

一個函數可以被多個類聲明爲“友元”,這樣就可以引用多個類中的私有數據。


友元類

友元類中的所有函數都是本類的友元函數,可以訪問本類的所有成員。

聲明的一般形式:friend 類名。


類模版

與函數模版類似,用於類體相同,而數據成員類型不同的情況。

template <class numtype>  //聲明一個類模版,numtype爲虛擬的類型名
class Compare
{
    public:
        Compare(numtype a, numtype b){x=a;y=b;}   //構造函數
        numtype max();
        numtype  min();
    private:
        numtype x,y;
}

numtype Compare <numtype>::max()
{ return (x>y)?x:y; }
numtype Compare <numtype>::min()
{ return (x<y)?x:y; }

//創建對象時,注意要加上<int> <float>等類型標誌
Compare <int> cmp;
Compare <int> cmp(3,7); 

類模版中可以定義多個不同的類型參數

template <class T1, class T2)     //T1,T2 爲兩種變量類型

class someclass

{...}

定義對象:

someclass <int,double> obj;   

************************************************************************************************************************************

二、類的繼承

基類、派生類的繼承方式:

public:基類的公用成員和保護成員在派生類中保持原有訪問屬性,其私有成員仍爲基類私有

protected:基類的公用成員和保護成員在派生類中成了保護成員,其私有成員仍爲基類私有

private:默認,基類的公用成員和保護成員在派生類中成了私有成員,其私有成員仍爲基類私有

保護成員:不能被外界引用,但可以被派生類的成員引用。

雖然派生類外不能通過派生類對象調用私有基類的公用成員函數,但可以通過派生類的成員函數調用私有基類的公用成員函數。

class Student
{
     public:
         Student(int a,string b,char c)   //基類構造函數
           {num=a;name=b;sex=c;}
         void get_value();
         void display();
     private:
         int num;
         string name;
         char sex;
}

class Student1: private Student
{
     public:
          void display1();
     private:
          int age;
          string addr;
}
void Student1::display1()
{
    display();      //派生類可以調用私有基類的公用成員函數,輸出三個成員的值
}

void main()
{
   Student1 stu1;
   stu1.display();    //錯誤,私有基類(private繼承)的公用成員函數在派生類中是私有函數,派生類對象不能調用私有基類的函數
   stu1.display1();
   stu1.age = 18;      // 錯誤,age是派生類的私有成員,外界不可訪問,只能通過成員函數訪問。
}

派生類的構造函數和析構函數

基類的構造函數不可被繼承,所以在設計派生類時不僅要考慮派生類所新增數據成員的初始化,還要考慮基類數據成員的初始化。

解決思路是在執行派生類的構造函數時,調用基類的構造函數。

一般形式:

派生類構造函數名(總參數表列):基類構造函數名(參數表列)

{

      派生類中新增數據成員初始化語句

}

注意:

(1)總參數表列中不僅包括新增數據成員,還要包括基類構造函數用到的數據成員。

(2)派生類構造函數名後面的參數表列中,包括參數類型和參數名(int n),而基類名後面的參數表列中只有參數名而無類型名(a,b,c),因爲這裏不是定義基類構造函數,而是調用基類構造函數,因此這些參數是實參而不是形參。它們可以是常量、全局變量和派生類構造函數總參數表中的參數。

(3)派生類構造函數會將前面3個參數傳遞給基類構造函數的形參。

class Student1: private Student
{
     public:
          Student1(int a,string b,char c,int n,string s):Student(a,b,c);
          void display1();
     private:
          int age;
          string addr;
}


有子對象的派生類的構造函數

派生類中有 對象 數據成員時(數據成員裏有其它類的對象 Student monitor),派生類的構造函數有三個任務:

(1)初始化基類數據成員

(2)初始化子對象數據成員  (!)

(3)初始化派生類數據成員

一般形式:

派生類構造函數名(總參數表列):基類構造函數名(參數表列),子對象名(參數表列)

{派生類中新增數據成員初始化語句。}

注:這裏的總參數表列包括基類構造函數所需數據成員,子對象對應類的構造函數所需數據成員,派生類構造函數所需數據成員

派生類構造函數的執行順序是

(1)調用基類構造函數

(2)調用子對象構造函數

(3)執行派生類的構造函數本身


多層派生時的構造函數

派生類2構造函數名(總參數表列):基類構造函數名(參數表列),派生類1構造函數名(參數表列) (從根向下)

{派生類中新增數據成員初始化語句。}


派生類中的析構函數

派生類不能繼承基類的析構函數,派生類析構函數會自動調用基類的析構函數。

執行順序從下往上,先執行派生類析構函數,再調用子對象的析構函數,最後執行基類析構函數。


多重繼承:

一個派生類繼承兩個或多個基類。class D: public A, protected B, private C;

構造函數:

派生類構造函數名(總參數表列): 基類1構造函數(參數表列), 基類2構造函數(參數表列), 基類3構造函數(參數表列)

{派生類中新增數據成員初始化語句。}

同名覆蓋:派生類新增加的數據成員會覆蓋基類的同名成員。成員函數在函數名、參數個數、參數類型相同的情況下也會覆蓋,否則爲函數重載。

如果C繼承的A,B兩個類中有同名成員,怎麼處理呢?

例:

C類中的數據成員: int A::a;     int  a::a1;     int B::a;    int B::a2;    int  a;  

               成員函數: void A::display();    void B::display();   void show();

類A、B中都有數據成員a、成員函數display,但它們代表不同的存儲單元,可以分別存放不同的數據。

類C如何訪問a、display呢? 解決方法:加上基類名

c1.A::a = 3;

c1.A::display();


虛基類

虛基類不是在聲明基類時聲明的,而是在聲明派生類時,指定繼承方式時聲明的,因爲一個基類可以在生成一個派生類時是虛基類,而在生成另一個派生類時不是派生類。

一般形式: class 派生類名: virtual 繼承方式 基類名

經過這樣的聲明後,當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次,也就是說基類成員只保留一次。

(否則,如果類D繼承自類B、類C,而類B、類C又是繼承自類A的情況下,類D中會存在兩份類A的數據成員)

最後的派生類不僅要負責對其直接基類進行初始化,還要負責對虛基類進行初始化。編譯系統只執行最後的派生類對虛基類的構造函數的調用,而忽略虛基類的其它派生類對虛基類的構造函數的調用,以保證虛基類的數據成員不會被多次初始化。

************************************************************************************************************************************

三、多態性

多態:一個事物有多種形態。

多態性:具有不同功能的函數可以用同一個函數名,這樣就可以用一個函數名調用不同內容的函數。

在面向對象方法中一般這樣描述:向不同的對象發送同一個消息,不同的對象在接收時會產生不同的行爲。

如果一種語言只支持類而不支持多態,是不能稱爲面向對象的,只能說是基於對象。

函數的重載、運算符重載等都是多態現象。

社會中的例子:學校校長通知9月1日開學。不同的對象會作出不同反應:學生準備返校上課;教師要備課;後勤部門要準備教室、宿舍

如果不利用多態性,校長就要分別給學生、老師、後勤部門發通知,規定每一種人接到通知後怎麼做,顯然是件十分複雜細緻的工作。

多態性分爲兩類:靜態多態性和動態多態性

函數重載、運算符重載實現的多態性屬於靜態多態性,在程序編譯時就能夠決定調用哪一個函數,故靜態多態性又稱編譯時的多態性,是通過函數的重載實現的(運算符重載實質上也是函數重載)。

動態多態性是在程序運行過程中才動態地確定操作所針對的對象,又稱運行時的多態性,是通過虛函數實現的。

虛函數

虛函數的作用是允許在派生類中重新定義與基類同名的函數,並且可以通過基類指針或引用來訪問基類和派生類的同名函數。

class Student
{
     public:
         Student(int a,string b,char c)   //基類構造函數
         void display(){cout<<num<<name<<sex;}
     private:
         int num;
         string name;
         char sex;
}

class Graduate: private Student
{
     public:
          void display(){cout<<age<<addr;}
     private:
          string addr;
}

void main()
{
   Student stu(1001,"LI",87.5);
   Graduate gra(2001,"Ta",90,"london");
   Student *pt = &stu;
   pt->display();
   pt=&gra;   //改變指針指向
   pt->display(); 
}

結果:兩次執行的都是基類的display函數,爲什麼呢?
原因:本來,基類指針是用來指向基類對象的,如果用它指向派生類對象,則進行指針類型轉換,將派生類對象的指針先轉換爲基類的指針,所以基類指針指向的是派生類對象
中的基類部分,因此無法用基類指針去調用派生類對象中的成員函數。
解決方法:
(1)直接用派生類的對象調用成員函數,gra.display
(2)再定義一個指針,指向派生類對象,Graduate *p2 = &gra;   gra->display();
(3)虛函數,將基類中display的定義改爲  virtual void display() 即可。
派生類中的虛函數取代了基類中原有的虛函數,因此使基類指針指向派生類對象後,調用虛函數時就會調用派生類的虛函數了。

虛函數是很有用處的:
在類的繼承中,基類的成員函數可能並不適用於派生類,在派生類中新建一個函數是一個選擇,但如果派生層次多,就要起很多函數名,不方便。如果採用同名函數,又會發生
同名覆蓋。
利用虛函數就能很好的解決這個問題,當把某個成員函數聲明爲虛函數後,允許在其派生類中對該函數進行重新定義賦予新的功能,並且可以通過指向基類的指針指向同一類族
中不同類的對象,從而調用其中的同名函數。

由虛函數實現的動態多態性就是:同一類族中不同類的對象,對同一函數作出不同的響應。

當一個成員函數被聲明爲虛函數後,其派生類中的同名函數都自動成爲虛函數。因此,在派生類中重新定義虛函數時,可以不加virtual,習慣上是加的以使程序清晰。

通過虛函數與指向基類對象的指針變量的配合使用,就能方便的調用同一類族中不同類的同名函數,只要先用基類指針指向即可。該指針指向不同類的對象,就會調用該對象的同名函數。

函數重載處理的是同一層次上同名函數問題,而虛函數處理的是不同派生層次上同名函數問題。同一類族的虛函數的首部是相同的,而函數重載時函數的首部是不同的。


靜態關聯與動態關聯

確定調用具體對象的過程成爲關聯(binding),這裏是指把將一個函數名與一個類對象捆綁在一起,建立關聯。一般來說,關聯是吧一個標識符和一個存儲地址聯繫起來。

編譯系統要根據已有的信息,對同名函數的調用作出判斷。例如函數的重載,系統是根據參數的個數和類型的不同去找與之匹配的函數的。

靜態關聯:函數重載和通過對象名調用的函數,在編譯時即可確定函數所屬的類,稱static binding,又稱早期關聯。

動態關聯:通過虛函數實現的綁定,只有在程序運行過程中,才能把要調用的虛函數和類對象關聯起來,故稱動態關聯,也稱滯後關聯,又稱運行時的多態性。


在什麼情況下應當聲明虛函數

注意事項:

(1)只能用virtual聲明類的成員函數,使它成爲虛函數,而不能將類外的普通函數聲明爲虛函數。因爲虛函數的作用是允許在派生類中對基類的虛函數重新定義。顯然,它只能用於類的繼承層次中。

(2)一個成員函數被聲明爲虛函數後,在同一類族中的類就不能再定義一個非virtual的但與該虛函數具有相同參數和返回值類型的同名函數。(這句沒用吧,不是系統默認把派生類的屬性定位virtual嗎)

根據什麼考慮把一個成員函數定義爲虛函數

(1)首先考慮該類是否會作爲基類,然後看該成員函數是否會被派生類修改。

(2)考慮對成員函數的調用是通過對象名還是基類指針或引用去訪問,如果是通過基類指針或引用去訪問的,則應當聲明爲虛函數。

(3)有時,定義虛函數時,其函數體可能是空的,其作用只是定義一個虛函數名,具體功能由派生類實現。


純虛函數

基類中某一成員函數定義爲虛函數,並非基類本身的需要,而是考慮到派生類的需要,因此在基類中預留了一個函數名,具體功能由派生類實現。

可以寫成這樣 virtual float area() const {return 0;}

爲了簡化,可以寫成 virtual float area() const = 0;  //

後面這種被“初始化”爲0的虛函數形式就被成爲純虛函數。  

純虛函數沒有函數體,後面的“=0”並不表示返回值爲0,而只起形式上的作用,告訴編譯系統這是純虛函數。這是一個純虛函數的聲明語句,後面要加分號。


抽象類 abstract class

不用來定義對象而只作爲一種基本類型用作繼承的類成爲抽象類,作爲基類時又被成爲抽象基類 abstract base class。凡是包含純虛函數的類都是抽象類,純虛函數不能被調用,包含純虛函數的類是無法建立對象的。如果派生類對所有純虛函數進行了定義,那麼這個派生類就不是抽象類了,可以用來創建對象。

如果聲明瞭一個類,一般用它來定義對象。但有些類不用來生成對象。定義它的目的是用它作基類去建立派生類,在此基礎上根據需要定義出功能各異的派生類。


虛析構函數

當派生類的對象從內存中撤銷時一般先調用派生類的析構函數,然後調用基類的析構函數。

但如果用new運算符建立了臨時對象,若基類中有析構函數,並且定義了一個指向該基類的指針變量。在程序用帶指針參數的delete運算符撤銷對象時,系統會只執行基類的析構函數,而不執行派生類的析構函數。

解決方法:將基類的析構函數聲明爲虛函數。



發佈了143 篇原創文章 · 獲贊 11 · 訪問量 77萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章