指針詳解1
運行環境以Dev-C++、Visual Studio 2022、MacOS的命令行和Xcode為主
1.指針的概念
-
1.1 內存
-
內存是
CPU和硬盤之間交換數據的緩沖區,屬于存儲設備,斷電后數據會丟失。 動態運行著的程序會加載到內存中,如正在玩的游戲、正在聽的歌、正在編輯的課件、正在瀏覽的網頁等 -
計算機
CPU處理數據時,通常從內存中讀取數據,處理后的數據也會寫到內存中。內存被劃分為多個內存單元,每個內存單元的大小為1個字節Byte,即8個比特bit -
買手機時的
16GB + 256GB,16 表示運行內存(運存)為16GB
![image]()
-
-
1.2 地址
-
計算機的每個內存單元都會有個編號(相當于門牌號),
CPU通過該編號可以快速定位到內存空間 -
在計算機中將內存單元的編號稱為地址,C語言又賦予這個地址一個新名稱——指針
![image]()
-
計算機中的編址,并不是把每個字節的地址記錄下來,而是通過硬件設計完成。硬件之間通過“線”傳遞的信息協同工作,比如
CPU和內存之間有大量數據交互,兩者通過“線”連接起來。這些“線”共有三類-
地址總線
-
定位數據的“導航系統”
-
傳遞
CPU要訪問的內存單元或外設端口的地址信息,用于確定數據的來源或目的地。CPU讀寫數據前,先通過地址總線指定要操作的內存地址,內存根據地址定位對應的存儲單元,存儲單元中的數據通過數據總線傳入CPU寄存器 -
32位機器有32根地址總線,尋址范圍為
4GB;64位機器有64根地址總線,尋址范圍為18EB
-
-
數據總線
-
傳輸主句的“主干道”
-
在
CPU與內存、外設之間雙向傳輸實際的數據。如指令、運算結果、輸入輸出數據等 -
數據既可從
CPU發送到外部(如寫入內存),也可從外部傳輸到CPU(如讀取內存數據)
-
-
控制總線
-
協調操作的“指揮系統”
-
傳輸各種控制信號和狀態信號,協調 CPU 與內存、外設之間的操作時序和同步
-
![image]()
-
-
-
1.3 指針
-
含義與特征
-
指針也就是內存地址,指針變量是用來存放這些內存地址的變量
-
不同類型的指針占用的存儲空間長度相同,因為它們都是地址,地址的長度與操作系統有關,與數據類型無關
-
-
定義變量的實質
-
編譯器根據變量的數據類型分配相應長度的存儲空間,該存儲空間內存儲的是變量的值,而存儲空間的地址就是變量的地址,即指針
-
用戶僅通過變量名就可訪問存儲空間的方式稱為“直接訪問方式”。類似地,通過變量的地址解引用獲取變量的值,這種訪問方式稱為“間接訪問方式”
![image]()
-
-
2.指針變量和地址
-
2.1 取地址操作符
&- 分析變量在內存中的存儲地址
#include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... int a = 10; printf("%d\n", a); return 0; } // 上述代碼創建整型變量 a 期間會向內存申請 4B,用于存放整數10,每個字節在內存中都有對應的地址 // 變量 a 在內存中的地址開辟情況如下圖![image]()
- 獲取變量在內存中的存儲地址
#include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... int a = 0; &a; // 獲取 a 的地址 printf("%p\n", &a); return 0; } // &a 會取出變量 a 所占的 4 個字節中地址較小的字節地址 // 只要知道了第 1 個字節的地址,就可以順藤摸瓜訪問 4 個字節的數據,這在指針訪問數組時指針的移動體現地尤為明顯 -
2.2 指針變量
- 通過取地址操作符
&獲取的地址是一個數值,比如0xffc018,有時候也需要將該值存儲方便后期使用,像這樣的地址值不能用普通變量存儲,應當使用指針變量
#include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... int a = 10; int *pa = &a; // 取出 a 的地址并存儲到指針變量 pa 中 printf("%p\n", &a); return 0; } // 指針變量也是一種變量,主要用來存放地址,存放在指針變量中的值均會被系統解讀為地址 - 通過取地址操作符
-
2.3 拆解指針類型
-
pa左邊寫的是int *,*表示pa是整型變量,int表示pa指向的是整型類型的對象 -
C 語言對
*、類型和變量名之間的空格沒有嚴格語法限制,建議在*左側與類型名之間留一個空格
int a = 10; int *pa = &a; int*pa = &a; // 不建議采用這種方式,無空格,可讀性差 char ch = 'w'; char *pc = &ch;![image]()
-
-
2.4 解引用操作符
-
C語言中只要獲取了地址(指針),就可以通過該地址使用解引用操作符
*找到它指向的對象 -
借助指針修改代碼中變量的值在一定程度上可提高寫代碼的靈活性,并提升程序的運行效率
#include <stdio.h> int main() { int a = 100; int *pa = &a; *pa = 0; // *pa 即通過 pa 中存放的地址找到指向的空間,空間中存儲了變量 a 的值,由 10 改為 0 printf("%d\n", a); return 0; } -
-
2.5 指針變量的大小
-
假設 32 位機器有 32 根地址總線,每根地址線發出的電信號轉為數字信號后是 1 或 0。將 32 根地址線產生的二進制序列當做一個地址,共 32 bit,需要 4 個字節才能存儲
-
類似地,假設 64 位機器共 64 根地址線,一個地址就是 64 個二進制位組成的二進制序列,需要 8 個字節才能存儲
-
既然指針用來存放地址,那么指針變量的大小也應該為 4 或 8 個字節,具體取決于系統是 32 位平臺還是 64 位平臺
#include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... printf("%zd\n", sizeof(char *)); printf("%zd\n", sizeof(short *)); printf("%zd\n", sizeof(int *)); printf("%zd\n", sizeof(double *)); return 0; } // 由運行結果可知:1.運行系統為 x_64 平臺;2.指針變量的大小與類型無關。只要是指針類型的變量在同一平臺下,大小均相同 // 思考:既然指針變量的大小和類型無關,為什么還要有各種各樣的指針類型呢?![image]()
-
-
2.6 指針變量類型的意義
-
1.指針的解引用
- 案例分析
// 觀察以下代碼在調試時內存的變化 // 代碼1 #include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... int n = 0x11223344; int *pi = &n; *pi = 0; return 0; } // 代碼2 #include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... int n = 0x11223344; char *pc = (char *)&n; *pc = 0; return 0; }-
代碼1運行前后的內存數據修改情況
-
將變量 n 的 4 個字節全部改為 0,4 個字節對應整型數據的長度
-
int *型指針的解引用可訪問 4 個字節
-
![image]()
![image]()
-
代碼2運行前后的內存數據修改情況
- 將變量 n 的第 1 個字節改為 0,1 個字節對應字符型數據的長度
char *型指針的解引用只能訪問 1 個字節
![image]()
![image]()
-
2.指針與整數的加減運算
- 案例分析
#include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... int n = 10; char *pc = (char *)&n; int *pi = &n; printf("&n = %p\n", &n); printf("pc = %p\n", pc); printf("pc+1 = %p\n", pc + 1); // char* 類型的指針變量加 1 跳過 1 個字節 printf("pi = %p\n", pi); printf("pi+1 = %p\n", pi + 1); // int* 類型的指針變量加 1 跳過 4 個字節 return 0; } // 這就是指針變量的類型差異帶來的變化,指針加 1 即跳過 1 個指針指向的元素 // 指針可以加 1,也可以減 1,指針的類型決定了指針向前或向后走一步有多少距離![image]()
-
3.
void *指針-
無具體類型的指針(泛型指針),可以用來接受任意類型地址
-
泛型指針不能直接進行指針的加減整數和解引用運算
-
泛型指針通常使用在函數參數中,用于接收不同類型數據的地址,從而實現泛型編程的效果
![image]()
![image]()
-
-
3.const修飾指針
-
3.1
const修飾變量-
變量可以修改,將變量的地址賦值給指針變量,就可以通過指針變量修改變量的值
-
const關鍵字可以限制變量的屬性,使得變量的值不能被修改,即為常變量
![image]()
![image]()
-
-
3.2
const修飾指針變量const在*的左邊,修飾的是指針指向的內容,此內容不能通過指針修改。但指針變量本身的內容(指向)可變
![image]()
const在*的右邊,修飾的是指針變量本身,保證指針變量的內容(指向)不能修改,但指針指向的內容可變
![image]()
![image]()
4.指針運算
-
4.1 指針加減整數
- 數組的元素值在內存中連續存放,只要確定了首元素地址,就可以順藤摸瓜找到后方所有元素
#include <stdio.h> int main(int argc, const char * argv[]) { int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p = &arr[0]; int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); // p+i 就是指針加整數 } return 0; } // 程序打印數組的所有內容 -
4.2 指針減指針
-
相減的前提是兩個指針指向了同一塊區間
-
指針減指針的絕對值是兩指針間元素的個數
#include <stdio.h> size_t my_strlen(char *s) { char *p = s; while (*p != '\0') { p++; } return p - s; } int main(int argc, const char * argv[]) { printf("%zd\n", my_strlen("abcdef")); // 6,即字符串的長度 return 0; } -
-
4.3 指針的關系運算
- 將指針和數組名、數組長度關聯
#include <stdio.h> int main(int argc, const char * argv[]) { int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p = &arr[0]; int sz = sizeof(arr) / sizeof(arr[0]); while (p < arr + sz) { // 指針的大小比較 printf("%d ", *p); p++; } return 0; } // 程序打印數組的所有內容
5.野指針
-
5.1 野指針的成因
- 野指針就是指針指向的位置不可知,即隨機的、不正確的、沒有明確限制的。指針未初始化、指針越界訪問、指針指向的空間沒有釋放都有可能產生野指針
// 1.指針未初始化 #include <stdio.h> int main(int argc, const char * argv[]) { int* p; // 局部變量指針未初始化,默認為隨機值 *p = 20; return 0; } // 2.指針越界訪問 #include <stdio.h> int main(int argc, const char * argv[]) { int arr[10] = {0}; int* p = &arr[0]; int i = 0; for (i = 0; i <= 11; i++) { *(p++) = i; // 當指針指向的范圍超出數組 arr 時,p就是野指針 } return 0; } // 3.指針指向的空間釋放 #include <stdio.h> int* test(void) { int n = 100; return &n; } // return &n 返回的是已經被釋放的內存地址,main 函數中接收該地址的指針 p 就成了 野指針 int main(int argc, const char * argv[]) { int* p = test(); // n的空間被釋放, p為野指針 printf("%d\n", *p); // 打印結果有可能是 100,但這不能說明程序正確 return 0; } // test函數返回后,main函數緊接著訪問 *p 以讀取 n 原內存地址中的數據 // 由于中間沒有其他函數調用或內存操作,這塊被釋放的內存還沒被新的數據覆蓋,仍然保留著100這個值 // 因此,printf 可能恰好讀取到了殘留的舊值,表現為 “運行正確”。![image]()
-
5.2 規避野指針的方法
-
1.指針初始化
- 若明確知道指針的指向地址就直接賦值,否則給指針賦值
NULL NULL是 C 語言中定義的一個標識符常量,值為 0。0 也是地址,該地址無法使用,讀寫該地址會報錯
#include <stdio.h> int main(int argc, const char * argv[]) { int num = 10; int* p1 = # int* p2 = NULL; // 初始化為 NULL,避免生成野指針 return 0; } - 若明確知道指針的指向地址就直接賦值,否則給指針賦值
-
2.避免指針越界
- 一個程序向內存申請了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是越界訪問
#include <stdio.h> int main(int argc, const char * argv[]) { int arr[3] = {10, 20, 30}; int *p = arr; // 正常訪問數組元素(索引 0-2) printf("正常訪問:\n"); for (int i = 0; i < 3; i++) { printf("arr[%d] = %d\n", i, *(p + i)); } // 指針越界訪問(索引3及以上,超出數組范圍) printf("\n越界訪問:\n"); for (int i = 3; i < 6; i++) { // 此時p + i已經指向數組外的無效內存(野指針狀態) printf("p + %d 指向的內存值:%d(地址:%p)\n", i, *(p + i), (void*)(p + i)); } return 0; }![image]()
-
3.不再使用的指針變量及時置
NULL,指針使用前檢查有效性- 當指針變量指向一塊區域時,可以通過指針訪問該區域。后期不再使用這個指針訪問空間時,可以將之置為
NULL - 約定俗成的一個規則:只要是
NULL就不去訪問,使用指針之前可以判斷指針是否為NULL
#include <stdio.h> int main(int argc, const char * argv[]) { int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p = &arr[0]; int i = 0; for (i = 0; i < 10; i++) { *(p++) = i; } // 此時p已經越界了,可以將p置為NULL p = NULL; // 下次使用的時候,判斷p不為NULL的時候再使用 p = &arr[0]; // 重新讓p獲得地址 if (p != NULL) { // ... } return 0; } - 當指針變量指向一塊區域時,可以通過指針訪問該區域。后期不再使用這個指針訪問空間時,可以將之置為
-
4.避免返回局部變量的地址
-
理解函數棧幀的生命周期和野指針的特性。以 5.1 第 3 段代碼為例,運行 “結果正確” 是因為野指針指向的內存數據尚未被覆蓋,屬于偶然現象,而非代碼邏輯正確。正確的做法是避免返回局部變量的地址
-
解決方法
-
使用靜態變量
static int n = 100; -
動態分配內存
int* n = malloc(sizeof(int)); *n=100; return n; -
由調用者傳入指針參數
void test(int* p) { *p = 100; }
-
-
-
6.assert斷言
-
6.1 定義與運行原理
assert.h頭文件定義了宏assert(),用于在運行時確保程序符合指定條件。如果不符合,就報錯終止運行,這個宏常被稱為“斷言”
assert(p != NULL);-
程序運行到上述代碼時,驗證變量
p是否等于NULL。如果確實不等于NULL,程序繼續運行,否則就會終止運行,并給出報錯信息提示 -
assert()宏接收一個表達式作為參數。若表達式值為真,assert()不會產生任何作用,程序繼續運行。若表達式為假,assert()就會報錯
-
6.2 使用斷言的優勢
-
對程序員非常友好,不僅能自動標識文件和出問題的行號,還有無需更改代碼就能開啟或關閉
assert()的機制 -
若確認程序沒有問題,不需要再做斷言,就在
#include <stdio.h>前面定義一個宏NDEBUG,再重新編譯程序,編譯器會禁用文件中所有assert()語句
#define NDEBUG #include <assert.h> -
-
6.3 使用斷言的劣勢
-
引入了額外的檢查,增加了程序的運行時間
-
通常在
Debug版本中使用,有利于程序員排查問題;Release版本中選擇禁用斷言,部分IDE會在Release版本中優化掉斷言,不影響用戶使用程序的效率
-
7.指針的使用和傳址調用
-
7.1
strlen的模擬實現-
庫函數
strlen的功能是求字符串長度,統計的是字符串中\0之前的字符個數 -
函數原型為
size_t strlen(const char *str),參數str接收一個字符串的起始地址,然后開始統計字符串中\0之前的字符個數 -
模擬實現的過程為從起始地址開始逐個字符向后遍歷,只要不是
\0字符,計數器就加 1,直到\0就停止
#include <stdio.h> #include <assert.h> size_t my_strlen(const char *str) { int count = 0; assert(str); while (*str) { count++; str++; } return count; } int main(int argc, const char * argv[]) { size_t len = my_strlen("abcdef"); printf("%zd\n", len); return 0; } -
-
7.2 傳值調用
- 實質:實參的值傳遞給形參的時候,形參會單獨創建一份臨時空間來接收實參,對形參的修改不影響實參
#include <stdio.h> void Swap1(int x, int y) { int temp = x; x = y; y = temp; } int main(int argc, const char * argv[]) { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交換前: a = %d b = %d\n", a, b); Swap1(a, b); printf("交換后: a = %d b = %d\n", a, b); return 0; }![image]()
![image]()
-
運行結果分析
-
1.在
main()函數內部創建了a和b,a的地址為0x0098fadc,b的地址為0x0098fad0。調用Swap1()函數時,將a和b傳遞給了Swap1()函數 -
2.在
Swap1()函數內部創建了形參x和y接收a和b的值,但x的地址是0x0098f9f8(不同于a的地址),y的地址是0x0098f9fc(不同于b的地址),即x和y是獨立的空間 -
3.因此在
Swap1()函數內部交換x和y的值,不會影響a和b。當Swap1()函數調用結束后回到main()函數,a和b并未交換 -
4.
Swap1()函數運行期間接收了來自main()函數的實參值,這種調用方式稱為傳值調用 -
5.如何修改代碼才能運行出正確的結果?其實只要保證運行
Swap1()函數時內部操作的是a和b就可以,結合指針的知識,在main()函數中將a和b的地址傳遞給Swap1()函數,就可以通過地址間接操作main()函數中的a和b
-
-
7.3 傳址調用
-
實質:使得其他函數與主函數之間建立真正的聯系,在函數內部課修改主調函數中的變量
-
使用情形:若函數中只是需要主調函數中的變量值,可采用傳值調用;若函數內部要修改主調函數中的變量值,可以采用傳址調用
#include <stdio.h> void Swap2(int *px, int *py) { int temp = 0; temp = *px; *px = *py; *py = temp; } int main(int argc, const char * argv[]) { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交換前: a = %d b = %d\n", a, b); Swap2(&a, &b); // 將變量的地址傳給了Swap2 printf("交換后: a = %d b = %d\n", a, b); return 0; }![image]()
-

























浙公網安備 33010602011771號