0、前言:
- 動態內存分配是一個重要概念,要和靜態數組對比著學習;
- 指針和數組搭配在一起,讓指針理解的難度上了一個臺階,尤其是二維數組搭配指針,要獲取數組的值,什么時候“取地址”,什么時候“解引用”都需要深刻理解一些概念才能正確使用指針和數組。
- 寫這些東西,就是把自己學習的筆記記錄下來,供自己日后翻找,若是與此同時能給別人提供些幫助,那就更好了。
1、動態內存分配:
- ★前提知識:內存分為棧和堆,
- “棧”是由編譯器自動分配和釋放,無需程序員手動操作。當函數執行結束時,其內部的局部變量會被自動彈出棧并釋放內存。主要存儲局部變量、函數參數、返回地址等。靜態分配,編譯時就能確定所需內存大小。注意如果一個數組定義在自定義函數當中,那它就位于棧當中,函數周期結束,該數組自動釋放。
- “堆”需要程序員手動分配(如 C 語言的malloc)和釋放(如free),否則可能導致內存泄漏。主要存儲動態分配的對象、數組、大型數據結構等。動態分配,運行時才能確定所需內存大。
- 在堆中開辟內存空間的三種方式:malloc、calloc、realloc;開辟堆空間成功后,都會返回一個 void* 類型指針,所以需要根據空間存儲內容,強轉這個指針。
- 數組是靜態內存分配,數組大小必須是常量。是和動態內存分配相對應的一種連續空間開辟的方式。C語言中二維數組空間地址連續,可以用指針遍歷。
- malloc:依賴于頭文件 stdlib.h,函數聲明:void* malloc(size_t size); // 從堆上分配一塊大小為size的“連續”空間, 并且返回它的首地址。
- calloc:malloc不會像數組初始化一樣把開辟出的內存空間初始化,這時就要使用calloc,會初始化空間;
- realloc:在不改變原來空間內容的情況下,對空間進行縮放。如果擴大了空間,就新增空間,但新增的空間不會初始化,然后返回這塊空間首地址,如果縮小了空間,就會截斷多余空間,其余空間內容保持不變(縮小可能導致數據丟失),然后返回這塊空間首地址。使用realloc之后,會自動釋放之前的空間,切記不要重復釋放之前的空間,否則程序會報錯。
- sizeof運算符是無法計算malloc、calloc、realloc動態生成的空間大小的,但是通過sizeof可以獲取數組的總的字節數。
#include<stdio.h>
#include<stdlib.h>// 在堆中用malloc開辟空間,存放五個double型數據,打印
void mal_loc() {// 開辟空間,把首地址給指針pdouble* p = (double*)malloc(sizeof(double) * 5);int i;// 給空間元素賦值for (i = 0; i < 5; i++) {if (p != NULL) {*(p + i) = (double)(5 + i) / 2;}}// 使用這塊空間for (i = 0; i < 5; i++) {if (p != NULL) {printf("%f\n", *(p + i)); // 也可以用p[i]遍歷}}// 釋放這塊空間free(p);// 指針指向空p = NULL;
}// 在堆中用calloc開辟空間,存放五個double型數據,驗證calloc是否會初始化空間
void cal_loc() {// 開辟空間,把首地址給指針pdouble* p = (double*)calloc(5, sizeof(double));int i;// 使用這塊空間for (i = 0; i < 5; i++) {if (p != NULL) {printf("%f\n", *(p + i)); // 也可以用p[i]遍歷}}// 釋放這塊空間free(p);// 指針指向空p = NULL;
}// 嘗試使用realloc對calloc開辟的5個int空間,縮小至4個int空間,驗證數據丟失
void real_loc() {// 用calloc開辟空間;int* p = (int*)calloc(5, sizeof(int));if (p == NULL) {return;}int i;// 給最后一位設為1;for (i = 0; i < 5; i++) {if (i == 4) {*(p + i) = 1;printf("%d, ", *(p + i));}else {printf("%d, ", *(p + i));}}printf("\n--------------\n");// 用realloc縮小空間;int* newp = (int*)realloc(p, 4*sizeof(int));// 驗證縮小空間是否成功if (newp == NULL) {free(p); p = NULL;// 避免出現懸空指針p = NULL;// 避免出現懸空指針return;}else {p = NULL;// 避免出現懸空指針// 創建成功,測試空間當中的內容for (i = 0; i < 4; i++) {printf("%d, ", *(newp+i)); // 驗證結果}// 最后釋放空間和避免出現懸空指針free(newp); newp = NULL;}
}int main()
{/*mal_loc();printf("-------------\n");cal_loc();printf("-------------\n");*/real_loc();return 0;
}
2、指針和常量:
- 常量指針:指針指向的值不可以通過指針改變;也就是說*p = num 這條語句失效。
- 指針常量:指針的指向不能變,也就是說 p = &num 這條語句失效。
// 常量指針
int a = 99;
int const* p1 = &a;
//*p1 = 100; // 會報錯// 指針常量
int b = 99;
int* const p2 = &b;
*p2 = 100;
//p2 = &a; // 會報錯
3、各種指針類型:一些指針在江湖上的諢名
- 萬能指針:void* p,這種萬能指針,可以強轉為其他任何類型的指針:
int *p2 = (int*)vp; // 顯式轉換:void* → int*
- 懸空指針:指針曾經指向有效的內存,但該內存已被釋放或失效。
int *p = (int*)malloc(sizeof(int));
*p = 42;
free(p); // 內存被釋放
// 避免懸空指針:p = NULL;
printf("%d", *p); // 懸空指針!p 指向的內存已無效
- 空指針:指向為NULL的指針,int *p = NULL; // 空指針
- 野指針:未初始化的指針,指向地址是隨機的,int *p; // 野指針!未初始化
3、指針和數組:
- ★首先搞明白什么是“指針數組”什么是“數組指針”這個很重要
- 指針數組:本質是數組,數組的每個元素是一個指針。
- 數組指針:指向一整個數組的指針。
- 具體的代碼實例:
// ---------指針數組:
int a = 10, b = 20, c = 30;
int *arr[3]; // 指針數組:3個元素的數組,每個元素是 int* 類型
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;printf("%d", *arr[1]); // 輸出 20(通過指針訪問 b 的值)
const char *names[] = {"Alice", "Bob", "Charlie"}; // 3個字符串的地址
printf("%s", names[0]); // 輸出 "Alice"
printf("%c\n", (names[0])[2]); // i
/*
names 是一個數組([] 表示數組)。
數組的每個元素是 const char*(指向常量字符的指針)。
因此,names 是一個 指針數組(數組的元素是指針),且這些指針指向 const char(常量指針)
*/// ---------數組指針
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5]; // 數組指針:指向一個包含 5 個 int 的數組
p = &arr; // p 指向整個數組 arrprintf("%d", (*p)[2]); // 輸出 3(解引用 p 得到數組 arr,再訪問下標 2)
- 通過二維數組的例子深度理解下數組和指針之間的關系:
// 獲取二維數組中第一個一維數組首地址
int arr[3][3] = {{1,2,3},{4,5,6},{7,8,9}
};
printf("*arr = %p\tarr[0]=%p\t&arr[0][0]=%p\tarr+0=%p\n", *arr, arr[0], &arr[0][0], arr + 0);
// 獲取二維數組中第二個一維數組首地址
printf("tarr[1]=%p\t&arr[1][0]=%p\tarr+1=%p\n", arr[1], &arr[1][0], arr + 1);
// 獲取二維數組中第三個一維數組首地址
printf("tarr[2]=%p\t&arr[2][0]=%p\tarr+2=%p\n", arr[2], &arr[2][0], arr + 2);
/*
理解 arr + 0/1/2作為地址的方式很簡單,對于arr數組而言,里面的一維數組就是它的元素,arr
就是這個以一維數組作為元素的數組的首地址,因此,arr每加一個單位,就是移動一個元素的位置。
*/
- 檢驗一下,在下面的代碼中,請說出常量指針是誰?指針數組又是誰?
const char *names[] = {"Alice", "Bob", "Charlie"};
// 在這個代碼中,指針數組是names,其中存放的都是常量指針
4、函數指針:
- 類比數組指針記憶,數組指針是指向整個數組的指針,函數指針就是指向整個函數的指針。
- 函數指針的定義
#include<stdio.h>int add(int a, int b) {return a + b;
}
int sub(int a, int b) {return a - b;
}
int main() {// 定義函數指針int (*Add)(int, int);Add = add;int (*Sub)(int, int);Sub = sub;// 借助指針調用printf("%d\n", Add(1, 2)); // 3printf("%d\n", Sub(3, 1)); // 2typedef int(*Func)(int, int); // 使用了這個重命名之后,就相當于用AddFunc代替了 int 函數名 ( int 參數1名, int 參數2名) 這種類型的指針名Func p1 = add;Func p2 = sub;printf("%d\n", p1(2, 1)); // 3printf("%d", p2(2, 1)); // 1return 0;
}
- 在給函數指針類型用typedef 起別名的時候,發現對typedef起別名時,簡單的類型還好寫,這種復雜類型寫起來就比較吃力了。因此總結如下:
1、給基本數據類型創建別名:typedef int Integer; // 為int起別名Integer
2、為指針類型創建別名:typedef int* IntPtr; // 為int*起別名IntPtr
3、為數組類型創建別名:typedef int IntArray5[5]; // 為數組類型創建別名(表示"包含5個int元素的數組")。例如:IntArray5 arr = {1, 2, 3, 4, 5}; // 等價于 int arr[5] = {1,2,3,4,5};
4、 ★為函數指針創建別名(最常用場景之一):typedef int (*CalcFunc)(int, int); // 定義一個函數指針類型(接收兩個int,返回int)
CalcFunc func1 = add; // 假設add是已經定義好的函數,func1是CalcFunc類型的函數指針;
上述函數指針其實就相當于:int (*func1)(int, int) = add;
- 總結:在給數組或者函數指針起別名的時候,方法就和定義數組或者定義函數時寫法一樣,這兩種相對其他數據類型起別名都比較特殊一點。
- 函數指針的用途之一:回調函數
- 回調函數就是往函數當中通過函數指針作為形參傳遞函數
- 我在學習回調函數的時候產生過這樣的疑問,為什么明明可以在一個函數當中就調用另一個函數,還非得用回調函數?經過學習我想通了,函數調用函數固然可以,但每次都是調用固定函數,而采用回調函數,就可以動態選擇傳入函數當中的函數。
#include<stdio.h>
// 調用函數的函數:作用是判斷數組當中有幾個1
int oneNum(int(*arr), int len, int (*Fun)(int)) {int i, count = 0;for (i = 0; i < len; i++) {if (Fun(arr[i])) {count += 1;}}return count;
}// 被調用的函數:作為條件判斷當前值是否為1
int oneNo(int a)
{if (a == 1) {return 1;}else {return 0;}
}
// c語言標準庫中快速排序qsort的回調函數
int cmp(void const * a, const void* b) {//return *(int const*)a - *(int const*)b; // 升序排列return *(int const*)b - *(int const*)a; // 降序排列
}int main() {int a[5] = { 1,5,4,2,3 };printf("%d\n", oneNum(a, 5, oneNo)); // 3qsort(a, 5,sizeof(int) , cmp);int i;for (i = 0; i < 5; i++) {printf("%d ", a[i]);}return 0;
}
總結:
- ★指針往細節學習,就會發現每個指針的大小都是固定的,一般電腦如果是64位的,指針大小就是64位(8個字節),如果電腦是32位的,指針大小就是32位(4個字節),這是因為指針存放的地址。指針前面的類型表示的是這個指針指向的空間當中存放的是什么類型,聲明指針類型,就是讓程序明白這一點,順著指針地址過去取值的時候取多大的空間,也就清清楚楚的告訴程序了。
- 數組指針&指針數組,其本質就是哪個詞在后面它的本質就是什么。
- 函數指針是個挺好用的東西,有了函數指針,我們就可以使用回調函數,向函數當中傳遞函數了。