指針
預備知識
內存地址
-
字節:字節是內存的容量單位,英文名Byte,1Byte=8bits
-
地址:系統為了便于區分每一個字節面對它們的逐一進行編號(編號是唯一的),稱為內存地址,簡稱地址。int a=5;
基地址(首地址)
-
單字節數據:對于單字節數據而言,其地址就是其字節編號。舉例:char a='A'
-
多字節數據:對于多字節數據而言,其地址是所有字節中編號最小的那個,稱為基地址()
取地址符
-
每個變量都是一塊的內存,都可以通過取址符
&
獲取其地址。 -
例如:
?int a=100;printf("整型變量a的地址是:%p\n",&a);//64位系統,是12位16進制int c='x';printf("字符變量c的地址是:%c");
-
主語:
-
雖然不同的變量的尺寸是不同的,但是它們的地址尺寸是一致的。
-
不同的地址雖然形式上看起來是一樣的,但由于它們代表的內存尺寸和類型都不同,因此它們在邏輯上嚴格區分的。
-
為什么要引入指針
-
為函數修改實參提供支持。
-
為動態內存管理提供支持。
-
為動態的的數據結構(鏈表、隊列等)提供支持
-
為內存訪問提拱了另一種途徑。
變量指針與指針變量
指針概念
內存單元與地址機制
-
內存單元劃分
-
系統將內存劃分為連續的基本存儲單元,每個單元的容量為1字節(8bits)
-
每個內存單元擁有唯一編號。稱為內存地址(十六進制表示:)
-
-
變量存儲特性
-
變量根據數據類型占據不同數量的內存單元:
-
char
類型占1個字節(1個單元) -
int
類型占4個字節(4個單元) -
double
類型占8個字節(8個單元)
-
-
變量的基地址(首地址)是其首個內存單元的地址(首地址一般是這個一組編號中最小的那個)
-
變量指針與指針變量
對比維度 | 變量指針 | 指針變量 |
---|---|---|
本質 | 內存地址(首地址\基地址) 變量指針其實就是變量的首地址 | 存儲地址的普通變量 |
操作符 | & (取地址符) | * (聲明符,解引用符,如:int *p ) |
代碼示例 | &a (獲取變量a地址) | int* p =&a |
核心特性 | 不可修改(地址由系統分配) | 可修改指向(p=&b ) |
指向:
指針變量中存放 誰 的地址,就說明該指針變量指向了 誰
指針的尺寸
系統類型 | 指針尺寸 | 地址位數 | 十六進制顯示長度 |
---|---|---|---|
32位系統 | 4字節(int) | 32bit | 8位(如0x0804A000) |
64位系統 | 8字節(long) | 48bit | 12位(如0x7FFDEADBEEF) |
指針的本質
-
變量指針:數據的門牌號(&a)
-
指針變量:存儲門牌號的筆記本(int *p)
-
指向操作:通過門牌號訪問數據(*p)
?int a=10;printf("%d\n",a);//訪問a的值?int *p=&a;printf("%d\n",*p);//訪問p指向a的值,其實就是訪問a的值
小貼士:
Linux系統中打印地址時,最多顯示12個十六進制數,為什么? Linux64位操作系統中,一個內存地址占8個字節,一個字節8bit位,所以一個地址88=64bit位,每4個bit可以表示1個十六進制數; 64個bit位用十六進制表示最多有16個數值位; 系統為了尋址方便,默認當前計算機系統沒必要尋址64bit為,只尋址了48個bit為,所以用12個十六進制數表示一個地址 二進制: 01001010 十六進制:0x4A 416+10=74 注意: 在Linux64位操作系統中,指針類型的變量占8個字節的內存空間 在Linux32位操作系統中,指針類型的變量占4個字節的內存空間
內存數據的存取方式
在c語言中對內存數據(變量、數組元素等)的存取有兩種方式:
直接存取
-
通過基本類型(整型、浮點型、字符型)的變量,訪問這個變量代表內存空間的數據
-
通過數組元素的引用,訪問這個引用代表的內存空間數據
?//基本類型變量int a=10;//存printf("%d\n",a);//取?//數組元素int arr[]={11,12,13};//存arr[0]=66;//存printf("%d\n",arr[0]);//取
間接存取
-
通過指針變量,間接訪問內存中的數據
-
解引用符
*
:讀作聲明符或者解引用符。如果*
前面有數據類型,讀作聲明指針;如果*
前面沒有數據類型,讀作解引用。案例
? #include <stdio.h>int main(int argc,char *argv[]){// 定義一個普通變量int a = 3;?// 定義一個指針變量,并賦值int* p = &a; // *p前面的數據類型是用來修飾 = 后面的 a的類型// 訪問變量a,直接訪問printf("直接訪問-%d\n",a);// 訪問變量a,通過指針變量p訪問,間接訪問printf("間接訪問-%d\n",*p);// *p 叫做解引用// 訪問指針變量p的值,也就是訪問變量a的地址 %p 輸出 變量的地址printf("地址訪問-%p,%p,%p\n",p,&p,&a);return 0;}??
變量指針與指針變量
指針變量的定義
語法:
?數據類型 *變量列表
舉例:
?int a;//普通變量,擁有真實的數據存放空間int *a_,*b_;//指針變量,無法存放數據,只能存儲其他變量的地址
注意:指針變量的值只能是8、12位的十六進制整數
注意:
①雖然定義指針變量
*a
,是在變量名前加上*
,但是實際變量名依然為a
,而不是*a
②使用指針變量間接訪問內存數據時,指針變量必須要明確的指向。(指向:指針變量存放誰的地址,就指向誰)
③如果想要借助指針變量間接訪問指針變量保存的內存地址上的數據,可以使用指針變量前加
*
,來間接返回訪問。指針變量前加*
,如果不帶數據類型,就稱之為對指針變量解引用?int i=5,*p;p=&i;//將i的地址賦值給指針變量p;printf("%lx,%p,%p\n",p,p,&p); %p-&p(p自己的地址) ?%p- p(i對應的地址)printf("%d\n",*p);解引用所儲存變量空間的地址*p=10;//間接給p對應的地址上的數據空間賦值,也就是給變量i賦值printf("%d,%d\n",*p,i);//10 10
④指針變量只能指向同類型的變量,借助指針變量訪問內存,一次訪問內存
大小是取決于指針變量的類型
?int a=10;int *p=&a;//*p前面的類型是p指向變量a的類型,這個類型要么完全一致,要么可以轉換
⑤指針變量在定義時可以初始化,這一點和普通變量是一樣的。
?int a=5;int *p=&a;//定義指針變量的同時初始化printf("%d\n",*p);?int b;int *p1=&b; //指針初始化的時候,不需要關注指向變量空間中是否有值printf("%d\n",*p1); 會出錯
指針變量的使用
使用
-
指針變量的賦值
?//方式1int a, *p;p=&a;// 先定義,后賦值??//方式2int a1,*p1,*q1=&al; //定義并初始化p1=ql;//其實就是變量賦值,指針變量q1的值賦值給p1,此時q1和p1同時指向a1
-
操作指針變量的值
?int a,*p,*q=&a;p=q;//將指針變量q的值賦值給指針變量p,此時p和q都指向了變量a?printf("%p",p);//訪問的是指針變量p的值(也就是變量a的地址)printf("%p",q);
-
操作指針變量指向的值
?int a=6;*q=&a,b=25;//一定要注意指針變量q的變量名就是q*q=10;//訪問q指向的變量a的空間,其實就是間接的給a賦值(a=10)printf("%d,%d\n",*q,a);//10 10?q=&b;//一個指針變量只能同一時刻指向一個變量,但是一個變量可以同時被多個指針變量指向?printf("%d,%d\n",*q,a);// 25 10
兩個指針運算符使用
-
&
取地址運算符。&a是變量a的地址。這個是變量指針 -
*
指針運算符、解引用符、間接訪問運算符,*p是指針變量p指向的對象值。這個是指針變量。
案例
案例1
-
需求:通過指針變量訪問整型變量
-
分析:
-
代碼:
?#include <stdio.h>?void main(){int a=3,b=4,*p1=&a,*p2=&b;printf("a=%d,b=%d\n",*p1,*p2);?}
案例2
-
需求:聲明a,b兩個變量,使用間接存取的方式實現數據交換。
-
分析:
-
代碼:
?#include <stdio.h>int main(){int a=3,b=4,*p_a=&a,*p_b=&b,*p_t;printf("交換前為%d,%d\n",*p_a,*P_b);//3,4//交換位置p_t=p_a;P_a=P_b;p_b=p_t;printf("交換后為%d,%d,%d,%d\n",*p_a,*P_b);}
總結:此時改變的只是指針的指向,原始變量a,b中數據并沒有發生改變
-
代碼:數據改變,不推薦
?#include <stdio.h>int main(){int a=3,b=4,*p_a=&a,*p_b=&b,temp;printf("交換前為%d,%d\n",*p_a,*P_b);//3,4//交換位置temp=*p_a;*P_a=*P_b;*p_b=temp;printf("交換后為%d,%d\n",*p_a,*P_b);}
總結:此時改變的只是指針的指向,原始變量a,b中數據發生改變
案例3
-
需求:輸入a、b兩個整數,按先大后小的順序輸出a和b
-
分析:
-
代碼:指向改變
?#include <stdio.h>int main(){int a=3,b=4,*p_a=&a,*p_b=&b,*p_t;if(a<b){p_t=p_a;P_a=P_b;p_b=p_t;}//交換位置printf("按從小到大的順序輸出a,b的值%d>%d\n",*p_a,*P_b);}
-
代碼:
?#include <stdio.h>int main(){int a=3,b=4,*p_a=&a,*p_b=&b,temp;printf("交換前為%d,%d\n",*p_a,*P_b);//3,4if(a<b){//交換位置temp=*p_a;*P_a=*P_b;*p_b=temp;}printf("按從小到大的順序輸出a,b的值%d>%d\n",*p_a,*P_b);}
指針變量做函數參數
指針變量做函數參數往往傳遞的是變量的地址(基地址),借助與指針變量間接訪問是可以修改實參變量數據的。
案例:
-
需求:有a,b兩個變量,要求交換后輸出(要求函數處理,用指針變量做函數的參數)
-
方式1:交換指向(指針指向發生改變,指針指向的對象的值不變)
?#include <stdio.h>//定義一個函數,實現兩個數的交換?void swap(int *p_a,int *p_b){int *p_t;//以下寫法只會改變指針指向 不會改變指向對象的值p_t=p_a;P_a=P_b;p_b=p_t;printf("%d,%d\n",*P_a,*p_b);}?int main(){int a=3,b=5,*p_a=&a,*p_b=&b,*p_t;swap(&a,&b);printf("a=%d,b=%d\n",a,b)return 0;}
-
方式2:交換數據(指針指向不改變,指針指向對象的值發生變化)
?#include <stdio.h>?void swap(int *p_a,int *p_b){int temp;//以下寫法只會改變指針指向 不會改變指向對象的值temp=*p_a;*P_a=*P_b;*p_b=temp;printf("%d,%d\n",*P_a,*p_b);}??int main(){int a=3,b=4,*p_a=&a,*p_b=&b,temp;swap(&a,&b);printf("a=%d,b=%d\n",a,b);}
指針變量指向數組元素
數組元素指針
-
數組的指針就是數組中的第一個元素的地址,也就是數組的首地址
-
數組元素的指針是指數組的首地址。因此,同樣可以用指針變量來指向數組或數組元素。
-
在C語言中,由于數組名代表數組的首地址,因此,數組名實際上也是指針,訪問數組名就是訪問數組首地址
?#include <stdio.h>//定義一個函數,實現兩個數的交換???int main(){int arr[]={11,12,13};int *p1=arr[0];int *p2=arr;printf("%p,%p,%p\n",p1,p2,arr);return 0;}
注意:雖然我們定義一個指針變量接收了數組地址,但不能理解為指針變量指向了數組。而應該理解為指向了數組的元素(默認為第1個元素)。
指針運算
指針運算:前提是指針變量必須要指向數組的某個元素。(指針運算只能發生在同一數組)
序號 指針運算 說明 1 自增:p++、++p、p=p+1、p+=1 指向下一個元素的地址 2 自減:p--、--p、p-=1 指向下一個元素的地址 3 加n個數:p+n(n*sizeof(type)) 后n個元素的(首)地址 4 減n個數:p-n 前n個元素的(首地址) 5 指針相減:p1-p2 p1,p2之間相差幾個元素 6 指針比較:p1<p2 前面的指針小于后面的指針 說明: ①如果指針變量p已指向數組中的一個元素,則p+1指向同一數組中的下一個元素,p-1指向同一數組中的上一個元素。即p+1或p-1也表示地址。但要注意的是,雖然指針變量p中存放的是地址,但p+1并不表示該地址加1,而表示在原地址的基礎上加了該數據類型所占的字節數d(d=sizeof(數據類型))。 ②如果p原來指向a[0],執行++p后p的值改變了,在p的原值基礎上加d,這樣p就指向數組的下一個元素a[1]。d是數組元素占的字節數。 ③如果p的初值為&a[0]則p+i和a+i就是數組元素a[i]的地址,或者說,它們指向a數組的第i個元素。 ④
*(p+i)
或(a+i)是p+i或a+i所指向的數組元素,即a[i]。 ⑤如果指針變量p1和p2都指向同一數組,如執行p2-p1,結果是兩個地址之差除以數組元素的長度d。?#include <stdio.h>?int main(){int arr[]={11,22,33,44,55};int *p1=arr+4;//55等價于arr[4]int *p2=arr+1;//22等價于arr[1]aiprintf("%ld\n",p1-p2);//3 ? ? 4*4-1*4=12 ? 12(相差的字節數)/4(每個元素的大小ai)=3(元素的大小)return 0;}
案例
案例1
-
需求:通過下標法和指針法遍歷數組
-
代碼:
?#include <stdio.h>?//下標法遍歷數組void arr1(int arr[],int len){//通過循環遍歷for(int i=0;i<len;i++){printf("%-3d",arr[i]);}printf("\n");}?void arr2(int arr[],int len){int *p=arr;register int i=0;for(;i<len;i++){printf("%-3d",*p;p++;}printf("\n");?}??void arr3(int arr[],int len){int *p=arr;register int i=0;for(;i<len;i++){printf("%-3d",*(arr+i));}printf("\n");}??int main(){int array[]={11,22,33,44,55};int len=sizeof(arr)/sizeof(arr[0]);arr1(array,len);arr2(array,len);arr3(array,len);return 0;}??
?#include <stdio.h>?int arr2() {int arr1[] = {11, 22, 33, 44, 55, 66, 77, 88};int *p = arr1;?// 1. 輸出數組首元素 11printf("%d\n", *p); ?// 2. 指針后移,輸出 22p++;printf("%d\n", *p); ?// 3. 先取值 22,指針再后移。此時 x=22,p 指向 33int x = *p++; printf("%d,%d\n", x, *p); // 輸出 22,33?// 4. 指針先前移到 44,取值 44。y=44,p 指向 44int y = *(++p); printf("%d,%d\n", y, *p); // 輸出 44,44?// 5. 對當前指向的元素(44)自增,變為 45(*p)++; printf("%d\n", *p); // 輸出 45?return 0;}?int main(int argc, char *argv[]) {arr2();return 0;}
*p++ 先解引用p,然后p這個指針自增
?int arr[]={11,22,33},*p=arr;int x=*p++; ?
(*p)++ 先解引用p,然后使用解引用出來的數據自增
?int arr[]={11,22,33};*p=arr;int x=(*p)++;
通過指針引用數組元素
引用一個數組元素,可以用:
①下標法:如:
arr[i]
形式②指針法:如:
*(arr+i)
或者*(p+i)
,其中arr是數組名,p是指向數組元素的指針變量,其初始值;p=arr;案例:
需求:有一個整型數組arr,有十個元素,輸出數組中的全部元素。
-
下標法:(通過改變下標輸出所有元素)
?#include <stdio.h>void main(){int arr[10];int i;// 給數組元素賦值for(i = 0; i < 10; i++)scanf("%d",&arr[i]);// 遍歷數組元素for(i = 0; i < 10; i++)printf("%-4d%",*(arr + i));printf("\n"); ? }
-
指針法(地址):(通過數組名計算出數組元素地址,找出數組元素)
?#include <stdio.h>void main(){int arr[10];int i;// 給數組元素賦值for(i = 0; i < 10; i++)scanf("%d",&arr[i]);// 遍歷數組元素for(i = 0; i < 10; i++)printf("%-4d%",arr[i]);printf("\n"); ? }
-
指針法(指針變量):用指針變量指向數組元素
?#include <stdio.h>void main(){int arr[10];int *p, i;// 給數組元素賦值for(i = 0; i < 10; i++)scanf("%d",&arr[i]);// 遍歷數組元素for(p = arr; p < (arr + 10); p++)printf("%-4d%",*p);printf("\n"); ? }
注意:數組一旦創建,就無法改變其值
以上三種寫法比較:
-
第種寫法和第②種寫法執行效率相同。系統是將arr[]轉換為*(arr+i)處理的,即先計算出地址,因此比較費時。
-
第③種方法比①②種方法快。用指針變量直接指向數組元素,不必每次都重新計算地址。(p++)能大大提高執行效率。
-
用第①種寫法比較直觀,而用地址法或者指針變量的方法難以很快判斷出當前處理的元素。
使用指針變量指向數組元素時(上面第③種寫法),注意以下三點:
①
*(p--)相當于arr[i--],先*p,在p--
;*(p++)相當于arr[i++],先*p,在p++
②
*(++p)相當于arr[++i],先++p,在*
;*(--p)相當于arr[--i],先--p,在*
③
*p++先*p,在p++
④
(*p)++先*p,在*p++
*p++ (p++) (*p)++
具體關系參照下面表格
數組名做函數參數
表現形式:
①形參和實參都是數組名
?void fun(int arr[],int len){......}void main(){int arr[]={11,22,33};fun(arr,sizeof(arr)/sizeof(arr[0]));}
②實參用數組名,形參用指針變量
?void fun(int *p,int len){......}void main(){int arr[]={11,22,33};fun(p,sizeof(arr)/sizeof(arr[0]));}
③實參和形參都用指針變量
?void fun(int *p,int len){......}void main(){int arr[]={11,22,33};int *p=arr;fun(p,sizeof(arr)/sizeof(arr[0]));}
④實參用指針,形參用數組名
?void fun(int arr[],int len){......}void main(){int arr[]={11,22,33};int *p=arr;fun(p,sizeof(arr)/sizeof(arr[0]));}
案例:
需求:將數組a中n個元素
分析:
代碼:
?#include <stdio.h>/*** 數組的反轉:數組實現*/ void inv(int arr[], int len){// 反轉思路:將第0個和len -1個進行對調,將第1個和len-2個進行對調...// 定義循環變量和臨時變量register int i = 0, temp;// 遍歷數組for(; i < len/2; i++){// 交換temp = arr[i];arr[i] = arr[len - 1 - i];arr[len - 1 - i] = temp;}}/*** 數組的反轉:指針實現*/ void inv2(int *p, int len){// 反轉思路:將第0個和len -1個進行對調,將第1個和len-2個進行對調...// 定義循環變量和臨時變量int *i = p, *j = p + len - 1, temp;// 遍歷數組for(; i < j;i++,j--){// 交換temp = *i;*i = *j;*j = temp;}}/*** 遍歷數組*/ void list(const int arr[],int len)for(int i = 0; i < len; i++) printf("%-3d",arr[i]);printf("\n");}int main(int argc,char *argv[]){int arr[] = {11,22,33,44,55,66};int len = sizeof(arr) / sizeof(arr[0]);list(arr,len);inv(arr,len);list(arr,len);inv2(arr,len);list(arr,len);return 0;}?
-