《隨筆八》—— C++中的“ 多型中的靜態聯編 、動態聯編”
目錄
下面先給出一些程式碼示例,引用出要使用多型的原因:
class Persion { public: void sleep() { cout << "呼叫的是基類中的sleep" << endl; } void breathe() { cout << "呼叫的是基類中的breathe" << endl; } }; class Man :public Persion { public: void breathe() { cout << "呼叫的是派生類中的breathe" << endl; } }; int main() { Man MyMan; Persion *an = &MyMan; an->breathe(); //呼叫基類中的該函式 MyMan.breathe(); MyMan.Persion::breathe(); //呼叫基類中的該函式 Man *s = new Man; s->breathe(); Persion myPersion; myPersion.breathe(); //呼叫基類中的該函式 system("pause"); return 0; } 輸出結果為: 呼叫的是基類中的breathe 呼叫的是派生類中的breathe 呼叫的是基類中的breathe 呼叫的是派生類中的breathe 呼叫的是基類中的breathe
在主函式中我們首先定義了一個Man類的物件myMan, 另一個Persion類的指標變數an,該指標變數指向Man類的物件myMan, 最後利用該指標變數呼叫了 an->breathe(), 大家認為 myMan 實際上是 Man類的物件,應該呼叫 Man類的breathe(), 但結果不是這樣的。
這是因為C++ 編譯器在編譯的時候,要確定每個物件呼叫的函式的地址,這稱為早期繫結。 當Man類的物件myMan 的地址賦給an時,C++編譯器進行了型別轉換, 此時C++編譯器認為變數 an 儲存的就是 Persion 物件的地址。 當在主函式中執行 an->breathe() 時,呼叫的是 Persion 物件的 breathe 函式。
大家可能想呼叫的是 派生類中的 breathe()函式, 這個問題可以使用多型來解決。
下面再看另外一個程式示例:
class father { public: father() { age = 54; cout << "呼叫基類建構函式:" << age << endl; } ~father() { cout << "呼叫基類解構函式:" << endl; } void jump()const { cout << "基類jump" << endl; } void run()const { cout << "基類run" << endl; } protected: int age; }; class son :public father { public: son() { cout << "呼叫派生類建構函式:" << endl; } ~son() { cout << "呼叫派生類解構函式:" << endl; } void math() { cout << "派生類math" << endl; } void jump()const { cout << "派生類jump" << endl; } void run()const { cout << "派生類run" << endl; } }; int main() { { father *pfather = new son; pfather->jump(); pfather->run(); //pfather->math(); 基類指標不可以訪問派生類中的成員 delete pfather; } { cout << endl; son myson; father *pfather = &myson; pfather->jump(); pfather->run(); /*pfather->math();基類指標不可以訪問派生類中的成員, 雖然我們可以把基類指標轉換成派生類指標,但是這樣會容易出錯*/ } { cout << endl; son mmson; mmson.jump(); mmson.father::jump(); } { cout << endl; son *sson = new son; /*假如我們需要訪問派生類中的成員,就要將該指標宣告為son類 */ sson->math(); delete sson; } system("pause"); return 0; } 輸出結果為: 呼叫基類建構函式:54 呼叫派生類建構函式: 基類jump 基類run 呼叫基類解構函式: 呼叫基類建構函式:54 呼叫派生類建構函式: 基類jump 基類run 呼叫派生類解構函式: 呼叫基類解構函式: 呼叫基類建構函式:54 呼叫派生類建構函式: 派生類jump 基類jump 呼叫派生類解構函式: 呼叫基類解構函式: 呼叫基類建構函式:54 呼叫派生類建構函式: 派生類math 呼叫派生類解構函式: 呼叫基類解構函式:
多型
class father
{
public:
father()
{
age = 54;
cout << "呼叫基類建構函式:" << age << endl;
}
~father()
{
cout << "呼叫基類解構函式:" << endl;
}
void jump()const
{
cout << "基類jump" << endl;
}
virtual void run()const
{
cout << "基類run" << endl;
}
protected:
int age;
};
class son :public father
{
public:
son()
{
cout << "呼叫派生類建構函式:" << endl;
}
~son()
{
cout << "呼叫派生類解構函式:" << endl;
}
void math()
{
cout << "派生類math" << endl;
}
void jump()const
{
cout << "派生類jump" << endl;
}
virtual void run()const
{
cout << "派生類run" << endl;
}
};
int main()
{
{
father *pfather = new son; //基類物件的指標指向派生類
pfather->jump(); //訪問基類中的jump() 函式
pfather->run(); //訪問基類中的run()虛擬函式,也就是訪問派生類中的該函式
delete pfather;
}
system("pause");
return 0;
}
輸出結果為:
呼叫基類建構函式:54
呼叫派生類建構函式:
基類jump
派生類run
呼叫基類解構函式:
這個程式跟上面的那個程式的輸出有顯著的區別, 我們在該程式中基類的函式 run() 函式宣告為虛擬函式,派生類中有無virtual關鍵字,都為虛擬函式。 當我們在主函式中使用基類的指標指向 派生類物件時, 使用該指標訪問 run() 成員函式, 訪問的是 派生類中的, 上面的那個程式訪問的是 基類中的run() 成員函式。
那這是為什麼呢?
這是因為不使用virtual 關鍵字之前,C++ 對過載的函式(原型相同,層次不同),使用了靜態聯編, 而使用了 virtual 以後, C++ 則對該函式使用動態聯編。
那什麼是靜態聯編和動態聯編呢?
將一個呼叫函式連結上正確的被調函式, 這一過程叫做函式聯編,一般稱為聯編。
因此在未加virtual 時, 該函式是靜態聯編,即被調函式和主調函式的關係以及它們的記憶體地址是在編譯時都已經確定好的。 程式執行時不在變化, 這樣的好處是速度快,因為執行的時候不用對各個物件的函式進行追蹤, 只需要傳遞引數, 執行確定好的函式並在函式呼叫完畢後清理記憶體即可 。
那麼由靜態聯編支援的多型性稱為編譯時的多型性或者靜態多型性, 也就是說確定同名操作的具體操作物件的過程是在編譯過程中完成的。 在C++中,我們可以使用函式過載 和 運算子過載來實現編譯時的多型性。
由動態聯編支援的多型性稱為執行時的多型性 或者 動態多型性,即確定同名操作的具體操作物件的過程是在執行過程中完成的。 在C++中, 可以使用虛擬函式來實現執行時的多型性。
虛擬函式是實現執行時多型的一個重要方式,是過載的另一種形式, 實現的是動態的過載, 即函式呼叫與函式體之間的聯絡是在執行時建立的,也就是動態聯編。
在編譯時的靜態聯編
在編譯時的靜態聯程式設計序示例:
class A
{
public:
int get()
{
return 1;
}
};
class B :public A
{
public:
int get()
{
return 2;
}
};
int main()
{
A a;
cout << "a的值:" << a.get() << endl;
B b;
cout << "b的值:" << b.get() << endl;
system("pause");
return 0;
}
當函式的引數完全相同,但不屬於同一個類時,為了讓編譯器能正確區分呼叫哪個類的同名函式, 可採用以下兩種方法:
用物件名區別: 在函式名前加上物件名來限制。 如: 物件名. 函式名
用類名和作用域運算子加以區別: 在函式名前加“ 類名 : : ” 來限制, 如 類名 ::函式名。
在上面的程式採用靜態聯編,即在編譯時就處理好了程式中主調函式和被呼叫函式之間的關係。 因此程式碼在編譯時與執行時的效果都是一樣的。
在執行時的靜態聯編
在執行時的靜態聯編程式碼示例:
class A
{
public:
int get()
{
return 1;
}
};
class B :public A
{
public:
int get()
{
return 2;
}
};
int main()
{
A *p = new A;
cout << "輸出p的值為:" << p->get() << endl;
A *pp = new B;
cout << "輸出pp的值為:" << pp->get() << endl;
delete p;
delete pp;
system("pause");
return 0;
}
輸出結果為:
輸出p的值為:1
輸出pp的值為:1
由於靜態聯編的物件與指標的關係在編譯時就已確定, 因此執行時再對它改變也是無效的。
在編譯時的動態聯編
在編譯時的動態聯編的程式碼示例:
class A
{
public:
virtual int get() //宣告虛擬函式
{
return 1;
}
};
class B :public A
{
public:
virtual int get()
{
return 2;
}
};
int main()
{
A a;
cout << "a的值:" << a.get() << endl;
B b;
cout << "b的值:" << b.get() << endl;
cout << "a的值:" << b.A::get() << endl;
system("pause");
return 0;
}
輸出結果為:
a的值:1
b的值:2
a的值:1
上面的程式我們可以看到,儘管我們在基類中使用 virtual 關鍵字宣告 get函式為虛擬函式, 但是我們在主函式中沒有使用基類物件的指標或者引用, 那麼我們就無法實現動態聯編, 那麼該程式還是靜態聯編。
在執行時的動態聯編
在執行時的動態聯編的程式碼示例:
class A
{
public:
virtual int get() //宣告虛擬函式
{
return 1;
}
};
class B :public A
{
public:
virtual int get()
{
return 2;
}
};
int main()
{
{
A *a = new A;
cout << "基類的值:" << a->get() << endl;
delete a;
}
B b;
A *p = &b;
cout << "派生類的值:" << p->get() << endl;
//使用派生類物件和基類物件的指標間接訪問基類中的成員
cout << "基類的值:" << p->A::get() << endl;
cout << "基類的值:" << b.A::get() << endl;
A &ss = b; //基類物件引用派生類物件,實現動態聯編
cout << "派生類的值:" << ss.get() << endl;
cout << "基類的值:" << ss.A::get() << endl;
system("pause");
return 0;
}
輸出結果為:
基類的值:1
派生類的值:2
基類的值:1
基類的值:1
派生類的值:2
基類的值:1
只有在使用基類物件的指標 或者引用時, 才能實現在執行時的動態聯編