第一章:繼承的概念及定義
1.1繼承的概念
繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,繼承是類設計層次的復用。
#include <iostream>
using namespace std;class Person {
public:void Print() {cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter";//姓名int _age = 18;//年齡
};//繼承后父類的Person的成員(成員函數+成員變量)都會變成子類的一部分。
//這里體現出了Student和Teacher復用了Person的成員。
class Student : public Person {
protected:int _stuid;//學號
};class Teacher : public Person {
protected:int _jobid;//工號
};int main() {Student s;Teacher t;//使用監視窗口查看Student和Teacher對象,可以看到變量的復用。s.Print();t.Print();//調用Print可以看到成員函數的復用。return 0;
}
1.2 繼承定義
1.2.1定義格式
1.2.2繼承關系和訪問限定符
1.2.3繼承基類成員訪問方式的變化
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
總結:
- 基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。
- 實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
- 使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。
- 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
第二章:基類和派生類對象賦值轉換
- 派生類對象可以賦值給基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
- 基類對象不能賦值給派生類對象。
- 基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態類型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 來進行識別后進行安全轉換。(ps:這個我們后面再講解,這里先了解一下)
//1.子類對象可以賦值給父類對象/指針/引用
//2.基類對象不能賦值給派生類對象
class Person {
protected:string _name;string _sex;int _age;
};class Student : public Person {
public:int _No;
};int main() {double d = 2.2;//int& i = d;//編譯報錯const int& i = d;string str = "xxxx";//string& rstr = "xxxx";//編譯報錯const string& rstr = "xxxx";//隱式類型轉換會生成臨時對象,臨時對象具有常性,所以引用加constStudent s;Person p = s;//公有繼承情況下,子類(派生類)可以賦值給父類(基類)Person& rp = s;//public繼承,子類對父類是is-a的關系。//子類對象賦值給父類對象/父類指針/父類的引用可以認為是天然的,中間不產生臨時對象,這個叫父子類賦值兼容規則(切割/切片)。//子類對象賦值給父類對象:把子類中 父類也有的成員 賦值給父類//父類引用子類:僅引用子類中 父類也有的成員return 0;
}
第三章:繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
class Person {
protected:string _name = "小李子";int _num = 111;//身份證號
};class Student : public Person {
public:void Print() {cout << "姓名:" << _name << endl;cout << "身份證號:" << Person::_num << endl;cout << "學號:" << _num << endl;//這里訪問的是子類Student的_num}
protected:int _num = 999;//學號//父類中有_num,子類中也能有_num。他們是兩個獨立的作用域//Student的_num和Person的_num構成隱藏關系,可以看出這樣代碼雖然能跑,但是非常容易混淆
};
//1.在繼承體系中基類和派生類都有獨立的作用域。
//2.子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,
// 也叫重定義。(在子類成員函數中,可以使用 基類::基類成員 顯示訪問)
void Test1() {Student s1;s1.Print();
};
//B中的fun和A中的fun不是構成重載,因為不是在同一作用域
//B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏。
class A {
public:void fun() {cout << "func()" << endl;}
};class B : public A {
public:void fun(int i) {A::fun();cout << "func(int i)->" << i << endl;}
};void Test2() {B b;b.fun(10);//b.fun();//不能調用父類,因為被子類隱藏了b.A::fun();//指定父類作用域才能調用
};
//3.需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
//4.注意在實際中在繼承體系里面最好不要定義同名的成員。
第四章:派生類的默認成員函數
6個默認成員函數,“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,這幾個成員函數是如何生成的呢?
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- 派生類的operator=必須要調用基類的operator=完成基類的復制。
- 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- 派生類對象初始化先調用基類構造再調派生類構造。
- 派生類對象析構清理先調用派生類析構再調基類的析構。
- 因為后續一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同(這個我們后面會講解)。那么編譯器會對析構函數名進行特殊處理,處理成destrutor(),所以父類析構函數不加virtual的情況下,子類析構函數和父類析構函數構成隱藏關系。
#include <iostream>
using namespace std;class Person {
public:Person(const char* name = "peter"): _name(name) {cout << "Person()" << endl;}Person(const Person& p): _name(p._name) {cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p) {cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person() { cout << "~Person()" << endl; }
protected:string _name;//姓名
};class Student : public Person {
public://構造函數//如果子類[沒有]顯式定義構造函數,會自動調用父類的默認構造函數//如果子類顯示定義構造函數,要把父類當成一個完整對象去初始化Student(const char* name, int num)//:_name(name) 該方式錯誤: Person(name) //用一個像匿名對象的方式調用父類構造函數, _num(num) {cout << "Student()" << endl;}//1.父類[沒有]默認構造函數 → 必須顯式調用父類的某個構造函數。//2.父類有默認構造函數 → 可以不顯式調用(編譯器自動調用),但如果你想用特定值初始化父類成員,仍然需要顯式調用。//3.調用方式:在子類構造函數的初始化列表中,像“匿名對象”一樣調用父類構造函數//拷貝構造//如果子類[沒有]顯式定義拷貝構造函數,會自動調用父類的拷貝構造函數Student(const Student& s): Person(s)//子類賦值給父類的引用,觸發切片。只賦值了子類中父類有的那部分, _num(s._num) {cout << "Student(const Student& s)" << endl;}//1.如果父類有默認拷貝構造函數(即沒有顯式定義,且所有成員都可拷貝),//子類可以不顯式調用父類拷貝構造,編譯器會自動調用父類的默認拷貝構造。//2.如果父類沒有默認拷貝構造函數(例如,父類顯式定義了拷貝構造但未提供默認版本,或者某些成員不可拷貝),//子類必須在初始化列表中顯式調用父類的拷貝構造函數,否則編譯錯誤//賦值重載//如果子類[沒有]顯式定義賦值重載,會自動調用父類的賦值重載Student& operator=(const Student& s) {cout << "Student& operator= (const Student& s)" << endl;if (this != &s) {//operator=(s);//錯誤,棧溢出。父類和子類的賦值構成隱藏,這種方式是調用子類的賦值重載Person::operator=(s);_num = s._num;}return *this;}//由于多態的原因,析構函數會被統一處理成destructor//子類和父類的析構構成隱藏~Student() {//~Person();//錯誤,構成隱藏,所以這種方式是訪問子類的析構//Person::~Person();//顯示調用的方式。而main函數中只有3個對象,但每個對象Person的析構都調用了2次//子類中父類是先聲明的,所以先構造父類。而析構反過來要先析構子類。//但自己不一定能保證這個析構順序,所以不能顯示調用父類析構函數。//子類析構函數結束時會自動調用父類析構函數//ChatGPT//C++ 規定,析構函數的調用順序與構造順序相反://1.先執行子類的析構函數體(~Derived() 的代碼)。//2.再自動調用父類的析構函數(~Base())。//3.最后析構成員變量(按聲明逆序析構)。//這種順序是 編譯器強制保證 的,無需程序員干預。析構必須保證逆向安全銷毀//析構順序必須嚴格反向(先子類后父類),如果允許手動調用父類析構://可能導致 重復析構(如果編譯器之后又自動調用一次)。//破壞對象析構的安全性(如父類資源被子類依賴時提前釋放)。cout << "~Student()" << endl;}
protected:int _num;//學號
};int main() {Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;return 0;
}
第五章:繼承與友元
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員
class Student;
class Person {
public:friend void Display(const Person& p, const Student& s);
protected:string _name;
};class Student : public Person {
protected:int _stuNum;
};//該函數不能訪問Student的成員
void Display(const Person& p, const Student& s) {cout << p._name << endl;cout << s._stuNum << endl;
}
補充:如何實現一個不能被繼承的類
//C++98 私有化父類的構造函數
class A {
public:
private:A() {}//父類私有化構造函數,子類無法創建對象//因為繼承以后不可見(子類的里面外面都不能訪問)
protected:int _a;int _b;
};class B : public A {
};//C++11新增final修飾父類,直接不能被繼承
class A final {
public:A() {}
private:int _a;int _b;
};class B : public A {
};
第六章:繼承與靜態成員
基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例 。
class Person {
public:Person() { ++_count; }
protected:string _name;
public:static int _count;//統計人的個數。
};
int Person::_count = 0;
//靜態成員為所有類對象所共享,不屬于某個具體的對象,存放在靜態區
//靜態成員也是類的成員,受public、protected、private 訪問限定符的限制class Student : public Person {
protected:int _stuNum;
};
第七章:復雜的菱形繼承及菱形虛擬繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況。?
菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。在Assistant的對象中Person成員會有兩份。
class Person {
public:string _name;//姓名int _age;
};class Student : public Person {
protected:int _num;//學號
};class Teacher : public Person {
protected:int _id;//職工編號
};class Assistant : public Student, public Teacher {
protected:string _majorCourse;//主修課程
};void Test() {//菱形繼承的問題:菱形繼承有數據冗余和二義性的問題。在Assistant的對象中Person成員會有兩份。 Assistant a; //a._name = "peter";//這樣會有二義性無法明確知道訪問的是哪一個// 需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是數據冗余問題無法解決a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
虛擬繼承可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student和Teacher的繼承Person時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。
class Person {
public:string _name;
};class Student : virtual public Person {
protected:int _num;
};class Teacher : virtual public Person {
protected:int _id;
};class Assistant : public Student, public Teacher {
protected:string _majorCourse;
};void Test() {Assistant a;//即可指定訪問,也能不指定訪問a.Student::_name = "xxx";a.Teacher::_name = "yyy";a._name = "peter";
}
虛擬繼承解決數據冗余和二義性的原理
為了研究虛擬繼承原理,我們給出了一個簡化的菱形繼承繼承體系,再借助內存窗口觀察對象成員的模型。
非虛擬繼承時D的情況演示
//非虛擬繼承時D的情況演示
class A {
public:int _a;
};class B : public A {
public:int _b;
};class C : public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};int main() {//A:_a//B:_a, _b//C:_a, _c//D:B::_a, _b, C::_a, _c, _dD d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
虛擬繼承時,D的情況演示
class A {
public:int _a;
};class B : virtual public A {
public:int _b;
};class C : virtual public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};int main() {D d;d._a = 1;d._b = 2;B b = d;b._a = 1;b._b = 2;return 0;
}
下圖是菱形虛擬繼承的內存對象成員模型:這里可以分析出D對象中將A放到的了對象組成的最下面,這個A同時屬于B和C,那么B和C如何去找到公共的A呢?這里是通過了B和C的兩個指針,指向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量可以找到下面的A。
虛繼承后B對象的成員內存分布
第八章:繼承的總結和反思
- 很多人說C++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜。所以一般不建議設計出多繼承,一定不要設計出菱形繼承。否則在復雜度及性能上都有問題。
- ?多繼承可以認為是C++的缺陷之一,很多后來的OO語言都沒有多繼承,如Java。
- 繼承和組合
- public繼承是一種is-a的關系。也就是說每個派生類對象都是一個基類對象。
- 組合是一種has-a的關系。假設B組合了A,每個B對象中都有一個A對象。
- 優先使用對象組合,而不是類繼承 。
- 繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。
- 對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內部細節是不可見的。對象只以“黑箱”的形式出現。組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于你保持每個類被封裝。
- 實際盡量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現多態,也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合。
//繼承
class A {
public:void func() {}
protected:int _a;
};class B : public A {
public:void f() {func();//父類的公有可以使用_a++;//父類的保護也可以使用}
protected:int _b;
};//組合
class C {
public:void func() {}
protected:int _c;
};class D {
public:void f() {cc.func();//組合類的公有可以使用//cc._c++;//組合類的保護不可以使用}
protected:C cc;int _d;
};int main() {cout << sizeof(B) << endl;//8cout << sizeof(D) << endl;//8D dd;//dd.func();//組合:dd對象不能直接調用funcB bb;bb.func();//繼承:bb對象可以直接調用return 0;
}
面試問題
1.多繼承中指針偏移問題?下面說法正確的是()
A:p1 == p2 == p3?
B:p1 < p2 < p3?
C:p1 == p3 != p2?
D:p1 != p2 != p3
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
//答案:C
//先繼承Base1,所以在前面。后繼承Base2,最后是_d。
//Base1是Derive的第一個基類,所以Base1的子對象位于Derive對象的起始位置。
//Base2是Derive的第二個基類,它的子對象位于Base1子對象之后。
//p3直接指向Derive對象的起始地址,所以p3的值與p1相同。
2. 以下程序輸出結果是什么()
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
class A {
public:A(const char* s) { cout << s << endl; }~A() {}
};class B :virtual public A {
public:B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};class C :virtual public A {
public:C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};class D :public B, public C {
public:D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1) {cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}//答案:A
//在 C++ 中,構造順序遵循:
//虛基類(按聲明順序,深度優先)。
//直接基類(按聲明順序)。
//成員對象(按聲明順序)。
//派生類自身的構造函數。//虛基類的構造由最派生類(D)直接初始化,忽略中間類(B 和 C)對虛基類的初始化。
//由于 A 是虛基類,B 和 C 對 A 的初始化會被忽略,A 由 D 直接初始化。
//因此,構造函數的調用順序和輸出為:
//1.A("class A") → class A。
//2.B("class A", "class B") → class B(忽略 A 的初始化)。
//3.C("class A", "class C") → class C(忽略 A 的初始化)。
//4.D 的構造函數體 → class D。//虛基類 A 的構造優先于所有非虛基類(B 和 C)。
//非虛基類的順序由 D 的繼承聲明順序決定(B → C)。
//因此構造順序是 A → B → C → D,對應選項 A。
作業
1.關于繼承說法正確的是( )
A.所有的類都可以被繼承
B.Car(汽車)類和Tire(輪胎)類可以使用繼承方式體現
C.繼承是實現代碼復用的唯一手段
D.狗是一種動物,可以體現出繼承的思想
答案:D
A.final說明的類不能被繼承
B.應該使用組合,因為Tire類跟Car類屬于Has-a的關系
C.模板也是代碼復用的重要手段
D.狗是動物的一種,屬于is-a關系,是繼承的體現
2.下面關于繼承說法不正確的是( )
A.繼承可以使用現有類的所有功能,并在無需重新編寫原來類的情況下對這些功能進行擴展
B.繼承體系中子類必須要體現出與基類的不同
C.子類對象一定比基類對象大
D.繼承呈現了面相對象程序設計的層次結構,體現了有簡單到復雜的認知過程
答案:C
A.這是繼承的功能,也是代碼復用的體現
B.繼承除了吸收基類成員之外,一般還需要擴充自己的數據成員,跟基類有所不一樣
C.不一定,有可能子類只是改寫父類的方法而已,并沒有增加其自身的數據成員,則大小一樣,故錯誤
D.繼承體現了一定的層次結構和認知過程
3.下面關于訪問權限與繼承權限說法不正確的是( )
A.訪問權限和繼承權限是不同的概念
B.訪問權限和繼承權限關鍵字上是一樣的,但是出現位置不一樣
C.如果是protected繼承方式,基類public的成員變量能通過基類對象在類外直接訪問
D.基類私有的成員變量在子類中都不能直接訪問,因為沒有被子類繼承了
答案:D
A.兩個權限控制的東西不一樣
B.訪問權限在類內部,繼承權限在類外
C.只要是public成員對象都可以直接訪問
D.基類私有成員不能直接訪問不是沒有被繼承,而是權限問題
4.下面關于繼承權限說法正確的是( )
A.派生類在繼承基類時,必須明確指定繼承方式
B.Class定義的類,默認的訪問權限是protected
C.struct定義的類,默認訪問權限是public
D.子類沒有繼承基類私有的成員
答案:C
A.可以不指定,默認為private
B.Class定義的類,默認的訪問權限是private
C.正確,在C++中,這是struct跟class唯一的區別
D. 私有的成員繼承下來了,但是在子類中不可見
5.下面代碼輸出結果:()
class A {
public:void f() { cout << "A::f()" << endl; }int a;
};class B : public A {
public:void f(int a) { cout << "B::f()" << endl; }int a;
};int main() {B b;b.f();return 0;
}
A.打印A::f()
B.打印B::f()
C.不能通過編譯,因為基類和派生類中a的類型以及名稱完全相同
D.以上說法都不對
答案:D
A.錯誤
B.錯誤
C.不能通過編譯是正確的,不過原因不是因為成員變量a的問題,而是子類同名隱藏了父類方法的原因
D.很顯然以上說法都不對
6.關于同名隱藏的說法正確的是( )
A.同一個類中,不能存在相同名稱的成員函數
B.在基類和子類中,可以存在相同名稱但參數列表不同的函數,他們形成重載
C.在基類和子類中,不能存在函數原型完全相同的函數,因為編譯時會報錯
D.成員函數可以同名,只要參數類型不同即可,成員變量不能同名,即使類型不同
答案:D
A.可以存在,如函數重載
B.基類與子類函數名字相同,參數不同,形成的是隱藏
C.可以共存
D.成員函數在同一個類里面同名,此時構成了重載,但變量一定不能同名,故正確
7.關于派生類構造函數與析構函數說法正確的是( )
A.在派生類對象構造時,先調用基類構造函數,后調用子類構造函數
B.在派生構造函數初始化列表的位置必須顯式調用基類構造函數
C.在派生類對象銷毀時,先調用基類析構函數,后調用子類析構函數
D.派生類的析構函數只需析構派生類的資源即可
答案:A
A.先構造父類,在構造子類 故正確
B.不一定,如果父類有默認構造函數就不需要
C.剛好相反,先調用子類,在調用父類
D.派生類的析構函數往往還需要連同父類析構函數一起調用,同時清除父類的資源
8.下列代碼中f函數執行結束后輸出( )
class A {
public:A() { cout << "A::A()" << endl; }~A() { cout << "A::~A()" << endl; }int a;
};class B : public A {
public:B() { cout << "B::B()" << endl; }~B() { cout << "B::~B()" << endl; }int b;
};void f() {B b;
}
A.B::B() B::~B()
B.B::B() A::A() A::~A() B::B()
C.A::A() B::B() B::~B() A::~A()
D.以上都不對
答案:C
分析: 子類實例化對象,由于繼承的有父類。所以會先構造父類,然后在構造子類,析構順序完全按照構造的相反順序進行析構,故答案為 C
9.下面說法正確的是( )
A.派生類構造函數初始化列表的位置必須顯式調用基類的構造函數,已完成基類部分成員的初始化
B.派生類構造函數先初始化子類成員,再初始化基類成員
C.派生類析構函數不會自動析構基類部分成員
D.子類構造函數的定義有時需要參考基類構造函數
答案:D
A.如果父類有默認構造函數,此時就不需要
B.順序相反,先初始化父類,再是子類
C.會調用,并且按照構造的相反順序進行調用
D.是的,需要看父類構造函數是否需要參數子類的,從而你決定子類構造函數的定義
10.關于基類哪些成員被子類繼承說法不正確的是( )
A.靜態成員函數
B.所有成員變量
C.基類的友元函數
D.靜態成員變量在整個繼承體系中只有一份
答案:C
A.靜態成員函數也可以被繼承
B.成員變量所有的都會被繼承,無論公有私有
C.友元函數不能被繼承,相當于你爹的朋友不一定是你的朋友
D.靜態成員屬于整個類,不屬于任何對象,所以在整體體系中只有一份
11.關于以下菱形繼承說法不正確的是( )
class B { public: int b; };
class C1 : public B { public: int c1; };
class C2 : public B { public: int c2; };
class D : public C1, public C2 { public: int d; };
A.D總共占了20個字節
B.B中的內容總共在D對象中存儲了兩份
C.D對象可以直接訪問從基類繼承的b成員
D.菱形繼承存在二義性問題,盡量避免設計菱形繼承
答案:C
A.C1中b和c1共8個字節,C2中c2和b共8個字節,D自身成員d 4個字節,一共20字節
B.由于菱形繼承,最終的父類B在D中有兩份
C.子類對象不能直接訪問最頂層基類B中繼承下來的b成員,因為在D對象中,b有兩份,一份是從C1中繼承的,一份是從C2中繼承的,直接通過D的對象訪問b會存在二義性問題,在訪問時候,可以加類名::b,來告訴編譯器想要訪問C1還是C2中繼承下來的b。
D.菱形繼承存在二義性問題,盡量避免設計菱形繼承,如果真有需要,一般采用虛擬繼承減少數據冗余
12. 下面哪項結果是正確的( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };class Derive : public Base1, public Base2 {
public: int _d;
};int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3
答案:C
分析:p1和p2雖然都是其父類,但在子類內存模型中,其位置不同,所以p1和p2所指子類的位置也不相同,因此p1!=p2,
由于p1對象是第一個被繼承的父類類型,所有其地址與子類對象的地址p3所指位置都為子類對象的起始位置,因此p1==p3,所以C正確
13.關于基類與子類對象之間賦值說法不正確的是( )
A.基類指針可以直接指向子類對象
B.基類對象可以直接賦值給子類對象
C.子類對象的引用不能引用基類的對象
D.子類對象可以直接賦值給基類對象
答案:B
A.這是賦值兼容規則的其中一條,正確
B.基類不能給子類對象直接賦值,因為父類類型對于子類類型來說類型不完全,故錯誤
C.不能用父類初始化子類引用
D.這也是賦值兼容規則的其中一條
14.關于基類與派生類對象模型說法正確的是()
A.基類對象中包含了所有基類的成員變量
B.子類對象中不僅包含了所有基類成員變量,也包含了所有子類成員變量
C.子類對象中沒有包含基類的私有成員
D.基類的靜態成員可以不包含在子類對象中
E.以上說法都不對
答案:E
A.靜態變量就不被包含
B.同理,靜態變量就不被包含
C.父類所有成員都要被繼承,因此包含了
D.靜態成員一定是不被包含在對象中的
E.很顯然,以上說法都不正確