文章目錄
- 一、內存和地址
- 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修飾指針變量
- 五、指針運算
- 5.1 指針±整數
- 5.2 指針-指針
- 5.3 指針的關系運算
- 六、野指針
- 6.1 野指針成因
- 6.2 如何規避野指針
- 七、assert斷言
- 八、指針的使用和傳址調用
- 8.1 strlen的模擬實現
- 8.2 傳值調用和傳址調用
一、內存和地址
1.1 內存的基本概念
在計算機中,CPU處理數據時需要從內存中讀取數據,處理后的數據也會放回內存。為了高效管理內存,內存被劃分為一個個大小為1字節的內存單元,每個內存單元都有唯一的編號,這個編號就是我們所說的地址,在C語言中也被稱為指針。
舉個例子:一棟宿舍樓有100個房間,給每個房間編上號(如101、102等),根據房間號就能快速找到房間。內存就像這棟宿舍樓,每個內存單元就像一個房間,地址則是房間號,有了地址,CPU就能快速找到對應的內存單元。
計算機中常見的存儲單位及換算關系如下:
- 1byte(字節)= 8bit(比特位)
- 1KB = 1024byte
- 1MB = 1024KB
- 1GB = 1024MB
- 1TB = 1024GB
- 1PB = 1024TB
1.2 編址的原理
CPU訪問內存中的某個字節空間,必須知道該字節空間的地址。計算機的編址是通過硬件設計實現的,就像鋼琴、吉他等樂器,制造商在硬件層面設計好,演奏者就能準確找到相應位置。
CPU和內存之間通過大量的線連接,其中一組重要的線是地址總線。32位機器有32根地址總線,每根線有0和1兩種狀態(表示電脈沖有無),32根地址線能表示2^32種不同的地址。地址信息通過地址總線傳給內存,內存根據地址找到對應數據,再通過數據總線傳入CPU寄存器。
二、指針變量和地址
2.1 取地址操作符(&)
在C語言中,創建變量的本質是向內存申請空間。
#include <stdio.h>
int main()
{int a = 10;return 0;
}
變量a
占用4個字節的內存空間,每個字節都有自己的地址。我們可以使用取地址操作符&
來獲取變量的地址,如&a
得到的是a
所占4個字節中地址較小的那個字節的地址。
2.2 指針變量和解引用操作符(*)
2.2.1 指針變量
通過取地址操作符&
得到的地址是一個數值,我們可以將其存儲在指針變量中。指針變量就是專門用來存放地址的變量。
#include <stdio.h>
int main()
{int a = 10;int* pa = &a; // 取出a的地址并存儲到指針變量pa中return 0;
}
2.2.2 指針類型的解讀
指針變量的類型由*
和前面的類型組成,如int*
表示該指針變量指向的是整型(int
)類型的對象。對于char
類型的變量ch
,其地址應存放在char*
類型的指針變量中。
2.2.3 解引用操作符
有了指針變量存儲地址后,我們可以使用解引用操作符*
通過地址找到對應的變量并進行操作。
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0; // 通過pa中存放的地址找到a,并將a的值改為0return 0;
}
這里*pa
就相當于變量a
,通過*pa
可以對a
進行修改,這為操作變量提供了另一種途徑。
2.3 指針變量的大小
指針變量的大小取決于地址的大小:
- 在32位平臺下,地址是32個比特位,指針變量大小為4個字節。
- 在64位平臺下,地址是64個比特位,指針變量大小為8個字節。
需要注意的是,指針變量的大小和其類型無關,在相同平臺下,所有指針類型的變量大小都是相同的。例如:
#include <stdio.h>
int main()
{printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(double*));return 0;
}
在32位環境下輸出結果均為4,在64位環境下輸出結果均為8。
三、指針變量類型的意義
雖然指針變量的大小和類型無關,但指針類型有著重要的意義。
3.1 影響解引用的權限
指針的類型決定了對指針解引用時的操作權限,即一次能操作的字節數。例如:
char*
類型的指針解引用只能訪問1個字節。int*
類型的指針解引用能訪問4個字節。
看下面兩段代碼:
// 代碼1
int main() {int a = 0x11223344;int* p = &a;*p = 0;return 0;
}
逐語句調試,代碼運行到15行時。int*
可以訪問四個字節,將四個字節都改為0
// 代碼2
int main() {int a = 0x11223344;char* p = &a;*p = 0;return 0;
}
逐語句調試,代碼運行到22行時。char*
只訪問訪問一個字節,將第一個字節改為0
3.2 影響指針 ± 整數的步長
指針的類型決定了指針向前或者向后走一步的距離。例如:
int* + 1
跳過四個字節(int大小),char* + 1
跳過一個字節(char大小)
3.3 void* 指針
void*
類型的指針可以接受任意類型的地址,可以理解為無具體類型指針(或者叫泛型指針)但它不能直接進行指針的±整數和解引用運算。通常用于函數參數部分,實現泛型編程的效果,以處理多種類型的數據。
四、const修飾指針
4.1 const修飾變量
被const
修飾的變量不能直接被修改。
#include <stdio.h>
int main()
{const int n = 0;n = 20; // 報錯,n不能被直接修改return 0;
}
此時變量具有常屬性,稱為常變量,但本質依舊是變量而不是常量。
在C++中被const
修飾則為常量。
但如果通過指針獲取其地址,還是可以修改該變量的值,這顯然打破了const
的限制,所以需要用const
修飾指針變量。
4.2 const修飾指針變量
const
放在*
的右邊:修飾的是指針變量本身,指針變量不可以再指向其他變量。但可以通過指針修改指向的內容。
int main()
{int a = 10;int b = 20;int * const p = &a;p = &b;//err*p = 100;//可以通過編譯return 0;
}
const
放在*
的左邊:限制指向的內容,不可以通過指針來修改,但可以修改指針指向的變量。
int main()
{int a = 10;int b = 20;int const* p = &a;p = &b;//可以通過編譯*p = 100;//errreturn 0;
}
五、指針運算
指針的基本運算有三種:
5.1 指針±整數
原理同本文3.2部分。
由于數組在內存中是連續存放的,知道第一個元素的地址后,通過指針±整數可以訪問數組中的其他元素。
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int* p = &arr[0];int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++){printf("%d ", *(p + i)); // 通過指針+整數訪問數組元素}return 0;
}
注意:
*(p+1)
不要寫成*p+1
,前者表示指針變量+1,后者表示p指向的內容+1- 在
sizeof()
中,輸入數組名arr
,計算整個數組的大小。
5.2 指針-指針
通過上述,我們可以明確:
指針1 + 整數 = 指針2
以此推理出
整數 = 指針2 - 指針1
類比“日期 - 日期”,得到之間的天數。兩個指針相減的結果是它們之間的元素個數,常用于計算字符串長度等場景。
strlen()
求字符串長度,統計字符串\0
之前字符個數- 數組名arr是數組首元素的地址。
arr
等價于&arr[0]
模擬實現strlen()
函數:
- 方法1 計數器
int my_strlen(char* s)
{int cnt = 0; //計數器while(*s != '\0'){cnt++;str++}return cnt; // 計算兩個指針之間的元素個數,即字符串長度
}
int main()
{printf("%d\n", my_strlen("abc")); //輸出3return 0;
}
- 方法2 指針 - 指針
int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s; // 計算兩個指針之間的元素個數,即字符串長度
}
int main()
{printf("%d\n", my_strlen("abc")); //輸出3return 0;
}
注意:
- 指針 - 指針 的前提時兩個指針指向同一塊空間!
例如:
int main()
{int arr[10] = {0};char ch[10] = {'0'};printf("%d\n",&ch[0] - &arr[0]);//errreturn 0;
}
- "日期 + 日期"沒有意義,同樣的,“指針 + 指針”也沒有任何意義。
5.3 指針的關系運算
指針與指針比較大小,其實就是地址與地質比較大小。
數組隨下標變大,地址由低變高。
int main()
{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) // 指針的大小比較,當p指向的地址小于arr+sz(相當于數組最后一個元素的地址)進入循環{printf("%d ", *p);p++;}//輸出 1 2 3 4 5 6 7 8 9 10return 0;
}
六、野指針
野指針就是指針指向內容是不可知的(不正確、隨機、沒有明確限制)
6.1 野指針成因
- 指針未初始化:局部變量指針未初始化時,其值是隨機的。
int main()
{ int *p;//此時p是局部變量,指針未初始化,默認為隨機值 *p = 20;return 0;
}
- 指針越界訪問:指針指向的范圍超出數組等申請的內存空間。
int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i=0; i<=11; i++){//當指針指向的范圍超出數組arr的范圍時,p就是野指針*(p++) = i;}return 0;
}
- 指針指向的空間釋放:返回局部變量的地址,該局部變量的空間在函數調用結束后會被釋放。
int* test()
{int n = 100;return &n;
}int main()
{int*p = test();printf("%d\n", *p);//此時test()調用完成,棧幀被銷毀, 內存被釋放return 0;
}
6.2 如何規避野指針
- 指針初始化:明確指向時直接賦值地址,否則賦值
NULL
(NULL
是值為0的標識符常量,該地址無法使用,讀寫地址也會報錯)。
int main()
{int* p = NULL;*p = 20//err
}
- 小心指針越界:不訪問超出申請內存范圍的空間。
- 及時置
NULL
并檢查:指針變量不再使用時置為NULL
,使用前判斷是否為NULL
。 - 避免返回局部變量的地址。
七、assert斷言
assert.h
頭文件中的assert()
宏用于在運行時確保程序符合指定條件,如果不符合就報錯終止運行。其表達式為真時程序繼續運行,為假時報錯并顯示相關信息。
assert(p != NULL);//確保 p為有效指針
assert()
斷言相對if
語句的優點:- 出現錯誤會直接報錯,指明在什么文件,哪一行
- 無需修改代碼就可以禁用
assert()
.可以通過在#include <assert.h>
前定義NDEBUG
宏來禁用assert()
語句.
- 缺點:
- 引入了額外的檢查,增加了程序運行時間
通常在Debug版本中使用,Release版本中禁用,以不影響程序效率。VS2022中release版會直接禁用assert
八、指針的使用和傳址調用
8.1 strlen的模擬實現
strlen
函數用于求字符串長度,統計的是字符串中\0
之前的字符個數。模擬實現如下:
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)//限制內容,不能被修改
{int count = 0;assert(str); // 確保 str不為NULLwhile (*str){count++;str++;}return count;
}
int main()
{int len = my_strlen("abcdef");printf("%zd\n", len);return 0;
}
求出的長度不可能是負數,因此返回值類型使用size_t
(無符號整型)更合適,打印應使用zd%
作為占位符。
8.2 傳值調用和傳址調用
-
傳值調用:實參傳遞給形參時,形參創建臨時空間接收實參。形參和實參是獨立的兩個空間。形參只是實參的一份臨時拷貝,對形參的修改不影響實參。
-
傳址調用:將變量的地址傳遞給函數,函數內部通過地址間接操作主調函數中的變量,可實現對變量的修改。
例如交換兩個整型變量的值,使用傳址調用:
#include <stdio.h>
void Swap(int* px, int* py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}
int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交換前:a=%d b=%d\n", a, b);Swap(&a, &b);printf("交換后:a=%d b=%d\n", a, b);return 0;
}
運行結果:
指針是C語言的精華,掌握好指針能讓我們在編程中更加得心應手。希望本文能幫助大家更好地理解指針的相關知識,后續還會有更深入的探討。