
前言本篇學習指針的相關內容包括內存和地址的相關概念指針基本概念定義調用數組與指針函數與指針二級指針野指針與懸空指針萬能指針等等相關內容。以及C語言的右左法則來輔助理解。一、基本概念1.1 內存與地址內存可以看作一列大小相等的格子他們連成一條“內存條”每個格子內能夠存放兩個16進制大小的數據即8bit數據每個格子又有一個地址編號記錄他們存在的位置。就像一個單元樓每層占用的地方一樣大內部如何構造存儲不同數據它的大小是固定的每層都有一個地址編號方便我們直接找到要去的地方。指針就是一種記錄地址并指向地址所在位置的類型。1.2 指針類型指針是一種專門用來處理“地址”的特殊數據類型本質就是地址地址就是內存單元的編號。1.3 定義格式基類型 *指針變量名基類型基類型表示的是指針所指向的地址所在空間的數據的類型。*在指針定義時是類型修飾符用來表示這個變量是指針類型。在運算時是解引用運算。指針變量名命名規(guī)則與標識符一致。// 舉例 int? p null; /* 表示定義了一個指針變量 p指向一個地址這個地址上的空間存放的是 int 型數據。*/1.4 指針的大小需要注意指針是一種“地址”類型它的大小與基類型無關它的大小與操作系統(tǒng)的地址總線相關。一個32位操作系統(tǒng)的地址總線位32位即4字節(jié)1字節(jié)8bit32位--32/84字節(jié)所以它的指針大小為4字節(jié)大小固定不變。同理64位操作系統(tǒng)的指針大小位8字節(jié)。1.5 解引用 *解引用分為三步且解引用的運算數類型必須為指針類型。1找通過指針找到地址所在位置2偏移通過基類型的大小圈定內存。如int型4字節(jié)就取4個字節(jié)的大小。3用將地址內的數據當作對應基類型的變量操作。1.6 實現被調修改主調指針可以實現被調修改主調。C語言中的傳遞都是值傳遞如果是一個普通變量通過值傳遞到函數內后函數內的變量接收這個值而這個變量在函數內的生命周期僅限變量定義到函數結束的過程所以無法做到普通變量的值修改。指針變量卻不同指針傳遞的是地址的值。只要有地址就能找到對應的數據就能直接對數據進行修改。1.7 野指針與懸空指針野指針沒有指向過有效內存地址是亂的。常見于沒有初始化指針。懸空指針指向過有效內存但內存沒了指針還留著舊地址。常見于指針指向的地址釋放后堆內存釋放未置空。解析舉例一個人A和另一個人B見面A把B的樣子記住了B走后A可以隨意修改他記住的B的樣貌對B沒有任何影響這就是普通變量的值傳遞。然后A在名片上知道了B的住址他通過住址指針找到了B并在B的臉上亂涂亂畫直接對B造成了影響這就是指針的被調修改主調通過一個住址就能對B進行修改。后來A不小心把名片搞臟了地址發(fā)生了改變變成一個并不存在的地址。如果A沒有驗證就按照這個地址去找會隨機找到一個地址這個地址是不確定的。即指針未初始化地址值是隨機的屬于野指針。后來A找回了B的地址但是B已經搬走了換成了別的人住此時A再按照地址找雖然能找到那個房子但是找不到B了。即指針指向的地址內的數據被釋放了給了“別人”但是指針還是指向那個地址此時指針屬于懸空指針。1.8 萬能指針 void *沒有定義指針的類型可以傳遞所有類型。然后通過強制轉換得到需要的類型。不能直接引用必須強制轉換。1.9 指針的基礎運算單個指針可以進行加減操作兩個指針之間只能在特定情況下做減法運算。其余的都是未定義行為。指針pi表示p移動i個基類型的地址p-i同理p1-p2表示兩個指針地址之間的差值。解析就類似一列相同的屋子的地址A、B、C他的占地大小我們就可以說是基類型大小。我們可以說A的前面第X個屋子是B后面第X個屋子是C但不能說A*X有什么含義。這是無法定義的。兩個屋子AB之間相減我們可以得到他們相距幾個屋子但相加也是沒有什么意義的。二、數組與指針2.1 指針傳遞數組的原因當我們需要傳遞一個數組數據時如果按照以前的方法 將數組數據通過循環(huán)一個個傳遞到函數內這樣的方式即麻煩又占用空間類比到其他的數據如果每次調用數據時都要傳遞他們的值是非常影響效率和空間的所以我們使用指針只需要傳遞他們的地址知道他們占用了多少空間就可以取得里面的數據。利用指針傳遞的方式提高了效率降低了空間占用率。2.2 整型一維數組指針在學習函數時我們知道數組的變量名代表了首元素地址而只要知道數組的長度就能得到數組的所有元素。2.2.1 定義所以我們可以直接定義一個指針變量記錄數組的首元素地址int[] a new int[10]; int[] p a;2.2.2 取值我們又知道數組的其他元素的下標含義就是距離首元素的距離。那么我們可以得到// 在 C# 中數組下標即元素位置 p a; // 首元素引用 p[1] a[1]; p[2] a[2]; // ……此時我們做到了用指針來表示數組。但這只是找到了地址如果需要使用地址里的值還需要進行解引用運算 *(p 1) a[1]。那么我們只需要傳入指針和數組的長度就能在函數里對數組進行修改了。此處需要注意指針變量的大小是固定的和操作系統(tǒng)有關。所以使用sizeof取得的值也是固定的這個值并不是數組的長度。2.2.3 示例// 遍歷打印 static void PrintArray(int[] arr) { foreach (int item in arr) { Console.Write(${item,3}); } Console.WriteLine(); }// 整型一維數組求最大值 static int MaxArray(int[] arr) { int max arr[0]; foreach (int item in arr) { if (item max) max item; } return max; }//逆序 void reverseArray(int *begin, int *end) { while (begin end) { swap(begin, end); begin; --end; } }//選擇排序 void selectSort(int *begin, int *end) { while (begin end) { int *i begin; while (i end) { if (*i *begin) swap(i, begin); i; } begin; } }//冒泡排序 void bubbleSort(int *begin, int *end) { while (begin end) { int *i begin; while (i end) { if (*i *(i 1)) swap(i, i 1); i; } --end; } }//插入排序 void insertSort(int *begin, int *end) { int *p; for (p begin 1; p end; p) { int t *p; int *q p; while (q begin *(q - 1) t) { *q *(q - 1); --q; } *q t; } }//二分查找 int *binaryFindIter(int *begin, int *end, int f) { while (begin end) { int *mid begin (end - begin) / 2; if (*mid f) end mid - 1; else if (*mid f) begin mid 1; else return mid; } return NULL; }//二分查找遞歸 int *binaryFindRec(int *begin, int *end, int f) { if (begin end) return NULL; int *mid begin (end - begin) / 2; if (*mid f) return binaryFindRec(begin, mid - 1, f); else if (*mid f) return binaryFindRec(mid 1, end, f); else return mid; }//快速排序 void quickSort(int *begin, int *end) { if (end begin) return; int *p begin; int *q end; int *k begin; while (begin end) { while (begin end *end *k) --end; while (begin end *begin *k) begin; swap(begin, end); } swap(k, begin); quickSort(p, end - 1); quickSort(begin 1, q); }2.3 字符型一維數組指針與整型數組同理但如果是單個字符的字符型我們按整型的傳入首地址指針和長度就可以。但是如果是字符串字符串有結束標志\0那么就不用傳入數組長度直接判斷到結束標志時結束就可以了。2.3.1 const與字符串常量區(qū)問題1字符串常量區(qū)在進行字符串處理時會出現報錯原因是字符串常量區(qū)的字符串是不可修改的只讀類型的常量。字符串常量區(qū)指的是當使用 雙引號定義字符串時這個定義出來的字符串存儲的位置在字符串常量區(qū)如果直接使用指針引用這個字符串的地址指向的是常量區(qū)地址是無法修改的所以會報錯。而數組在定義時會先開辟一塊數組大小的空間然后進行賦值運算將字符串常量區(qū)的值賦值到數組中。char *s hello; //錯誤 char s[10] hello;//正確2const關鍵字修飾在C語言中存在關鍵字const可以限定某個變量為只讀模式。存在就近原則。const char *p限定 *p 只讀內容不可改 char * const p限定 p 只讀指向不可改 const char * const p指向和內容都不可改。當存在只讀數據時盡量都加上const修飾防止修改不能修改的數據造成錯誤。2.3.2 示例//遍歷 void printStr(char *s) { while (*s ! \0) { putchar(*s); } putchar(\n); }//求字符串長度 int Strlen(const char *s) { const char *p s; while (*p ! \0) p; return (int)(p - s); }//strcpy char *Strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) { dest; src; } return ret; }//strcat char *Strcat(char *dest, const char *src) { char *ret dest; while (*dest ! \0) dest; while (*src ! \0) *dest *src; *dest \0; return ret; }//strcmp int Strcmp(const char *s1, const char *s2) { while (*s1 *s2 *s1 ! \0) { s1; s2; } return *s1 - *s2; }//strncmp int Strncmp(const char *s1, const char *s2, int n) { if (n 0) return 0; while (n 1 *s1 *s2 *s1 ! \0) { s1; s2; --n; } return *s1 - *s2; }//逆序 void reverseStr(char *begin, char *end) { while (begin end) { char t *begin; *begin *end; *end-- t; } }//字符轉整數 int strToInt(char *begin, char *end) { if (end begin) return *end - 0; return (*end - 0) strToInt(begin, end - 1) * 10; }//單詞逆序 void reverseWords(char *begin, char *end) { reverseStr(begin, end); char *left begin, *right begin; while (right end 1) { if (*right || *right \0) { reverseStr(left, right - 1); left right 1; } right; } }2.4 整型二維數組指針2.4.1 定義格式int *p[n] 本質是int[n] *p 基類型為數組類型 數組指針指向一維數組的指針2.4.2 取值定義時p 表示一個變量元素。*p 表示一個數組內的變量元素。**p 表示一個數組的數組的變量元素就是二維變量元素取值時p 表示二維數組首地址*p 表示行首元素的數據**p 表示數組元素數據pi 第i行的整行地址*pi第i行的首元素地址*pi 第一行首元素的下i個元素地址*pij i行j列的元素地址**pij i行j列的元素值二維數組理解為一維數組的數組a[2][3]表示兩行三列可以理解成{a1[3],a2[3]}兩個長度為3的數組在一個數組里。而一個a[3]里又有三個元素。類似于這樣a0[3]----a00 a01 a02a1[3]----a10 a11 a12取值時對p運算相當于二維數組中的行地址運算和一維數組移動類似,就是移動行。對*p運算相當于對行地址里的數據進行運算就是對列地址運算。對**p運算相當于再去列地址里的數據進行運算就是二維數組的每個元素進行運算了。2.4.3 示例//初始化 int a[3][4] {1,2,3,4,5,6,7,8,9,10,11,12}; int (*p)[4] a; // p 指向 a[0] 這一行 // *(*(pi)j) 等價于 a[i][j]//遍歷 void printMatrix(int (*a)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { printf(%3d, *(*(a i) j)); } putchar(\n); } }//指針函數求最大值 int *maxMatrix(int (*a)[4], int rows) { int *ret *a; for (int i 0; i rows; i) for (int j 0; j 4; j) if (*ret *(*(a i) j)) ret *(a i) j; return ret; }2.5 字符型二維數組指針字符型的二維數組在利用指針傳遞時如果是單個字符元素和整型相似。如果是字符串就相當于基類型為字符串的一維數組。char s[ 5 ][ 10 ]; char *p[ 10 ] s; 指針表示指向一個一維數組類型存放的基類型是長度10以內的字符串數據只需要對*p運算就能得到某一行的字符串數據。2.5.1 示例//定義 char s[5][10] {12,34,56,78,910}; char (*p)[10] s; // *(pi) 是第 i 行首地址類型 char*//遍歷 void printStrMatrix(char (*p)[10], int rows) { for (int i 0; i rows; i) { printf(%s\n, *(p i)); } }2.6 指針數組指針數組表示一個數組數組內的基類型是指針類型。2.6.1 示例//遍歷 void printPtrArray(char *s[], int len) { for (int i 0; i len; i) printf(%s\n, s[i]); }//逆序交換指針 void reversePtrArray(char *s[], int len) { for (int i 0; i len / 2; i) { char *t s[i]; s[i] s[len - 1 - i]; s[len - 1 - i] t; } }//選擇排序。交換指針 void selectSortStr(char *s[], int len) { for (int i 0; i len - 1; i) { int min i; for (int j i 1; j len; j) if (strcmp(s[j], s[min]) 0) min j; char *t s[i]; s[i] s[min]; s[min] t; } }//冒泡排序。交換指針 void bubbleSortStr(char *s[], int len) { for (int i 0; i len - 1; i) for (int j 0; j len - 1 - i; j) if (strcmp(s[j], s[j 1]) 0) { char *t s[j]; s[j] s[j 1]; s[j 1] t; } }//插入排序。交換指針 void insertSortStr(char *s[], int len) { for (int i 1; i len; i) { char *k s[i]; int j i - 1; while (j 0 strcmp(s[j], k) 0) { s[j 1] s[j]; --j; } s[j 1] k; } }//找最大字符串 char *maxStr(char **s, int len) { char *ret *s; for (int i 0; i len; i) if (strcmp(ret, *(s i)) 0) ret *(s i); return ret; }三、函數與指針3.1 函數指針函數指針表示一個指針指向了一個函數。3.1.1 定義int (*p)int int 表示一個指針變量。指向返回值為int類型帶有兩個int類型形參的函數函數名表示了它的地址類似數組去掉標識符剩下的就是函數的返回值類型。3.1.2 函數回調把函數的地址傳遞到另一個函數。應用場景通用算法qsortbsearch事件驅動編程底層開發(fā)GUI編程按鈕點擊事件中斷處理狀態(tài)機與命令模式經典示例qsortvoid qsort(void *base, size_t nmemb, size_t size,int (*compar)(const void *, const void *));描述qsort() 函數用于對一個包含 nmemb 個元素、每個元素大小為 size 的數組進行排序。base 參數指向數組的起始位置。數組中的內容將根據比較函數進行升序排序該比較函數由 compar 指向并通過兩個指向被比較對象的參數調用。比較函數必須返回一個整數值當第一個參數被認為小于、等于或大于第二個參數時分別返回小于零、等于零或大于零的值。如果兩個元素在比較中被視為相等則它們在排序后的數組中的順序是未定義的。qsort_r() 函數與 qsort() 函數相同唯一的區(qū)別在于比較函數 compar 需要傳入第三個參數。通過 arg 參數傳遞給比較函數的指針使得比較函數無需使用全局變量來傳遞任意參數因此具有可重入性可在多線程環(huán)境中安全使用。3.1.3 示例//函數指針 int add(int a, int b) { return a b; } int main(void) { int (*p)(int, int) add; // p 指向 add printf(%d\n, (*p)(1, 2)); // 輸出 3 return 0; }//排序回調 void selectSortCb(int *a, int len, int (*key)(int)) { for (int i 0; i len - 1; i) for (int j i 1; j len; j) if (key(a[i]) key(a[j])) { int t a[i]; a[i] a[j]; a[j] t; } } // 調用selectSortCb(a, 10, func2); 按平方值排序//函數指針數組實現四則運算 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 divi(int a, int b) { return a / b; } int (*ops[4])(int, int) { add, sub, mul, divi }; // 調用ops[i](10, 4)3.2 指針函數表示一個函數函數的返回值類型為指針。3.2.1 定義int *pintint 表示一個函數返回值為整型指針3.2.2 示例//返回最大元素地址 int *maxArray(int *begin, int *end) { int *ret begin; while (begin end) { if (*begin *ret) ret begin; begin; } return ret; }//返回最長字符串 char *maxStr(char **s, int len) { char *ret *s; for (int i 0; i len; i) if (strcmp(ret, *(s i)) 0) ret *(s i); return ret; }//Memcpy void *Memcpy(void *dest, const void *src, int n) { char *d (char *)dest; const char *s (const char *)src; while (n--) *d *s; return dest; }//二分查找返回元素地址 int *binaryFindIter(int *begin, int *end, int f) { while (begin end) { int *mid begin (end - begin) / 2; if (*mid f) end mid - 1; else if (*mid f) begin mid 1; else return mid; } return NULL; }四、二級指針4.1 如何理解C語言是值傳遞當一個指針作為形參傳入函數時這個指針的地址值被函數內的指針變量接收是局部變量只能修改它指向的地址內的數據因為函數內外的指針指向的是同一個地址修改同一個地址的值從而做到被調修改主調。 但如果對這個一級指針進行修改它依舊是被作為形參局部變量修改無法帶出它的指針值。于是當需要修改一級指針的指向時需要使用二級來修改。就是取一級指針的地址值通過二級指針指向一級指針地址值然后傳遞修改。一級指針作為函數形參被傳遞時函數內部修改的是一級指針指向的值。 這里一級指針有一片空間這個空間存放的是一個指向的地址的地址值就像定義一個變量內存里存放的是它的變量值這個變量被傳遞時只是傳遞它的值的大小傳遞到函數后重新開辟一塊空間來存放這個數據。所以我們無法對這個變量做出修改。所以我們無法修改這個一級指針因為函數收到的是一個地址值并新開辟一塊空間來存放這個地址值。 但是這個數據的地址沒”死亡“前是不會變化的它里面的數據就能通過函數修改。4.2 三個應用場景1被調修改主調2指針數組傳參3動態(tài)創(chuàng)建真正的二維數組在堆區(qū)申請一個 3 行 4 列 的二維數組非連續(xù)空間常用于字符串數組int **arr (int **)malloc(3 * sizeof(int *));// 申請 3 個行指針 for(int i 0; i 3; i) { arr[i] (int *)malloc(4 * sizeof(int));// 為每一行申請 4 個整型空間 } // 使用完之后必須逐行 free再 free(arr)4.3 易錯點不要越級賦值int a 10; int **pp a; // 報錯 a 的地址里裝的是 int而 pp 期望的是指針的地址。二級指針步長二級指針pp1移動的字節(jié)數卻決于操作系統(tǒng)對應的指針的大小??邕^的是一個一級指針的大小32位4字節(jié)64位8字節(jié)*pp類型*pp是int *類型值為地址4.4 示例//二級指針修改外部指針動態(tài)分配 void getMemory(char **p, int num) { *p (char *)malloc(num); } // 調用char *str NULL; getMemory(str, 100);//二級指針遍歷數組 void printByPtr(char **s, int len) { for (int i 0; i len; i) printf(%s\n, *(s i)); }//二級指針交換字符串指針 void swapPtr(char **s1, char **s2) { char *t *s1; *s1 *s2; *s2 t; }//二級指針選擇排序 void selectSortPtr(char **begin, char **end) { while (begin end) { char **min begin; for (char **i begin; i end; i) if (strcmp(*i, *min) 0) min i; swapPtr(min, begin); begin; } }//二級指針二分查找 char *binaryFindStr(char **begin, char **end, char *f) { while (begin end) { char **mid begin (end - begin) / 2; int cmp strcmp(f, *mid); if (cmp 0) begin mid 1; else if (cmp 0) end mid - 1; else return *mid; } return NULL; }五、右左法則在分析指針與其他的數據類型結合時常常分不清變量的含義到底是什么。此時使用右左法則來分析變量的類型是什么是整型指針數組指針函數指針函數指針數組5.1 核心標準5.2 分析步驟5.3 舉例1、普通的指針和數組 int *p1; 先向右沒有 再向左確認p是指針 再向左int指針指向的類型是int型數據 int arr1[5]; 向右[ ]變量是一個數組 向左int數組內的元素是int型 2、指針數組和數組指針 int *p2[5]; // 指針數組 先向右數組確認變量是一個數組類型 向左指針數組內的元素為指針類型 向左int指針指向的類型為int型 總結定義一個數組變量數組元素的基類型為指向整型的指針 指針數組一個基類型為指針的數組 int (*p3)[5]; // 數組指針 向右括號返回向左 向左指針變量類型為指針類型 再向右數組指針指向的是數組 再向左int數組元素的類型為int 總結定義了一個指針變量。指向一個基類型為整型的數組 數組指針指向數組的指針 3、指針函數和函數指針 int *f1(int); // 指針函數 變量向右int表示一個函數函數內的參數類型為int帶參函數 向左*指針表示函數返回值為指針 再向左int表示返回的指針指向整型 總結定義一個函數變量函數的返回值類型為一個指向整型的指針 指針函數一個函數返回值是指針 注意不要返回局部變量地址生命周期有限 int (*f2)(int); // 函數指針 先向右強制向左*指針表示變量為指針 向右int表示這個指針變量指向函數 再向左函數的返回值類型為整型 總結定義一個指針變量指向一個返回值類型為整型的函數 函數指針一個指針指向的是函數 4、進階函數指針數組 int (*f3[5])(int); // 函數指針數組 變量向右變量類型為數組類型 向左數組元素為指針 向右指針指向函數 向左函數返回值為int 總結定義一個數組數組基類型為指向一個返回值為int型的函數的指針 如果把變量外的去掉會報錯 int *f35 假設正確按右左法則分析向右是數組再向右是函數數組的基類型是函數再向左函數返回值是整型指針。 但是有一個問題函數的大小是不確定的所以無法定義函數數組。 5、返回數組指針的函數 int (*f5(int))[5]; // 返回數組指針的函數 向右變量類型為函數 向左函數返回值為指針 向右指針指向數組 向左數組類型為int型 總結定義一個函數變量返回值為指向一個基類型為int型的數組的指針 // 如果用 typedef 就一目了然 typedef int(*pArr)[5]; pArr f6(int); 6、二級指針 int **pp p; // 二級指針 變量向右向左變量類型為指針類型 向左指針指向的是一個指針 向左。指針指向的指針指向的是一個整型int 說明最后的數據類型為整型5.4 總結總結在判斷變量類型時先判斷它最先和誰結合確認它的變量類型然后第二個是修飾這個已經結合的變量第三個修飾已經修飾過的結合變量……依次類推。就是后結合的修飾上一級結合的。六、總結指針的主要作用是被調修改主調還有減少重復的數據傳遞優(yōu)化空間利用率。指針學習以“地址”為總開關先抓定義與解引用找地址、按基類型偏移、按類型使用再沿“一維數組→字符串→二維數組指針/指針數組→函數指針/指針函數→二級指針→右左法則”這條主線展開一維整型指針重點練遍歷、反轉、選擇/冒泡/插入/快速排序和二分查找迭代與遞歸字符型指針重點練長度、復制、連接、比較、反轉、轉整數和單詞逆序二維數組指針抓int (*p)[n]、char (*p)[n]的行指針本質指針數組抓“元素是指針、排序交換的是指針”函數指針抓回調與函數指針數組指針函數抓“返回指針的函數”二級指針抓“被調修改主調、指針數組傳參、動態(tài)二維”全程用右左法則辨析復雜聲明并始終防野指針/懸空指針、字符串常量區(qū)只讀、const修飾、\0終止、二分邊界begin end、釋放后置NULL、不返回局部變量地址這些坑。