(被調)函數內的局部變量在函數返回時被釋放,不該被外部引用。雖然並不是真正的釋放,經過內存地址仍可能訪問該棧區變量,但其安全性不被保證。後續若還有其餘函數調用,則其局部變量可能覆蓋該棧區內容。常見狀況有兩種:前次調用影響當前調用的局部變量取值(函數的"遺產");被調函數返回指向棧內存的指針,主調函數經過該指針訪問被調函數已釋放的棧區內容(召喚亡靈)。數組
【示例1】前後連續調用Ancestor和Sibling函數,注意函數內的dwLegacy整型變量。安全
1 void Ancestor(void){ 2 int dwLegacy = 42; 3 } 4 void Sibling(void){ 5 int dwLegacy; 6 printf("%d\n", dwLegacy); 7 } 8 int main(void){ 9 Ancestor(); 10 Sibling(); 11 return 0; 12 }
若使用普通編譯(如gcc test.c),則輸出42,由於編譯器重用以前函數的調用棧;若打開優化開關(如gcc -O test.c),則輸出一個隨機的垃圾數,由於Ancestor函數將被優化爲空函數,也不會被main函數調用。ide
所以,爲避免這種干擾,建議聲明自動局部變量時對其顯式賦初值(初始化)。函數
【示例2】前後調用Ancestor和Sibling函數,注意函數內的aLegacy數組變量。測試
1 void Ancestor(void){ 2 int aLegacy[10], dwIdx = 0; 3 for(dwIdx = 0; dwIdx < 10; dwIdx++) 4 aLegacy[dwIdx] = dwIdx; 5 } 6 void Sibling(void){ 7 int aLegacy[10], dwIdx = 0; 8 for(dwIdx = 0; dwIdx < 10; dwIdx++) 9 printf("%d ", aLegacy[dwIdx]); 10 }
若使用普通編譯,則輸出0 1 2 3 4 5 6 7 8 9(Ancestor函數內的數組賦值會影響Sibling函數的數組初值);若打開優化開關,則輸出一串隨機的垃圾數。優化
【示例3】連續調用兩次Func函數。spa
1 void Func(void){ 2 char acArr[25]; 3 printf("%s ", acArr); //注意此句打印結果 4 acArr[0]= 'a'; acArr[1] = 'b'; acArr[2] = 'c'; acArr[3]= '\0'; 5 printf("%s ", acArr); 6 } 7 void FuncInsert(void){char acArr[25] = {0};}
若使用普通編譯,則輸出(亂碼) abc abc abc;若打開優化開關,則輸出(空串) abc abc abc。操作系統
若在兩次調用中間插入其餘函數調用(如FuncInsert),則使用普通編譯時輸出(亂碼) abc (空串) abc;若打開優化開關時仍輸出(空串) abc abc abc(FuncInsert函數被優化掉)。指針
【示例4】Specter函數返回局部變量dwDead的地址,main函數試圖打印該地址內容。code
1 int *Specter(void){ 2 int dwDead = 1; 3 return &dwDead; //編譯器將提出警告,如function returns address of local variable 4 } 5 int main(void){ 6 int *pAlive = Specter(); 7 printf("*pAlive = %d\n", *pAlive); 8 return 0; 9 }
若使用普通編譯,則輸出* pAlive = 1;若打開優化開關,則Specter函數跳過賦值語句直接返回dwDead變量地址,故輸出*p = (隨機的垃圾數)。
注意,Specter函數返回值(地址)存放在%eax寄存器內,main函數讀取寄存器值,將其做爲內存地址訪問該地址處的存儲內容——該內容極可能並未初始化,或即將被新的調用棧覆蓋!
【示例5】GetString函數返回局部字符數組szStr的地址,main函數試圖打印該地址內容。
1 char *GetString(void){ 2 char szStr[] = "Hello World"; //此句後增長printf("%s\n", szStr);可防止賦值被優化掉 3 return szStr; //編譯器將提出警告,如function returns address of local variable 4 } 5 int main(void){ 6 char *pszStr = GetString(); //pszStr指向"Hello World"的副本 7 8 //GetString函數返回後,嘗試輸出GetString函數內局部字符數組szStr的內存內容 9 #ifdef LOOP_COPY 10 unsigned char ucIdx = 0; 11 char szStackStr[sizeof("Hello World")] = {0}; 12 for(ucIdx = 0; ucIdx < sizeof("hello world"); ucIdx++) 13 szStackStr[ucIdx] = pszStr[ucIdx]; 14 printf("szStackStr = %s\n", szStackStr); //原szStr處的內容,"Hello World" 15 #endif 16 #ifdef MEMCOPY_CALL //當內存拷貝函數內部無局部或臨時變量時,可用該法 17 char szStr[sizeof("Hello World")] = {0}; 18 memcpy(szStr, pszStr, sizeof(szStr)); 19 printf("szStr = %s\n", szStr); 20 #endif 21 #ifdef CHAR_PRINT 22 printf("pszStr = %c%c%c%c%c%c%c%c%c%c%c%c\n", \ 23 pszStr[0],pszStr[1],pszStr[2],pszStr[3],pszStr[4],pszStr[5], \ 24 pszStr[6],pszStr[7],pszStr[8],pszStr[9],pszStr[10],pszStr[11]); 25 #endif 26 #ifdef JUNK_PRINT 27 printf("pszStr = %s\n", pszStr); //當前pszStr處的內容,垃圾 28 #endif 29 return 0; 30 }
調用GetString函數時,將只讀數據段存放的字符串常量"Hello World"拷貝至堆棧臨時分配的字符數組szStr,即szStr指向該字符串的可讀寫副本。函數返回szStr地址,同時棧頂指針下移以保證堆棧指針平衡。此時如有函數調用或單步跟蹤(軟中斷也使用堆棧),則可能覆蓋szStr所指向的內存。爲保留和查看棧區szStr處的內容,可採用示例中的LOOP_COPY、MEMCOPY_CALL或CHAR_PRINT方法(爲避免相互影響,三者中應任選一個)。
若使用普通編譯,則三種方法都可輸出"Hello World";若打開優化開關且在GetString函數返回前添加輸出szStr內容的語句(以防賦值被跳過),則三種方法仍可輸出"Hello World"。這也證實GetString函數調用返回後,堆棧內存szStr處的內容並未清除。
注意,JUNK_PRINT不管何種編譯方式均輸出亂碼。
另見下面的代碼片斷:
測試1 |
測試2 |
測試3 |
//採用return返回動態內存地址 char* GetMemory1(char *p, int size){ p = (char *)malloc(size);* return p; } void Test1(void){ char *str = NULL; str = GetMemory1(str, 100); strcpy(str, "Hello\n"); printf(str); free(str); } |
//採用二級指針返回動態內存地址 void GetMemory2(char **p,int size){ *p = (char *)malloc(size); } void Test2(void){ char *str = NULL; GetMemory2(&str, 100); strcpy(str, "Hello"); printf(str); free(str); if(str != NULL)* strcpy(str,"World\n"); printf("%s", str); } |
//正確返回只讀字符串地址,但無心義(沒法修改內容) char* GetMemory3(void){ char *p = "Hello World";* return p; } void Test3(void){ char *str = NULL; str = GetMemory3(); printf(str); } |
Test1輸出Hello 【注*】malloc函數返回void*指針,但C++不容許void*隱式轉換到任意類型指針(須要static_cast)。故建議以下兼容寫法: T* p = (T*)malloc(size * sizeof(*p));或 T* p = (T*)malloc(size * sizeof(T)); |
Test2輸出Hello World 【注*】進程中內存管理由庫函數完成。當釋放內存時,一般不會將內存歸還給操做系統,故可繼續訪問該地址。但因其已被」回收」,若輸出語句前再次分配內存,則同段空間可能被從新分配給其餘變量,形成錯誤。 |
Test3輸出Hello World 【注*】此處若寫爲char p[] = "Hello World";則返回無效指針,輸出不肯定。 |