1. 程式人生 > >OOP3(繼承中的類作用域/構造函數與拷貝控制/繼承與容器)

OOP3(繼承中的類作用域/構造函數與拷貝控制/繼承與容器)

-a 控制 拷貝控制 函數調用 iostream 分配 類繼承 導致 每一個

當存在繼承關系時,派生類的作用域嵌套在其基類的作用域之內。如果一個名字在派生類的作用域內無法正確解析,則編譯器將繼續在外層的基類作用域中尋找該名字的定義

在編譯時進行名字查找:

一個對象、引用或指針的靜態類型決定了該對象的哪些成員是可見的,即使靜態類型與動態類型不一致:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 class A{
 5 public:
 6     // A();
 7     // ~A();
 8     ostream& print(ostream& os) const {
 9
os << x; 10 return os; 11 } 12 13 protected: 14 int x; 15 }; 16 17 class B : public A{ 18 public: 19 // B(); 20 // ~B(); 21 ostream& f(ostream &os) const { 22 os << y; 23 return os; 24 } 25 26 private: 27 int y; 28 }; 29 30
int main(void) { 31 B b; 32 33 b.f(cout) << endl;//正確,b的動態類型和靜態類型都是B,B::f對b是可見的 34 35 A *a = &b; 36 // b->f(cout) << endl;//錯誤,靜態類型是A,B::f對A的對象是不可見的 37 38 B *p = &b; 39 p->f(cout) << endl;//正確,靜態類型是B,B::f對B的對象是可見的 40 41 return 0; 42 }
View Code

名字沖突與繼承:

派生類的成員將隱藏同名的基類成員:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 struct Base{
 5     Base() : mem(0) {}
 6 
 7 protected:
 8     int mem;
 9 };
10 
11 struct Derived : Base{
12     Derived(int i) : mem(i) {}
13 
14     int get_mem() {
15         return mem;
16     }
17 
18 protected:
19     int mem;//隱藏基類中的mem
20 };
21 
22 int main(void) {
23     Derived d(42);
24     cout << d.get_mem() << endl;//42
25 
26     return 0;
27 }
View Code

通過域作用符可以使用隱藏的成員:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 struct Base{
 5     Base() : mem(0) {}
 6 
 7 protected:
 8     int mem;
 9 };
10 
11 struct Derived : Base{
12     Derived(int i) : mem(i) {}
13 
14     int get_mem() {
15         // return mem;
16         return Base::mem;
17     }
18 
19 protected:
20     int mem;//隱藏基類中的mem
21 };
22 
23 int main(void) {
24     Derived d(42);
25     cout << d.get_mem() << endl;//0
26 
27     return 0;
28 }
View Code

c++ 成員函數調用過程。假設我們調用 p_>mem()(或者 obj.mem()):

首先確定 p(或 obj) 的靜態類型。

在 p(或 obj) 的靜態類型對應的類中查找 mem。如果找不到,則依次在直接基類中不斷查找直至達到繼承鏈的頂端。如果仍然找不到則編譯器報錯

一旦找到了 mem,就進行常規的類型檢查以確認對於當前找到的 mem,本次調用是否合法。

假設調用合法,則編譯器將根據調用的是否是虛函數而產生不同的代碼:

——如果 mem 是虛函數且我們是通過引用或指針進行的調用,則編譯器產生的代碼將在運行時確定到底運行哪個版本,依據是對象的動態類型

——反之,如果 mem 不少虛函數或者我們是通過對象(非指針或引用)進行的調用,則編譯器將產生一個常規函數調用

名字查找先於類型檢查:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 struct Base{
 5     int memfcn();
 6 };
 7 
 8 int Base::memfcn() {
 9     //
10 }
11 
12 struct Derived : Base{
13     int memfcn(int);//隱藏基類的memfcn,即便形參不同
14 };
15 
16 int Derived::memfcn(int a) {
17     //
18 }
19 
20 int main(void) {
21     Derived d;
22     Base b;
23 
24     b.memfcn();//調用Base::memfcn
25     d.memfcn(10);//調用Derived::memfcn
26 
27     // d.memfcn();//錯誤,參數列表為空的memfcn被隱藏了
28     d.Base::memfcn();//正確,調用Base::memfcn
29 
30     return 0;
31 }
View Code

註意:如前所述,聲明在內層作用域的函數並不會重載聲明在外層作用域的函數。如果派生類的成員與基類的某個成員同名,則派生類將在其作用域內隱藏該基類成員。即使派生類成員和基類成員的形參列表不一致,基類成員仍然會被隱藏

虛函數與作用域:

由上面這段話我們可以理解為什麽基類與派生類中的虛函數必須有相同的形參列表了。假如基類與派生類的虛函數形參列表不同,則基類的同名函數會在派生類中被隱藏,我們也就無法通過基類的引用或指針調用派生類的虛函數了:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base{
 5 public:
 6     virtual int fcn();
 7 };
 8 
 9 int Base::fcn() {
10     cout << "int Base::fcn" << endl;
11 }
12 
13 class D1 : public Base{
14 public:
15     // 隱藏基類的fcn,這個fcn不是虛函數
16     // D1繼承了Base::fcn()的定義
17     int fcn(int);//形參列表與Base中的fcn不一致
18     virtual void f2(){//是一個新的虛函數,在Base中不存在
19         cout << "void D1::f2" << endl;
20     }
21 };
22 
23 int D1::fcn(int a) {
24     cout << "int D1::fcn int" << endl;
25 }
26 
27 // void D1::f2() {
28 
29 // }
30 
31 class D2 : public D1{
32 public:
33     int fcn(int);//是一個非虛函數,隱藏了D1::fcn(int)
34     int fcn();//覆蓋了Base的虛函數fcn
35     void f2();//覆蓋了D1的虛函數f2
36 };
37 
38 int D2::fcn(int a) {
39     cout << "int D2::fcn int" << endl;
40 }
41 
42 int D2::fcn() {
43     cout << "int D2::fcn" << endl;
44 }
45 
46 void D2::f2() {
47     cout << "void D2::f2" << endl;
48 }
49 
50 int main(void) {
51     Base bobj;
52     D1 d1obj;
53     D2 d2obj;
54 
55     Base *bp1 = &bobj;
56     Base *bp2 = &d1obj;
57     Base *bp3 = &d2obj;
58     bp1->fcn();//虛調用,將在運行時調用Base::fcn
59     bp2->fcn();//虛調用,將在運行時調用Base::fcn,因為在D1中沒有覆蓋Base::fcn
60     bp3->fcn();//虛調用,將在運行時調用D2::fcn,D2中覆蓋了Base::fcn
61     cout << endl;
62 
63     D1 *d1p = &d1obj;
64     D2 *d2p = &d2obj;
65     
66     // bp2->f2();//錯誤,靜態類型Base中沒有名為f2的成員
67 
68     d1p->f2();//虛調用,將在運行時調用D1::f2
69     d2p->f2();//虛調用,將在運行時調用D2::f2
70     cout << endl;
71 
72     Base *p1 = &d2obj;
73     D1 *p2 = &d2obj;
74     D2 *p3 = &d2obj;
75 
76     // p1->fcn(42);//錯誤,Base中沒有接受一個int的fcn
77     p2->fcn(42);//靜態類型D1中的fcn(int)是一個非虛函數,執行靜態綁定,調用D1::fcn(int)
78     p3->fcn(42);//靜態類型D2中的fcn(int)是一個非虛函數,執行靜態綁定,調用D2::fcn(int)
79 
80 // 輸出:
81 // int Base::fcn
82 // int Base::fcn
83 // int D2::fcn
84 
85 // void D1::f2
86 // void D2::f2
87 
88 // int D1::fcn int
89 // int D2::fcn int
90     return 0;
91 }
View Code

註意:如果派生類中沒有覆蓋基類中的虛函數,則運行時解析為基類定義的版本

覆蓋重載的函數:

成員函數無論是否是虛函數都能被重載。派生類可以覆蓋重載函數的 0 個或多個實例。如果派生類希望所有的重載版本對於它來說都是可見的,那麽它就需要覆蓋所有版本,或者一個也不覆蓋。

我們可以為重載的成員提供一條 using 聲明語句,這樣我們就無需覆蓋基類中的每一個版本。using 聲明指定一個名字而不指定形參列表,所以一條基類成員函數的 suing 聲明語句就可以把該函數的所有重載實例添加到派生類的作用域中。此時,派生類只需要定義其特有的函數就可以了,而無需為繼承而來的其它函數重新定義。

構造函數與拷貝控制:

虛析構函數:

如果基類的析構函數不是虛函數,則 delete 一個指向派生類對象的基類指針將產生未定義的行為。因此我們通常應該給基類定義一個虛析構函數。同時,定義了析構函數應該定義拷貝和賦值操作這條準則在這裏不適用。還需要註意的是,定義了任何拷貝控制操作後編譯器都不會再合成移動操作

合成拷貝控制與繼承:

基類或派生類的合成拷貝控制成員的行為與其它合成的構造函數、賦值運算符或析構函數類似:它們對類本身的成員一次進行初始化、賦值或銷毀操作。此外,這些合成的成員還負責適用直接基類中對應的操作對一個對象的直接基類部分進行初始化、賦值或銷毀的操作

派生類中刪除的拷貝控制與基類的關系:

就像其它任何類的情況一樣,基類或派生類也能處於同樣的原因將其合成默認構造函數或者任何一個拷貝控制成員被定義成刪除的函數。此外,某些定義基類的方式也可能導致有的派生類成員城外刪除的函數:

如果基類中的默認構造函數、拷貝構造函數、拷貝賦值運算符或析構函數是被刪除的函數或不可訪問的,則派生類中對應的成員將是被刪除的,原因是編譯器不能適用基類成員來執行派生類對象基類部分的構造、賦值或銷毀操作

如果在基類中有一個不可訪問或刪除的析構函數,則派生類中合成的默認和拷貝構造函數將是被刪除的,因為編譯器無法銷毀派生類的基類部分

編譯器不會合成一個刪除掉的移動操作。當我們使用 =default 請求一個移動操作時,如果基類中的對應操作是刪除的或不可訪問的,那麽派生類中該函數將是被刪除的,原因是派生類對象的基類部分不可移動。同樣,如果基類的析構函數是刪除的或不可訪問的,則派生類的移動構造函數也將是被刪除的:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 class B{
 5 public:
 6     B(){}
 7     B(const B&) = delete;
 8     // ~B();
 9 };
10 
11 class D : public B{
12 public:
13     // D();
14     // ~D();
15     
16 };
17 
18 
19 int main(void) {
20     D d;//正確,D的合成默認構造函數使用B的默認構造函數
21     // D d2(d);//錯誤,D的合成拷貝構造函數是被刪除的
22     // D d3(std::move(d));//錯誤,沒有移動構造函數,所以會調用拷貝構造函數,但是D的合成拷貝構造函數是刪除的
23 
24     return 0;
25 }
View Code

移動操作與繼承:

大多數基類都會定義一個虛析構函數。因此在默認情況下,基類通常不含有合成的移動操作,而且在它的派生類中也沒有合成的移動操作。因為基類缺少移動操作會阻止派生類擁有自己的合成移動操作(派生類的合成移動構造函數會調用基類的移動構造函數來完成繼承自基類的數據成員的移動操作),所以當我們確實需要執行移動操作時應該首先在基類中定義:

技術分享圖片
 1 class Quote{
 2 public:
 3     Quote() = default;
 4     Quote(const Quote&) = default;
 5     Quote(Quote&&) = default;
 6     Quote& operator=(const Quote&) = default;
 7     Quote& operator=(Quote&&) = default;
 8     ~Quote() = default;
 9     
10 };
View Code

註意:一旦基類定義了自己的移動操作,那麽它必須同時顯式地定義拷貝操作,否則拷貝操作成員將被默認合成為刪除函數

派生類的拷貝控制成員:

移動構造函數在拷貝和移動自有成員的同時,也要拷貝和移動基類部分的成員。類似的,派生類賦值運算符也必須為其基類部分的成員賦值。和構造函數及賦值運算符不同的是,析構函數只負責銷毀派生類自己分配的資源。對象的成員是被隱式銷毀的,類似的,派生類對象的基類部分也是自動銷毀的:

技術分享圖片
1 class D : public Base{
2 public:
3     //Base::~Base被自動調用
4     ~D(){
5         // 該處由用戶定義釋放派生類資源的操作
6     }
7     
8 };
View Code

對象銷毀的順序與創建的順序相反

註意:在默認情況下,基類默認構造函數初始化派生類對象的基類部分。如果我們想拷貝(賦值或移動)基類部分,則必須在派生類的構造函數初始值列表中顯式地使用基類的拷貝(賦值或移動)構造函數

不要在構造函數和析構函數中調用虛函數:

如果構造函數或析構函數調用了某個虛函數,則執行與構造函數或析構函數所屬類型相對應的虛函數版本(這可能不是我們所期望的)

詳見:http://blog.csdn.net/xtzmm1215/article/details/45130929

繼承的構造函數:

構造函數不能以常規的方法繼承:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 class A{
 5 public:
 6     A(int a = 0, int b = 0) : x(a), y(b) {}
 7     int get_x(void) const {
 8         return x;
 9     }
10 
11     int get_y(void) const {
12         return y;
13     }
14 
15 protected:
16     int x, y;
17 };
18 
19 class B : public A{
20     // 沒有使用 using 聲明來繼承構造函數,所以 B 沒有繼承 A(int a, int b)
21     // 由於我們沒有在 B 中定義構造函數,所以 B 中會合成默認構造函數
22 };
23 
24 int main(void) {
25     // B b(1, 2);//錯誤,不能使用構造函數
26     B b;//使用 B 類中編譯器合成的默認構造函數
27     cout << b.get_x() << " " << b.get_y() << endl;//0 0 
28     // 派生類的合成默認構造函數會自動調用基類的默認構造函數來初始化基類的數據成員
29 
30     return 0;
31 }
View Code

我們可以通過 using 聲明來使派生類繼承基類的構造函數:

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 class A{
 5 public:
 6     A() : x(-1), y(-1) {}
 7 
 8     A(int a, int b) : x(a), y(b) {}
 9     int get_x(void) const {
10         return x;
11     }
12 
13     int get_y(void) const {
14         return y;
15     }
16 
17 protected:
18     int x, y;
19 };
20 
21 class B : public A{
22     using A::A;//通過using說明,繼承了 A 中定義的構造函數
23     // 對於基類的每個構造函數,編譯器都在派生類中生成一個形參列表與之完全相同的構造函數
24 
25     //派生類不會繼承基類的默認構造函數, 由於我們沒有在 B 中定義默認構造函數,所以 B 中會合成默認構造函數
26 };
27 
28 int main(void) {
29     B b(1, 2);//通過 using 聲明,B 繼承了 A 中定義的構造函數
30     cout << b.get_x() << " " << b.get_y() << endl;//1 2 
31 
32     B c;//使用合成的默認構造函數
33     cout << c.get_x() << " " << c.get_y() << endl;//-1 -1
34     // 派生類的合成默認構造函數會自動調用基類的構造函數來初始化基類的數據成員
35 
36     return 0;
37 }
View Code

註意:通常情況下,using 聲明只是令某個名字在當前作用域內可見。而當作用於構造函數時,using 聲明語句將令編譯器產生代碼,但不會改變該構造函數的訪問級別。對於基類的每個構造函數,編譯器都生成一個與之對應的派生類構造函數。換句話說,對於基類的每個構造函數,編譯器都在派生類中生成一個形參列表與之完全相同的構造函數。

一個 using 聲明不能指定 explicit 或 constexpr。如果基類的構造函數是 explicit 或者 constexpr 的,則其繼承的構造函數也擁有相同的屬性

派生類類不能繼承默認,拷貝和移動構造函數。如果派生類沒有直接定義這些構造函數,則編譯器將為派生類合成它們。

技術分享圖片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 class A{
 5 public:
 6     A() : x(-1), y(-1) {//默認構造函數
 7         cout << "ji lei mo ren gou zao han shu" << endl;
 8     }
 9     A(int a, int b) : x(a), y(b) {//構造函函數
10         cout << "ji lei gou zao han shu" << endl;
11     }
12     A(const A &a) : x(a.x), y(a.y) {//拷貝構造函數
13         cout << "ji lei kao bei gou zao han shu" << endl;
14     }
15     A(A &&a) : x(a.x), y(a.y) {//移動構造函數
16         cout << "ji lei yi dong gou zao han shu" << endl;
17     }
18 
19     virtual A& operator=(const A &a) {//可以寫成虛函數,說明拷貝賦值運算符會被派生類繼承
20         this->x = a.x;
21         this->y = a.y;
22         cout << "ji lei kao bei fu zhi yun suan fu" << endl;
23         return *this;
24     }
25 
26     virtual A& operator=(A &&a) {//可以寫成虛函數,說明移動賦值運算符會被派生類繼承
27         this->x = a.x;
28         this->y = a.y;
29         cout << "ji lei yi dong fu zhi yun suan fu" << endl;
30         return *this;        
31     }
32 
33 protected:
34     int x, y;
35 };
36 
37 class B : public A{
38     using A::A;//通過using說明,繼承了 A 中定義的構造函數
39     // 對於基類的每個構造函數,編譯器都在派生類中生成一個形參列表與之完全相同的構造函數
40 
41     //派生類不會繼承基類的默認、拷貝、移動構造函數, 
42     //由於我們沒有在 B 中定義默認構造函數,所以 B 中會合成默認構造函數,
43     //又由於我們沒有在派生類中定義任何拷貝控制成員,所以會合成拷、移動構造函數
44 };    
45 
46 int main(void) {
47     B b(1, 2);//通過 using 聲明,B 繼承了 A 中定義的構造函數
48     cout << endl;
49 
50     B c;//使用合成的默認構造函數
51     // 派生類的合成默認構造函數會自動調用基類的構造函數來初始化基類繼承自部分的數據成員
52     cout << endl;
53 
54     B d = c;
55     // 派生類的合成拷貝構造函數會自動調用基類的拷貝構造函數來拷貝繼承自基類部分的數據成員
56     cout << endl;
57 
58     d = c;//使用繼承自基類的拷貝賦值運算符
59     cout << endl;
60 
61     B e = std::move(b);
62     // 派生類的合成移動構造函數會自動調用基類的移動構造函數來移動繼承自基類部分的數據成員
63     cout << endl;
64 
65     e = std::move(b);//使用繼承自基類的移動賦值運算符
66     cout << endl;
67 
68     return 0;
69 }
View Code

註意:

派生類的合成默認構造函數、合成拷貝構造函數、合成移動構造函數中會自動使用基類的對應構造函數來操作派生類中繼承自基類部分數據成員,而派生類的新成員執行默認初始化

定義派生類的默認、拷貝、移動構造函數時我們應該調用基類中的對應操作來完成繼承自基類部分的數據成員的操作,否則我們可能無法完成繼自基類的 private 數據成員的操作

當我們在派生類中覆蓋拷貝、移動賦值運算符時,應該調用基類中的對應操作來完成繼承自基類部分的數據成員的操作,否則我們可能無法完成繼自基類的 private 數據成員的操作

當一個基類構造函數含有默認實參時,這些實參並不會被繼承。相反,派生類將獲得多個繼承的構造函數,其中每個構造函數分別省略掉一個含有默認實參的形參

容器與繼承:

當我們希望在容器中存儲具有繼承關系的對象時,在容器中存放基類(智能)指針而非對象 ,因為其動態類型既可以是基類類型,也可以是派生類類型

OOP3(繼承中的類作用域/構造函數與拷貝控制/繼承與容器)