個人主頁:平行線也會相交💪
歡迎 點贊👍 收藏? 留言? 加關注💓本文由 平行線也會相交 原創
收錄于專欄【C++之路】💌
本專欄旨在記錄C++的學習路線,望對大家有所幫助🙇?
希望我們一起努力、成長,共同進步。🍓
目錄
- 一、作用域
- 出個小題
- 小總結
- 二、派生類的默認成員函數
- 構造函數
- 拷貝構造函數
- 賦值運算符重載
- 析構函數
- 小總結
- 三、繼承與友元
- 四、繼承和靜態成員
一、作用域
接下來對C++繼承體系中的作用域展開分析。
在C++繼承體系中,子類和父類有各自的作用域,所以子類和父類可以定義同名的成員。
請看針對不同作用域的舉例:
局部域和當前類域
這里有個小概念:
隱藏/重定義
:子類和父類有同名成員時,子類的成員隱藏了父類的成員。(如上左圖所示)
指定當前的父域:
作用域當然也對成員函數起作用,請看:
出個小題
類B和類A中的fun()函數有什么關系。
class A
{
public:void fun(){cout << "fun()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "fun(int i)" << endl;}
};
B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏。并不會構成函數重載(因為函數重載針對的是不同的作用域)
小總結
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,
也叫重定義。(在子類成員函數中,可以使用基類::基類成員
進行顯示訪問,舉個例子就比如說:B b; b.A::fun();
) - 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
- 但是其實在實際中在繼承體系里面最好不要定義同名的成員(省的給自己添麻煩)。
二、派生類的默認成員函數
再來回顧一下C++中的6個默認成員函數:構造函數、析構函數、拷貝構造函數、賦值運算符重載、取地址及const取地址運算符重載。
構造函數
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 id = 0):_id(0){}
protected:int _id;
};
int main()
{Student s;return 0;
}
運行結果如下:
上述代碼中我們并沒有定義Person類對象,但是卻調用了Person類中的默認構造函數,為什么呢?
因為C++規定了派生類必須調用父類的成員函數來初始化父類的成員變量。
這里是在初始化列表來調用父類中的默認成員函數的。
在來看下面的情況,請看:
解釋:在創建Student對象時,先調用Person類的構造函數來初始化Person類的成員變量_name,然后再調用Student類的構造函數來初始化Student類的成員變量_id。
所以這里是Person類中的成員函數先進行初始化,然后再對Student中的成員進行初始化。即派生類的構造函數在執行之前,基類的構造函數必須首先完成。
重點:通過使用初始化列表,并在其中調用基類的構造函數來初始化基類的成員變量,可以確保在派生類的構造函數中正確初始化基類的數據成員。這是由于派生類的構造函數在執行之前,基類的構造函數必須首先完成。
拷貝構造函數
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(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};
class Student : public Person
{
public://構造函數Student(const char* name = "李四",int id = 0):_id(0),Person(name){}//拷貝構造函數Student(const Student& s):Person(s), _id(s._id){}
protected:int _id;
};
int main()
{Student s1;Student s2(s1);return 0;
}
運行結果如下:
如果我們去掉基類拷貝構造函數中的Person(s)會怎樣呢(即沒有顯式調用基類中的拷貝構造函數)?
解析:去掉Person(s)將導致基類Person的成員變量_name不會被復制,而是會調用基類中的默認構造函數,而倘若此時基類也沒有提供默認構造函數的話就會直接報錯。
所以,我們應該顯式調用拷貝構造函數。如下:
//拷貝構造函數
Student(const Student& s):Person(s)//這里要顯式調用拷貝構造函數,否則會調用基類中的默認構造函數, _id(s._id)
{}
一句話總結:派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
賦值運算符重載
//父類賦值運算符重載
Person& operator=(const Person& p)
{cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;
}
//子類賦值運算符重載
Student& operator=(const Student& s)
{cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator=(s);_id = s._id;}return *this;
}
運行結果如下:
這里有的小伙伴看到Student s2 = s1;
可能會產生疑惑,為什么這里不調用賦值運算符重載函數。
解答
:
因為在語句Student s2 = s1;中,發生的是對象的初始化,而不是賦值操作。
當使用Student s2 = s1;來初始化一個已存在的對象s2時,會調用拷貝構造函數而不是賦值運算符重載函數。拷貝構造函數用來創建一個新對象,并將其內容初始化為另一個同類型對象的副本。
如果要調用賦值運算符重載函數,需要使用賦值操作符=來對已存在的對象進行賦值,例如s2 = s1;。這樣才會調用賦值運算符重載函數,將s1的值賦給s2。
析構函數
//父類析構函數
~Person()
{cout << "~Person()" << endl;
}
//子類析構函數
~Student()
{cout << "~Student()" << endl;
}
在C++中,無法顯式調用父類的析構函數。當一個派生類對象被銷毀時,首先會自動調用派生類的析構函數,然后再自動調用基類的析構函數(即按照先父后子的順序來完成對對象的析構)。
如果要顯式調用是沒有辦法保證先子后父進行析構的。
小總結
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認
的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。 - 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- 派生類的operator=必須要調用基類的operator=完成基類的復制。
- 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。即按照先清理派生類對象,再清理基類對象的順序。
- 派生類對象初始化先調用基類構造再調派生類構造;同時派生類對象初始化先調用基類構造再調派生類構造。
三、繼承與友元
友元關系不能繼承,即基類友元不能訪問子類私有和保護成員,基類的友元只能訪問基類的成員而不能訪問派生類的成員。
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{friend void Display(const Person& p, const Student& s);
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;
}
解釋:Person類和Student類互相引用對方作為友元函數,因此需要先進行一次前向聲明(即開頭的class Student;
)。這樣可以確保在實際定義這兩個類的成員函數之前,編譯器已經知道這兩個類的存在。
四、繼承和靜態成員
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()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << &p._count << endl;cout << &s._count << endl;cout << &Person::_count << endl;cout << &Student::_count << endl;
}
運行結果如下:
靜態成員變量是一種屬于類而不是類的實例的變量。它在所有類的實例之間共享,并且在整個程序的生命周期中只存在一個副本。靜態成員變量是在類定義外部進行初始化的。
靜態成員變量適用于在類的多個實例之間共享數據,并且可以通過類名直接訪問,而無需實例化類對象。它們在數據共享和數據統計方面非常有用。需要注意的是,靜態成員變量僅屬于類,而不屬于類的任何特定實例。
靜態成員變量的訪問方式:靜態成員變量可以使用類名::成員變量名的方式進行訪問(即類名::成員變量名
),例如Person::_count
。
下面請看下面代碼,要統計Person類及其Person派生類對象總共創建了多少個
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()
{Person p;Student s1;Student s2;Student s3;Graduate s4;cout << Person::_count << endl;
}
運行結果:Person類及其派生類對象總共創建了4個對象
。
解釋:在代碼中,將_count定義為靜態成員變量是為了在整個類層級中共享同一個計數變量。當創建派生類對象時,構造函數會依次調用每個類的構造函數,包括父類的構造函數。所以在父類的構造函數中進行++_count
操作,可以確保每個派生類對象的創建都能正確地增加計數。
好了,本文到這里就結束了,希望對大家學習C++繼承體系有所幫助。
再見啦,友友們!!!