
本文對C語言指針和指針使用時的問題做一個概覽性的總結,并對一些值得探討的問題進行討論。閱讀本文,讀者能達到統覽C語言指針的目的。以下的討論只針對32/64位機器。
(資料圖片僅供參考)
要知道什么是指針,就要先了解內存的編址方法。
存儲器由一塊塊的空間(存儲單元)組成,為了方便尋找到每一塊空間,我們需要對每一個空間進行標識——內存編址。字節(Byte)是討論內存空間時的基本單位,每個存儲單元的大小是一個字節。
對內存進行編址,使得每個內存中的每個字節都有一個特定的編號,這個編號就是該內存單元的地址。
指針就是內存的地址。指針變量是存儲指針的變量。平常說的指針通常指的是指針變量。
在32位機器上,地址是32個二進制位(bit)組成的二進制序列,需要用4個字節進行存儲,所以指針變量的大小是4個字節;同理,在64位機器上,指針變量的大小是8個字節。
取地址操作符(&)可以拿到一個變量的地址,進而可以將其放到一個指針變量中。指針變量是個變量,本身占用內存空間,并通過存儲內存地址另外指向一塊內存空間。如果對指針變量進行取地址操作,取出的便是指針變量的地址,若用一個變量存儲這個地址,則這個變量便是二級指針變量。通過這個二級指針可以拿到其指向的一級指針。
n級指針變量用來存儲(n - 1)級指針變量的地址,這個指針變量就是一個多級指針。
int main(){ int a = 10; int* pa = &a;//pa是一個一級指針 int** ppa = &pa;//ppa是一個二級指針 **ppa = 20;//通過二級指針操作數據 return 0;}
作為一個變量,指針具有多種類型。
指向一個整型空間的指針變量就是整型指針。
需要注意的是,整型變量具有4個字節,而一個指針只能標識一個字節的空間,所以整型指針指向的僅僅是整型變量的第一個字節,對指針進行解引用時,編譯器會自行向后取四個字節以完整地拿到這個整型。
void test02(void){ int a = 10; int* pa = &a;}
指向一個字符空間的指針變量就是字符指針。
需要注意的是常量字符串的存儲方式,用一個字符指針存儲常量字符串時,字符指針存儲的僅是第一個字符的地址,且該地址指向的內容不可被修改(常量字符串不能被修改),最好用??const?
?對該指針進行修飾。
void test03(void){ const char* p = "hello world!";}
指向一個數組的指針變量就是數組指針。對數組指針進行解引用,拿到的是該數組的數組名。
函數具有地址。函數在被準備調用時會在棧區創建函數棧幀,為函數和函數參數創建臨時空間,當一切準備工作就緒時,某地址處的函數被調用。
存儲函數地址的指針變量就是函數指針。通過函數指針可以找到指向的函數,進而可以調用該函數。
int Add(int x, int y){ int sum = x + y; return sum;}void test04(void){ int a = 10; int b = 20; int ret = Add(a, b); int(*pf)(int, int) = &Add;//pf是一個函數指針 pf(3, 4);//通過函數指針調用函數、 (*pf)(3, 4);與第13行代碼效果相同,*號無實際意義}
回調函數是通過函數指針被調用的函數。如果將一個函數指針作為參數傳遞給另外一個函數,當函數通過這個函數指針調用指向的函數時,被調用的這個函數就是一個回調函數。回調函數不是實現方直接調用的,而是特定條件或事件發生時由另一方調用的。
通過回調函數,我們可以設計一個可以排序多種數據類型的冒泡排序:
void Swap(char* e1, char* e2, size_t width){ //逐字節交換 for (size_t i = 0; i < width; i++) { char tmp = *e1; *e1 = *e2; *e2 = tmp; e1++; e2++; }} //改造冒泡排序void BubbleSort(void* base, size_t num, size_t width, int (*cmp_fun)(const void*, const void*)){ for (size_t i = 0; i < num - 1; i++) { for (size_t j = 0; j < num - 1 - i; j++) { if (cmp_fun((char*)base + j * width, (char*)base + (j + 1) * width) > 0) { Swap((char*)base + j * width, (char*)base + (j + 1) * width, width); } } }} int cmp_int(const void* e1, const void* e2){ return *(int*)e1 - *(int*)e2;} void test1(void){ int arr[] = { 1,3,5,7,2,4,6,8 }; int sz = sizeof(arr) / sizeof(arr[0]); BubbleSort(arr, sz, sizeof(int), cmp_int);//回調函數} struct Demo{ int n; char arr[10];}; int cmp_str(const void* e1, const void* e2){ return strcmp(((struct Demo*)e1)->arr, ((struct Demo*)e2)->arr);} void test2(void){ struct Demo d[3] = { {2, "zeze"}, {3, "ahah"}, {1, "hehe"} }; BubbleSort(d, 3, sizeof(d[0]), cmp_str);} int main(){ //test1(); test2(); return 0;}
其中??cmp_?
?就是一個回調函數。
指針類型決定了指針與整數運算時,指針移動的步長;
指針類型決定了對指針進行解引用時訪問空間的大小。
“野指針”就是指向位置不可知(隨機的、不正確的、沒有明確限制的)的指針。野指針是造成內存錯誤使用和管理的重要原因。
指針未初始化。當定義一個指針變量時,其指向的內容是隨機的,若直接使用就會造成問題。
指針越界訪問。這個問題常見于數組操作中。操作數組時,若不注意數組的大小和范圍,就會造成指針越界。
指針指向的空間被釋放。返回指向棧區空間的指針,或者??free?
?空間后仍使用該指針,就會造成問題。
關于更多野指針的討論,可以參考我之前的一篇關于??內存管理??的文章。
作為一種變量,指針和整數、指針和指針之間都可以進行運算。兩種運算分別有不同的意義。
指針+-整數運算可以實現指針的移動,移動的步長取決于指針的類型。
指向同一塊空間的兩指針的減法運算的結果的絕對值是兩指針之間的元素數目。指針之間的加法運算沒有實際意義。
strlen()函數的實現:
size_t my_strlen(const char* p){ assert(p); const char* end = p; while (*end++); return end - p - 1;//指針運算}
指針之間可以進行關系運算。指向同一塊空間的指針的關系運算常用于控制一些操作的開始或終止。
需要注意的時,ANSI C規定,允許指向數組元素的指針與指向數組最后一個元素后面的那個內存位置的指針比較,但是不允許與指向第一個元素之前的那個內存位置的指針進行比較。
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--){ *vp = 0;}//不建議這樣寫//規范寫法:for(vp = &values[0]; vp <= &values[N_VALUES-1]; vp++){ *vp = 0;}
二維數組的存儲空間是連續的,可以將二維數組看做存儲數個一維數組的數組,二維數組的每行的元素是一維數組的數組名。二維數組的數組名是第一行的一維數組的地址,即一個數組指針。
數組名是數組的首元素地址;對數組名進行取地址操作,取出的是整個數組的地址,需要用一個數組指針存儲。這樣我們就可以理解為什么二維數組的數組名是一個數組指針:二維數組的數組名是二維數組首元素的地址,而通過對二維數組的理解可以得知,二維數組的首元素其實是第一行的數組名,對數組名進行取地址操作,取出的是第一行的地址,所以拿到二維數組的數組名就拿到了二維數組第一行的地址。
另外,用??sizeof(ArrayName)?
?計算數組大小時,這里的數組名代表的同樣是整個數組。
指針數組是存放指針的數組。
數組傳參時,函數可以用一個數組接收參數,也可以用一個指針接收參數,但是數組本質上傳遞的是一個指針,該指針保存了數組首元素的地址。要注意選擇合適的指針類型接收數組參數。
void Func(int arr[ROW][COL], int row, int col){ //用數組接收參數 ;}void Func(int(*parr)[COL], int row, int col){ //用指針接收參數 ;}
函數指針數組常被用作轉移表(jump table)。使用轉移表可以減少代碼冗余,增加代碼可讀性,是一種良好的設計方案。轉移表中的函數必須是同類的函數。
簡易計算器:
int add(int a, int b){ return a + b;}int sub(int a, int b){ return a - b;}int mul(int a, int b){ return a * b;}int div(int a, int b){ return a / b;}int main(){ int x, y; int input = 1; int ret = 0; int(*p[5])(int x, int y) = { 0, add, sub, mul, div };//轉移表 while (input) { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf("*************************\n"); printf("請選擇:"); scanf("%d", &input); if ((input <= 4 && input >= 1)) { printf("輸入操作數:"); scanf("%d %d", &x, &y); ret = (*p[input])(x, y); } else { printf("輸入有誤\n"); } printf("ret = %d\n", ret); }}
在C語言中,線性表和二叉樹的實現離不開指針,指針將各個結構的各個節點連接起來,進而保證結構的完整性。更高級的數據結構的實現都是建立在此基礎上的。
C語言內存管理必須要有指針,C程序員通過指針對內存進行布局和使用,離開了指針,程序員面對內存就會手足無措。
作為一把無所不能的菜刀,使用指針管理內存時往往會出現一些問題,常見的??內存管理??問題和規避方法可以參考我之前的一篇文章。