C++面試題目彙總(一)

光看C++ primer plus老記不住,所以找些面試題,來進行鞏固強化。

1、什麼是虛函數?什麼是純虛函數?

答:

虛函數聲明如下: virtual ReturnType FunctionName(Parameter);引入虛函數是爲了動態綁定。 
純虛函數聲明如下:virtual ReturnType FunctionName()= 0;引入純虛函數是爲了派生接口

2、基類爲什麼需要虛析構函數?

答:

標準規定:當derived class經由一個base class指針被刪除而該base class的析構函數爲non-virtual時,將發生未定義行爲。通常將發生資源泄漏。 
解決方法即爲:爲多態基類聲明一個virtual 析構函數

3.、++i和i++哪個更快一點?i++和++i的區別是什麼?

答:

理論上++i更快,實際與編譯器優化有關,通常幾乎無差別。 
i++實現的代碼爲:

//i++實現代碼爲:                                    
int operator++(int)                                  
{
    int temp = *this;                                     
    ++*this;                                             
    return temp;                                    
}//返回一個int型的對象本身

++i的實現代碼:

// ++i實現代碼爲:
int& operator++()
{
    *this += 1;
    return *this;
}//返回一個int型的對象引用

i++和++i的考點比較多,簡單來說,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一個確定的值,是一個可修改的左值,如下使用:

    cout << ++(++(++i)) << endl;
    cout << ++ ++i << endl;

可以不停的嵌套++i。 
這裏有很多的經典筆試題,一起來觀摩下:

int main()
{

    int i = 1;
    printf("%d,%d\n", ++i, ++i);    //3,3
    printf("%d,%d\n", ++i, i++);    //5,3
    printf("%d,%d\n", i++, i++);    //6,5
    printf("%d,%d\n", i++, ++i);    //8,9
    system("pause");
    return 0;
}

首先是函數的入棧順序從右向左入棧的,計算順序也是從右往左計算的,不過都是計算完以後在進行的壓棧操作: 
對於第5行代碼,首先執行++i,返回值是i,這時i的值是2,再次執行++i,返回值是i,得到i=3,將i壓入棧中,此時i爲3,也就是壓入3,3; 
對於第6行代碼,首先執行i++,返回值是原來的i,也就是3,再執行++i,返回值是i,依次將3,5壓入棧中得到輸出結果 
對於第7行代碼,首先執行i++,返回值是5,再執行i++返回值是6,依次將5,6壓入棧中得到輸出結果 
對於第8行代碼,首先執行++i,返回i,此時i爲8,再執行i++,返回值是8,此時i爲9,依次將i,8也就是9,8壓入棧中,得到輸出結果。 
上面的分析也是基於vs搞的,不過準確來說函數多個參數的計算順序是未定義的(the order of evaluation of function arguments are undefined)。筆試題目的運行結果隨不同的編譯器而異。 
參考:http://www.stroustrup.com/bs_faq2.html#evaluation-order 

4.、vector的reserve和capacity的區別?

答:

reserve()用於讓容器預留空間,避免再次內存分配;capacity() 返回在重新進行內存分配以前所能容納的元素數量。

5、在模板中,如何聲明嵌套從屬類型(即模板嵌套類型)?

答:

template內出現的類型如果依賴於某個template參數,則稱之爲從屬類型;如果從屬類型在class內呈嵌套狀,則稱之爲嵌套從屬類型。

template<typename C>
void doSomething(const C& container)
{
    if(container.size() > 0)
        C::iterator iter(container.begin());
}

此時,根據C++的規則,編譯器先假設C::iterator不是一個類型。然而iter的聲明只有在C::iterator是一個類型時才合理。因此需要我們自己告訴編譯器。 
那麼,就需要再C::iterator之前加上typename,告訴編譯器C::iterator是一個類型。

template<typename C>
void doSomething(const C& container)
{
    if(container.size() > 0)
        typename C::iterator iter(container.begin());
}

6、 auto_ptr能作爲vector的元素嗎?爲什麼?

答:

不可以。 
當複製一個auto_ptr時,它所指向的對象的所有權被交到複製的auto_ptr上面,而它自身將被設置爲null。複製一個auto_ptr意味着改變它的值。

7、如何初始化const和static數據成員?

答:

通常在類外初始化static數據成員,但是 static const 的整型(bool,char,int,long)可以在類聲明中初始化, 
static const的其他類型也必須在類外初始化(包括整型的數組)。

8、如何確保對象在拋出異常時也能被刪除?什麼是RAII?

答:

總的思想是RAII:設計一個class,令他的構造函數和析構函數分別獲取和釋放資源。 
有兩個方法:1、利用“函數的局部對象無論函數以何種方式(包括因異常)結束都會被析構”這一特性,將“一定要釋放的資源”放進局部對象的析構函數; 
2、使用智能指針。

9、 爲什麼需要private繼承?

答:在這裏把各種繼承都說一下: 
補充:不同繼承描述符下,基類的各種修飾符下的成員轉換爲子類成員的修飾符示意: 
參考:《TC++PL》 p363
1、public:只繼承基類的接口。當繼承是接口的一部分時,就選用public繼承。 
2、private:只繼承基類的實現。當繼承是實現細節時,就選用private繼承。 
3、protected:當繼承是面向派生類而不是面向用戶接口中的一部分時,就選用protected繼承。 
private意味着”根據某物實現出“的語義。和複合擁有同樣的語義。

10、如何實現單例模式?如何避免發生對象的用戶複製行爲?如何實現線程安全的單例模式?DCLP是什麼,有什麼問題?

答: 
(1)、將構造函數、析構函數、複製構造函數、賦值操作符聲明爲私有,即可實現單例模式 
單例模式實現代碼通常爲:

class Singleton
{
public:
    static Singleton* Instance();
protected:
    Singleton();
private:
    static Singleton* _instance;
};
Singleton::Singleton(){}
Singleton* Singleton::_instance = nullptr;
Singleton* Singleton::Instance()
{
    if(_instance == nullptr)
        _instance = new Singleton;
    return _instance;
}

(2)、避免用戶的複製行爲,可以將複製構造函數聲明爲private或者使用C++11中的delete語法。 
(3)、實現線程安全的單例模式:上面實現中的GetInstance()不是線程安全的,因爲在單例的靜態初始化中存在競爭條件。如果碰巧有多個線程在同時調用該方法,那麼就有可能被構造多次。 
比較簡單的做法是在存在競爭條件的地方加上互斥鎖。這樣做的代價是開銷比較高。因爲每次方法調用時都需要加鎖。 
比較常用的做法是使用雙重檢查鎖定模式(DCLP)。但是DCLP並不能保證在所有編譯器和處理器內存模型下都能正常工作。如,共享內存的對稱多處理器通常突發式提交內存寫操作,這會造成不同線程的寫操作重新排序。這種情況通常可以採用volatile解決,他能將讀寫操作同步到易變數據中,但這樣在多線程環境下仍舊存在問題。

11、相等和等價的區別?哪些類型的容器使用相等或等價?

答:

相等(equality)是以operator==爲基礎,如果x==y爲真,則判定x和y相等。 
等價(equavalence)是以operator<爲基礎,如果!(x < y) && !(y < x)爲真,則判定x和y等價。 
通常,關聯容器採用“等價”,而順序容器採用“相等”。

12、如何實現仿函數?爲什麼需要通過繼承自unary_function 或者 binary_function來實現仿函數?

答:

function object就是重載了函數調用操作符 operator()的一個struct或者class 
所有內置一元仿函數均繼承自unary_function,所有內置二元仿函數均繼承自binary_function 
繼承自unary_function和binary_function的仿函數可以成爲“可配接“的仿函數。可配接的仿函數,能夠與其他STL組件更”和諧“地協同工作。

13、如果在構造函數和析構函數中拋出異常會發生什麼?什麼是棧展開?

答:

(1)、構造函數拋異常:不會發生資源泄漏。假設在operator new()時拋出異常,那麼將會因異常而結束此次調用,內存分配失敗,不可能存在內存泄露。假設在別處(operator new() )執行之後拋出異常,此時析構函數調用,已構造的對象將得以正確釋放,且自動調用operator delete()釋放內存 
析構函數拋異常: 
可以拋出異常,但該異常必須留在析構函數;若析構函數因異常退出,情況會很糟糕(all kinds of bad things are likely to happen) 
a、可能使得已分配的對象未能正常析構,造成內存泄露; 
b、例如在對像數組的析構時,如果對象的析構函數拋出異常,釋放代碼將引發未定義行爲。考慮一個對象數組的中間部分在析構時拋出異常,它無法傳播,因爲傳播的話將使得後續部分不能正常釋放;它也無法吸收,因爲這違反了”異常中立“原則(異常中立,就是指任何底層的異常都會拋出到上層,也就相當於是異常透明的)。 
(2)、拋出異常時,將暫停當前函數的執行,開始查找匹配的catch子句。首先檢查throw本身是否在try塊內部如果是,檢查與該try相關的catch子句,看是否可以處理該異常。如果不能處理,就退出當前函數,並且釋放當前函數的內存並銷燬局部對象,繼續到上層的調用函數中查找,直到找到一個可以處理該異常的catch。

14、如何在const成員函數中賦值?

答:

使用mutable去掉const的成員函數的const性質 
const_cast和mutable的比較 
const_cast: 
1) 強制去掉對象的const屬性。 
2) 缺點:對const對象,調用包含const_cast的const成員函數,屬於未定義行爲。 
mutable: 
1) 使用場景:對可能要發生變化的成員前,加上存儲描述符mutable。 
2) 實質:對加了mutable的成員,無視所有const聲明。 
爲什麼要有這種去除常量標誌的需求? 
答:兩個概念:物理常量性和邏輯常量性 
物理常量性:實際上就是常量。 
邏輯常量性:對用戶而言是常量,但在用戶不能訪問的細節上不是常量。 
參考:《TC++PL》 p206

15、兩種常用的實現隱式類類型轉換的方式是什麼?如何避免隱式類型轉換?

答:(1) 
a、使用單參數的構造函數或N個參數中有N-1個是默認參數的構造函數,如:

class A
{
public:
      A(stirng s);
      A(string s,int a = 0);
};

b、使用operator what_you_want_to_convert_type() const

 class A
        {
        public:
                operator char*() const
                {
                    return data;//當從其他類型轉換到char*時自動調用
                }
        private:
                char* data;
        };


(2)、在單參數的構造函數或N個參數中有N-1個是默認參數的構造函數聲明之前加上explicit。

16、STL中的vector:增減元素對迭代器的影響

答:這個問題主要是針對連續內存容器和非連續內存容器。 
a、對於連續內存容器,如vector、deque等,增減元素均會使得當前之後的所有迭代器失效。因此,以刪除元素爲例:由於erase()總是指向被刪除元素的下一個元素的有效迭代器,因此,可以利用該連續內存容器的成員erase()函數的返回值。常見的編程寫法爲:

    for(auto iter = myvec.begin(); iter != myvec.end())  //另外注意這裏用 "!=" 而非 "<"
    {
        if(delete iter)
            iter = myvec.erase(iter);
        else ++iter;
    }

還有兩種極端的情況是: 
(1)、vector插入元素時位置過於靠前,導致需要後移的元素太多,因此vector增加元素建議使用push_back而非insert; 
(2)、當增加元素後整個vector的大小超過了預設,這時會導致vector重新分分配內存,效率極低。因此習慣的編程方法爲:在聲明瞭一個vector後, 立即調用reserve函數,令vector可以動態擴容。通常vector是按照之前大小的2倍來增長的。

 
b、對於非連續內存容器,如set、map等。增減元素只會使得當前迭代器無效。仍以刪除元素爲例,由於刪除元素後,erase()返回的迭代器 將是無效的迭代器。因此,需要在調用erase()之前,就使得迭代器指向刪除元素的下一個元素。常見的編程寫法爲:

    for(auto iter = myset.begin(); iter != myset.end())  //另外注意這裏用 "!=" 而非 "<"
    {
        if(delete iter)
            myset.erase(iter++);  //使用一個後置自增就OK了
        else ++iter;
    }

其實在C++11中erase的返回值就是下一個節點,也可以利用函數的返回值。

17、New和malloc的區別

答:這個問題答案過於複雜,建議直接百度這個問題,網上的說法已經足夠完善。 
不過針對網上的一些說法補充並且糾正一下。網上在回答這個問題的時候沒有對new操作符進行深入的解釋,在這裏大致說一下。 
new可分爲operator new(new 操作)、new operator(new 操作符)和placement new(定位 new)。其中operator new執行和malloc相同的任務,即分配內存,但對構造函數一無所知;而 new operator則調用operator new,分配內存後再調用對象構造函數進行對象的構造。 
其中operator new是可以重載的。placement new,就是operator new的一個重載版本,允許你在一個已經分配好的內存中構造一個新的對象。而網上對new說法,大多針對operator new而言,因此說new是帶有類型的(以爲調用了類的構造函數),不過如果直接就說new是帶有類型的話,明顯是不合適的,比如原生的operator new。可以參考我的一個程序,這個程序是用代理模式實現一個自定義二維數組,在第二個維度拷貝構造的時候, 拷貝構造需要深拷貝(當然第一個維度也需要),執行深拷貝時代碼大致如下:

    class Array2D    //二維數組模板
    {
    private:
            size_t length2,length1; //數組各個維的大小
            Array1D<T>* data;
    }
    void* raw  =::operator new[](length2*sizeof(Array1D<T>));
        data = static_cast<Array1D<T>*>(raw);

可見執行operator new的時候申請的原生內存是可以不帶有類型的。 
1) malloc()分配指定字節數的未經初始化的內存空間,返回的是void指針;new操作符爲一個指定類型的對象分配空能,並調用其構造函數初始化,返回的是該對象的指針。 
2) malloc()必須要做初始化,以及將void指針轉換成合適類型的指針。同時要考慮到分配的內存大小正好是你所需要的大小。當new操作符使用”(value)” notation,即可得到值爲value的初始化。如果考慮上初始化的開銷,malloc()和new沒有性能上的差別。

18、C++如何避免內存泄漏

答:這其實可以看做是一個編程風格的問題。 
a、使用RAII(Resource Acquisition Is Initialization,資源獲取即初始化)技法,以構造函數獲取資源(內存),析構函數釋放。 
b、相比於使用原生指針,更建議使用智能指針,尤其是C++11標準化後的智能指針。 
c、注意delete和delete[]的使用方法。 
d、這是很複雜的一種情況,是關於類的copy constructor的。首先先介紹一些概念。 
同default constructor一樣,標準保證,如果類作者沒有爲class聲明一個copy constructor,那麼編譯器會在需要的時候產生出來(這也是一個常考點:問道”如果類作者未定義出default/copy constructor,編譯器會自動產生一個嗎?”答案是否定的) 
不過請注意!!這裏編譯器即使產生出來,也是爲滿足它的需求,而非類作者的需求!! 
而什麼時候是編譯器”需要”的時候呢?是在當這個class 【不表現出】bitwise copy semantics(位逐次拷貝,即淺拷貝)的時候。 
在4中情況下class【不表現出】bitwise copy semantics 
(1)、當class內含一個member object且該member object聲明瞭一個copy constructor(無論該copy ctor是類作者自己生明的還是編譯器合成的); 
(2)、當class繼承自一個base class且該base class有一個copy constructor(無論該copy ctor是類作者自己生明的還是編譯器合成的); 
(3)、當class聲明瞭virtual function; 
(4)、當class派生自一個繼承鏈,且該鏈中存在virtual base class時。 
言歸正傳,如果class中僅僅是一些普通資源,那麼default memberwise copy是完全夠用的;然而,擋在該class中存在了一塊動態分配的內存,並且在之後執行了bitwise copy semantics後,將會有一個按位拷貝的對象和原來class中的某個成員指向同一塊heap空間,當執行它們的析構函數後,該內存將被釋放兩次,這是未定義的行爲。因此,在必要的時候需要使用user-defined explicit copy constructor,來避免內存泄露。

19、STL中排序算法的實現是什麼

答:STL中的sort(),在數據量大時,採用quicksort,分段遞歸排序;一旦分段後的數量小於某個門限值,改用Insertion sort,避免quicksort深度遞歸帶來的過大的額外負擔,如果遞歸層次過深,還會改用heapsort。

20、指針和引用的區別

本質:指針是一個變量,存儲內容是一個地址,指向內存的一個存儲單元。而引用是原變量的一個別名,實質上和原變量是一個東西,是某塊內存的別名。 
指針的值可以爲空,且非const指針可以被重新賦值以指向另一個不同的對象。而引用的值不能爲空,並且引用在定義的時候必須初始化,一旦初始化,就和原變量“綁定”,不能更改這個綁定關係。 
不過如下的寫法也是可以通過編譯器的:

    int * iptr = NULL;
    int & iref = *iptr;

但是,上面的寫法是非人類的。 
對指針執行sizeof()操作得到的是指針本身的大小(32位系統爲4,64位系統爲8)。而對引用執行sizeof()操作,由於引用本身只是一個被引用的別名,所以得到的是所綁定的對象的所佔內存大小。 
指針的自增(++)運算表示對地址的自增,自增大小要看所指向單元的類型。而引用的自增(++)運算表示對值的自增。 
在作爲函數參數進行傳遞時的區別:指針作爲函數傳輸作爲傳遞時,函數內部的指針形參是指針實參的一個副本,改變指針形參並不能改變指針實參的值,通過解引用*運算符來更改指針所指向的內存單元裏的數據。而引用在作爲函數參數進行傳遞時,實質上傳遞的是實參本身,即傳遞進來的不是實參的一個拷貝,因此對形參的修改其實是對實參的修改,所以在用引用進行參數傳遞時,不僅節約時間,而且可以節約空間。 
上面是引用和指針的區別,總的來說,如果你需要一個可能會爲空,或者還會指向別的值的時候,就使用指針,如果是要一開始就要指向一個object,並不會改變的時候就可以只用引用。

21、指針數組和數組指針的區別

顧名思義,數組指針應該是指向數組的指針,而指針數組則是指該數組的元素均爲指針。 

數組指針,是指向數組的指針,其本質爲指針,形如int (*p)[10],p即爲指向數組的指針。

數組指針是指向數組首元素地址的指針,其本質爲指針,可以看成是二級指針。

指針數組,在C語言和C++中,數組元素全爲指針的數組稱爲指針數組,其中一維指針數組的定義形式爲: 
類型名 *數組標識符[數組長度] 
指針數組中每一個元素均爲指針,其本質爲數組,例如我們經常使用的動態數組的就是基於此的使用,如下示例:

size_t row,col;
//輸入row和col的數值
int **MathTable = new int*[row];
for( int i = 0 ; i < row ; i++ )
    MathTable[i] = new int[col];
//code
for( int i = 0 ; i < row ; i++ )
    delete [] MathTable[i];
delete []MathTable;

也就是形如int p[10]這樣的聲明,就是我們這裏的指針數組,從聲明形態上來講,是由於[]的優先級高於,又有諸如下面的指針:

*ptr_arry[i]


指針數組中的元素可以表示爲:

*(*(ptr_arry+i))


()的優先級較高,又由於又結合的原因,可以化簡爲:

**(ptr_arry+i)


由於數組元素均爲指針,因此prt_array[i]是指第i+1個元素的指針。

22、指針函數和函數指針

此處還有兩個需要區分的概念,就是函數指針和指針函數。 
 
函數指針:指向函數的指針變量,在C編譯時,每一個函數都有一個入口地址,那麼指向這個函數的函數指針便是指向這個地址。函數指針主要有兩個作用:用作調用函數和做函數的參數。 
int (*func)(int x); 
諸如上面的代碼這是申明瞭一個函數指針,代碼(*func)中括號是必須的,這會告訴編譯器這是一個函數指針而不是聲明一個具有返回類型爲指針的函數,後面的形參要是這個函數所指向的函數形參而定。使用如下面的代碼:

#include <iostream>

using namespace std;

int(*func)(int a, int b);
int bar(int a, int b)
{
    return a + b;
}

int foo(int a, int b)
{
    return a;
}
int _tmain(int argc, _TCHAR* argv[])
{
    func = bar;
    cout << func(12, 34) << endl;
    system("pause");
    func = foo;
    cout << func(12, 34) << endl;
    system("pause");
    return 0;
}


函數指針的另一個作用就是作爲函數的參數,可以在一個函數的形參列表中傳入函數指針,然後邊可以在這個函數中使用這個函數指針所指向的函數,這樣邊可以使程序變得更加清晰和簡潔。

#include <iostream>

using namespace std;

typedef int(*PF)(int, int);
//int(*func)(int a, int b);
int bar(int a, int b)
{
    return a + b;
}

int foo(int a, int b)
{
    return a;
}


void func(int a, int b, PF ptr)
{
    cout << ptr(a, b) << endl;
    return;
}
int _tmain(int argc, _TCHAR* argv[])
{
    PF ptr;
    ptr = bar;
    func(12, 34, ptr);
    system("pause");
    ptr = foo;
    func(12, 34, ptr);
    system("pause");
    return 0;
}


一旦知道函數指針是如何工作的,我們就可以構建一些複雜的定義,例如:

void *(*(*fp1)(int))[10];

fp1是一個指向函數的指針,該函數接受一個整型參數,並且返回類型是一個指向包含了10個void指針數組的指針。是不是很繞?

float (*((*fp2)(int,int,float)))(int);

fp2是一個指向函數的指針,該函數接受三個參數(int,int,float),返回值是一個指向函數的指針,該函數接受一個整型參數並返回一個float。

typedef doubele (*(*(*fp3)())[10])();

fp3是一個函數指針,該函數無參數,且返回一個指向含有10個指向函數指針指針數組的指針,這些函數不接收參數,且返回值是double值

int (*(*fp4())[10])();

fp4是一個返回指針的函數,該指針指向含有10個函數指針的數組,這些函數的返回值是整型。

指針函數 
與函數指針相區別的定義應該就是指針函數,指針函數本質上是一個函數,是指函數的返回值爲指針的函數,一般是形如下的函數: 
int* func(int x,int y); 
如上就是一個返回值是指針的函數,很常見。 
函數對象 
上面談到了函數指針以及應用,這裏涉獵下函數對象。從一般函數回調意義上來說,函數對象和函數指針是相同的,但是函數對象卻具有許多函數指針不具有的優點,函數對象使程序設計更加靈活,而且能夠實現函數的內聯(inline)調用,使整個程序實現性能加速。

23、二維動態數組的申請和刪除

首先是如何申請二維的數組,這裏我們先申請一個指針數組,然後令指針數組中的每一個元素都指向一個數組,這樣二維數組就成了:

    size_t row, col;
    //輸入row和col的數值
    int **MathTable = new int*[row];
    for (int i = 0; i < row; i++)
        MathTable[i] = new int[col];

然後是釋放空間的過程:

    //code
    for (int i = 0; i < row; i++)
        delete[] MathTable[i];
    delete[]MathTable;

符合new和delete配對的原則,怎麼new出來就怎麼delete掉。

 

24、關於shared_ptr使用需要記住什麼?

總結下來需要注意的大概有下面幾點: 
1)、儘量避免使用raw pointer構建shared_ptr,至於原因此處不便於多講,後續還有講解 
2)、shared_ptr使得依據共享生命週期而經行地資源管理進行垃圾回收更爲方便 
3)、shared_ptr對象的大小通常是unique_ptr的兩倍,這個差異是由於Control Block導致的,並且shared_ptr的引用計數的操作是原子的,這裏的分析也會在後續看到 
4)、默認的資源銷燬是採用delete,但是shared_ptr也支持用戶提供deleter,與unique_ptr不同,不同類型的deleter對shared_ptr的類型沒有影響。

25、C和C++的區別

1)、標準:分別隸屬於兩個不同的標準委員會。C以C99標準爲主流,C11已經發布;C++以C++98/03爲主流,C++11/14也日趨流行。 
2)、語言本身: 
1、C++是面嚮對象語言,C是面向過程語言。 
2、結構:C以結構體struct爲核心結構;C++以類class爲核心結構。 
3、多態:C可以以宏定義的方式“自定義”部分地支持多態;C++自身提供多態,並以模板templates支持編譯期多態,以虛函數virtual function支持運行期多態。 
4、頭文件的調用:C++用< >代替” “代表系統頭文件;且複用C的頭文件時,去掉”.h”在開頭加上”C”。 
5、輸入輸出:鑑於C++中以對象作爲核心,輸入和輸出都是在流對象上的操作。 
6、封裝:C中的封裝由於struct的特性全部爲公有封裝,C++中的封裝由於class的特性更加完善、安全。 
7、常見風格:C中常用宏定義來進行文本替換,不具有類型安全性;C++中常建議採用常量定義,具有類型安全性。 
8、效率:常見的說法是同等目的C通常比C++更富有效率(這其實有一定的誤解,主要在於C++代碼更難於優化且少有人使用編譯期求值的特性)。 
9、常用語言/庫特性: 
a、數組:C中採用內建數組,C++中建議採用vector。相比之下vector的大小可以動態增長,且使用一些技巧後增長並不低效,且成員函數豐富。 
b、字符串 C中採用C風格的string(實則爲字符串數組),C++中建議採用string,對比與上一條類似。 
c、內存分配:C中使用malloc與free,它們是是C標準庫函數,C++中建議使用new/delete代替前者,他們說是C++的運算符(這是筆試面試常考點)以C++中的new爲例,new可分爲operator new(new 操作)、new operator(new 操作符)和placement new(定位 new)。其中operator new執行和malloc相同的任務,即分配內存,但對構造函數一無所知;而 new operator則調用operator new,分配內存後再調用對象構造函數進行對象的構造。其中operator new是可以重載的。placement new,就是operator new的一個重載版本,允許你在一個已經分配好的內存中構造一個新的對象。 
d、指針:C中通常使用的是原生指針(raw pointer),由於常出現程序員在申請後忘記釋放造成資源泄漏的問題,在C++98中加入了“第一代”基於引用計數的智能指針auto_ptr,由於初代的各種問題(主要是無法解決循環指針),在03標準也就是TR1中引入了shared_ptr,weak_ptr和unique_ptr這三個功能各異的智能指針,並與11標準中正式確定,較好的解決了上述問題。 
三、僅有C++纔有的常用特性: 
1、語言(範式)特性: 
a、面向對象編程:C++中以關鍵字class和多態特性支持的一種編程範式; 
b、泛型編程:C++中以關鍵字template支持的一種編程範式; 
c、模板元編程 :C++中以模板特化和模板遞歸調用機制支持的一種編程範式。 
d、C++中以對象和類型作爲整個程序的核心,在對象方面,時刻注意對象創建和析構的成本,例如有一個很常用的(具名)返回值優化((N)RVO); 
在類型方面,有運行時類型信息(RTTI)等技術作爲C++類型技術的支撐。 
e、函數重載:C++允許擁有不同變量但具有相同函數名的函數(函數重載的編譯器實現方式、函數重載和(主)模板特化的區別都曾考過)。 
f、異常:以catch、throw、try等關鍵字支持的一種機制。 
g、名字空間:namespace,可以避免和減少命名衝突且讓代碼具有更強的可讀性。 
h、謂詞用法:通常以bool函數或仿函數(functor)或lambda函數的形式,出現在STL的大多數算法的第三個元素。 
2、常見關鍵字(操作符)特性: 
a、auto:在C中,auto代表自動類型通常都可省略;而在C++11新標準中,則起到一種“動態類型”的作用——通常在自動類型推導和decltype搭配使用。 
b、空指針:在C中常以NULL代表空指針,在C++中根據新標準用nullptr來代表空指針。 
c、&: 在C中僅代表取某個左值(lvalue)的地址,在C++中還可以表示引用(別名)。 
d、&&:在C中僅能表示邏輯與,在C++中還可以表示右值引用。 
e、[]:在C中僅能表示下標操作符,在C++中還可以表示lambda函數的捕捉列表。 
f、{}:在C中僅能用於數組的初始化,在C++中由於引入了初始化列表(initializer_list),可用於任何類型、容器等的初始化。 
g、常量定義:C中常以define來定義常量,C++中用const來定義運行期常量,用constexpr來定義編譯器常量。 
3、常用新特性: 
a、右值引用和move語義(太多內容,建議自查)。 
b、基於範圍的for循環(與python中的寫法類似,常用於容器)。 
c、基於auto——decltype的自動類型推導。 
d、lambda函數(一種局部、匿名函數,高效方便地出現在需要局部、匿名語義的地方)。 
e、標準規範後的多線程庫。

26、C++的內存對齊

爲什麼要內存對齊?這個網上講得更全面,就不再多說了。簡而言之,爲了速度和正確性。詳見網頁:http://blog.csdn.net/lgouc/article/details/8235471,英文原版:(http://www.ibm.com/developerworks/library/pa-dalign/) 
C/C++中的內存對齊之基礎篇 
這裏主要以【不帶有】虛繼承鏈和虛函數的struct/class爲例,【注意:所有的空間均需要爲最大類型大小的整數倍】 
1、這裏有一個C和C++不同的情況:在C中,空struct/class不佔有內存,在C++中,空struct/class通常佔有1byte的空間,原因是編譯器強行在裏面放入了一個char,這樣可以使得這個class的不同實例化在內存中分配到獨一無二的地址。 
2、最基本的內存對齊情況(其中註釋代表該類型實際大小)

        struct A
        {
                char   c; //1byte
                double d; //8byte
                int i;    //4byte
        };
在64位g++和vs2013下運行sizeof(A)結果均爲24。這種情況下的計算都比較簡單,首先確定最大類型的大小,這裏是double,因此Max = 8,因此佔據的空間就應該是8的倍數(相應的,若struct內最大的類型爲int,那麼佔據的空間就應該是4的倍數)。補齊的大小就根據最大類型的長度來確定。通常在內存中按照變量聲明的順序來分配空間,先爲char分配,佔據1byte, 8 - 1 = 7,剩餘空間小於下一個變量double的需要空間,因此另外開闢一個8byte用於安放double,緊接着安放int,它佔據另一個8byte空間的4個byte。而char後面的7byte、int後面的4byte都用於內存對齊。 
因此總大小爲8+8+8 = 24(可以看成1+7 + 8 + 4+4)。

        struct A
        {
                double d; //8byte
                char   c; //1byte
                int i; //4byte
        };

在64位g++和vs2013下運行sizeof(A)結果均爲16。根據上述說明,很容易得到 8 + 1+4+3 = 16,其中3爲char、int之後的補齊。 
3、稍複雜一點的內存對其情況

        class A
        {
        public:
                static double dd;
                char   c; //1byte
                double d; //8byte
                static A a;
                int i; //4byte
        };

在64位g++和vs2013下運行sizeof(A)結果均爲24。這裏只需要注意一下,static data member會被放在class object之外。因此sizeof(A)時,不會計算他們的大小在內。其餘計算同 2 中的第一個例子相同。 
4、只有一種類型時的情況:如一個struct中僅有一個char或int等,由於“所有的空間均需要爲最大類型大小的整數倍”這個原則,struct的空間大小就是這些類型各自的大小,而不用再進行補齊。 
C/C++中的內存對齊之深入篇——虛繼承和虛函數 
這裏主要以【帶有】虛繼承鏈和虛函數的struct/class爲例(【注意:這裏不同編譯器的實現可能不同,仍以g++和vs2013爲例】) 
1、帶有虛函數的class的內存對齊情況 
這種情況下就只需要考慮到虛函數對應一個vptr(virtual ptr,虛指針)就行了。而不同的編譯器,放置vptr的位置可能不同,通常是放在對象的頭部或者尾部。

        class A
        {
        public:
                virtual ~A();
                char   c; //1byte
                double d; //8byte
                int i; //4byte
        };
在32位g++下運行sizeof(A)結果爲24,在64位g++下運行sizeof(A)結果爲32,在vs2013下運行sizeof(A)結果爲32。 
32位g++下:通常將vptr放在object的最前面,可以確定該內存空間與data member的內存空間不需要獨立。也就是說,該例中,無論虛析構函數被聲明在哪裏,都會在分配空間時最先給一個vptr分配4byte的空間,且該空間後是可以緊接着分配char的1byte的空間。以此類推,結合上面的例子,可以得出4(vptr)+1(char)+3(補齊) + 8 + 4+4 = 24 
64位g++下:通常將vptr放在object的最前面,無法確定該內存空間與data member的內存空間是否需要獨立。也就是說,該例中,無論虛析構函數被聲明在哪裏,都會在分配空間時最先給一個vptr分配8byte的空間,且不清楚該空間後是否可以緊接着分配char的1byte的空間(由於該vptr佔據8byte,無論是否需要間隔,效果都一樣),以此類推,結合上面的例子,可以得出 
8(vptr)+ 1(char)+7(補齊) + 8 + 4+4 = 32 
在vs2013下:通常將vptr放在object的最前面,vptr的大小與實際最大類型的大小相關。也就說說,該例中,無論虛析構函數被聲明 在哪裏,都會在分配空間時最先給一個vptr分配4byte的空間,由於後面存在double類型,需要將vptr補齊。結合上面的例子,可以得出 
4(vptr)+4(補齊) + 1+7 + 8 +4+4 = 32 
虛基類的繼承是C++中爲了多重繼承而產生的,但是虛基類的繼承有帶來了新的問題,如何能夠實現這種動態綁定呢?

27、爲什麼函數參數的入棧的順序是從右往左

因爲好多函數是不定參數個數的,比如最常用的printf,所以需要參數的入棧順序是從右往左。

28、C++的多態機制

 

29、C++中的轉化機制?各適用於什麼環境?dynamic_cast轉換失敗時,會出現什麼情況?

對指針,返回NULL.對引用,拋出bad_cast異常more Effective C++ 
C++引入了4種類型轉化操作符(cast operator):static_cast,const_cast,dynamic_cast和reinterpret_cast,使用方法與C語言中略有不同:

(type)expression;   //這是C語言的
1
然後引入C++的:

static_cast<type>(expression);//這是C++的
1
然後看一下各自的適用範圍: 
static_cast:static_cast基本上擁有與C舊式轉型相同的威力和意義,以及相同的限制。但是,該類型轉換操作符不能移除常量性,因爲有一個專門的操作符用來移除常量性。 
const_cast:用來改變表達式中的常量性(constness)或者易變形(volatileness),只能用於此功能。 
dynamic_cast:將指向基類basic class object的pointer或者reference轉型爲指向派生類derived(或這sibling base)class object的pointer或者reference中,並且可以獲知是否轉型成功:如果轉型失敗,當轉型對象是指針的時候會返回一個null指針;當轉型對象是reference會拋出一個異常exception。dynamic_cast無法應用在缺乏虛函數的類型上,也不能改變類型的常量性。 
此外,dynamic_cast還有一個用途就是找出被對象佔用的內存的起始點。 
reinterpret_cast:這個操作符的轉換結果幾乎總是和編譯器平臺相關,所以不具有移植性。reinterpret_cast的最常用用途是轉換“函數指針”類型,如下:

typedef void(*FuncPtr)();
int doSomething();
int main()
{
    FuncPtr funcPtrArray[10];
    funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);
    return 0;
}


通過reinterpret_cast強迫編譯器了,併成功的將不同的類型的函數&doSomething轉換爲需要的類型。不過這個操作符進行的轉換動作不具有移植性(C++不保證所有的函數指針都能以此方式重新呈現),某些情況下這樣的轉型可能會導致不正確的結果,所以這種操作不到萬不得已不要使用。

30、 拷貝構造函數作用及用途?什麼時候需要自定義拷貝構造函數?

答:

(1)、在C++中,有下面三種對象需要拷貝的情況: 
a、一個對象以值傳遞的方式傳入函數體 
b、一個對象以值傳遞的方式從函數返回 
c、一個對象需要通過另外一個對象進行初始化 
以上的情況就需要拷貝構造函數的調用。 
(2)、當類中的數據成員需要動態分配存儲空間時,不可以依賴default copy constructor。當default copy constructor被因編譯器需要而合成時,將執行default memberwise copy語義。 
此時如果類中有動態分配的存儲空間時,將會發生慘重的災情。 
在需要時(包括這種對象要賦值、這種對象作爲函數參數要傳遞、函數返回值爲這種對象等情況),要考慮到自定義拷貝構造函數。

31、構造函數可以調用虛函數嗎?語法上通過嗎?語義上可以通過嗎?

不能,語法上通過,語義上有問題。 
derived class對象內的base class成分會在derived class自身構造之前構造完畢。因此,在base class的構造函數中執行的virtual函數將會是base class的版本,決不會是derived class的版本。 
即使目前確實正在構造derived class。

32、深拷貝和淺拷貝的區別

答:淺拷貝:如果在類中沒有顯式地聲明一個拷貝構造函數,那麼,編譯器將會根據需要生成一個默認的拷貝構造函數,完成對象之間的位拷貝。default memberwise copy即稱爲淺拷貝。 
此處需要注意,並非像大多數人認爲的“如果class未定義出copy constructor,那麼編譯器就會爲之合成一個執行default memberwise copy語義的copy constructor”。 
通常情況下,只有在default copy constructor被視爲trivial時,纔會發生上述情況。一個class,如果既沒有任何base/member class含有copy constructor,也沒有任何virtual base class或 virtual functions, 
它就會被視爲trivial。 
通常情況下,淺拷貝是夠用的。 
深拷貝:然而在某些狀況下,類內成員變量需要動態開闢堆內存,如果實行位拷貝,也就是把對象裏的值完全複製給另一個對象,如A=B。 
這時,如果B中有一個成員變量指針已經申請了內存,那A中的那個成員變量也指向同一塊內存。如果此時B中執行析構函數釋放掉指向那一塊堆的指針,這時A內的指針就將成爲懸掛指針。 
因此,這種情況下不能簡單地複製指針,而應該複製“資源”,也就是再重新開闢一塊同樣大小的內存空間。

33、動態綁定和靜態綁定的區別

(1)、對象的靜態類型:對象在聲明時採用的類型。是在編譯期確定的。 
(2)、對象的動態類型:目前所指對象的類型。是在運行期決定的。對象的動態類型可以更改,但是靜態類型無法更改。 
(3)、靜態綁定:綁定的是對象的靜態類型,某特性(比如函數)依賴於對象的靜態類型,發生在編譯期。 
(4)、動態綁定:綁定的是對象的動態類型,某特性(比如函數)依賴於對象的動態類型,發生在運行期。

34、

35、virtual函數能聲明爲內聯嗎?爲什麼?

答:通常情況下是不能的 
原因:inline是編譯期決定,他意味着在執行前就將調用動作替換爲被調用函數的本體; 
virtual是運行期決定,他意味着直到運行期才決定調用哪個函數。 
這兩者之間通常是衝突的。 
然而也有特例,就是當編譯階段就已經知道調用虛函數的指針爲多態指針。這裏就不再敖述了。

36、哪些類型的對象不可以作爲union的成員?爲什麼有這種限制?

答: 標準規定,凡是具有non-trivial constructor、non-trivial destructor、non-trivial copy constructor、non-trivial assignment operator的class對象都不能作爲union的成員。 
即是說,這個class的以上四種成員必須均經由編譯器合成且該class無虛函數和虛基類。 
有這種限制是爲了兼容C。

37、C++11有哪些進步?有哪些新的東西?

lambda 
線程庫 
智能指針 
auto

38、如何實現一個不能在堆分配的類,如何實現一個不能被繼承的類

如何實現一個不能在堆上分配的類,如果要在堆上分配就是會使用new,所以可以重載new 操作符,並將其重載於class A的private內:

class A 
{
public:
    A(int a):_x(a){}
    int Display() {
        return _x;
    }
    void setVal(int x) {
        _x = x;
        return;
    }
private:
    //
    int _x;
    void* operator new(size_t t){
    }
};


如何實現一個不能被繼承的類,這裏有一個比較簡單的方法,利用C++11的新關鍵字final:

class B final {
public:
    B(int a) {
    }
};

現在就不能繼承該類。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章