28、不一樣的C++系列--繼承與多型
阿新 • • 發佈:2019-01-27
繼承與多型
父子間的同名衝突
首先來看一段程式碼:
#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++中支援多型
的唯一方式- 被重寫的
虛擬函式
可表現出多型的特性