1. 程式人生 > >C++ 指標與作用域

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++中的記憶體管理學問太深,這篇部落格是小弟個人拙見,也許漏洞百出,還是希望各路高手指教,不勝感激!