為了更好地理解本篇文章的知識內容,讀者可以將以下文章作為補充知識進行閱讀?:
C語言————原碼 補碼 反碼 (超絕詳細解釋)-CSDN博客
C語言————二、八、十、十六進制的相互轉換-CSDN博客
C語言————斐波那契數列的理解和運用-CSDN博客
目錄
1. 內存和指針(地址)
1.1 內存的介紹
1.1.1內存的劃分
1.2 指針和地址
2. 指針變量和地址
2.1 取地址操作符&
2.2 指針變量
2.2.1 指針變量的定義
2.2.2 指針變量的類型
2.2.3 指針變量的大小?
2.3 解引用操作符*
?2.4 指針變量類型的意義
3. 指針計算
3.1 指針+-整數
3.2 指針-指針
3.3 指針的關系運算
4.野指針
4.1 野指針的成因和解決方法
4.1.1 指針未初始化
4.1.2 指針越界訪問
4.1.3 指針指向的空間釋放
5. void指針和assert斷言
5.1 void指針
5.2 assert斷言
6. 指針的應用
6.1 strlen的模擬實現
遞歸實現方法
迭代法?
6.2 傳值調用和傳址調用
7. 指針和數組
7.1 數組名的理解
7.2 指針訪問數組
7.3 數組傳參
7.4 數組指針
1. 內存和指針(地址)
1.1 內存的介紹
在計算機中,有各種各樣的數據,他們的存儲需要在內存中劃分空間,計算機中的內存空間大小是有限的。如果把數據比作水,內存就是用以承載水的容器,而我們知道在生活中容器的大小都是有限的。因此我們可以 更好地理解內存之于數據的意義。
1.1.1內存的劃分
一個整型變量a= 10存儲在程序中需要占據4個字節的內存空間大小,而數據的單位是多種多樣的,那我們在內存中應該按照何種單位進行空間劃分呢?
為了內存空間的高效管理,內存被劃分為一個個的內存單元,而每個內存單元的大小為1字節。
?
其中,一個bit位可以存儲一個二進制的0或1,一個字節可以存儲8個bit位,即一個內存單元能存儲8個bit位。?
在內存中存儲的數據需要通過CPU的處理,那么CPU又是如何讀取這些數據的呢?
1.2 指針和地址
我們打個比方,當我們在入住一個酒店時,服務員會給我們對應的 房號和房卡,這樣我們就能快速找到對應的房間。CPU和內存中的數據傳輸也是同樣的道理,他們之間通過很多的地址總線進行連接,每根線只有兩態,表示0或1(聯想到二進制),那么通過地址總線不同的脈沖組合形成的這樣一個二進制數字,就是對應數據的地址編碼,即地址。
?在C語言中,我們將這樣的地址起名為指針。
所以我們可以理解為:
內存單元的編號 == 地址 == 指針
2. 指針變量和地址
2.1 取地址操作符&
我們在學習scanf函數時知道,scanf函數除格式化字符串以外的參數代表的都是地址。
當我們在創建變量的時候,他會向內存申請空間,我們想知道他具體的地址編號時就需要用到操作符&,示例如下:
?
?如圖創建的整型變量a,通過查看內存,我們知道他的地址即指針為0x00000099588FFB14-0x00000099588FFB17(x64環境下),共四個字節;但如果我們對a的地址進行打印的話(x86環境下,更加便于查看),結果又是怎樣的呢?
?
我們會發現,他只打印了一個地址編號,這是因為一個數據進行存儲時,他的內存空間都是連續的,打印的往往是最低的那個地址編號,進而根據數據的內存大小,從低往高訪問對應數據。
?
經過多次嘗試我們會發現,每一次變量的地址都是在發生變化的,這是因為在每次運行程序時,操作系統的內存分配情況存在差異,所以分配給變量的具體內存地址是不同的。
2.2 指針變量
2.2.1 指針變量的定義
那么通過取地址操作符&得到的地址我們又該將他存儲在哪呢?為了方便提取這些指針的數據,C語言中用指針變量作為他的容器。如:
#include <stdio.h>
int main()
{int a = 10;int * pa = &a;//取出a的地址并存儲到指針變量pa中return 0;
此時的pa就是一個指針變量,而他的類型為int *;?
指針變量也是?種變量,這種變量就是?來存放地址的,存放在指針變量中的值都會理解為地址。
?在C語言中,地址就是指針,指針就是地址。
2.2.2 指針變量的類型
由上我們知道的一種指針變量類型為int *,我們應該怎么去理解他呢?
我們單獨看int * pa = &a這段語句可以知道,a為整型變量,pa存儲的是a的地址。由此知道:
int 代表pa存儲的指針所指向的數據a的類型(整型),* 表明pa為指針變量。
a和pa分別都在內存中劃分了屬于他們自己的空間。
?
那么字符類型的變量a,他的地址又該放在上面類型的指針變量中呢?
我們可以進一步推導如下:
int main()
{char a = '2';char* pc = &a;//字符指針pc,類型為char *return 0;
}
2.2.3 指針變量的大小?
在介紹內存中,我們知道地址的編號是由地址總線輸出的信號轉換得到的,32位機器假設有32根地址總線,他們產生的二進制序列作為一個地址,那么一個地址就是32個bit位,需要4個字節的存儲空間,指針變量的大小就是 4個字節。
同理64位機器,假設有64根地址線,一個地址就是64個二進制位組成的二進制序列,存儲起來就需要 8個字節的空間,指針變量的大小就是8個字節。
我們可以輸出如下的代碼進行測試:
#include <stdio.h>
//指針變量的??取決于地址的??
//32位平臺下地址是32個bit位(即4個字節)
//64位平臺下地址是64個bit位(即8個字節)
int main()
{printf("%zd\n", sizeof(char *));printf("%zd\n", sizeof(short *));printf("%zd\n", sizeof(int *));printf("%zd\n", sizeof(double *));return 0;
}
在x86環境下,即32位操作系統
?
在x64環境下,即64位操作系統?
?
結論:
1.?32位平臺下地址是32個bit位,指針變量大小是4個字節
2. 64位平臺下地址是64個bit位,指針變量大小是8個字節
注:指針變量的大小和類型是無關的,同樣指針類型的變量,在相同平臺下,大小都是相同的
2.3 解引用操作符*
那么對于指針變量,他們應該如何使用呢?這里我們將介紹一個關鍵的操作符——解引用操作符*
?他相當于是一把鑰匙,指針變量是對應的地址,指針變量指向的數據相當于被存儲在對應的地址,但我們無法直接操作他,因此需要通過鑰匙打開這道壁壘,這樣我們在不直接使用數據變量時,也能對數據進行相應的操作。示例如下:
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0;//找到變量a,并通過*打開操作他的權限return 0;
}
?我們會發現,通過解引用,*pa就相當于變量a,我們能夠對他進行重新賦值
?2.4 指針變量類型的意義
由2.1中我們知道,指針變量會存儲數據空間中最小的地址編號,整型指針變量解引用時,他會向上訪問四個字節的內存空間,我們思考一下,如果我們使用字符指針變量對&a進行訪問,能得到正確的數據么?
在int *整型指針下,我們打印讀取的整型變量數據是正確的(十六進制11223344轉為十進制為287454020?)
?當我們使用char *字符指針對整型變量n進行讀取時,我們發現,他僅讀取了n內存空間中的一個內存單元,數據為十六進制的44,轉為十進制為68。
在這里我們可以發現,不同的指針變量所訪問的內存空間大小也是不一樣的,因此學習指針變量的類型也是十分關鍵的。
注:在進行地址存儲時,指針變量的類型應該和地址的類型相對應,在代碼中我們可以看到(char*)&n,由于&n的類型為int *,我們用char *的指針變量接收他,兩者類型不同,為了使指針變量pc能夠順利存儲n的地址,我們需要對&n進行強制轉換,如果不進行強制轉換,編譯器會發出警告。(編譯器會進行隱式轉換類型)
結論:指針的類型決定了對指針解引?的時候有多大的權限(?次能操作幾個字節)。
如: char* 的指針解引用就只能訪問?個字節,而?int* 的指針的解引用就能訪問四個字節。
3. 指針計算
3.1 指針+-整數
我們觀察如下代碼的運行結果:
我們可以發現,整型指針變量+1,他的地址跳過了4個字節;字符指針變量+1,他的地址跳過了1個字節。
指針+1,就是跳過1個指針指向的元素。指針可以+1,那也可以-1。
跳過一個指針的空間大小就取決于指針的類型
3.2 指針-指針
此運算的前提條件:
- 參與減法運算的兩個指針必須指向同一數組中的元素(或數組最后一個元素的下一個位置)
- 兩個指針必須指向相同類型的數據(即指針變量類型一致)
運算結果:
結果是一個ptrdiff_t
類型(在<stddef.h>
中定義的有符號整數類型),表示兩個指針所指向元素之間的元素個數差,而不是字節數差。
?我們觀察如下代碼:
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = arr;int* p1 = &arr[9];printf("%d\n", (int)(p1 - p));//p1 - p為ptrdiff_t類型,%d無法讀取,需要強制轉換return 0;
}
?指針求差他們的結果就是兩個指針之間內存的元素個數,如下圖,兩個箭頭之間的元素有1,2,3,4,5,6,7,8,9。共9個整型元素,故輸出9。
3.3 指針的關系運算
我們知道內存的地址編碼是從低到高依次排布的,因為指針是可以用來比較大小的。
常見的關系運算符包括:== 、!= 、< 、> 、<= 、>= 。
這些關系符構建的指針關系運算可以作為語句的判斷條件;
指針的關系運算常用于數組遍歷或內存區間判斷。
int arr[5] = {1,2,3,4,5};
int *p;// 遍歷數組:當p未超過數組最后一個元素時繼續循環
for (p = &arr[0]; p < &arr[5]; p++) {printf("%d ", *p);
}
4.野指針
- 可能立即觸發程序崩潰(如段錯誤)
- 可能暫時正常運行,但在后續操作中引發錯誤
- 可能修改無關內存區域,導致數據損壞或程序邏輯錯誤
- 可能觸發安全漏洞,被用于緩沖區溢出等攻擊
4.1 野指針的成因和解決方法
4.1.1 指針未初始化
#include <stdio.h>
int main()
{ int *p;//局部變量指針未初始化,默認為隨機值*p = 20;return 0;
}
局部變量p未進行初始化,此時的p就是野指針。?
解決方法:
4.1.2 指針越界訪問
#include <stdio.h>
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;
}
當指針指向的范圍超出數組arr的范圍時,p就是野指針?。
4.1.3 指針指向的空間釋放
#include <stdio.h>
int* test()
{int n = 100;return &n;
}
int main()
{int*p = test();
printf("%d\n", *p);return 0;
}
當程序運行函數test()結束后,由于n為局部變量,他在內存中所占的空間就會被銷毀,導致指針p無法指向具體的變量,成為了野指針。
解決方法:
1. 指針變量不再使?時,及時置NULL,指針使?之前檢查有效性,置NULL的指針不指向任何有效內存;
2. 如造成野指針的第3個例?,不要返回局部變量的地址。
———————————————————————————————————————————
持續更新中
5. void指針和assert斷言
5.1 void指針
在指針類型中有一種類型為void *空類型指針(void在英文中的意思是空的),也可以理解為無類型的指針或者叫做泛型指針。
void*空類型指針難道是任何類型的數據地址都不能存放的指針么?恰恰相反,他可以接收任意類型地址。
void*空類型指針也有他的局限性:
1. 不能直接進行解引用;
2. 不能直接進行指針運算。
原因:空類型指針無法確定訪問的內存字節數目。
void* 空類型指針的核心作用就是作為“通用地址容器”,必須通過類型轉換才能進行解引用或者指針運算。
5.2 assert斷言
在C語言中,assert()是一個用于調試的宏(不是函數),主要作用是在程序運行時檢查某個條件是否為真。
基本用法:
1. 在使用assert()時,需要包含標準庫頭文件<assert.h>;
2. 他的語法形式為: assert(表達式);
3. 運行時判斷“表達式”是否為真(非0),若為真,assert()不做任何操作,程序繼續運行;若為假,assert()會打印錯誤信息(包含文件名、行號、表達式),并調用abort()函數終止程序。
如下所示:
abort()函數是用于異常終止程序的標準庫函數,他的作用是在程序發生嚴重錯誤時強制結束運行,不執行正常的清理操作:
#include <stdlib.h> //使用前需包含頭文件 void abort(void);//此為函數原型,無參數,無返回值 作用:立即終止當前程序,并返回一個非零狀態碼給操作系統,表示程序異常退出//可以聯想main()函數中的return 0;0 表示程序正常結束
?如果我們給上述報錯代碼加上 #define NDEBUG ,就可以關閉assert()宏對表達式的判斷。
#define NDEBUG//解除斷言
#include <assert.h>
int main()
{int a = 10;int* p = &a;p = NULL;assert(p != NULL);return 0;
}
?assert()他的作用和if語句十分相近,他們的對比如下:
assert()的缺點是,因為引入了額外的檢查,增加了程序的運行時間。?
在Release版本中,程序會將assert()優化掉,從而不影響用戶使用程序時的效率。
6. 指針的應用
6.1 strlen的模擬實現
我們可以思考試著解決這樣一個題目:
模擬實現庫函數strlen
注:strlen是用來測量字符串長度的一個標準庫函數,他的頭文件是<string.h>。他的工作原理是從字符串的起始地址開始遍歷,當遇到 '\0' 時就會停止,他計算的就是第一個字符到 '\0' 之間的字符個數(不包括\0)。
遞歸實現方法
int mystrlen(char* str)//構建自有函數mystrlen
{int count;if (*str == '\0'){count = 0;//趨近條件return count;}else {count = 1 + mystrlen(str + 1);//遞歸法return count;}
}
int main()
{char str[] = "sadas";printf("%d", mystrlen(str));return 0;
}
迭代法?
int mystrlen_2(char* str)
{int count = 0;while (*str)//當*str == '\0'時,判斷為0,循環結束{count++;str++;}return count;
}
int main()
{char str[] = "sadas";printf("%d", mystrlen_2(str));return 0;
}
在學習字符函數后,我們會繼續更新第三種方法?
6.2 傳值調用和傳址調用
指針最常見的使用,就是利用地址解引用,對相關數據進行一個修改,但是我們知道更直接的一種方法就是變量賦值,那什么情況下非指針不可呢?
我們觀察下面的代碼:
#include <stdio.h>
void Swap1(int x, int y)//內部交換x,y
{int tmp = x;x = y;y = tmp;
}
int main()
{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;
}
在自建的函數中,我們對x和y的值進行了交換,那么他會影響main函數中變量a和b的值么??
我們發現,a和b的值并沒有發生交換,這是為什么呢?
我們嘗試對這段代碼進行調試:
實參a和b同形參x和y他們的地址是不一樣的,指針指向數據存儲的區域,那我們能夠知道,x和y的數值交換,并不影響實參a和b的值,因為他們的地址不同。swap1函數實際上,只是把實參a和b的數值傳遞給了形參x和y,這種就叫做傳值調用。
結論:實參傳遞給形參的時候,形參會單獨創建?份臨時空間來接收實參,對形參的修改不影響實 參。
那我們要怎么通過函數實現a和b的數值交換呢?我們前面提到了指針,那是不是我們可以把a和b的指針傳遞過去呢?基于這個想法,我們對代碼進行修改:
#include <stdio.h>
void Swap2(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);Swap2(&a, &b);printf("交換后:a=%d b=%d\n", a, b);return 0;
}
成功實現了a和b的數值交換,我們使用指針作為參數傳遞的方式,叫做傳址調用。
7. 指針和數組
7.1 數組名的理解
7.2 指針訪問數組
7.3 數組傳參
7.4 數組指針
持續更新中
打怪升級中.........................................................................................................................................