1. 程式人生 > >C/C++ 常見問題總結_面向物件與類

C/C++ 常見問題總結_面向物件與類

面向物件與類

面向物件基本知識

  1. 面向物件與面向過程的區別

    面向過程是一種以過程為中心的程式設計思想,以演算法進行驅動。面向物件是一種以對像為中心的程式設計思想,以訊息進行驅動。面向過程程式語言的組成為:程式=演算法+資料,面向物件程式語言的組成為:程式=物件+訊息。

  2. 面向物件的特徵是什麼
    面向物件三個要素為:封裝、繼承、多型。面向物件中所有的物件都可以歸屬為一個類。
    封裝:將抽象得到的資料和行為相結合,形成一個有機的整體,也就是將資料與操作資料的原始碼進行有機的結合,形成類。(資料抽象:依賴於類的介面和實現分離的技術)
    繼承:通過繼承聯絡在一起的類能夠形成一種層次關係。通常在層次關係的根部有一個基類,其他類直接或者間接地從基類繼承而來,這些繼承得到的類稱為派生類。基類負責定義在層次關係中所有類共同擁有的成員,而每個派生類定義各自特有的成員。
    多型:

    (動態繫結)我們把具有繼承關係的多個型別稱為多型型別,因為我們能使用這些型別的“多種形式”而無須在意他們的差異。引用或指標的靜態型別與動態型別不同這一事實正是c++語言支援多型性的根本存在。當我們使用基類的引用或指標呼叫基類中定義的一個函式時,我們並不知道該函式真正作用的物件是什麼,他可能是一個基類的物件也可能是一個派生類的物件 。如果該函式是一個虛擬函式, 被呼叫的函式是與繫結到指標或引用上的物件的動態型別相匹配的那一個,直到執行時才會決定呼叫那個版本(通常情況下,如果我們不使用某個函式,則無須為該函式提供定義,但是我們必須為每一個虛擬函式提供定義,不管他是否被用到)。另一方面,對非虛擬函式的呼叫在編譯時繫結,此外通過物件進行的函式呼叫也在編譯時繫結(靜態型別與動態型別相同)。

  1. 抽象類及它的用途
    包含純虛擬函式的類稱為抽象類。抽象類把有共同屬性或方法的物件抽象成一個類。
    將虛擬函式的函式體位置(宣告語句的分號之前)書寫=0(只能出現在類內部的虛擬函式宣告處),就可以將一個虛擬函式宣告成虛擬函式。純虛擬函式無須提供定義,但也可以提供定義,不過函式體必須定義在類的外部。不能建立抽象基類的物件,派生類必須重新定義純虛擬函式,不然仍是抽象基類。

類成員

  1. 成員變數有哪些訪問控制方式
    在類的成員中,類的資料變數就是類的成員變數,通過宣告private、protected和public 3種訪問許可權來對成員變數進行訪問控制。

  2. 類的靜態成員
    宣告為static的類成員能在類的範圍內共享,這樣的類成員就是類的靜態成員。類的靜態成員存在於任何物件之外, 物件中不包含任何與靜態資料成員有關的資料。類似的, 靜態成員函式也不與任何物件繫結一起,它們不包含 this 指標。作為結果,靜態成員函式不能宣告成 const 的,而且我們也不能在 static 函式體內使用 this 指標。(這一限制既適用於 this 的顯示使用, 也對呼叫非靜態成員的隱式使用有效。)類的靜態成員函式只能訪問類的靜態成員。

 **使用類的靜態成員**
1、使用作用域運算子直接訪問靜態成員
2、可以使用類的物件、引用或指標來訪問靜態成員。
3、成員函式不通過作用域運算子就可以訪問靜態成員。

多型

  1. 什麼是多型?多型的作用
    編寫程式實際上就是一個將世界的具體事物進行抽象化的過程,多型就是抽象化的一種體現,把一系列具體事物的共同點抽象出來,再通過這個抽象的事務,與不同的具體事務進行對話。
    派生類的物件可以繫結在基類的指標或者引用上,此時將會發生靜態型別與動態型別不一致的情況,當其呼叫虛擬函式時將會發生動態繫結。

繼承

  1. 派生類與基類的轉換
    派生類物件可以繫結在基類的指標或引用上。
    不存在從基類向派生類的隱式型別轉換:
    之所以存在派生類向基類的型別轉換是因為每個派生類物件都包含一個基類部分,而基類的引用或指標可以繫結到該基類部分上。一個基類物件既可以以獨立的形式存在,也可以以派生類物件的一部分存在,所以不能講一個派生類的引用或指標繫結到基類的物件上,不存在基類向派生類的自動型別轉換。
//quote父類 bulk_quote子類
quote base;
bulk_quote*bulk=&base;//錯誤
bulk_quote &bulkref=base;//錯誤

注意:即使一個基類的指標或者引用繫結到一個派生類物件上,我們也不能執行從基類向派生類的轉換

bulk_quot bulk;
quote*item=&bulk;
bulk_quote*bulkp=item;//錯誤

當我們確定已經是安全的時候,可以使用static_cast來強制覆蓋編譯器的檢查工作。
在物件之間不存在型別轉換:
派生類向基類的自動型別轉換隻對指標或引用有效, 在派生類型別和基類型別之間不存在這樣的轉換。由於拷貝建構函式或拷貝賦值運算子時這些成員接受引用作為引數,所以派生類向基類的轉換允許我們給基類對應的成員(拷貝構造、賦值運算子、移動等)傳遞一個派生類物件,此時實際執行的成員(拷貝構造、賦值運算子、移動等)是基類中的那個,它只能處理基類自己的成員,因此當我們用一個派生類物件為一個基類物件初始化或賦值時,只有該派生類物件的基類部分會被拷貝、移動或賦值,它的派生類部分將會被忽略掉。

  1. 什麼是虛擬函式?有什麼作用?
    虛擬函式是用於面向物件中實現多型的機制。它的核心理念就是通過基類訪問派生類定義的函式。虛擬函式必須是費靜態成員函式。當某個函式被宣告成虛擬函式時, 則在所有派生類中它都是虛擬函式(可以使用 virtual 關鍵字指出該性質,然而並非必須這麼做)。
    當派生類的函式覆蓋了某個繼承而來的虛擬函式則:1、形參列表相同;2、返回型別匹配(特殊情況: 當類的虛擬函式是類本身的指標或引用
    時);
    說明符final和override
    可以使用 override 來說明派生類中的虛擬函式, 可以將某個函式指定為 final, 則之後任何嘗試覆蓋該函式的操作都將引發錯誤。
    說明符 final 和 override 說明符出現在形參列表之後(包括 const 和引用修飾符,以及尾置返回型別)。
    虛擬函式與預設實參
    當使用了預設實參時,則該實參值由本次呼叫的靜態型別決定,即:如果我們通過基類的引用或指標呼叫函式,則使用基類中定義的預設實參,即使實際執行的是派生類中的函式也是如此。

  2. 建構函式與解構函式呼叫時機
    (總結拷貝控制時再講)

訪問控制
定義在 public 說明符之後的成員在整個程式內可被訪問, public 成員定義類的介面;
定義在 private 說明符之後的成員可以被類的成員函式或友元訪問,但是不能被使用該類的程式碼訪問, private 部分封裝了類的實現細節。
每個訪問說明符指定了接下來的成員的訪問級別, 其有效範圍直到出現下一個訪問說明符或者達到類尾為止。
protected說明符:一個類使用 protected 關鍵字來宣告那些他希望與派生類分享但是不
想被其他公共訪問使用的成員。
1、和私有成員類似,受保護的成員對於類的使用者來說是不可訪問的。
2、和公有成員類似,受保護的成員對於派生類的成員和友元來說是可以訪問的
3、派生類的成員或友元只能通過派生類物件來訪問基類的受保護成員。派生類對於一個基類物件中的受保護成員沒有任何訪問特權。

class base{
    protected:
        int prot_mem;
};
class sneaky:public base{
    friend void clobber(sneaky&);
    friend void clobber(base&);
    int j;
};
//clobber可以訪問sneaky物件的private和protected成員
void clobber(sneaky&s){s.j=s.prot_mem=0;}
//錯誤 clobber不能訪問base的protected成員
void clobber(base&b){b.j=b.prot_mem=0;}

派生類的成員和友元只能訪問派生類物件中的基類部分的受保護成員;對於普通的基類物件中的成員不具有特殊訪問許可權

1.繼承方式與繼承時訪問級別的變化
共有、私有和受保護繼承
某個類對其繼承而來的成員的訪問許可權受到兩個因素影響:
1、在基類中該成員的訪問說明符
2、 派生類派生列表中的訪問許可權符
派生訪問說明符對於派生類的成員(及友元)能否訪問其直接基類的成員沒有什麼影響, 對基類成員的訪問許可權只與基類中的訪問說明符有關。派生訪問說明符的目的是控制派生類使用者(包括派生類的派生類在內)對於基類成員的訪問許可權。
當以 public 方式繼承時, 成員遵循原有的訪問說明符, 以 protected方式繼承時,訪問說明降一級,當以 private 方式繼承時,都為 private。

派生類向基類轉換的可訪問性
1、如果是 public 繼承,則使用者程式碼和後代類都可以使用派生類到基類的轉換。
2、如果是 private 或 protected 繼承的,則使用者程式碼不可以使用派生類向基類的轉換。
3、如果是 private 繼承,則從 private 繼承類派生的孫類不能轉換為基類
4、如果是 protected 繼承,則從 protected 繼承派生的孫類的成員函式可以轉換為基類
5、 無論以什麼派生訪問標號, 派生類本身都可以使用派生類向基類的轉換。
NOTE: 要確定到基類的轉換是否可訪問,可以考慮基類的 public 成員是否可訪問,如果可以,轉換是可以訪問的,否則,轉換是不可訪問的。

友元與繼承
友元關係不能傳遞和繼承。 每個類控制自己的成員的訪問許可權,基類的友元在訪問派生類成員時不具有特殊性,類似地, 派生類的友元也不能隨意訪問基類的成員。

class base{
    friend class pal;
    protected:
        int prot_mem;
};

class sneaky:public base{
    friend void clobber(sneaky&);
    friend void clobber(base&);
    int j;
};

class pal{
    public:
        int f(base b) {return b.prot_mem;}//正確:pal是base的友元
        int f2(sneaky s){return s.j;}//pal不是sneaky的友元
        //對基類的訪問許可權由基類本身控制,即使對於派生類的基類部分也是如此
        int f3(sneaky s) {return s.prot_mem;}//正確
}

對於 f3 的解釋: pal 是 base 的友元, 所以 pal 能夠訪問 base 物件的成員,這種可訪問性包括了 base 物件內嵌在其派生類物件中的情況。

繼承中的作用域
每個類定義自己的作用域,在這個作用域內我們定義類的成員。當存在繼承關係時,派生類的作用域巢狀在基類的作用域之類。

class quote{
    public:
        quote()=default;
        quote(const string&book,double sales_price):bookNo(book),price(sales_price){}
        string isban() const{return bookno;}
        virtual double net_price(size_t n) const{return n*price;}
        virtual ~quote()=default;
    private:
        string bookno;
    protected:
        double price;
};

class disc_quote:public quote{
    public:
        disc_quote()=default;
        disc_quote(string &book,double price,size_t qty,double disc):
            quote(book,price),quantity(qty),discount(disc){}
        double net_price(size_t) const=0;
    protected:
        size_t quantity=0;
        double discount=0.0;
};

class bulk_quote:public disc_quote{
    public:
        bulk_quote()=default;
        bulk_quote(string &book,double price,size_t qty,double disc):
            disc_quote(book,price,qty,disc){}
        double net_price(size_t) const override;
};
bulk_quote bulk;
cout<<bulk.isbn();

名字 isban 的解析過程:
1、我們通過bulk_quote 的物件呼叫 isban,首先在bulk_quote 中尋找;
2、接下來在 disc_qutoe 中查詢;
3、最後在 quote 中查詢,最終被解析為 quote 中的 isban。
在編譯時進行名字查詢
一個物件、 指標或引用的靜態型別決定了該物件的哪些成員是可見的。 即使, 靜態型別與動態型別可能不一致。 但我們能使用哪些成員仍熱是由靜態型別決定的。搜尋是由靜態型別開始的。

class disc_quote:public quote{
    public:
        pair<size_t,double>discount_policy() const 
        {return {quantity,discount};}
};
bulk_quote bulk;
bulk_quote*bulkp=&bulk;
quote*itemp=&bulk;//靜態型別與動態型別不一致
bulkp->discount_policy(); //正確:
itemp->discount_policy();// 錯誤 搜尋從 quote開始將找不到discount_policy()成員

名字衝突與繼承
派生類也可以重用定義在其直接基類或間接基類中的名字, 此時定義在內層作用域中的名字, 將隱藏定義在外層作用域中的名字。
假定我們呼叫 p->mem()(或者 obj.mem())步驟如下:
1、首先確定 p->mem()(或者 obj.mem())的靜態型別
2、在靜態型別中查詢 mem。如果找不到則依次在直接基類中不斷查詢直至到達繼承鏈的頂端。3、一旦找到了 men,就進行常規的型別檢查,確認本次呼叫是否合法。
4、加入呼叫合法則:如果是虛擬函式且我們通過指著或引用呼叫,編譯器產生的程式碼在執行時確定該執行虛擬函式的哪個版本。若不是虛擬函式(或者我們是通過物件進行的呼叫),則編譯器產生一個常規函式呼叫。

名字查詢先於型別檢查
宣告在內層作用域中的函式不會過載宣告在外層作用域的函式。 如果派生類的成員與基類的某個成員名字相同, 就會隱藏基類成員。(即使形參列表不一致)。一旦名字找到編譯器就不會再繼續查詢。

struct base{
    int memfcn();
};
struct derived:base{
    int memfcn(int);  //隱藏基類的memfcn
};
derived d; base b;
b.memfcn();
d.memfcn(10);
d.memfcn(); //錯誤:引數列表為空的函式被隱藏了
d.base::memfcn();  //正確

虛擬函式與作用域
基類與派生類中的虛擬函式必須有相同的引數列表, 如果不同我們無法通過基類的指標或這引用呼叫派生類的虛函數了。

class base{
    public:
        virtual int fcn();
};
class d1:public base{
    public:
    //隱藏了基類的fcn,這個fcn不是虛擬函式 d1繼承了base::fcn()的定義
        int fcn(int);
        virtual void f2();
};
class d2 :public d1{
    public:
        int fcn(int);    //隱藏d1::fcn(int)
        int fcn();      //覆蓋base虛擬函式
        void f2();      //覆蓋d1的虛擬函式
};
base bobj; d1 d1obj; d2 d2obj;
base *bp1=&bobj, *bp2=&d1obj,*bp3=&d2obj;
bp1->fcn();  //虛呼叫 執行時呼叫 base::fcn()
//虛呼叫 執行時呼叫 base::fcn()  實際繫結的雖然是d1,而d1沒有覆蓋,所以將在執行時解析為base定義的版本
bp2->fcn();  
bp3->fcn();  //虛呼叫 執行時呼叫 d2::fcn

d1 *d1p=&d1obj ; d2 *d2p=&d2obj;
bp2->f2();   //錯誤
d1p->f2();   //虛呼叫執行時呼叫d1::fcn
d2p->f2();   //虛呼叫執行時呼叫d2::fcn

base *p1=&d2obj; d1*p2=&d2obj; d2*p3=&d2obj;
//沒有呼叫虛擬函式,不會發生動態繫結
p1->fcn(42);   //錯誤 
p2->fcn(42);   // 呼叫d1::fcn
p3->fcn(42);   // 呼叫d2::fcn