文章目錄
- 前言
- 一、內存和地址
- 1.1 內存
- 1.2 究竟該如何理解編址
- 二、指針變量和地址
- 2.1 取地址操作符&
- 2.2 指針變量和解引用操作符*
- 2.2.1 指針變量
- 2.2.2 如何拆解指針類型
- 2.2.3 解引用操作符
- 2.3 指針變量的大小
- 三、指針變量類型的意義
- 3.1 指針的解引用
- 3.2 指針+-整數
- 3.3 void*指針
- 四、const修飾指針
- 4.1 const修飾變量
- 4.2 const修飾指針變量
- 4.2.1 指針指向的數據是常量
- 4.2.2 指針本身是常量
- 五、指針運算
- 5.1 指針+-整數
- 5.2 指針-指針
- 5.3 指針的關系運算
- 六、野指針
- 6.1 野指針的成因
- 6.2 如何規避野指針
- 6.2.1 指針初始化
- 6.2.2 小心指針越界
- 6.2.3 指針變量不再使用時,及時置NULL,指針使用之前檢查有效性
- 6.2.4 避免返回局部變量的地址
- 七、assert斷言
- 八、指針的使用和傳址調用
- 8.1 `strlen`的模擬實現
- 8.2 傳址調用和傳值調用
- 8.2.1 傳值調用
- 8.2.2 傳址調用
- 總結
前言
在C語言中,指針是一個非常重要且強大的概念。它允許程序員直接操作內存,從而實現高效的數據處理和靈活的程序控制。然而,指針也是C語言中較為復雜和容易出錯的部分。深入理解指針的原理和用法,對于每一個C語言開發者來說都至關重要。本篇博客將從內存和地址的基本概念入手,逐步深入探討指針變量、指針類型、指針運算、野指針以及指針在函數傳址調用中的應用等內容。
一、內存和地址
1.1 內存
計算機的內存就像一個巨大的倉庫,里面有很多很多的小格子,每個小格子都可以用來存放數據。這些小格子的大小和用途各不相同,有的可以存放整數,有的可以存放字符,還有的可以存放浮點數等等。
假設你有一個超市的貨架,貨架上有很多層,每一層都有很多個格子,每個格子都可以用來存放商品。這些格子的大小和用途各不相同,有的可以存放大型商品,如家電;有的可以存放中型商品,如食品;還有的可以存放小型商品,如化妝品。當你需要存放或者取用某樣商品的時候,你只需要知道對應的格子位置,就可以直接找到那個格子。
在計算機中,內存被劃分成一個個小的存儲單元,每個單元都有一個唯一的編號,這個編號就是內存地址。這些存儲單元可以用來存放各種各樣的數據,比如數字、文字、圖片等等。通過內存地址,程序可以訪問和操作特定存儲單元中的數據。
舉個例子,假設我們有一個變量 num
,它的值是10。編譯器會為這個變量分配一個內存地址,比如說 0x7ffee5e0
。這個內存地址就像是 num
這個變量在內存倉庫里的格子位置。我們可以用這個格子位置來找到這個變量的值,就像用貨架上的格子位置找到對應的商品一樣。
1.2 究竟該如何理解編址
編址就像是給內存倉庫里的每個格子分配一個唯一的門牌號。通過這個門牌號,我們可以準確地找到每個格子的位置,從而進行數據的存取操作。
在現實生活中,我們經常需要通過地址來找到某個地方。比如,你想要去一個朋友的家里,你需要知道他家的詳細地址,包括城市、街道、門牌號等。只有這樣,你才能準確地找到他的家。同樣地,在計算機中,通過內存地址,程序可以準確地找到對應的存儲單元。
內存地址通常以十六進制形式表示,例如 0x7ffee5e0
。每個內存地址對應一個存儲單元,存儲單元的大小由數據類型決定。例如,一個 int
類型的變量通常占用4個字節的內存空間,這意味著它需要連續的4個存儲單元。
舉個例子,假設我們有一個數組 arr
,它包含5個整數。編譯器會為這個數組分配連續的內存空間,每個整數占用4個字節。數組的首地址是 0x7ffee5e0
,那么數組中各個元素的地址如下:
arr[0]
的地址是0x7ffee5e0
arr[1]
的地址是0x7ffee5e4
arr[2]
的地址是0x7ffee5e8
arr[3]
的地址是0x7ffee5ec
arr[4]
的地址是0x7ffee5f0
通過這種方式,程序可以按照一定的規則訪問數組中的每個元素。
在程序中,我們經常需要通過指針來操作內存地址。指針變量存儲的是內存地址,通過指針可以間接訪問該地址中的數據。這就像通過朋友家的門牌號找到他的家,然后進行拜訪一樣。
通過這樣的類比,我們可以更直觀地理解內存和編址的概念。內存就像是一個巨大的倉庫,被劃分成一個個小的存儲單元,每個單元都有一個唯一的地址。編址就是給這些單元分配門牌號,方便程序進行數據的存取操作。這種機制使得程序能夠高效地管理和操作數據,為后續的指針操作奠定了基礎。
二、指針變量和地址
2.1 取地址操作符&
在C語言中,&
符號被稱為取地址操作符。它的作用是獲取一個變量在內存中的地址。這就像我們通過地圖應用獲取某個地點的坐標一樣。
舉個例子,假設我們有一個變量 num
,它的值是10。我們可以通過 &num
獲取它的內存地址。
int num = 10;
printf("num的地址是:%p", &num);
在這個例子中,&num
獲取了變量 num
的內存地址,并通過 printf
函數輸出。%p
是用于打印內存地址的格式說明符。
2.2 指針變量和解引用操作符*
2.2.1 指針變量
指針變量是一種特殊的變量,它存儲的是內存地址。我們可以把指針變量想象成一個快遞單號,快遞單號本身并不包含商品,但它能告訴你商品存放在哪個倉庫的哪個位置。
int num = 10;
int *p; // 聲明一個指針變量p,它可以存儲int類型的內存地址
p = # // 將num的地址賦值給指針變量p
在這個例子中,p
是一個指針變量,它保存了變量 num
的內存地址。
2.2.2 如何拆解指針類型
指針變量是有類型的,它決定了指針所指向的數據的類型和大小。指針類型需要與所指向的數據類型匹配,這樣編譯器才能正確地進行內存操作。
一個指針類型的聲明可以拆解為以下幾個部分:
- 指針所指向的數據類型(如
int
、float
、char
等) - 指針本身的類型(
*
表示這是一個指針)
例如,int *p;
表示 p
是一個指向 int
類型數據的指針。
2.2.3 解引用操作符
*
符號除了用于聲明指針變量外,還用于解引用操作。解引用操作符的作用是獲取指針所指向的內存單元中的值。這就像我們通過快遞單號找到商品所在的倉庫位置,然后取出商品一樣。
printf("%d", *p); // 輸出10,即num的值
在這個例子中,*p
表示獲取指針 p
所指向的內存單元中的值,也就是變量 num
的值。
2.3 指針變量的大小
指針變量的大小取決于計算機系統的架構。在32位系統中,指針變量通常占用4個字節;在64位系統中,指針變量通常占用8個字節。這是因為32位系統使用32位(4字節)的內存地址,而64位系統使用64位(8字節)的內存地址。
printf("指針變量的大小是:%zu字節", sizeof(p));
在這個例子中,sizeof(p)
獲取了指針變量 p
的大小,并通過 printf
函數輸出。%zu
是用于打印大小的格式說明符。
通過這樣的類比和解釋,我們可以更直觀地理解指針變量和地址的概念。指針變量就像是快遞單號,它本身并不包含數據,但它能告訴你數據存放在內存的哪個位置。通過取地址操作符 &
,我們可以獲取變量的內存地址;通過解引用操作符 *
,我們可以訪問指針所指向的內存單元中的值。這種機制使得程序能夠靈活地操作內存中的數據。
三、指針變量類型的意義
3.1 指針的解引用
指針的解引用操作符 *
用于獲取指針所指向的內存單元中的值。這就像我們通過快遞單號找到商品所在的倉庫位置,然后取出商品一樣。
int num = 10;
int *p = # // p是int類型的指針,指向num的地址
printf("%d", *p); // 輸出10,即num的值
在這個例子中,*p
表示獲取指針 p
所指向的內存單元中的值,也就是變量 num
的值。通過解引用,我們可以訪問和操作指針所指向的數據。
3.2 指針±整數
指針支持與整數進行加法和減法運算。這在遍歷數組或處理連續內存塊時非常有用。指針與整數相加或相減時,會根據指針的類型自動計算偏移量。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p指向數組的第一個元素for (int i = 0; i < 5; i++) {printf("%d ", *p); // 輸出數組元素的值p++; // 指針自增,指向下一個元素
}
在這個例子中,p
是一個指向 int
類型的指針,初始時指向數組的第一個元素。通過指針自增運算,我們可以遍歷整個數組。每次自增操作都會使指針移動到下一個元素的位置。
3.3 void*指針
void*
是一種通用指針類型,它可以指向任何類型的數據。void*
指針不與任何特定的數據類型關聯,因此在使用時需要進行類型轉換。
int num = 10;
void *p = # // p是一個void類型的指針,指向num的地址// 要訪問指針所指向的值,需要進行類型轉換
printf("%d", *(int*)p); // 輸出10,即num的值
在這個例子中,p
是一個 void*
指針,它指向了 num
的地址。由于 void*
指針不與任何特定的數據類型關聯,所以在訪問它所指向的值時,需要將其轉換為具體的指針類型(如 int*
),然后進行解引用操作。
通過理解指針變量類型的意義,我們可以更靈活地使用指針進行內存操作。指針的解引用允許我們訪問指針所指向的數據,指針與整數的運算使得遍歷數組等操作更加方便,而 void*
指針則提供了一種通用的指針類型,適用于多種場景。這些特性使得指針在C語言中非常強大和靈活。
四、const修飾指針
4.1 const修飾變量
const
關鍵字用于聲明常量,表示該變量的值在聲明后不能被修改。這就像我們去博物館參觀展品,這些展品是受到保護的,我們只能觀看,不能觸摸或改變它們。
const int num = 10; // 聲明一個常量num,值為10
// num = 20; // 錯誤:不能修改常量的值
在這個例子中,num
被聲明為一個常量,它的值在程序運行過程中不能被修改。
4.2 const修飾指針變量
const
修飾指針變量時,可以有以下兩種情況:
4.2.1 指針指向的數據是常量
const
修飾指針指向的數據,表示不能通過該指針修改所指向的數據。這就像我們拿到了一份只讀的文件,我們可以查看文件內容,但不能修改它。
const int *p; // p是一個指向常量int的指針,不能通過p修改它所指向的int值
int num = 10;
const int *p = # // p指向num的地址
// *p = 20; // 錯誤:不能通過const指針修改數據
在這個例子中,p
是一個指向常量 int
的指針,不能通過 p
修改它所指向的 int
值。
4.2.2 指針本身是常量
const
修飾指針本身,表示指針的值(即它所存儲的內存地址)不能被修改。這就像我們拿到了一張電影票,票上的座位號是固定的,我們不能隨意更換座位。
int *const p; // p是一個const指針,即指針本身是常量,不能改變它所指向的地址
int num = 10;
int *const p = # // p被初始化為指向num的地址
// p = &another_num; // 錯誤:不能修改const指針的值
在這個例子中,p
是一個 const
指針,一旦被初始化為指向某個地址,就不能再改變它所指向的地址。
通過使用 const
修飾指針,我們可以確保數據的完整性和指針的穩定性,避免不必要的修改和錯誤。這種特性在編寫安全、可靠的代碼時非常有用。
五、指針運算
5.1 指針±整數
指針支持與整數進行加法和減法運算。這就像你在圖書館的書架前,想要找到某本書的位置。假設你站在書架的起點(指針初始位置),然后向前走幾步(加整數)或者向后退幾步(減整數),就能到達目標書籍的位置。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p指向數組的第一個元素// 指針加法
p = p + 2; // p現在指向數組的第三個元素
printf("%d", *p); // 輸出3// 指針減法
p = p - 1; // p現在指向數組的第二個元素
printf("%d", *p); // 輸出2
在這個例子中,p
是一個指向 int
類型的指針。通過指針與整數的加法和減法運算,我們可以靈活地在數組中移動指針,訪問不同的元素。
5.2 指針-指針
兩個指針相減可以得到它們之間的元素個數。這就像你在一條街道上,想知道兩個房子之間相隔多少戶。通過計算兩個房子的門牌號之差,再除以每戶的平均間距,就能得到它們之間的戶數。
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr; // p1指向數組的第一個元素
int *p2 = arr + 4; // p2指向數組的第五個元素int distance = p2 - p1; // 計算兩個指針之間的元素個數
printf("%d", distance); // 輸出4
在這個例子中,p2 - p1
計算了兩個指針之間的元素個數,結果是4,表示從 p1
到 p2
之間有4個元素。
5.3 指針的關系運算
指針支持關系運算,如大于(>
)、小于(<
)、大于等于(>=
)、小于等于(<=
)等。這些運算用于比較兩個指針所指向的內存地址的大小。這就像你在比較兩個建筑物的高度,看哪個更高,哪個更矮。
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr; // p1指向數組的第一個元素
int *p2 = arr + 2; // p2指向數組的第三個元素if (p2 > p1) {printf("p2指向的地址比p1大");
} else {printf("p1指向的地址比p2大");
}
在這個例子中,p2 > p1
判斷 p2
是否指向的內存地址比 p1
大。由于 p2
指向數組的第三個元素,而 p1
指向第一個元素,所以條件成立,輸出 “p2指向的地址比p1大”。
通過指針運算,我們可以靈活地操作內存中的數據,實現數組遍歷、內存塊操作等功能。指針與整數的運算、指針之間的減法以及指針的關系運算,都是C語言中非常實用的特性,能夠幫助我們編寫高效、靈活的代碼。
六、野指針
6.1 野指針的成因
野指針是指指向不確定內存地址的指針。野指針可能是未初始化的指針,或者是已經釋放了內存但仍然指向該內存地址的指針。這就像你拿到了一張沒有明確地址的快遞單,或者快遞員已經送完快遞但你還在等待。
int *p; // 未初始化的指針,是一個野指針
printf("%d", *p); // 錯誤:訪問野指針可能導致程序崩潰
在這個例子中,p
是一個未初始化的指針,它指向的內存地址是隨機的,訪問這樣的指針可能會導致程序崩潰或出現不可預測的行為。
6.2 如何規避野指針
6.2.1 指針初始化
在聲明指針時,最好對其進行初始化,使其指向一個有效的內存地址或 NULL
。這就像在拿到快遞單時,先確認收貨地址是否正確,或者先將快遞單標記為“待發貨”。
int num = 10;
int *p = # // 初始化指針,使其指向有效的內存地址
或者
int *p = NULL; // 初始化指針為NULL,表示它目前不指向任何有效的內存地址
6.2.2 小心指針越界
指針越界是指指針超出了其合法的內存范圍。這就像你在圖書館找書時,超出了書架的范圍,可能會撞到墻壁或其他書架。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p指向數組的第一個元素p = p + 5; // 錯誤:指針越界,數組只有5個元素,索引從0到4
printf("%d", *p); // 錯誤:訪問越界的指針
在這個例子中,p + 5
超出了數組的范圍,訪問這樣的指針是不安全的。
6.2.3 指針變量不再使用時,及時置NULL,指針使用之前檢查有效性
當指針變量不再使用時,將其置為 NULL
,以避免懸掛指針(dangling pointer)。在使用指針之前,檢查它是否為 NULL
或指向有效的內存地址。這就像在快遞送達后,將快遞單標記為“已收貨”,避免再次使用無效的快遞單。
int num = 10;
int *p = # // p指向num的地址// 在某個操作后,num不再需要被訪問
p = NULL; // 將指針置為NULLif (p != NULL) {printf("%d", *p); // 安全地使用指針
} else {printf("指針無效");
}
6.2.4 避免返回局部變量的地址
函數中的局部變量在函數返回后會被銷毀,因此不要返回局部變量的地址。這就像快遞員送完快遞后離開,但收件人還在等待,導致無法取到快遞。
int* get_pointer() {int num = 10;int *p = #return p; // 錯誤:返回局部變量的地址,num的生命周期已經結束
}int main() {int *p = get_pointer();printf("%d", *p); // 錯誤:p是一個野指針,因為num的生命周期已經結束return 0;
}
在這個例子中,get_pointer
函數返回了一個局部變量的地址,當函數返回后,局部變量的生命周期已經結束,p
成為了一個野指針。正確的做法是使用動態分配的內存或靜態變量。
通過以上方法,我們可以有效規避野指針的出現,確保程序的穩定性和安全性。野指針是C語言中常見的錯誤來源,但通過良好的編程習慣和細心的操作,我們可以避免這些問題,編寫出高效、可靠的代碼。
七、assert斷言
assert
是一個宏,用于在調試階段檢查程序的狀態是否符合預期。如果斷言的條件為假,程序將終止并輸出錯誤信息。
#include <assert.h>int *get_pointer() {int num = 10;int *p = #return p; // 返回局部變量的地址,返回后num的生命周期已經結束
}int main() {int *p = get_pointer();assert(p != NULL); // 檢查p是否為NULL指針printf("%d", *p); // 錯誤:p是一個野指針,因為num的生命周期已經結束return 0;
}
在這個例子中,get_pointer
函數返回了一個局部變量的地址,當函數返回后,局部變量的生命周期已經結束,p
成為了一個野指針。使用 assert
檢查 p
是否為 NULL
指針,但實際上 p
并不是 NULL
,而是指向了一個無效的內存地址,因此程序可能會崩潰。
八、指針的使用和傳址調用
8.1 strlen
的模擬實現
strlen
是C標準庫中的一個函數,用于計算字符串的長度。通過指針,我們可以模擬實現 strlen
的功能。這就像你想要知道一本書有多少頁,你需要從第一頁開始,一頁一頁地數,直到看到最后一頁的頁碼。
#include <stdio.h>// 模擬實現strlen函數
int my_strlen(const char *str) {int length = 0;while (*str != '\0') { // 遍歷字符串,直到遇到空字符'\0'length++;str++;}return length;
}int main() {const char *str = "Hello, World!";printf("字符串長度是:%d", my_strlen(str)); // 輸出13return 0;
}
在這個例子中,我們通過指針 str
遍歷字符串,逐個檢查每個字符,直到遇到空字符 \0
為止。每檢查一個字符,長度計數器 length
就增加1。最終返回字符串的長度。
8.2 傳址調用和傳值調用
在函數調用中,C語言支持兩種參數傳遞方式:傳值調用和傳址調用。傳值調用是將實際參數的值傳遞給形式參數,函數內部對形式參數的修改不會影響實際參數。傳址調用是將實際參數的地址傳遞給形式參數,函數內部通過指針可以修改實際參數的值。
8.2.1 傳值調用
傳值調用就像你給朋友寫信,你把信的內容抄寫一份寄給他。他在回信中修改了信的內容,但你的原始信件內容不會改變。
#include <stdio.h>void swap(int a, int b) {int temp = a;a = b;b = temp;
}int main() {int x = 10, y = 20;swap(x, y);printf("x = %d, y = %d", x, y); // 輸出x = 10, y = 20return 0;
}
在這個例子中,swap
函數的參數 a
和 b
是通過傳值調用傳遞的。函數內部對 a
和 b
的修改不會影響主函數中的 x
和 y
。
8.2.2 傳址調用
傳址調用就像你把一份重要文件的地址告訴給朋友,他可以直接到那個地址去修改文件內容。這樣,文件的原始內容就會被改變。
#include <stdio.h>void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}int main() {int x = 10, y = 20;swap(&x, &y); // 傳址調用printf("x = %d, y = %d", x, y); // 輸出x = 20, y = 10return 0;
}
在這個例子中,swap
函數的參數 a
和 b
是通過傳址調用傳遞的。函數內部通過指針修改了 x
和 y
的值,因此主函數中的 x
和 y
的值發生了變化。
通過指針的使用和傳址調用,我們可以實現函數對變量的直接修改,這在很多場景下非常有用,比如交換變量值、修改結構體內容等。傳址調用提高了函數的靈活性和效率,避免了大量數據拷貝帶來的性能損失。
總結
指針是C語言中一個強大而靈活的概念,它允許程序員直接操作內存,實現高效的數據處理和靈活的程序控制。然而,指針也是C語言中較為復雜和容易出錯的部分。通過深入理解內存和地址、指針變量、指針類型、指針運算、野指針以及指針在函數傳址調用中的應用等內容,我們可以更好地掌握指針的使用方法,避免常見的錯誤和陷阱,編寫出高效、可靠的C語言程序。