目錄
00.引言
01.繼承的定義
02.基類和派生類對象
03.繼承中的作用域
04.派生類的默認成員函數
05.友元、靜態成員
00.引言
繼承是面向對象編程中的一個重要概念,它的作用是創建一個新的類,該類可以從一個已存在的類(父類/基類)繼承屬性和方法。
通過下面一個例子可以理解繼承的作用:
class teacher
{
private:char _sex;int _age;char _name;
private:int _jobid; // 工號
};class student
{
private:char _sex;int _age;char _name;
private:int _stuid; // 學號
};
在創建學校學校成員信息的時候,需要定義不同的兩個類分別用于存放老師和學生的信息種類,但是兩者的信息類別是存在重復的(性別、年齡、姓名)?,定義兩個類還好,但是如果定義需要更多的類呢,那樣就會進行過多的重復工作。此時就可以用繼承的方法,將同樣的信息類別在父類中定義:
class people
{
public:char _sex;int _age;char _name;
};
使用正確的繼承語法,繼承父類person的屬性:
class teacher : public people
{
public:teacher(): _sex('男') // 在成員初始化列表進行初始化 報錯, _age(40), _name('張'){}private:int _jobid; // 工號
};class student : public people
{
public:student(){_sex = '男';_age = 20;_name = '李';}
private:int _stuid; // 學號
};
這里注意:在定義默認構造函數時,不能夠通過初始化列表初始化其父類的成員,因為初始化列表只能初始化其自身的成員,所以 teacher() 函數會報錯。
01.繼承的定義
如圖:
我們可以看到,student是子類,也叫派生類,他繼承的是父類people,也叫基類,而public是繼承方式,繼承方式也可以是protected、private,和訪問限定符是一樣的。
使用不同的繼承方式繼承的基類成員的訪問權限是不一樣的,具體關系可以看下面這張表:
可以看出基類的私有成員在派生類中是不可見的,而其他成員的訪問方式取決于基類中成員的訪問限定符和繼承方式。通常情況下,使用
public
繼承是最常見的方式,因為它保留了基類的接口,并且派生類可以訪問基類的公共和受保護成員。?
注意:
使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過 最好顯示的寫出繼承方式。
在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里 面使用,實際中擴展維護性不強。
02.基類和派生類對象
當一個派生類對象被賦值給一個基類對象時,如果使用的是賦值操作符 =
或者拷貝構造函數,那么只會復制派生類對象中基類部分的內容,而派生類特有的成員會被截斷。
這種行為稱為對象切片,因為派生類對象被“切片”成了基類對象。
基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。
class people
{
public:char _sex;int _age;char _name;
};class student : public people
{
public:student(){_sex = '男';_age = 20;_name = '李';}
public:int _stuid; // 學號
};int main()
{student s1;s1._stuid; // 可以訪問people p1 = s1;p1._stuid; // 無法訪問return 0;
}
這里將派生類對象s1賦值給基類對象p1后,?p1就無法訪問s1中的_stuid元素,因為s1是被切片賦值給p1的,如圖:
下面的示例用來解釋基類的指針賦值:
class Person
{protected :string _name; // 姓名string _sex; // 性別int _age; // 年齡
};
class Student : public Person
{
public:int _No; // 學號
};
void Test ()
{Student sobj;// 1.子類對象可以賦值給父類對象/指針/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.基類對象不能賦值給派生類對象sobj = pobj;//3.基類的指針可以通過強制類型轉換賦值給派生類的指針pp = &sobjStudent* ps1 = (Student*)pp; // 這種情況轉換時可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 這種情況轉換時雖然可以,但是會存在越界訪問的問ps2->_No = 10;
}
03.繼承中的作用域
在繼承體系中,基類和派生類都有獨立的作用域。
訪問限定符決定了類的成員在類內部和外部的可見性和可訪問性。繼承方法決定了基類的的成員繼承到派生類之后的作用域。
比如用public方法繼承基類的成員,那么就在派生類的內部和外部都可以調用基類的成員,但是如果派生類內部也定義了一個和基類名字相同的成員呢?就像這樣:
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; // 學號
};
此時派生類Student和基類Person都定義了成員變量_num,此時在派生類內部調用_num,會默認調用派生類本身的,而基類的_num會被隱藏掉,因為,當子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。但是在子類成員函數中,也可以使用 基類::基類成員 顯示訪問。
同名成員的定義降低了代碼的可讀性,所以我們在實際繼承體系中最好不要定義同名的成員,上述的代碼就可以這么修改:
class Person
{
protected:string _name = "小李子"; // 姓名int _IDnum = 111;// 身份證號
};
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份證號:" << _IDnum << endl;cout << " 學號:" << _STnum << endl;}
protected:int _STnum = 999; // 學號
};
04.派生類的默認成員函數
?首先我們要知道默認成員函數是什么:默認成員函數提供默認行為,包括默認構造函數、拷貝構造函數、運算符重載等,這樣即使我們沒有顯式地編寫這些函數,類仍然可以正常地進行對象的創建、復制和賦值等操作。那么在派生類中,這幾個成員函數是如何生成的呢?
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; //學號
};
int main()
{Student s1("jack", 18);return 0;
}
運行結果:
Person()
Student()
可以看出,編譯器先調用基類的默認構造函數后再調用的派生類的默認構造函數。
2.析構函數
派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。如此一來保證了先清理派生類成員再清理基類成員的順序。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}~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(){cout << "~Student()" << endl;}
protected:int _num; //學號
};
int main()
{Student s1("jack", 18);return 0;
}
運行結果:
Person()
Student()~Student()
~Person()
因為子類是對基類的繼承與延伸,子類受基類的影響,但是基類并不受子類影響,所以在銷毀子類對象時,首先釋放子類特有的資源,然后再釋放基類的資源。
3.拷貝構造函數
派生類的拷貝構造函數需要對兩部分進行拷貝構造,一個是派生類自身的成員變量,一個是基類的成員變量,所以派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
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; //學號
};int main()
{Student s1("jack", 18);Student s2(s1);return 0;
}
運行結果:
Person(const Person& p)
Student(const Student& s)
可以看出,也是先調用了基類的拷貝構造函數再調用派生類的。?
4.賦值運算符重載
與拷貝構造類似,派生類的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);_num = s._num;}return *this;}
protected:int _num; //學號
};int main()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;return 0;
}
運行結果:
Student& operator= (const Student& s)
Person operator=(const Person& p)
因為 s3
是 Student
類型的對象。因此,會先輸出 Student& operator= (const Student& s)
。然后在 Student
類的賦值運算符內部,通過 Person::operator=(s);
調用了基類 Person
的賦值運算符重載函數。
05.友元、靜態成員
友元關系不能繼承,基類的友元不能訪問子類私有和保護成員,但是可以通過將派生類定義成友元的方式,使得派生類也可以訪問基類的私有成員:
#include <iostream>class Shape {
private:double _width;double _height;public:Shape(double width, double height) : _width(width), _height(height) {}friend class Rectangle; // 將Rectangle類聲明為Shape類的友元
};class Rectangle : public Shape {
public:Rectangle(double width, double height) : Shape(width, height) {}void displayArea() {double area = _width * _height; // 可以直接訪問基類的私有成員std::cout << "Area of rectangle: " << area << std::endl;}
};int main() {Rectangle rect(5.0, 3.0);rect.displayArea();return 0;
}
在這個例子中,Rectangle
類通過將 Shape
類聲明為友元,可以直接訪問 Shape
類的私有成員 _width
和 _height
。
如果基類定義了一個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;
}
以上就是繼承相關的知識的整理了,歡迎在評論區留言,覺得這篇博客對你有幫助的,可以點贊收藏關注支持一波~😉