1. 程式人生 > >C++之繼承與多型

C++之繼承與多型

在程式設計領域,一個廣泛認可的定義是“一種將不同的特殊行為和單個泛化記號相關聯的能力”。和純粹的面向物件程式設計語言不同,C++中的多型有著更廣泛的含義。除了常見的通過類繼承和虛擬函式機制生效於執行期的動態多型(dynamic polymorphism)外,帶變數的巨集,模板,函式過載,運算子過載,拷貝構造等也允許將不同的特殊行為和單個泛化記號相關聯,由於這種關聯處理於編譯期而非執行期,因此被稱為靜態多型(static polymorphism)。

靜態多型性

1、 函式過載與預設引數

(1)函式過載的實現原理

假設,我們現在想要寫一個函式(如Exp01),它即可以計算整型資料又可以計算浮點數,那樣我們就得寫兩個求和函式,對於更復雜的情況,我們可能需要寫更多的函式,但是這個函式名該怎麼起呢?它們本身實現的功能都差不多,只是針對不同的引數:

int sum_int(int nNum1, int nNum2)

{

    return nNum1 + nNum2;

}

double sum_float(float nNum1, float nNum2)

{

    return nNum1 + nNum2;

}

C++中為了簡化,就引入了函式過載的概念,大致要求如下:

1、    過載的函式必須有相同的函式名

2、    過載的函式不可以擁有相同的引數

2、 運算子過載

運算子過載也是C++多型性的基本體現,在我們日常的編碼過程中,我們經常進行+、—、*、/等操作。在C++中,要想讓我們定義的類物件也支援這些操作,以簡化我們的程式碼。這就用到了運算子過載。

比如,我們要讓一個日期物件減去另一個日期物件以便得到他們之間的時間差。再如:我們要讓一個字串通過“+”來連線另一個字串……

要想實現運算子過載,我們一般用到operator關鍵字,具體用法如下:

返回值  operator 運算子(引數列表)

{

         // code

}

例如:

CMyString Operator +(CMyString & csStr)

{

int nTmpLen = strlen(msString.GetData());

if (m_nSpace <= m_nLen+nTmpLen)

{

char *tmpp = new char[m_nLen+nTmpLen+sizeof(char)*2];

strcpy(tmpp, m_szBuffer);

strcat(tmpp, msString.GetData());

delete[] m_szBuffer;

m_szBuffer = tmpp;

}

}

這樣,我們的函式就可以寫成:

int sum (int nNum1, int nNum2)

{

    return nNum1 + nNum2;

}

double sum (float nNum1, float nNum2)

{

    return nNum1 + nNum2;

}

到現在,我們可以考慮一下,它們既然擁有相同的函式名,那他們怎麼區分各個函式的呢?

那就是通過C++名字改編(C++名字粉碎),,對於過載的多個函式來說,其函式名都是一樣的,為了加以區分,在編譯連線時,C++會按照自己的規則篡改函式名字,這一過程為"名字改編".有的書中也稱為"名字粉碎".不同的C++編譯器會採用不同的規則進行名字改編,例如以上的過載函式在VC6.0下可能會被重命[email protected]@[email protected][email protected]@[email protected]這樣方便聯結器在連結時正常的識別和找到正確的函式。

(2)預設引數

無論是Win系統下的API,還是Linux下的很多系統庫,它們的好多的函式存在許多引數,而且大部分都是NULL,倘若我們有個函式大部分的時候,某個引數都是固定值,僅有的時候需要改變一下,而我們每次呼叫它時都要很費勁的輸入引數豈不是很痛苦?C++提供了一個給引數加預設引數的功能,例如:

double sum (float nNum1, float nNum2 = 10);

我們呼叫時,預設情況下,我們只需要給它第一個引數傳遞引數即可,但是使用這個功能時需要注意一些事項,以免出現莫名其妙的錯誤,下面我簡單的列舉一下大家瞭解就好。

A、 預設引數只要寫在函式宣告中即可。

B、 預設引數應儘量靠近函式引數列表的最右邊,以防止二義性。比如

double sum (float nNum2 = 10,float nNum1);

這樣的函式宣告,我們呼叫時:sum(15);程式就有可能無法匹配正確的函式而出現編譯錯誤。

3.巨集多型

帶變數的巨集可以實現一種初級形式的靜態多型: 
// macro_poly.cpp

#include <iostream>
#include <string>

// 定義泛化記號:巨集ADD
#define ADD(A, B) (A) + (B);

int main()
{
    int i1(1), i2(2);
    std::string s1("Hello, "), s2("world!");
    int i = ADD(i1, i2);                        // 兩個整數相加
    std::string s = ADD(s1, s2);                // 兩個字串“相加”
    std::cout << "i = " << i << "\n";
    std::cout << "s = " << s << "\n";
}
當程式被編譯時,表示式ADD(i1, i2)和ADD(s1, s2)分別被替換為兩個整數相加和兩個字串相加的具體表達式。整數相加體現為求和,而字串相加則體現為連線(注:string.h庫已經過載了。程式的輸出結果符合直覺:
1 + 2 = 3
Hello, + world! = Hello, world! 

4.類中的早期繫結

先看以下的程式碼:

#include<iostream>
using namespace std;
class animal
{
    public:
        void sleep(){
            cout<<"animal sleep"<<endl;
            
        }
    void breathe(){
     cout<<"animal breathe"<<endl;
    }
};
class fish:public animal
{
    public:
        void breathe(){
            cout<<"fish bubble"<<endl;
        }
};

int main()
{
    fish fh;
    animal *pAnimal=&fh;
    pAnimal->breathe();
}

答案是輸出:animal breathe

從編譯的角度
C++編譯器在編譯的時候,要確定每個物件呼叫的函式的地址,這稱為早期繫結(early binding),當我們將fish類的物件fh的地址賦給pAn時,C++編譯器進行了型別轉換,此時C++編譯器認為變數pAn儲存的就是animal物件的地址。當在main()函式中執行pAn->breathe()時,呼叫的當然就是animal物件的breathe函式。

記憶體模型的角度

對於簡單的繼承關係,其子類記憶體佈局,是先有基類資料成員,然後再是子類的資料成員,當然後面講的複雜情況,本規律不一定成立。


我們構造fish類的物件時,首先要呼叫animal類的建構函式去構造animal類的物件,然後才呼叫fish類的建構函式完成自身部分的構造,從而拼接出一個完整的fish物件。當我們將fish類的物件轉換為animal型別時,該物件就被認為是原物件整個記憶體模型的上半部分,也就是圖中的“animal的物件所佔記憶體”。那麼當我們利用型別轉換後的物件指標去呼叫它的方法時,當然也就是呼叫它所在的記憶體中的方法。因此,輸出animal breathe,也就順理成章了。

前面輸出的結果是因為編譯器在編譯的時候,就已經確定了物件呼叫的函式的地址,要解決這個問題就要使用遲繫結(late binding)技術。當編譯器使用遲繫結時,就會在執行時再去確定物件的型別以及正確的呼叫函式。而要讓編譯器採用遲繫結,就要在基類中宣告函式時使用virtual關鍵字(注意,這是必須的,很多學員就是因為沒有使用虛擬函式而寫出很多錯誤的例子),這樣的函式我們稱為虛擬函式。一旦某個函式在基類中宣告為virtual,那麼在所有的派生類中該函式都是virtual,而不需要再顯式地宣告為virtual。

動態多型性

下面我們將上面一段程式碼進行部分修改

#include<iostream>
using namespace std;
class animal
{
	public:
		void sleep(){
			cout<<"animal sleep"<<endl;
			
		}
	virtual void breathe(){
     cout<<"animal breathe"<<endl;
    }
};
class fish:public animal
{
	public:
		void breathe(){
			cout<<"fish bubble"<<endl;
		}
};

int main()
{
	fish fh;
	animal *pAnimal=&fh;
	pAnimal->breathe();
}

  執行結果:fish bubble

編譯器為每個類的物件提供一個虛表指標,這個指標指向物件所屬類的虛表。在程式執行時,根據物件的型別去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在呼叫虛擬函式時,就能夠找到正確的函式。由於pAn實際指向的物件型別是fish,因此vptr指向的fish類的vtable,當呼叫pAn->breathe()時,根據虛表中的函式地址找到的就是fish類的breathe()函式。正是由於每個物件呼叫的虛擬函式都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的。換句話說,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛擬函式。那麼虛表指標在什麼時候,或者說在什麼地方初始化呢?

答案是在建構函式中進行虛表的建立和虛表指標的初始化。還記得建構函式的呼叫順序嗎,在構造子類物件時,要先呼叫父類的建構函式,此時編譯器只“看到了”父類,並不知道後面是否後還有繼承者,它初始化父類物件的虛表指標,該虛表指標指向父類的虛表。當執行子類的建構函式時,子類物件的虛表指標被初始化,指向自身的虛表。

     當fish類的fh物件構造完畢後,其內部的虛表指標也就被初始化為指向fish類的虛表。在型別轉換後,呼叫pAn->breathe(),由於pAn實際指向的是fish類的物件,該物件內部的虛表指標指向的是fish類的虛表,因此最終呼叫的是fish類的breathe()函式。

下面詳細的介紹記憶體的分佈

基類的記憶體分佈情況

對於無虛擬函式的類A:

class A
{
void g(){.....}
};
則sizeof(A)=1;
如果改為如下:
class A
{
public:
    virtual void f()
    {
       ......
    }
    void g(){.....}
}
則sizeof(A)=4! 這是因為在類A中存在virtual function,為了實現多型,每個含有virtual function的類中都隱式包含著一個靜態虛指標vfptr指向該類的靜態虛表vtable, vtable中的表項指向類中的每個virtual function的入口地址
例如 我們declare 一個A型別的object :
    A c;
    A d;
則編譯後其記憶體分佈如下:

從 vfptr所指向的vtable可以看出,每個virtual function都佔有一個entry,例如本例中的f函式。而g函式因為不是virtual型別,故不在vtable的表項之內。說明:vtab屬於類成員靜態pointer,而vfptr屬於物件pointer

繼承類的記憶體分佈狀況
假設程式碼如下:
public B:public A
{
public :
    int f() //override virtual function
    {
        return 3;
    }
};

A c;
A d;
B e;
編譯後,其記憶體分佈如下:

從中我們可以看出,B型別的物件e有一個vfptr指向vtable address:0x00400030 ,而A型別的物件c和d共同指向類的vtable address:0x00400050a

動態繫結過程的實現
    我們說多型是在程式進行動態繫結得以實現的,而不是編譯時就確定物件的呼叫方法的靜態繫結。
    其過程如下:
    程式執行到動態繫結時,通過基類的指標所指向的物件型別,通過vfptr找到其所指向的vtable,然後呼叫其相應的方法,即可實現多型。
例如:
A c;
B e;
A *pc=&e; //設定breakpoint,執行到此處
pc=&c;
此時記憶體中各指標狀況如下:

可以看出,此時pc指向類B的虛表地址,從而呼叫物件e的方法。繼續執行,當執行至pc=&c時候,此時pc的vptr值為0x00420050,即指向類A的vtable地址,從而呼叫c的方法。

對於虛擬函式呼叫來說,每一個物件內部都有一個虛表指標,該虛表指標被初始化為本類的虛表。所以在程式中,不管你的物件型別如何轉換,但該物件內部的虛表指標是固定的,所以呢,才能實現動態的物件函式呼叫,這就是C++多型性實現的原理。

   需要注意的幾點
   總結(基類有虛擬函式):
     1、每一個類都有虛表。
     2、虛表可以繼承,如果子類沒有重寫虛擬函式,那麼子類虛表中仍然會有該函式的地址,只不過這個地址指向的是基類的虛擬函式實現。如果基類3個虛擬函式,那麼基類的虛表中就有三項(虛擬函式地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛擬函式,那麼虛表中的地址就會改變,指向自身的虛擬函式實現。如果派生類有自己的虛擬函式,那麼虛表中就會新增該項。
     3、派生類的虛表中虛擬函式地址的排列順序和基類的虛表中虛擬函式地址排列順序相同。

下面想將虛擬函式和純虛擬函式做個比較

虛擬函式

 引入原因:為了方便使用多型特性,我們常常需要在基類中定義虛擬函式。

  純虛擬函式
 引入原因:為了實現多型性,純虛擬函式有點像java中的介面,自己不去實現過程,讓繼承他的子類去實現。

    在很多情況下,基類本身生成物件是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理。 這時我們就將動物類定義成抽象類,也就是包含純虛擬函式的類
    純虛擬函式就是基類只定義了函式體,沒有實現過程定義方法如下

  virtual void Eat() = 0; 直接=0 不要 在cpp中定義就可以了 
虛擬函式和純虛擬函式的區別
1虛擬函式中的函式是實現的哪怕是空實現,它的作用是這個函式在子類裡面可以被過載,執行時動態繫結實現動態
純虛擬函式是個介面,是個函式宣告,在基類中不實現,要等到子類中去實現
2 虛擬函式在子類裡可以不過載,但是虛擬函式必須在子類裡去實現。

類的多繼承

一個類可以從多個基類中派生,也就是說:一個類可以同時擁有多個類的特性,是的,他有多個基類。這樣的繼承結構叫作“多繼承”,最典型的例子就是沙發-床了:

SleepSofa類繼承自Bed和Sofa兩個類,因此,SleepSofa類擁有這兩個類的特性,但在實際編碼中會存在如下幾個問題。

a)         SleepSofa類該如何定義?

Class SleepSofa : public Bed, public Sofa

{

       ….

}

 構造順序為:Bed  sofa  sleepsofa (也就是書寫的順序)

b)        Bed和Sofa類中都有Weight屬性頁都有GetWeight和SetWeight方法,在SleepSofa類中使用這些屬性和方法時,如何確定呼叫的是哪個類的成員?

可以使用完全限定名的方式,比如:

Sleepsofa objsofa;

Objsofa.Bed::SetWeight(); // 給方法加上一個作用域,問題就解決了。

虛繼承

倘若,我們定義一個SleepSofa物件,讓我們分析一下它的構造過程:它會構造Bed類和Sofa類,但Bed類和Sofa類都有一個父類,因此Furniture類被構造了兩次,這是不合理的,因此,我們引入了虛繼承的概念。

class Furniture{……};

class Bed : virtual public Furniture{……}; // 這裡我們使用虛繼承

class Sofa : virtual public Furniture{……};// 這裡我們使用虛繼承

class sleepSofa : public Bed, public Sofa {……};

                     這樣,Furniture類就之構造一次了……

總結下繼承情況中子類物件的記憶體結構:

單繼承情況下子類例項的記憶體結構

假設我們有這樣的一個類: class Base {      public:             virtual void f() { cout << "Base::f" << endl; }             virtual void g() { cout << "Base::g" << endl; }             virtual void h() { cout << "Base::h" << endl; } }; 按照上面的說法,我們可以通過Base的例項來得到虛擬函式表。 下面是實際例程:           typedef void(*Fun)(void);             Base b;             Fun pFun = NULL;             cout << "虛擬函式表地址:" << (int*)(&b) << endl;             cout << "虛擬函式表 — 第一個函式地址:" << (int*)*(int*)(&b) << endl;             // Invoke the first virtual function              pFun = (Fun)*((int*)*(int*)(&b));             pFun(); 實際執行經果如下:(Windows XP+VS2003,  Linux 2.6.22 + GCC 4.1.3) 虛擬函式表地址:0012FED4 虛擬函式表 — 第一個函式地址:0044F148 Base::f 通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛擬函式表的地址,然後,再次取址就可以得到第一個虛擬函式的地址了,也就是Base::f(),這在上面的程式中得到了驗證(把int* 強制轉成了函式指標)。通過這個示例,我們就可以知道如果要呼叫Base::g()和Base::h(),其程式碼如下:             (Fun)*((int*)*(int*)(&b)+0);  // Base::f()             (Fun)*((int*)*(int*)(&b)+1);  // Base::g()             (Fun)*((int*)*(int*)(&b)+2);  // Base::h() 這個時候你應該懂了吧。什麼?還是有點暈。也是,這樣的程式碼看著太亂了,用下圖解釋一下。如下所示:

(1)一般繼承(無虛擬函式覆蓋)

假設有如下所示的一個繼承關係:

在這個繼承關係中,子類沒有過載任何父類的函式。那麼,在派生類的例項中,其虛擬函式表如下所示:  對於例項:Derive d; 的虛擬函式表如下: 我們可以看到下面幾點: 1)虛擬函式按照其宣告順序放於表中。 2)父類的虛擬函式在子類的虛擬函式前面。

(2)一般繼承(有虛擬函式覆蓋)

在這個類的設計中,假設只覆蓋了父類的一個函式:f()。那麼,對於派生類的例項,其虛擬函式表會是下面的一個樣子

我們從表中可以看到下面幾點, 1)覆蓋的f()函式被放到了虛表中原來父類虛擬函式的位置。 2)沒有被覆蓋的函式依舊。 這樣,我們就可以看到對於下面這樣的程式,             Base *b = new Derive();             b->f(); 由b所指的記憶體中的虛擬函式表的f()的位置已經被Derive::f()函式地址所取代,於是在實際呼叫發生時是Derive::f()被呼叫了。這就實現了多型。 在單繼承下,對應於例程:

class A

{

public:

    A(){m_A = 0;}

    virtual fun1(){};

    int m_A;

};

class B:public A

{

public:

    B(){m_B = 1;}

    virtual fun1(){};

    virtual fun2(){};

    int m_B;

};

int main(int argc, char* argv[])

{

    B* pB = new B;

       return 0;

}

則在VC6.0下的記憶體分配圖:

在該圖中,子類只有一個虛擬函式表,與以上的兩種情況向符合。

多繼承情況下子類例項的記憶體結構(非虛繼承)

(1)多重繼承(無虛擬函式覆蓋)

假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函式:

對於子類例項中的虛擬函式表,是下面這個樣子:

我們可以看到: 1)  每個父類都有自己的虛表。 2)  子類的成員函式被放到了第一個父類的表中。(所謂的第一個父類是按照宣告順序來判斷的)

(2)多重繼承(有虛擬函式覆蓋)

下圖中,我們在子類中覆蓋了父類的f()函式。

下面是對於子類例項中的虛擬函式表的圖:

我們可以看見,三個父類虛擬函式表中的f()的位置被替換成了子類的函式指標。這樣,我們就可以任一靜態型別的父類來指向子類,並呼叫子類的f()了。如:             Derive d;             Base1 *b1 = &d;             Base2 *b2 = &d;             Base3 *b3 = &d;             b1->f(); //Derive::f()             b2->f(); //Derive::f()             b3->f(); //Derive::f()             b1->g(); //Base1::g()             b2->g(); //Base2::g()             b3->g(); //Base3::g()

在多繼承(非虛繼承)情況下,對應於以下例程式:

#include <stdio.h>

class A

{

public:

    A(){m_A = 1;};

    ~A(){};

    virtual int funA(){printf("in funA\r\n"); return 0;};

    int m_A;

};

class B

{

public:

    B(){m_B = 2;};

    ~B(){};

    virtual int funB(){printf("in funB\r\n"); return 0;};

    int m_B;

};

class C

{

public:

    C(){m_C = 3;};

    ~C(){};

    virtual int funC(){printf("in funC\r\n"); return 0;};

    int m_C;

};

class D:public A,public B,public C

{

public:

    D(){m_D = 4;};

    ~D(){};

    virtual int funD(){printf("in funD\r\n"); return 0;};

    int m_D;

};

則在VC6.0下的記憶體分配圖:

從該圖中可以看出,此時子類中確實有三個來自於父類的虛表。

多繼承情況下子類例項的記憶體結構(存在虛繼承)

在虛繼承下,Der通過共享虛基類SuperBase來避免二義性,在Base1,Base2中分別儲存虛基類指標,Der繼承Base1,Base2,包含Base1, Base2的虛基類指標,並指向同一塊記憶體區,這樣Der便可以間接存取虛基類的成員,如下圖所示:

class SuperBase

{

public:

    int m_nValue;

    void Fun(){cout<<"SuperBase1"<<endl;}

    virtual ~SuperBase(){}

};

class Base1:  virtual public SuperBase

{

public:

virtual ~ Base1(){}

};

class Base2:  virtual public SuperBase

{

public:

virtual ~ Base2(){}

};

class Der:public Base1, public Base2

{

public:

virtual ~ Der(){}

};

void main()

{

cout<<sizeof(SuperBase)<<sizeof(Base1)<<sizeof(Base2)<<sizeof(Der)<<endl;

}

1) GCC中結果為8, 12, 12, 16

解析:sizeof(SuperBase) = sizeof(int) + 虛擬函式表指標

sizeof(Base1) = sizeof(Base2) = sizeof(int) + 虛擬函式指標 + 虛基類指標

sizeof(Der) = sizeof(int) + Base1中虛基類指標 + Base2虛基類指標 + 虛擬函式指標 

GCC共享虛擬函式表指標,也就是說父類如果已經有虛擬函式表指標,那麼子類中共享父類的虛擬函式表指標空間,不在佔用額外的空間,這一點與VC不同,VC在虛繼承情況下,不共享父類虛擬函式表指標,詳見如下。

2)VC中結果為:8, 16, 16, 24

 解析:sizeof(SuperBase) = sizeof(int) + 虛擬函式表指標

sizeof(Base1) = sizeof(Base2) = sizeof(int) + SuperBase虛擬函式指標 + 虛基類指標 + 自身虛擬函式指標

sizeof(Der) = sizeof(int) + Base1中虛基類指標 + Base2中虛基類指標 + Base1虛擬函式指標 + Base2虛擬函式指標 + 自身虛擬函式指標

 如果去掉虛繼承,結果將和GCC結果一樣,A,B,C都是8,D為16,原因就是VC的編譯器對於非虛繼承,父類和子類是共享虛擬函式表指標的。

 (1)  部分虛繼承的情況下子類例項的記憶體結構:

#include "stdafx.h"

class A

{

public:

  A(){m_A = 0;};

  virtual funA(){};

  int m_A;

};

class B

{

public:

  B(){m_B = 1;};

  virtual funB(){};

  int m_B;

};

class C

{

public:

  C(){m_C = 2;};

  virtual funC(){};

  int m_C;

};

class D:virtual public A,public B,public C

{

public:

    D(){m_D = 3;};

    virtual funD(){};

    int m_D;

};

int main(int argc, char* argv[])

{

    D* pD = new D;

       return 0;

}

(2)全部虛繼承的情況下,子類例項的記憶體結構

class A

{

public:

    A(){m_A = 0;}

    virtual funA(){};

    int m_A;

};

class B

{

public:

    B(){m_B = 1;}

    virtual funB(){};

    int m_B;

};

class C:virtual public A,virtual public B

{

public:

    C(){m_C = 2;}

    virtual funC(){};

    int m_C;

};

int main(int argc, char* argv[])

{

    C* pC = new C;

       return 0;

}

 

(3) 菱形結構繼承關係下子類例項的記憶體結構

class A

{

public:

    A(){m_A = 0;}

    virtual funA(){};

    int m_A;

};

class B :virtual public A

{

public:

    B(){m_B = 1;}

    virtual funB(){};

    int m_B;

};

class C :virtual public A

{

public:

    C(){m_C = 2;}

    virtual funC(){};

    int m_C;

}; 

class D: public B, public C

相關推薦

C++繼承

在程式設計領域,一個廣泛認可的定義是“一種將不同的特殊行為和單個泛化記號相關聯的能力”。和純粹的面向物件程式設計語言不同,C++中的多型有著更廣泛的含義。除了常見的通過類繼承和虛擬函式機制生效於執行期的動態多型(dynamic polymorphism)外,帶變數的巨集,模板,函式過載,運算子過載,拷貝構造等

小白學python繼承_學習筆記

本文以廖雪峰的官方網站為參考來學習python的。其學習連結為廖雪峰小白學python教程。 本文是學習到python的繼承與多型。參考連結廖雪峰python繼承與多型。 class Animal(object): def run(self): print('Anim

C++【繼承

兩個類間的關係:組合與繼承 繼承:可以使用現有類的所有功能,並在無需編寫原來的類的情況下對這些功能進行擴充套件。 通過繼承建立的新類稱為“子類”或“派生類”。   被繼承的類稱為“基類”,“父類”或“超類”。 繼承的作用:程式碼的複用 訪問許可權

C++】繼承

物件模型:     物件中成員變數在記憶體中的佈局形式。     面向物件程式設計的核心思想是封裝(資料抽象)、繼承(程式碼複用)和多型(動態繫結)。 1.通過使用資料抽象,我們可以將類的介面與實現分離; 2.使用繼承,可以定義相似的型別並對其相似關係建模; 3.使用動態繫

28、不一樣的C++系列--繼承

繼承與多型 父子間的同名衝突 首先來看一段程式碼: #include <iostream> #include <string> using namespace std; class Parent { public:

C++的繼承

浪費了“黃金五年”的Java程式設計師,還有救嗎? >>>   

C#】 封裝、繼承

我們知道封裝、繼承和多型是面向物件方法設計中的三大基本特性,下面將具體講解這三個特性的具體表現及意義。 #一、封裝 ##1、說明   從字面意思上看,封裝就是打包的意思,將什麼包裝起來,專業一點就是資訊的隱藏,將物件的屬性和方法打包成一個相對獨立的單位,儘可能隱蔽物件的內部細

Python3 C# 面向物件繼承

Python的多繼承最好是當C#或者Java裡面的介面使用,這樣結構不會混亂( 特殊情況除外) 來個例子: class Animal(object): pass class Flyable(object): """飛的方法""" pass class Ru

C++設計模式-繼承影響耦合性(最基礎的簡單工廠模式小例項)

繼承與多型影響耦合性(最基礎的簡單工廠模式小例項) 原理: 通過繼承和虛擬函式的方式修改某個子類對應函式的功能; 通過簡單工廠模式到底例項化誰; 如果要增加複雜的運算只有增加響應的子類,以及工廠的分支即可;   程式執行截圖如下:   目錄結構如

Java路:類的封裝、繼承

面向物件有三大特點:封裝性、繼承性和多型性。 一、封裝 1、封裝的含義 封裝 (Encapsulation)是將描述某類事物的資料與處理這些資料的函式封裝在一起,形成一個有機整體,稱為類。 類所具有的封裝性可使程式模組具有良好的獨立性與可維護性,這對大型程式的開發是特別重要的

Java面向物件執行緒綜合實驗(一)封裝、繼承

編寫一個程式,實現檔案管理系統中的使用者管理模組。要求模組中實現使用者的模擬登入過程。通過使用者輸入,獲取使用者名稱和口令;與事先記錄在程式中的使用者資訊進行對比,通過口令驗證後才能使用系統。使用者分為系統管理人員、檔案錄入人員,檔案瀏覽人員三類,相關類圖如下所示。 (1)要求在使用者類中

C++總結5——繼承【轉】

一、繼承  1.C++的繼承  繼承有3種形式:私有繼承、保護繼承、公有繼承,預設的繼承方式是私有繼承。通常使用 public 繼承。 不論哪種繼承方式,派生類都是顯示的繼承類基的保護成員變數和函式和公有成員變數和函式,繼承方式只是限定在派生類中這兩種成員變數的

C++總結6——繼承的筆試題【轉】

1————————————————— #include <iostream> using namespace std; class Base { public:     Base(int data):_ma(data)       {         co

c/c++ 繼承 引用有的時候並不能達到的效果

繼承與多型 引用有的時候並不能達到多型的效果 問題:c++ primer 第五版說,只有指標和引用呼叫虛擬函式時才會發生動態繫結(多型)。實踐一下,發現引用有的時候不能發生多型繫結(多型)。 下面的例子,父類是Quote,在Quote裡定義了一個虛擬函式debug,用來打印出各自成員的值。2個子類Bulk

c/c++ 繼承 繼承中的public, protected, private

問題:類B私有繼承類A,類A有個protected成員,那麼在類B的成員函式裡是否可以使用類A的protected成員? 可以使用。 估計有的同學說不對吧,類B都私有繼承了類A了,怎麼還能訪問類A的protect成員呢?請看下面的例子。在類Pri_derv的f()成員函式裡是可以方位父類Base的pub_

c/c++ 繼承 靜態呼叫動態呼叫

靜態呼叫,與動態呼叫。 #include <iostream> class Base{ public: virtual int fcn(){ std::cout << "base fcn()" << std::endl; } }; class D1 :

JAVA基礎封裝、繼承

封裝、繼承、多型 1、封裝:隱藏實現細節,明確標識出允許外部使用的所有成員函式和資料項,從而防止程式碼或資料被破壞。 2、繼承:子類繼承父類,擁有父類的所有功能,並且可以在父類的基礎上進行擴充套件。實現了程式碼的重用性。子類和父類是相容的。 3、多型:一個介面有多個子類或實現類,

c/c++ 繼承 文字查詢的小例子(智慧指標版本)

為了更好的理解繼承和多型,做一個文字查詢的小例子。 介面類:Query有2個方法。 eval:查詢,返回查詢結果類QueryResult rep:得到要查詢的文字 客戶端程式的使用方法: //查詢包含Daddy的文字所在的行 Query q("Daddy"); //查詢不包含Alice的

c/c++ 繼承 文字查詢的小例子(非智慧指標版本)

問題:在上一篇繼承與多型 文字查詢的小例子(智慧指標版本)在Query類裡使用的是智慧指標,只把智慧指標換成普通的指標,並不新增拷貝構造方法,會發生什麼呢? 執行時,程式碼崩掉。 分析下面一行程式碼: Query qb = ~Query("Alice"); 1,首先呼叫Query(string)的建構

C++繼承(一)

一、繼承相關概念   1.繼承:繼承是面向物件複用的重要手段。通過繼承定義一個類,繼承是型別之間的關係建模,共享共有的東西,實現各自本質不同的東西。     2.成員訪問限定符與繼承的關係