??專欄介紹:淺嘗C++專欄是用于記錄C++語法基礎、STL及內存剖析等。
??每日格言:每日努力一點點,技術變化看得見。
繼承的概念及定義
繼承的概念
我們生活中也有繼承的例子,例如:小明繼承了孫老師傅做拉面的手藝。繼承就是一種延續(xù)、復用的方式。C++為了提高代碼的可復用性,引入了繼承機制,概念如下↓↓↓
繼承機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產(chǎn)生新的類,稱派生類。繼承呈現(xiàn)了面向對象程序設計的層次結構,體現(xiàn)了由簡單到復雜的認知過程。以前我們接觸的復用都是函數(shù)復用,繼承是類設計層次的復用。
繼承的定義
定義格式
下圖演示的繼承的格式,其中Person是父類,也稱作基類;Student是子類,也稱作派生類。
下面給出代碼示例(下面代碼中,Student類繼承父類Person)↓↓↓
#include <iostream>
using namespace std;
class Person
{
public:
void Show()
{
cout << _name << " " << _age << endl;
}
protected:
string _name = "jammingpro"; //姓名
int _age = 18; //年齡
};
class Student : public Person
{
private:
int _stuId;
};
int main()
{
Person p;
Student s;
p.Show();
s.Show();
return 0;
}
上面代碼中,Student繼承父類Person的成員(成員函數(shù)+成員變量)后,這些成員都變成了子類的一部分。這里的Student復用了Person的成員。通過監(jiān)視窗口可以看到,Student中也有自己的_name、_age成員變量。
繼承關系與訪問限定符
在C++的繼承機制中,包含3種繼承方式及3種類訪問限定符(如下圖所示),下面將分別介紹它們。
我們在學習類和對象時,就已經(jīng)接觸過訪問限定符。其中public成員可以在類外訪問,而protected與private成員不能在類外訪問。但這里的protected和private在繼承時是有區(qū)別的:
●如果父類愿意讓自己的成員被外界訪問并愿意讓子類繼承,則定義為public的;
●如果父類希望自己的成員不被外界訪問而愿意讓子類繼承,則需要定義為protected;
●如果父類不希望自己的成員被外界訪問、被繼承,則需要定義為private的。
父類中的訪問限定符表示父類愿不愿意讓子類繼承,而繼承方式則可以讓子類縮小父類成員的訪問權限,但不能放大父類成員的訪問權限。
父類成員/子類繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
父類的public成員 | 變?yōu)樽宇惖膒ublic成員 | 變?yōu)樽宇恜rotected成員 | 變?yōu)樽宇惖膒rivate成員 |
父類的protected成員 | 變?yōu)樽宇惖膒rotected成員 | 變?yōu)樽宇惖膒rotected成員 | 變?yōu)樽宇惖膒rivate成員 |
父類的private成員 | 子類不可見 | 子類不可見 | 子類不可見 |
總結
-
基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面
都不能去訪問它。 -
基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現(xiàn)的。
-
實際上面的表格我們進行一下總結會發(fā)現(xiàn),基類的私有成員在子類都是不可見?;惖钠渌蓡T在子類的訪問方式等于
Min{成員在基類的訪問限定符,繼承方式}
,其中,public>protected>private。 -
使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。
-
在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
針對于總結中的第一點,父類private成員實際上還是被子類繼承了,只是子類無法訪問,下面使用代碼驗證↓↓↓
#include <iostream>
using namespace std;
class Base
{
private:
int _base;
};
class Son : public Base
{}
int main()
{
Son s;
return 0;
}
基類和派生類對象賦值轉換
●派生類對象可以賦值給基類的對象/基類的指針/基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
下面給出代碼示例↓↓↓
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{}
Person(const string& name, const char& sex, const int& age)
:_name(name)
,_sex(sex)
,_age(age)
{}
protected:
string _name;
char _sex;
int _age;
};
class Student : public Person
{
public:
Student(const string& name, const char& sex, const int& age, const int& stuId)
:Person(name, sex, age)
,_stuId(stuId)
{}
private:
int _stuId;
};
int main()
{
Student s("Jammingpro", 'M', 18, 123456);
Person p;
p=s;
return 0;
}
從監(jiān)視窗口可以看到,Person對象保存了Student對象的父類成員部分,而舍棄了子類自有成員,這就是切片。
●基類對象不能賦值給派生類對象。(基類對象無法用于構造派生類對象,也無法使用派生類對象的拷貝賦值函數(shù);但可以顯示提供派生類賦值給基類的operator=實現(xiàn))
★ps:由于派生類中的成員函數(shù)、成員對象一般情況下都會多于基類,如果基類直接賦值給派生類會導致部分成員數(shù)值不確定。因此,C++默認不提供基類賦值給派生類。
●基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針指向派生類對象時才是安全的。
下面代碼演示了基類賦值給派生類指針,派生類賦值給基類指針↓↓↓
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{}
Person(const string& name, const char& sex, const int& age)
:_name(name)
, _sex(sex)
, _age(age)
{}
string _name;
char _sex;
int _age;
};
class Student : public Person
{
public:
Student(const string& name, const char& sex, const int& age, const int& stuId)
:Person(name, sex, age)
, _stuId(stuId)
{}
int _stuId;
};
int main()
{
Student s("Jammingpro", 'M', 18, 123456);
Person p("xiaoming", 'M', 20);
Person* p_s = &s;//安全
Student* s_p = (Student*) & p;//不安全
cout << s_p->_stuId << endl;
return 0;
}
為什么說,Student* s_p = (Student*) & p;
是不安全的呢?由于Person對象中沒有申請_stuId的空間,但在Student*類型看來,它認為它指向的對象有_stuId成員。如果用戶訪問了s_p->_stuId可能會因為內存非法訪問而報錯。
繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構成隱藏(而不需要返回值相同,或是參數(shù)列表相同)。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
下面給出派生類成員變量與基類成員變量同名的例子↓↓↓
下面代碼中,由于Son類中的成員變量與Base類中的成員變量重名,構成了隱藏。如果使用Son s; cout << s._name << endl;
,則只能訪問到Son對象中的成員變量,而無法訪問到父類中的_name成員變量。若需要訪問父類的_name成員變量,則可以使用類型+類作用域符號::
來訪問,即s.Base::_name
。
#include <iostream>
using namespace std;
class Base
{
public:
string _name = "Jammingpro";
};
class Son : public Base
{
public:
string _name = "xiaoming";
};
int main()
{
Son s;
cout << s._name << endl;
cout << s.Base::_name << endl;
return 0;
}
下面給出派生類成員函數(shù)與基類成員函數(shù)同名的例子↓↓↓這里與成員變量同名的情況相同,同名成員函數(shù)也會構成隱藏關系,如果需要訪問父類的同名成員函數(shù),需要使用類名+類作用域運算符::
。
#include <iostream>
using namespace std;
class Base
{
public:
void Show()
{
cout << "I am _Base" << endl;
}
};
class Son : public Base
{
public:
void Show()
{
cout << "I am _Son" << endl;
}
};
int main()
{
Son s;
s.Show();
s.Base::Show();
return 0;
}
★( ??? )?test:下面的兩個同名函數(shù)(函數(shù)名相同,參數(shù)列表不同)分別屬于基類和派生類,它們構成的關系是隱藏還是函數(shù)重載呢?
#include <iostream>
using namespace std;
class Base
{
public:
void print(char ch)
{
cout << "Base->" << ch << endl;
}
};
class Son : public Base
{
public:
void print(int num)
{
cout << "Son->" << num << endl;
}
};
int main()
{
Son s;
s.print('A');
return 0;
}
Base中的成員函數(shù)比Son中的成員函數(shù)更匹配(不需要隱式類型轉換),而這里還是調用Son中的成員函數(shù),說明兩者構成的關系是隱藏,而不是函數(shù)重載。這里要注意:在繼承關系中,派生類與基類只要存在同名函數(shù)(不管參數(shù)列表、返回值是否相同),都是隱藏關系。
派生類的默認成員函數(shù)
6個默認成員函數(shù),“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,這幾個成員函數(shù)是如何生成的呢?
- 派生類的構造函數(shù)必須調用基類的構造函數(shù)初始化基類的那一部分成員。如果基類沒有默認的構造函數(shù),則必須在派生類構造函數(shù)的初始化列表階段顯示調用。
基類提供默認構造函數(shù)的情況
派生類在構造時,會自動調用基類的構造函數(shù)↓↓↓
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base() is called" << endl;
}
};
class Son : public Base
{
public:
Son()
{
cout << "Son() is called" << endl;
}
};
int main()
{
Son s;
return 0;
}
基類沒有提供默認構造函數(shù)的情況
基類沒有提供默認構造時,子類必須在初始化參數(shù)列表中顯示調用基類的構造函數(shù),否則會報錯。
#include <iostream>
using namespace std;
class Base
{
public:
Base(int b)
:_b(b)
{
cout << "Base(int b) is called" << endl;
}
private:
int _b
};
class Son : public Base
{
public:
Son(int b)
:Base(b)
{
cout << "Son() is called" << endl;
}
};
int main()
{
Son s(5);
return 0;
}
- 派生類的拷貝構造函數(shù)必須調用基類的拷貝構造完成基類的拷貝初始化。
#include <iostream>
using namespace std;
class Base
{
public:
Base(int b)
:_b(b)
{}
Base(const Base& b)
{
_b = b._b;
cout << "Base(const Base& b) is called" << endl;
}
private:
int _b;
};
class Son : public Base
{
public:
Son(int s, int b)
:_s(s)
,Base(b)
{}
Son(const Son& s)
:Base(s)
{
_s = s._s;
cout << "Son(const Son& s) is called" << endl;
}
private:
int _s;
};
int main()
{
Son s(55, 66);
Son s2(s);
return 0;
}
如果我們將上面代碼中顯示調用基類構造函數(shù)的代碼去掉,則會出現(xiàn)如下報錯↓↓↓
★ps:對于上面的報錯,雖然可以通過給基類提供默認構造函數(shù)解決,但卻無法完成子類中的基類成員的拷貝操作。
- 派生類的operator=可以調用基類的operator=完成基類的復制,這樣可以避免代碼冗余(這里派生類調用基類的operator=不是必須的,只是因為基類已經(jīng)實現(xiàn)了該操作,派生類不必再重復編寫相同內容)。
下面代碼演示了派生類調用基類operator=函數(shù)帶來的代碼簡化↓↓↓
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
class Person
{
public:
Person(const string& name, const int& age, const char& gender)
:_name(name)
,_age(age)
,_gender(gender)
{}
Person& operator=(const Person& p)
{
_name = p._name;
_age = p._age;
_gender = p._gender;
return *this;
}
void Show()
{
cout << "My name is " << _name << ", I am " << _age << " years old, I am a "
<< (_gender == 'M' ? " boy " : "girl") << endl;
}
private:
string _name;
int _age;
char _gender;
};
class Student : public Person
{
public:
Student(const string& name, const int& age, const char& gender, const char* detail)
:Person(name, age, gender)
{
_detail = new char[strlen(detail) + 1];
strcpy(_detail, detail);
}
Student& operator=(const Student& s)
{
Person::operator=(s);
char* detail = new char[strlen(s._detail)];
return *this;
}
void Show()
{
Person::Show();
cout << "My detail infomation is " << _detail << endl;
}
~Student()
{
delete[] _detail;
}
private:
char* _detail;
};
int main()
{
Student s("Jammingpro", 18, 'M', "He is good at coding");
Student copy = s;
s.Show();
}
-
派生類的析構函數(shù)會在被調用完成后自動調用基類的析構函數(shù)清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
-
派生類對象初始化先調用基類構造再調派生類構造。
下面代碼演示了基類與派生類的構造與析構順序↓↓↓
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base() is called" << endl;
}
~Base()
{
cout << "~Base() is deleted obj" << endl;
}
};
class Son : public Base
{
public:
Son()
{
cout << "Son is called" << endl;
}
~Son()
{
cout << "~Son() is deleted obj" << endl;
}
};
int main()
{
Son s;
return 0;
}
- 編譯器會對析構函數(shù)名進行特殊處理,處理成destrutor(),所以父類析構函數(shù)不加virtual的情況下,子類析構函數(shù)和父類析構函數(shù)構成隱藏關系。
★ps:關于virtual關鍵字將于多態(tài)中講解
繼承與友元
友元關系不能不繼承,也就是說:基類的友元不能訪問派生類的私有和保護成員。
下面代碼演示了友元關系無法繼承↓↓↓
#include <iostream>
using namespace std;
class Base
{
friend void print();
private:
int _base = 88;
};
class Son : Base
{
private:
int _son = 66;
};
void print()
{
Base b;
cout << b._base << endl;
Son s;
cout << s._son << endl;
}
int main()
{
print();
return 0;
}
繼承與靜態(tài)成員
基類定義了static靜態(tài)成員,則整個繼承體系中只能有一個這樣的成員。無論派生出多少多少個子類,都只有一個static成員實例。
#include <iostream>
using namespace std;
class Base
{
public:
static int val;
};
//靜態(tài)非const成員變量需要在類外初始化
int Base::val = 66;
class Son : public Base
{
};
class GrandSon : Son
{
};
int main()
{
cout << &Base::val << endl;
cout << &Son::val << endl;
cout << &GrandSon::val << endl;
return 0;
}
復雜的菱形繼承及菱形虛擬繼承
單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承
下面是一份單繼承的代碼↓↓↓
#include <iostream>
using namespace std;
class Base
{
public:
void base_func()
{}
int _base;
};
class Son : public Base
{
public:
void son_func()
{}
int _son;
};
class GrandSon : public Son
{
public:
void gs_func()
{}
int _gs;
};
int main()
{
GrandSon gs;
cout << &gs << endl;
gs._base = 1;
cout << &gs._base << endl;
gs._son = 2;
cout << &gs._son << endl;
gs._gs = 3;
cout << &gs._gs << endl;
cout << "===================================" << endl;
Son s;
cout << &s << endl;
s._base = 1;
cout << &s._base << endl;
s._son = 2;
cout << &s._son << endl;
return 0;
}
上述代碼調試時,通過監(jiān)視窗口查看結果如下圖所示。我們可以發(fā)現(xiàn),GrandSon對象保存了其祖先類Son、Base的成員變量。Son對象保存了其基類成員變量。
下圖為GrandSon對象的存儲情況↓↓↓
通過分析上面的執(zhí)行結果,可以得出如下結論:再單繼承中,某個類的成員變量放置于類空間最后,該成員變量前放置的是直接父類,再往上是爺爺類,以此類推。類對象的地址,與最頂層的祖先的成員變量地址相同。
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
下面給出多繼承的演示代碼↓↓↓
#include <iostream>
using namespace std;
class Base1
{
public:
void base1_func()
{}
int _base1;
};
class Base2
{
public:
void base2_func()
{}
int _base2;
};
class Son : public Base1, public Base2
{
public:
void son_func()
{}
int _son;
};
int main()
{
Son s;
cout << &s << endl;
s._base1 = 1;
cout << &s._base1 << endl;
s._base2 = 2;
cout << &s._base2 << endl;
s._son = 3;
cout << &s._son << endl;
return 0;
}
如果Son先繼承Base1再繼承Base2,則會將Base1的成員變量放在前面,后繼承的Base2的成員變量放在后面。
如果將Son先繼承Base2,再繼承Base1呢?
由上面的執(zhí)行結果可知,先繼承的基類的成員變量放置于類對象的前面位置,后即成的基類的成員變量放置于類對象的后面位置,類自身的成員變量放置于最后。
菱形繼承:菱形繼承是多繼承的一種特殊情況
下面給出多繼承的演示代碼↓↓↓
#include <iostream>
using namespace std;
class Share
{
public:
void share_func()
{}
int _share;
};
class Base1 : Share
{
public:
void base1_func()
{}
int _base1;
};
class Base2 : Share
{
public:
void base2_func()
{}
int _base2;
};
class Son : public Base1, public Base2
{
public:
void son_func()
{}
int _son;
};
int main()
{
Son s;
s._base1 = 1;
s._base2 = 2;
s._son = 3;
return 0;
}
在上述代碼的監(jiān)視窗口可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題。s中繼承了兩份Share類的成員變量_share。
C++中為了避免菱形繼承導致的數(shù)據(jù)冗余和二義性,它引入了虛擬繼承。虛擬繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余的問題。下面給出修改后的代碼(引入虛擬繼承的代碼)↓↓↓
#include <iostream>
using namespace std;
class Share
{
public:
void share_func()
{}
int _share;
};
class Base1 : virtual public Share
{
public:
void base1_func()
{}
int _base1;
};
class Base2 : virtual public Share
{
public:
void base2_func()
{}
int _base2;
};
class Son : public Base1, public Base2
{
public:
void son_func()
{}
int _son;
};
int main()
{
Son s;
cout << &s << endl;
s._share = 0;
cout << &s._share << endl;
s._base1 = 1;
cout << &s._base1 << endl;
s._base2 = 2;
cout << &s._base2 << endl;
s._son = 3;
cout << &s._son << endl;
return 0;
}
這里是通過了兩個指針,指向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量可以找到下面的Share。文章來源:http://www.zghlxwxcb.cn/news/detail-846131.html
??歡迎進入淺嘗C++專欄,查看更多文章。
如果上述內容有任何問題,歡迎在下方留言區(qū)指正b( ̄▽ ̄)d文章來源地址http://www.zghlxwxcb.cn/news/detail-846131.html
到了這里,關于【淺嘗C++】繼承機制=>虛基表/菱形虛繼承/繼承的概念、定義/基類與派生類對象賦值轉換/派生類的默認成員函數(shù)等詳解的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!