[C/C++] 讓你減少錯誤的 3 個指標使用技巧
若是讓大家票選 “C 語言中的哪個部分殘害你最深” 想必會有超過半數的人回答指標,而這也是 C 語言之所以有趣的地方,只要善用指標,許多困難的操作都不再讓人恐懼,自古有云 “指標用的好 C 語言是彩色的,指標用不好 C 語言是黑白的”,也就是這樣的道理
1. 更好的指標宣告
一般來說,我們在學習 C 語言的時候,老師總是這麼教
int *pointer;
試想,若是程式碼多一點,我們就很可能沒辦法一眼看出他是指標,反而還比較像在宣告一個整數,所以我們可以改成這樣寫
int* pointer;
看吧!這不就一目了然啦!是不是指標一眼就看的出來!
舉例來說,若有一段程式碼這樣宣告:
int a; int *ptr; *ptr = &a;一時之間很難發現問題,但如果會這麼做就代表誤解了解參考運算子(*)的意思,因為在第 3 行時 “*” 是運算子,而不是變數的一部分,所以正確的寫法應為:
ptr = &a; // <- correct若使用前述的作法,就可以避免誤解
什麼?擔心不能用?痾…我們來問一下 Compiler 不就好了?
#include <iostream> using namespace std; int main() { int aNumber = 10; int *pointer = &aNumber; cout << "aNumber = " << aNumber << '\n'; cout << "&aNumber = " << &aNumber << '\n'; cout << "pointer = " << pointer << '\n'; cout << "*pointer = " << *pointer << '\n'; return 0; }
▲ 原本標準的寫法
aNumber = 10 &aNumber = 0x28fef8 pointer = 0x28fef8 *pointer = 10
▲輸出
#include <iostream> using namespace std; int main() { int aNumber = 10; int* pointer = &aNumber; cout << "aNumber = " << aNumber << '\n'; cout << "&aNumber = " << &aNumber << '\n'; cout << "pointer = " << pointer << '\n'; cout << "*pointer = " << *pointer << '\n'; return 0; }
▲ 改過之後的結果
aNumber = 10 &aNumber = 0x28fef8 pointer = 0x28fef8 *pointer = 10
▲輸出
至於為什麼執行 2 次,印出來的地址都一樣,聽說有一種東西叫做 “記憶體管理” 裡面有一種技術叫 “虛擬記憶體映射”,想知道的人可以自己去查。
注意:在特定情況下,使用此技巧可能造成誤解!
int* ptr1, ptr2;像這樣的一段程式碼,在編譯器的眼中代表的意思其實是
int* prt1; int ptr2;與我們的預期不同,因為對編譯器來說,若要宣告一個指標變數,它一定要在變數名稱前方看到一個 “*”(可以理解為編譯器是由後往前讀變數宣告的)。
因此,建議把每一個變數用獨立的一行宣告
若真的很想在同一行宣告,又想保持易讀性,請參考以下做法:
typedef int* PINT PINT ptr1, ptr2;
2. 總是在宣告指標時將它指定為 NULL
int* ptr = NULL;
試想,在一指標變數被初始化之前,裡面裝的是甚麼呢?很抱歉,根據 C 語言的標準,這屬於未定義行為,代表它可能是任何的東西,也就是說它可能指向任何位置,甚至是程式所能存取的記憶體範圍之外!
最常見的結果為 程式停止執行,與 節區錯誤(Segment fault)
若指定為 NULL,一旦誤用,就會在執行時產生錯誤(Runtime Error),這樣一來,程式發生錯誤的現象將被固定,使得除錯時更好追蹤。
這樣的技巧同樣適用於任何一種變數的宣告!
3. 使用 calloc 來取代 malloc
在 C 語言中,calloc 與 malloc 的最大差別在於,calloc 會在取得記憶體空間之後,將空間內所有位元(bit)設為 0;而 malloc 並不會這麼做。
也就是說:
int* ptr = (int*)calloc(i, sizeof(int));相當於
int* ptr = (int*)malloc(sizeof(int)); memset (ptr, 0, sizeof(int) );
因此,在執行速度上,calloc 與 malloc 存在差異,但若以執行結果正確為前提的話,犧牲一點點效能,換取 程式執行結果的正確性 和 開發上的便利,應該還算合理。
注:也可以理解成與前方的技巧相同,預先將變數設為 NULL(雖然 NULL 跟 0 的關係會因為平台與編譯器而有所不同)
Extra. 指定被釋放的指標為 NULL
當我們使用 malloc 於堆積區配置記憶體之後,都要記得用 free 來釋放它,這個道理人人都知道,但你知道嗎?在釋放之後,原本的指標其實並未被清空。
已就是說,指標仍然指向它原本的那一塊記憶體位址,只是它指的地方已經交還給作業系統了;若是嘗試存取的話,隨著系統的不同可能會造輕重不一的系統錯誤。
在較短的程式碼之中,或許不容易會發生,但若是在實作較為複雜的資料結構時,這樣的可能性並不是沒有。
為了避免因為這樣的錯誤造成系統的崩潰,我們應該在使用 free 之後,立即將它指定為 NULL,範例如下。
#include <stdio.h> #include <stdlib.h> int main() { int* pointer = (int*)malloc(sizeof(int)); *pointer = 10; printf("pointer 的數值 = %p\n", pointer); printf("對 pointer 取值 = %d\n", *pointer); printf("====== 釋放 pointer ======\n"); free(pointer); printf("pointer 的數值 = %p\n", pointer); printf("對 pointer 取值 = %d\n", *pointer); pointer = NULL; printf("pointer 的數值 = %p\n", pointer); printf("對 pointer 取值 = %d\n", *pointer); return 0; }
▲ 範例程式碼
pointer 的數值 = 007E2F88 對 pointer 取值 = 10 ====== 釋放 pointer ====== pointer 的數值 = 007E2F88 對 pointer 取值 = 8262744 ===== pointer 為 NULL ==== pointer 的數值 = 00000000 Process returned -1073741819 (0xC0000005)
▲ 執行結果
嗯?這樣好像沒有比較好,畢竟在我們設定為 NULL 之後,它反而崩潰了,但其實發生這樣的誤用,繼續下去的意義也已經失去,若是在功能較簡單的 OS 上,甚至可能會危及整個 OS 的穩定,我仍然認為這是好的習慣。
但是對光是一個 free 就會常常忘記寫的我來說,這樣還是太容易出錯了,所以寫一個副程式(function)來幫自己完成這樣的工作,似乎比較容易,所以小獅我就寫了以下的東西,大家參考一下就好,不一定要用
其實這篇文章也是(完整程式碼)void freePointer(int** pointer) { free(*pointer); *pointer = NULL; }但這樣還是無法解決 “重複釋放指標” 的問題,而且僅能支援 int 指標,所以我們要來做一點小修改(完整程式碼)
void saferFree(void** ptr) { if ( ptr != NULL && *ptr != NULL ) { free(*ptr); *ptr = NULL; } }使用了 void** 來接收任何形式的指標,並且加入判斷式來避免嘗試釋放 NULL 指標。
但是這樣好像還差一步,因為呼叫時還是要使用如下的方式呼叫:
saferFree((void**)&ptrToFree);若是可以像 free 一樣好用就好了,於是我們使用 巨集(Macro)來完成最後的目標!(完整程式碼)
#define safeFree(p) saferFree((void**)&(p));
是說寫這篇的最大原因其實是,小獅的指標又想跑回去老師那裡了,所以要趕快寫一篇文章來把它們抓回來(感覺好有畫面~)
若有幫助到大家,請給我一點回應喔!這會幫助我繼續寫作下去!