C++ OOP

目錄ios

1.概述c++

2.繼承git

2.0 基類github

2.1 派生類編程

2.2 空基類優化(本節不理解tcp

2.3 虛基類ide

2.4 繼承方式函數

2.5 成員名稱查找(不理解)佈局

2.6 C++11新增優化

override

final

3. 動態綁定

3.1 對象模型:虛表和虛指針

3.2 動態綁定的三個條件

3.3 static type和dynamic type區別

3.4 關於this 

4. 虛成員、虛函數

4.1 定義

4.2 調用(重要)

4.3 覆寫(重要)

4.4 協變返回類型

4.5 在構造與析構期間

5. 抽象類 與 純虛類


1.概述


2.繼承

struct Base {
    int a, b, c;
};
// 每一個 Derived 類型對象包含 Base 爲子對象
struct Derived : Base {
    int b;
};
// 每一個 Derived2 類型對象包含 Derived 與 Base 爲子對象
struct Derived2 : Derived {
    int c;
};
  • 從內存的角度:派生類繼承了基類的成員變量(data)。
  • 從多態的角度:能夠重寫基類的成員函數。函數的繼承是繼承父類的調用權

2.0 基類

基類必須已經定義,才能派生。

基類的成員函數定義

  • non-virtual fun 非虛函數:不但願派生類重寫。
  • virtual fun 虛函數 :但願派生類重寫。
  • pure virtual fun 純虛函數 : 派生類必須重寫。

虛析構函數

雖然析構函數是不繼承的,若基類聲明器其析構函數爲 virtual ,則派生的析構函數始終覆寫它

這使得能夠經過指向基類的指針 delete 動態分配的多態類型對象。

class Base {
 public:
    virtual ~Base() { /* 釋放 Base 的資源 */ }
};
 
class Derived : public Base {
    ~Derived() { /* 釋放 Derived 的資源 */ }
};
 
int main()
{
    Base* b = new Derived;
    delete b; // 進行到 Base::~Base() 的虛函數調用
              // 由於它爲虛,故它調用 Derived::~Derived() ,
              // 能釋放派生類的資源,而後遵循一般析構順序
              // 調用 Base::~Base()
}

【重要!】任何基類的析構函數必須爲公開且虛public virtual,或protected受保護且非虛 

若類爲多態(聲明或繼承至少一個虛函數),且其析構函數非虛,會致使資源泄漏。由於派生類的資源未釋放。


2.1 派生類

  • 派生類必須用 :指定父類。 用逗號分開。基類前面能夠加三種訪問說明符之一。
  • 若省略訪問說明符 ,則它對以類關鍵 struct 聲明的類默認爲 public ,對以類關鍵 class 聲明的類爲 private 。
  • 列於 base-clause 的類是直接基類,其基類是間接基類。
  • 同一類不能指定於直接基類多於一次,但同一類能夠既是直接又是間接基類。
  • 每一個直接和間接基類都做爲基類子對象,以實現定義的偏移存在於派生類的對象表示中。
  • 由於空基類優化,空基類一般不會增長派生類對象的大小。
  • 類不能派生本身。
  • 派生類聲明中。不能包含基類列表。
  • 派生類能夠隱式轉換爲基類。

2.2 空基類優化(本節不理解

容許空的基類子對象大小爲零。

爲保證同一類型的不一樣對象地址始終有別,要求任何對象或成員子對象的大小至少爲 1 ,即便該類型是空類類型(即無非靜態數據成員的 class 或 struct )。

然而,基類子對象不受這種制約,並且能夠徹底從對象佈局中被優化掉:

#include <cassert>
 
struct Base {}; // 空類
 
struct Derived1 : Base {
    int i;
};
 
int main()
{
    // 任何空類類型的對象大小至少爲 1
    assert(sizeof(Base) > 0);
 
    // 應用空基優化
    assert(sizeof(Derived1) == sizeof(int));
}

若空基類之一亦爲首個非靜態數據成員的類型或其類型的基類,則禁用空基優化,由於要求同類型二個基類子對象在最終派生類的對象表示中擁有不一樣地址。

這種狀況的典例是 std::reverse_iterator 的樸素實現(派生自空基類 std::iterator ),它保有底層迭代器(亦派生自 std::iterator )爲其首個非靜態數據成員。

#include <cassert>
 
struct Base {}; // 空類
 
struct Derived1 : Base {
    int i;
};
 
struct Derived2 : Base {
    Base c; // Base ,佔用 1 字節,後隨爲 i 的填充
    int i;
};
 
struct Derived3 : Base {
    Derived1 c; // 從 Base 派生,佔用 sizeof(int) 字節
    int i;
};
 
int main()
{
    // 不該用空基優化
    // 基類佔用 1 字節,Base 成員佔用 1 字節,後隨2個填充字節以知足 int 對齊要求
    assert(sizeof(Derived2) == 2*sizeof(int));
 
    // 不該用空基類優化,
    // 基類佔用至少 1 字節加填充以知足首個成員的對齊要求(其對齊要求同 int )
    assert(sizeof(Derived3) == 3*sizeof(int));
}

C++11:對於標準佈局類型 (StandardLayoutType) 要求有空基類優化,以維持指向標準佈局對象的指針,用 reinterpret_cast 轉換後,還指向其首成員,這是標準佈局類型「無擁有非靜態數據成員的基類,且無與其首個非靜態數據成員同類型的基類」的緣由。


2.3 虛基類

【注意】對於每一個指定爲 virtual 的相異基類,最終派生類對象僅含有該類型的一個基類子對象,即便該類在繼承層級中出現屢次(只要它每次都以 virtual 繼承)。

下面的AA對象只有2個B基類子對象,XY共有一個虛B基類,Z有一個非虛B基類。

struct B { int n; };
class X : public virtual B {};//相異基類
class Y : virtual public B {};//相異基類
class Z : public B {};
// 每一個 AA 類型對象擁有一個 X ,一個 Y ,一個 Z 和二個 B :
// 其一是 Z 的基類,另外一者爲 X 與 Y 所共享
struct AA : X, Y, Z {
    void f() {
        X::n = 1; // 修改虛 B 基類子對象的成員
        Y::n = 2; // 修改同一虛 B 基類子對象的成員
        Z::n = 3; // 修改非虛 B 基類子對象的成員
 
        std::cout << X::n << Y::n << Z::n << '\n'; // 打印 223
    }
};

【例子: iostream 】

繼承層級有虛基類的例子之一是標準庫的 iostream 的繼承層級:

std::istream 與 std::ostream 從 std::ios 使用虛繼承派生。 

std::iostream 繼承 std::istream 和 std::ostream ,

故每一個 std::iostream 實例含一個 std::ostream 子對象、一個 std::istream 子對象和僅一個 std::ios 子對象(繼而有一個 std::ios_base )。


(這裏不理解)

全部 虛基類子對象 在 任何 非虛基類子對象 前 初始化,故 只有 最終派生類於其成員初始化器列表調用虛基類的構造函數: 

struct B {
    int n;
    B(int x) : n(x) {}
};
struct X : virtual B 
{ 
    X() : B(1) {} 
};
struct Y : virtual B {
     Y() : B(2) {} 
};
struct AA : X, Y{
    AA() : B(3),X(),Y() {} //逗號分隔基類列表。前面能夠有訪問說明。
};
 
// AA 的默認構造函數調用 X 和 Y 的默認構造函數
// 但這些構造函數不調用 B 的構造函數,由於 B 是虛基類
AA a; // a.n == 3
// X 的默認構造函數調用 B 的構造函數
X x; // x.n == 1

涉及虛繼承時,類成員的非限定名稱查找有特殊規則(有時被引用爲支配規則),見 unqualified_lookup#成員函數定義。 


2.4 繼承方式

公開繼承 public

公開繼承模擬面向對象編程的子類型關係:派生類對象是( IS-A )基類子對象。期待派生類對象的引用和指針,可爲使用期待到其任何基類的引用和指針的代碼所用(見 LSP ),或者爲了 DbC ,派生類應該維護其公開基類的類不變量,不該強化任何其所覆寫的成員函數的前置條件,或弱化任何其後置條件。 

受保護繼承 protected

受保護繼承可用於「受控制的多態」:在派生類的成員中,還有在全部進一步派生類的成員中,派生類是( IS-A )基類:到派生類的引用和指針可用於期待到基類的引用和指針處。

私有繼承 private

私有繼承經常使用於基於策略的設計,由於策略常是空基類,而使用基類能夠啓用靜多態並活用空基類優化.


私有繼承亦可用於實現合成關係基類子對象是派生類對象的實現細節)。成員使用提供更好的封裝,並且一般受到偏好,除非派生類要求訪問基類的受保護成員(包含構造函數)、須要覆寫基類的虛成員、須要基類構造先於或析構後於某其餘基類子對象,須要共享虛基類或須要控制虛基類的構造。實現合成的成員使用亦不可應用於從參數包多重繼承的狀況,或在編譯時經過模板元編程肯定基類身份的狀況。

同受保護繼承,私有繼承亦可用於受控制的多態:在派生類的成員內(但不在進一步派生類內),派生類是( IS-A )基類。

template<typename Transport>
class service : Transport  // 從 Transport 策略私有繼承
{
public:
    void transmit() {
        this->send(...);  // 發送傳輸所提供的任何內容
    }
};
// TCP 傳輸策略
class tcp {
public:
    void send(...);
};
// UDP 傳輸策略
class udp {
public:
    void send(...);
};
 
service<tcp> service(host, port); 
service.transmit(...); // 發送完畢 TCP

2.5 成員名稱查找(不理解)

類成員非限定及限定名稱查找的規則詳細列於名稱查找


2.6 C++11新增

派生類內部必須對全部重定義的虛函數進行聲明,能夠在函數前加上virtual關鍵字,也能夠不加。

只有虛函數才能被覆蓋。簽名要匹配。

爲了方便編譯器找出錯誤。C++經過override和final顯式說明派生類的虛函數,這兩個說明符在語句的最後

override

在成員函數聲明或定義中, override 能夠顯式地指出該函數爲虛函數,覆寫來自基類的虛函數。 

struct A
{
    virtual void foo();
    void bar();
};
 
struct B : A
{
    void foo() const override; // 錯誤: B::foo 不覆寫 A::foo
                               // (簽名不匹配)
    void foo() override; // OK : B::foo 覆寫 A::foo
    void bar() override; // 錯誤: A::bar 非虛
};

final

在虛函數聲明或定義中使用時, final 確保函數爲虛,不可被派生類覆寫

 final 亦可用於聯合體定義,此狀況下它無效(除了 std::is_final 上的結果),由於不能派生聯合體。final 是在用於成員函數聲明或類頭部時有特殊含義的標識符。其餘語境中它非保留並且可用於命名對象或函數。 

struct Base
{
    virtual void foo();
};
 
struct A : Base
{
    void foo() final; // A::foo 被覆寫且是最終覆寫
    void bar() final; // 錯誤:非虛函數不能被覆寫或是 final
};
 
struct B final : A // struct B 爲 final
{
    void foo() override; // 錯誤: foo 不能被覆寫,由於它在 A 中是 final
};
 
struct C : B // 錯誤: B 爲 final
{
};

3. 動態綁定(很是重要)

使用基類的引用或指針,調用一個虛函數,虛函數運行時,形參的版本由實參對象的類型決定。

C++ OOP的關鍵:基類和派生類之間的類型轉換


3.1 對象模型:虛表和虛指針

  •  函數也佔內存,也有地址。虛函數纔有虛指針,虛函數表(裏面都是指向函數的指針。
  •  c調用: 靜態綁定 call+地址。c++調用:動態綁定。指向C的指針p想調用虛函數v1(動態綁定)經過指針找到vptr虛指針,找到vtbl虛函數表,獲得要調用的函數地址。
  •  p->vptr[n]是c語言的描述,n是虛函數在vtbl中的第幾個位置。編譯器在編譯的時候看vfun是第幾個出現的,就肯定了n的值。 
  • 類的內存:父類數據+本身數據+1個或0個虛指針。

  • 容器裏裝的必定是一個指向父類的指針。list<A*>,由於無法肯定形狀的大小因此是指針,並且必須得是父類。
  • 只有虛函數才能被override(c++)。不用像c那樣去判斷類型。由於父類可能加新的子類。

3.2 動態綁定的三個條件

  1. 經過指針或引用調用。
  2. 指針必須向上轉型。由子類轉向父類。
  3. 調用的是虛函數。 

3.3 static type和dynamic type區別

  • 靜態類型在編譯時就被肯定了。它是【變量聲明時的類型】 或【表達式生成的類型】。
  • 動態類型在運行時才知道。它是【變量或表達式表明的內存中的對象的類型】。


3.4 關於this 

  •  this是個指針。也能夠說this指的那個object。
  • main(){derivedclass object;object.func();}至關於調用baseclass::func(&object);&object就是this,一般不寫。

3.5 類型轉換

只有在指針和引用之間纔有類型轉換。在對象之間沒有類型轉換。


4. 虛成員、虛函數

4.1 定義

  • 基類但願該成員在派生類中從新定義,除了構造函數和靜態成員,類中任何成員均可以被virtual聲明爲虛成員。
  • 該函數在派生類中隱式的也是虛函數。

4.2 調用(重要)

若使用到基類的指針或引用處理派生類,則對被覆寫虛函數的調用,將會調用定義於派生類的行爲

若使用有限定名稱查找做用域解決運算符 :: ),調用做用域內部的非虛函數

#include <iostream>
struct Base {
   virtual void f() {
       std::cout << "base\n";
   }
};
struct Derived : Base {
    void f() override { // 'override' 可選
        std::cout << "derived\n";
    }
};
int main()
{
    Base b;
    Derived d;
 
    // 經過引用調用虛函數
    Base& br = b; // br 的類型是 Base&
    Base& dr = d; // dr 的類型也是 Base&
    br.f(); // 打印 "base"
    dr.f(); // 打印 "derived"
 
    // 經過指針調用虛函數
    Base* bp = &b; // bp 的類型是 Base*
    Base* dp = &d; // dp 的類型也是 Base*
    bp->f(); // 打印 "base"
    dp->f(); // 打印 "derived"
 
    // 非虛函數調用
    br.Base::f(); // 打印 "base"
    dr.Base::f(); // 打印 "base"
}

4.3 覆寫(重要)

若某成員函數 vf 在類 Base 中聲明爲 virtual ,且某個直接或間接從 Base 派生的類 Derived 擁有下列幾點與之相同的成員函數聲明

  • 名稱
  • 參數列表(但非返回類型)
  • cv 限定符
  • 引用限定符

則類 Derived 中的此函數亦爲(不管是否於其聲明使用關鍵詞 virtual )並覆寫 Base::vf (不管是否於其聲明使用詞 override)。

要覆寫的 Base::vf 不須要可見(可聲明爲 private ,或用私有繼承繼承)。

class B {
    virtual void do_f(); // 私有成員
 public:
    void f() { do_f(); } // 公開繼承
};
struct D : public B {
    void do_f() override; // 覆寫 B::do_f
};
 
int main()
{
    D d;
    B* bp = &d;
    bp->f(); // 內部調用 D::do_f();
}

對於每一個虛函數,存在最終覆寫者,它在虛函數調用進行時執行。基類 Base 虛成員函數 vf 是最終覆寫者,除非派生類聲明或繼承(經過多重繼承)另外一覆寫 vf 的函數。

struct A { virtual void f(); };     // A::f 爲 virtual
struct B : A { void f(); };         // B::f 覆寫 A::f in B
struct C : virtual B { void f(); }; // C::f 覆寫 A::f in C
struct D : virtual B {}; // D 不引入覆寫者, B::f 在 D 中爲最終
struct E : C, D  {       // E 不引入覆寫者, C::f 在 E 中爲最終
    using A::f; // 非函數聲明,僅令 A::f 能爲查找所見
};
int main() {
   E e;
   e.f();    // 虛調用調用 C::f , e 中的最終覆寫者
   e.E::f(); // 非虛調用調用 A::f ,它在 E 中可見
}

虛函數只能有一個最終覆寫者:

struct A {
    virtual void f();
};
struct VB1 : virtual A {
    void f(); // 覆寫 A::f
};
struct VB2 : virtual A {
    void f(); // 覆寫 A::f
};
// struct Error : VB1, VB2 {
//     // 錯誤: A::f 在 Error 中擁有二個最終覆寫者
// };
struct Okay : VB1, VB2 {
    void f(); // OK :這是 A::f 的最終覆寫者
};
struct VB1a : virtual A {}; // 不聲明覆寫者
struct Da : VB1a, VB2 {
    // Da 中, A::f 的最終覆寫者是 VB2::f
};

擁有同名和相異參數列表的函數不覆寫同名的基類函數,但隱藏它:在非限定名稱查找檢驗派生類的做用域時,查找找到該聲明,且不檢驗基類。

struct B {
    virtual void f();
};
struct D : B {
    void f(int); // D::f 隱藏 B::f (錯誤的參數列表)
};
struct D2 : D {
    void f(); // D2::f 覆寫 B::f (它不可見是沒關係的)
};
 
int main()
{
    B b;   B& b_as_b   = b;
    D d;   B& d_as_b   = d;    D& d_as_d = d;
    D2 d2; B& d2_as_b  = d2;   D& d2_as_d = d2;
 
    b_as_b.f(); // 調用 B::f()
    d_as_b.f(); // 調用 B::f()
    d2_as_b.f(); // 調用 D2::f()
 
    d_as_d.f(); // 錯誤: D 中的查找只找到 f(int)
    d2_as_d.f(); // 錯誤: D 中的查找只找到 f(int)
}

非成員函數和靜態成員函數不能爲虛。

函數模板不能爲虛 virtual 。這隻應用於自身是模板的函數——類模板的常規成員函數能聲明爲虛。

在編譯時替換虛函數的默認實參


4.4 協變返回類型

若函數 Derived::f 覆寫 Base::f ,則其返回類型必須相同或爲協變。若知足全部下列要求,則二個類型爲協變:

  • 二個類型均爲到類的指針或引用(左值或右值)。不容許多級指針或引用。
  • Base::f() 的返回類型中被引用/指向的類,必須是 Derived::f() 的返回類型中被引用/指向的類的無歧義且可直接或間接訪問的基類。
  • Derived::f() 的返回類型必須有相對於 Base::f() 的返回類型的相等或較少的 cv 限定

Derived::f 的返回類型中的類必須是 Derived 自身,或必須是於 Derived::f 聲明點的完整類型

進行虛函數調用時,最終覆寫者的返回類型被隱式轉換成本該調用的被覆寫函數的返回類型:

class B {};
 
struct Base {
    virtual void vf1();
    virtual void vf2();
    virtual void vf3();
    virtual B* vf4();
    virtual B* vf5();
};
 
class D : private B {
    friend struct Derived; // Derived 中, B 是 D 的可訪問基類
};
 
class A; // 前置聲明類是不完整類型
 
struct Derived : public Base {
    void vf1();    // 虛,覆寫 Base::vf1()
    void vf2(int); // 非虛,隱藏 Base::vf2()
//  char vf3();    // 錯誤:覆寫 Base::vf3 ,但有相異而非協變返回類型
    D* vf4();      // 覆寫 Base::vf4() 並用有協變返回類型
//  A* vf5();      // 錯誤: A 是不完整類型
};
 
int main()
{
    Derived d;
    Base& br = d;
    Derived& dr = d;
 
    br.vf1(); // 調用 Derived::vf1()
    br.vf2(); // 調用 Base::vf2()
//  dr.vf2(); // 錯誤: vf2(int) 隱藏 vf2()
 
    B* p = br.vf4(); // 調用 Derived::vf4() 並轉換結果爲 B*
    D* q = dr.vf4(); // 調用 Derived::vf4() 並不轉換結果爲 B*
 
}

4.5 在構造與析構期間

  • 派生類構造函數。首先初始化基類的部分。再按照聲明的順序依次初始化派生類的成員。
  • 每一個類控制它本身的成員初始化過程。(關鍵;要想與類的對象交互。必須使用該類的接口。)

當直接或間接從構造函數或從析構函數調用虛函數(包含在類的非靜態成員函數的構造或析構期間,例如在初始化器列表中),且應用調用的對象是正在構造或析構中的對象,則所調用的函數是構造函數或析構函數的類中的最終覆寫者,而非進一步派生類中的覆寫者。 換言之,在構造和析構期間,進一步派生類不存在。

構建有多分支的複雜類時,在屬於一個分支的構造函數內,多態被限制到該類與其基類:若它得到到其子層級外的基類子對象的指針,且試圖進行虛函數調用(例如經過顯式成員訪問),則行爲未定義:

struct V {
    virtual void f();
    virtual void g();
};
 
struct A : virtual V {
    virtual void f(); // A::f 是 V::f 在 A 中的最終覆寫者
};
struct B : virtual V {
    virtual void g(); // B::g 是 V::g 在 B 中的最終覆寫者
    B(V*, A*);
};
struct D : A, B {
    virtual void f(); // D::f 是 V::f 在 D 中的最終覆寫者
    virtual void g(); // D::g 是 V::g 在 D 中的最終覆寫者
 
    // 注意: A 初始化先於 B
    D() : B((A*)this, this) 
    {
    }
};
 
// B 的構造函數,從 D 的構造函數調用 
B::B(V* v, A* a)
{
    f(); // 對 V::f 的虛調用(儘管 D 擁有最終覆寫者, D 也不存在)
    g(); // 對 B::g 的虛調用,在 B 中是最終覆寫者
 
    v->g(); // v 的類型 V 是 B 的基類,虛調用如前調用 B::g
 
    a->f(); // a 的類型 A 不是 B 的基類,它屬於層級中的不一樣分支。
            // 嘗試經過不一樣分支的虛調用致使未定義行爲,
            // 即便此狀況下 A 已徹底構造
            // (它在 B 前構造,由於它在 D 的基類列表中出現先於 B )
            // 實踐中,對 A::f 的虛調用會試圖使用 B 的虛成員函數表,
            // 由於它在 B 的構造中活躍
}

5. 抽象類 與 純虛類

定義不能被實例化,但能用做基類的抽象類型。 


純虛 (pure virtual) 函數是聲明器擁有下列語法的虛函數

此處序列 = 0 被稱做 pure-specifier ,且當即出現於 declarator 後或於可選的 virt-specifier ( override 或 final )後。

pure-specifier 不能出現於成員函數定義中。

struct Base { virtual int g(); virtual ~Base() {} };
struct A : Base{
    // OK :聲明三個成員虛函數,其二爲純
    virtual int f() = 0, g() override = 0, h();
    // OK :析構函數亦能爲純
    ~A() = 0;
    // 錯誤:純指定符在函數定義上
    virtual int b()=0 {}
};

abstract class 是定義或繼承了至少一個最終覆寫爲 pure virtual 的函數的類。


抽象類用於表示通用概念(例如 Shape 、 Animal ),它可用做具體類(例如 Circle 、 Dog )的基類。

不能建立抽象類的實例。抽象類型不能用做參數類型、函數返回類型或顯式轉換的類型。能夠聲明到抽象類的指針或引用。

struct Abstract {
    virtual void f() = 0; // 純虛
}; // "Abstract" 爲抽象
 
struct Concrete : Abstract {
    void f() override {} // 非純虛
    virtual void g();     // 非純虛
}; // "Concrete" 爲非抽象
 
struct Abstract2 : Concrete {
    void g() override = 0; // 純虛覆寫
}; // "Abstract2" 爲抽象
 
int main()
{
    // Abstract a; // 錯誤:抽象類
    Concrete b; // OK
    Abstract& a = b; // OK :到抽象基類的引用
    a.f(); // 到 Concrete::f() 的虛派發
    // Abstract2 a2; // 錯誤:抽象類( g() 的最終覆寫爲純)
}

能夠提供純虛函數的定義(並且若析構函數爲純虛則必須提供):導出類的成員函數能夠自由地用有限定函數 id 調用虛基類的純虛函數。此定義必須在類體外(函數聲明的語法不容許純虛指定符 = 0 和函數體一塊兒出現)。

從抽象類的構造函數或析構函數進行純虛函數的虛調用是未定義行爲(不管純虛函數是否擁有定義)。

struct Abstract {
    virtual void f() = 0; // 純虛
    virtual void g() {} // 非純虛
    ~Abstract() {
        g(); // OK :調用 Abstract::g()
        // f(); // 未定義行爲!
        Abstract::f(); // OK :非虛調用
    }
};
 
// 純虛函數的定義
void Abstract::f() { std::cout << "A::f()\n"; }
 
struct Concrete : Abstract {
    void f() override {
        Abstract::f(); // OK :調用純虛函數
    }
    void g() override {}
    ~Concrete() {
        g(); // OK :調用 Concrete::g()
        f(); // OK :調用 Concrete::f()
    }
};
相關文章
相關標籤/搜索