1. 程式人生 > >C++面試題(一)

C++面試題(一)

1、什麼是虛擬函式?什麼是純虛擬函式?

答:虛擬函式宣告如下: virtual ReturnType FunctionName(Parameter);引入虛擬函式是為了動態繫結。
純虛擬函式宣告如下:virtual ReturnType FunctionName()= 0;引入純虛擬函式是為了派生介面。
虛擬函式
定義一個函式為虛擬函式,不代表函式為不被實現的函式
定義他為虛擬函式是為了允許用基類的指標來呼叫子類的這個函式

C++中的虛擬函式的作用主要是實現了多型的機制。基類定義虛擬函式,子類可以重寫該函式;在派生類中對基類定義的虛擬函式進行重寫時,需要再派生類中宣告該方法為虛方法。

當子類重新定義了父類的虛擬函式後,當父類的指標指向子類物件的地址時,[即B b; A a = &b;] 父類指標根據賦給它的不同子類指標,動態的呼叫子類的該函式,而不是父類的函式(如果不使用virtual方法,請看後面),且這樣的函式呼叫發生在執行階段,而不是發生在編譯階段,稱為動態聯編。而函式的過載可以認為是多型,只不過是靜態的。注意,非虛擬函式靜態聯編,效率要比虛擬函式高,但是不具備動態聯編能力。
如果使用了virtual關鍵字,程式將根據引用或指標指向的 對 象 類 型 來選擇方法,否則使用引用型別或指標型別來選擇方法。
純虛擬函式
純虛擬函式是在基類中宣告的虛擬函式,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛擬函式的方法是在函式原型後加“=0”

 virtual void funtion1()=0

引入原因
  1、為了方便使用多型特性,我們常常需要在基類中定義虛擬函式。
  2、在很多情況下,基類本身生成物件是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理。
  為了解決上述問題,引入了純虛擬函式的概念,將函式定義為純虛擬函式(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多型性。同時含有純虛擬函式的類稱為抽象類,它不能生成物件。這樣就很好地解決了上述兩個問題。
聲明瞭純虛擬函式的類是一個抽象類。所以,使用者不能建立類的例項,只能建立它的派生類的例項。
純虛擬函式最顯著的特徵是:它們必須在繼承類中重新宣告函式(不要後面的=0,否則該派生類也不能例項化),而且它們在抽象類中往往沒有定義。
定義純虛擬函式的目的在於,使派生類僅僅只是繼承函式的介面。

2.純虛擬函式是否可以例項化

c++中包含純虛擬函式的類是不允許被例項化的!!!,進一步說,如果繼承該類的類不重寫這個純虛擬函式的話,也是不允許被例項化的。即,包含純虛函是的類派生出來的類都必須重寫這個純虛擬函式!

為什麼要有這個機制呢?
例如動物可以派生出貓、狗等。 貓和狗可以例項化,而動物這個概念是不可以例項化的。

3.不能宣告為虛擬函式的有哪些

1、靜態成員函式; 2、類外的普通函式; 3、建構函式; 4、友元函式
虛擬函式是為了實現多型特性的。虛擬函式的呼叫只有在程式執行的時候才能知道到底呼叫的是哪個函式,其是有有如下幾點需要注意:
(1) 類的建構函式不能是虛擬函式
建構函式是為了構造物件的,所以在呼叫建構函式時候必然知道是哪個物件呼叫了建構函式,所以建構函式不能為虛擬函式。
(2) 類的靜態成員函式不能是虛擬函式
類的靜態成員函式是該類共用的,與該類的物件無關,靜態函式裡沒有this指標,所以不能為虛擬函式。
(3)行內函數
行內函數的目的是為了減少函式呼叫時間。它是把行內函數的函式體在編譯器預處理的時候替換到函式呼叫處,這樣程式碼執行到這裡時候就不需要花時間去呼叫函式。inline是在編譯器將函式類容替換到函式呼叫處,是靜態編譯的。而虛擬函式是動態呼叫的,在編譯器並不知道需要呼叫的是父類還是子類的虛擬函式,所以不能夠inline宣告展開,所以編譯器會忽略。
(4)友元函式
友元函式與該類無關,沒有this指標,所以不能為虛擬函式。

4、基類為什麼需要虛解構函式?

防止記憶體洩露,delete p(基類)的時候,它很機智的先執行了派生類的解構函式,然後執行了基類的解構函式。
如果基類的解構函式不是虛擬函式,在delete p(基類)時,呼叫解構函式時,只會看指標的資料型別,而不會去看賦值的物件,這樣就會造成記憶體洩露。

5、如何初始化const和static資料成員?

答:通常在類外初始化static資料成員,但是 static const 的整型(bool,char,int,long)可以再類宣告中初始化,static const的其他型別也必須在類外初始化(包括整型的陣列)。

const定義的常量在超出其作用域之後其空間會被釋放,而static定義的靜態常量在函式執行後不會釋放其儲存空間。
static表示的是靜態的。類的靜態成員函式、靜態成員變數是和類相關的,而不是和類的具體物件相關的。即使沒有具體物件,也能呼叫類的靜態成員函式和成員變數。一般類的靜態函式幾乎就是一個全域性函式,只不過它的作用域限於包含它的檔案中。

在C++中,static靜態成員變數不能在類的內部初始化。在類的內部只是宣告,定義必須在類定義體的外部,通常在類的實現檔案中初始化,如:double Account::Rate = 2.25;static關鍵字只能用於類定義體內部的宣告中,定義時不能標示為static

在C++中,const成員變數也不能在類定義處初始化,只能通過建構函式初始化列表進行,並且必須有建構函式。

const資料成員 只在某個物件生存期內是常量,而對於整個類而言卻是可變的。因為類可以建立多個物件,不同的物件其const資料成員的值可以不同。所以不能在類的宣告中初始化const資料成員,因為類的物件沒被建立時,編譯器不知道const資料成員的值是什麼。

const資料成員的初始化只能在類的建構函式的初始化列表中進行。要想建立在整個類中都恆定的常量,應該用類中的列舉常量來實現,或者static cosnt。

6、如何確保物件在丟擲異常時也能被刪除?什麼是RAII?

答:總的思想是RAII:設計一個class,令他的建構函式和解構函式分別獲取和釋放資源。
有兩個方法:

利用“函式的區域性物件無論函式以何種方式(包括因異常)結束都會被析構”這一特性,將“一定要釋放的資源”放進區域性物件的解構函式;
使用智慧指標。

7、 為什麼需要private繼承?

private繼承與public的繼承是完全不同的,主要體現在兩個地方:

其一,public繼承在子類中保持父類的訪問許可權,即父類中是public的成員函式或成員變數,在子類中仍是public,對private或者protected的成員函式或成員變數亦是如此;但private繼承則不是這樣了,它破壞了父類中的訪問許可權標定,將之都轉成private,這對子類本身並無影響(照常訪問),但卻影響了子類的子類,子類的子類將無法訪問這些宣告/定義在爺爺輩類的成員變數或成員函式。

其二,Liskov法則不再適用,也就是說“一切父類出現的地方都可以被子類所替代”的法則在private這裡不成立

8、如果在建構函式和解構函式中丟擲異常會發生什麼?什麼是棧展開?

答: (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。

9、如何在const成員函式中賦值?

答:使用mutable去掉const的成員函式的const性質
const_cast和mutable的比較
const_cast:
1) 強制去掉物件的const屬性。
2) 缺點:對const物件,呼叫包含const_cast的const成員函式,屬於未定義行為。
mutable:
1) 使用場景:對可能要發生變化的成員前,加上儲存描述符mutable。
2) 實質:對加了mutable的成員,無視所有const宣告。
為什麼要有這種去除常量標誌的需求?
答:兩個概念:物理常量性和邏輯常量性
物理常量性:實際上就是常量。
邏輯常量性:對使用者而言是常量,但在使用者不能訪問的細節上不是常量。

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

答:(1)、
a、使用單引數的建構函式或N個引數中有N-1個是預設引數的建構函式,如:

class A
{
publicA(stirng s);
      A(string s,int a = 0);
};
class String   
{  
public:  
    String ( const char* p ); // 用C風格的字串p作為初始化值  
    //…  
}  

String s1 = “hello”; //OK 隱式轉換,等價於String s1 = String(”hello”),將char型變成了string型別  

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。

11、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的返回值就是下一個節點,也可以利用函式的返回值。

12、New和malloc的區別

malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子。它們都可用於申請動態記憶體和釋放記憶體。
1. 申請的記憶體所在位置不同,
mollloc函式從自由儲存區(free store)上為物件動態分配記憶體空間,而new操作符從堆上動態分配記憶體。new甚至可以不為物件分配記憶體!定位new的功能可以辦到這一點:new (place_address) type,operator new不分配任何的記憶體,它只是簡單地返回指標實參。
2.返回型別安全性
new操作符記憶體分配成功時,返回的是物件型別的指標,型別嚴格與物件匹配,無須進行型別轉換,故new是符合型別安全性的操作符。而malloc記憶體分配成功則是返回void * ,需要通過強制型別轉換將void*指標轉換成我們需要的型別。
3.記憶體分配失敗時的返回值
new記憶體分配失敗時,會丟擲bac_alloc異常,它不會返回NULL;malloc分配記憶體失敗時返回NULL。
4.是否需要指定記憶體大小
使用new操作符申請記憶體分配時無須指定記憶體塊的大小,編譯器會根據型別資訊自行計算,而malloc則需要顯式地指出所需記憶體的尺寸。
5.是否呼叫建構函式/解構函式
new 第一步:呼叫operator new 函式(對於陣列是operator new[])分配一塊足夠大的,原始的,未命名的記憶體空間以便儲存特定型別的物件。
第二步:編譯器執行相應的建構函式以構造物件,併為其傳入初值。
第三步:物件構造完成後,返回一個指向該物件的指標。
使用delete操作符來釋放物件記憶體時會經歷兩個步驟:
第一步:呼叫物件的解構函式。
第二步:編譯器呼叫operator delete(或operator delete[])函式釋放記憶體空間。
6.對陣列的處理
C++提供了new[]與delete[]來專門處理陣列型別:。
7.是否相互呼叫
new底層其實還是用malloc實現動態記憶體分配的,所以new呼叫malloc,malloc不會呼叫new。
8.函式過載
new可以過載
malloc不行

13、C++如何避免記憶體洩漏

java語言有自己的垃圾回收機制,而c/c++卻要程式設計師手動的釋放用關鍵字new或者 malloc系統函式申請的記憶體空間,然而由於程式設計師的疏忽可能會忘記去手動釋放記憶體,這樣就導致了程式記憶體的洩漏。
a、使用RAII(Resource Acquisition Is Initialization,資源獲取即初始化)技法,以建構函式獲取資源(記憶體),解構函式釋放。
b、相比於使用原生指標,更建議使用智慧指標,尤其是C++11標準化後的智慧指標。
c、注意delete和delete[]的使用方法。
d、這是很複雜的一種情況,是關於類的copy constructor的。首先先介紹一些概念。

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

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

15、指標和引用的區別

相同點:
●都是地址的概念;
指標指向一塊記憶體,它的內容是所指記憶體的地址;而引用則是某塊記憶體的別名。
不同點:
●指標是一個實體,而引用僅是個別名;
●引用只能在定義時被初始化一次,之後不可變;指標可變;引用“從一而終”,指標可以“見異思遷”;
●引用沒有const,指標有const,const的指標不可變;(具體指沒有int& const a這種形式,而const int& a, 前者指引用本身即別名不可以改變,這是當然的,所以不需要這種形式,後者指引用所指的值不可以改變)
●引用不能為空,指標可以為空;
●“sizeof 引用”得到的是所指向的變數(物件)的大小,而“sizeof 指標”得到的是指標本身的大小;
●指標和引用的自增(++)運算意義不一樣;
●引用是型別安全的,而指標不是 (引用比指標多了型別檢查)

16、指標陣列和陣列指標的區別

指標陣列
首先它是一個數組,陣列的元素都是指標,陣列佔多少個位元組由陣列本身的大小決定,每一個元素都是一個指標,在32 位系統下任何型別的指標永遠是佔4 個位元組。它是“儲存指標的陣列”的簡稱。

char *arr[4] = {"hello", "world", "shannxi", "xian"};

陣列指標
首先它是一個指標,它指向一個數組。在32 位系統下任何型別的指標永遠是佔4 個位元組,至於它指向的陣列佔多少位元組,不知道,具體要看陣列大小。它是“指向陣列的指標”的簡稱。

char (*pa)[10];

17、指標函式和函式指標的區別

指標函式是指帶指標的函式,即本質是一個函式,函式返回型別是某一型別的指標。

int *GetDate();
int * aaa(int,int);

函式指標是指向函式的指標變數,即本質是一個指標變數。

  int (*f) (int x); /*宣告一個函式指標 */

   f=func; /* 將func函式的首地址賦給指標f */
#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;
}
int _tmain(int argc, _TCHAR* argv[])
{
    PF func;
    func = bar;
    cout << func(12, 34) << endl;
    system("pause");
    func = foo;
    cout << func(12, 34) << endl;
    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個函式指標的陣列,這些函式的返回值是整型。

18、二維動態陣列的申請和刪除

這裡寫圖片描述
這裡寫圖片描述

// a = new int*[n];//建立行指標陣列  
int **a = new int*[n];//這一步是前面兩行的結合體  
for(int i=0;i<n;++i)  
{  
    *(a+i)= new int[n];//為每一行分配空間  
    //a[i] = new int[n];  
    memset(a[i],0,n*sizeof(int));  
}  

首先是如何申請二維的陣列,這裡我們先申請一個指標陣列,然後令指標陣列中的每一個元素都指向一個數組,這樣二維陣列就

    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掉。

19、C和C++的區別

1、標準:分別隸屬於兩個不同的標準委員會。C以C99標準為主流,C11已經發布;C++以C++98/03為主流,C++11/14也日趨流行。
2、語言本身:

  • C++是面嚮物件語言,C是面向過程語言。
  • 結構:C以結構體struct為核心結構;C++以類class為核心結構。
  • 多型:C可以以巨集定義的方式“自定義”部分地支援多型;C++自身提供多型,並以模板templates支援編譯期多型,以虛擬函式virtual
    function支援執行期多型。
  • 標頭檔案的呼叫:C++用< >代替” “代表系統標頭檔案;且複用C的標頭檔案時,去掉”.h”在開頭加上”C”。
  • 輸入輸出:鑑於C++中以物件作為核心,輸入和輸出都是在流物件上的操作。
  • 封裝:C中的封裝由於struct的特性全部為公有封裝,C++中的封裝由於class的特性更加完善、安全。
  • 常見風格:C中常用巨集定義來進行文字替換,不具有型別安全性;C++中常建議採用常量定義,具有型別安全性。
  • 效率:常見的說法是同等目的C通常比C++更富有效率(這其實有一定的誤解,主要在於C++程式碼更難於優化且少有人使用編譯期求值的特性)。

常用語言/庫特性:

  • 陣列:C中採用內建陣列,C++中建議採用vector。相比之下vector的大小可以動態增長,且使用一些技巧後增長並不低效,且成員函式豐富。
  • 字串 C中採用C風格的string(實則為字串陣列),C++中建議採用string,對比與上一條類似。
  • 記憶體分配: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的一個過載版本,允許你在一個已經分配好的記憶體中構造一個新的物件。
  • 指標:C中通常使用的是原生指標(raw
    pointer),由於常出現程式設計師在申請後忘記釋放造成資源洩漏的問題,在C++98中加入了“第一代”基於引用計數的智慧指標auto_ptr,由於初代的各種問題(主要是無法解決迴圈指標),在03標準也就是TR1中引入了shared_ptr,weak_ptr和unique_ptr這三個功能各異的智慧指標,並與11標準中正式確定,較好的解決了上述問題。

僅有C++才有的常用特性:
語言(正規化)特性:

  • 面向物件程式設計:C++中以關鍵字class和多型特性支援的一種程式設計正規化;
  • 泛型程式設計:C++中以關鍵字template支援的一種程式設計正規化;
  • 模板超程式設計 :C++中以模板特化和模板遞迴呼叫機制支援的一種程式設計正規化。
  • C++中以物件和型別作為整個程式的核心,在物件方面,時刻注意物件建立和析構的成本,例如有一個很常用的(具名)返回值優化((N)RVO);

在型別方面,有執行時型別資訊(RTTI)等技術作為C++型別技術的支撐。

  • 函式過載:C++允許擁有不同變數但具有相同函式名的函式(函式過載的編譯器實現方式、函式過載和(主)模板特化的區別都曾考過)。
  • 異常:以catch、throw、try等關鍵字支援的一種機制。
  • 名字空間:namespace,可以避免和減少命名衝突且讓程式碼具有更強的可讀性。
  • 謂詞用法:通常以bool函式或仿函式(functor)或lambda函式的形式,出現在STL的大多數演算法的第三個元素。

常見關鍵字(操作符)特性:

  • auto:在C中,auto代表自動型別通常都可省略;而在C++11新標準中,則起到一種“動態型別”的作用——通常在自動型別推導和decltype搭配使用。
  • 空指標:在C中常以NULL代表空指標,在C++中根據新標準用nullptr來代表空指標
  • &: 在C中僅代表取某個左值(lvalue)的地址,在C++中還可以表示引用(別名)。
  • &&:在C中僅能表示邏輯與,在C++中還可以表示右值引用。
  • []:在C中僅能表示下標操作符,在C++中還可以表示lambda函式的捕捉列表。
  • {}:在C中僅能用於陣列的初始化,在C++中由於引入了初始化列表(initializer_list),可用於任何型別、容器等的初始化。

常量定義:

  • C中常以define來定義常量,C++中用const來定義執行期常量,用constexpr來定義編譯器常量。

常用新特性:

  • 右值引用和move語義(太多內容,建議自查)。
  • 基於範圍的for迴圈(與python中的寫法類似,常用於容器)。
  • 基於auto——decltype的自動型別推導。
  • lambda函式(一種區域性、匿名函式,高效方便地出現在需要區域性、匿名語義的地方)。
  • 標準規範後的多執行緒庫。

20、C++的記憶體對齊

C/C++中的記憶體對齊之基礎篇
這裡主要以【不帶有】虛繼承鏈和虛擬函式的struct/class為例,【注意:所有的空間均需要為最大型別大小的整數倍】
這裡有一個C和C++不同的情況:在C中,空struct/class不佔有記憶體,在C++中,空struct/class通常佔有1byte的空間,原因是編譯器強行在裡面放入了一個char,這樣可以使得這個class的不同例項化在記憶體中分配到獨一無二的地址。
最基本的記憶體對齊情況(其中註釋代表該型別實際大小)

        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之後的補齊。

稍複雜一點的記憶體對其情況

        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 中的第一個例子相同。

只有一種型別時的情況:如一個struct中僅有一個char或int等,由於“所有的空間均需要為最大型別大小的整數倍”這個原則,struct的空間大小就是這些型別各自的大小,而不用再進行補齊。
C/C++中的記憶體對齊之深入篇——虛繼承和虛擬函式

        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
2、帶有普通繼承的class的記憶體對齊情況

        class A
        {
                int i;    //4byte
                char c1;//1byte
        };
        class B :  public A
        {
                char c2;//1byte
        };
        class C :  public B
        {
             char c3;//1byte
        };

在64位g++下,呼叫sizeof(A)、sizeof(B)、sizeof(C)後的結果均為8;在vs2013下分別為8,12,16

g++下:普通繼承時,派生類和基類的記憶體空間沒有間隔。
A:4+1+3(補齊) = 8
B:4+1+1(c2)+2(補齊) = 8
C:4+1+1(c2)+1(c3)+1(補齊) = 8
注意這裡所有成員均為私有成員,如果改成public或protected則大小會有變化
vs2013下:普通繼承時,派生類和基類的記憶體空間需要獨立,即先補齊基類,再分配派生類。
A:4+1+3(補齊) = 8
B:4+1+3(補齊) + 1(c2)+3(補齊) = 12
C:4+1+3(補齊) + 1(c2)+3(補齊) + 1(c3)+3(補齊) = 16
3、帶有虛擬繼承鏈的class的記憶體對齊情況

        class A
        {
                int i;    //4byte
                char c1;//1byte
        };
        class B : virtual public A
        {
                char c2;//1byte
        };
        class C : virtual public B
        {
             char c3;//1byte
        };

呼叫sizeof(A)、sizeof(B)、sizeof(C)後,32位g++下,分別為8,16,24;64位g++下,分別為:8,24,40;vs2013下分別為8,16,24

32位g++下:
A:仍然是4+1+3(補齊) = 8
B:4+1+3 + 4(vptr)+1(c2)+3(補齊) = 16
C;4+1+3 + 4(vptr)+1(c2)+3(補齊) + 4(vptr)+1(c3)+3(補齊) = 24
64位g++下:
A:仍然是4+1+3(補齊) = 8
B:4+1+3 + 8(vptr)+1(c2)+7(補齊) = 24
C;4+1+3 + 8(vptr)+1(c2)+7(補齊) + 8(vptr)+1(c3)+7(補齊) = 40
vs2013下:
A:仍然是4+1+3(補齊) = 8
B:4+1+3 + 4(vptr)+1(c2)+3(補齊) = 16
C;4+1+3 + 4(vptr)+1(c2)+3(補齊) + 4(vptr)+1(c3)+3(補齊) = 24
注意這裡vs2013的情況表面看上去和32位g++相同,實則不然。例如去掉class B對於A的虛擬繼承性

        class A
        {
                int i;    //4byte
                char c1;//1byte
        };
        class B :  public A    /*注意這裡跟上面相比不是虛擬繼承了*/
        {
                char c2;//1byte
        };
        class C : virtual public B
        {
             char c3;//1byte
        };

呼叫sizeof(A)、sizeof(B)、sizeof(C)後,32位g++下:分別為8,8,16;vs2013下分別為8,12,20

32位g++下:
A:仍然是4+1+3(補齊) = 8
B:B:4+1+1(c2)+2(補齊) = 8(因為不是虛擬繼承)
C;4+1+1(c2)+2(補齊) + 4(vptr)+1(c3)+3(補齊) = 16
vs2013下:
A:仍然是4+1+3(補齊) = 8
B:4+1+3(補齊) + 1(c2)+3(補齊) = 12
C;4+1+3(補齊) + 1(c2)+3(補齊) + 4(vptr)+1(c3)+3(補齊) = 20