1.宣告與定義的區別
int a
定義變數需要為變數在記憶體中分配儲存空間
extern int a
宣告不需要分配儲存空間
宣告的目的是為了在定義之前使用,如果不需要在定義之前使用,那麼就沒有單獨宣告的必要
2.static來宣告一個變數的作用
區域性變數用static宣告,變數由動態儲存方式改變為靜態儲存方式。為該變數分配的儲存空間會存在於整個程式執行過程中。靜態區域性變數作用域侷限於本函式內。
外部變數用static宣告,使變數區域性化(區域性於本檔案),但仍為靜態儲存方式。靜態外部變數作用域侷限於本檔案內
static靜態變數雖然和整個程式共生存期,但是作用域還是需要看其定義的地方,當你在某個函式中定義一個變數,該變數作用域僅在該函式中。但你在檔案開頭定義一個全域性變數,該變數作用域僅在該檔案中。所以當你宣告一個變數呼叫另一個檔案靜態變數,編譯器會報錯的。
3.全域性變數和區域性變數在記憶體中是否有區別?如果有,是什麼區別?
全域性變數儲存在靜態儲存區,區域性變數存在於堆疊中。動態申請資料存在於(堆)中。
4.區域性變數能否和全域性變數重名?
能,區域性會遮蔽全域性。要用全域性變數,需要使用"::"
5.全域性變數可不可以定義在可被多個.C檔案包含的標頭檔案中?為什麼?
可以,在不同的C檔案中以static形式來宣告同名全域性變數。前提是其中只能有一個C檔案中對此變數賦初值,此時連線不會出錯。
變數的定義只能出現一次,否則會導致重複定義。但卻可以宣告多次。全域性變數定義在標頭檔案中。當該標頭檔案被多個c檔案包含的話,就會導致重複定義。所以全域性變數不可以定義在標頭檔案中。
6.static全域性變數與普通的全域性變數有什麼區別?static區域性變數和普通區域性變數有什麼區別?static函式與普通函式有什麼區別?
-
static全域性變數:靜態全域性變數限制了其作用域,只在定義該變數的原始檔內有效。在同一源程式的其它原始檔中不能使用它。static全域性變數只初始化一次,防止在其他檔案單元中被引用。
-
普通全域性變數:非靜態全域性變數的作用域是整個源程式,當一個源程式由多個原始檔組成時,非靜態的全域性變數在各個原始檔中都是有效的。
-
static區域性變數:變數由動態儲存方式改變為靜態儲存方式。static區域性變數只被初始化一次,下一次依據上一次結果值。
-
普通區域性變數:還是動態儲存方式,儲存在堆疊中。
-
static函式:作用域僅在本檔案中,只在當前原始檔中使用的函式應該說明為內部函式(static),內部函式應該在當前原始檔中說明和定義。static函式在記憶體中只有一份。
-
普通函式:可在當前原始檔以外使用,需應該在一個標頭檔案中說明,要使用這些函式的原始檔要包含這個標頭檔案。普通函式在每個被呼叫中維持一份拷貝。
7.extern 和 static 的區別,什麼情況用前者什麼情況用後者
auto自動變數:表明變數自動具有本地範圍,在離開作用域,無論塊作用域,檔案作用域還是函式作用域,變數都會被程式隱藏或自動釋放。然後等你重新進入該作用域,變數又重新被定義和呼叫。使用auto變數優勢是無需考慮變數是否被釋放。
static靜態變數:簡單說就是在函式等呼叫結束後,該變數也不會被釋放,儲存的值還保留。即它的生存期是永久的,直到程式執行結束,系統才會釋放,但也無需手動釋放。
extern外部變數:它屬於變數宣告,extern int a和int a的區別就是,前者告訴編譯器,有一個int型別的變數a定義在其他地方,如果有呼叫請去其他檔案中查詢定義。
關於extern變數宣告使用,例如一個工程中:
Test1.cpp檔案開頭定義了int i =10
; //定義了一個全域性變數
Test2.cpp檔案中定義:extern int i
; //宣告在另一個編譯單元有i變數
注意:不可以寫成extern int i =10,因為變數已經存在,不可以在宣告時候賦初始值。
8.x=x+1,x+=1,x++哪個效率高
x++ > x+=1 > x=x+1
- x++:讀取x的值->x自增1
- x+=1:讀取右x的地址->x值+1->將得到的值傳回給x(x地址已經讀出)
- x=x+1:讀取右x的地址->x值+1->讀取左x的地址(並不知道左右是同一個x,所以要讀取兩遍)->將右值傳給左值
9.const 和#define 的優缺點
使用const關鍵字來宣告變數,表明,記憶體被初始化後,程式便不能再對它進行修改。 在預設的情況下,全域性變數的連結性為外部的,但const全域性變數的連結性為內部的。也就是說,在C++看來,全域性const定義就像使用了static說明符一樣。
const int Months = 12; 此時,應該注意的是應該在宣告中對const進行初始化,我們應該避免如下的寫法: const int Months; Months = 12;
- define由預處理程式處理,const由編譯程式處理。
- #define不分記憶體,因為它是預編譯指令,編譯前進行了巨集替換。
- const定義常量是有資料型別的,這樣const定義的常量編譯器可以對其進行資料靜態型別安全檢查,而#define巨集定義的常量卻只是進行簡單的字元替換,沒有型別安全檢查,且有時還會產生邊際效應。
- 有些除錯程式可對const進行除錯,但不對#define進行除錯
- const在編譯期間會計算其值,而define不會 -當定義區域性變數時,const作用域僅限於定義區域性變數的函式體內。但用#define時其作用域不僅限於定義區域性變數的函式體內,而是從定義點到整個程式的結束點。但也可以用#undef取消其定義從而限定其作用域範圍。只用const定義常量,並不能起到其強大的作用。const還可修飾函式形式引數、返回值和類的成員函式等。從而提高函式的健壯性。因為const修飾的東西能受到c/c++的靜態型別安全檢查機制的強制保護,防止意外的修改。
10.strcpy和memcpy的區別
strcpy提供了字串的複製。即strcpy只用於字串複製,並且它不僅複製字串內容之外,還會複製字串的結束符。
strcpy函式的原型是:char* strcpy(char* dest, const char* src)
;
strcpy的風險:(strcpy本身沒有什麼風險,風險來源於傳遞進去的兩個引數)
1、記憶體不夠:strcpy(x,y),字串y比x大的話,就越界了
2、沒有結束符
3、拷貝自身
memcpy提供了一般記憶體的複製。即memcpy對於需要複製的內容沒有限制,因此用途更廣。
char *strcpy(char * dest, const char * src) // 實現src到dest的複製
{
if ((src == NULL) || (dest == NULL)) { //判斷引數src和dest的有效性
return NULL;
}
char *strdest = dest; //儲存目標字串的首地址
while ((*strDest++ = *strSrc++)!='\0'); //把src字串的內容複製到dest下
return strdest;
}
void *memcpy(void *memTo, const void *memFrom, size_t size)
{
if ((memTo == NULL) || (memFrom == NULL)) {//memTo和memFrom必須有效
return NULL;
}
char *tempFrom = (char *)memFrom; //儲存memFrom首地址
char *tempTo = (char *)memTo; //儲存memTo首地址
while (size -- > 0) { //迴圈size次,複製memFrom的值到memTo中
*tempTo++ = *tempFrom++ ;
}
return memTo;
}
複製程式碼
strcpy和memcpy主要有以下3方面的區別。
1、複製的內容不同。strcpy只能複製字串,而memcpy可以複製任意內容,例如字元陣列、整型、結構體、類等。
2、複製的方法不同。strcpy不需要指定長度,它遇到被複制字元的串結束符"\0"才結束,所以容易溢位。memcpy則是根據其第3個引數決定複製的長度。
3、用途不同。通常在複製字串時用strcpy,而需要複製其他型別資料時則一般用memcpy
11.size_t
size_t型別是一個型別定義,通常將一些無符號的整形定義為size_t,比如說unsigned int或者unsigned long,甚至unsigned long long。每一個標準C實現應該選擇足夠大的無符號整形來代表該平臺上最大可能出現的物件大小。
size_t的定義在<stddef.h>, <stdio.h>,<stdlib.h>,<string.h>,<time.h>和<wchar.h>這些標準C標頭檔案中,也出現在相應的C++標頭檔案, 等等中,在使用size_t之前應該標頭檔案中至少包含一個這樣的標頭檔案。
包含以上任何C標頭檔案(由C或C++編譯的程式)表明將size_t作為全域性關鍵字。
根據定義,size_t是sizeof關鍵字(注:sizeof是關鍵字,並非運算子)運算結果的型別。所以,應當通過適當的方式宣告n來完成賦值:
n = sizeof(thing);
引數中帶有size_t的函式通常會含有區域性變數用來對陣列的大小或者索引進行計算,在這種情況下,size_t是個不錯的選擇。適當地使用size_t還會使你的程式碼變得如同自帶文件。當你看到一個物件宣告為size_t型別,你馬上就知道它代表位元組大小或陣列索引,而不是錯誤程式碼或者是一個普通的算術值。
12.new 和malloc 的區別
相同點:都可用於申請動態記憶體和釋放記憶體
不同點:
- 申請的記憶體所在位置不同:new操作符從堆上為物件動態分配記憶體,malloc函式從自由儲存區上為物件動態分配記憶體。
- 記憶體分配失敗時的返回值不同:new記憶體分配失敗時,會丟擲bac_alloc異常,它不會返回NULL;malloc分配記憶體失敗時返回NULL。 在使用C語言時,我們習慣在malloc分配記憶體後判斷分配是否成功:
- 操作物件有所不同:malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子。對於非內部資料類的物件而言,光用maloc/free 無法滿足動態物件的要求。物件在建立的同時要自動執行建構函式,物件消亡之前要自動執行解構函式。由於malloc/free 是庫函式而不是運算子,不在編譯器控制許可權之內,不能夠把執行建構函式和解構函式的任務強加malloc/free。
- 用法不同:
-
函式malloc 的原型如下:
void * malloc(size_t size)
用malloc 申請一塊長度為length 的整數型別的記憶體,程式如下:
int *p = (int *) malloc(sizeof(int) * length)
1、malloc 返回值的型別是void *,所以在呼叫malloc 時要顯式地進行型別轉換,將void * 轉換成所需要的指標型別。
2、 malloc 函式本身並不識別要申請的記憶體是什麼型別,它只關心記憶體的總位元組數。
-
函式free 的原型如下:
void free( void * memblock )
為什麼free 函式不象malloc函式那樣複雜呢?這是因為指標p的型別以及它所指的記憶體的容量事先都是知道的,語句free(p)能正確地釋放記憶體。如果p 是NULL 指標,那麼free對p 無論操作多少次都不會出問題。如果p 不是NULL 指標,那麼free 對p連續操作兩次就會導致程式執行錯誤。
-
運算子new 使用起來要比函式malloc 簡單得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length)
int *p2 = new int[length]
這是因為new 內建了sizeof、型別轉換和型別安全檢查功能。對於非內部資料型別的物件而言,new 在建立動態物件的同時完成了初始化工作。如果物件有多個建構函式,那麼new 的語句也可以有多種形式。
如果用new 建立物件陣列,那麼只能使用物件的無引數建構函式。例如:
Obj *objects = new Obj[100]
// 建立100 個動態物件不能寫成
Obj *objects = new Obj[100](1)
// 建立100 個動態物件的同時賦初值1 -
在用delete 釋放物件陣列時,留意不要丟了符號‘[]’。例如:
delete []objects
// 正確的用法delete objects;
// 錯誤的用法後者相當於delete objects[0],漏掉了另外99 個物件。
-
綜上:
1、new自動計算需要分配的空間,而malloc需要手工計算位元組數
2、new是型別安全的,而malloc不是,比如:
int* p = new float[2]
// 編譯時指出錯誤
int* p = malloc(2*sizeof(float))
// 編譯時無法指出錯誤
new operator 由兩步構成,分別是 operator new 和 construct
3、operator new對應於malloc,但operator new可以過載,可以自定義記憶體分配策略,甚至不做記憶體分配,甚至分配到非記憶體裝置上。而malloc無能為力
4、new將呼叫constructor,而malloc不能;delete將呼叫destructor,而free不能。
5、malloc/free要庫檔案支援,new/delete則不要。
13.C++中的new/delete與operator new/operator delete
new operator/delete operator就是new和delete操作符,而operator new/operator delete是函式。
new operator:
(1)呼叫operator new分配足夠的空間,並呼叫相關物件的建構函式
(2)不可以被過載
operator new:
(1)只分配所要求的空間,不呼叫相關物件的建構函式。當無法滿足所要求分配的空間時,則
->如果有new_handler,則呼叫new_handler,否則
->如果沒要求不丟擲異常(以nothrow參數列達),則執行bad_alloc異常,否則
->返回0
複製程式碼
(2)可以被過載
(3)過載時,返回型別必須宣告為void*
(4)過載時,第一個引數型別必須為表達要求分配空間的大小(位元組),型別為size_t
(5)過載時,可以帶其它引數
14.C++的記憶體分配
在C++中,記憶體分成5個區,他們分別是:
-
堆:就是那些由new分配的記憶體塊,他們的釋放編譯器不去管,由我們的應用程式去控制,一般一個new就要對應一個delete。如果程式設計師沒有釋放掉,那麼在程式結束後,作業系統會自動回收。
-
棧:就是那些由編譯器在需要的時候分配,在不需要的時候自動清除的變數的儲存區。裡面的變數通常是區域性變數、函式引數等。
-
自由儲存區:就是那些由malloc等分配的記憶體塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。
-
全域性/靜態儲存區:全域性變數和靜態變數被分配到同一塊記憶體中,在以前的C++堆疊中,全域性變數又分為初始化的和未初始化的,在C++裡面沒有這個區分了,他們共同佔用同一塊記憶體區。
-
常量儲存區:常量儲存區,這是一塊比較特殊的儲存區,他們裡面存放的是常量,不允許修改(當然,你要通過非正當手段也可以修改,而且方法很多,取地址修改)
15.堆和棧的區別
-
管理方式不同
對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程式設計師控制,容易產生memory leak
-
空間大小不同
一般來講在32位系統下,堆記憶體可以達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,預設的棧空間大小是1M。(這個值可以通過編譯器修改)
-
能否產生碎片不同
對於堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個問題,因為棧是先進後出的佇列,他們是如此的一一對應,以至於永遠都不可能有一個記憶體塊從棧中間彈出,在他彈出之前,在他上面的後進的棧內容已經被彈出,詳細的可以參考資料結構。
-
生長方向不同
對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長。
-
分配方式不同
堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由 malloc 函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
-
分配效率不同
棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是 C/C++ 函式庫提供的,它的機制是很複雜的,例如為了分配一塊記憶體,庫函式會按照一定的演算法(具體的演算法可以參考資料結構/作業系統)在堆記憶體中搜尋可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就有機會分到足夠大小的記憶體,然後進行返回。顯然,堆的效率比棧要低得多。
16. 建構函式和解構函式可不可以為虛擬函式,為什麼?
建構函式不能為虛擬函式,而解構函式可以且常常是虛擬函式。
-
建構函式不能為虛擬函式
1)從儲存空間角度:
虛擬函式對應一個虛擬函式表vtable,這個vtable其實是儲存在物件的記憶體空間的。但是,如果建構函式是虛的,就需要通過vtable來呼叫,可是物件還沒有例項化,也就是記憶體空間還沒有,無法找到vtable,所以建構函式不能是虛擬函式。
即vtable是在建構函式呼叫後才建立,因而建構函式不可能成為虛擬函式。
2)從使用角度:
虛擬函式主要用於在資訊不全的情況下,能使過載的函式得到對應的呼叫。建構函式本身就是要初始化例項,那使用虛擬函式也沒有實際意義,所以建構函式沒有必要是虛擬函式。
虛擬函式的作用在於通過父類的指標或者引用來呼叫它的時候能夠變成呼叫子類的那個成員函式。而建構函式是在建立物件時自動呼叫的,不可能通過父類的指標或者引用去呼叫,因此也就規定建構函式不能是虛擬函式。
-
解構函式可以是虛擬函式,且常常如此
這個就好理解了,因為此時vtable已經初始化了;況且我們通常通過基類的指標來銷燬物件,如果解構函式不為虛的話,就不能正確識別物件型別,從而不能正確銷燬物件。
在類的繼承中,如果有基類指標指向派生類,那麼用基類指標delete時,如果不定義成虛擬函式,派生類中派生的那部分無法析構。在類的繼承體系中,基類的解構函式不宣告為虛擬函式容易造成記憶體洩漏。所以如果你設計一定類可能是基類的話,必須要宣告其為虛擬函式。
17.如何限制一個類物件只能在堆(棧)上分配空間
在C++中,類的物件建立分為兩種
一種是靜態建立,如A a
另一種是動態建立,如A* ptr=new A
這兩種方式是有區別的:
靜態建立類物件: 是由編譯器為物件在棧空間中分配記憶體,是通過直接移動棧頂指標,挪出適當的空間,然後在這片記憶體空間上呼叫建構函式形成一個棧物件。使用這種方法,直接呼叫類的建構函式。
動態建立類物件: 是使用new運算子將物件建立在堆空間中。這個過程分為兩步,第一步是執行operator new()函式,在堆空間中搜尋合適的記憶體並進行分配;第二步是呼叫建構函式構造物件,初始化這片記憶體空間。這種方法,間接呼叫類的建構函式。
-
只能在堆上分配類物件——就是不能靜態建立類物件,即不能直接呼叫類的建構函式。 當物件建立在棧上面時,是由編譯器分配記憶體空間的,呼叫建構函式來構造棧物件。當物件使用完後,編譯器會呼叫解構函式來釋放棧物件所佔的空間。編譯器管理了物件的整個生命週期。如果編譯器無法呼叫類的解構函式,情況會是怎樣的呢?比如,類的解構函式是私有的,編譯器無法呼叫解構函式來釋放記憶體。所以,編譯器在為類物件分配棧空間時,會先檢查類的解構函式的訪問性,其實不光是解構函式,只要是非靜態的函式,編譯器都會進行檢查。如果類的解構函式是私有的,則編譯器不會在棧空間上為類物件分配記憶體。
因此,將解構函式設為私有,類物件就無法建立在棧上了。
程式碼如下:
class A { public: A(){} void destory(){delete this;} private: ~A(){} }; 複製程式碼
試著使用A a;來建立物件,編譯報錯,提示解構函式無法訪問。這樣就只能使用new操作符來建立物件,建構函式是公有的,可以直接呼叫。類中必須提供一個destory函式,來進行記憶體空間的釋放。類物件使用完成後,必須呼叫destory函式。
上述方法的缺點:
-
無法解決繼承問題。
如果A作為其它類的基類,則解構函式通常要設為virtual,然後在子類重寫,以實現多型。 因此解構函式不能設為private。
還好C++提供了第三種訪問控制,protected。 將解構函式設為protected可以有效解決這個問題,類外無法訪問protected成員,子類則可以訪問。
-
類的使用很不方便
使用new建立物件,卻使用destory函式釋放物件,而不是使用delete。 (使用delete會報錯,因為delete物件的指標,會呼叫物件的解構函式,而解構函式類外不可訪問。這種使用方式比較怪異。)
為了統一,可以將建構函式設為protected,然後提供一個public的static函式來完成構造,這樣不使用new,而是使用一個函式來構造,使用一個函式來析構。
程式碼如下,類似於單例模式:
class A { protected: A(){} ~A(){} public: static A* create() { return new A(); } void destory() { delete this; } }; 複製程式碼
這樣,呼叫create()函式在堆上建立類A物件,呼叫destory()函式釋放記憶體。
-
-
只能在棧上分配類物件
只有使用new運算子,物件才會建立在堆上,因此,只要禁用new運算子就可以實現類物件只能建立在棧上。 雖然你不能影響new operator的能力(因為那是C++語言內建的),但是你可以利用一個事實:new operator 總是先呼叫 operator new,而後者我們是可以自行宣告重寫的。
因此,將operator new()設為私有即可禁止物件被new在堆上。 程式碼如下:
class A { private: void* operator new(size_t t){} // 注意函式的第一個引數和返回值都是固定的 void operator delete(void* ptr){} // 過載了new就需要過載delete public: A(){} ~A(){} }; 複製程式碼
18.拷貝建構函式如果用值傳遞會有什麼影響?
引數為引用,不為值傳遞是為了防止拷貝建構函式的無限遞迴,最終導致棧溢位。
如果拷貝建構函式中的引數不是一個引用,即形如CClass(const CClass c_class)
,那麼就相當於採用了傳值的方式(pass-by-value),而傳值的方式會呼叫該類的拷貝建構函式,從而造成無窮遞迴地呼叫拷貝建構函式。因此拷貝建構函式的引數必須是一個引用。
需要澄清的是,傳指標其實也是傳值,如果上面的拷貝建構函式寫成CClass(const CClass* c_class)
,也是不行的。事實上,只有傳引用不是傳值外,其他所有的傳遞方式都是傳值。