繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。
繼承是類設計層次的復用。
簡單圖示:
一個簡單例子:
// 父類
class Person
{public:
void Print()
{cout<< "name:"<< _name<< endl;
cout<< "age:"<< _age<< endl;
}
protected:
string _name = "Peter"; // 姓名
int _age = 20; // 年齡
};
// 繼承后父類Person的成員(成員函數+成員變量)都會變成子類的一部分
// 這里體現出了Student和Teacher復用了Person的成員。
// 可以通過監(jiān)視窗口查看Student和Teacher對象
// 可以看到變量的復用。調用Print可以看到成員函數的復用。
// 子類
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;
}
2.繼承的定義查看監(jiān)視窗口:
定義格式:
繼承關系和訪問限定符:
派生類繼承基類成員后,基類成員在派生類中的訪問方式:
用一張表來描述,就是:
基類成員/派生類的繼承方式 | public 繼承 | protected 繼承 | private 繼承 |
---|---|---|---|
基類的 public 成員 | 派生類的 public 成員 | 派生類的 protected 成員 | 派生類的 private 成員 |
基類的 protected 成員 | 派生類的 protected 成員 | 派生類的 protected 成員 | 派生類的 private 成員 |
基類的 private 成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
繼承影響的是繼承下來的基類成員,跟子類成員無關,不要混淆。
說明:
派生類對象可以賦值給基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割,寓意把派生類中父類那部分切來賦值過去,僅限于 public 繼承。
基類對象不能賦值給派生類對象。
基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用,但這很危險,存在越界訪問的風險,不要這么用。這里基類如果是多態(tài)類型,可以使用 RTTI(Run-Time Type Identification)的 dynamic_cast 來進行識別后進行安全轉換。
測試代碼:
class Person
{protected:
string _name; // 姓名
string _sex; // 性別
int _age; // 年齡
};
class Student : public Person
{public:
int _No; // 學號
};
int main()
{Person p;
Student s;
// 子類對象可以賦值給父類的對象/父類的指針/父類的引用
// 賦值兼容 ->切割/切片
// 這里不存在類型轉換,是語法天然支持的行為
p = s;
Person* pp = &s;
Person& rp = s;
// 父類對象不能賦值給子類對象
//s = p; s = (Student)p; // 怎樣都不行
// 父類的指針或者引用可以通過強制類型轉換賦值給子類的指針或者引用
// 但這很危險,存在越界訪問的風險,不要這么用
Student* ps = (Student*)&p;
//ps->_No = 1;
Student& rs = (Student&)p;
//rs._No = 2;
return 0;
}
三、繼承中的作用域將上述代碼圖示:
說明:
?① 類的成員,包括成員變量和成員函數。
?② 構成隱藏 / 重定義之后,到底是操作父類還是子類的成員,本質上還是要看作用域,跟同名的局部變量和全局變量類似,都是就近原則。
測試代碼1:
// Student的_num和Person的_num構成隱藏/重定義關系
// 可以看出這樣的代碼雖然能跑,但是非常容易混淆
class Person
{protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份證號
};
class Student : public Person
{public:
void Print()
{cout<< "姓名:"<< _name<< endl;
cout<< _num<< endl; // 就近原則
cout<< Person::_num<< endl; // 若要訪問父類同名成員,需要指明類域
}
protected:
int _num = 999; // 學號
};
int a = 0;
int main()
{int a = 1;
cout<< a<< endl; // 此處訪問的是局部變量a,因為就近原則
cout<< ::a<< endl; // 要想訪問全局變量a,需指明作用域
Student s;
s.Print();
return 0;
}
測試代碼2:
// B中的func和A中的func不構成函數重載,因為不是在同一作用域
// B中的func和A中的func構成隱藏,成員函數滿足函數名相同就構成隱藏
class A
{public:
void func()
{cout<< "func()"<< endl;
}
void f()
{cout<< "f()"<< endl;
}
};
class B : public A
{public:
void func(int i)
{cout<< "func(int i)->"<< i<< endl;
}
};
int main()
{B b;
b.func(1);
//b.func(); // 編譯報錯,因為父類A的func被隱藏了
b.A::func(); // 若想調用被隱藏的父類同名成員函數,必須指明類域
b.f();
return 0;
}
四、派生類的默認成員函數先拋開繼承不談,類的構造函數和析構函數的行為分別是:
?① 構造函數:先根據成員變量的聲明次序在初始化列表中順序完成成員變量的初始化,再執(zhí)行函數體內的語句。
?② 析構函數:先執(zhí)行函數體內的語句,再根據成員變量的聲明次序逆序完成成員變量的析構。
也就是說,類的構造和析構保證符合棧的后進先出。
加入繼承后,可以把繼承下來的父類理解成子類的一個自定義類型成員變量,且在成員變量中的聲明次序是順序第一位。
測試代碼1:
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:
protected:
int _num = 1; // 學號
string _s = "hello world";
};
// 派生類的重點的四個默認成員函數,我們不寫,編譯器默認生成的:
// 我們不寫默認生成的派生類的構造和析構:
// a.父類繼承下來的(調用父類默認構造和析構處理) b.自己的(按普通類處理)
// 我們不寫默認生成的派生類的拷貝構造和operator=:
// a.父類繼承下來的(調用父類拷貝構造和operator=) b.自己的(按普通類處理)
// 總結:繼承下來的調用父類處理,自己的按普通類處理
int main()
{Student s1;
Student s2(s1);
Student s3;
s1 = s3;
return 0;
}
測試代碼2:
class Person
{public:
Person(const char* name)
: _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;
//delete[] _ptr;
}
protected:
string _name; // 姓名
//int* _ptr = new int[10];
};
class Student : public Person
{public:
Student(const char* name = "張三", int num = 4)
:Person(name)
, _num(num)
{}
// s2(s1)
// 其實可以不寫,默認生成的就可以了
Student(const Student& s)
:Person(s) // 傳參時將子類對象賦值給父類的引用,即切片
,_num(s._num)
{}
// s2 = s1
// 其實可以不寫,默認生成的就可以了
Student& operator=(const Student& s)
{if (this != &s)
{ Person::operator=(s); // 傳參時將子類對象賦值給父類的引用,即切片
_num = s._num;
}
return *this;
}
// 析構函數名字會被統(tǒng)一處理成destructor()
// 那么子類的析構函數跟父類的析構函數就構成隱藏
// 所以,要調用父類的析構函數,需要指明父類類域
~Student()
{//Person::~Person();
//delete[] _ptr;
}
// 但是子類析構函數結束時,會自動調用父類的析構函數(保證符合棧的后進先出)
// 所以我們自己實現子類析構函數時,不需要顯式調用父類析構函數
// 否則會調用兩次父類析構函數,可能導致運行出錯
protected:
int _num; // 學號
//string _s = "hello world";
//int* _ptr = new int[10];
};
// 什么情況下必須自己寫子類的默認成員函數?
// 1.父類沒有默認構造,需要顯式寫構造
// 2.若子類有資源需要釋放,就需要顯式寫析構
// 3.若子類存在淺拷貝問題,就需要自己實現拷貝構造和賦值重載解決淺拷貝問題
// 若必須寫子類的默認成員函數,如何寫?
// 1.父類成員,調用父類的對應構造、拷貝構造、operator=和析構處理
// 2.自己成員,按普通類處理
// 總結:繼承下來的調用父類處理,自己的按普通類處理
int main()
{Student s1;
Student s2(s1);
Student s3("Jack", 19);
s1 = s3;
return 0;
}
說明:
destructor()
,所以父類析構函數不加 virtual 關鍵字的情況下,子類析構函數和父類析構函數構成隱藏關系。友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員。
測試代碼:
class Student;
class Person
{friend void Display(const Person& p, const Student& s);
public:
protected:
string _name; // 姓名
};
class Student : public Person
{protected:
int _stuNum; // 學號
};
void Display(const Person& p, const Student& s)
{cout<< p._name<< endl;
//cout<< s._stuNum<< endl; // 編譯報錯,友元關系不能繼承
}
int main()
{Person p;
Student s;
Display(p, s);
return 0;
}
六、繼承與靜態(tài)成員若基類定義了 static 成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個 static 成員實例。
測試代碼:
class Person
{public:
Person()
{++_count;
}
protected:
string _name; // 姓名
public:
static int _count; // 統(tǒng)計人的個數
};
int Person::_count = 0;
class Student : public Person
{protected:
int _stuNum; // 學號
};
class Graduate : public Student
{protected:
string _seminarCourse; // 研究科目
};
int main()
{Person p;
Student s;
Graduate g;
cout<< Person::_count<< endl;
cout<< Student::_count<< endl;
cout<< Graduate::_count<< endl;
cout<< &Person::_count<< endl;
cout<< &Student::_count<< endl;
cout<< &Graduate::_count<< endl;
return 0;
}
七、復雜的菱形繼承及菱形虛擬繼承
1.繼承關系單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承。
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承。
菱形繼承:菱形繼承是多繼承的一種特殊情況。
菱形繼承存在數據冗余和二義性的問題。
比如下面的對象成員模型構造:
在 Assistant 對象中 Person 成員會有兩份,若要訪問 _name ,是訪問從 Student 繼承過來的還是訪問從 Teacher 繼承過來的呢?所以說 Assistant 存在數據冗余和二義性的問題。
class Person
{public:
string _name; // 姓名
int _a[10000];
};
class Student : public Person
{public:
int _num; // 學號
};
class Teacher : public Person
{public:
int _id; // 職工編號
};
class Assistant : public Student, public Teacher
{protected:
string _majorCourse; // 主修課程
};
int main()
{Assistant a;
a._num = 1;
a._id = 2;
//a._name = "張三"; // 編譯報錯,對_name訪問不明確
//指明父類類域,可以解決二義性的問題
a.Student::_name = "小張";
a.Teacher::_name = "張老師";
//但是數據冗余的問題無法解決,萬一數據很大就會浪費掉很多空間
cout<< sizeof(a)<< endl;
return 0;
}
3.虛擬繼承可以解決菱形繼承數據冗余和二義性的問題虛擬繼承可以解決菱形繼承數據冗余和二義性的問題。
比如上面的繼承關系,在 Student 和 Teacher 繼承 Person 時使用虛擬繼承,即可解決問題。
class Person
{public:
string _name; // 姓名
int _a[10000];
};
class Student : virtual public Person // 虛擬繼承
{public:
int _num; // 學號
};
class Teacher : virtual public Person // 虛擬繼承
{public:
int _id; // 職工編號
};
class Assistant : public Student, public Teacher
{protected:
string _majorCourse; // 主修課程
};
int main()
{Assistant a;
// 虛擬繼承可以解決菱形繼承的數據冗余和二義性的問題
a.Student::_name = "小張";
a.Teacher::_name = "張老師";
a._name = "張三";
cout<< sizeof(a)<< endl;
return 0;
}
4.虛擬繼承解決菱形繼承數據冗余和二義性的原理為了研究虛擬繼承的原理,我們給出了一個簡化的菱形繼承繼承體系,再借助內存窗口觀察對象成員的模型。
測試代碼1:不使用虛擬繼承。
class A
{public:
int _a;
};
class B : public A
//class B : virtual public A
{public:
int _b;
};
class C : public A
//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;
}
下圖是菱形繼承的內存對象成員模型:這里可以看到數據冗余。
測試代碼2:使用虛擬繼承。
class A
{public:
int _a;
};
//class B : public A
class B : virtual public A
{public:
int _b;
};
//class C : public A
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;
d._a = 0;
return 0;
}
八、繼承的總結和反思下圖是菱形虛擬繼承的內存對象成員模型:這里可以分析出 D 對象中將 A 放到了對象組成的最下面,這個 A 是 B 和 C 的公共基類,那么 B 和 C 如何去找到公共的 A 呢?這里是通過了 B 和 C 里的兩個指針,各指向一張表。這兩個指針叫虛基表指針,這兩張表叫虛基表。虛基表中存有偏移量,通過偏移量可以找到 A 。
A 是被虛擬繼承的基類,稱之為虛基類。B 或 C 需要找 A ,就要通過虛基表中的偏移量進行計算。
此時的 A 既不屬于 B ,也不屬于 C ,而是屬于 D 。
// BMW和Benz與Car構成is-a的關系
class Car
{protected:
string _colour = "白色"; // 顏色
string _num = "粵A XXX00"; // 車牌號
};
class BMW : public Car
{public:
void Drive() {cout<< "好開-操控"<< endl; }
};
class Benz : public Car
{public:
void Drive() {cout<< "好坐-舒適"<< endl; }
};
// Car和Tire構成has-a的關系
class Tire
{protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car
{protected:
string _colour = "白色"; // 顏色
string _num = "粵A XXX00"; // 車牌號
Tire _t; // 輪胎
};
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網查看詳情吧