印象筆記同步分享:《程序員面試寶典》學習記錄7
整個C++程序設計全面圍繞面向對象的方式進行。類的繼承特性是C++的一個非常重要的機制。繼承特性可以使一個新類獲得其父類的操作和數據結構,程序員只需在新類中增加原有類沒有的成分。
在面試過程中,各大企業會考量你對虛函數、純虛函數、私有繼承、多重繼承等知識點的掌握程度
1、以下代碼的輸出結果是什么?
#include<iostream>using namespace std;
class A
{
protected:
int m_data;
public:
A(int data = 0)
{
m_data = data;
}
int GetData()
{
return doGetData();
}
virtual int doGetData()
{
return m_data;
}
};
class B : public A
{
protected:
int m_data;
public:
B(int data = 1)
{
m_data = data;
}
int doGetData()
{
return m_data;
}
};
class C : public B
{
protected:
int m_data;
public:
C(int data = 2)
{
m_data = data;
}
};
int main ()
{
C c(10);
cout << c.GetData() <<endl;
C中未定義,故調用B中的,但是B中也未定義,故調用A中的GetData(),因為A中的doGetData()是虛函數,所以調用B類中的doGetData(),而B類的doGetData()返回B::m_data, 故輸出 1。
cout << c.A::GetData() <<endl;
因為A中的doGetData()是虛函數,所以調用B類中的doGetData(),而B類的doGetData()返回B::m_data,故輸出 1。
cout << c.B::GetData() <<endl;
肯定是B類的返回值 1 了。
cout << c.C::GetData() <<endl;
C類中未重定義GetData(),故調用從B繼承來的GetData(),但是B類也未定義,所以調用A中的GetData(),因為A中的doGetData()是虛函數,所以調用B類的doGetData(),股輸出為1
cout << c.doGetData() <<endl;
B類的返回值 1 了。
cout << c.A::doGetData() <<endl;
因為直接調用了A的doGetData() ,所以輸出0。
cout << c.B::doGetData() <<endl;
調用了B的doGetData(),所以輸出 1。
cout << c.C::doGetData() <<endl;
調用了B的doGetData(),所以輸出 1。
return 0;
}
總結:這里要注意存在一個就近調用,如果父類存在相關接口則優先調用父類接口,如果父類也不存在相關接口則調用祖父輩接口。
考點2:虛函數覆蓋虛函數
以下代碼輸出結果是什么?
#include<iostream>using namespace std;
class A
{
public:
void virtual f()
{
cout<<"A"<<endl;
}
};
class B : public A
{
public:
void virtual f()
{
cout<<"B"<<endl;
}
};
int main ()
{
A* pa=new A();
pa->f(); 這個很明顯A
B* pb=(B*)pa;
pb->f(); 這個強制將pa復制到pb,所以pb指向A
delete pa,pb; 刪除pa,pb所指向的地址,但是pa、pb指針并沒有刪除,懸浮指針
pa=new B();
pa->f(); B
pb=(B*)pa;
pb->f(); B
return 0;
}
考點1:公有繼承和私有繼承的區別
公有繼承(public)
公有繼承的特點是基類的公有成員和保護成員作為派生類的成員時,它們都保持原有的狀態,而基類的私有成員仍然是私有的,不能被這個派生類的子類所訪問。
私有繼承(private)
私有繼承的特點是基類的公有成員和保護成員都作為派生類的私有成員,并且不能被這個派生類的子類所訪問。(私有繼承使父類中的函數轉化為私有)
保護繼承(protected)
保護繼承的特點是基類的所有公有成員和保護成員都成為派生類的保護成員,并且只能被它的派生類成員函數或友元訪問,基類的私有成員仍然是私有的。
public protected private
共有繼承 public protected 不可見
私有繼承 private private 不可見
保護繼承 protected protected 不可見
考點2:保護繼承和私有繼承后,子類對象想訪問父類成員
公有繼承:子類對象可以直接訪問父類的public的成員
保護繼承:繼承之后的類相對于父類是獨立的,不能直接訪問父類成員,其類對象,在公共場合無法使用基類成員,也只能通過自己的成員函數來訪問父類的protected和public成員。
私有繼承:繼承之后也不能直接訪問父類成員,只能通過子類的成員函數來訪問父類的protected和public成員。
#include
class Animal
{
public:
Animal(){}
void eat(){cout << "eat
";}
};
class Giraffe:protected Animal
{
Giraffe(){}
void StrechNeck(double)
{cout << "strechneck
";}
void take()
{
eat(); //ok
}
};
void main()
{
Giraffe girl;
girl.eat(); 錯誤 保護繼承不能直接訪問父類成員
girl.take(); 正確 保護繼承只能通過子類的成員函數來訪問父類成員
girl.StretchNeck(); 正確 保護繼承只能通過子類的成員函數來訪問父類成員
}
考點3:派生類的三種繼承深入了解
#include <iostream>#include <stdio.h>
class Parent
{
public:
Parent(int var = -1)
{
m_nPub = var;
m_nPtd = var;
m_nPrt = var;
}
public:
int m_nPub;
protected:
int m_nPtd;
private:
int m_nPrt;
};
class Child1:public Parent
{
public:
int GetPub(){return m_nPub;};
int GetPtd(){return m_nPtd;};
int GetPrt(){return m_nPrt;}; 錯誤 父類私有變量不能被子類訪問
};
class Child2:protected Parent
{
public:
int GetPub(){return m_nPub;};
int GetPtd(){return m_nPtd;};
int GetPrt(){return m_nPrt;}; 錯誤 父類私有變量不能被子類訪問
};
class Child3:private Parent
{
public:
int GetPub(){return m_nPub;};
int GetPtd(){return m_nPtd;};
int GetPrt(){return m_nPrt;}; 錯誤 父類私有變量不能被子類訪問
};
int main()
{
Child1 cd1;
Child2 cd2;
Child3 cd3;
int nVar = 0;
//公有繼承
cd1.m_nPud = nVar; 正確公有繼承訪問并改變公有變量
cd1.m_nPtd = nVar; 錯誤公有繼承m_nPtd可以被繼承訪問但是不能被修改
nVar = cd1.GetPtd(); 正確公有繼承通過函數訪問父類的公有變量
//保護繼承
cd2.m_nPtd = nVar; 錯誤 保護繼承 保護繼承不能直接訪問父類的成員
nVar = cd2.GetPtd(); 正確 保護繼承 通過函數來訪問父類成員
//私有繼承
cd3.m_nPub = nVar; 錯誤 是有繼承 不能直接修改父類的公有變量
nVar = cd3.GetPtd(); 正確 可以通過函數訪問父類的保護變量
return 0;
}
考點1:理解虛方法(虛函數)
每個對象里有虛表指針,指向虛表,虛表里存放了虛函數的地址,虛函數表是順序存放虛函數地址的,不需要用到鏈表。所以類中的每一個對象都有一個鏈表來存虛方法地址,那就是虛表。
虛函數的實現要求對象攜帶額外的信息,這些信息用于在運行時確定后該對象應該調用哪一個虛函數,典型的情況下,這個信息具有一種被稱為vptr虛函數指針的指針形式,vptr指向一個被稱為vtbl的虛函數表函數指針數組,每一個虛函數都關聯到vtbl,當一個對象調用了虛函數,實際的被調用函數通過下面步驟確定,找到對象的vptr指向的vtbl,之后在vtbl中尋找合適的函數指針。
虛擬函數使用的缺點
優點講了一大堆,現在談一下缺點,虛函數最主要的缺點是執行效率較低,看一看虛擬函數引發的多態性的實現過程,你就能體會到其中的原因,另外就是由于要攜帶額外的信息(VPTR),所以導致類多占的內存空間也會比較大,對象也是一樣的
考點2:虛函數、虛函數表、虛函數指針的聯系
每一個具有虛函數的類都有一個虛函數表VTABLE,里面按在類中聲明的虛函數的順序存放著虛函數的地址,這個虛函數表VTABLE是這個類的所有對象所共有的,也就是說無論用戶聲明了多少個類對象,但是這個VTABLE虛函數表只有一個。
在每個具有虛函數的類的對象里面都有一個VPTR虛函數指針,這個指針指向VTABLE的首地址,每個類的對象都有這么一種指針。
考點3:虛函數的繼承
1)空類、單一繼承的空類、多重繼承的空類所占空間大小為:1(字節,下同);
2)一個類中,虛函數本身、成員函數(包括靜態與非靜態)和靜態數據成員都是不占用類對象的存儲空間的;
3)類對象的大小=各非靜態數據成員(包括父類的非靜態數據成員但都不包括所有的成員函數)的總和+ vfptr指針(多繼承下可能不止一個)+vbptr指針(多繼承下可能不止一個)+編譯器額外增加的字節。
4)當類中聲明了虛函數(不管是1個還是多個),那么在實例化對象時,編譯器會自動在對象里安插一個指針vPtr指向虛函數表VTable;
#include<iostream>#include<memory.h>#include<assert.h>
using namespace std;
class A
{
char k[3]; 所占的大小為3
public:
virtual void aa(){}; 虛指針大小為4
};
class B : public virtual A
{
char j[3];
public:
virtual void bb(){};
};
class C : public virtual B
{
char i[3];
public:
virtual void cc(){};
};
int main(int argc, char *argv[])
{
cout << "sizeof(A): " << sizeof(A) << endl; 大小為4(char)+4(虛表)=8
cout << "sizeof(B): " << sizeof(B) << endl; 大小為8(A副本)+4(char)+4(虛表)=16
cout << "sizeof(C): " << sizeof(C) << endl; 大小為16(B副本)+4(char)+4(虛表)=24
return 0;
}
考點3:什么是虛繼承?它和一般的繼承有什么不同?有什么用
虛擬繼承是多重繼承中特有的概念。虛擬基類是為了解決多重繼承而出現的,可以節省內存空間
請看下圖:
在圖 1中,類D接觸自類B和類C,而類B和類C都繼承自類A,因此出現了圖 2所示的情況。
在圖 2中,類D中會出現兩次A。為了節省內存空間,可以將B、C對A的繼承定義為虛擬繼承,而A成了虛擬基類。最后形成了圖 3。
代碼如下:
class A;
class B : public virtual A;
class C : public virtual A;
class D : public B,public C;
考點4:區分虛函數繼承和虛繼承
虛擬繼承是多重繼承中特有的概念,是為解決多重繼承的。用虛繼承可以節省內存空間
虛函數是面向對象多態性的主要方式,通過繼承基類中的虛函數在子類中重載實現不同操做。繼承的虛函數在子類中不需要加virtual,默認就是虛函數。可以被它的子類覆蓋。
考點4:區分虛繼承和直接繼承
#include <stdio.h>
class A {
public:
int a;
}; sizeof(A)=4
class B : virtual public A {
public:
int b; sizeof(B)=4(虛表)+4(A副本)+4(自己變量)=12
};
class C : virtual public B { sizeof(c)= 12(B副本)+4(虛表) = 16 如果這里改為直接繼承,那么sizeof(c)=12
};
int main() {
printf("%d
", sizeof(C));
return 0;
}
再舉一個例子:
#include <stdio.h>
class A {
public:
int a;
}; sizeof(A) = 4
class B : virtual public A {
}; sizeof(B) =4+4=8
class C : virtual public A { sizeof(C) =4+4=8
};
class D : public B, public C{ sizeof(D)=8+8-4=12 這里需要注意要減去4 因為B和C同時繼承A,屬于只需要保存一個A的副本就好了 sizeof(D)=4(A的副本)+4(B的虛表)+4(C的虛表)=12
};
int main() {
printf("%d
", sizeof(D));
return 0;
}
再舉一個例子:含有普通繼承
class A
{
};
class B
{
char ch;
virtual void func0() { }
};
class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};
class D: public A, public C
{
int d;
virtual void func() { }
virtual void func1() { }
};
class E: public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};
int main(void)
{
cout<<"A="<<sizeof(A)<<endl; result=1 空類所占空間的大小為1
cout<<"B="<<sizeof(B)<<endl; result=8 1+4 對其 8
cout<<"C="<<sizeof(C)<<endl; result=8 1+1+4 對其 8
cout<<"D="<<sizeof(D)<<endl; result=12 C的副本+D本身=12
cout<<"E="<<sizeof(E)<<endl; result=20 B的副本+C的副本+E本身=20
return 0;
}
這里需要區分一下:①不沒有繼承的時候,存在虛函數則需要加上虛指針,如果有多個也只需要加上一個,因為只有一個虛指針;②對于普通繼承,類D和類E中自己的虛函數,大小為0,因為他沒有虛表③對于虛繼承中,派生類中存在一個或多個虛函數的時候,它本身就有一個虛表,指向自己的虛表,所以要加4
再舉一個例子:含有虛繼承
class CommonBase
{
int co; 4
};
class Base1: virtual public CommonBase 4副本+4虛指針+4自身+4=16
{
public:
virtual void print1() { }
virtual void print2() { }
private:
int b1;
};
class Base2: virtual public CommonBase 同理16
{
public:
virtual void dump1() { }
virtual void dump2() { }
private:
int b2;
};
class Derived: public Base1, public Base2 16+16-4+4=32
{
public:
void print2() { }
void dump2() { }
private:
int d;
};
class Derived size(32):
+---
| +--- (base class Base1)
| | {vfptr}
| | {vbptr}
| | b1
| +---
| +--- (base class Base2)
| | {vfptr}
| | {vbptr}
| | b2
| +---
| d
+---
+--- (virtual base CommonBase)
| co
+---
再舉一個例子:
class A
{
public:
virtual void aa() { }
virtual void aa2() { }
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈