C++ 面向對象編程


C++ 面向對象編程
 
     面向對象編程基於三個基本概念: 數據抽象繼承動態綁定
 
1 基類和派生類 
1.1 定義基類 
    在基類中,除了構造函數之外,任意非 static 成員函數都可以是虛函數。
     基類通常應將派生類需要重定義的任意函數定義為虛函數。 
 
1.2 訪問控制 
(1)private  成員
•  通過類對象無法訪問類的private成員。
•  在派生類中不能訪問基類的private成員。 
•  private 成員只能在當前類的作用域內訪問,類的友元也可以訪問類的private 成員。例如,在成員函數中可以訪問private 成員,在成員函數中還可以通過自己類的對象來訪問類的private 成員。類的 作用域包括:類定義的{}之內,類定義之外的成員函數的函數體,形參列表等。
class Base
{
public:
     void Test1(Base& b)
     {
          b.iBase = 0;//有沒有問題?
     }
private:
     int iBase;
};
 
class Derived : public Base
{
public:
     void Test2(Base& b)
     {
          b.iBase = 0;//有沒有問題?
     }
     void Test3(Derived& d)
     {
          d.iDerived = 0;//有沒有問題?
     }
private:
     int iDerived;
};
 
 
 
(2)protected  成員
•  通過類對象無法訪問protected 成員。 
•  protected 成員可被public派生類(包括派生類的派生類,向下傳遞)訪問,也就是說在派生類中可以使用基類的protected 成員。 
•  派生類只能通過派生類對象訪問其基類的 protected 成員,派生類無法訪問其基類類型對象的 protected 成員。

1.3 派生類
     類派生列表指定了一個或多個基類,具有如下形式:
     class classname: access-label base-class 
     這里 access-label 是 public、protected 或 private,base-class 是已定義的類的名字,類派生列表可以指定多個基類。   
     一旦函數在基類中聲明為虛函數,它就一直為虛函數,派生類無法改變該函數為虛函數這一事實。派生類重定義虛函數時,可以使用 virtual 保留字,也可以不使用。     
(1)派生類一般會重定義所繼承的虛函數。如果派生類沒有重定義某個虛函數,則使用基類中定義的版本。 
(2)一般情況下,派生類中虛函數的聲明必須與基類中的定義方式完全匹配例外返回對基類型A的引用(或指針)的虛函數。派生類中的虛函數可以返回類A的派生類的引用(或指針)。
提示:絕對不要重新定義繼承而來的non-virtual函數
     因為non-virtual函數是靜態綁定的。一個子類對象綁定到一個父類指針,另一個子類對象綁定到一個子類指針,通過父類指針調用該函數,調用的是父類的該函數,而不是子類的函數。例如:
class Base
{
public:
     void FuncTest()
     {
          std::cout << "Base" << std::endl;
     }
};
 
class Derived: public Base
{
public:
     void FuncTest()
     {
          std::cout << "Derived" << std::endl;
     }
};
Derived d;
Base* pB = &d;
Derived* pD = &d;
d.FuncTest(); //輸出“Derived”
pB->FuncTest();//輸出“Base”
pD->FuncTest();//輸出“Derived”
1.4 non-virtual 和  virtual  函數的調用
(1) 將基類類型的引用或指針綁定到派生類對象,如果調用非虛函數,則無論實際對象是什么類型,都執行基類類型所定義的函數。
class Base
{
public:
void FuncTest()
{
     std::cout << "Base" << std::endl;
}
};
 
class Derived: public Base
{
public:
};
 
Derived d;
Base* pB = &d;
pB->VirtFunc();//輸出“Base”
 
(2)將基類類型的引用或指針綁定到派生類對象,如果調用虛函數,則直到運行時才能確定調用哪個函數,運行的虛函數是引用所綁定的或指針所指向的對象所屬類型定義的版本。
class Base
{
public:
     virtual void VirtFunc()
     {
          std::cout << "Base" << std::endl;
     }
};
 
class Derived: public Base
{
public:
     void VirtFunc()
     {
          std::cout << "Derived" << std::endl;
     }
};
Derived d;
Base* pB = &d;
Derived* pD = &d;
pB->VirtFunc();//輸出“Derived”
pD->VirtFunc();//輸出“Derived”
     
1.5 虛函數與默認實參 
     虛函數也可以有默認實參。如果一個調用省略了具有默認值的實參,則所用的值由調用該函數的類型定義,與對象的動態類型無關。通過基類的引用或指針調用虛函數時,默認實參為在基類虛函數聲明中指定的值,如果通過派生類的指針或引用調用虛函數,則默認實參是在派生類的版本中聲明的值。
提示: 絕不重新定義繼承而來virtual函數的缺省參數值
     不要重新定義繼承而來的non-virtual函數,但是可以重新定義一個繼承而來的virtual函數。
     virtual函數是動態綁定,而該函數的缺省參數值卻是靜態綁定。C++這么做的就是為了提高運行期效率。
     如果子類重新定義了繼承而來的virtual函數的缺省參數值,那么使用父類指針指向子類對象,然后使用父類指針來調用該函數,所使用的默認參數仍然是從父類繼承而來,而非子類重新定義定義的。例如:
class Base
{
public:
     virtual  void VirtFunc (string sMsg = " Base ")
     {
          cout << sMsg << endl;
     }
};
class Derived:public Base
{
public:
     void VirtFunc(string sMsg = " Derived ")
     {
          cout << sMsg << endl;
     }
};
 
Derived d;
d.VirtFunc();//輸出"Derived"
Base* pD = &d;
pD->VirtFunc();//輸出"Base",而不是"Derived"
 
     為了避免出現上面這種情況,必須將子類中繼承而來的virtual函數設計的跟父類一樣,也就是有同樣的缺省參數值。如果父類修改了,子類也必須跟着同樣修改。
     替換的設計方案是:設計一個 public non-virtual函數(帶有默認參數值)來調用private virtual函數(不帶默認參數值)。public non-virtual函數在子類中不能重新定義,但是private virtual函數可以在子類中重新定義。
class Base
{
public:
     void Func(string sMsg = "Base")
     {
          VirtFunc(sMsg);
     }
private:
     virtual void VirtFunc(string sMsg )
     {
          cout << sMsg << endl;
     }
};
 
class Derived:public Base
{
private:
     virtual void VirtFunc(string sMsg)
     {
          cout << sMsg << endl;
     }
};
 
D d;
d.Func();//輸出"Base"
B* pD = &d;
pD->mf();//輸出"Base"
 
1.6 友元關系與繼承
     友元關系 不能繼承:
(1)基類的友元對派生類的成員沒有特殊訪問權限。
(2)友元類的派生類不能訪問授予友元關系的類。

1.7 繼承與靜態成員
     如果基類定義 static 成員,則整個繼承層次中只有一個這樣的成員。無論從基類派生出多少個派生類,每個 static 成員只有一個實例。
     static 成員遵循常規訪問控制:如果成員在基類中為 private,則派生類不能訪問它。
     如果可以訪問成員,則既可以通過基類訪問 static 成員,也可以通過派生類訪問 static 成員。一般而言,既可以使用作用域操作符也可以使用點或箭頭成員訪問操作符。 

2 轉換和繼承
2.1 派生類到基類的轉換
(1)指針或引用
     派生類型引用到基類類型引用
     派生類型指針到基類類型指針。
     反之是不行的。
(2)對象
     一般可以使用派生類型的對象對基類類型的對象進行初始化或賦值,但沒有從派生類型對象到基類類型對象的直接轉換,編譯器不會自動將派生類型對象轉換為基類類型對象。
 
3 構造函數和復制控制
3.1派生類構造函數
     構造函數和復制控制成員不能繼承,每個類定義自己的構造函數和復制控制成員。像任何類一樣,如果類不定義自己的默認構造函數和復制控制成員,就將使用合成版本。
     派生類的合成默認構造函數,除了初始化派生類的數據成員之外,它還初始化派生類對象的基類部分。基類部分由基類的默認構造函數初始化。
     派生類構造函數的初始化列表只能初始化派生類的成員,不能直接初始化繼承成員。派生類構造函數只能通過將基類包含在構造函數初始化列表中來間接初始化繼承成員。
class People
{
public:
     People(std::string s1, int i1) : name("s1"),age(i1)
     {
     }
private:
     std::string name;
     int age;
};
 
class Student: public People
{
public:
     Student(std::string s1, int i1,std::string s2) : uniName("s2"),People(s1,i1)
     {
     }
private:
     std::string uniName;//學校名稱
};
     構造函數初始化列表為類的基類和成員提供初始值,它並不指定初始化的執行次序。首先初始化基類,然后根據聲明次序初始化派生類的成員。
     一個類只能初始化自己的直接基類(通過將基類包含在構造函數初始化列表中來間接初始化基類)。

3.2 復制控制和繼承
     只包含類類型或內置類型數據成員、不含指針的類一般可以使用合成操作。
     具有指針成員的類一般需要定義自己的復制控制來管理這些成員。   
(1) 派生類的復制構造函數
     如果派生類定義了自己的復制構造函數,該復制構造函數一般應顯式使用基類復制構造函數初始化對象的基類部分:
class Base { /* ... */ };
class Derived: public Base
{
public:
         Derived(const Derived& d):Base(d) { /*... */ }
}; 
(2)派生類賦值操作符
     賦值操作符通常與復制構造函數類似:如果派生類定義了自己的賦值操作符,則該操作符必須對基類部分進行顯式賦值。
//Base::operator=(const Base&)
Derived &Derived::operator=(const Derived &rhs)
{
        if (this != &rhs) //防止給自己賦值
        {
            Base::operator=(rhs); // 調用 Base 類的賦值操作符給基類部分賦值
             ……//為派生類Derived 的成員賦值
        }
        return *this;
(3)派生類析構函數
     派生類析構函數不負責撤銷基類對象的成員。每個析構函數只負責清除自己的成員。
class Derived: public Base
{
public:
    ~Derived()    {
/*... */}
};
(4)虛析構函數
     如果層次中基類的析構函數為虛函數,則派生類析構函數也將是虛函數,無論派生類顯式定義析構函數還是使用合成析構函數,派生類析構函數都是虛函數。
     建議: 即使析構函數沒有工作要做,繼承層次的根類也應該定義一個虛析構函數。
     在復制控制成員中,只有析構函數應定義為虛函數,構造函數不能定義為虛函數。
建議:為多態基類聲明為virtual析構函數
     父類指針指向子類對象,delete父類指針時,如果父類含有non-virtual析構函數,則只有繼承自父類的部分被銷毀,而子類非繼承的部分沒有被銷毀,也就是說子類對象被部分銷毀。 解決的辦法是,將父類的析構函數定義為virtual析構函數。
class Base
{
public:
     Base()
     {
          // std::cout << "Base的構造函數" << std::endl;
     }
 
     ~Base()
     {
          std::cout << "Base的析構函數" << std::endl;
     }
};
 
 
class Derived : public Base
{
public:
     Derived()
     {
          // std::cout << "Derived的構造函數" << std::endl;
     }
 
     ~Derived()
     {
          std::cout << "Derived的析構函數" << std::endl;
     }
};
Base* pB = new Derived;
delete pB;//顯示“Base的析構函數”
如果把Base的析構函數前面添加virtual,則上面結果輸出:
Derived的析構函數
Base的析構函數
注意:
     任何class只要帶有virtual函數,幾乎確定應該也有一個virtual析構函數。一般的,基類應該含有virtual析構函數。基類定義了virtual關鍵字,子類就不用添加該關鍵字了。
     如果class不含有virtual函數,通常表示它並不打算被用作一個基類,所以就不應該將析構函數定義為virtual的。
 
4 繼承情況下的類作用域
4.1 名字沖突與繼承
     與基類成員同名的派生類成員將屏蔽對基類成員的直接訪問。
     可以使用作用域操作符訪問被屏蔽的基類成員:
struct Derived : Base
{
         int get_base_mem() { return Base::mem; }
};

設計派生類時,只要可能,最好避免與基類成員的名字沖突。

4.2 作用域與成員函數
     在基類和派生類中使用同一名字的成員函數,其行為與數據成員一樣:在派生類作用域中派生類成員將屏蔽基類成員。即使函數原型不同,基類成員也會被屏蔽。

4.3 重載函數
     如果派生類重定義了重載成員,則通過派生類型只能訪問派生類中重定義的那些成員。
     如果派生類想要通過自身類型來使用重載的版本,那么派生類必須重定義所有的重載版本,但這樣會繁瑣,可以通過給重載成員提供using 聲明來達到簡化的目的。
using Base::Func;//注意,將所有基類Base中的Func函數在本類中可見。

4.3 名字查找與繼承
(1)首先確定進行函數調用的對象、引用或指針的靜態類型。
(2)在該類中查找函數,如果找不到,就在直接基類中查找,如此循着類的繼承鏈往上找,直到找到該函數或者查找完最后一個類。如果不能在類或其相關基類中找到該名字,則調用是錯誤的。
(3)一旦找到了該名字,就進行常規類型檢查,查看如果給定找到的定義,該函數調用是否合法。
(4)假定函數調用合法,編譯器就生成代碼。如果函數是虛函數且通過引用或指針調用,則編譯器生成代碼以確定根據對象的動態類型運行哪個函數版本,否則,編譯器生成代碼直接調用函數。

5 純虛函數
     在函數形參表后面寫上 = 0 以指定 純虛函數。該函數為后代類型提供了可以覆蓋的接口,但是這個類中的版本決不會調用。
     含有(或繼承)一個或多個純虛函數的類是 抽象基類。除了作為抽象基類的派生類的對象的組成部分,不能創建抽象類型的對象。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM