C++ 指標與作用域
C/C++中的指標操作是一個令人抓狂的問題,這幾天在溫習林銳的《高質量C++C程式設計指南》,裡面的記憶體管理這一章對我受益匪淺。看到下面的一段內容,不禁和作者提出相同的疑問:該程式不出錯是因為編譯器的原因嗎?並在網上查詢相關資料。
原始碼1:
#include <iostream> using namespace std; class A { public: void Func(void) { cout<<"Func of class A"<<endl; } }; void Test(void) { A *p; { A a; p = &a; // 注意 a 的生命期 } p->Func(); // p是“野指標” } int main() { Test(); return 0; }
為了探索這個問題,我分別在CodeBlocks、VC++6.0和VS2010上執行這段程式碼,發現並無錯誤,所以,這個並非編譯器的原因,那麼到底是什麼原因呢?
1、我們知道在Test函式中內部用花括號括起來的部分是一個區域性作用範圍,因此,該語句塊擁有區域性的作用域,在裡面定義的區域性物件a,是一個存放在棧中的自動變數,一旦離開了這個作用域(離開花括號),就不復存在了。可以通過簡單的單步除錯,跟蹤物件a的變化,實踐證明,這個想法是對的。
2、一般來說,一個物件所佔的空間大小隻取決於該物件中資料成員所佔的空間,而與成員函式無關。通過下面一段程式碼可以驗證
原始碼2:
#include <iostream> using namespace std; class A { int i; char chr; }; class B { int i; char chr; void fun() { int j = 1; cout<<j<<endl; } }; int main() { A a; B b; cout<<"sizeof A = "<<sizeof(a)<<endl; cout<<"sizeof B = "<<sizeof(b)<<endl; return 0; }
輸出為:
,在類A和類B中都有一個int和char型別的成員變數,根據記憶體對齊,取其中較大的int變數的位元組數4作為倍數,所以sizeof(A) = 2 * 4 = 8位元組。儘管類B中有成員函式fun,但是該物件b所佔用空間並沒有計算fun函式的空間。
3、那麼成員函式放在哪裡?物件是怎麼呼叫成員函式的呢?
先來看一個圖片:
通過圖片可以看出,物件例項的成員函式(非靜態成員函式)有一個公用函式程式碼儲存空間,物件通過this指標呼叫公用函式程式碼中的成員函式,這樣可以減少儲存空間的使用。
小結:1) 不論成員函式在類內定義還是在類外定義,成員函式的程式碼段都用同一種方式儲存;
2) 函式程式碼是儲存在物件空間之外的;
3) 通常所說的“某某物件的成員函式”,是從邏輯的角度而言的;而成員函式的儲存方式,是從物理的角度而言的,二者是不矛盾的。
4、返回原來討論的問題,既然成員函式存放在公用函式程式碼段裡面,而在原始碼1中,指標p 是儲存了在花括號裡面的物件a的地址的,物件a銷燬了,我們不能使用變數a,但是它的地址是不會改變的,編譯器只是銷燬了物件a這個變數,並沒有銷燬物件a原來所在儲存區域(當然是在棧裡面的儲存區域)中的成員變數和成員函式,所以,這裡得到的指標p,還是指向花括號中定義的物件a的首地址,這就是ptr就是原來的物件a的this指標,當使用p->Func();語句呼叫成員函式的時候,自然就可以通過this指標在公用函式程式碼段中找到對應的成員函式,執行該函式(我猜想在公用函式程式碼段中應該維護了一個this指標與成員函式的對照表)。
5、錯誤的呼叫返回了正確的結果,著實令人頭痛。顯然,應該堅決杜絕這樣的呼叫方法。對於野指標(不是NULL指標,是指向被釋放的或者訪問受限記憶體的指標),我們使用delete或者free釋放儲存空間的時候,把指標p賦值為NULL是一個好習慣,這樣,通過if(p != NULL)語句就可以判斷該指標是否是合法的。
擴充套件:
上述研究的內容是針對一個物件例項的,假設是一個int型別或者字串型別,結果是否一樣呢。來看一段程式碼。
原始碼3:
#include <iostream>
using namespace std;
char *getstr(int flag)
{
if(flag == 1)
{
char *str = "12345";//字串"12345"存放在文字常量區
return str;
}
else
{
char str[] = "ABCDE";//字元陣列
return str;
}
}
int *getint()
{
int i = 2014;
return &i;
}
int main()
{
//字串型別測試
cout<<"字串型別測試(字串):"<<getstr(1)<<endl;
cout<<"字串型別測試(字元陣列):"<<getstr(2)<<endl;
//char *str = getstr(2);//使用指標str儲存返回的getstr(2)指標,打印出來的是亂碼,說明指標沒有正確返回
//使用getstr(2)列印確實正常的,如下:
for(int i = 0; i < 6; i++)
{
cout<<"str"<<i<<": "<<getstr(2)[i]<<endl;
}
cout<<endl<<"****************分割線******************"<<endl<<endl;
//整型型別測試
cout<<"整型型別測試:"<<*getint()<<endl;
return 0;
}
執行結果(VC++6.0):
1) 對於整型變數來說,與之前的說法是一致的,getint方法中變數i在退出函式時便不能使用了,但是返回的地址所指向的儲存區域內容並沒有改變依舊是2014;
2) char *str = "12345";這種定義方式的字串"12345"是存放在文字常量區的,該字串不能修改,其生命週期和整個程式的生命週期一樣長,但是依舊不建議這麼編寫程式碼。
3) char str[] = "ABCDE";這裡定義的是一個字元陣列,這是一種直觀的初始化方式,注意這樣不代表字串"ABCDE"是存放在文字常量區,而是等價於char str[] = {'A', 'B', 'C', 'D', 'E', '\0'};;離開了getstr函式,字元陣列str便不復存在,這裡返回的指標不一定是原先字串"ABCDE"的首地址,這裡打印出來的地址是亂碼,但是單個取值卻沒有問題。個人猜想:陣列名和指標不是一個概念,getstr函式中分配字元陣列空間的時候,還要儲存資料的型別等資訊,返回的也是陣列名str,然後離開了getstr函式,陣列str不存在了,返回的“陣列名”是沒有意義的,因此打印出來的是亂碼。
總結:
離開了作用域的變數,變數被銷燬,變數所在儲存區域的內容並沒有被系統自動實時釋放和回收;也許該儲存區域只是做了“可分配”標記。
注:不能因為系統沒有實時回收這些變數而存在僥倖心理,熟練使用指標並對記憶體管理有清晰的理解才是王道。
C/C++中的記憶體管理學問太深,這篇部落格是小弟個人拙見,也許漏洞百出,還是希望各路高手指教,不勝感激!