1、虛函數的語義html
C++中的函數默認是不會出發動態綁定的,要觸發C++的動態綁定機制,從而實現運行時多態,須要在使咱們的程序知足如下兩個條件:第一是必須使用基類類型的指針或者引用來進行函數調用的操做;第二是隻有指定爲虛函數的成員函數才能進行動態綁定。ios
從派生類到基類的轉換編程
有如下代碼段:緩存
double print_total(const Item_base& ,size_t); Item_base item; print_total(item,10); Item_base *p=&item; Bulk_item bulk; print_total(bulk,10); p=&bulk;
這段代碼使用同一基類類型的指針指向基類類型的對象和派生類類型的對象,還傳遞基類類型和派生類類型的對象來調用須要基類類型引用的函數。由於可使用基類類型的指針或引用來引用派生類對象,因此使用基類類型的引用或指針時,不知道指針或者引用所綁定的對象的類型。不管實際對象具備什麼類型,編譯器都把它看成是基類類型對象。安全
能夠在運行時肯定virtual函數的調用多線程
對象的實際類型可能不一樣於該對象引用或指針的靜態類型,這是C++中動態綁定的關鍵。經過指針或者引用調用虛函數時,編譯器將生成代碼,在運行時肯定調用哪一個函數,被調用的是與動態類型相對應的函數。併發
有如下代碼段:函數
void print_total(ostream &os,const Item_base &item,size_t n) { os<<」ISBN:」<<item.book()<<item.net_price(n)<<endl; }
由於item形參是一個引用且net_price是一個虛函數,item.net_price(n)所調用的net_price版本取決於在運行時綁定到item形參item的實參類型。佈局
Item_base base; Bulk_item derived; print_total(cout,base,10); print_total(cout,derived,10);
很明顯,第一個函數調用基類版本,第二個函數調用派生類版本。引用和指針的動態類型和靜態類型能夠不一樣,這是C++支持多態性的基礎。性能
在編譯時肯定非virtual調用
非虛函數老是在編譯時根據調用該函數的對象,引用或指針的類型而肯定的。調用的函數版本和該對象,引用或指針的類型保持一致。
虛函數與默認實參
虛函數能夠有默認實參。默認實參將在編譯時肯定,若是一個調用省略了具備默認值的實參,則所用的值由調用該函數的類型定義,與對象的動態類型無關。默認實參的值將與調用該虛函數的指針或引用的類型保持一致。在同一個虛函數中的基類版本和派生類版本中使用不一樣的默認實參幾乎老是會引發錯誤,由於C++有動態綁定的功能。
2、虛函數表的實現機制
虛函數機制的實現是經過一張叫作虛函數表實現的。虛函數表簡單的來講是一張類的虛函數的地址表,它就像地圖同樣,指明瞭實際所應該調用的函數。C++編譯器應該保證虛函數表的指針存在於對象實例中最前面的位置。藉此能夠由對象實例獲得虛函數表的地址,而後遍歷其中的函數指針,調用對應的函數。
對於下面的代碼段,咱們來詳述虛函數表的實現:
class Base { public: virtual void f(){cout<<」Base:f」<<endl;} virtual void f(){cout<<」Base:f」<<endl;} virtual void f(){cout<<」Base:f」<<endl;} }
因爲虛函數主要是經過繼承來實現多態的,因此接下來咱們從不一樣的繼承方法來研究虛函數表,部份內容來自他人博客。在文章最後會給出參考的博客連接:
通常繼承:
由簡單的類圖能夠發現,子類沒有重載父類的函數,那麼在派生類實例中,其虛函數表以下,注意是在派生類實例的前提下。
虛函數按照其聲明順序存放在虛函數表中,父類的虛函數在子類的虛函數前面。
通常繼承,有函數重載;
子類重載了基類的f函數,咱們看看虛函數表發生了什麼變化:
覆蓋的f函數被放到了虛函數表中原來基類的虛函數的位置,其餘的虛函數的位置保持不變。
Base *b=new Derive();
b->f();
因爲b綁定了派生類對象,因此b所指的內存中的虛函數表的f的位置已經被派生類的函數地址所代替,因而實際調用,派生類的f函數被調用了。
多重繼承;
派生類實例對應的虛函數表以下:
每一個基類的都有本身的虛函數表,派生類的成員函數被放到了第一個基類的虛函數表中。
多重繼承,有重載的時候:
派生類覆蓋了基類的f函數,此時虛函數表變爲:
三個基類的虛函數表中的f函數的位置被替換爲了派生類的f函數的地址。這樣咱們可使用任一基類的對象的指針或引用來調用派生類對象的
3、線程安全的C++代碼和內聯函數的討論
若是咱們要開發一個線程安全的string類,使得這個類能夠在win32環境下被併發線程安全的使用。在這種狀況下,有多種用戶模式級的同步方法可使用。C++中的同步機制。分爲用戶模式的線程同步和內核對象的線程同步兩大類。用戶模式的同步方法,有原子訪問和臨界區等。內核對象的線程同步主要由事件,等待定時器,信號量等構成。使用時必須將線程從用戶模式切換到內核模式。
臨界區:經過對多線程的串行化來訪問公共資源或一段代碼。
臨界區的兩個操做原語:EnterCriticalSection()進去臨界區,LeaveCriticalSection()離開臨界區。只能用來同步本進程內的線程,而不能用來同步多個進程中的線程。
互斥量:爲協調對一個共享資源的單獨訪問而設計的。
互斥對象只有一個,所以在任何狀況此共享資源都不會同時被多個線程訪問,互斥量比臨界區複雜,由於使用互斥能夠實如今不一樣的應用程序的線程之間實現資源的安全共享。
CreateMutex() 建立一個互斥量
OpenMutex()打開一個互斥量
ReleaseMutex()釋放互斥量
WaitForSinglObjects()等待互斥量。
信號量:爲控制一個具備有限數量的用戶資源而設計。
信號量容許多個線程同時使用共享資源,它指出了同時訪問共享資源的的線程的最大數目。
事件:用來通知線程有一些事情已經發生,從而啓動後繼任務的開始。
線程局部存儲 (TLS),同一進程中的全部線程共享相同的虛擬地址空間。不一樣的線程中的局部變量有不一樣的副本,可是static和globl變量是同一進程中的全部線程共享的。使用TLS技術能夠爲static和globl的變量,根據當前進程的線程數量建立一個array,每一個線程能夠經過array的index來訪問對應的變量,這樣也就保證了static和global的變量爲每個線程都建立不一樣的副本。
瞭解了C++的線程同步方法,咱們有如下三種設計方法能夠實現線程安全的string 類:
硬編碼:
能夠從string類中派生出三個獨立類:CriticalSectionString、MutexString和SemaphoreString。每一個類實現各自的同步機制。
繼承:
能夠派生出單一的ThreadSafeString類,它包含了指向Locker對象的指針。在運行期間經過多態機制選擇特定的同步機制。
模板:
基於模板的string類,該類由Locker類型參數化後獲得。
這裏着重介紹基於模板的方法,避免繼承實現時因爲虛函數調用lock()和unlock()僅在執行期間解析,所以不能實現內聯。
template<class LOCKER> class ThreadSafeString:public string{ public: ThreadSafeString(const char *s):string(s){} int length(); private: LOCKER lock; } length函數的實現策略: template<class LOCKER> inline int ThreadSafeString<LOCKER>::length() { lock.lock(); int len=string::length(); lock.unlock(); return len; }
在具體調用的時候,只要傳入相應的參數如CriticalSectionString或者MutexLock便可。這樣的作的好處了是避免了對lock和unlock的虛函數調用,編譯器能夠解析這兩個虛函數並內聯他們。虛函數的代價在於沒法內聯函數調用,由於虛函數的調用實在運行時動態綁定的。惟一潛在的問題的效率問題是從內聯獲取的速度。可是對於內聯對性能的貢獻,咱們還須要重新討論,下面對內聯是否能對效率有貢獻作一個簡單的討論,後續專題中會對內聯進行詳細的解釋。
內聯的做用:編譯時就能夠將這個函數的拷貝直接放在每一個使用這個函數的地方,避免函數 調用的發生。
內聯函數會提升效率嗎?
:不必定。
:首先須要問清你所指的效率是什麼?是程序體積、內存佔用、執行時間仍是開發速度仍是編譯時間。
程序體積:若是編譯器爲了執行函數調用而不得不生成的代碼的體積比內聯函數的體積還小,此時會減少車工虛體積。
內存佔用:對內存的使用幾乎沒有影響或者有不多影響。
執行時間:若是這個函數不是被頻繁調用的時候,整個程序的執行時間一般不會有明顯的改善,有可能還會拔苗助長,若是內聯增長了函數調用的體積,它會下降調用者的引用局部性,若是調用者的內部指令循環再也不和高速緩存的大小想匹配,整個程序的執行時間實際上會下降,大多數程序的瓶頸在與IO,帶寬。
開發速度和編譯時間:被內聯的代碼必須對調用者可見,調用者必須依賴與被內聯代碼的內部細節,這就增長了模塊之間的耦合性。普通函數被修改時只須要從新連接便可,內聯函數被修改時,必需要從新編譯。
4、虛繼承和對象類型探索
學習了上述的理論後,這裏區分幾個概念問題:
靜態成員函數不能是虛函數,爲何?
由於靜態成員函數是在編譯時肯定的,爲這個函數分配共享的內存位置。而虛函數通常是用於C++的多態中是要進行動態綁定的。是在運行時肯定的。
內聯函數也不能是虛函數,由於內聯函數也是在編譯時肯定的。
在編譯時就可以肯定哪一個重載函數被調用的狀況叫作先期聯編,而在系統運行時,可以根據其類型肯定哪一個重載的成員函數被調用,則成爲多態性,叫作滯後聯編。
下面來學習一下虛繼承,先來一個簡單虛繼承的實例程序:
class A: { public: void fun(); protected: int a; }; class B: virtual public A { protected: int b; }; class C: virtual pulic A { protected: int c; }; class D: public B, public C { public: int g(); protected: int d; };
對於上述代碼,不一樣繼承路徑上的虛基類子對象在派生類中被合併成一個子對象了。在內存中,虛基類子對象是村放在派生類對象的所佔內存塊的尾部。
虛基類的構造函數:
初始化派生類的對象的時候,派生類的構造函數須要調用基類的構造函數。因爲派生類對象中只有一個虛基類子對象,因此虛基類的構造函數只能被調用一次。若是繼承層次很深,那麼把真正建立對象的類稱爲最派生類。虛基類子對象是由最派生類經過調用虛基類的構造函數進行初始化的。若是一個派生類有一個直接或簡間接的虛基類,那麼派生類的構造函數成員初始化列表中必須列出對虛基類構造函數的調用,若是沒有列出,則表示使用該虛基類的缺省構造函數來初始化派生類對象中 的虛基類子對象。
從虛基類直接或間接繼承的派生類的成員初始化列表中必須列出對該虛基類構造函數的調用,可是隻有真正用與建立該對象的那個最派生類的構造函數纔會真正調用虛基類的構造函數。而該派生類的基類中所列出的對這個虛基類的構造函數的調用在實際執行中被忽略,這樣就保證對虛基類的子對象只初始化一次。
C++又規定:在一個成員初始化列表中同時出現對虛基類和非虛基類的構造函數的調用時,則虛基類的構造函數限於非虛基類的構造函數被執行。因爲只有在最派生生中才會真正調用虛基類的構造函數,因此在虛基類中通常不要生命任何數據成員,避免某些上層子類獲得的某些虛基類子對象不是本身真正須要的。因此虛基類通常作爲接口生命。
關於C++的對象類型的構造,將會在一些高級主題中提到。
5、習題自測
有以下代碼,分析其執行結果:
#include "stdafx.h" #include "stdio.h" #include "string.h" class Father { public: name() {printf("father name\n");}; virtual call() {printf("father call\n");}; }; class Son: public Father { public: name() {printf("Son name\n");}; virtual call() {printf("Son call\n");}; }; main() { Son *Son1=new Son(); Father *father1=(Father *)Son1; father1->call(); father1->name(); ((Son *)(father1))->call(); ((Son *)(father1))->name(); Father *f2=new Father(); Son *s2=(Son*)f2; s2->call(); s2->name(); ((Father *)(s2))->call(); ((Father *)(s2))->name(); }
虛函數的調用是經過虛函數指針調用,若是new的對象是Son的,則無論他轉換成什麼指針,他的指針都是Son內部的,與指針類型無關,只與指針地址有關。
非虛函數的調用則是由指針類型決定的。
執行結果:
son call
:father1指向的是son1的指針,在虛函數表中位置不變。
father name
:son1指針轉換爲了Father類型,則調用基類類型的非虛函數。
son call
:這個指針依然是son1類型的。
son name
:可是指針類型轉換爲派生類對象了,則調用子類的函數。
father call
:f2是基類類型的
Son name
:s2轉換爲派生類類型了
father call
:依然是基類類型
father name
:轉換爲基類類型。
理解上述運算結果,不只要知道虛函數在繼承層次下的的動態綁定特性和非虛函數的調用特徵,還須要掌握在繼承層次下類類型指針的類型轉換規則和內部實現。
在C++中,指針的類型轉換是常常發生的事情。須要把派生類指針轉換爲基類指針,將基類指針轉換爲派生類指針。指針的本質是一個整數,用以記錄虛擬內存空間中的地址編號,而指針的類型決定了編譯器對其指向的內存空間的解釋方式。
#include <iostream> using namespace std; class CBaseA { public: char m_A[32]; }; class CBaseB { public: char m_B[64]; }; class CDerive : public CBaseA, public CBaseB { public: char m_D[128]; }; int main() { auto pD = new CDerive; auto pA = (CBaseA *)pD; auto pB = (CBaseB *)pD; cout << pA << '\n' << pB << '\n' << pD << endl; cout << (pD == pB) << endl; }
0x9f1080
0x9f10a0
0x9f1080
1
能夠看出,指向同一個堆上new出來的對象指針,通過類型轉換以後,其值會發生改變。其緣由是由C++中多重繼承的內存佈局提及。new Cderive執行以後,生成的內存佈局以下:
則pD針只想這段內存的起始位置,而pB與pD的差值正好是CBaseA佔用的內存大小32字節。而pA與pD都指向了同一段地址。:將一個派生類的指針轉換爲某一個基類的指針,編譯器會將指針的值偏移到該基類在對象內存中的起始位置。
但是爲何pB和pD作等運算後,卻輸出的值是1呢?這是由於當編譯器發現一個指向派生類的指針和指向某個基類的指針進行==運算時,會自動將指針作隱士類型提高已屏蔽多重繼承帶來的指針差別,只要兩個指針指向同一個內存實例。就認爲他們兩個是相等的。
此時再回過頭來解釋那個虛函數那個題目。
6、區分const char*, char const* and char *const 的區別
這三個概念很容易混淆。Bjarne在他的The C++ Programming Language裏面給出一種方法,把一個聲明從右向左讀。
char *const cp; (*都成pointer to)
cp is a const pointer to char
const char *p;
p is a pointer to const char;
C++標準規定,const關鍵字放在類型或者變量名以前是等價的。
第三種狀況和第二種狀況同樣。
Const int n=5; 和int const m=10;是等價的。
參考資料:
C++ Primer
深刻探索C++對象模型
提升C++編程性能的技術
http://blog.csdn.net/haoel/article/details/1948051/
http://www.cnblogs.com/yangyh/archive/2011/06/04/2072393.html
http://destiny6.blog.163.com/blog/static/34241669201072524612781/