C++中virtual的三種用法 (2012-09-24 13:11:05)轉載▼
標簽: 雜談 分類(lèi): 編程語(yǔ)言相關(guān)
virtual用法一
#include
using namespace std;
class A{
public:
virtual void display(){ cout<<"A"<<ENDL; }
};
class B : public A{
public:
void display(){ cout<<"B"<<ENDL; }
};
void doDisplay(A *p)
{
p->display();
delete p;
}
int main(int argc,char* argv[])
{
doDisplay(new B());
return 0;
}
這段代碼打印出的結果為B,但是當把A類(lèi)中的virtual去掉之后打印出的就為A。
當基類(lèi)中沒(méi)有virtual的時(shí)候,編譯器在編譯的時(shí)候把p看做A類(lèi)的對象,調用的自然就是A類(lèi)的方法。
但是加上virtual之后,將dispaly方法變成了虛方法,這樣調用的時(shí)候編譯器會(huì )看調用的究竟是誰(shuí)的實(shí)例化對象,
這樣就實(shí)現了多態(tài)的效果。
也就是說(shuō),當基類(lèi)的派生類(lèi)中有重寫(xiě)過(guò)基類(lèi)的虛方法的時(shí)候,使用基類(lèi)
的指針指向派生類(lèi)的對象,調用這個(gè)方法實(shí)際上調用的會(huì )是派生類(lèi)最后實(shí)現的方法
virtual用法二
#include
using namespace std;
class Person{
public: Person(){ cout<<"Person構造"<<ENDL; }
~Person(){ cout<<"Person析構"<<ENDL; }
};
class Teacher : virtual public Person{
public: Teacher(){ cout<<"Teacher構造"<<ENDL; }
~Teacher(){ out<<"Teacher析構"<<ENDL; }
};
class Student : virtual public Person{
public: Student(){ cout<<"Student構造"<<ENDL; }
~Student(){ cout<<"Student析構"<<ENDL; }
};
class TS : public Teacher, public Student{
public: TS(){ cout<<"TS構造"<<ENDL; }
~TS(){ cout<<"TS析構"<<ENDL; }
};
int main(int argc,char* argv[])
{
TS ts;
return 0;
}
這段代碼的終端輸出結果為:
Person構造
Teacher構造
Student構造
TS構造
TS析構
Student析構
Teacher析構
Person析構
當Teacher類(lèi)和Student類(lèi)沒(méi)有虛繼承Person類(lèi)的時(shí)候,也就是把virtual去掉時(shí)候終端輸出的結果為:
Person構造
Teacher構造
Person構造
Student構造
TS構造
TS析構
Student析構
Person析構
Teacher析構
Person析構
大家可以很清楚的看到這個(gè)結果明顯不是我們所期望的。
我們在構造TS的時(shí)候需要先構造他的基類(lèi),也就是Teacher類(lèi)和
Student類(lèi)。
而Teacher類(lèi)和Student類(lèi)由都繼承于Person類(lèi)。這樣就導致了構造TS的時(shí)候實(shí)例化了兩個(gè)Person類(lèi)。同樣的道理,析構
的時(shí)候也是析構了兩次Person類(lèi),這是非常危險的,也就引發(fā)出了virtual的第三種用法,虛析構。
virtual用法三
#include
using namespace std;
class Person{
public: Person() {name = new char[16];cout<<"Person構造"<<ENDL;}
virtual ~Person() {delete []name;cout<<"Person析構"<<ENDL;}
private:
char *name;
};
class Teacher :virtual public Person{
public: Teacher(){ cout<<"Teacher構造"<<ENDL; }
~Teacher(){ cout<<"Teacher析構"<<ENDL; }
};
class Student :virtual public Person{
public: Student(){ cout<<"Student構造"<<ENDL; }
~Student(){ cout<<"Student析構"<<ENDL; }
};
class TS : public Teacher,public Student{
public: TS(){ cout<<"TS構造"<<ENDL; }
~TS(){ cout<<"TS析構"<<ENDL; }
};
int main(int argc,char* argv[])
{
Person *p = new TS();
delete p;
return 0;
}
這段代碼的運行結果為:
Person構造
Teacher構造
Student構造
TS構造
TS析構
Student析構
Teacher析構
Person析構
但是當我們把Person類(lèi)中析構前面的virtual去掉之后的運行結果為:
Person構造
Teacher構造
Student構造
TS構造
Person析構
程序崩潰
很明顯這個(gè)結果不是我們想要的程序,崩潰造成的后果是不可預計的,所以我們一定要注意在基類(lèi)的析構函數前面加上virtual
,使其變成虛析構在C++程序中使用虛函數,虛繼承和虛析構是很好的習慣 可以避免許多的問(wèn)題。
//////
The concept of the virtual function solves the following problem:
In object-oriented programming, when a derived class inherits from a base class, an object of the derived class may be referred to via a
pointer or reference of the base class type instead of the derived class type. If there are base class methods overridden by the derived
class, the method actually called by such a reference or pointer can be bound either 'early' (by the compiler), according to the declared
type of the pointer or reference, or 'late' (i.e. by the runtime system of the language), according to the actual type of the object referred to.
Virtual functions are resolved 'late'. If the function in question is 'virtual' in the base class, the most-derived class's implementation of the
function is called according to the actual type of the object referred to, regardless of the declared type of the pointer or reference. If it is not
'virtual', the method is resolved 'early' and the function called is selected according to the declared type of the pointer or reference.
Virtual functions allow a program to call methods that don't necessarily even exist at the moment the code is compiled.
In C++, virtual methods are declared by prepending the virtual keyword to the function's declaration in the base class. This modifier is
inherited by all implementations of that method in derived classes, meaning that they can continue to over-ride each other and be late-bound.
Example[edit]
Class Diagram of Animal
For example, a base class Animal could have a virtual function eat. Subclass Fish would implement eat() differently than subclass Wolf, but
one can invoke eat() on any class instance referred to as Animal, and get the eat() behavior of the specific subclass.
class Animal {
void /*non-virtual*/ move(void) {
std::cout << "This animal moves in some way" << std::endl;
}
virtual void eat(void) {}
};
// The class "Animal" may possess a definition for eat() if desired.
class Llama : public Animal {
// The non virtual function move() is inherited but cannot be overridden
void eat(void) {
std::cout << "Llamas eat grass!" << std::endl;
}
};
This allows a programmer to process a list of objects of class Animal, telling each in turn to eat (by calling eat()), without needing to know
what kind of animal may be in the list, how each animal eats, or what the complete set of possible animal types might be.
Abstract classes and pure virtual functions[edit]
A pure virtual function or pure virtual method is a virtual function that is required to be implemented by a derived class if that class is not
abstract. Classes containing pure virtual methods are termed "abstract" and they cannot be instantiated directly. A subclass of an abstract
class can only be instantiated directly if all inherited pure virtual methods have been implemented by that class or a parent class. Pure virtual
methods typically have a declaration (signature) and no definition (implementation).
As an example, an abstract base class MathSymbol may provide a pure virtual function doOperation(), and derived classes Plus and Minus
implement doOperation() to provide concrete implementations. Implementing doOperation() would not make sense in the MathSymbol class,
as MathSymbol is an abstract concept whose behaviour is defined solely for each given kind (subclass) of MathSymbol. Similarly, a given
subclass of MathSymbol would not be complete without an implementation of doOperation().
Although pure virtual methods typically have no implementation in the class that declares them, pure virtual methods in C++ are permitted to
contain an implementation in their declaring class, providing fallback or default behaviour that a derived class can delegate to, if appropriate.
Pure virtual functions can also be used where the method declarations are being used to define an interface - similar to what the interface
keyword in Java explicitly specifies. In such a use, derived classes will supply all implementations. In such a design pattern, the abstract
class which serves as an interface will contain only pure virtual functions, but no data members or ordinary methods. In C++, using such
purely abstract classes as interfaces works because C++ supports multiple inheritance. However, because many OOP languages do not
support multiple inheritance, they often provide a separate interface mechanism. An example is the Java programming language.
Behavior during construction and destruction[edit]
Languages differ in their behavior while the constructor or destructor of an object is running. For some languages, notably C++, the virtual
dispatching mechanism has different semantics during construction and destruction of an object. While it is recommended that virtual
function calls in constructors should be avoided for C++,[1] in some other languages, for example C# and Java, the derived implementation
can be called during construction and design patterns such as the Abstract Factory Pattern actively promote this usage in languages
supporting the ability.
Virtual destructors[edit]
Object-oriented languages typically manage memory allocation and de-allocation automatically when objects are created and destroyed.
However, some object-oriented languages allow a custom destructor method to be implemented, if desired. If the language in question uses
automatic memory management, the custom destructor (generally called a finalizer in this context) that is called is certain to be the
appropriate one for the object in question. For example, if an object of type Wolf that inherits Animal is created, and both have custom
destructors, the one called will be the one declared in Wolf.
In manual memory management contexts, the situation can be more complex, particularly as relates to static dispatch. If an object of type
Wolf is created but pointed to by an Animal pointer, and it is this Animal pointer type that is deleted, the destructor called may actually be the
one defined for Animal and not the one for Wolf, unless the destructor is virtual. This is particularly the case with C++, where the behavior is
a common source of programming errors.
////
Virtual是C++ OO機制中很重要的一個(gè)關(guān)鍵字。只要是學(xué)過(guò)C++的人都知道在類(lèi)Base中加了Virtual關(guān)鍵字的函數就是虛擬函數(例如
下面例子中的函數print),于是在Base的派生類(lèi)Derived中就可以通過(guò)重寫(xiě)虛擬函數來(lái)實(shí)現對基類(lèi)虛擬函數的覆蓋。當基類(lèi)Base的
指針point指向派生類(lèi)Derived的對象時(shí),對point的print函數的調用實(shí)際上是調用了Derived的print函數而不是Base的print函數。這是面
向對象中的多態(tài)性的體現。(關(guān)于虛擬機制是如何實(shí)現的,參見(jiàn)Inside the C++ Object Model ,Addison Wesley 1996)
[cpp] view plaincopy
class Base
{
public:Base(){}
public:
virtual void print(){cout<<"Base";}
};
class Derived:public Base
{
public:Derived(){}
public:
void print(){cout<<"Derived";}
};
int main()
{
Base *point=new Derived();
point->print();
}
//---------------------------------------------------------
Output:
Derived
//---------------------------------------------------------
這也許會(huì )使人聯(lián)想到函數的重載,但稍加對比就會(huì )發(fā)現兩者是完全不同的:
(1) 重載的幾個(gè)函數必須在同一個(gè)類(lèi)中;
覆蓋的函數必須在有繼承關(guān)系的不同的類(lèi)中
(2) 覆蓋的幾個(gè)函數必須函數名、參數、返回值都相同;
重載的函數必須函數名相同,參數不同。參數不同的目的就是為了在函數調用的時(shí)候編譯器能夠通過(guò)參數來(lái)判斷程序是在調用的
哪個(gè)函數。這也就很自然地解釋了為什么函數不能通過(guò)返回值不同來(lái)重載,因為程序在調用函數時(shí)很有可能不關(guān)心返回值,編譯
器就無(wú)法從代碼中看出程序在調用的是哪個(gè)函數了。
(3) 覆蓋的函數前必須加關(guān)鍵字Virtual;
重載和Virtual沒(méi)有任何瓜葛,加不加都不影響重載的運作。
關(guān)于C++的隱藏規則:
我曾經(jīng)聽(tīng)說(shuō)過(guò)C++的隱藏規則:
(1)如果派生類(lèi)的函數與基類(lèi)的函數同名,但是參數不同。此時(shí),不論有無(wú)virtual
關(guān)鍵字,基類(lèi)的函數將被隱藏(注意別與重載混淆)。
(2)如果派生類(lèi)的函數與基類(lèi)的函數同名,并且參數也相同,但是基類(lèi)函數沒(méi)有virtual
關(guān)鍵字。此時(shí),基類(lèi)的函數被隱藏(注意別與覆蓋混淆)。
----------引用自《高質(zhì)量C++/C 編程指南》林銳 2001
這里,林銳博士好像犯了個(gè)錯誤。C++并沒(méi)有隱藏規則,林銳博士所總結的隱藏規則是他錯誤地理解C++多態(tài)性所致。下面請看林
銳博士給出的隱藏規則的例證:
[cpp] view plaincopy
#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
void main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// Bad : behavior depends on type of the pointer
pb->g(3.14f); // Base::g(float) 3.14
pd->g(3.14f); // Derived::g(int) 3 (surprise!)
// Bad : behavior depends on type of the pointer
pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
pd->h(3.14f); // Derived::h(float) 3.14
}
林銳博士認為bp 和dp 指向同一地址,按理說(shuō)運行結果應該是相同的,而事實(shí)上運行結果不同,所以他把原因歸結為C++的隱藏規
則,其實(shí)這一觀(guān)點(diǎn)是錯的。決定bp和dp調用函數運行結果的不是他們指向的地址,而是他們的指針類(lèi)型?!爸挥性谕ㄟ^(guò)基類(lèi)指針或
引用間接指向派生類(lèi)子類(lèi)型時(shí)多態(tài)性才會(huì )起作用”(C++ Primer 3rd Edition)。pb是基類(lèi)指針,pd是派生類(lèi)指針,pd的所有函數調用都
只是調用自己的函數,和多態(tài)性無(wú)關(guān),所以pd的所有函數調用的結果都輸出Derived::是完全正常的;pb的函數調用如果有virtual則
根據多態(tài)性調用派生類(lèi)的,如果沒(méi)有virtual則是正常的靜態(tài)函數調用,還是調用基類(lèi)的,所以有virtual的f函數調用輸出Derived::,其
它兩個(gè)沒(méi)有virtual則還是輸出Base::很正常啊,nothing surprise!
所以并沒(méi)有所謂的隱藏規則,雖然《高質(zhì)量C++/C 編程指南》是本很不錯的書(shū),可大家不要迷信哦。記住“只有在通過(guò)基類(lèi)指針或
引用間接指向派生類(lèi)子類(lèi)型時(shí)多態(tài)性才會(huì )起作用”。
純虛函數:
C++語(yǔ)言為我們提供了一種語(yǔ)法結構,通過(guò)它可以指明,一個(gè)虛擬函數只是提供了一個(gè)可被子類(lèi)型改寫(xiě)的接口。但是,它本身并
不能通過(guò)虛擬機制被調用。這就是純虛擬函數(pure
virtual function)。 純虛擬函數的聲明如下所示:
[cpp] view plaincopy
class Query {
public:
// 聲明純虛擬函數
virtual ostream& print( ostream&=cout ) const = 0;
// ...
};
這里函數聲明后面緊跟賦值0。
包含(或繼承)一個(gè)或多個(gè)純虛擬函數的類(lèi)被編譯器識別為抽象基類(lèi)。試圖創(chuàng )建一個(gè)抽象基類(lèi)的獨立類(lèi)對象會(huì )導致編譯時(shí)刻錯誤
。(類(lèi)似地通過(guò)虛擬機制調用純虛函數也是錯誤的)
[cpp] view plaincopy
// Query 聲明了純虛擬函數, 我們不能創(chuàng )建獨立的 Query 類(lèi)對象
// 正確: NameQuery 是 Query 的派生類(lèi)
Query *pq = new NameQuery( "Nostromo" );
// 錯誤: new 表達式分配 Query 對象
Query *pq2 = new Query();
虛析構:
如果一個(gè)類(lèi)用作基類(lèi),我們通常需要virtual來(lái)修飾它的析構函數,這點(diǎn)很重要。如果基類(lèi)的析構函數不是虛析構,當我們用delete
來(lái)釋放基類(lèi)指針(它其實(shí)指向的是派生類(lèi)的對象實(shí)例)占用的內存的時(shí)候,只有基類(lèi)的析構函數被調用,而派生類(lèi)的析構函數不會(huì )被
調用,這就可能引起內存泄露。如果基類(lèi)的析構函數是虛析構,那么在delete基類(lèi)指針時(shí),繼承樹(shù)上的析構函數會(huì )被自低向上依次
調用,即最底層派生類(lèi)的析構函數會(huì )被首先調用,然后一層一層向上直到該指針聲明的類(lèi)型。
虛繼承:
如果只知道virtual加在函數前,那對virtual只了解了一半,virtual還有一個(gè)重要用法是virtual public,就是虛擬繼承。虛擬繼承在C++
Primer中有詳細的描述,下面稍作修改的闡釋一下:
在缺省情況下C++中的繼承是“按值組合”的一種特殊情況。當我們寫(xiě)
class Bear : public ZooAnimal { ... };
每個(gè)Bear 類(lèi)對象都含有其ZooAnimal 基類(lèi)子對象的所有非靜態(tài)數據成員以及在Bear中聲明的非靜態(tài)數據成員。類(lèi)似地當派生類(lèi)自己
也作為一個(gè)基類(lèi)對象時(shí)如:
class PolarBear : public Bear { ... };
則PolarBear 類(lèi)對象含有在PolarBear 中聲明的所有非靜態(tài)數據成員以及其Bear 子對象的所有非靜態(tài)數據成員和ZooAnimal 子對象的
所有非靜態(tài)數據成員。在單繼承下這種由繼承支持的特殊形式的按值組合提供了最有效的最緊湊的對象表示。在多繼承下當一個(gè)
基類(lèi)在派生層次中出現多次時(shí)就會(huì )有問(wèn)題最主要的實(shí)際例子是iostream 類(lèi)層次結構。ostream 和istream 類(lèi)都從抽象ios 基類(lèi)派生而
來(lái),而iostream 類(lèi)又是從ostream 和istream 派生
class iostream :public istream, public ostream { ... };
缺省情況下,每個(gè)iostream 類(lèi)對象含有兩個(gè)ios 子對象:在istream 子對象中的實(shí)例以及在ostream 子對象中的實(shí)例。這為什么不好
?從效率上而言,iostream只需要一個(gè)實(shí)例,但我們存儲了ios 子對象的兩個(gè)復本,浪費了存儲區。此外,在這一過(guò)程中,ios的構
造函數被調用了兩次(每個(gè)子對象一次)。更嚴重的問(wèn)題是由于兩個(gè)實(shí)例引起的二義性。例如,任何未限定修飾地訪(fǎng)問(wèn)ios 的成員
都將導致編譯時(shí)刻錯誤:到底訪(fǎng)問(wèn)哪個(gè)實(shí)例?如果ostream 和istream 對其ios 子對象的初始化稍稍不同,會(huì )怎樣呢?怎樣通過(guò)
iostream 類(lèi)保證這一對ios 值的一致性?在缺省的按值組合機制下,真的沒(méi)有好辦法可以保證這一點(diǎn)。
C++語(yǔ)言的解決方案是,提供另一種可替代按“引用組合”的繼承機制--虛擬繼承(virtual inheritance)。在虛擬繼承下只有一個(gè)共享
的基類(lèi)子對象被繼承而無(wú)論該基類(lèi)在派生層次中出現多少次。共享的基類(lèi)子對象被稱(chēng)為虛擬基類(lèi)。
通過(guò)用關(guān)鍵字virtual 修正,一個(gè)基類(lèi)的聲明可以將它指定為被虛擬派生。例如,下列聲明使得ZooAnimal 成為Bear 和Raccoon
的虛擬基類(lèi):
// 這里關(guān)鍵字 public 和 virtual的順序不重要
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };
虛擬派生不是基類(lèi)本身的一個(gè)顯式特性,而是它與派生類(lèi)的關(guān)系。如前面所說(shuō)明的,虛擬繼承提供了“按引用組合”。也就是說(shuō),對
于子對象及其非靜態(tài)成員的訪(fǎng)問(wèn)是間接進(jìn)行的。這使得在多繼承情況下,把多個(gè)虛擬基類(lèi)子對象組合成派生類(lèi)中的一個(gè)共享實(shí)例
,從而提供了必要的靈活性。同時(shí),即使一個(gè)基類(lèi)是虛擬的,我們仍然可以通過(guò)該基類(lèi)類(lèi)型的指針或引用,來(lái)操縱派生類(lèi)的對象
。