C++學習記錄(三):多態

之前呢學習了繼承的基本知識、創建繼承層次結構並明白公有繼承、私有繼承以及保護繼承的區別。接下來該學習面向對象編程的核心——多態,並應用這些知識。

這裏將學到:

  • 多態意味着什麼;
  • 虛函數的用途和用法;
  • 什麼是抽象類以及如何聲明它們;
  • 徐濟成意味着什麼以及在什麼情況下使用它們。

3.1 多態基礎

多態是面嚮對象語言的一種特徵,讓程序員能夠以類似的方式處理不同類型的對象。這裏重點介紹多態行爲,也成爲子類型多態。在C++中可以通過繼承層次結構來實現。

3.1.1 爲何需要多態行爲

在上個博客,Tuna和Carp在Fish那裏繼承了方法Swim()。然而,Tuna和Carp都可以提供自己的Swim()方法,以定製其游泳方式,但是鑑於它們都是Fish,如果將Tuna實例作爲實參傳給Fish參數,並通過該參數調用Swim(),最終執行的將是Fish::Swim(),而不是Tuna::Swim()。

#include <iostream>
using namespace std;

class Fish
{
public:
void Swim()
{
cout<<"Fish swims!"<<endl;
}

};

class Tuna:public Fish
{
public:
void Swim()
{
cout<<"Tuna swims!"<<endl;
}

};

void MakeFishSwim(Fish& InputFish)
{
// calling Fish::Swim
InputFish.Swim();
}

int main()
{
Tuna myDinner;

myDinner.Swim();

MakeFishSwim(myDinner);

return 0;
}

Tuna類以公有方式繼承了Fish類,它還自定義了Swim(),覆蓋了Fish::Swim()。在main()中,直接調用了Tuna::Swim(),並且將myDinner(其類型爲Tuna)作爲參數傳達給了MakeFishSwim(),而該函數將其視爲Fish引用。換句話說,雖然傳入的是Tuna對象,MakeFishSwim(Fish&)也將其視爲Fish,進而調用Fish::Swim()。

理想情況下,我們希望Tuna對象表現出金槍魚的行爲,即便通過Fish參數調用Swim()時也如此。換句話說,調用InputFish.Swim()時,我們希望執行額是Tuna::Swim()。要實現這種多態行爲——讓Fish參數表現出其實際類型(派生類Tuna)的行爲,可將Fish::Swim()聲明爲虛函數。

3.1.2 使用虛函數實現多態行爲

可以通過Fish指針或者Fish引用來訪問Fish對象,這種指針或引用可指向Fish、Tuna或Carp對象,但是不關心他們指向哪種對象。要通過這種指針或引用來調用方法Swim(),可以像下面的做法一樣:

pFish->Swim();
myFish.Swim();

我們通過這種指針或者引用調用Swim()時,如果他們指向的是Tuna對象,則可像Tuna那樣有用,如果指向的是Carp對象,則可像Carp那樣游泳,如果指向的是Fish,則可像Fish那樣游泳。爲此可在基類Fish中將Swim()聲明爲虛函數:

class Base
{
virtual ReturnType FunctionName (Parameter List);
};

class Derived
{
ReturnType FunctionName (Parameter List);
};

通過使用關鍵字virtual,可確保編譯器調用覆蓋版本。也就是說,如果Swim()被聲明爲虛函數,則將參數myFish(其類型爲Fish&)設置爲一個Tuna對象時,myFish.Swim()將執行Tuna::Swim(),如果下:

#include <iostream>
using namespace std;

class Fish
{
public:
virtual void Swim()
{
cout<<"Fish swims!"<<endl;
}
};

class Tuna: public Fish
{
public:
// override Fish::Swim
void Swim()
{
cout<<"Tuna swims!"<<endl;
}
};

class Carp: public Fish
{
// override Fish::Swim
void Swim()
{
cout<<"Carp swims!"<<endl;
}
};

void MakeFishSwim(Fish& InputFish)
{
// calling virtual method Swim()
InputFish.Swim();
}

int main()
{
Tuna myDinner;
Carp myLunch;

MakeFishSwim(myDinner);

MakeFishSwim(muLunch);

return 0;
}

函數MakeFishSwim(Fish&),根本沒有調用Fish::Swim(),因爲存在覆蓋版本Tuna::Swim()和Carp::Swim(),它們優先於被聲明爲虛函數的Fish::Swim()。這很重要,它意味着在MakeFishSwim()中,可通過Fish&參數調用派生類定義的Swim(),而無需知道該參數指向的是哪種類型的對象。

這就是多態:將派生類對象視爲基類對象,並執行派生類的Swim()的實現。

3.1.3 爲何需要虛構造函數

前面的代碼演示了一個問題,就是將派生類對象傳遞給基類參數時,並通過該參數調用函數時,將執行基類的函數。但是還存在一個問題:如果基類指針指向的是派生類對象,通過該指針調用運算符delete時,結果如何?

將調用哪個析構函數?

#include <iostream>
using namespace std;

class Fish
{
public:
Fish()
{
cout<<"Constructed Fish"<<endl;
}

~Fish()
{
cout<<"Destroyed Fish"<<endl;
}
}; 

class Tuna:public Fish
{
public:
Tuna()
{
cout<<"Constructed Tuna"<<endl;
}

~Tuna()
{
cout<<"Destoryed Tuna"<<endl;
}
};

void DeleteFishMemory(Fish* pFish)
{
delete pFish;
}

int main()
{
cout<<"Allocating a Tuna on the free store:"<<endl;
Tuna* pTuna = new Tuna();
cout<<"Deleting the Tuna:"<<endl;
DeleteFishMemory(pTuna);

cout<<"Instantiating a Tuna on the stack:"<<endl;
Tuna myDinner;
cout<<"Automatic destruction as it goes out of scope:"<<endl;

return 0;
}

在main()中,使用new在自由存儲區創建了一個Tuna實例;然後馬上使用輔助函數DeleteFishMemory()釋放分配的內存。出於對比,又在棧上創建了另一個Tuna實例——局部變量myDinner,在main()結束時,它將不再在作用域內。

我們根據輸出可以看出,由於使用了關鍵字new,自由存儲區構造了Fish和Tuna類,但是delete沒有調用Tuna的析構函數,只調用了Fish的析構函數;而構造和析構局部變量myDinner時,調用了基類和派生類的構造函數和析構函數,形成了鮮明的對比。

在析構過程中,需要調用所有相關的析構函數,包括~Tuna();顯然是哪裏出了問題。

上面那個代碼表明,對於使用new在自由存儲區中實例化的派生類對象,如果將其賦值給基類指針,並通過該指針調用delete,將不會調用派生類的析構函數。自然會導致資源未釋放、內存泄漏等問題。

要避免這種問題,可將析構函數聲明爲虛函數:

#include <iostream>
using namespace std;

class Fish
{
public:
Fish()
{
cout<<"Constructed Fish"<<endl;
}

virtual ~Fish()
{
cout<<"Destroyed Fish"<<endl;
}
};

class Tuna:public Fish
{
public:
Tuna()
{
cout<<"Constructed Tuna"<<endl;
}

~Tuna()
{
cout<<"Destroyed Tuna"<<endl;
}

};

void DeleteFishMemory(Fish* pFish)
{
delete pFish;
}

int main()
{
cout<<"Allocating a Tuna on the free store:"<<endl;
Tuna* pTuna = new Tuna();
cout<<"Deleting the Tuna:"<<endl;
DeleteFishMemory(pTuna);

cout<<"Instantiating a Tuna on the stack:"<<endl;
Tuna myDinner;
cout<<"Automatic dectruction as it goes out of scope:"<<endl;

return 0;

}

相比於上面的上面的代碼,這裏只是經Fish的析構函數添加了關鍵字virtual。這種修改將導致運算符用於Fish時,如果該指針指向的是Tuna對象,則編譯器不僅會執行Fish::~Fish(),還會執行Tuna::~Tuna()。輸出還表明,無論Tuna對象是使用new在自由存儲區中實例化的,還是以局部變量方式在棧中實例化的,構造函數和析構函數的調用順序都相同。

注意,務必像下面這樣將基類的析構函數聲明爲虛函數:

class Base
{
public:
virtual ~Base(){}; //virtual destructor
};

這可避免將delete用於Base指針時,不會調用派生類的析構函數的情況發生。

3.1.4 虛函數的工作原理——理解虛函數表

這一節掠過

3.1.5  抽象基類和純虛函數

不能實例化的基類被稱爲抽象基類,這樣的基類只有一個用途,那就是從它派生出其他類。在C++中,要創建抽象基類,可以通過聲明純虛函數。

以下述方式聲明的虛函數被稱爲純虛函數:

class AbstractBase
{
public:
virtual void DoSomething() = 0; //pure virtual method
};

這個聲明告訴編譯器,AbstractBase地派生類必須實現方法DoSomething():

class Derived:public AbstractBase
{
public:
void DoSomething()
{
cout<<"Implemented virtual function"<<endl;
}
};

AbstractBase類要求Dreived類必須提供虛方法DoSomething()地實現,這讓基類可指定派生類種方法地名稱和特徵,即指定派生類的接口。

再次以Tuna爲例子,假定它繼承了Fish類,沒有覆蓋Fish::Swim(),因此不能遊得很快。這種實現存在缺陷。通過將Swim聲明爲純虛函數,讓Fish變成抽象基類,可以確保從Fish派生而來的Tuna類實現Tuna::Swim(),從而像金槍魚那樣遊動:

#include <iostream>
using namespace std;

class Fish
{
public:
virtual void Swim() = 0;
};

class Tuna:public Fish
{
public:
void Swim()
{
cout<<"Tuna swims in the sea"<<endl;
}
};

class Carp:public Fish
{
public:
void Swim()
{
cout<<"Carp swims in the lake"<<endl;
}
};

void MakeFishSwim(Fish& inputFish)
{
inputFish.Swim();
}

int main()
{
Carp myLunch;
Tuna myDinner;

MakeFishSwim(myLunch);

MakeFishSwim(myDinner);

return 0;
}

main()中不能實例化Fish,但是如果派生類不實現純虛函數,那也將報錯。

3.2 使用虛函數解決菱形問題

鴨嘴獸具備哺乳動物、鳥類和爬行動物的特徵,這意味着Platypus類需要繼承Mammal、Bird和Reptile。這些類都是從同一個類派生而來:

實例化Platypus時,結果會如何呢?對於每個Platypus實例,將實例化多少個Animal實例呢?

#include <iostream> 
using namespace std;

class Animal
{
public:
Animal()
{
cout<<"Animal constructor"<<endl;

int age;
}
};

class Mammal:public Animal
{

};

class Bird:public Animal
{

};

class Reptile:public Animal
{

};

class Platypus:public Mammal,public Bird,public Reptile
{
public:
platypus()
{
cout<<"Platypus constructor"<<endl;
}
};

int main()
{
Platypus duckBilledP;

return 0;
}

輸出表明,由於採用了多繼承,且Platypus的全部三個基類都是從Animal派生而來的,因此創建Platypus實例時,自動創建了三個Animal實例。這比較扯淡,因爲鴨嘴獸是一種動物,繼承了三個類的屬性。存在多個Animal的問題並非僅限於會佔用更多內存。Animal有一個整型成員——Animal::Age,因此爲方便說明,將其聲明爲公有的了。但是如果像通過Platypus實例訪問Animal::Age,將導致編譯錯誤,因爲編譯器不知道要設置Mammal::Animal::Age、Bird::Animal::Age還是Reptile::Animal::Age。

如果願意,可以分別設置這三個屬性:

duckBilledP.Mammal::Animal::Age = 25;
duckBilledP.Bird::Animal::Age = 25;
duckBilledP.Reptile::Animal::Age = 25;

顯然,鴨嘴獸應該只有一個Age屬性,但也希望Platypus公有繼承Mammal、Bird和Reptile。解決方案是使用虛繼承。如果派生類可能被作爲基類,派生它時最好使用換剪子virtual:

class Derived1:public virtual Base
{
\\... members and functions
};

class Derived2:public virtual Base
{
\\... members and functions
};

且看下面的代碼:

#include <iostream>
using namespace std;

class Animal:
{
public:
Animal()
{
cout<<"Animal constructor"<<endl;
}

int Age;
};

class Mammal:public virtual Animal
{

};

class Bird:public virtual Animal
{

};

class Reptile:public virtual Animal
{

};

class Platypus:public Mammal,public Bird,public Reptile
{
public:
Platypus()
{
cout<<"Platypus constructor"<<endl;
}

};

int main()
{
Platypus duckBilledP;
duckBilledP.Age = 25;
return 25;
}

完美解決上面的問題。

注意,再繼承層次結構中,繼承多個從同一個類派生而來的基類時,如果這些基類沒有采用虛繼承,將導致二義性。這中二義性稱爲菱形問題。

C++關鍵字virtual的含義隨上下文而異,總結如下:

在函數聲明中,virtual意味着當基類指針指向派生對象時,通過它可以調用派生類的相應函數。從Base類派生出Derived1和Dreived2類時,如果使用關鍵字virtual,則意味着再從Derived1和Derived2派生出Derived3時,每個Derived3實例只包含一個Base實例。綜上,關鍵字virtual被用於實現兩個不同的概念。

3.3 可將複製構造函數聲明爲虛函數麼?

這部分先掠過。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章