既然c++的非virtual的函數可以重定義,virtual函數相比非virtual的有什麼優勢?

在base class裡面定義的virtual函數可以在derived class裡面override,一般的base class的函數可以在derived class裡面被redefine,書本還說virtual函數會有額外開銷,1)那麼virtual函數應該在什麼情況下用呢?2)是不是用virtual的函數都能用非virtual的函數替代?3)那麼是不是virtual沒什麼用呢?這塊一直比較不懂,網上看到好多都只是給個代碼例子解釋不清楚,本渣不是看的很懂,希望得到詳細的指點,謝謝各位!


回答居然靠前了點。那我在原來例子之後,補上正確點的答案。因為統計發現,如果只給出錯誤作法的示例,那麼就很容易給看過的人留下潛意識:原來可以這麼錯。

/////原例(錯誤用法)

class 會飛的傢伙

{

public:

void fly()

{cout &<&< "我飛飛飛。" &<&< endl; }

};

class 鳥 : public 會飛的傢伙

{

public:

void fly()

{cout &<&< "我拍翅飛。" &<&< endl; }

};

class 火箭 : public 會飛的傢伙

{

public:

void fly()

{cout &<&< "我噴氣飛。" &<&< endl; }

};

void test()

{

vector&<會飛的傢伙 *&> v;

v.push_back(new 鳥);

v.push_back(new 火箭);

for(auto f : v)

{

f-&>fly();

}

}

fly 不是虛函數,結果屏幕輸出全部是"我飛飛飛"。

/////補充例子(正確用法,不過不使用智能指針了,保持簡單)

class 會飛的傢伙

{

public:

//更常用的寫法是聲明為純虛析構

//然後在cpp文件中卻提供析構的實現。

virtual ~會飛的傢伙() {}

virtual void fly() = 0;

};

class 鳥 : public 會飛的傢伙

{

public:

void fly() override

{cout &<&< "我拍翅飛。" &<&< endl; }

};

class 火箭 : public 會飛的傢伙

{

public:

void fly() override

{cout &<&< "我噴氣飛。" &<&< endl; }

};

void test()

{

vector&<會飛的傢伙 *&> v;

v.push_back(new 鳥);

v.push_back(new 火箭);

for(auto f : v)

{

f-&>fly();

delete f;

}

}


題主請看這段代碼:

#include &

class Base
{
public:
void print1() {printf("Hello from Base::print1!
");}
virtual void print2() {printf("Hello from Base::print2!
");}
};

class Derived: public Base
{
public:
void print1() {printf("Hello from Derived::print1!
");}
virtual void print2() {printf("Hello from Derived::print2!
");}
};

int main()
{
Derived *pd = new Derived();
Base *pb = pd;

pd-&>print1();
pb-&>print1();

pd-&>print2();
pb-&>print2();

return 0;
}

這是輸出:

Hello from Derived::print1!
Hello from Base::print1!
Hello from Derived::print2!
Hello from Derived::print2!

只有虛函數才能使你在僅用一個指針進行調用時,確保訪問到期望的方法。非虛函數就沒有這個特性。

----------------2017-02-08的分割線----------------------

關於答主的三個問題,今天抽空再強行補充一波。

1)那麼virtual函數應該在什麼情況下用呢?

  • virtual 函數應該在某個類的某個函數需要多態性的情況下使用

  • 還有一個使用場景是若類存在派生,那麼析構函數必須是virtual的,否則可能會導致資源泄露。

  • 而非 virtual 函數,能夠確保你使用基類指針就一定調用基類實現,防止派生類無意中覆蓋。

2)是不是用virtual的函數都能用非virtual的函數替代?

  • 正相反,若在開發過程中做了良好約定,所有的非virtual函數都能用virtual函數代替。
  • 由於調用virtual函數多了一次查vtable的動作,可能導致(微不可查)的性能損失。不過這並非是不可接受的,某些框架例如Qt,大部分類都是採用這種哲學進行設計,並沒有什麼問題。
  • 有些語言比如 python 就是這麼乾的,程序員自己去保障覆蓋問題。

3)那麼是不是virtual沒什麼用呢?

  • 若是不考慮效率和強行避免覆寫問題,其實非virtual函數才是沒什麼用的。


這麼說吧,如果你的程序沒有動態載入plugin,如果你願意狂擼TMP,其實不用virtual也沒什麼關係,該實現的功能一樣能實現出來。


那我問你個問題,base定義了一個非virtual的func,在derived中重載了這個func,如果我這樣寫你說調用哪個func?

base *p = new derived;

p-&>func();

你說virtual有用沒?

c++編譯器會為虛函數建立虛函數表來實現多態,深刻理解下多態,或者去看看《深入c++對象模型》


推薦題主看下《effective c++》,裡面有很詳細的講解。

虛函數的一個重要作用是向後兼容(不對現有的代碼進行修改就可以實現新添加的功能[這是理想狀態])。

在軟體開發中,添加功能是一個很平常的事情,很多時候我們無法預料未來會有什麼變化,只能有限地向後兼容,虛函數算是這樣的一個例子吧。

虛函數一般是基類聲明介面,子類繼承介面並實現。試想一個場景,航空公司開發一個飛機控制軟體,此時在基類中聲明了飛行方法。

class Base{
public:
// 飛行
virtual void fly(){//默認實現};
}

航空公司有波音747和777。

於是有

class Boeing747 : public Base{
public:
// 飛行
virtual void fly(){//747實現};
}

// 後面777類似,代碼就省略了

假如控制軟體有一個流程,依次起飛所有的飛機,則實現可以為:

// 起飛所有飛機

void take_off_all_planes(std::vector& planes)
{
for(auto it : planes)
{
(*it)-&>fly();
}
}

int main()
{
// 添加飛機
std::vector& planes;
Base* pBoeing747 = new Boeing747();
planes.push_back(pBoeing747);
Base* pBoeing777 = new Boeing777();
planes.push_back(pBoeing777);

take_off_all_planes(planes);
//TODO::銷毀申請的資源

return 0;
}

以上代碼完成了所有起飛邏輯。但是後來航空公司又買了空客320飛機,很明顯,空客飛機也需要起飛,需要在起飛全部飛機邏輯中添加空客的邏輯,此時我們可以這樣做:

class Airbus320 : public Base{
public:
// 飛行
virtual void fly(){//空客320的實現};
}

只需要在main()函數中添加對應的機型,而不用修改take_off_all_planes方法。

int main()
{
// 添加飛機
std::vector& planes;
Base* pBoeing747 = new Boeing747();
planes.push_back(pBoeing747);
Base* pBoeing777 = new Boeing777();
planes.push_back(pBoeing777);
Base* pAirbus320 = new Airbus320();
planes.push_back(pAirbus320);

take_off_all_planes(planes);
//TODO::銷毀申請的資源
return 0;
}

題主可以思考下,如果沒有虛函數,該如何寫類似的代碼。之後再思考下問題描述里的三個問題。


我面試的時候最愛問這個問題,介紹一下你所理解的C++語言是如何實現多態性的,有些十多年C++經驗的人也沒有做出比較清晰準確的總結。

______________________

評論中有人回答了「大家」所理解的多態,雖然跟我這個回答就差了幾個字的表述,大家還是以評論中那個回答的為準吧…如果「大家」覺得在學習和傳播知識的時候可以把「一般」,「習慣上」這種主觀的總結歸納忽略,那我覺得「大家」可能缺乏一種構建知識體系的能力。如果「大家」在這裡說c++的多態就是虛函數,並且一直這樣傳播下去,那若干年之後的下一代程序員學習到的就「c++的多態就是虛函數」,那他們的知識體系中就相比上一代程序員缺失了一塊。這種知識的變遷在更新速度超快的計算機互聯網行業太多了,這跟名人名言被閹割不一樣,因為名人名言本身就是主觀的,被閹割後可以按照新的意思來解讀,但是知識是客觀的,知識即使被閹割它也不會改變原有的客觀性,而只能使受對這個知識點的認知變得片面。當一個人看到他所缺少的某種知識的時候,有的會深究,有的會反對,哪種人具有更好的學習能力呢?一個人學習的能力體現在深度理解,廣度關聯,舉一反三,而不是掐枝去葉。而在運用知識的時候才應該字斟句酌,精挑細選。

你非要說靜態多態不算C++的多態,那我也沒辦法,你自己願意構建什麼樣的知識體系是你自己的選擇,你高興就行。

這個答案就不更新了。

_____________________________________________

C++的多態性是通過兩種方式來實現的:

1.編譯時多態(靜態多態),也可以說是前綁定,通過函數重載(overload)來實現,就是題主說的非virtual重寫(模板屬於此類多態的另一個實現方式)。這種方式的本質是,通過編譯期的語法分析來確定代碼中調用的同名函數的正確引用。注意這裡是相同函數名,但是不同函數簽名,函數簽名就是函數名+參數類型列表組成的一個唯一id(不包括返回值類型)。你可以寫兩個同名非virtual函數,但是如果參數類型列表完全一樣,編譯時候會得到ambiguous的二義性錯誤,編譯器會覺得你在戲弄他。為啥叫前綁定,因為是在你運行之前,一處調用就已經跟目標符號代碼綁定在一起了,每次在程序執行到這句函數調用的彙編代碼比如call xxxxxx的時候目標函數地址總是同一個值(忽略Windows操作系統ASLR特性)。

評論中有人說我的理解有誤,說編譯時多態不屬於多態。我只記得我當時學習polymorphism這詞的時候理解到的通用意思是:一種將不同的特殊行為和單個泛化記號相關聯的能力。按照這個通用理解然後帶入C++語言中的函數重載,對於一個被重載的方法來說單個泛化記號即函數名,不同特殊性為即那個重載方法的不同實現。所以我認為函數重載肯定是符合多態定義的。然後順便查一下維基百科貼出來:

多型(英語:polymorphism),是指計算機程序執行時,相同的訊息可能會送給多個不同的類別之物件,而系統可依據物件所屬類別,引發對應類別的方法,而有不同的行為。簡單來說,所謂多型意指相同的訊息給予不同的物件會引發不同的動作稱之。

多態也可定義為「一種將不同的特殊行為和單個泛化記號相關聯的能力」。

多態可分為變數多態與函數多態。變數多態是指:基類型的變數(對於C++是引用或指針)可以被賦值基類型對象,也可以被賦值派生類型的對象。函數多態是指,相同的函數調用界面(函數名與實參表),傳送給一個對象變數,可以有不同的行為,這視該對象變數所指向的對象類型而定。因此,變數多態是函數多態的基礎。

多態還可分為:

  • 動態多態(dynamic polymorphism):通過類繼承機制和虛函數機制生效於運行期。可以優雅地處理異質對象集合,只要其共同的基類定義了虛函數的介面。也被稱為子類型多態(Subtype polymorphism)或包含多態(inclusion polymorphism)。在面向對象程序設計中,這被直接稱為多態
  • 靜態多態(static polymorphism):模板也允許將不同的特殊行為和單個泛化記號相關聯,由於這種關聯處理於編譯期而非運行期,因此被稱為「靜態」。可以用來實現類型安全、運行高效的同質對象集合操作。C++ STL不採用動態多態來實現就是個例子。
    • 非參數化多態或譯作特設多態(Ad-hoc polymorphism):
      • 函數重載(Function Overloading)
      • 運算符重載(Operator Overloading)
      • 帶變數的宏多態(macro polymorphism)
    • 參數化多態(Parametric polymorphism):把類型作為參數的多態。在面向對象程序設計中,這被稱作泛型編程

對於C++語言,帶變數的宏和函數重載(function overload)機制也允許將不同的特殊行為和單個泛化記號相關聯。然而,習慣上並不將這種函數多態(function polymorphism)、宏多態(macro polymorphism)展現出來的行為稱為多態(或靜態多態),否則就連C語言也具有宏多態了。談及多態時,默認就是指動態多態,而靜態多態則是指基於模板的多態

至於後面的習慣上這一段話,仁者見仁智者見智吧。

2.運行時多態(動態多態),也可以說是後綁定,通過函數覆蓋(override)來實現,就是題主說的virtual函數重寫。我們說C語言的靈魂是指針,那麼C++的靈魂就是虛函數了。虛函數是配合每個對象的虛函數表指針和每個類類型的虛函數表來實現的,這裡要是展開講就有點多,大清早失眠醒了手機碼字不方便,先點到為止。

這兩種C++特性本質上是完全不一樣,表面上也是有很大區別,如果連使用場景都不能搞清楚,我覺得學習C++的道路還很長很長。


1)那麼virtual函數應該在什麼情況下用呢?

2)是不是用virtual的函數都能用非virtual的函數替代?

3)那麼是不是virtual沒什麼用呢?

1. virtual函數用來實現多態,在運行時基於一個類型選擇調用相應的函數(dynamic single-dispatch subtype polymorphism)。純虛函數定義一個介面,派生類必須實現這個介面。

2. 是。理論上講,用C part of C++手寫虛表,也能實現類似於虛函數的動態分發,so……(類似於理論上所有軟體都可以純用彙編完成,meaningless)。

3. 不是,參考1,2。


考慮下驅動程序,驅動程序常見的幾個動作如讀寫等,就用虛函數。這樣底層可以調用這些虛函數可以實現不同類型的設備讀寫功能。如果不用虛函數,得獲得不同設備驅動的實例。用虛函數則不用,只需要底層如基類即可。虛函數實際上就是個函數指針。非虛函數實際上是不同的函數名,編譯器知道你調用哪個函數而已。反正就一句話,想通過基類調用不同繼承類的函數就用虛函數,常見於驅動程序。。


如果在基類里函數聲明使用virtual,則當使用基類的指針或引用指向派生類對象時,調用此函數實際調用的是在派生類中重寫的函數,也就是說調用函數由對象的實際類型決定。反之,不使用virtual,則會調用基類中的方法,也就是說調用函數由指針或引用對應的類型決定。

class A {

public:

void func() { cout &<&< "A" &<&< endl; }

virtual void v_func() { cout &<&< "A" &<&< endl; }

};

class B : public A {

public:

void func() { cout &<&< "B" &<&< endl; }

void v_func() { cout &<&< "B" &<&< endl; }

};

A a = B();

a.func();

a.v_func();

// 將輸出

// A

// B

這一特性很有用,大多數時候我們應該把析構函數聲明為虛函數,因為如何析構對象應該取決於對象的真實類型。如果不這樣做,則析構時將按指針或引用類型進行析構,可能會使子類中使用new分配的內存被泄露。

實現原理為虛函數表,題主習慣英文,應該是vftable。確實會造成一定性能損失,時間上調用時多訪問一次虛函數表,空間上每個對象多了一個指向虛函數表的指針。

注意事項是當基類中的虛函數存在重載情況時,子類如果要重寫,則必須全部重寫。

個人總結,手機打字,代碼格式可能不好,見諒。


virtual函數指針都是保存在virtual table里的。每個對象實例內都保存有指向virtual table的指針。

因此,virtual函數調用時要通過virtual table指針找到對應的函數指針。而非virtual函數在編譯階段已經確定了具體的調用函數入口。非virtual函數調用效率要高一些。

同時,virtual函數動態靈活性要強於非virtual函數。一個對象被轉換成父類後,調用virtual函數依然會找到子類中定義的函數,而非virtual函數只會找到父類中的定義。


舉個最簡單的例子:

class shape
{
public:
virtual void draw() = 0;
};

class circle :public shape
{
public:
circle(){}
void draw(){ printf("draw circle
"); }
};

class rectangle :public shape
{
public:
rectangle(){}
void draw(){ printf("draw rectangle
"); }
};

void draw_something(shape *p)
{
p-&>draw();
}

int main()
{
rectangle r;
circle c;
draw_something(r);
draw_something(c);
getchar();
return 0;
}

如果沒有virtual機制,必須為每個shape的派生類實現一個draw_something,派生類一多就沒法看了。


先說一下什麼時候定義虛函數?首先看成員函數所在的類是否會作為基類。然後看成員函數在類的繼承後有無可能被更改功能,如果希望更改其功能的,一般應該將它聲明為虛函數。如果成員函數在類被繼承後功能不需修改,或派生類用不到該函數,則不要把它聲明為虛函數。

虛函數對於多態具有決定性的作用,有虛函數才能構成多態。

聲明虛函數的基本規則如下:

1) 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。

2) 為了方便,你可以只將基類中的函數聲明為虛函數,這樣所有派生類中具有遮蔽(覆蓋)關係的同名函數都將自動成為虛函數。

3) 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那麼將使用基類的虛函數。

4) 只有派生類的虛函數遮蔽基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。例如基類虛函數的原型為virtual void Func();,派生類虛函數的原型為virtual void Func(int);,那麼當基類指針 Pointer 指向派生類對象時,語句Pointer-&> Func(100);將會出錯,而語句Pointer-&> Func();將調用基類的函數。

5) 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明為虛函數沒有什麼意義。

6) 析構函數可以聲明為虛函數,而且有時候必須要聲明為虛函數。

下面通過一個簡例來展示虛函數的作用:

#include&
#include&
using namespace std;

const double pi = 3.1415926;
class Circle
{
public:
Circle(char *name,double radius):Name(name), Radius(radius){}
~Circle(){}
void Display()
{
cout &<&< "My name is:" &<&< Name &<&< ", and my area is:" &<&< pi*Radius*Radius &<&< endl; } protected: char *Name; double Radius; }; class Sphere :public Circle { public: Sphere(char *name, double radius) :Circle(name,radius){} ~Sphere() {} void Display() { cout &<&< "My name is:" &<&< Name &<&< ", my superficial area is:" &<&< 4.*pi*Radius*Radius &<&<", and my volume is:"&<&<4./3.*pi*Radius*Radius*Radius &<&< endl; } }; int main() { Circle *Pointer = new Circle("Circle", 4.2); Pointer-&>Display();
Pointer = new Sphere("Sphere", 4.2);
Pointer-&>Display();
delete Pointer;
system("pause");
return 0;
}

上面代碼的運行結果為:

My name is:Circle, and my area is:55.4177
My name is:Sphere, and my area is:55.4177

我們直觀上認為,如果指針指向了派生類對象,那麼就應該使用派生類的成員變數和成員函數,這符合人們的思維習慣。但是本例的運行結果卻告訴我們,當基類指針 Pointer指向派生類 Sphere 的對象時,雖然使用了 Sphere 的成員變數,但是卻沒有使用它的成員函數,導致輸出結果不倫不類(當半徑都一樣時,球體的表面積和圓的面積肯定不一樣),不符合我們的預期。

換句話說,通過基類指針只能訪問派生類的成員變數,但是不能訪問派生類的成員函數。

為了消除這種尷尬,讓基類指針能夠訪問派生類的成員函數,C++ 增加了虛函數(Virtual Function)。使用虛函數非常簡單,只需要在函數聲明前面增加 virtual 關鍵字。

#include&
#include&
using namespace std;

const double pi = 3.1415926;
class Circle
{
public:
Circle(char *name,double radius):Name(name), Radius(radius){}
~Circle(){}
virtual void Display()
{
cout &<&< "My name is:" &<&< Name &<&< ", and my area is:" &<&< pi*Radius*Radius &<&< endl; } protected: char *Name; double Radius; }; class Sphere :public Circle { public: Sphere(char *name, double radius) :Circle(name,radius){} ~Sphere() {} virtual void Display() { cout &<&< "My name is:" &<&< Name &<&< ", my superficial area is:" &<&< 4.*pi*Radius*Radius &<&<", and my volume is:"&<&<4./3.*pi*Radius*Radius*Radius &<&< endl; } }; int main() { Circle *Pointer = new Circle("Circle", 4.2); Pointer-&>Display();
Pointer = new Sphere("Sphere", 4.2);
Pointer-&>Display();
delete Pointer;
system("pause");
return 0;
}

此次的運行結果為:

My name is:Circle, and my area is:55.4177
My name is:Sphere, my superficial area is:221.671, and my volume is:310.339

和前面的例子相比,本例僅僅是在 Display() 函數聲明前加了一個virtual關鍵字,將成員函數聲明為了虛函數(Virtual Function),這樣就可以通過 Pointer 指針調用 Sphere類的成員函數了,運行結果也證明了這一點(球體擁有了自己的表面積和體積,而不是亂用圓的面積)。

有了虛函數,基類指針指向基類對象時就使用基類的成員(包括成員函數和成員變數),指向派生類對象時就使用派生類的成員。換句話說,基類指針可以按照基類的方式來做事,也可以按照派生類的方式來做事,它有多種形態,或者說有多種表現方式,我們將這種現象稱為多態Polymorphism)。

上面的代碼中,同樣是Pointer-&>Display();這條語句,當 Pointer指向不同的對象時,它執行的操作是不一樣的。同一條語句可以執行不同的操作,看起來有不同表現方式,這就是多態

多態是面向對象編程的主要特徵之一,C++中虛函數的唯一用處就是構成多態。

C++提供多態的目的是:可以通過基類指針對所有派生類(包括直接派生和間接派生)的成員變數和成員函數進行「全方位」的訪問,尤其是成員函數。如果沒有多態,我們只能訪問成員變數。

通過指針調用普通的成員函數時會根據指針的類型(通過哪個類定義的指針)來判斷調用哪個類的成員函數,但是通過分析可以發現,這種說法並不適用於虛函數,虛函數是根據指針的指向來調用的,指針指向哪個類的對象就調用哪個類的虛函數。

引用在本質上是通過指針的方式實現的,既然藉助指針可以實現多態,那麼我們就有理由推斷:藉助引用也可以實現多態。

此時,只需要將mian()函數做如下修改即可

int main()
{
Circle C("Circle", 4.2);
Sphere S("Sphere", 4.2);
Circle rC = C;
Sphere rS = S;
rC.Display();
rS.Display();
system("pause");
return 0;
}

運行結果為:

My name is:Circle, and my area is:55.4177
My name is:Sphere, my superficial area is:221.671, and my volume is:310.339

當然,有一點需要注意的是:當用引用的時候,上面例子中的成員函數Display()即使不聲明為虛函數也可以輸出正確的結果。因為用引用是分別綁定的,下面有詳解。

由於引用類似於常量,只能在定義的同時初始化,並且以後也要從一而終,不能再引用其他數據,所以本例中必須要定義兩個引用變數,一個用來引用基類對象,一個用來引用派生類對象。從運行結果可以看出,當基類的引用指代基類對象時,調用的是基類的成員,而指代派生類對象時,調用的是派生類的成員。

不過引用不像指針靈活,指針可以隨時改變指向,而引用只能指代固定的對象,在多態性方面缺乏表現力。本例的主要目的是讓大家知道,除了指針,引用也可以實現多態

當派生類比較多時,如果不使用多態,那麼就需要定義多個指針變數,很容易造成混亂;而有了多態,只需要一個指針變數 Pointer 就可以調用所有派生類的虛函數。

也可以發現,對於具有複雜繼承關係的大中型程序,多態可以增加其靈活性,讓代碼更具有表現力。


虛函數的作用是支持多態,也就是動態綁定。

比如,你現在有一個容器,裡面保存的是某個基類的指針(因為是基類指針,所以可能指向基類也可能指向派生類),你通過這些指針調用類中的函數,會分為兩種情況:

1、你調用的是非虛函數

此時不會發生動態綁定,被調用的函數會和指針類型相關,即基類中定義的版本。

2、你調用的是虛函數

由於你是用指針調用的虛函數,而指針所指向的對象類型未知(可能是基類,也可能是派生類),所以此時會發生動態綁定,程序會根據指針所指實際類型調用對應的函數,即指針所指向的是基類便會調用基類定義版本、指針所指為派生類便會調用派生類中定義的版本。

(動態綁定只在用引用或指針調用虛函數時才會發生,因為只有這兩種情況下,你所使用的實際類型才有可能是未知的,可能是派生類也可能是基類。)


用了為期不短的C++,我的開發生涯里從未遇到需要「函數重定義"這樣的環境,所以今天第一次知道這事。嚇得我趕緊擼了幾行代碼壓壓驚:

1. 不用virtual,僅僅重定義,那麼方法不進入vtable。調用的時候,編譯器根據上下文語境直接選取合適的函數進行調用。例如:

  • class A {
  • public:
  • void test(){
  • printf("A::test
    ");
  • }
  • };
  • class B : public A {
  • public:
  • void test() {
  • printf("B::test
    ");
  • }
  • };
  • int _tmain(int argc, _TCHAR* argv[])
  • {
  • B b;
  • A* a = (A*)b;
  • a-&>test();
  • return 0;
  • }

輸出:A::test

2.如果test定義為虛函數,則上例輸出應為: B::test

3.就這點區別。另外我現在還想不出來什麼時候要用到重定義。。。


手機不方便敲代碼,代碼部分就看其他答主吧。

virtual函數主要是實現了運行時多態。

問題1:什麼時候該用virtual函數?

如果有繼承體系,則基類應該聲明virtual析構函數,否則可能會造成內存泄漏。相反,如果不打算某類作為基類,就不要作死聲明virtual析構函數,因為virtual函數會增加對象體積。請看《EFfective c++》條款6:為多態基類聲明virtual destructor。

問題2:是不是所有用virtual函數的地方都能用非virtual函數替代?

不是。非virtual不能實現運行時多態。

但是virtual函數是可以用其他技術手段來替代的。請看《effective c++》條款35:考慮virtual函數以外的其他選擇。這個條款我也沒怎麼看懂。。

另外,對於非virtual函數,派生類版本會覆蓋其基類的同名版本,但是通過virtual函數實現的運行時多態可以避免這個問題。

問題3:是不是virtual函數沒什麼用呢?

請看問題1。


《C++ primer plus》第13章,題主可以看下,絕對藥到病除。

總結下就是virtual決定運行時行為,體現了運行時的多態。

首先,如果沒有定義virtual,程序按照指針類型進行調用,指針定義為基類,就調用基類的函數;指針定義為派生類,就調用派生類函數。

其次,定義了virtual後,編譯器對虛函數有特殊的處理,給對象增加一個指針,指向函數地址數組,也就是虛函數表,在運行時系統就會根據指針指向的地址調用為對象進行聲明的函數地址

virtual對於析構函數有尤其重要意義


動態綁定,

就是可以用父類指針直接使用子類的函數。

編譯器不會報錯還能正確的調用到你的子類函數上。


動態綁定 有了動態綁定能幹的事就多了


聲明virtual是為了實現多態,學C++ class相關的,還是要先了解一些多態的。


推薦閱讀:

用MFC做的貪吃蛇遊戲,求大神。?
C++ primer 第四版這段關於vector的程序是否有未定義的行為?
c/c++中簡單加法出現奇怪的錯誤?
如何使用C++11實現跨平台的定時器timer?
在 Windows 上不用 Win32 API 可以繪製出一個窗口么?

TAG:面向對象編程 | C | CC |