C語言詳解指針地址及內存管理


指針

指針是 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

上面示例中,j1j2是兩個指向 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()會將所分配的內存全部初始化為0malloc()不會對內存進行初始化,如果想要初始化為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指向同一塊內存,現在該內存有pq兩種訪問方式。這就違反了對編譯器的承諾,后面通過*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

destsource都是 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;

}

上面示例中,不管傳入的destsrc是什么類型的指針,將它們重新定義成一字節的 Char 指針,這樣就可以逐字節進行復制。*d++ = *s++語句相當於先執行*d = *s(源字節的值復制給目標字節),然后各自移動到下一個字節。最后,返回復制后的dest指針,便於后續使用。

memmove()

memmove()函數用於將一段內存數據復制到另一段內存。它跟memcpy()的主要區別是,它允許目標區域與源區域有重疊。如果發生重疊,源區域的內容會被更改;如果沒有重疊,它與memcpy()行為相同。

該函數的原型定義在頭文件string.h

void* memmove(
  void* dest, 
  void* source, 
  size_t n
);

上面代碼中,dest是目標地址,source是源地址,n是要移動的字節數。destsource都是 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

上面示例比較s1s2的前三個字節,由於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的內存區域。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM