C++ Primer 筆記——OOP
1.基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作也是如此。
2.任何構造函數之外的非靜態函數都可以是虛函數,關鍵字virtual只能出現在類內部的聲明語句之前而不能用於類外部的函數定義。如果基類把一個函數聲明成虛函數,則該函數在派生類中隱式地也是虛函數。成員函數如果沒有被聲明成虛函數,則其解析過程發生在編譯時而非運行時。
3.繼承的訪問說明符的作用是控制派生類從基類繼承而來的成員是否對派生類用戶可見。
4.在一個對象中,繼承自基類的部分和派生類自定義的部分不一定是連續存儲的。C++標準並沒有明確規定派生類的對象在內存中如何分布。
5.在派生類對象中含有與其基類對應的組成部分,這一事實是繼承的關鍵所在。所以我們能把派生類的對象當成基類對象來使用,和其他類型一樣,編譯器會隱式的執行派生類到基類的轉換。
6.每個類控制它自己的成員初始化過程。盡管在派生類對象中含有從基類繼承而來的成員,但是派生類並不能直接初始化這些成員,而必須使用基類的構造函數來初始化它的基類部分。
7.派生類的初始化順序是首先初始化基類的部分,然後按照聲明的順序依次初始化派生類的成員。
8.如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義,而且靜態成員遵循通用的訪問控制規則。
9.派生類的聲明中不能包含它的派生列表。
class testex; // 正確 class testex : public test; // 錯誤
10.如果我們想將某個類用作基類,則該類必須已經定義而非僅僅聲明。因為派生類中包含並且可以使用它從基類繼承而來的成員。派生類必須要知道它們是什麽。此規定還有一層隱含的意思,即一個類不能派生它本身。
11.直接基類出現在派生列表中,而間接基類由派生類通過其直接基類繼承而來。最終的派生類將包含它的直接基類的子對象以及每個間接基類的子對象。
12.C++11新標準提供了一種防止繼承發生的方法,即在類名後面跟一個關鍵字 final 。
13.和內置指針一樣,智能指針類也支持派生類向基類的類型轉換。
14.表達式的靜態類型在編譯時總是已知的,它是變量聲明時的類型或表達式生成的類型,動態類型則是變量或表達式表示的內存中的對象的類型,直到運行時才可知。如果表達式既不是引用也不是指針,則它的動態類型永遠與靜態類型一致。
15.當我們用一個派生類對象為一個基類對象初始化或賦值時,只有該派生類對象中的基類部分會被拷貝,移動或賦值,它的派生類部分將被忽略掉。
16.通常情況下如果我們不使用某個函數,則無須為該函數提供定義,但是我們必須為每個虛函數提供定義,因為連編譯器也無法確定到底會使用哪個虛函數。
17.OOP的核心思想是多態性,我們把具有繼承關系的多個類型稱為多態類型,因為我們能使用這些類型的“多種形式”而無須在意它們的差異。
18.當派生類覆蓋了某個虛函數時,該該函數在基類中的形參必須與派生類的嚴格匹配,返回類型也必須匹配,但是有個例外,當類的虛函數返回類型是類本身的指針或引用時,返回類型可以不完全一樣。
19.C++11新標準中可以使用override關鍵字來說明派生類中的虛函數,如果我們使用了這個關鍵字,但是該函數並沒有覆蓋已存在的虛函數,此時編譯器會報錯。我們還可以把某個函數指定為final,如果我們已經把函數定義成了final了,則之後任何嘗試覆蓋該函數的操作都將引發錯誤。
class test { public: virtual int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; }; virtual int div(int a, int b) final { return a / b; } }; class testex : public test { public: virtual int add(double a, int b) { return a + b; } // 正確 virtual int add(int a, double b) override { return a + b; } // 錯誤 int sub(int a, int b) override { return a - b; }; // 錯誤,sub不是虛函數不可以覆蓋 int div(int a, int b) { return (a + 1) / b; } // 錯誤,final函數不可以覆蓋 };
20.基類和派生類的虛函數的默認實參可以不相同,該實參由本次調用的靜態類型決定,但是最好定義成一致的。
class test { public: virtual int add(int a, int b = 1) { return a + b; } }; class testex : public test { public: virtual int add(int a, int b = 2) override { return a + b; } };
21.如果一個派生類的虛函數需要調用它的基類版本,但是沒有使用作用域運算符,則會導致無限遞歸。
class test { public: virtual int add(int a, int b) { return a + b; } }; class testex : public test { public: virtual int add(int a, int b) override { int tmp = add(a, b); // 錯誤,會無線遞歸調用testex::add int tmp = test::add(a, b); // 正確 return tmp + 1; } };
22. =0可以將一個虛函數說明為純虛函數,但是只能出現在類內部的聲明語句處。和普通的虛函數不一樣,純虛函數無須定義。但是我們可以在類的外部定義純虛函數(即使被定義了,如果派生類不希望是純虛函數,則仍然需要覆蓋這個函數)。含有(或未經覆蓋直接繼承)純虛函數的類是抽象基類,我們不能(直接)創建一個抽象基類的對象(實際上它的派生類在構造時還是會構造一個抽象基類的對象)。
class test { public: test(int i ):m_id(i) {} virtual int add(int a, int b) = 0; int m_id; }; int test::add(int a, int b) { return a + b; } class testex : public test { public: testex(int i) :test(i) {} // 構造testex時也構造了test }; testex t(1); // 錯誤,testex是純虛函數
23.派生類的成員或友元只能通過派生類對象來訪問基類的受保護成員,派生類對於一個基類對象中的受保護成員沒有任何訪問特權。
class test { public: test() :m_count(0) {} protected: int m_count; }; class testex : public test { public: testex() :test() {} int add(test t) { //++t.m_count; // 錯誤 ++test::m_count; // 正確 return ++m_count; // 正確 } };
24. 派生訪問說明符對於派生類的成員(及友元)能否訪問其直接基類的成員沒有什麽影響,目的是控制派生類用戶對於基類成員的訪問權限。對基類成員的訪問權限只與基類中的訪問說明符有關。 派生訪問說明符還可以控制繼承自派生類的新類的訪問權限。
25.派生類向基類的轉換是否可訪問由使用該轉換的代碼決定,同時派生類的派生訪問說明符也會有影響,假設B繼承自A:
- 只有當B公有地繼承A時,用戶代碼才能使用派生類向基類的轉換。
- 不論B以什麽方式繼承A,B的成員函數和友元都能使用派生類向基類轉換。
- 如果B繼承A的方式是共有的或者受保護的,則D的派生類的成員和友元可以使用B向A的類型轉換。
26.就像友元關系不能傳遞一樣,友元關系同樣也不能繼承。
class test { friend class test_friend; private: int m_count; }; class testex : public test { private: int m_id; }; class test_friend { public: int count(test t) { return t.m_count; } // 正確 int id(testex t) { return t.m_id; } // 錯誤,友元不能繼承 int testex_count(testex t) { return t.m_count; } // 正確 };
27.有時候我們需要改變派生類繼承的某個成員的訪問級別,通過使用using聲明,派生類只能為那些它可以訪問的名字提供using聲明。
class test { public: int m_public_count; protected: int m_protected_count; }; class testex : private test { public: using test::m_public_count; protected: using test::m_protected_count; int get_count() { return m_protected_count; } // 正確 }; testex t; t.m_public_count; // 正確
28.默認情況下class關鍵字定義的派生類是私有繼承,而struct關鍵字定義的派生類是公有繼承的。除了這個和默認成員訪問說明符,struct和class再無其它任何不同之處。
29.如果一個名字在派生類的作用域內無法正確解析,則編譯器將繼續在外層的基類作用域中尋找該名字的定義。
30.派生類的成員將隱藏同名的基類成員。我們可以用作用域運算符來使用被隱藏的基類成員。
31.定義在派生類中的函數不會重載其基類中的成員,如果成員同名,則派生類將在其作用域內隱藏該基類成員,即使這個成員的形參列表不一致。
class test { public: int add(int a, int b) { return a + b; } }; class testex : public test { public: int add(int a) { return a + 1; } }; testex tex; tex.add(1, 2); // 錯誤,test::add(int,int)被隱藏了 tex.test::add(1, 2); // 正確
32.和其他函數一樣,成員函數無論是否是虛函數都能被重載,如果派生類希望所有的重載版本對於它來說都是可見的,那麽它就需要覆蓋所有的版本,或者一個也不覆蓋。為了避免這個限制,我們可以使用using聲明。
class test { public: int add(int a) { return a + 1; } int add(int a, int b) { return a + b; } }; class testex : public test { public: using test::add; int add(int a) { return a + 2; } }; testex tex; tex.add(1, 2); // 如果沒有using語句就會報錯 tex.add(2); // 正確
33.如果基類的析構函數不是虛函數,則delete一個指向派生類對象的基類指針將產生未定義的行為。
34.如果一個類定義了析構函數,即使它通過=default的形式使用了合成的版本,編譯器也不會為這個類合成移動操作。
35.對於派生類的析構函數來說,它除了銷毀派生類自己的成員外,還負責銷毀派生類的直接基類,該直接基類又銷毀它自己的直接基類,以此類推直。
36.某些定義基類的方式可能導致有的派生類成為被刪除的函數:
- 如果基類中的默認構造函數,拷貝構造函數,拷貝賦值運算符或析構函數是被刪除的或不可訪問的,則派生類中對應的成員將是被刪除的。
- 如果在基類中有一個不可訪問或刪除掉的析構函數,則派生類中合成的默認和拷貝構造函數將是被刪除的,因為編譯器無法銷毀派生類對象的基類部分。
- 如果基類中的移動操作是刪除的或不可訪問的,那麽派生類中該函數將是被刪除的。同樣,如果基類的析構函數是刪除的或不可訪問的,則派生類的移動構造函數也將是被刪除的。
37.當派生類定義了拷貝或移動操作時,該操作負責拷貝或移動包括基類部分成員在內的整個對象。默認情況下,基類的默認構造函數初始化派生類對象的基類部分,如果我們想拷貝或移動基類部分必須顯示地使用基類的拷貝(或移動)構造函數。
class test { public: int m_count; }; class testex : public test { public: testex() {} testex(const testex& tex) :test(tex){} }; testex tex; tex.m_count = 1; testex tex1(tex); // 此時tex1的m_count=1,如果沒有顯示調用則m_count是未初始化的
38.派生類的析構函數首先執行,然後是基類的析構函數,派生類的析構函數只負責銷毀由派生類自己分配的資源。
39.在構造函數和析構函數中調用了某個虛函數,則調用的虛函數是這個構造函數或者析構函數所屬類型對應的虛函數版本。
class test { public: test() { m_count = get_default_count(); } virtual int get_default_count() { return 1; } int m_count; }; class testex : public test { public: testex():test() {} virtual int get_default_count() { return m_default_count; } private: int m_default_count; }; test *t = new testex(); // 理論上構造test的時候應該調用testex的虛函數, // 但是如果這麽做,會用到testex的m_default_count,而此時m_default_count還沒有構造
40.一個類只能繼承其直接基類的構造函數,類不能繼承默認,拷貝和移動構造函數。派生類繼承基類構造函數的方式是提供一條註明了(直接)基類名的using聲明語句。通常情況下,using聲明語句只是令某個名字在當前作用域可見,而當作用於構造函數時,using語句將令編譯器產生代碼,對於基類的每個構造函數,編譯器都生成一個與之對應的派生類構造函數。換句話說,對於基類的每個構造函數,編譯器都在派生類中生成一個形參列表完全相同的構造函數。
class test { public: test(int count) { m_count = count; } int m_count; }; class testex : public test { public: using test::test; // 等價於下面的構造函數 // testex(int count) :test(count) {} private: int m_id; }; testex tex(1); // m_id默認初始化
41.和普通成員的using不一樣,一個構造函數的using聲明不會改變該構造函數的訪問級別。如果基類構造函數是explicit或者constexpr,則繼承的構造函數也擁有相同的屬性。
42.當一個基類構造函數含有默認實參時,這些實參並不會被繼承,相反,派生類將獲得多個繼承的構造函數。
class test { public: test(int id, int count = 0) { m_id = id; m_count = count; } int m_id; int m_count; }; class testex : public test { public: using test::test; // 等價於下面的兩個構造函數 // testex(int id) :test(id) {} // testex(int id, int count) :test(id, count) {} }; testex tex(1); // m_id=1 m_count=0 testex tex1(2, 1); // m_id=2 m_count=1
43.如果基類含有幾個構造函數,則除了兩個例外請況,大多數的時候派生類會繼承所有這些構造函數。第一個例外是派生類定義了具有相同參數列表的自己的版本,第二個例外是默認,拷貝和移動構造函數不會被繼承。繼承的構造函數不會被作為用戶定義的構造函數來使用,因此如果一個類只含有繼承的構造函數,則它也將擁有一個合成的默認構造函數。
44.當派生類對象被賦值給基類對象時,其中的派生類部分將被“切掉”,因此容器中存在繼承關系的類型無法兼容。解決這個問題的方法就是在容器中存放基類指針(最好是智能指針)。
C++ Primer 筆記——OOP