指針
指針是 C 語言最重要的概念之一,也是最難理解的概念之一。
簡介
指針是什么?首先,它是一個值,這個值代表一個內存地址,因此指針相當於指向某個內存地址的路標。
字符*
表示指針,通常跟在類型關鍵字的后面,表示指針指向的是什么類型的值。比如,char*
表示一個指向字符的指針,float*
表示一個指向float
類型的值的指針。
int* intPtr;
上面示例聲明了一個變量intPtr
,它是一個指針,指向的內存地址存放的是一個整數。
星號*
可以放在變量名與類型關鍵字之間的任何地方,下面的寫法都是有效的。
int *intPtr;
int * intPtr;
int* intPtr;
本書使用星號緊跟在類型關鍵字后面的寫法(即int* intPtr;
),因為這樣可以體現,指針變量就是一個普通變量,只不過它的值是內存地址而已。
這種寫法有一個地方需要注意,如果同一行聲明兩個指針變量,那么需要寫成下面這樣。
// 正確
int * foo, * bar;
// 錯誤
int* foo, bar;
上面示例中,第二行的執行結果是,foo
是整數指針變量,而bar
是整數變量,即*
只對第一個變量生效。
一個指針指向的可能還是指針,這時就要用兩個星號**
表示。
int** foo;
上面示例表示變量foo
是一個指針,指向的還是一個指針,第二個指針指向的則是一個整數。
* 運算符
*
這個符號除了表示指針以外,還可以作為運算符,用來取出指針變量所指向的內存地址里面的值。
void increment(int* p) {
*p = *p + 1;
}
上面示例中,函數increment()
的參數是一個整數指針p
。函數體里面,*p
就表示指針p
所指向的那個值。對*p
賦值,就表示改變指針所指向的那個地址里面的值。
上面函數的作用是將參數值加1
。該函數沒有返回值,因為傳入的是地址,函數體內部對該地址包含的值的操作,會影響到函數外部,所以不需要返回值。事實上,函數內部通過指針,將值傳到外部,是 C 語言的常用方法。
變量地址而不是變量值傳入函數,還有一個好處。對於需要大量存儲空間的大型變量,復制變量值傳入函數,非常浪費時間和空間,不如傳入指針來得高效。
& 運算符
&
運算符用來取出一個變量所在的內存地址。
int x = 1;
printf("x's address is %p\n", &x);
上面示例中,x
是一個整數變量,&x
就是x
的值所在的內存地址。printf()
的%p
是內存地址的占位符,可以打印出內存地址。
上一小節中,參數變量加1
的函數,可以像下面這樣使用。
void increment(int* p) {
*p = *p + 1;
}
int x = 1;
increment(&x);
printf("%d\n", x); // 2
上面示例中,調用increment()
函數以后,變量x
的值就增加了1,原因就在於傳入函數的是變量x
的地址&x
。
&
運算符與*
運算符互為逆運算,下面的表達式總是成立。
int i = 5;
if (i == *(&i)) // 正確
指針變量的初始化
聲明指針變量之后,編譯器會為指針變量本身分配一個內存空間,但是這個內存空間里面的值是隨機的,也就是說,指針變量指向的值是隨機的。這時一定不能去讀寫指針變量指向的地址,因為那個地址是隨機地址,很可能會導致嚴重后果。
int* p;
*p = 1; // 錯誤
上面的代碼是錯的,因為p
指向的那個地址是隨機的,向這個隨機地址里面寫入1
,會導致意想不到的結果。
正確做法是指針變量聲明后,必須先讓它指向一個分配好的地址,然后再進行讀寫,這叫做指針變量的初始化。
int* p;
int i;
p = &i;
*p = 13;
上面示例中,p
是指針變量,聲明這個變量后,p
會指向一個隨機的內存地址。這時要將它指向一個已經分配好的內存地址,上例就是再聲明一個整數變量i
,編譯器會為i
分配內存地址,然后讓p
指向i
的內存地址(p = &i;
)。完成初始化之后,就可以對p
指向的內存地址進行賦值了(*p = 13;
)。
為了防止讀寫未初始化的指針變量,可以養成習慣,將未初始化的指針變量設為NULL
。
int* p = NULL;
NULL
在 C 語言中是一個常量,表示地址為0
的內存空間,這個地址是無法使用的,讀寫該地址會報錯。
指針的運算
指針本質上就是一個無符號整數,代表了內存地址。它可以進行運算,但是規則並不是整數運算的規則。
(1)指針與整數值的加減運算
指針與整數值的運算,表示指針的移動。
short* j;
j = (short*)0x1234;
j = j + 1; // 0x1236
上面示例中,j
是一個指針,指向內存地址0x1234
。你可能以為j + 1
等於0x1235
,但正確答案是0x1236
。原因是j + 1
表示指針向內存地址的高位移動一個單位,而一個單位的short
類型占據兩個字節的寬度,所以相當於向高位移動兩個字節。同樣的,j - 1
得到的結果是0x1232
。
指針移動的單位,與指針指向的數據類型有關。數據類型占據多少個字節,每單位就移動多少個字節。
(2)指針與指針的加法運算
指針只能與整數值進行加減運算,兩個指針進行加法是非法的。
unsigned short* j;
unsigned short* k;
x = j + k; // 非法
上面示例是兩個指針相加,這是非法的。
(3)指針與指針的減法
相同類型的指針允許進行減法運算,返回它們之間的距離,即相隔多少個數據單位。
高位地址減去低位地址,返回的是正值;低位地址減去高位地址,返回的是負值。
這時,減法返回的值屬於ptrdiff_t
類型,這是一個帶符號的整數類型別名,具體類型根據系統不同而不同。這個類型的原型定義在頭文件stddef.h
里面。
short* j1;
short* j2;
j1 = (short*)0x1234;
j2 = (short*)0x1236;
ptrdiff_t dist = j2 - j1;
printf("%d\n", dist); // 1
上面示例中,j1
和j2
是兩個指向 short 類型的指針,變量dist
是它們之間的距離,類型為ptrdiff_t
,值為1
,因為相差2個字節正好存放一個 short 類型的值。
(4)指針與指針的比較運算
指針之間的比較運算,比較的是各自的內存地址哪一個更大,返回值是整數1
(true)或0
(false)。
C 語言的內存管理
簡介
C 語言的內存管理,分成兩部分。一部分是系統管理的,另一部分是用戶手動管理的。
系統管理的內存,主要是函數內部的變量(局部變量)。這部分變量在函數運行時進入內存,函數運行結束后自動從內存卸載。這些變量存放的區域稱為”棧“(stack),”棧“所在的內存是系統自動管理的。
用戶手動管理的內存,主要是程序運行的整個過程中都存在的變量(全局變量),這些變量需要用戶手動從內存釋放。如果使用后忘記釋放,它就一直占用內存,直到程序退出,這種情況稱為”內存泄漏“(memory leak)。這些變量所在的內存稱為”堆“(heap),”堆“所在的內存是用戶手動管理的。
void 指針
前面章節已經說過了,每一塊內存都有地址,通過指針變量可以獲取指定地址的內存塊。指針變量必須有類型,否則編譯器無法知道,如何解讀內存塊保存的二進制數據。但是,向系統請求內存的時候,有時不確定會有什么樣的數據寫入內存,需要先獲得內存塊,稍后再確定寫入的數據類型。
為了滿足這種需求,C 語言提供了一種不定類型的指針,叫做 void 指針。它只有內存塊的地址信息,沒有類型信息,等到使用該塊內存的時候,再向編譯器補充說明,里面的數據類型是什么。
另一方面,void 指針等同於無類型指針,可以指向任意類型的數據,但是不能解讀數據。void 指針與其他所有類型指針之間是互相轉換關系,任一類型的指針都可以轉為 void 指針,而 void 指針也可以轉為任一類型的指針。
int x = 10;
void* p = &x; // 整數指針轉為 void 指針
int* q = p; // void 指針轉為整數指針
上面示例演示了,整數指針和 void 指針如何互相轉換。&x
是一個整數指針,p
是 void 指針,賦值時&x
的地址會自動解釋為 void 類型。同樣的,p
再賦值給整數指針q
時,p
的地址會自動解釋為整數指針。
注意,由於不知道 void 指針指向什么類型的值,所以不能用*
運算符取出它指向的值。
char a = 'X';
void* p = &a;
printf("%c\n", *p); // 報錯
上面示例中,p
是一個 void 指針,所以這時無法用*p
取出指針指向的值。
void 指針的重要之處在於,很多內存相關函數的返回值就是 void 指針,只給出內存塊的地址信息,所以放在最前面進行介紹。
malloc()
malloc()
函數用於分配內存,該函數向系統要求一段內存,系統就在“堆”里面分配一段連續的內存塊給它。它的原型定義在頭文件stdlib.h
。
void* malloc(size_t size)
它接受一個非負整數作為參數,表示所要分配的內存字節數,返回一個 void 指針,指向分配好的內存塊。這是非常合理的,因為malloc()
函數不知道,將要存儲在該塊內存的數據是什么類型,所以只能返回一個無類型的 void 指針。
可以使用malloc()
為任意類型的數據分配內存,常見的做法是先使用sizeof()
函數,算出某種數據類型所需的字節長度,然后再將這個長度傳給malloc()
。
int* p = malloc(sizeof(int));
*p = 12;
printf("%d\n", *p); // 12
上面示例中,先為整數類型分配一段內存,然后將整數12
放入這段內存里面。這個例子其實不需要使用malloc()
,因為 C 語言會自動為整數(本例是12
)提供內存。
有時候為了增加代碼的可讀性,可以對malloc()
返回的指針進行一次強制類型轉換。
int* p = (int*) malloc(sizeof(int));
上面代碼將malloc()
返回的 void 指針,強制轉換成了整數指針。
由於sizeof()
的參數可以是變量,所以上面的例子也可以寫成下面這樣。
int* p = (int*) malloc(sizeof(*p));
malloc()
分配內存有可能分配失敗,這時返回常量 NULL。Null 的值為0,是一個無法讀寫的內存地址,可以理解成一個不指向任何地方的指針。它在包括stdlib.h
等多個頭文件里面都有定義,所以只要可以使用malloc()
,就可以使用NULL
。由於存在分配失敗的可能,所以最好在使用malloc()
之后檢查一下,是否分配成功。
int* p = malloc(sizeof(int));
if (p == NULL) {
// 內存分配失敗
}
// or
if (!p) {
//...
}
上面示例中,通過判斷返回的指針p
是否為NULL
,確定malloc()
是否分配成功。
malloc()
最常用的場合,就是為數組和自定義數據結構分配內存。
int* p = (int*) malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++)
p[i] = i * 5;
上面示例中,p
是一個整數指針,指向一段可以放置10個整數的內存,所以可以用作數組。
malloc()
用來創建數組,有一個好處,就是它可以創建動態數組,即根據成員數量的不同,而創建長度不同的數組。
int* p = (int*) malloc(n * sizeof(int));
上面示例中,malloc()
可以根據變量n
的不同,動態為數組分配不同的大小。
注意,malloc()
不會對所分配的內存進行初始化,里面還保存着原來的值。如果沒有初始化,就使用這段內存,可能從里面讀到以前的值。程序員要自己負責初始化,比如,字符串初始化可以使用strcpy()
函數。
char* p = malloc(4);
strcpy(p, "abc");
// or
p = "abc";
上面示例中,字符指針p
指向一段4個字節的內存,strcpy()
將字符串“abc”拷貝放入這段內存,完成了這段內存的初始化。
free()
free()
用於釋放malloc()
函數分配的內存,將這塊內存還給系統以便重新使用,否則這個內存塊會一直占用到程序運行結束。該函數的原型定義在頭文件stdlib.h
里面。
void free(void* block)
上面代碼中,free()
的參數是malloc()
返回的內存地址。下面就是用法實例。
int* p = (int*) malloc(sizeof(int));
*p = 12;
free(p);
注意,分配的內存塊一旦釋放,就不應該再次操作已經釋放的地址,也不應該再次使用free()
對該地址釋放第二次。
一個很常見的錯誤是,在函數內部分配了內存,但是函數調用結束時,沒有使用free()
釋放內存。
void gobble(double arr[], int n) {
double* temp = (double*) malloc(n * sizeof(double));
// ...
}
上面示例中,函數gobble()
內部分配了內存,但是沒有寫free(temp)
。這會造成函數運行結束后,占用的內存塊依然保留,如果多次調用gobble()
,就會留下多個內存塊。並且,由於指針temp
已經消失了,也無法訪問這些內存塊,再次使用。
calloc()
calloc()
函數的作用與malloc()
相似,也是分配內存塊。該函數的原型定義在頭文件stdlib.h
。
兩者的區別主要有兩點:
(1)calloc()
接受兩個參數,第一個參數是某種數據類型的值的數量,第二個是該數據類型的單位字節長度。
void* calloc(size_t n, size_t size);
calloc()
的返回值也是一個 void 指針。分配失敗時,返回 NULL。
(2)calloc()
會將所分配的內存全部初始化為0
。malloc()
不會對內存進行初始化,如果想要初始化為0
,還要額外調用memset()
函數。
int* p = calloc(10, sizeof(int));
// 等同於
int* p = malloc(sizeof(int) * 10);
memset(p, 0, sizeof(int) * 10);
上面示例中,calloc()
相當於malloc() + memset()
。
calloc()
分配的內存塊,也要使用free()
釋放。
realloc()
realloc()
函數用於修改已經分配的內存塊的大小,可以放大也可以縮小,返回一個指向新的內存塊的指針。如果分配不成功,返回 NULL。該函數的原型定義在頭文件stdlib.h
。
void* realloc(void* block, size_t size)
它接受兩個參數。
block
:已經分配好的內存塊指針(由malloc()
或calloc()
或realloc()
產生)。size
:該內存塊的新大小,單位為字節。
realloc()
可能返回一個全新的地址(數據也會自動復制過去),也可能返回跟原來一樣的地址。realloc()
優先在原有內存塊上進行縮減,盡量不移動數據,所以通常是返回原先的地址。如果新內存塊小於原來的大小,則丟棄超出的部分;如果大於原來的大小,則不對新增的部分進行初始化(程序員可以自動調用memset()
)。
下面是一個例子,b
是數組指針,realloc()
動態調整它的大小。
int* b;
b = malloc(sizeof(int) * 10);
b = realloc(b, sizeof(int) * 2000);
上面示例中,指針b
原來指向10個成員的整數數組,使用realloc()
調整為2000個成員的數組。這就是手動分配數組內存的好處,可以在運行時隨時調整數組的長度。
realloc()
的第一個參數可以是 NULL,這時就相當於新建一個指針。
char* p = realloc(NULL, 3490);
// 等同於
char* p = malloc(3490);
如果realloc()
的第二個參數是0
,就會釋放掉內存塊。
由於有分配失敗的可能,所以調用realloc()
以后,最好檢查一下它的返回值是否為 NULL。分配失敗時,原有內存塊中的數據不會發生改變。
float* new_p = realloc(p, sizeof(*p * 40));
if (new_p == NULL) {
printf("Error reallocing\n");
return 1;
}
注意,realloc()
不會對內存塊進行初始化。
restrict 說明符
聲明指針變量時,可以使用restrict
說明符,告訴編譯器,該塊內存區域只有當前指針一種訪問方式,其他指針不能讀寫該塊內存。這種指針稱為“受限指針”(restrict pointer)。
int* restrict p;
p = malloc(sizeof(int));
上面示例中,聲明指針變量p
時,加入了restrict
說明符,使得p
變成了受限指針。后面,當p
指向malloc()
函數返回的一塊內存區域,就味着,該區域只有通過p
來訪問,不存在其他訪問方式。
int* restrict p;
p = malloc(sizeof(int));
int* q = p;
*q = 0; // 未定義行為
上面示例中,另一個指針q
與受限指針p
指向同一塊內存,現在該內存有p
和q
兩種訪問方式。這就違反了對編譯器的承諾,后面通過*q
對該內存區域賦值,會導致未定義行為。
memcpy()
memcpy()
用於將一塊內存拷貝到另一塊內存。該函數的原型定義在頭文件string.h
。
void* memcpy(
void* restrict dest,
void* restrict source,
size_t n
);
上面代碼中,dest
是目標地址,source
是源地址,第三個參數n
是要拷貝的字節數n
。如果要拷貝10個 double 類型的數組成員,n
就等於10 * sizeof(double)
,而不是10
。該函數會將從source
開始的n
個字節,拷貝到dest
。
dest
和source
都是 void 指針,表示這里不限制指針類型,各種類型的內存數據都可以拷貝。兩者都有 restrict 關鍵字,表示這兩個內存塊不應該有互相重疊的區域。
memcpy()
的返回值是第一個參數,即目標地址的指針。
因為memcpy()
只是將一段內存的值,復制到另一段內存,所以不需要知道內存里面的數據是什么類型。下面是復制字符串的例子。
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "Goats!";
char t[100];
memcpy(t, s, sizeof(s)); // 拷貝7個字節,包括終止符
printf("%s\n", t); // "Goats!"
return 0;
}
上面示例中,字符串s
所在的內存,被拷貝到字符數組t
所在的內存。
memcpy()
可以取代strcpy()
進行字符串拷貝,而且是更好的方法,不僅更安全,速度也更快,它不檢查字符串尾部的\0
字符。
char* s = "hello world";
size_t len = strlen(s) + 1;
char *c = malloc(len);
if (c) {
// strcpy() 的寫法
strcpy(c, s);
// memcpy() 的寫法
memcpy(c, s, len);
}
上面示例中,兩種寫法的效果完全一樣,但是memcpy()
的寫法要好於strcpy()
。
使用 void 指針,也可以自定義一個復制內存的函數。
void* my_memcpy(void* dest, void* src, int byte_count) {
char* s = src;
char* d = dest;
while (byte_count--) {
*d++ = *s++;
}
return dest;
}
上面示例中,不管傳入的dest
和src
是什么類型的指針,將它們重新定義成一字節的 Char 指針,這樣就可以逐字節進行復制。*d++ = *s++
語句相當於先執行*d = *s
(源字節的值復制給目標字節),然后各自移動到下一個字節。最后,返回復制后的dest
指針,便於后續使用。
memmove()
memmove()
函數用於將一段內存數據復制到另一段內存。它跟memcpy()
的主要區別是,它允許目標區域與源區域有重疊。如果發生重疊,源區域的內容會被更改;如果沒有重疊,它與memcpy()
行為相同。
該函數的原型定義在頭文件string.h
。
void* memmove(
void* dest,
void* source,
size_t n
);
上面代碼中,dest
是目標地址,source
是源地址,n
是要移動的字節數。dest
和source
都是 void 指針,表示可以移動任何類型的內存數據,兩個內存區域可以有重疊。
memmove()
返回值是第一個參數,即目標地址的指針。
int a[100];
// ...
memmove(&a[0], &a[1], 99 * sizeof(int));
上面示例中,從數組成員a[1]
開始的99個成員,都向前移動一個位置。
下面是另一個例子。
char x[] = "Home Sweet Home";
// 輸出 Sweet Home Home
printf("%s\n", (char *) memmove(x, &x[5], 10));
上面示例中,從字符串x
的5號位置開始的10個字節,就是“Sweet Home”,memmove()
將其前移到0號位置,所以x
就變成了“Sweet Home Home”。
memcmp()
memcmp()
函數用來比較兩個內存區域。它的原型定義在string.h
。
int memcmp(
const void* s1,
const void* s2,
size_t n
);
它接受三個參數,前兩個參數是用來比較的指針,第三個參數指定比較的字節數。
它的返回值是一個整數。兩塊內存區域的每個字節以字符形式解讀,按照字典順序進行比較,如果兩者相同,返回0
;如果s1
大於s2
,返回大於0的整數;如果s1
小於s2
,返回小於0的整數。
char* s1 = "abc";
char* s2 = "acd";
int r = memcmp(s1, s2, 3); // 小於 0
上面示例比較s1
和s2
的前三個字節,由於s1
小於s2
,所以r
是一個小於0的整數,一般為-1。
下面是另一個例子。
char s1[] = {'b', 'i', 'g', '\0', 'c', 'a', 'r'};
char s2[] = {'b', 'i', 'g', '\0', 'c', 'a', 't'};
if (memcmp(s1, s2, 3) == 0) // true
if (memcmp(s1, s2, 4) == 0) // true
if (memcmp(s1, s2, 7) == 0) // false
上面示例展示了,memcmp()
可以比較內部帶有字符串終止符\0
的內存區域。