C++繼承
繼承的概念及定義
繼承的概念
繼承機制是面向對象程序設計使代碼可以複用的常用手段,它允許程序員保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到複雜的認知過程。以前我們接觸的複用是函數複用,繼承是類設計層次的複用。
最普通的繼承例子
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();
}
繼承的定義
定義格式如圖
繼承基類成員訪問方式的變化
類成員(列)/繼承方式(行) | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 派生類中不可見 | 派生類中不可見 | 派生類中不可見 |
繼承小結
1.基類private成員在派生類中無論以什麼方式繼承都不可見。這裏的**不可見是指在基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類的裏面還是類的外面都不能去訪問它。**可以概括的理解不可見那麼派生類就完全不能訪問基類的所有成員。
2.基類private成員在派生類中是無法被訪問的,如果基類成員不想在類外直接被訪問,但需要在派生類能訪問,就定義爲protected。可以看出保護成員限定符是因繼承纔出現。
3.根據表格可以看到,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問==min(成員在基類的訪問限定符,繼承方式)。
4.使用關鍵字class時默認的繼承方式是private,使用struct時的默認繼承方式是public,不過最好顯示的寫出繼承方式。
5.在實際運用中一般都是用public繼承,protected/private繼承一般很少用。也不提倡使用這兩種繼承,實際中的擴展與維護不強。
基類和派生類對象賦值轉換
派生類對象可以賦值給基類的對象/基類的指針/基類的引用。這裏有個形象的說法叫做切片(其實我個人認爲與python中的切片意思差不多)。把派生類中父類那部分切下賦值過去。
基類對象不能賦值給派生類對象
基類的指針可以通過強制類型轉換賦值給派生類的指針。但是必須瞭解
上圖爲切片的模型
class Person
{
public:
void Print()
{
cout<<"---"<<endl;
}
protected :
string _name; // 姓名
string _sex; // 性別
int _age; // 年齡
};
class Student : public Person
{
public :
void Print()
{
cout<<"_No"<<_No<<endl;
}
int _No ; // 學號
};
void test()
{
Student A;
//1.子類對象可以賦值給父類對象/指針/引用
Person B;
Person* bb = &A;
Person& aa = A;
bb->Print();
aa.Print();
A = B;//這是錯誤的賦值方式,2.基類的對象不能賦值給派生類對象
//3.基類的指針可以通過強制類型轉換賦值給派生類的指針
bb = &A;
Student* pp1 = (Student*)bb;
pp1->_No = 10;
pp1->Print();
bb = &B;
Student* pp2 = (Student*)bb;//這相當於將基類強轉成派生類,在賦值給派生類,這種轉換雖然可以,但會是存在越界訪問的情況
pp2->_No = 1000;
pp2->Print();
}
繼承中的作用域
在繼承體系中基類和派生類都有獨立的作用域
關鍵!!!
關鍵!!!
關鍵!!!
子類和父類中有同名函數,子類成員將屏蔽父類對成員函數的直接訪問,這種情況叫隱藏,也叫重定義。在子類成員函數中,可以使用基類::基類成員
顯示訪問。
如果是成員函數的隱藏,只要函數名相同就構成隱藏。所以在實際中在繼承體系裏面最好不要定義同名的成員。
class Person
{
protected :
string _name = "skr"; // 姓名
int _num = 666; // 身份證號
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份證號:"<<Person::_num<< endl;
cout<<" 學號:"<<_num<<endl;
}
protected:
int _num = 12138; // 學號
};
雖然這樣編譯可以編譯過也能運行,但是太容易讓人混淆了,都是_num,當程序複雜起來這就一定會成爲一個BUG的大坑等着自己去跳。
class skr
{
public:
void fun()
{
cout << "Do you have freestyle?" << endl;
}
};
class cxk : public skr
{
public:
void fun(int i)
{
skr::fun();
cout <<"I like singsing dance rap and basketball "<<i<<endl;
}
};
void Test()
{
cxk b;
b.fun(10);
};
當skr::fun()調用時的情況完全不同
當沒有skr::fun()直接調用時
當具有skr::fun()直接調用時
skr類和cxk類中的fun函數不是構成重載,因爲它們不是同一個作用域。它們構成了隱藏。這就印證了當子類與父類同時具有同名函數時,子類函數將屏蔽父類對成員函數的訪問。其實就是它們都不能在隨便的互相訪問了,父類對象只能訪問父類自己的成員函數,子類可以訪問雙方的成員函數,但是子類如果想要訪問父類的成員函數需要使用固定的格式基類::成員函數
,纔可以進行訪問。
派生類的默認成員函數
在基類中我們一般新寫了一個類之後,如果不是必須的話,我們由系統操作完成對默認成員函數的創建,但是派生類中是繼承基類,又是如何?
-
派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
-
派生類的拷貝構造函數必須調用基類的拷貝構造函數完成基類的拷貝初始化
-
派生類的operator=必須要調用基類的operator=完成基類的複製
-
派生類的析構函數會在被調用完成後自動調用基類的析構函數清理基類成員。但是清理的順序是先清理派生類成員在清理基類的成員的順序。
-
派生類對象初始化先調用基類構造函數在調用派生類構造
-
派生類對象析構先調用派生類析構函數在調用基類析構函數
#include <iostream> #include <string> 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) :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; }; int main() { Student s1("jack",18); cout<<endl; Student s2(s1); cout<<endl; Student s3("rose",17); cout<<endl; s1 = s3; cout<<endl; return 0; }
運行如圖,打印的結果爲派生類和基類的先後調用順序
常見面試題
實現一個類不能被繼承
class NoInherit
{
public:
static NoInherit GetInstance()
{
return NoInherit();
}
private:
NoInherit()
{
}
};
//在c++98中構造函數私有化,派生類中調不到基類中的構造函數,則無法繼承
class NoInherit final
{}
//在C++11中給出了新的關鍵字final禁止繼承
繼承與友元
友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬於任何類,但需要在類的內部聲明,聲明時需要加friend關鍵字。
但是友元關係不能被繼承,也就是說類友元不能訪問子類私有和保護成員
比如在實例代碼中,友元函數想要訪問子類的私有成員和保護的成員,結果顯示編譯錯誤
繼承與靜態成員
基類定義了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 ; // 研究科目
};
int main()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人數 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人數 :"<< Person ::_count << endl;
}
運行結果爲0
和4
,這說明靜態成員只有一個,最後設爲0,就又被賦值爲0了。
菱形繼承和菱形虛擬繼承
單繼承
一個子類只有一個直接父類時稱這個繼承關係爲單繼承
class A
{}
class B:public A
{}
class C:public B
{}
多繼承
一個子類有兩個或以上直接父類時稱這個繼承關係爲多繼承
class A
{}
class B
{}
class C:public A,public B
{}
菱形繼承
class Person
{}
class Student:pubilc Person
{}
class Teacher:public Person
{}
class Assistant:public Student,public Teacher
{}
菱形繼承的問題從對象成員模型構造中可以看出菱形繼承有數據冗餘和二義性的問題。在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和Teacherd的繼承Person時使用虛擬繼承。虛擬繼承不能亂用,更不能將其想爲虛函數,它們是完全沒有關係的
當使用了虛繼承之後,數據只有一份了,改動任意一處,都只會對這一部分數據產生變化
虛基表
總的來說,虛基表是因爲虛繼承的存在纔出現的,爲了解決數據二義性和數據冗餘,虛基表爲了能夠準確尋找到基類的地址(就是爲了從派生類中訪問虛基類成員時),通過虛基表指針來訪問基類成員變量。
具體操作爲該指針地址+偏移量即爲基類成員的地址
繼承的總結
多繼承可以體現出C++語法的複雜。有了多繼承,就存在菱形繼承,有了菱形繼承,就有菱形虛擬繼承,這讓底層的實現越來越複雜。所以我們也不建議使用多繼承去設計。
多繼承也被看做是C++的缺陷之一。
繼承和組合
繼承是一種is-a
的關係,每個派生類對象都是一個基類對象。
組合是一種has-a
的關係,B組合了A,每個B對象中都有一個A對象
優先使用組合,而不是類繼承
繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的複用通常被稱爲白箱複用 (white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關係很強,耦合度高。
對象組合是類繼承之外的另一種複用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種複用風格被稱爲黑箱複用(black-box reuse),因爲對象的內部細節是不可見的。對象只以“黑箱”的形式出現。 組合類之間沒有很強的依賴關係,耦合度低。優先使用對象組合有助於你保持每個類被封裝。實際儘量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關係就適 合繼承那就用繼承,另外要實現多態,也必須要繼承。類之間的關係可以用繼承,可以用組合,就用組合。
// 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的關係
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 顏色
string _num = "陝ABIT00"; // 車牌號
Tire _t; // 輪胎
};