更多C++知識:c++目錄索引
1. 繼承
1.1 繼承概念
繼承是面向對象複用的重要手段。通過繼承定義一個類,繼承是類型之間的關系模組化,共享共有的東西,實作自己本質的東西。
1. 2 通路限定符 && 繼承關系
private(私有):從字面上來看,私有屬于個人的,不會讓其他人使用;那麼在父類裡,成員為私有限定,意味着不管是什麼繼承,這個成員就是不讓你看,即使你是從我這裡繼承過去的
protect(保護):父類成員受保護,說明通路還是可以讓你通路的,但隻能是子類在自己的内部通路,不可以在類外面通過類對象進行通路,不管是基類的對象還是子類的對象
三種繼承關系
繼承方式 | 基類public成員 | 基類protect成員 | 基類private成員 | 繼承引起的通路限制關系 |
---|---|---|---|---|
public繼承 | 仍為public成員 | 仍為protect成員 | 不可見 | 基類的非私有成員在子類的通路屬性都不變 |
protect繼承 | 變為protect成員 | 仍為protect成員 | 不可見 | 基類的非私有成員都成為子類的保護成員 |
private繼承 | 變為private成員 | 變為private成員 | 不可見 | 基類的非私有成員都成為子類的私有成員 |
例子:
class father
{
public:
char* _fname;//名字都可以知道
protected:
char* _fIDCard;//銀行卡号要受保護的,隻有我和我的繼承者可以知道
private:
char* _fpassward;//銀行卡密碼隻有自己知道
};
class son : public father
{
public:
char* _sname;
protected:
char* _sIDCard;
private:
char* _spassward;
public:
void func()
{
_sIDCard = _fIDCard;//在子類的内部可以通路父類的保護成員
}
};
void Test()
{
son s;
s._fIDCard = "123456";//不可通過子類對象通路父類的保護成員
}
結果:
幾點總結:
- 基類中的private成員,不管通過何種方式繼承,在其子類中均不能被通路。
- 某個成員不想被基類對象直接通路,但要在子類中能被通路,就定義成protected成員。
- public繼承是一個接口繼承,保持is-a原則,每個父類可用的成員對子類也可用,因為每個子類對象也都是一個父類對象。
- protetced/private繼承是一個實作繼承,基類的部分成員并未完全成為子類接口的一部分,是 has-a 的關系原則,是以非特殊情況下不會使用這兩種繼承關系,在絕大多數的場景下使用的都是公有繼承。
- 不管是哪種繼承方式,在派生類内部都可以通路基類的公有成員和保護成員,但是基類的私有成員存在但是在子類中不可見(不能通路)。
- 使用關鍵字class時預設的繼承方式是private,使用struct時預設的繼承方式是public,不過最好顯示的寫出繼承方式。
- 在實際運用中一般使用都是public繼承,極少場景下才會使用protetced/private繼承。
注:
is-a原則: 全稱為is-a-kind-of,顯然這裡is-a原則指的是子類是父類的一種;例如:長方形是基類,正方形是子類,子類和基類構成繼承關系;滿足is-a 原則;正方形是長方形的一種
has-a 原則:代表的是對象和它的成員的從屬關系。同一種類的對象,通過它們的屬性的不同值來差別,是有一個的原則;
例如:下面ID,Name,telephon 和student 是has-a的關系;學生有一個學号,有一個名字,有一個電話
class ID{.....};
class Name{......};
class telephon{.....};
class student{//學生類裡面包含了ID,Name,tele
puvlic:
private:
ID _ID;
Name _name;
telephon _tel;
};
注:關于is-a更多解釋可閱讀Effective C++ 條款32;關于has-a更多解釋可閱讀Effective C++ 條款38
2. 繼承與轉換—-指派相容規則(public繼承)
- 子類對象可以指派給父類對象
- 父類對象不能複制給子類對象
- 父類的指針/引用可以指向子類對象
- 子類的指針/引用不能指向父類對象(可以通過強制類型轉換完成,但存在隐患)
class Father
{
public:
int _fid;
int _ftel;
};
class Son:public Father
{
public:
int _sid;
};
void Test()
{
Son s;
Father f;
f = s;//子類可以賦給父類(通過切片)
s = f;//父類不可賦給子類
//父類的引用可以指向子類
Father& f1 = f;
f1 = s;
//父類的指針可以指向子類
Father* f2 = &f;
f2 = &s;
//子類的引用不能指向父類(可以通過強轉完成)
Son& s1 = s;
s = (Son&)f;
//子類的指針不能指向父類(可以通過強轉完成)
Son* s2 = &s;
s2 = (Son*)&f;
}
個人了解:
關于子類可以賦給父類:
子類是從父類繼承的,故而父類必定包含子類的部分成員,在指派時,父類隻取自己的那部分成員,并不接受子類的成員,進而發生切片,丢棄了子類的成員;
父類不可指派給子類:
父類給子類指派時,子類并不知道父類有多少成員,部分成員繼承過來,但私有成員不可見,故而指派時子類沒有更多的空間來存放父類的私有成員;如果說要進行切片,那麼要哪些成員,不要那些成員,并不知道;而且你既然是從父類繼承的,有什麼權利說丢棄父類的成員
3. 成員函數的重載、 覆寫和隐藏
3.1 重載
- 在同一個類的作用域裡,成員函數的名字相同,參數不同,兩個函數構成重載
- 參數不同可以是順序不同,數目不同(包括const參數和非const參數)
- virtual 關鍵字可有可無
3.2 覆寫
- 覆寫指派生類重新實作(或改寫)了基類的成員函數
- 發生覆寫的 兩個成員函數分别在兩個不同的類域,分别為基類和派生類
- 發生覆寫的兩個成員函數的函數名相同,參數完全相同(參數順序,參數數目)
- 基類的成員函數必須為虛函數(virtual 關鍵字修飾)
3.3 隐藏
隐藏指派生類的成員函數遮蔽了與其函數名相同的基類成員函數,具體規則如下:
- 派生類的函數名和基類的函數名相同,參數不同;基類的成員函數被隐藏(注意不要和重載混淆)
- 派生類的函數名和基類的函數名相同,參數完全相同,但基類成員函數沒有virtual關鍵字;基類的成員函數被隐藏(注意不要和覆寫混淆)
執行個體1:
class Father
{
public:
void func(int x)
{
cout << "Father:func" << endl;
}
void func1(int x, int y)
{
cout << "Father:func1" << endl;
}
virtual void func2(int x)
{
cout << "Father:func2" << endl;
}
};
class Son :public Father
{
public:
void func(float)
{
cout << "Son: func1" << endl;
}
void func1(int x, int y)
{
cout << "Son: func1" << endl;
}
virtual void func2(int x)
{
cout << "Son: func2" << endl;
}
};
//Father::func 和 Father::func1 構成重載
//Son::func 隐藏了Fther::func,不是重載,作用域不同
//Son::func1 隐藏了Father::func1,不是覆寫,基類func1不是虛函數
//Son::func2 覆寫了Father::func2
執行個體2:
class AA
{
public:
void f()
{
cout<<"AA::f()"<<endl;
}
};
class BB : public AA
{
public:
void f(int a)
{
cout<<"BB::f()"<<endl;
}
};
int main()
{
AA aa;
BB bb;
aa.f();
bb.f();//想調基類的f函數,但基類AA的f函數被隐藏,故而bb調f時調子類的f,此時子類f帶參,報錯
//bb.AA::f(); 可注明作用域調f函數
system("pause");
return ;
}
總結:
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員将屏蔽父類對成員的直接通路(在子類成員函數中,可以使用 基類::基類成員 通路)
- 注意在實際中在繼承體系裡面最好不要定義同名的成員。
關于重載、覆寫和隐藏的更多解釋閱讀《高品質程式設計指南 C/C++》第14章
4. 派生類的預設成員函數
- 派生類繼承父類時,并不會繼承基類的成員函數(構造函數、析構函數、指派函數)
- 在繼承關系裡面,在派生類中如果沒有顯示定義六個成員函數,編譯系統則會預設合成這六個預設的成員函數。(1.構造函數、2.拷貝構造函數、3.析構函數、4.指派操作符重載、5.取位址操作符重載、6.const修飾的取位址操作符重載)
- 派生類的構造函數(包括拷貝構造函數)應在其初始化清單也隻能在其初始化清單裡(不能在派生類構造函數内調用基類的構造函數)顯示地調用基類的構造函數(除非基類的構造函數不可通路)。
- 如果基類是多态類,那麼必須把基類的析構函數定義為虛函數
- 實作派生類的指派函數時,派生類中繼承自父類的成員變量可以直接調用基類的指派函數實作。
代碼:
class Base
{
public:
Base(const char* name = "")
:_name(name)
{
cout << "Base()構造" << endl;
}
Base(const Base& b)
:_name(b._name)
{
cout << "Base()拷貝構造" << endl;
}
Base& operator=(const Base& b)
{
cout << "Base()指派運算符重載" << endl;
if (this != &b)
{
_name = b._name;
}
return *this;
}
~Base()
{
cout << "Base() 析構" << endl;
}
protected:
string _name;
};
class Dervied : public Base
{
public:
Dervied(const char* name,int num)
:Base(name)//顯示調基類的構造
, _num(num)
{
cout << "Dervied()構造" << endl;
}
Dervied(const Dervied& der)
:Base(der)//顯示調基類的拷貝構造
, _num(der._num)
{
cout << "Dervied()拷貝構造" << endl;
}
Dervied& operator=(const Dervied& der)
{
cout << "Dervied()指派運算符重載" << endl;
if (this != &der)
{
Base::operator=(der);//調基類的指派運算符重載
_num = der._num;
}
return *this;
}
~Dervied()
{
cout << "Dervied()析構" << endl;
}
protected:
int _num;
};
void Test()
{
Dervied d("xiaozhang", );
cout << endl;
Dervied d1(d);
cout << endl;
Dervied d3("xiaoming", );
cout << endl;
d1 = d3;
cout << endl;
}
int main()
{
Test();
system("pause");
return ;
}
分析:在實作預設成員函數時,從基類繼承的成員就調用基類的成員函數進行構造,拷貝構造等;在析構時,順序則相反,後構造的先析構
注:派生類的析構函數隐藏了基類的析構函數,在調用完自己的析構函數時,自動調用父類的析構函數,(編譯器會把所有析構函數名換成destructor,這樣就構成了隐藏)
5. 單繼承和多繼承
5.1 單繼承
單繼承指單一繼承,一個子類隻 有一個直接父類時稱這個繼承關系為單繼承。這種關系比較簡單是一對一的關系:
5.2 多繼承
- 多繼承是指 一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承。這種繼承方式使一個子類可以繼承多個父類的特性。
- 多繼承可以看作是單繼承的擴充。派生類具有多個基類,派生類與每個基類之間的關系仍可看作是一個單繼承。
- 多繼承下派生類的構造函數與單繼承下派生類構造函數相似,它必須同時負責該派生類所有基類構造函數的調用。同時,派生類的參數個數必須包含完成所有基類初始化所需的參數個數。在子類的記憶體中它們是按照聲明定義的順序存放的,下面的截圖将清晰看到。
6. 菱形繼承和虛繼承
6.1 菱形繼承
兩個子類繼承同一個父類,而又有子類同時繼承這兩個子類,也被稱為鑽石繼承。
6.2 菱形繼承的問題
- 資料備援
- 産生二義性
模型:
執行個體:
class Farther
{
public:
void func()
{
cout << "Farther::func()" << endl;
}
protected:
int _father;
};
class Son1 :public Farther
{
protected:
int _son1;
};
class Son2 :public Farther
{
protected:
int _son2;
};
class GrandSon :public Son1, public Son2
{
protected:
int _gson;
};
void Test()
{
GrandSon gson;
gson.func();//産生二義性,此時son1和son2都繼承了Farther,有兩份,找func時目标不明确
}
int main()
{
Test();
system("pause");
return ;
}
分析: son1和son2都繼承了父類Farther,此時func存了兩份,GrandSon又繼承了son1和son2,必定去它的父類裡面找,但現在有兩份,不知道選哪一個,是以産生了二義性。
解決辦法:
1. 指明類域
void Test()
{
GrandSon gson;
gson.Son1::func();
gson.Son2::func();
}
2.采用虛繼承
class Farther
{
public:
void func()
{
cout << "Farther::func()" << endl;
}
protected:
int _father;
};
class Son1 :virtual public Farther//son1虛繼承Farther
{
protected:
int _son1;
};
class Son2 :virtual public Farther//son2虛繼承Farther
{
protected:
int _son2;
};
class GrandSon :public Son1, public Son2
{
protected:
int _gson;
};
void Test()
{
GrandSon gson;
cout << sizeof(gson) << endl;
gson._father = ;
gson._son1 = ;
gson._son2 = ;
}
int main()
{
Test();
system("pause");
return ;
}
虛繼承:
- 虛繼承即讓son1和son2在繼承Farther時加上virtural關鍵字,不可寫成GrandSon:virtual public son1,public son2
- 虛繼承解決了在菱形繼承體系裡面子類對象包含多份父類對象的資料備援&浪費空間的問題。
- 虛繼承體系看起來好複雜,在實際應用我們通常不會定義如此複雜的繼承體系。一般不到萬不得已都不要定義菱形結構的虛繼承體系結構,因為使用虛繼承解決資料備援問題也帶來了性能上的損耗。
虛繼承的記憶體分布:
先來看下gson在虛繼承和普通繼承下的大小
普通繼承:
虛繼承:
從兩個圖上看出,虛繼承下的Son2比普通繼承的大小還要大,下來看看虛繼承下的記憶體配置設定
gson的記憶體
從記憶體中可以看到資料确實存進去了,但2(Son1的成員)上面還有一個指針,3(Son2的成員)上面同樣存在一個指針,可檢視到底存的是什麼?
Son1的成員上指針指向的内容,儲存了數字20
Son2的成員上指針指向的内容,儲存了數字12
兩個數字的用途:
- 一個是數值20,一個是12。這時候看看記憶體1這張圖檔,我們發現gson. _ farther(1)的位址和 gson. _ son1(2)位址之差是20,gson. _ son2(3) 的位址和 gson. _ farther(1)位址之差是12。
- 可以推算出,每一個繼承自父類對象的執行個體中都存有一個指針,這個指向指向虛基表的其中一項,裡面存的是一個偏移量。對象的執行個體可以通過自身的位址加上這個偏移量找到存放繼承自父類對象的位址。
虛繼承下的記憶體模型