首先外面要知道:參數(shù)的傳遞本質(zhì)上是一次賦值的過程,賦值就是對(duì)內(nèi)存進(jìn)行拷貝。所謂內(nèi)存拷貝,是指將一塊內(nèi)存上的數(shù)據(jù)復(fù)制到另一塊內(nèi)存上,對(duì)于聚合類 型(復(fù)雜類型,類似結(jié)構(gòu)體和類這些)消耗的內(nèi)存可能會(huì)非常大。
創(chuàng)新互聯(lián)公司是一家專注于成都做網(wǎng)站、網(wǎng)站建設(shè)與策劃設(shè)計(jì),橋西網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)公司做網(wǎng)站,專注于網(wǎng)站建設(shè)十余年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:橋西等地區(qū)。橋西做網(wǎng)站價(jià)格咨詢:18982081108
引用可以看做是數(shù)據(jù)的一個(gè)別名,通過這個(gè)別名和原來的名字都能夠找到這份數(shù)據(jù)(指向同一個(gè)內(nèi)存)
注意:
&
,在使用時(shí)不能添加&
,使用時(shí)添加&
表示取地址int a = 99;
int &r = a;
cout << a << ", " << r << endl;
一般c++中,引用作為函數(shù)參數(shù),代替了指針的功能,一樣達(dá)到改變數(shù)據(jù)內(nèi)容的效果, 非常實(shí)用;
同時(shí)c++中,引用可以作為函數(shù)返回值,但是!?。〔荒芊祷鼐植繑?shù)據(jù)(例如局部變量、局部對(duì)象、局部數(shù)組等)的引用,因?yàn)楫?dāng)函數(shù)調(diào)用完成后局部數(shù)據(jù)就會(huì)被銷毀,有可能在下次使用時(shí)數(shù)據(jù)就不存在了
int &plus10(int &r) {
int m = r + 10;
return m; //返回局部數(shù)據(jù)的引用
}
//對(duì)于一些編譯器,就會(huì)報(bào)錯(cuò)
int &num3 = plus10(num1);
int &num4 = plus10(num3);
//但是有些編譯器是可以運(yùn)行的,比如gcc,但是num3和num4的值是一樣的,因?yàn)?函數(shù)是在棧上運(yùn)行的,并且運(yùn)行結(jié)束后會(huì)放棄對(duì)所有局部數(shù)據(jù)的管理權(quán),后面的函數(shù)調(diào)用會(huì)覆蓋前面函數(shù)的局部數(shù)據(jù),兩個(gè)指向的地方改成了最后一個(gè)值;
引用的本質(zhì):
其實(shí)引用只是對(duì)指針進(jìn)行了簡(jiǎn)單的封裝,它的底層依然是通過指針實(shí)現(xiàn)的,引用占用的內(nèi)存和指針占用的內(nèi)存長(zhǎng)度一樣,在 32 位環(huán)境下是 4 個(gè)字節(jié),在 64 位環(huán)境下是 8 個(gè)字節(jié),之所以不能獲取引用的地址,是因?yàn)榫幾g器進(jìn)行了內(nèi)部轉(zhuǎn)換:
int a = 99;
int &r = a;
r = 18;
cout<<&r<
&r
取地址時(shí),編譯器會(huì)對(duì)代碼進(jìn)行隱式的轉(zhuǎn)換,使得代碼輸出的是 r 的內(nèi)容(a 的地址),而不是 r 的地址,這就是為什么獲取不到引用變量的地址的原因。也就是說,不是變量 r 不占用內(nèi)存,而是編譯器不讓獲取它的地址。
指針和引用的其他區(qū)別:
引用必須在定義時(shí)初始化,并且以后也要從一而終,不能再指向其他數(shù)據(jù);而指針沒有這個(gè)限制,指針在定義時(shí)不必賦值,以后也能指向任意數(shù)據(jù)
可以有 const 指針,但是沒有 const 引用,r 本來就不能改變指向,加上 const 是多此一舉。
指針可以有多級(jí),但是引用只能有一級(jí)(學(xué)了引用折疊可以想想還正不正確),例如,int **p
是合法的,而int &&r
是不合法的(c++11增加右值引用,合法),下面這個(gè)是可以的
int a = 10;
int &r = a;
int &rr = r;
//都是指向a的地址
指針和引用的自增(++)自減(--)運(yùn)算意義不一樣。對(duì)指針使用 ++ 表示指向下一份數(shù)據(jù),對(duì)引用使用 ++ 表示它所指代的數(shù)據(jù)本身加 1
引用一般不能綁定臨時(shí)數(shù)據(jù):
指針和引用只能指向內(nèi)存,不能指向寄存器或者硬盤,因?yàn)榧拇嫫骱陀脖P沒法尋址。
定義的變量、創(chuàng)建的對(duì)象、字符串常量、函數(shù)形參、函數(shù)體本身、new
或malloc()
分配的內(nèi)存等,這些內(nèi)容都可以用&
來獲取地址。
什么數(shù)據(jù)不能用&,它是會(huì)在寄存器:
int *p2 = &(n + 100);//不行,n+100會(huì)在寄存器中,常量表達(dá)式也在寄存器中
S s1 = {23, 45};
S s2 = {90, 75};
S *p1 = &(s1 + s2);//visualC++中可以,s1+s2在內(nèi)存中,但是?。。?!gcc不行,因?yàn)間cc不能指代任何臨時(shí)變量!??!
bool isOdd(int &n){
if(n%2 == 0){
return false;
}else{
return true;
}
}
isOdd(a); //正確
isOdd(a + 9); //錯(cuò)誤,有時(shí)候很容易給它傳遞臨時(shí)數(shù)據(jù)
要去看看
const引用綁定臨時(shí)數(shù)據(jù):
常引用:編譯器會(huì)為臨時(shí)數(shù)據(jù)創(chuàng)建一個(gè)新的、無名的臨時(shí)變量,并將臨時(shí)數(shù)據(jù)放入該臨時(shí)變量中,然后再將引用綁定到該臨時(shí)變量。
改為常引用即可,因?yàn)?為普通引用創(chuàng)建臨時(shí)變量沒有任何意義,創(chuàng)建的臨時(shí)變量,修改的也僅僅是臨時(shí)變量里面的數(shù)據(jù),不會(huì)影響原來的數(shù)據(jù),意義不大。 而常引用,我們只能通過 const 引用讀取數(shù)據(jù)的值,而不能修改它的值,所以不用考慮同步更新的問題,也不會(huì)產(chǎn)生兩份不同的數(shù)據(jù)。
bool isOdd(const int &n){ //改為常引用
if(n/2 == 0){
return false;
}else{
return true;
}
}
isOdd(7 + 8); //正確
isOdd(a + 9); //正確
const引用與類型轉(zhuǎn)換:
指針類型轉(zhuǎn)換是錯(cuò)誤的(意想不到的錯(cuò)誤hhh),因?yàn)椴煌愋偷臄?shù)據(jù)占用的內(nèi)存數(shù)量不一樣,處理方式也不一樣(可以看看《整數(shù)在內(nèi)存中是如何存儲(chǔ)的》《小數(shù)在內(nèi)存中是如何存儲(chǔ)的》借鑒一下);因?yàn)橐玫谋举|(zhì)也是指針,所以引用的類型轉(zhuǎn)換也是錯(cuò)誤的。
int n = 100;
int *p1 = &n; //正確
float *p2 = &n; //錯(cuò)誤
int &r1 = n; //正確
float &r2 = n; //錯(cuò)誤
但是?。?!加常引用就可以發(fā)生類型轉(zhuǎn)換
原理:引用的類型和數(shù)據(jù)的類型不一致時(shí),如果它們的類型是相近的,并且遵守「數(shù)據(jù)類型的自動(dòng)轉(zhuǎn)換」規(guī)則,那么編譯器就會(huì)創(chuàng)建一個(gè)臨時(shí)變量,并將數(shù)據(jù)賦值給這個(gè)臨時(shí)變量(這時(shí)候會(huì)發(fā)生自動(dòng)類型轉(zhuǎn)換),然后再將引用綁定到這個(gè)臨時(shí)的變量,這與「將 const 引用綁定到臨時(shí)數(shù)據(jù)時(shí)」采用的方案是一樣的。
int n = 100;
int &r1 = n; //正確
const float &r2 = n; //正確
綜上:如果函數(shù)不要求改變所引用的值,函數(shù)形參盡量使用const;一來避免臨時(shí)數(shù)據(jù),二來避免非同類型,三來const和非const實(shí)參都可以接受;
double volume(const double &len, const double &width, const double &hei){
return len*width*2 + len*hei*2 + width*hei*2;
}
double v4 = volume(a+12.5, b+23.4, 16.78);
double v5 = volume(a+b, a+c, b+c);
被繼承的類稱為父類或基類,繼承的類稱為子類或派生類。“子類”和“父類”通常放在一起稱呼,“基類”和“派生類”通常放在一起稱呼。是同一個(gè)意思。
什么時(shí)候用繼承:肯定是類與類之間有很大關(guān)聯(lián),有很多共同成員函數(shù)和成員變量啦。
繼承過來的成員,可以通過子類對(duì)象訪問,就像自己的一樣。
繼承格式:
class 派生類名:[繼承方式] 基類名{
派生類新增加的成員
};
三種繼承方式:
public繼承方式
protected繼承方式
private繼承方式
protected屬性只有在派生類中(類代碼中) 才能訪問;其他都不可以;
不難發(fā)現(xiàn):
1)繼承方式中的 public、protected、private 是用來指明基類成員在派生類中的最高訪問權(quán)限的,是不可以超過的,即即使基類中是public成員屬性,派生類中采用protected繼承,那public成員屬性也只能變成protected;
2)基類中的 private 成員在派生類中始終不能使用,但是可以通過public的set和get函數(shù)使用(在派生類中訪問基類 private 成員的唯一方法就是借助基類的非 private 成員函數(shù))
3)如果希望基類的成員能夠被派生類繼承并且毫無障礙地使用,那么這些成員只能聲明為 public 或 protected
4)如果希望基類的成員既不向外暴露(不能通過對(duì)象訪問),還能在派生類中使用,那么只能聲明為 protected
注意????我們這里說的是基類的 private 成員不能在派生類中使用,并沒有說基類的 private 成員不能被繼承。實(shí)際上,基類的 private 成員是能夠被繼承的,并且(成員變量)會(huì)占用派生類對(duì)象的內(nèi)存,它只是在派生類中不可見,導(dǎo)致無法使用罷了。
改變?cè)L問權(quán)限的方法,用using關(guān)鍵字
using 只能改變基類中 public 和 protected 成員的訪問權(quán)限,不能改變 private 成員的訪問權(quán)限,因?yàn)榛愔?private 成員在派生類中是不可見的,寫不了;
//基類People
class People {
public:
void show();
protected:
char *m_name;
int m_age;
};
void People::show() {
cout << m_name << "的年齡是" << m_age << endl;
}
//派生類Student
class Student : public People {
public:
void learning();
public:
using People::m_name; //將protected改為public
using People::m_age; //將protected改為public
float m_score;
private:
using People::show; //將public改為private
};
繼承時(shí)的名字遮蔽
對(duì)于函數(shù),不管函數(shù)的參數(shù)如何,只要名字一樣就會(huì)造成遮蔽,不會(huì)有重載,如果想要調(diào)用就用域名和域解析符;
對(duì)于成員變量,只要名字一樣,派生類的會(huì)遮蔽基類,但是基類的成員變量一樣時(shí)存在的,此時(shí)從基類中繼承的get和set方法都是對(duì)基類同名變量的操作,不會(huì)是對(duì)派生類的同名變量操作。
假設(shè) Base 是基類,Derived 是派生類,那么它們的作用域的嵌套關(guān)系會(huì)有:
編譯器會(huì)在當(dāng)下類作用域從內(nèi)找到外:
通過 obj (c類對(duì)象)訪問成員變量 n 時(shí),在 C 類的作用域中就能夠找到了 n 這個(gè)名字。雖然 A 類和 B 類都有名字 n,但編譯器不會(huì)到它們的作用域中查找,所以是不可見的,也即派生類中的 n 遮蔽了基類中的 n。
通過 obj 訪問成員函數(shù) func() 時(shí),在 C 類的作用域中沒有找到 func 這個(gè)名字,B沒找到,再繼續(xù)到 A 類的作用域中查找,結(jié)果就發(fā)現(xiàn)了 func 這個(gè)名字,查找結(jié)束,編譯器決定調(diào)用 A 類作用域中的 func() 函數(shù)(這個(gè)過程叫名字查找,都是通過名字查找,除非直接通過域名和域解析符去找,就不會(huì)有這個(gè)過程);
對(duì)象內(nèi)存模型:
無繼承的時(shí)候比較簡(jiǎn)單,變量存在堆或者棧區(qū),函數(shù)存在代碼段;
存在繼承時(shí):
所有變量連續(xù)存在堆區(qū)或者棧區(qū)(成員變量按照派生的層級(jí)依次排列,新增成員變量始終在最后,而且private的、遮掩的也會(huì)在內(nèi)存中),函數(shù)存在代碼區(qū)(所有對(duì)象共享,但是能不能用也要看它的權(quán)限,如果時(shí)private也用不了)
例子:
obj_a 是基類對(duì)象,obj_b 是派生類對(duì)象。假設(shè) obj_a 的起始地址為 0X1000,那么它的內(nèi)存分布如下圖所示:
假設(shè) obj_b 的起始地址為 0X1100,a類中的m_b是private,那么它的內(nèi)存分布如下圖所示:
假設(shè) obj_c 的起始地址為 0X1300,存在遮掩的情況,那么它的內(nèi)存分布如下圖所示:
總結(jié):在派生類的對(duì)象模型中,會(huì)包含所有基類的成員變量。這種設(shè)計(jì)方案的優(yōu)點(diǎn)是訪問效率高,能夠在派生類對(duì)象中直接訪問基類變量,無需經(jīng)過好幾層間接計(jì)算。
在設(shè)計(jì)派生類時(shí),對(duì)繼承過來的成員變量的初始化工作也要由派生類的構(gòu)造函數(shù)完成,但是大部分基類都有 private 屬性的成員變量,它們?cè)谂缮愔袩o法訪問,更不能使用派生類的構(gòu)造函數(shù)來初始化。解決這個(gè)問題的思路是:在派生類的構(gòu)造函數(shù)中調(diào)用基類的構(gòu)造函數(shù)。
#include
using namespace std;
//基類People
class People{
protected:
char *m_name;
int m_age;
public:
People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}
//派生類Student
class Student: public People{
private:
float m_score;
public:
Student(char *name, int age, float score);
void display();
};
//People(name, age)就是調(diào)用基類的構(gòu)造函數(shù)
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
cout<
People(name, age)
就是調(diào)用基類的構(gòu)造函數(shù),并將 name 和 age 作為實(shí)參傳遞給它,m_score(score)
是派生類的參數(shù)初始化表。其次,m_score(score)放在前面也沒有問題,它都會(huì)遵循先調(diào)用基類構(gòu)造函數(shù)再執(zhí)行參數(shù)初始化表中的其他成員變量初始化。
構(gòu)造函數(shù)調(diào)用順序:
當(dāng)A->B->C類, 執(zhí)行的順序是 A類構(gòu)造函數(shù) --> B類構(gòu)造函數(shù) --> C類構(gòu)造函數(shù),A是C的間接基類,B才是C的直接基類; 派生類構(gòu)造函數(shù)中只能調(diào)用直接基類的構(gòu)造函數(shù),不能調(diào)用間接基類的,因?yàn)镃掉用B類的構(gòu)造函數(shù),B中又會(huì)先去調(diào)用A類的構(gòu)造函數(shù), 相當(dāng)于 C 間接地(或者說隱式地)調(diào)用了 A 的構(gòu)造函數(shù),如果再在 C 中顯式地調(diào)用 A 的構(gòu)造函數(shù),那么 A 的構(gòu)造函數(shù)就被調(diào)用了兩次,相應(yīng)地,初始化工作也做了兩次,這不僅是多余的,還會(huì)浪費(fèi)CPU時(shí)間以及內(nèi)存。
基類構(gòu)造函數(shù)調(diào)用規(guī)則:
通過派生類創(chuàng)建對(duì)象時(shí)必須要調(diào)用基類的構(gòu)造函數(shù),這是語法規(guī)定。換句話說,定義派生類構(gòu)造函數(shù)時(shí)最好指明基類構(gòu)造函數(shù);如果不指明,就調(diào)用基類的默認(rèn)構(gòu)造函數(shù)(不帶參數(shù)的構(gòu)造函數(shù));如果沒有默認(rèn)構(gòu)造函數(shù)。
#include
using namespace std;
//基類People
class People{
public:
People(); //基類默認(rèn)構(gòu)造函數(shù)
People(char *name, int age);
protected:
char *m_name;
int m_age;
};
People::People(): m_name("xxx"), m_age(0){ }
People::People(char *name, int age): m_name(name), m_age(age){}
//派生類Student
class Student: public People{
public:
Student();
Student(char*, int, float);
public:
void display();
private:
float m_score;
};
Student::Student(): m_score(0.0){ } //派生類默認(rèn)構(gòu)造函數(shù)
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
cout<
創(chuàng)建對(duì)象 stu1 時(shí),執(zhí)行派生類的構(gòu)造函數(shù)Student::Student()
,它并沒有指明要調(diào)用基類的哪一個(gè)構(gòu)造函數(shù),從運(yùn)行結(jié)果可以很明顯地看出來,系統(tǒng)默認(rèn)調(diào)用了不帶參數(shù)的構(gòu)造函數(shù),也就是People::People()
。
創(chuàng)建對(duì)象 stu2 時(shí),執(zhí)行派生類的構(gòu)造函數(shù)Student::Student(char *name, int age, float score)
,它指明了基類的構(gòu)造函數(shù)。
對(duì)于析構(gòu)函數(shù)
析構(gòu)函數(shù)也不能被繼承。與構(gòu)造函數(shù)不同的是,在派生類的析構(gòu)函數(shù)中不用顯式地調(diào)用基類的析構(gòu)函數(shù),因?yàn)槊總€(gè)類只有一個(gè)析構(gòu)函數(shù),編譯器知道如何選擇,無需我們干涉。
析構(gòu)函數(shù)的執(zhí)行順序和構(gòu)造函數(shù)的執(zhí)行順序也是剛好相反:
c++不僅有單繼承,還有多繼承
class D: public A, private B, protected C{
//類D新增加的成員
}
D 是多繼承形式的派生類,它以公有的方式繼承 A 類,以私有的方式繼承 B 類,以保護(hù)的方式繼承 C 類。D 根據(jù)不同的繼承方式獲取 A、B、C 中的成員,確定它們?cè)谂缮愔械脑L問權(quán)限。
多繼承下的構(gòu)造:
基類構(gòu)造函數(shù)的調(diào)用順序和和它們?cè)谂缮悩?gòu)造函數(shù)中出現(xiàn)的順序無關(guān),而是和聲明派生類時(shí)基類出現(xiàn)的順序相同(和類中的變量相似),像上面的例子就會(huì)先構(gòu)造A,再構(gòu)造B,然后是C,最后是D;
命名沖突:
當(dāng)兩個(gè)或多個(gè)基類中有同名的成員(成員變量或成員函數(shù))時(shí),如果直接訪問該成員,就會(huì)產(chǎn)生命名沖突,編譯器不知道使用哪個(gè)基類的成員。這個(gè)時(shí)候需要在成員名字前面加上類名和域解析符::
,以顯式地指明到底使用哪個(gè)類的成員,消除二義性。
內(nèi)存模型:
直接上例子:
#include
using namespace std;
//基類A
class A{
public:
A(int a, int b);
protected:
int m_a;
int m_b;
};
A::A(int a, int b): m_a(a), m_b(b){ }
//基類B
class B{
public:
B(int b, int c);
protected:
int m_b;
int m_c;
};
B::B(int b, int c): m_b(b), m_c(c){ }
//派生類C
class C: public A, public B{
public:
C(int a, int b, int c, int d);
public:
void display();
private:
int m_a;
int m_c;
int m_d;
};
C::C(int a, int b, int c, int d): A(a, b), B(b, c), m_a(a), m_c(c), m_d(d){ }
void C::display(){
printf("A::m_a=%d, A::m_b=%d\n", A::m_a, A::m_b);
printf("B::m_b=%d, B::m_c=%d\n", B::m_b, B::m_c);
printf("C::m_a=%d, C::m_c=%d, C::m_d=%d\n", C::m_a, C::m_c, m_d);
}
int main(){
C obj_c(10, 20, 30, 40);
obj_c.display();
return 0;
}
借助指針突破訪問權(quán)限:
想想指針 指向的是內(nèi)存的地址,對(duì)象指針指向的是對(duì)象的內(nèi)存地址,而通過內(nèi)存模型可以知道private也是在連續(xù)的內(nèi)存中,所以?。?!只要使用指針偏移就可以強(qiáng)行訪問private成員變量;例如:
圖中假設(shè) obj 對(duì)象的起始地址為 0X1000,m_a(public)、m_b(public)、m_c (private)與對(duì)象開頭分別相距 0、4、8 個(gè)字節(jié),我們將這段距離稱為偏移(Offset)
要知道:
int b = p -> m_b;
會(huì)轉(zhuǎn)換成:int b = * (int*)( (int)p + sizeof(int) );
實(shí)際上就是:int b = * (int*) ( (int)p + 4 );
有:
所以:int c = * (int* )( (int)p + sizeof(int)*2 );//就是這么簡(jiǎn)單
1.什么是虛繼承和虛基類
多繼承容易產(chǎn)生命名沖突:經(jīng)典的菱形繼承
第一類問題:在一個(gè)派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數(shù)據(jù),但大多數(shù)情況下這是多余的:因?yàn)楸A舳喾莩蓡T變量不僅占用較多的存儲(chǔ)空間,還容易產(chǎn)生命名沖突。假如類 A 有一個(gè)成員變量 a,那么在類 D 中直接訪問 a 就會(huì)產(chǎn)生歧義,編譯器不知道它究竟來自 A -->B-->D 這條路徑,還是來自 A-->C-->D 這條路徑。
為了消除歧義,我們可以在 m_a 的前面指明它具體來自哪個(gè)類(用域來處理)
但是內(nèi)存中還是存在兩份間接基類,是消耗內(nèi)存的,所以為了解決多繼承時(shí)的命名沖突和冗余數(shù)據(jù)問題,就 提出了虛繼承,使得在派生類中只保留一份間接基類的成員。
//間接基類A
class A{ //虛基類
protected:
int m_a;
};
//直接基類B
class B: virtual public A{ //虛繼承
protected:
int m_b;
};
//直接基類C
class C: virtual public A{ //虛繼承
protected:
int m_c;
};
//派生類D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正確
void setb(int b){ m_b = b; } //正確
void setc(int c){ m_c = c; } //正確
void setd(int d){ m_d = d; } //正確
private:
int m_d;
};
int main(){
D d;
return 0;
}
虛繼承的目的是讓某個(gè)類做出聲明,承諾愿意共享它的基類。其中,這個(gè)被共享的基類就稱為虛基類。
可以看出一個(gè)問題:必須在虛派生的真實(shí)需求出現(xiàn)前就已經(jīng)完成虛派生的操作,即在出現(xiàn)D的需求前,就要把B、C設(shè)定成虛繼承;
即是虛派生只影響從指定了虛基類的派生類中進(jìn)一步派生出來的類(繼承BC的類,如E繼承B,F(xiàn)繼承C,就會(huì)有所影響),它不會(huì)影響派生類本身(BC);
實(shí)際開發(fā)中,位于中間層次的基類將其繼承聲明為虛繼承一般不會(huì)帶來什么問題。使用虛繼承的類層次是由一個(gè)人或者一個(gè)項(xiàng)目組一次性設(shè)計(jì)完成,這樣不需要因?yàn)闆]有考慮到后面的需求而重新修改中間的函數(shù); c++庫iostream就是采用虛繼承。
2.虛基類成員的的可見性
以菱形繼承為例,假設(shè) A 定義了一個(gè)名為 x 的成員變量,當(dāng)我們?cè)?D 中直接訪問 x 時(shí),會(huì)有三種可能性:
第二類問題:就是BC中含有優(yōu)先級(jí)相等的相同變量,這個(gè)時(shí)候只能用域解析來去除二義性。
不提倡在程序中使用多繼承?。?!只有在比較簡(jiǎn)單和不易出現(xiàn)二義性的情況或?qū)嵲诒匾獣r(shí)才使用多繼承,能用單一繼承解決的問題就不要使用多繼承。小伙子,這不是你能駕馭的????????????
3.虛繼承的構(gòu)造函數(shù)和內(nèi)存模型:
與繼承時(shí)的構(gòu)造過程不同,最終派生類的構(gòu)造函數(shù)必須要調(diào)用虛基類的構(gòu)造函數(shù)。
#include
using namespace std;
//虛基類A
class A{
public:
A(int a);
protected:
int m_a;
};
A::A(int a): m_a(a){ }
//直接派生類B
class B: virtual public A{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"m_a="<class A{
protected:
int m_a1;
int m_a2;
};
class B: public A{
protected:
int b1;
int b2;
};
class C: public B{
protected:
int c1;
int c2;
};
class D: public C{
protected:
int d1;
int d2;
};
int main(){
A obj_a;
B obj_b;
C obj_c;
D obj_d;
return 0;
}
A類所處的內(nèi)存位置一直在前頭
1)修改上面的代碼,使得 A 是 B 的虛基類:
class B: virtual public A
A會(huì)移動(dòng)到后面
2)再假設(shè) A 是 B 的虛基類,B 又是 C 的虛基類
從上面的兩張圖中可以發(fā)現(xiàn),虛繼承時(shí)的派生類對(duì)象被分成了兩部分:
而有一個(gè)問題:如何計(jì)算共享部分的偏移量?
對(duì)于虛繼承,將派生類分為固定部分和共享部分,并把共享部分放在最后,幾乎所有的編譯器都在這一點(diǎn)上達(dá)成了共識(shí)。主要的分歧就是如何計(jì)算共享部分的偏移,百花齊放,沒有統(tǒng)一標(biāo)準(zhǔn)。
這里舉例出VS的解決辦法:
VC 引入了虛基類表,如果某個(gè)派生類有一個(gè)或多個(gè)虛基類,編譯器就會(huì)在派生類對(duì)象中安插一個(gè)指針,指向虛基類表。虛基類表其實(shí)就是一個(gè)數(shù)組,數(shù)組中的元素存放的是各個(gè)虛基類的偏移字節(jié)數(shù)。
假設(shè) A 是 B 的虛基類,同時(shí) B 又是 C 的虛基類,那么各對(duì)象的內(nèi)存模型如下圖所示:
虛繼承表中保存的是所有虛基類(包括直接繼承和間接繼承到的)相對(duì)于當(dāng)前對(duì)象的偏移,這樣通過派生類指針訪問虛基類的成員變量時(shí),不管繼承層次都多深,只需要一次間接轉(zhuǎn)換就可以。
這種方案還可以避免有多個(gè)虛基類時(shí)讓派生類對(duì)象額外背負(fù)過多的指針,只需要背負(fù)一個(gè)指針即可。例如,假設(shè) A、B、C、D 類的繼承關(guān)系為:
內(nèi)存模型為:
發(fā)生數(shù)據(jù)類型轉(zhuǎn)換時(shí), int 類型的數(shù)據(jù)賦值給 float 類型的變量時(shí),編譯器會(huì)先把 int 類型的數(shù)據(jù)轉(zhuǎn)換為 float 類型再賦值;類似的,類也可以發(fā)生數(shù)據(jù)類型轉(zhuǎn)換,它也是一種數(shù)據(jù)類型;
不過這種轉(zhuǎn)換只有在基類和派生類之間才有意義,并且只能將派生類賦值給基類,包括將派生類對(duì)象賦值給基類對(duì)象、將派生類指針賦值給基類指針、將派生類引用賦值給基類引用,這在 C++ 中稱為向上轉(zhuǎn)型(Upcasting)。相應(yīng)地,將基類賦值給派生類稱為向下轉(zhuǎn)型。
上轉(zhuǎn)型時(shí)非常安全的。
賦值的本質(zhì)是將現(xiàn)有的數(shù)據(jù)寫入已分配好的內(nèi)存中,對(duì)象的內(nèi)存只包含了成員變量,所以對(duì)象之間的賦值是成員變量的賦值,成員函數(shù)不存在賦值問題。
這種轉(zhuǎn)換關(guān)系是不可逆的,只能用派生類對(duì)象給基類對(duì)象賦值,而不能用基類對(duì)象給派生類對(duì)象賦值。(因?yàn)榛惒话缮惖某蓡T變量,無法對(duì)派生類的成員變量賦值。同理,同一基類的不同派生類對(duì)象之間也不能賦值)。
#include
using namespace std;
//基類
class A{
public:
A(int a);
public:
void display();
public:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
cout<<"Class A: m_a="<
除了派生類對(duì)象賦值給類基類對(duì)象,還可以將派生類指針賦值給基類指針:
下列繼承關(guān)系:
#include
using namespace std;
//基類A
class A{
public:
A(int a);
public:
void display();
protected:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
cout<<"Class A: m_a="<
可以看出:與對(duì)象變量之間的賦值不同的是,對(duì)象指針之間的賦值并沒有拷貝對(duì)象的成員,也沒有修改對(duì)象本身的數(shù)據(jù),僅僅是改變了指針的指向。
第一個(gè)問題:為什么pa的地址改變了,指向了pd,為什么調(diào)用的函數(shù)依舊是pa的呢?
輸出值解釋:pa 本來是基類 A 的指針,現(xiàn)在指向了派生類 D 的對(duì)象,這使得隱式指針 this 發(fā)生了變化,也指向了 D 類的對(duì)象,所以最終在 display() 內(nèi)部使用的是 D 類對(duì)象的成員變量,相信這一點(diǎn)不難理解。
函數(shù)調(diào)用解釋:編譯器雖然通過指針的指向來訪問成員變量,但是卻不通過指針的指向來訪問成員函數(shù):編譯器通過指針的類型來訪問成員函數(shù)。對(duì)于 pa,它的類型是 A,不管它指向哪個(gè)對(duì)象,使用的都是 A 類的成員函數(shù)。具體原因已在 之前筆記:C++函數(shù)編譯原理和成員函數(shù)的實(shí)現(xiàn)中 做了詳細(xì)講解;
第二個(gè)問題: 為什么pc的地址不是和pd的地址一樣呢,而pa和pb和pd都是一樣的呢;
我們通常認(rèn)為,賦值就是將一個(gè)變量的值交給另外一個(gè)變量,這種想法雖然沒錯(cuò),但是有一點(diǎn)要注意,就是賦值以前編譯器可能會(huì)對(duì)現(xiàn)有的值進(jìn)行處理。例如將 double 類型的值賦給 int 類型的變量,編譯器會(huì)直接抹掉小數(shù)部分,導(dǎo)致賦值運(yùn)算符兩邊變量的值不相等
將派生類的指針賦值給基類的指針時(shí)也是類似的道理,編譯器也可能會(huì)在賦值前進(jìn)行處理:
對(duì)象的指針必須要指向?qū)ο蟮钠鹗嘉恢谩?duì)于 A 類和 B 類來說,它們的子對(duì)象的起始地址和 D 類對(duì)象一樣,所以將 pd 賦值給 pa、pb 時(shí)不需要做任何調(diào)整,直接傳遞現(xiàn)有的值即可;而 C 類子對(duì)象距離 D 類對(duì)象的開頭有一定的偏移,將 pd 賦值給 pc 時(shí)要加上這個(gè)偏移,這樣 pc 才能指向 C 類子對(duì)象的起始位置,即執(zhí)行pc = pd;
語句時(shí)編譯器對(duì) pd 的值進(jìn)行了調(diào)整,才導(dǎo)致 pc、pd 的值不同
內(nèi)存模型:
將派生類引用賦值給基類引用
引用只是將指針封裝了一下,本質(zhì)上并沒有什么區(qū)別,所以猜想派生類引用賦值給基類引用的效果和指針應(yīng)該是一樣的。
//改改上述main函數(shù)中的內(nèi)容
int main(){
D d(4, 40, 400, 4000);
A &ra = d;
B &rb = d;
C &rc = d;
ra.display();
rb.display();
rc.display();
return 0;
}
果然,運(yùn)行結(jié)果:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
具體分析都和指針一樣;
可以去看看這個(gè)博客加強(qiáng)向上轉(zhuǎn)型的理解