一、繼承的概念及定義
1、繼承的概念
繼承機制是面向物件程式設計使代碼可以復用的最重要的手段,它允許程式員在保持原有類特性的基礎上進行擴展,增加新的功能,這樣產生新的類,新類稱為派生類或基類,繼承是類設計層次的復用,呈現了面向物件程式設計的層次結構,
class Person {
public:
void Print() {
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; //預設值
int _age = 18;
};
class Student :public Person {
protected:
int _stuid;
};
class Teacher:public Person{
protected:
int _jobid;
};
int main() {
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
繼承后父類的Person的成員(成員函式+成員變數)都會變成子類的一部分,這里體現出了Student和Teacher復用了Person的成員,下面我們使用監視視窗查看Student和Teacher物件,可以看到變數的復用,呼叫Print可以看到成員函式的復用,

2、繼承定義
2.1、定義格式
下面我們看到Person是父類(基類),Student是子類(派生類),

2.2、繼承方式和訪問限定符

2.3、繼承基類成員訪問方式的變化
| 類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
|---|---|---|---|
| 基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
| 基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
| 基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
總結:
1、基類private成員在派生類中無論以什么方式繼承都是不可見的,這里的不可見是指基類的私有成員在派生類中還是存在,但是語法上限制派生類物件不管在類里面還是類外面都不能去訪問它,

但是我們還是可以呼叫Print方法列印_name的值,不可訪問是指我們不管在派生類里還是派生類外面都不可訪問,
2、基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但是需要在派生類中能被訪問,就定義為protected,可以看出保護成員限定符是因繼承才出現的,
3、除了基類的私有成員在子類中都是不可見的,基類的其他成員在子類的訪問方式=min(成員在基類的訪問限定符,繼承方式),權限:public>protected>private,
4、還需要注意:class的默認繼承方式是private,struct的默認繼承方式是public,
二、基類和派生類物件賦值轉換
- 派生類物件可以賦值給基類的物件 / 基類的指標 / 基類的參考,稱為切割或切片,寓意把派生類中父類的那部分切來賦值過去,
- 基類物件不能賦值給派生類,

class Person
{
protected :
string _name; // 姓名
string _sex; // 性別
int _age; // 年齡
};
class Student : public Person
{
public :
int _No ; // 學號
};
int main()
{
Student sobj ;
// 1.子類物件可以賦值給父類物件/指標/參考
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基類物件不能賦值給派生類物件
//sobj = pobj;
// 3.基類的指標可以通過強制型別轉換賦值給派生類的指標
pp = &sobj
Student* ps1 = (Student*)pp; // 這種情況轉換時可以的,因為pp本身就是指向一個Student*型別
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 這種情況轉換時雖然可以,但是會存在越界訪問的問題
ps2->_No = 10;
return 0;
}
三、繼承中的作用域
- 1、在繼承體系中基類和派生類都是獨立的作用域
- 2、子類和父類中有同名成員時,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏(重定義),(在子類成員函式中,可以使用基類::基類成員 顯示訪問),
- 3、對于成員函式的隱藏,只要函式名相同就構成隱藏,
成員屬性構成重定義:
// Student的_num和Person的_num構成隱藏關系,可以看出這樣代碼雖然能跑,但是非常容易混淆
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;
}
protected:
int _num = 999; // 學號
};
void Test()
{
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)
{
cout<<"void fun(int i)"<<endl;
}
};
void test(){
B b;
b.func(10);
}
四、派生類的默認成員函式
"默認"的意思就是指我們不寫,編譯器會為我們自動生成,那么在派生類中,編譯器主要為我們生成以下幾個默認成員函式:
4.1、建構式
派生類的建構式必須呼叫基類的建構式初始化基類的那一部分成員,不能自己顯示去初始化,如果基類沒有默認的建構式,則必須在派生類建構式的初始化串列階段顯示呼叫,
派生類物件初始化先呼叫基類構造再呼叫派生類構造,
class Person{
public:
Person(const char* name="peter")
:_name(name)
{
cout<<"Person()"<<endl;
}
protected:
string _name;
}
class Student:public Person{
public:
Student(const char* name,int num)
:Person(name),_num(num)
{
cout<<"Student()"<<endl;
}
protected:
int _num;//學號
}
4.2、拷貝建構式
派生類的拷貝建構式必須呼叫基類的拷貝建構式完成基類的拷貝構造,
class Person{
public:
Person(const Person& p )
:_name(p._name)
{
cout<<"Person(const Person& p)"<<endl;
}
protected:
string _name;
}
class Student:public Person{
public:
Student(const Student& s)
:Person(s),_num(s._num)
{
cout<<" Student(const Student& s)"<<endl;
}
protected:
int _num;//學號
}
4.3、賦值運算子多載函式
派生類的operator=必須要呼叫基類的operator=完成基類的復制,
class Person{
public:
Person& operator=(const Person& p)
{
cout<<"Person operator=(const Person& p)"<< endl;
if(this!=&p)
_name=p._name;
return *this;
}
protected:
string _name;
}
class Student:public Person{
public:
Student& operator=(const Student& s)
{
cout<<"Student& operator= (const Student& s)"<< endl;
if(this!=&s)
{
Person::operator=(s); //要顯示指定呼叫父類的operator=,因為子類和父類構成重定義
_num=s._num;
}
return *this;
}
protected:
int _num;//學號
}
4.4、解構式
派生類的解構式會在被呼叫完成后
自動呼叫基類的解構式清理基類的成員,因為這樣才能保證派生類物件先清理派生類成員再清理基類成員的順序,
派生類物件析構清理先呼叫派生類析構再呼叫基類的析構,
class Person{
public:
~Person()
{
cout<<"~Person()"<<endl;
}
protected:
string _name;
}
class Student:public Person{
public:
//兩個迷惑點:
//1、子類的解構式和父類的解構式構成隱藏,所有類的解構式,名字會被統一處理成destructor
//2、自己顯示呼叫,會存在父類先析構,不符合先構造先析構,后構造后析構的原則,
~Student()
{
//Person::~Person(); 不要顯示呼叫,要保證子類先析構
cout<<" ~Student()"<<endl;
}
protected:
int _num;//學號
}
編譯器在出作用域時自動呼叫父類的解構式,看以下匯編語言:
~Student()
{
008E3D50 push ebp
008E3D51 mov ebp,esp
008E3D53 push 0FFFFFFFFh
008E3D55 push 8EAB30h
008E3D5A mov eax,dword ptr fs:[00000000h]
008E3D60 push eax
008E3D61 sub esp,0CCh
008E3D67 push ebx
008E3D68 push esi
008E3D69 push edi
008E3D6A push ecx
008E3D6B lea edi,[ebp-0D8h]
008E3D71 mov ecx,33h
008E3D76 mov eax,0CCCCCCCCh
008E3D7B rep stos dword ptr es:[edi]
008E3D7D pop ecx
008E3D7E mov eax,dword ptr [__security_cookie (08EF008h)]
008E3D83 xor eax,ebp
008E3D85 push eax
008E3D86 lea eax,[ebp-0Ch]
008E3D89 mov dword ptr fs:[00000000h],eax
008E3D8F mov dword ptr [this],ecx
008E3D92 mov ecx,offset _6235FB68_testvecotr@cpp (08F202Ah)
008E3D97 call @__CheckForDebuggerJustMyCode@4 (08E1537h)
cout << "~Student()" << endl;
008E3D9C mov esi,esp
008E3D9E push offset std::endl<char,std::char_traits<char> > (08E1055h)
008E3DA3 push offset string "~Student()" (08ECDF0h)
008E3DA8 mov eax,dword ptr [__imp_std::cout (08F00E0h)]
008E3DAD push eax
008E3DAE call std::operator<<<std::char_traits<char> > (08E12B2h)
008E3DB3 add esp,8
008E3DB6 mov ecx,eax
008E3DB8 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (08F00A0h)]
008E3DBE cmp esi,esp
008E3DC0 call __RTC_CheckEsp (08E13D9h)
}
008E3DC5 mov ecx,dword ptr [this]
008E3DC8 call Person::~Person (08E1177h) //當出了作用域時,編譯器自動呼叫父類的解構式

五、繼承與友元
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員

六、繼承與靜態成員
基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員,無論派生多少個子類,都只有一個static成員實體,
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 統計人的個數,
};
int Person :: _count = 0;
class Student : public Person
{
protected :
int _stuNum ; // 學號
};
class Graduate : public Student
{
protected :
string _seminarCourse ; // 研究科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人數 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人數 :"<< Person ::_count << endl;
}
//運行結果:
人數 :4
人數 :0
七、復雜的菱形繼承及菱形虛擬繼承
單繼承:一個子類
只有一個直接父類時稱這個繼承關系為單繼承,

多繼承:一個子類有
兩個或以上直接父類時稱這個繼承關系為多繼承

菱形繼承:菱形繼承是多繼承的一種特殊情況,

菱形繼承的問題:從下面的物件成員模型構造,可以看出菱形繼承有
資料冗余和二義性的問題,在Assistant的物件中Person會有兩份,
class Person
{
public:
string _name; // 姓名
};
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 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 //加關鍵字virtual
{
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._name = "peter";
// 需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是資料冗余問題無法解決
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
虛擬繼承解決資料冗余和二義性的原理
為了研究虛擬繼承的原理,我們給出了一個簡化的菱形繼承繼承體系,再借助記憶體視窗觀察物件成員的模型,
菱形繼承:
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() {
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
菱形繼承可以看到資料的冗余性,_a屬性在兩塊空間都有存盤,

菱形虛擬繼承:
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.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
下圖是菱形虛擬繼承的記憶體物件成員模型:這里可以分析出D物件中A放到了物件組成的最下面,這個A同時也是屬于B、C,那么B、C是如何找到公共的A呢?這里是通過了B和C的兩個指標,指向的一張表,這兩個指標叫虛基表指標,這兩個表叫虛基表,虛基表中存的偏移量,通過偏移量可以找到A,

當發生派生類物件轉換為基類物件時,那么基類物件怎么去找屬于自己的那一部分資料呢?
D d;
B b=d;
C c=d;
下圖是上面菱形繼承的原理解釋圖:

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/282653.html
標籤:其他
上一篇:初步學習BFS的心得體會
