文章目錄
- 1. 繼承的概念及定義
-
- 1.1 繼承的概念
- 1.2 代碼示例
- 1.3 繼承定義
-
- 1.3.1定義格式
- 1.3.2繼承關系和通路限定符
- 1.3.3繼承基類成員通路方式
- 2.基類和派生類對象指派轉換
-
- 2.1 代碼示例
- 3.繼承中的作用域
-
- 3.1 代碼示例
- 4.派生類的預設成員函數
-
- 4.1 代碼示例與詳解
- 5.繼承與友元
- 6. 繼承與靜态成員
- 7.複雜的菱形繼承及菱形虛拟繼承
-
- 7.1 代碼示例
- 8. 繼承原了解釋
- 9 .繼承的總結和反思
- 10 .筆試面試題
1. 繼承的概念及定義
1.1 繼承的概念
- 繼承(inheritance)機制是面向對象程式設計
的最重要的手段,它允許程式員在使代碼可以複用
,增加功能,這樣産生新的類,稱派生類。繼承呈現了面向對象程式設計的層次結構,展現了由簡單到複雜的認知過程。以前我們接觸的複用都是函數複用,繼承是類設計層次的複用。保持原有類特性的基礎上進行擴充
1.2 代碼示例
#include<iostream>
#include<string>
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的成員。下面我們使用監視視窗檢視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;
}
1.3 繼承定義
1.3.1定義格式
- Person是父類,也稱作基類。Student是子類,也稱作派生類。
1.3.2繼承關系和通路限定符
1.3.3繼承基類成員通路方式
總結
:
- 基類private成員在派生類中無論以什麼方式繼承都是不可見的。
。不可見是指基類的私有成員還是被繼承到了派生類對象中,但是文法上限制派生類對象不管在類裡面還是類外面都不能去通路它
- 基類private成員在派生類中是不能被通路,如果基類成員不想在類外直接被通路,但需要在派生類中能通路,就定義為protected。
。可以看出保護成員限定符是因繼承才出現的
- 表格總結發現,
。基類的私有成員在子類都是不可見
- 使用關鍵字class時預設的繼承方式是private,使用struct時預設的繼承方式是public,
顯示的寫出繼承方式。
-
在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承
。不建議使用
protetced/private繼承,因為protetced/private繼承下來的成員都隻能在派生類的類裡面使用,實際中擴充維護性不強。
2.基類和派生類對象指派轉換
- 派生類對象 可以指派給
的引用。這裡有個形象的說法叫基類的對象 / 基類的指針 / 基類
或者切片
。把派生類中父類那部分切來指派過去。切割
- 基類對象不能指派給派生類對象。
- 基類的指針可以通過強制類型轉換指派給派生類的指針。但是必須是基類的指針是指向派生類對象時才是安全的。這裡基類如果是多态類型,可以使用RTTI的dynamic_cast 來進行識别後進行安全轉換。
2.1 代碼示例
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
//c++11 給預設值而不是初始化
string _name="小明"; // 姓名
string _sex="男"; // 性别
int _age=15; // 年齡
};
class Student : public Person
{
public:
int _No=20; // 學号
};
void Test()
{
Person pobj;
Student sobj;
sobj._name = "小紅";
sobj._sex = "女";
//pobj = sobj;
//Person* pp = &sobj;
//Person& rp = sobj;
//2.基類對象不能指派給派生類對象
//sobj = pobj;
// 3.基類的指針可以通過強制類型轉換指派給派生類的指針
Person* pp;
pp = &sobj;
Student *ps1 = (Student*)pp; // 這種情況轉換時可以的。
ps1->_No = 10;
//pp = &pobj;
//Student* ps2 = (Student*)pp; // 這種情況轉換時雖然可以,但是會存在越界通路的問題
//ps2->_No = 10;
}
int main()
{
Test();
return 0;
}
1. 子類對象可以指派給父類對象/指針/引用
2. 基類對象不能指派給派生類對象
3. 基類的指針可以通過強制類型轉換指派給派生類的指針
3.繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員将屏蔽父類對同名成員的直接通路,這種情況叫**
。(在子類成員函數中,可以隐藏,也叫重定義
)使用 基類::基類成員 顯示通路
- 需要注意的是如果是成員函數的隐藏,隻需要
。函數名相同就構成隐藏
- 注意在實際中在繼承體系裡面
。最好不要定義同名的成員
3.1 代碼示例
// 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();
};
4.派生類的預設成員函數
- 6個預設成員函數,“預設”的意思就是指我們不寫,編譯器會變我們自動生成一個,那麼在派生類中,這幾個成員函數是如何生成的呢?
4.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 :
Student(const char* name, int num)
: Person(name )
, _num(num )
{
cout<<"Student()" <<endl;
}
Student(const Student& s)
: Person(s)
, _num(s ._num)
{
cout<<"Student(const Student& s)" <<endl ;
}
Student& operator = (const Student& s )
{
cout<<"Student& operator= (const Student& s)"<< endl;
if (this != &s)
{
Person::operator =(s);
_num = s ._num;
}
return *this ;
}
~Student()
{
cout<<"~Student()" <<endl;
}
protected :
int _num ; //學号
};
void Test ()
{
Student s1 ("jack", 18);
Student s2 (s1);
Student s3 ("rose", 17);
s1 = s3 ;
}
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有預設的構造函數,則必須在派生類構造函數的初始化清單階段顯示調用。
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- 派生類的operator=必須要調用基類的operator=完成基類的複制。
- 派生類的析構函數會在被調用完成後自動調用基類的析構函數清理基類成員。因為這樣才能
。保證派生類對象先清理派生類成員再清理基類成員的順序
:子類的析構函數和父類的析構函數構成隐藏。所有類的析構函數,名字會被統一處理成destructor;注意
5.繼承與友元
- 友元關系不能繼承,基類友元不能通路子類私有和保護成員
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
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;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
6. 繼承與靜态成員
- 基類定義了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;
}
7.複雜的菱形繼承及菱形虛拟繼承
- 單繼承:一個子類隻有一個直接父類稱這個繼承關系為單繼承
- 多繼承:一個子類有兩個或以上直接父類稱這個繼承關系為多繼承
- 菱形繼承:菱形繼承是多繼承的一種特殊情況
- 菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有資料備援和二義性的問題。在Assistant的對象中Person成員會有兩份。
7.1 代碼示例
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時使用虛拟繼承,即可解決問題。需要注意的是,虛拟繼承不要在其他地方去使用。加資料備援
;virtual
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._name = "peter";
}
8. 繼承原了解釋
- 虛拟繼承解決資料備援和二義性的原理,研究虛拟繼承原理,給出了一個簡化的菱形繼承繼承體系,借助記憶體視窗觀察對象成員的模型。
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;
}
-
下圖是菱形虛拟繼承的記憶體對象成員模型:分析出D對象中将A放到的了對象組成的最
同時屬于B和C,那麼B和C如何去找到公共的A?
-
這裡是通過了B和C的兩個指針,指向的一張針叫虛基表指針,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量可以找到下面的A。
- Person關系菱形虛拟繼承的原了解釋:
9 .繼承的總結和反思
-
C++文法複雜,其實多繼承就是一個展現。有了多繼承,就存在菱形繼承,有了菱形繼承就有
菱形虛拟繼承,底層實作就很複雜。是以一般不建議設計出多繼承,一定不要設計出菱形繼承。否則在複雜度及性能上都有問題。
- 多繼承可以認為是C++的缺陷之一,很多後來的OO語言都沒有多繼承,如Java。
- 繼承群組合
- public繼承是一種
的關系。也就是說is-a
。每個派生類對象都是一個基類對象
- 組合是一種
的關系。假設B組合了A,每個B對象中都有一個A對象。has-a
-
。優先使用對象組合,而不是類繼承
// Car和BMW Car和Benz構成is-a的關系:
// Car和BMW Car和Benz構成is-a的關系
class Car{
protected:
string _colour = "白色"; // 顔色
string _num = "陝ABIT00" // 車牌号
};
class BMW : public Car{
public:
void Drive() {cout << "好開-操控" << endl;}
};
class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
// Tire和Car構成has-a的關系:
// Tire和Car構成has-a的關系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 顔色
string _num = "陝ABIT00"; // 車牌号
Tire _t; // 輪胎
};
10 .筆試面試題
1.什麼是菱形繼承?菱形繼承的問題是什麼?
- 菱形繼承:派生類繼承了多個基類,而這多個基類又同時繼承了同一個類。
- 菱形繼承有
和資料備援
的問題。二義性
2.什麼是菱形虛拟繼承?如何解決資料備援和二義性的
- 虛拟繼承:在
,定義虛拟繼承。基類對象前加virtual關鍵字
- 将備援的資料單獨開辟空間進行儲存(解決了資料備援問題),使用虛表指針指向虛表,**虛表中儲存了從基類成員(備援的成員)到備援資料儲存位置的
。通過該偏移量可以找到備援的資料,進而解決了二義性問題。偏移量
3.繼承群組合的差別?什麼時候用繼承?什麼時候用組合?
- public繼承是一種 is -a 關系,組合是一種 has -a的關系。
- 軟體工程裡面,認為組合好。能用has-a組合就用組合,組合更符合高内聚,低耦合。is-a關系就适合繼承那就用繼承,另外要實作多态,也必須要繼承。