2011/10/31

從 C#/VB 開發者的角度解析 C++ 中的指標

最近在維護一個 C++ 專案,在迫不得已的情況下重新溫習了已經二十幾年沒踫的指標 (Pointer)。而且,很不幸的,跟二十幾年前一樣被搞得暈頭轉向 (在 C# 跟 VB 中根本不會有這種問題),所以只好花點時間把 C++ 指標相關重點摘錄起來供自已及有興趣的朋友們參考。
首先,我把 C++ 跟 C#/VB 在這方面的根本差異處先條列在前面。沒有學過 C++ 跟沒有學過 C#/VB 的人同樣可以把下列重點當作本文的前題說明。
  1. C++ 並不像 C#/VB 一樣會做嚴格的記憶體位址的 boundary check。換句話說,在 C++ 中你可以很容易的讀寫未經正式配置的記憶體位址 (雖然有時候會被 runtime check 攔截並發出 exception)。在 C#/VB 中絕對不會允許這種事情發生 (應該也辦不到 - 除非你寫 unmanaged code)。
  2. 在 C++ 程式中,你必須很謹慎的管理你的變數,否則你很容易在不小心的情形下在 heap 中留下未受控制 (orphaned) 的記憶體區塊,從而造成 memory leak 的情況。同樣的,這種事情在 C#/VB 中也不會發生。
  3. 在 C++ 中各種型別並不像 C#/VB 一樣區分得很清楚。在 C#/VB 中型別之間的轉換在 compiler 這一關就會被嚴格檢驗,在 C++ 並不會。例如你可以使用 *pStrArray 取得 *pStrArray[0] 的值 (是的,pStrArray 是個指向陣列資料的指標),甚至改寫其值。如果你的同事寫程式都不按照規範的話 (例如他總是學不會把指標變數取一個以 p 開頭的名字),我想你絕對不會很樂意去維護他的程式的。
  4. 在 C++ 中,char* 的指標和其它型別的指標在行為上有很大的差異。在其它型別中 (例如一個指向 int* 的指標 pInt),使用 *pInt 即可取得資料,使用 pInt 即可取得資料的位址,對 char* 指標卻完全不是這回事。要取得 char* 指標所指向的資料所在的位址必須透過迂迴的方式,已列在後面的範例程式中。
  5. 傳統 C++ 和 C++/CLI (即從 CLR 樣版頁籤下新增的專案類型) 在許多地方有相當大的差異,和 C#/VB 也幾乎完全不一樣。讀者應該仔細分辨以免搞混。

表面上 C++ 程式語法雖然接近 C#,但是事實上它與 C# 差異真的很大。我覺得寫過組合語言的人反而比較容易學會操控 C++;所以一開始就接觸高階語言 (C#/VB/Java) 的人若要學習 C/C++,恐怕會面臨一段很長、很痛苦的 learning curve。或許倒過來還容易一些。

宣告指標

語法:
  • int* pNumber; // 可以把星號標在 int 後面
  • int *pNumber; // 也可以把星號標在變數名稱前面
  • int *pNumber, number = 5;
    // 可以在同一行中同時宣告一個指標變數跟一個普通變數;但如果你選擇把星號標在 int 後面,建議你還是分做兩行吧!
  • int *pNumber = NULL; // 宣告這個指標並未指向任何值
  • int *pNumber;
    pNumber = NULL; // 這兩行程式和上面 int *pNumber = NULL; 是同樣的意思
  • int *pNumber = 0; // 此種寫法與 *pNumber = NULL 一樣。注意:不能使用 0 以外的任何數字當做初始值。

經由如上的宣告,pNumber 代表一個指向整數資料的指標 (pointer)。星號靠近 int 或靠近變數名稱都可以,但是我個人建議你把星號緊緊標在變數名稱 (如上例中的 pNumber) 的前面。此外,既然這個變數是個指標,那麼你最好讓這個變數名稱以 p 開頭。這將使得你在任何時候一看見這個變數就知道它是個指標變數。普通變數和指標變數的行為差異很大,你不會喜歡隨時都在懷疑程式中某一個變數到底是不是一個指標變數的。

取址運算子 "&"

語法:
  • int *pNumber = NULL, number = 5; // 宣告一個指標變數跟一個普通變數
    pNumber = &number; // 使用取址運算子 & 取得 number 變數的位址並賦予 pNumber,現在這兩個變數其實已指向同一個位址
    cout << *pNumber << endl; // 此時 pNumber 已指向 number 變數的位址,再使用 *pNumber 取得其值
    cout << hex << &number << endl; // 以十六進位觀察 number 的位址
    cout << hex << pNumber << endl; // 以十六進位觀察 pNumber 的值,應該與上值相同
在 C++/CLI 程式裡面並沒有 NULL 可以使用;所以你只能使用如下的寫法:
  • int *pNumber = 0; 

解參考運算子 "*"

解參考(de-reference)運算子也稱為間接運算子,因為程式必須先取得一個「位址」,再去取這個位址所儲存的「值」,所以是一種間接取值的方式。我們已經在宣告指標變數的語法中看過這個運算子了,只是解釋起來方向略有不同而已。
語法:
  • int *pNumber = NULL, number = 5; // 宣告一個指標變數跟一個普通變數
    pNumber = &number; // 使用取址運算子 & 取得 number 變數的位址並賦予 pNumber,現在這兩個變數其實已指向同一個位址
    cout << *pNumber << endl; // 此時 pNumber 已指向 number 變數的位址,再使用 *pNumber 取得其值
  • int number = 5;
    int *pNumber = &number; // 也可以寫成這樣

在第一例中,pNumber 一開始是指向 NULL (亦即 0),也就是代表它並未指向任何地方。在這個時候,如果你下達 *pNumber = 1; 這樣的指令將引發錯誤。但是,當你下達 pNumber = &number; 指令之後,pNumber 已經正式指向某一個位址。這時再下達 *pNumber = 1; 這行指令就不會引發錯誤。
在 C++ 中要判斷一個指標變數是不是 NULL,可以使用如下的判斷式:
  • if (pNumber == NULL) // 或者
  • if (pNumber == 0) // 或者
  • if (!pNumber)

同樣的,在 C++/CLI 程式中第一種寫法是錯誤的。

為何需要指標?

為何需要指標?我認為這是一個很難回答的問題。在 C#/VB/Java 裡面是沒有指標的 (其實並不是真的沒有,只是我們通常不會用到),我們照樣可以把事情辦好。然而,我在前面提過,如果你寫過組合語言的話,你就比較容易體會出使用指標的感䁷。這種感覺有點像我們在使用 delegate 的樣子。大部份使用 delegate 的場合都可以以不使用  delegate 的方法取代,但是一旦你習慣使用 delegate 的話,你並不會認為使用 delegate 是什麼難以理解或多此一舉的做法。
C 跟 C++ 是介於組合語言與其它高階語言中間的一種比較偏低階的語言。它的發展背景處於軟體資源與硬體效能比較受限的年代,那時候的語言開發者都是習慣於組合語言寫法的工程師 (至少我是這麼認為),而組合語言中通常是大量使用指標的。所以我們不太容易在 C/C++ 裡面看到高階語言裡面那些可以隨意配置動態記憶體,或者可以隨便使用 linked list 串來串去的資料結構。C/C++ 似乎較少套用那種大格局的程式寫法,但是它的彈性其實是比較大的。因此,即便 .Net 已經發展了十年,我們在市面上看到的大多數重要的產品仍然是使用 C++ 所開發的。

字串指標

在 .Net 程式中,字串是個特別的型別,在 C++ 中也是。只不過在 C++ 中對於字串的處理方式恐怕不是習慣於 C#/VB 程式的人可以一下子就搞懂的。
語法:
  • char *pStr("A great failure is better than an excusable absence."); // 或者
  • char *pStr = "A great failure is better than an excusable absence.";
    cout << pStr << endl; // 輸出 "A great failure is better than an excusable absence." 這串字
    cout << strlen(pStr) << endl; // 使用 strlen 函式得到字串長度 (不包含尾巴的 \0 字元)

是的,你沒看錯,我對一個字元指標賦予一個字串初值。寫 C#/VB 程式的人應該沒辦法理解為什麼我對一個型別為 char 的變數賦予了字串 (或者說是字元陣列) 的初值。然而在 C++ 裡面就是這樣。
事實上,如果你把 *pStr 這個字最前面的星號拿掉,這行指令就沒辦法編譯。因此,正確地講,我是對一個型別為 char 的指標變數賦予了字串 (或者說字元陣列) 的初值。
如果你夠細心的話,你或許會留意到,我在上面舉過的例子中提到,指標變數在宣告時不能同時賦予一個常數值 (例如 int *pNumber = 5; 這道指令是錯的)。那為什麼在這裡卻又可以?
這是因為 C++ runtime 會自動幫你建立起初始值的一串常數字元陣列 (而且會自動在這字串的最後面補上一個 \0 表示結尾),然後把其位址指向這個指標變數。這可是 char* 型別的指標變數才能享有的特殊禮遇,其它型別的指標變數都沒有 (請乖乖使用 new 指令 - 後面會介紹)。
或許你也同時發現 cout << pStr << endl; 這行指令似乎有點玄機。是的,如果這不是一個 char* 變數,而是一個 int* 變數的話,它的輸出應該是該指標變數中儲存的位址。但是對 char* 變數而言,它卻會把它所指向的整個字串輸出。
此外,在本例中我們使用 strlen 函式以得到字串的長度,該函式的邏輯是從字串開頭一直計數到發現 \0 字元為止的累積長度。但是如果此處的 pStr 指標已經指向一個不正確的地方,那麼使用這個函式的結果有可能是錯誤、甚至有點糟糕的。那麼,什麼時候指標會指向「不正確」的地方呢?我在下面會進一步說明。
如果在本例程式中你使用了中文字串的話,使用 strlen 得到的會是 byte 數而不是字元數。在 C++ 中要使用計算中文字串的長度,必須使用 wchar_t 型別而不是 char 型別;記算字元數的函式也不能使用 strlen 而必須使用 wcslen,如下例:
  • wchar_t *pChStr = L"點部落";
    cout << wcslen(pChStr) << endl; // 結果是 3
對於中英混雜的字串,也必須使用 wchar_t / wcslen 才能得到字元數,否則都會得到 byte 數。

指標與陣列的混用

上例中的 pStr 雖然是一個以 char* 型別宣告的指標變數,你卻可以把它當作 char array 來使用:
  • for (int i = 0; pStr[i] != 0; i++)
       cout << pStr[i]; // 輸出 "A great failure is better than an excusable absence." 這串字
把指標變數拿來當作陣列來用,並不是 char* 指標變數的專利。所有指標變數都可以拿來這樣用,如下例:
  • int *pNumber = NULL, number = 5;
    pNumber = &number;
    cout << pNumber << endl; // 輸出值為 pNumber 的值,亦即真正值的位址
    cout << *pNumber << endl; // 輸出值為 5
    cout << pNumber[0] << endl; // 輸出值為 5
    cout << pNumber[1] << endl; // 輸出值為隨機的無意義數字
可以這樣用,並不表示這樣用是對的。在 C++ 中顯然並沒有在陣列和非陣列之間做嚴格的區分,所以你可以使用這種方法取得預期之外的記憶體內容。但如果你是一個嚴謹的開發者,這種做法是應該極力避免的。
我們再來看看把指標變數指向傳統陣列的情況:
  • double *pData, data[3] = { 0.1, 0.2, 0.3 };
    pData = data; // 傳統陣列變數本身也是指標,所以你不能再使用 &data 來取得它所指向的資料的位址
    cout << pData[0] << endl; // 輸出 0.1
    cout << pData[1] << endl; // 輸出 0.2
    cout << pData[2] << endl; // 輸出 0.3
    cout << pData[3] << endl; // 輸出奇怪的數字
在本例中,data 本身也是一個指標,雖然它並沒有使用間接運算子來做宣告。你可以試試看把這個程式中最後四行中的 pData 改成 data,結果是完全一樣的。
仿上例,如果我們把這個指標變數指向陣列中的某個元素的位址,情況又是如何?
  • double *pData, data[3] = { 0.1, 0.2, 0.3 };
    pData = &data[0]; // data[0] 已經是資料了,所以你可以透過取址運算子取得它的位址
    cout << *pData << endl; // 輸出 0.1
    cout << *(++pData) << endl; // 輸出 0.2
    cout << *(++pData) << endl; // 輸出 0.3
    cout << *(++pData) << endl; // 輸出奇怪的數字
在這個示例中,我們可以使用這個技巧取得陣列的下一個元素。由於這裡 pData 和 data 是同屬 double 型別的變數 (大小為 8),所以我們自然可以使用指標變數的運算取得下一個元素的位址值。但這並不代表我們一定得採用這個方法才能在陣列元素中巡覽 - 直接使用 data[i] 來取值不是更快更直接嗎?
從以上的例子中,我們不難得到一個結論,也就是陣列實際上可以當作一個指標來對待。我們再看一個例子:
  • char pStr1[10] = "Johnny", *pStr2 = " Lee";
    strcat(pStr1, pStr2);
    cout << pStr1 << endl; // 這兩行也可以簡化為 cout << strcat(pStr1, pStr2) << endl; 一行
在這裡我宣告了一個字元陣列 pStr1 與一個字元指標 pStr2。經由 strcat 函式,我把 pStr2 接到 pStr1 的後面,輸出結果為 "Johnny Lee"。不過這個程式雖然可以執行,卻通不過 run-time check,主要是因為連起來的字串長度超過原來的宣告空間。若把開頭的 pStr1[10] 改成 pStr1[11] 就可以了 ("Johnny Lee" 佔了十個字元,加上最後面的 \0 應該是11個字元)。
有趣的是,如果你把第一行程式改成 char *pStr1 = "Johnny", *pStr2 = " Lee"; 則雖然可以通過編譯,卻根本無法執行。主要是因為 char *pStr1 = "Johnny" 等同於 char pStr1[7] = "Johnny",所以發生錯誤的原因基本上是一模一樣的,雖然發出錯誤的機制看起來有點不同。
看到這裡,C#/VB 使用者恐怕會覺得不可思議。我相信已經有很多人對於到底何時該加上或不該加上 "*" 這個解參考運算子感到迷糊了。但容我再強調一次,在 C++ 程式裡就是這樣!這實在不是一個難或不難、合理或不合理的問題;只要你的觀念夠清楚,剩下的就只是單純的習慣不習慣的問題而已。

指標陣列

前面談的都是把指標指向陣列;那麼,如果一個陣列本身就是由指標變數所組成的 (特別是 char* 陣列),那又是什麼情況?
語法:
  • char *pStr[] = { "a", "b", "c" }; // 宣告一個指標陣列且賦予初始值
    cout << "\"" << pStr[0] << "\"" << endl; // "a"
    cout << "\"" << pStr[1] << "\"" << endl; // "b"
    cout << "\"" << pStr[2] << "\"" << endl; // "c"
    cout << "Size of long integer is " << sizeof(long) << endl; // 對照一下一個 long integer 的大小
    cout << "pStr has " << (sizeof pStr) / (sizeof pStr[0]) << " members" << endl; // 必須使用迂迴的方式才能得出陣列的元素個數
在本例中,使用 pStr[0] 可以得到第一個元素中的字串,pStr[1] 得到第二個,依此類推。但是使用 sizeof pStr 卻會得到 pStr 中元素個數 (3) 乘以指標的值 (即位址) 的大小 (亦即 long 整數的大小, 4),結果就是 12,而不是 "a", "b", "c" 三個字串加在一起所佔的空間大小。然而,我們並不能從程式中立即取得這個指標陣列內含幾個元素,而是必須使用倒推的方式,即使用 (sizeof pStr) / (sizeof pStr[0]) 以推算 pStr 裡面到底有幾個元素。
你必須自已把本例程式拿去 VS 上面 (開一個 Win32 的空白專案) 執行一下,才會明白我在上面到底寫些什麼。如果你只是用看的,你大概會以為我是在繞口令,並且害你很快速的睡著。
接下來,我要繼續介紹如何動態配置記憶體的方法。但是在開始之前,我要先把 C++ 對於記憶體的分配原則說明一下。

C++ 對記憶體的運用

在一個 C++ 程式中,記憶體可以分為四塊:
  • 程式碼區域 (Code Area) - 編譯好的可執行程式碼存放的地方
  • 全域記憶體 (Global Area) - 存放全域變數的地方
  • Heap (也稱為 Free Store) - 動態配置的變數存放的地方
  • Stack (也稱為 Call Stack) - 程式內參數與局部變數存放的地方

請記得,程式中所有變數所使用的記憶體都必須透過 C++ run-time 的管理。如果你讓某塊記憶體不再受到管理,就會造成 memory leak。如果你只是寫一些測試用的小程式,那麼 memory leak 不會造成什麼問題,但當你寫的程式愈來愈大、愈來愈重要時,就很可能在你無法預期的時候把記憶體吃光,甚至造成其它程式 (包括作業系統) 的大問題。所以建議你不妨把這一點好好記在心裡。

動態記憶體配置

我們在前面所舉出的例子中,多半使用常數值。然而常數值在實際應用上,用處實在並不大。要讓程式的彈性變大、應用範圍變廣,一定要能動態配置記憶體。
語法:
  • int *pInt = NULL;
    pInt = new int(10); // 使用 new 配置一個 int 型別的值,並將其設為 10
    cout << *pInt; // 輸出 10
    delete pInt; // 使用 delete 歸還配置的記憶體
  • char *pStr = new char[10] ;
    cout << "pStr points to " << static_cast<void *>(pStr) << endl; // 輸出 pStr 位址
    char *pStr1 = pStr;
    cout << "pStr1 points to " << static_cast<void *>(pStr1) << endl; // 輸出 pStr1 位址
    *pStr1 = 'J'; // 把開頭字元變成 J 以方便辨識
    cout << "pStr = " << pStr << endl;
    cout << "pStr1 = " << pStr1 << endl;
    delete [] pStr; // 現在將原先配置給 pStr 的記憶體空間歸還系統
    pStr = NULL; // 養成重設指標的好習慣
    cout << "pStr1 points to " << static_cast<void *>(pStr1) << endl; // 再次確認 pStr1 仍指向同一個地方
    cout << "pStr1 = " << pStr1 << endl; // 這時發現原來內容已不被保留
    cin >> input;

請先看第一例程式。
當我們使用 new 運算子建立一個型別為 int 的變數實體時,C++ runtime 會在 heap 中配置一塊 int 寬度 (4 bytes) 的記憶體,並且把這塊記憶的開頭位址記錄在 pInt 的儲存值,之後我們便可以使用 *pInt 寫入或取出。請注意:如果我們在本程式中使用 new int 而不是 new int(0) 的話,pInt 所指向的值就是原來那一小塊記憶體的既有值,也等於是一個隨機的值 - 這是必須避免的。
當你不使用配置的記憶體時,可以使用 delete 關鍵字將以使用 new 配置的記憶體歸還給 C++ run-time 的管理,使得它可以再被配置出去。
有一點請特別注意:即使你接連著配置兩塊記憶體 (無論是同一型別或不同型別),這兩塊記憶體並不一定是相連的。其實這也是多工作業系統的特性之一。
在第二例程式中,我們建立一個字元指標變數 pStr 和另一個對照用的 pStr1。我們同時使用 new 關鍵字建立起一個 char[] 的陣列。
首先,我們先把 pStr 所指向的位址複製給 pStr1,再把二者所存的位址輸出以便檢查它們確實指向同一個地方。然後把剛才配置的陣列的第一個元素改成 J 以便辨識。此外我又把 pStr 與 pStr1 所指向的字串輸出來以再度檢查它們是否確實指向同一個地方。
接著,我把 pStr 原本所指向陣列以 delete [] 方式歸還給系統 (這是歸還陣列資料空間的語法),並且將 pStr 設為 NULL (這是一個必須養成的習慣)。但由於 pStr1 仍保留著原來的位址,所以我們就可以經由輸出 pStr1 所指向的字串,來看看原來的位址的內容有沒有什麼改變。果然,我們可以發現原來位址所存放的內容已經改變,表示這塊記憶體的內容已不再被系統所保留。
程式的執行結果如下圖所示:

我在「指標與陣列的混用」一節中的最後舉了一個字元指標與字元陣列混用的例子。對於那種動不動就可能使陣列索引超出界限之外的情形,如果我們可以動態配置記憶體,會不會有所改善呢?
在 C#/VB 中,字串的操作實際上都是使用拷貝原值到新字串的方式來處理的,如此我們才能隨心所欲的靈活操作字串。那麼,我們能不能模擬 C#/VB 的做法呢?如下例:
  • char *pStr1 = "Johnny", *pStr2 = " Lee";
    char *pStrNew = new char[strlen(pStr1) + strlen(pStr2) + 1];
    strcat(strcpy(pStrNew, pStr1), pStr2);
    cout << pStrNew << endl;
當然,這個例子只是為了證明可以這麼做而已。如果你的程式裡總是這麼做的話,會在記憶體裡面留下很多 memory leak。
此外,與 Stack 比較起來,Heap 的空間大得多了!例如:
  • int main()
    {
         int inStack[100000000];
    }
不管你的電腦記憶體有多大,這個程式一定會觸發 Stack Overflow 錯誤。我在上面提到過,區域變數是放置在 Stack 裡面,而程式內的 Stack 記憶體是很小的。
那麼,如果放在 Heap 裡面呢?如下例:
  • int *pHeap = new int[100000000];
除非你的電腦記憶體真的很小,否則這道指令應該可以正常執行。

參考

參考 (Reference) 變數是一個用起來很像指標,卻不是指標的東西。你可以說參考變數其實只是一個一般變數的別名 (alias)。
語法:
  • int number = 10;
    int &rNumber = number; // & 符號可以放在變數的前面
  • int& rNumber = number; // & 符號也可以放在型別的後面

我們知道 & 叫做「取址運算子」。如果我們在宣告一個變數時加上取址運算子,就表示將它宣告為參考變數。
跟指標變數不同,在宣告參考變數時,一定要同時賦予初始值,如同本例所示。如果你只宣告參考變數而未賦予初始值 (如 int &rNumber;),會引發 compiler 錯誤。
同樣的,你最好將參考變數取名以 r 開頭的名稱以便識別。
當你為一個普通變數建立參考之後,這個參考變數就完全等於原來的變數,如下例:
  • long number = 10;
    long &rNumber = number; // & 符號可以放在變數的前面
    rNumber +=10;
    cout << &rNumber << endl; // 使用 &rNumber 可以取出資料所在的位址
    cout << &number << endl;  // 此時 rNumber 與 number 都指向同一個位址
    cout << rNumber << endl;  // 使用 rNumber 可以取出資料的值
    cout << number << endl; // number 的值與 rNumber 相同
從本例中可以看出 rNumber 與 number 兩個變數具有相同的行為。不但值相同,連位址都一樣。換句話說,一旦建立 rNumber 與 number 之間的參考關係,你對 rNumber 所進行的操作,都會反應到 number 上面。指標雖然能做到相同的事情,卻必須經由解參考運算子;參考則完全不需要透過任何運算子。

Handle

我前面一再提到 C++ 對於動態記憶體配置的不足之處,亦即開發人員必須很謹慎的處理自已配置的記憶體,否則很容易造成 memory leak。而身為 .Net 的一份子,C++/CLI 則提供了變數的 Handle (也稱為 Tracking Handle),用來追蹤變數。被列入追蹤的變數會被隨時監視,一旦有記憶體不再被任何變數所指向 (即前面提過的 "Orphaned" 的情況),就會被 .Net 的 GC 機制回收,如此我們就不必費心的自行處理變數的廢棄空間。
再強調一次,這個功能只在 C++/CLI 程式中生效。所以傳統 C++ (像前面的 Win32 專案) 程式中是無法使用的。如果你想測試本節中的程式,請新增一個 CLR 主控台應用程式。
語法:
  • int ^hInt = 10, ^hResult = 0;
    hResult = *hInt * 2;
    Console::WriteLine(hResult);
Handle (在中國這個字普遍被翻譯為「句柄」;台灣則沒有適當的翻譯,因此我保留這個字的原文不予翻譯) 在技術上和指標 (Pointer) 並不完全一樣,但是除了它的代表符號 ( ^ ) 不同外,它和指標在許多情況下都很類似。例如,你一樣可以使用間接運算子取得它的對應值。不過,就像範例程式所示,你並不一定隨時都要使用間接運算子來讀寫它的值。
事實上,你也可以強迫加上間接運算子,如下例:
  • int ^hInt = 10, ^hResult = 0;
    *hResult = *hInt * 2;
    Console::WriteLine(*hResult);
第二個程式的執行結果和第一個程式是一樣的。
和指標一樣,我建議你為此類變數取一個以 h 開頭的名字,以利識別。
不過,以上兩個程式會引發編譯上的警告 (不是錯誤),主要是因為 ^hResult = 0 這道指令。C++ compiler 會很親切 (或者說雞婆) 的提醒你,如果你要把 handle 變數指向 NULL 的話,不能採用和指標相同的方法 (即賦予 0),而是應該使用像 ^hResult = nullptr 的方法。不過我們在這裡並沒有打算把 hResult 指向 NULL,而是要賦予 hResult 的對應值為 0,所以我們完全不需要理會那個警告。
當我們在程式中下達 ^hInt = 10, ^hResult = 0 這兩道指令時,不但宣告了這兩個 handle 變數,也同時在 heap 裡面 new 了兩個值,一個是10,一個是 0,並且把這兩塊記憶體的位址傳給了這兩個 handle 變數。如果你未來把 handle 變數指向另一個地方,那麼原來的記憶體就會自動歸還給 C++ run-time,不需要你額外下 delete 指令 (事實上也沒這個指令可以下)。
C++/CLI 中的 handle 變數也可以指向陣列。不過其語法和用法與傳統 C++ 差異很大,如下例:
  • array<int> ^hIntArray = gcnew array<int>(3) { 2, 4, 6 };
    for (int i = 0; i < hIntArray -> Length; i++)
    Console::Write("{0, 3}", hIntArray[i] - 1);
這種語法看起來既不像傳統 C++ 程式,也不像 C#。值得注意的是 gcnew 這個關鍵字;你必須記得不能使用普通的 new 去配置參考型別的動態記憶體。陣列一定是參考型別 (Reference Type),所以一定要使用 gcnew;只有實值型別 (Value Type) 才能使用 new (但是對指向實值型別的 handle 變數卻又不需使用 new;你只需使用 hInt = 10 即可指定初始值,C++ compiler 會自動幫你配置記憶體)。這是和 C#/VB 不一樣的地方。

沒有留言:

張貼留言