c++面試常見問題總結
近來在面試的過程,發現面試官在c++方面總是喜歡問及的一些相關問題總結,當時沒怎麼答出來,或者是答的不怎麼全面,故而查詢相關資料總結下。(後面實際工作會進行實時更新資訊)
<一>c++虛擬函式方面
虛擬函式(Virtual Function)是通過一張虛擬函式表(Virtual Table)來實現的。簡稱為V-Table。在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函式。這樣,在有虛擬函式的類的例項中這個表被分配在了這個例項的記憶體中,所以,當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得由為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。
那麼虛擬函式表的指標存放在哪裡呢?
上述已經描述過了,存放在具體的例項物件中,通過虛擬函式指標來操控虛擬函式表,在進行多型的時候,需要用到,根據具體的例項物件就能確定所要呼叫的時哪一個具體類的具體方法。都是通過虛擬函式表來完成相應的操作的。在虛擬函式表中存放的時具體例項類重寫的父類的虛擬函式方法地址。當呼叫時,根據具體的例項即可訪問到具體方法。綜上所述虛擬函式可以概括為以下幾點:
a>虛擬函式表是全域性共享元素,全域性就只有一個。
b>虛擬函式表好比是一個類似陣列的東西,在類物件中儲存vptr(虛擬函式指標),並指向虛擬函式表,因為其不屬於方法,也不屬於程式碼,所以不能存放在程式碼段。
c>虛擬函式表中儲存的是虛擬函式的方法的地址,其大小在編譯節點就已經確定好了,根據的繼承關係,就能確定好虛擬函式表的大小。所以不用動態的分配記憶體,不在堆中。
d>由於虛擬函式表是全域性共享的,類似於static變數一樣,儲存在全域性程式碼區域。
<二>有關c++類佔用記憶體的多少計算
在c++中一個空類所佔記憶體的大小為1位元組,例如:
class A{}; 計算其佔用記憶體大小:sizeof(A) = 1;為什麼一個空類的大小佔用1位元組呢?是因為類的例項化實質上就是在記憶體中分配一塊獨一無二地址,這樣保證了例項是唯一存在的。所以,給空類分配一個位元組,就相當於給例項分配了一個地址。如果不隱含包含一個位元組的話,就不能進行例項化。當該類作為基類被繼承的時候,系統會優化該類成為0位元組,這個被稱為空白基類最優化過程。
以下幾種型別的類佔用記憶體位元組的大小,在32位系統下:
class A{}; sizeof(A) = 1;該大小上述已經具體化介紹。
//只包含普通成員函式的類,成員函式不佔用記憶體空間
class B{
public:
B(){}
~B(){}
};
sizeof(B) = 1;
//包含普通成員變數的類,根據變數實際大小計算佔用記憶體大小
class C{
public:
C(){}
~C(){}
private:
int c;
};
sizeof(C) = 4 ;
//包含虛擬函式的類,包含虛擬函式指標,虛擬函式指標變數本身佔用記憶體大小
class D{
public:
D(){}
virtual ~D(){}
private:
int d;
};
sizeof(D) = 8;
//包含繼承關係的類
class E:public D{
public:
E(){}
~E(){}
private:
int e;
};
sizeof(E) = 8;
從以上A到E幾個例項來解釋該類所佔用記憶體大小的原因。
A類,是一個空類,因為空類也可以進行例項化,例項化就需要系統分配一塊唯一地址的記憶體,所有系統隱含新增一個位元組大小。
B類,雖然包含有建構函式和解構函式,但是在類中,成員函式是不佔用記憶體的,另外該類並無成員變數,所以佔用的記憶體大小和仍舊是1位元組。同樣是需要例項化所需要的。
C類,包含成員變數,系統給成員變數按照實際型別來分配具體記憶體大小的。例如,一個int型變數,佔用4位元組,所有sizeof(C) = 4.
D類,存在虛擬函式的類都有一個一維的虛擬函式表叫虛表,虛表裡存放的就是虛擬函式的地址了,因此,虛表是屬於類的。這樣的類物件的前四個位元組是一個指向虛表的指標,類內部必須得儲存這個虛表的起始指標。在32位的系統分配給虛表指標的大小為4個位元組,得到類D的大小為4,所以在算上成員變數d所佔用的位元組數,sizeof(D) = 8;
E類,繼承了D類,E類和D類共享一個虛擬函式指標,故而算上E類自身的一個成員變數e,sizeof(E) = 8;
綜上所述:
空類:佔用一個位元組大小,因為每一個類都需要例項化時分配一塊獨一無二的記憶體空間。
類內部:普通成員變數根據各自型別佔用相應的記憶體大小,但是static變數不佔用累記憶體大小,其存放在全域性區域。子類繼承父類,則將父類的成員變數計算進入子類佔用的大小。非虛的成員函式不佔用記憶體大小,但是虛擬函式,需要維護一張虛擬函式表存放相應的虛擬函式地址,所以虛擬函式指標在類內部,指標佔用相應的記憶體大小,並且繼承之後,父子類共享此虛指標。
<三>c++中的虛繼承的作用?
在c++中需要一個繼承是一個特性,通常使用的都是一些繼承虛擬函式,為了實現多型的過程。但是往往存在一種情況,為了提高程式碼的服用性,有一個基類,其自身有很多方法,是很多子類都能使用,所以往往就讓子類直接將該類繼承過來使用,避免了子類自己在實現一邊,這樣會造成大量的程式碼冗餘,維護也不方便。但是,如果大量的子類都繼承同一個基類,同樣也是有問題的,比如從不同途徑繼承來的同名的資料成員在記憶體中有不同的拷貝造成資料不一致問題,佔用記憶體大小。所以,使用虛繼承實現將共同基類設定為虛基類。這時從不同的路徑繼承過來的同名數據成員在記憶體中就只有一個拷貝,同一個函式名也只有一個對映。
虛繼承的原理?
虛繼承的原理過程是通過虛基類指標和虛基類表來實現,一個虛基類指標佔用四個位元組的大小,虛基類表不佔用類儲存空間大小,在虛基類表中儲存的時虛基類相對於派生類的偏移量,這樣就根據偏移量找到虛基類成員。如果虛繼承的類被繼承,該派生類同樣有一份虛積類指標的拷貝。這樣就能保證虛基類中在子類中存在一份拷貝。避免有多分拷貝造成二義性。
語法:
class 派生類: virtual 基類1,virtual 基類2,...,virtual 基類n
{
...//派生類成員宣告
};
如圖所示:
建構函式的和解構函式的執行順序
首先執行虛基類的建構函式,多個虛基類的建構函式按照被繼承的順序構造;
執行基類的建構函式,多個基類的建構函式按照被繼承的順序構造;
執行成員物件的建構函式,多個成員物件的建構函式按照申明的順序構造;
執行派生類自己的建構函式;
析構以與構造相反的順序執行;
注:
從虛基類直接或間接派生的派生類中的建構函式的成員初始化列表中都要列出對虛基類建構函式的呼叫。但只有用於建立物件的最派生類的建構函式呼叫虛基類的建構函式,而該派生類的所有基類中列出的對虛基類的建構函式的呼叫在執行中被忽略,從而保證對虛基類子物件只初始化一次。
在一個成員初始化列表中同時出現對虛基類和非虛基類建構函式的呼叫時,虛基類的建構函式先於非虛基類的建構函式執行。
例如:如下程式碼所示:
一個派生類繼承多個基類,多個基類中包含重複名稱的方法。(普通繼承)
class A{
public:
A(){cout<<"A is been called"<<endl;}
void fun(){cout<<"A fun been called"<<endl;}
};
class B{
public:
B(){cout<<"B is been called"<<endl;}
void fun(){cout<<"B fun been called"<<endl;}
};
class C:public A,public B{
public:
C(){cout<<"C is been called"<<endl;}
};
此種方法會導致同名方法存在多分拷貝,導致呼叫時會產生二義性,只能使用該種方法呼叫,如下:
int main(void){
C c;
//c.fun()錯誤,會產生二義性
c.A::fun();
c.B::fun();
return 0;
}
2,繼承一個多層的類
class F{
public:
F(){cout<<"F been called"<<end;}
void gun(){cout<<"F gun been called"<<endl;}
};
class S1:public F{
public:
S1(){cout<<"S1 been called"<<endl;}
};
class S2:public F{
public:
S2(){cout<<"S2 been called"<<endl;}
};
class Son:public S1,public S2{
public:
Son(){cout<<"Son been called"<<endl;}
};
int main(){
Son son;
//son.gun();有二義性,因為該方法在S1和S2中各有一份備份
son.S1::gun();
son.S2::gun();
return 0;
}
虛繼承就避免了這樣的二義性,也節省了空間
(1)
class F{
public:
F(){cout<<"F been called"<<end;}
void gun(){cout<<"F gun been called"<<endl;}
};
class S1:public virtual F{
public:
S1(){cout<<"S1 been called"<<endl;}
};
class S2:public virtual F{
public:
S2(){cout<<"S2 been called"<<endl;}
};
class Son:public S1,public S2{
public:
Son(){cout<<"Son been called"<<endl;}
};
int main(){
Son son;
son.gun();//也可使用先前的方法呼叫
return 0;
}
<四>為什麼建構函式不能寫成虛擬函式,解構函式需要寫成虛擬函式?以及什麼情況下,子類的析構不會被呼叫?
因為虛擬函式的是通過虛擬函式指標操控虛擬函式表來實現的,且虛擬函式指標存放在例項物件的頭部位置,在建立一個例項物件時,需要呼叫對應的建構函式,此刻物件還未生成,是不能呼叫虛擬函式的,故而建構函式不能為虛擬函式。
對於解構函式,是例項物件將要釋放資源,需要呼叫呼叫解構函式,在繼承關係中,為了防止帶釋放物件的時候呼叫解構函式,只調用了基類的析構而沒有對派生類的構造進行調動,所以,將基類的解構函式記性虛化,從而保證基類和派生類的解構函式都被呼叫,確保釋放其所佔用的資源。
當基類指標值想子類物件,但是子類解構函式不是虛擬函式,當呼叫解構函式的時候,子類的解構函式是不會被呼叫的。
<五>volatile是作用
訪問暫存器要比訪問記憶體要塊,因此CPU會優先訪問該資料在暫存器中的儲存結果,但是記憶體中的資料可能已經發生了改變,而暫存器中還保留著原來的結果。為了避免這種情況的發生將該變數宣告為volatile,告訴CPU每次都從記憶體去讀取資料。
注:一個引數可以即是const又是volatile的嗎?可以,一個例子是隻讀狀態暫存器,是volatile是因為它可能被意想不到的被改變,是const告訴程式不應該試圖去修改他。
<六>解構函式能丟擲異常嗎
答案肯定是不能。
C++標準指明解構函式不能、也不應該丟擲異常。C++異常處理模型最大的特點和優勢就是對C++中的面向物件提供了最強大的無縫支援。那麼如果物件在執行期間出現了異常,C++異常處理模型有責任清除那些由於出現異常所導致的已經失效了的物件(也即物件超出了它原來的作用域),並釋放物件原來所分配的資源, 這就是呼叫這些物件的解構函式來完成釋放資源的任務,所以從這個意義上說,解構函式已經變成了異常處理的一部分
<七>避免在基類的建構函式和解構函式中呼叫虛擬函式?
例如:
class A{
public:
A(){
fun();
}
virtual void fun(){
cout<<"Afun"<<endl;
}
};
class B:public A{
public:
virtual void fun(){
cout<<"Bfun"<<endl;
}
};
int main(){
B b;
return 0;
}
如上所示,的呼叫結果是輸出Afun,這是因為在建立物件b的時候,因為B繼承A,在進行建構函式的呼叫的時候,優先呼叫基類的建構函式,此時的派生類物件尚未完成初始化,此刻虛擬函式指標還未完成初始化,不能夠去檢索對應的虛擬函式表,所以此時進行構造呼叫的時候為基類的方法。
建構函式的呼叫順序是從基類到派生類,逐層構造。在構造的過程中,vptr被指向本層的vtable。而虛擬函式的行為依賴於vptr。因此,在本層建構函式中,編譯器無法獲知派生類的任何資訊,因此無法形成正確的vtable訪問。
<.八>c++中const的用法介紹
在c++模型中,const關鍵字通常使用的放變數物件意外的改變的功能,在c++中const可以用來修飾的物件有變數,函式返回值,函式引數,以及函式本身,下面介逐一介紹其方式;
const修飾變數:通常是用來定義個常量的屬性,表示在白變數在使用範圍內是一個常量,不能進行修改的,如果強制修改的話,會出現錯誤。例如:const int a = 10;
const修飾函式引數:在函式的引數傳遞中有三種類型的傳遞方式,值傳遞,指標傳遞,引用傳遞,只有值傳遞的時候會出現臨時變數的產生,其餘兩種的傳遞都是相當於呼叫所傳的物件的本身。在這三種傳遞方式中,需要注意的是,在和const結合使用的時候的一些注意事項:
函式引數為傳入引數,不管是指標傳遞和引用傳遞,為了防止意外更改該引數,加上const修飾,可以起到保護作用。例如:void fun(const int *p) or void fun(const int & x).
函式引數為傳出引數,此時的引數為輸出引數,不能使用const進行修飾,否則,此刻該引數將失去輸出引數的功能。例如:void fun( char* in,char * out ) or void fun(char* in,char* & out).
const修飾函式的返回值:
當函式的返回值為指標,使用const修飾,表的是函式的返回的指標指向的內容是不可變,但是指標本身的指向是可以改變,並且,返回的指標必須使用一個對應的const修飾的指標進行接收,
例如:const char* GetString(), const char* ptr = GetString()
當函式的返回值為值傳遞的方式,使用const沒有價值的,因為返回的是一個臨時物件。
如果函式返回值是引用,需要格外注意的是,根據實際情況來區分是要獲取該物件的一份拷貝,還是該物件的一個別名使用。要根據實際情況做判斷。通常引數返回引用使用使用在類賦值函式中使用,用於鏈式表示式的呼叫,a=b=c;具體可參考string類的賦值函式。
const修飾函式:
再類中,任何不會修改成員的函式都應該白定義成const成員函式,如果在const成員函式中修改了成員,會出現錯誤。通過此種類型提高程式的健壯性。有關const成員函式的幾個特性:
const物件只能訪問const成員函式,非const物件兩者都可訪問;
const物件的成員是不可修改,但是通過指標維護的物件是可以修改的
const成員函式不可以修改物件成員,不管物件是否具有const屬性,在編譯是以是否修改成員為依據,進行檢查
如果一個成員被mutable修飾,那個任何方式都可以修改該成員,即便是const成員函式,也可以修改他
例如:
#include <iostream>
#include <string.h>
using namespace std;
//const修飾函式的返回值
const char* getString(char* str){
char *p = str;
return p;
}
void GetInfo(const char* src, char* dst){
memcpy(dst,src,strlen(src));
}
class A{
public:
A(int a,int b,int c):a(a),b(b),c(c){
}
~A(){}
//非const方法,都可以修改
void fun(int a,int b,int c){
cout<<"修改之前"<<endl;
cout<<"this->a "<<this->a<<endl;
cout<<"this->b "<<this->b<<endl;
cout<<"this->c "<<this->c<<endl;
//this->a = a; //const成員不可更改
this->b = b;
this->c = c;
cout<<"修改之後"<<endl;
cout<<"this->a "<<this->a<<endl;
cout<<"this->b "<<this->b<<endl;
cout<<"this->c "<<this->c<<endl;
}
//const成員
void gun(int a,int b,int c) const{
cout<<"修改之前"<<endl;
cout<<"this->a "<<this->a<<endl;
cout<<"this->b "<<this->b<<endl;
cout<<"this->c "<<this->c<<endl;
//cosnt成員不可以修改成員
//this->a = a; //本身是cosnt成員,不能修改
//this->b = b; //普通成員,但是在const函式中不可修改
this->c = c; //使用了mutable修飾,任何地方丟可修改
cout<<"修改之後"<<endl;
cout<<"this->a "<<this->a<<endl;
cout<<"this->b "<<this->b<<endl;
cout<<"this->c "<<this->c<<endl;
}
private:
const int a;
int b;
mutable int c;
};
int main(int argc, char const *argv[]) {
//此刻返回的指標的內容是不可更改的
const char* p = getString("liux");
cout<<"p = "<<p<<endl;
char c[12] = {"0"};
char *dst = c;
GetInfo(p,dst);
cout<<"dst = "<<dst<<endl;
A* a = new A(1,2,3);
a->fun(7,8,9);
a->gun(10,11,12);
const A *a1= new A(11,22,33);
//a1->fun(0,0,0);//const物件只能訪問const成員
a1->gun(21,31,41);
delete a;
return 0;
}
<九>static關鍵字修飾總結
static關鍵字修飾變數,表示該變數為一個靜態變數,並且全域性共享(在全域性作用於中定義,並且變數作用範圍僅限於定義改變數的檔案中)
static 關鍵字在函式體中修飾變數,表示該變數只進行一次初始化,之後每次呼叫進來該函式,該變數的值仍舊是上一次呼叫時的值,如果更能該了個變數的值,就保持了該值,擁有一種持久化儲存的性質。而函式體中普通的變數生命週期在函式返回時會被銷燬,之後從新呼叫的時候才會重新定義。
static在類中修飾資料成員,表示該變數不屬類中的某個例項,所有的例項共享此變數,改變數需要在類外進行初始化,實際使用中,儘量避免在.h檔案初始化該變數,容易造成重複定義。除此之外,類的靜態變數可以被類的const成員方法合法更改,見如下程式碼演示:
static 修飾的類成員方法的地址,可以直接使用普通的函式指標來儲存,而普通的成員方法的地址必須使用類成員函式指標來儲存,詳情將如下程式碼所示:
注:static修飾的函式在記憶體中只有一份,而普通函式在每個呼叫者中都維持一份拷貝。
以上幾種總結方式示例程式碼如下:
#include <iostream>
using namespace std;
class A{
public:
A(){}
~A(){}
void gun(int m){
cout<<"gun修改前: "<<val<<endl;
val = m;
cout<<"gun修改前: "<<val<<endl;
}
//const成員函式可以更改靜態成員變數的值
void fun(int m) const{
cout<<"fun修改前: "<<val<<endl;
val = m;
cout<<"fun修改前: "<<val<<endl;
}
//靜態成員可以作為靜態成員方法的預設引數,普通方法不允許
static void hun(int m = val){
cout<<"hun() = "<<m<<endl;
/*
靜態方法只能訪問靜態成員,不能訪問非靜態成員,否則會報錯,如下
cout<<" hun() nal = "<<nal<<endl;
*/
}
//靜態方法和普通方法的函式指標
static void s_fun(){ cout<<"++++++++++++++"<<endl;}
void f_fun(){cout<<"******************"<<endl;}
//函式內使用static修飾變數讓其持久化
void last(){
static int lt = 888;
cout<<"lt value is "<<lt--<<endl;
}
private:
static int val;
int nal;
};
//在類外初始化靜態成員變數,注儘量在.h檔案中初始化靜態成員變數,不然很容易引起衝定義的錯誤
//該出是為了實現方便,都在.cpp檔案中
int A::val = 100;
int main(){
A* a = new A();
//通過const成員變數修改靜態成員變數
//a->fun(222);
//使用靜態成員變數作為靜態方法的引數
a->hun();
/**
靜態成員函式的地址可以使用普通的函式指標來儲存,而普通成員函式的地址
必須使用成員函式指標來儲存,如下所示:
**/
void (*s_ptr)() = &A::s_fun;
void (A::*f_ptr)() = &A::f_fun;
f_ptr = &A::f_fun;
s_ptr(); //通過普通的函式指標可以直接呼叫類中的靜態方法
(a->*f_ptr)(); //通過類成員函式指標呼叫類中的成員方法
//============================================
/*
以下寫法錯誤,涉及到運算子的優先順序,如下幾種寫法,因括號優先順序最高,編譯是報錯,正確寫法附上所示
a->*f_ptr(); or (a->*f_ptr()());報錯:must use ‘.*’ or ‘->*’ to call pointer-to-member function in ‘f_ptr (...)’, e.g. ‘(... ->* f_ptr) (...)’
a->(*f_ptr)();報錯:invalid use of unary ‘*’ on pointer to member
*/
//============================================
//函式內使用static修飾變數持久化
int i;
for(i = 0;i < 5;++i){
a->last();
}
return 0;
}
<十>explcit關鍵字
該關鍵字用來修飾單參或者除了第一個引數其餘都是取勝引數的建構函式,避免建構函式進行隱式轉化。
例如:
class A{
public:
explcit A(int i){n = i;}
void print(){cout<<"n = "<<n<<endl;}
private:
int n;
};
int main(){
A a(1);
a.print();
//此時因為建構函式被explcit修飾,不能隱士轉化,如下不能使用此方法呼叫,否則會報錯。
//A b = 2;
// b.print();
return 0;
}