1. 程式人生 > >28、不一樣的C++系列--繼承與多型

28、不一樣的C++系列--繼承與多型

繼承與多型

父子間的同名衝突

首先來看一段程式碼:

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    int mi;
};

class Child : public Parent
{
public:
    int mi;
};

int main()
{
    Child c;

    //這裡的mi是Parent中的還是Child中的呢?
    c.mi = 100;

    return 0;
}

編譯通過,說明子類可以定義和父類相同的同名成員。

  • 子類可以定義父類中的同名成員
  • 子類中的成員將隱藏父類中的同名成員
  • 父類中的同名成員依然存在於子類中
  • 通過作用域分辨符( : : )訪問父類中的同名成員
  • 訪問父類中的同名成員
Child c;

//子類中的mi
c.mi = 100;

//父類中的mi
c.Parent::mi = 1000;

再來看一個例子:

#include <iostream>
#include <string>

using namespace std;

//定義一個名稱空間A
namespace A
{
    int g_i = 0;
}
//定義一個名稱空間B
namespace B
{
    int
g_i = 1; } class Parent { public: int mi; Parent() { cout << "Parent() : " << "&mi = " << &mi << endl; } }; class Child : public Parent { public: int mi; Child() { cout << "Child() : " << "&mi = " << &mi << endl; } }; int
main() { Child c; //向子類的mi成員賦值100 c.mi = 100; //通過作用域向父類的mi成員賦值1000 c.Parent::mi = 1000; //列印子類中mi的地址 cout << "&c.mi = " << &c.mi << endl; //列印子類中mi的內容 cout << "c.mi = " << c.mi << endl; //列印父類中mi的地址 cout << "&c.Parent::mi = " << &c.Parent::mi << endl; //列印父類中mi的內容 cout << "c.Parent::mi = " << c.Parent::mi << endl; return 0; }

輸出結果為:

Parent() : &mi = 0x7fff57f57a90
Child() : &mi = 0x7fff57f57a94
&c.mi = 0x7fff57f57a94
c.mi = 100
&c.Parent::mi = 0x7fff57f57a90
c.Parent::mi = 1000
  • 類中的成員函式可以進行過載
    • 過載函式的本質為多個不同的函式
    • 函式名和引數列表是唯一的標識
    • 函式過載必須發生在同一個作用域中
    • 所以父子之間的同名成員不構成過載

比如像這樣:

class Parent
{
public:
    int mi;

    void add(int v)
    {
        mi += v;
    }

    void add(int a, int b)
    {
        mi += (a + b);
    }
};

class Child : public Parent
{
public:
    int mi;

    void add(int v)
    {
        mi += v;
    }

    void add(int a, int b)
    {
        mi += (a + b);
    }

    void add(int x, int y, int z)
    {
        mi += (x + y + z);
    }
};

程式碼 Parent類 和 Child類 中有同名的函式add ,但是兩個類之間不構成過載,只有Parent類中多個add函式構成過載。

  • 子類中的函式將隱藏父類的同名函式
  • 子類無法過載父類中的成員函式
  • 使用作用域分辨符訪問父類中的同名函式
  • 子類可以定義父類中完全相同的成員函式

父子間的賦值相容

  • 子類物件可以當作父類物件使用(相容性)
    • 子類物件可以直接賦值給父類物件
    • 子類物件可以直接初始化父類物件
    • 父類指標可以直接指向子類物件
    • 父類引用可以直接引用子類物件

舉個例子:

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    int mi;

    void add(int i)
    {
        mi += i;
    }

    void add(int a, int b)
    {
        mi += (a + b);
    }
};

class Child : public Parent
{
public:
    int mv;

    void add(int x, int y, int z)
    {
        mv += (x + y + z);
    }
};

int main()
{
    Parent p;
    Child c;
    //子類物件可以直接賦值給父類物件
    p = c;
    //子類物件可以直接初始化父類物件
    Parent p1(c);
    //父類引用可以直接引用子類物件
    Parent& rp = c;
    //父類指標可以直接指向子類物件
    Parent* pp = &c;

    return 0;
}

在main函式中進行上述幾條的操作都沒有出現編譯出錯。現在進行這樣操作:

rp.mi = 100;
rp.add(5);        
rp.add(10, 10);  

發現可以編譯通過,並沒有出現同名覆蓋的問題。但是如果這樣操作:

pp->mv = 1000;
pp->add(1, 10, 100);

執行以後就會報錯,報錯資訊如下:

48-1.cpp:51:10: error: no member named 'mv' in 'Parent'
     pp->mv = 1000;
     ~~  ^
48-1.cpp:52:10: error: no matching member function for call to 'add'
     pp->add(1, 10, 100);
     ~~~~^~~
48-1.cpp:16:10: note: candidate function not viable: requires 2 arguments, but 3
      were provided
    void add(int a, int b)
         ^
48-1.cpp:11:10: note: candidate function not viable: requires single argument
      'i', but 3 arguments were provided
    void add(int i)
         ^
2 errors generated.

資訊提示沒有找到帶有3個引數的add函式。為什麼呢?

  • 當使用父類指標(引用)指向子類物件時
    • 子類物件退化為父類物件
    • 只能訪問父類中定義的成員
    • 可以直接訪問被子類覆蓋的同名成員

特殊的同名函式

  • 子類中可以衝定義父類中已經存在的成員函式
  • 這種衝定義發生在繼承中,叫做函式重寫
  • 函式重寫是同名覆蓋的一種特殊情況

例如:

class Parent
{
    public:
        void print()
        {
            cout << "I'm Parent." << endl;
        }
};

//函式重寫

class Child : public Parent
{
    public:
        void print()
        {
            cout << "I'm Child" << endl;
        }
};

假如函式重寫和賦值相容同時出現呢? 就像這樣:

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    int mi;

    void add(int i)
    {
        mi += i;
    }

    void add(int a, int b)
    {
        mi += (a + b);
    }

    void print()
    {
        cout << "I'm Parent." << endl;
    }
};

class Child : public Parent
{
public:
    int mv;

    void add(int x, int y, int z)
    {
        mv += (x + y + z);
    }

    void print()
    {
        cout << "I'm Child." << endl;
    }
};

void how_to_print(Parent* p)
{
    p->print();
}

int main()
{
    Parent p;
    Child c;

    how_to_print(&p);    // Expected to print: I'm Parent.
    how_to_print(&c);    // Expected to print: I'm Child.

    return 0;
}

預期輸出是 I'm Parent.I'm Child. 。但是實際輸出:

I'm Parent.
I'm Parent.
  • 問題分析
    • 編譯期間,編譯器只能根據指標的型別判斷所指向的物件
    • 根據賦值相容,編譯器認為父類指標指向的是父類物件
    • 因此,編譯結果只可能是呼叫父類中定義的同名函式

在編譯 void how_to_print(Parent* p) 這個函式時,編譯器不可能知道指標p究竟指向了什麼,但是編譯器沒有理由報錯。於是,編譯器認為最安全的做法是呼叫父類的print函式,因為父類和子類肯定都有相同的print函式。

多型的概念和意義

  • 函式重寫回顧
    • 父類中被重寫的函式依然會繼承給子類
    • 子類中重寫的函式將覆蓋父類中的函式
    • 通過作用域分辨符( : : )可以訪問到父類中的函式

就像這樣:

Child c;
Parent* p = &c;

c.Parent::print();  //從父類中繼承
c.print();          //從子類中重寫

p->print();         //父類中定義

雖然程式邏輯是這樣,但並不是我們所期望的。面向物件中期望的行為:

  • 根據 實際的物件型別 判斷如何呼叫重寫函式
  • 父類指標(引用) 指向
    • 父類物件 則呼叫 父類 中定義的函式
    • 子類物件 則呼叫 子類 中定義的重寫函式

這裡就引出了面向物件中的 多型 的概念:

  • 根據實際的 物件型別決定函式呼叫 的具體目標
  • 同樣的 呼叫語句 在實際執行時有 多種不同的表現形態

例如:

p->print();

p指向父類物件時,會執行

void print()
{
    cout << "I'm Parent" << end;
}

p指向子類物件時,會執行

void print()
{
    cout << "I'm Child" << endl;
}
  • C++語言直接支援多型的概念
    • 通過使用 virtual 關鍵字對多型進行支援
    • virtual 宣告的函式被重寫後具有多型特性
    • virtual 宣告的函式叫做虛擬函式

舉個例子:

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    //用 virtual 關鍵字修飾,則具有多型特性
    virtual void print()
    {
        cout << "I'm Parent." << endl;
    }
};

class Child : public Parent
{
public:
    void print()
    {
        cout << "I'm Child." << endl;
    }
};

void how_to_print(Parent* p)
{
    // 展現多型的行為
    p->print();     
}

int main()
{
    Parent p;
    Child c;

    how_to_print(&p);    // Expected to print: I'm Parent.
    how_to_print(&c);    // Expected to print: I'm Child.

    return 0;
}

執行結果為:

I'm Parent.
I'm Child.
  • 多型的意義
    • 在程式執行過程中展現出動態的特性
    • 函式重寫必須多型實現,否則沒有意義
    • 多型是面向物件元件化程式設計的基礎特性

靜態聯編和動態聯編

  • 理論中的概念
    • 靜態聯編
      • 在程式的編譯期間就能確定具體的函式呼叫。 如:函式過載
    • 動態聯編
      • 在程式實際執行後才能確定具體的函式呼叫。如:函式重寫

舉個例子:

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    //函式過載  並且用 virtual 關鍵字修飾
    virtual void func()
    {
        cout << "void func()" << endl;
    }
    //函式過載  並且用 virtual 關鍵字修飾
    virtual void func(int i)
    {
        cout << "void func(int i) : " << i << endl;
    }
    //函式過載  並且用 virtual 關鍵字修飾
    virtual void func(int i, int j)
    {
        cout << "void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
    }
};

class Child : public Parent
{
public:
    //函式過載
    void func(int i, int j)
    {
        cout << "void func(int i, int j) : " << i + j << endl;
    }
    //函式過載
    void func(int i, int j, int k)
    {
        cout << "void func(int i, int j, int k) : " << i + j + k << endl;
    }
};

void run(Parent* p)
{
    p->func(1, 2);     // 展現多型的特性
                       // 動態聯編
}

int main()
{
    Parent p;

    p.func();         // 靜態聯編
    p.func(1);        // 靜態聯編
    p.func(1, 2);     // 靜態聯編

    cout << endl;

    Child c;

    c.func(1, 2);     // 靜態聯編

    cout << endl;

    run(&p);
    run(&c);

    return 0;
}

執行結果為:

void func()
void func(int i) : 1
void func(int i, int j) : (1, 2)

void func(int i, int j) : 3

void func(int i, int j) : (1, 2)
void func(int i, int j) : 3

小結

  • 子類可以定義父類的 同名成員 ,定義時子類中的成員將 隱藏 父類中的 同名成員
  • 子類和父類 中的函式 不能構成過載關係
  • 使用 作用域分辨符 可以訪問父類中的 同名成員
  • 子類物件 可以當做 父類物件 使用
  • 父類指標 可以正確的指向 子類物件
  • 父類引用 可以正確的代表 子類物件
  • 子類中可以重寫父類中的 成員函式
  • 函式重寫只可能發生在 父類與子類 之間
  • 多型是根據 實際物件的型別 確定呼叫的 具體函式
  • virtual關鍵字 是C++中支援 多型 的唯一方式
  • 被重寫的 虛擬函式 可表現出多型的特性