C++面向對象總結——多態


引言

了解c++的三大特性是對c++的整體的認識。

  • 封裝性: 類將成員變量和成員函數封裝在類的內部,根據需要設置訪問權限,通過成員函數管理內部狀態(用訪問修飾符設置)

  • 繼承:繼承所表達的是類之間相關的關系,這種關系使得對象可以繼承另外一類對象的特征和能力。作用:避免公用代碼的重復開發,減少代碼和數據冗余。
  • 多態:多態性可以簡單地概括為“一個接口,多種方法”,字面意思為多種形態。程序在運行時才決定調用的函數,它是面向對象編程領域的核心概念。比如函數重載、運算符重載、虛函數等

前些章已經介紹了繼承,重載,本篇就在此基礎上詳說一下多態。


 一,C++ 多態

多態按字面的意思就是多種形態。當類之間存在層次結構,並且類之間是通過繼承關聯時,就會用到多態。

C++ 多態意味着調用成員函數時,會根據調用函數的對象的類型來執行不同的函數。

下面的實例中,基類 Shape 被派生為Rectangle類,如下所示:

#include <iostream> 
using namespace std;

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
    int area()
    {
        cout << "Parent class area :" << endl;
        return 0;
    }
};
//將Rectangle類繼承Shape類
class Rectangle : public Shape {
public:
    Rectangle(int a,int b) :Shape(a, b) { }
    int area()
    {
        cout << "Rectangle class area :" <<width*height<< endl;
        return 0;
    }
};

// 程序的主函數
int main()
{
    Shape* shape;//定義shpae類指針
    Rectangle rec(10, 7);//派生類對象
    // 基類指針指向派生類對象(存儲矩形的地址)
    shape = &rec;
    // 調用矩形的求面積函數 area
    shape->area();
    return 0;
}

 可以發現運行結果和我們期望的不一樣。什么原因造成的呢?

我們直觀上認為,如果指針指向了派生類對象,那么就應該使用派生類的成員變量和成員函數,這符合人們的思維習慣。但是本例的運行結果卻告訴我們,當基類指針 shape指向派生類 Rectangle的對象時,雖然使用了 Rectangle的成員變量,但是卻沒有使用它的成員函數,導致輸出結果不符合我們的預期。

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

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

但現在,讓我們對程序稍作修改,在 Shape 類中,area() 的聲明前放置關鍵字 virtual,如下所示:

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
   virtual int area()
    {
        cout << "Parent class area :" << endl;
        return 0;
    }
};

修改后,當編譯和執行前面的實例代碼時,它會產生以下結果:(運行成功!)

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

二,虛函數

虛函數對於多態具有決定性的作用,有虛函數才能構成多態,這節我們來重點說一下虛函數的注意事項。

  • 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
  • 為了方便,可以只將基類中的函數聲明為虛函數,這樣所有派生類中具有遮蔽(覆蓋)關系的同名函數都將自動成為虛函數。

C++繼承時的名字遮蔽

1️⃣如果派生類中的成員(包括成員變量和成員函數)和基類中的成員重名,那么就會遮蔽從基類繼承過來的成員。(即使用派生類新增的成員)

2️⃣基類成員和派生類成員的名字一樣時會造成遮蔽,這句話對於成員變量很好理解,對於成員函數要引起注意,不管函數的參數如何,只要名字一樣就會造成遮蔽。換句話說,基類成員函數和派生類成員函數不會構成重載,如果派生類有同名函數,那么就會遮蔽基類中的所有同名函數,不管它們的參數是否一樣。


  •  當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那么將使用基類的虛函數。
  • 只有派生類的虛函數遮蔽基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。例如基類虛函數的原型為virtual void func();,派生類虛函數的原型為virtual void func(int);,那么當基類指針 p 指向派生類對象時,語句p -> func(100);將會出錯,而語句p -> func();將調用基類的函數。
  • 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明為虛函數沒有什么意義。
  • 析構函數可以聲明為虛函數,而且有時候必須要聲明為虛函數。

🧡構成多態的條件

下面是構成多態的條件:

  • 必須存在繼承關系;
  • 繼承關系中必須有同名的虛函數,並且它們是遮蔽(覆蓋)關系。
  • 存在基類的指針,通過該指針調用虛函數。

下面的例子對各種混亂情形進行了演示:

#include <iostream> 
using namespace std;

//基類Base
class base
{
public:
    virtual void func()
    {
        cout << "void Base::func()" << endl;
    }
    virtual void func(int)
    {
        cout << "void Base::func(int)" << endl;
    }
};
//派生類Derived
class Dervied :public base
{
public:
    void func()
    {
        cout << "void Derived::func()" << endl;
    }
    void func(char*str)
    {
        cout << "void Derived::func(char *shr)" << endl;
    }
};
int main()
{
    base* p = new Dervied();//創建基類指針*p指向派生類對象
    p->func();
    p->func(10);
    //p->func("學習c++");//報錯
}

輸出結果:

在基類 Base 中我們將void func()聲明為虛函數,這樣派生類 Derived 中的void func()就會自動成為虛函數。p 是基類 Base 的指針,但是指向了派生類 Derived 的對象。

語句p -> func();調用的是派生類的虛函數,構成了多態(由於派生類遮蔽了基類函數)

語句p -> func(10);調用的是基類的虛函數,因為派生類中沒有函數遮蔽它。

語句p -> func("學習c++");出現編譯錯誤,因為通過基類的指針只能訪問從基類繼承過去的成員,不能訪問派生類新增的成員。

💙純虛函數和抽象類

如果我們想要在基類中定義虛函數,以便在派生類中重新定義該函數更好地適用於對象,但是在基類中又不能對虛函數給出有意義的實現,這個時候就會用到純虛函數,語法格式為:

virtual 返回值類型 函數名 (函數參數) = 0;

 我們可以把基類中的虛函數 area() 改寫如下:

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
    //純虛函數
    virtual int area() = 0;
};

area()= 0並不表示函數返回值為0,它只起形式上的作用,告訴編譯系統“這是純虛函數”。

包含純虛函數的類稱為抽象類(Abstract Class)。之所以說它抽象,是因為它無法實例化,也就是無法創建對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法為其分配內存空間。

抽象類通常是作為基類,讓派生類去實現純虛函數。派生類必須實現純虛函數才能被實例化。

純虛函數使用舉例:

#include <iostream> 
using namespace std;

//基類 line
class line
{
public:
    line(float len):m_len(len){}//初始化列表
    virtual float area() = 0;//純虛函數
    virtual float volume() = 0;//純虛函數
protected:
    float m_len;
};

//派生類 rect:基類 line
class rect :public line
{
public:
    rect(float len,float width):line(len),m_width(width){}
    float area()
    {
        return m_len * m_width;
    }
protected:
    float m_width;
};

//派生類 cuboid:基類 rect
class cuboid :public rect
{
public:
    cuboid(float len, float width, float height) :rect(len, width), m_height(height) {}
    float volume()
    {
        return m_len * m_width * m_height;
    }
protected:
    float m_height;
};
int main()
{
    line* p = new cuboid(10,20,30);//基類指針指向派生類對象
    cout << "The area of Cuboid is " << p->area() << endl;
    cout << "The volume of Cuboid is " << p->volume() << endl;
}

 本例定義了三個類,其繼承關系為:line->rect->cuboid。

line是一個抽象類,也是最頂層的基類,在 line類中定義了兩個純虛函數 area() 和 volume()。

  • 在 rect類中,實現了 area() 函數;所謂實現,就是定義了純虛函數的函數體。但這時 rect類仍不能被實例化,因為它沒有實現繼承來的 volume() 函數,volume() 仍然是純虛函數,所以 rect也仍然是抽象類。
  • 直到 cuboid類,才實現了 volume() 函數,才是一個完整的類,才可以被實例化。

可以發現,line類表示“線”,沒有面積和體積,但它仍然定義了 area() 和 volume() 兩個純虛函數。這樣的用意很明顯:line類不需要被實例化,但是它為派生類提供了“約束條件”,派生類必須要實現這兩個函數,完成計算面積和體積的功能,否則就不能實例化。
在實際開發中,你可以定義一個抽象基類,只完成部分功能,未完成的功能交給派生類去實現(誰派生誰實現)。這部分未完成的功能,往往是基類不需要的,或者在基類中無法實現的。雖然抽象基類沒有完成,但是卻強制要求派生類完成,這就是抽象基類的“霸王條款”。

 抽象基類除了約束派生類的功能,還可以實現多態。指針 p 的類型是 line,但是它卻可以訪問派生類中的 area() 和 volume() 函數,正是由於在 line類中將這兩個函數定義為純虛函數;如果不這樣做,后面的代碼都是錯誤的。我想,這或許才是C++提供純虛函數的主要目的。

關於純虛函數的幾點說明:

1) 一個純虛函數就可以使類成為抽象基類,但是抽象基類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。
2) 只有類中的虛函數才能被聲明為純虛函數,普通成員函數和頂層函數均不能聲明為純虛函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM