C語(yǔ)言指針,這可能是史上最干最全的講解啦(附代碼)
為什么需要指針?
因?yàn)橹T如結(jié)構(gòu)體等大型數(shù)據(jù),占用的字節(jié)數(shù)多,復(fù)制很消耗性能。
但使用指針就可以很好的避免這個(gè)問(wèn)題,因?yàn)槿魏晤愋偷闹羔樥加玫淖止?jié)數(shù)都是一樣的(根據(jù)平臺(tái)不同,有4字節(jié)或者8字節(jié)或者其他可能)。
還有:C語(yǔ)言中的一切函數(shù)調(diào)用中,值傳遞都是“按值傳遞”的。
如果我們要在函數(shù)中修改被傳遞過(guò)來(lái)的對(duì)象,就必須通過(guò)這個(gè)對(duì)象的指針來(lái)完成。
地址總線專門(mén)用于尋址,CPU通過(guò)該地址進(jìn)行數(shù)據(jù)的訪問(wèn),然后把處于該地址處的數(shù)據(jù)通過(guò)數(shù)據(jù)總線進(jìn)行傳送,傳送的長(zhǎng)度就是數(shù)據(jù)總線的位數(shù)。地址總線的位數(shù)決定了CPU可直接尋址的內(nèi)存空間大小,比如CPU總線長(zhǎng)32位,其最大的直接尋址空間長(zhǎng)232KB,也就是4G。
sizeof(char)=1;
sizeof(int)=4;
同樣指針這個(gè)概念也泛指一類數(shù)據(jù)類型,int指針類型,double指針類型,char指針類型等等。
而為了保存一個(gè)數(shù)據(jù)在內(nèi)存中的地址,我們就需要指針變量。
Type *p;
我們說(shuō)p是指向type類型的指針,type可以是任意類型,除了可以是char,short, int, long等基本類型外,還可以是指針類型,例如int *, int **, 或者更多級(jí)的指針,也可是是結(jié)構(gòu)體,類或者函數(shù)等。于是,我們說(shuō):
int * 是指向int類型的指針;
struct xxx *,是指向struct xxx類型的指針;
其實(shí),說(shuō)這么多,只是希望大家在看到指針的時(shí)候,不要被int ***這樣的東西嚇到,就像前面說(shuō)的,指針就是指向某種類型的指針,我們只看最后一個(gè)*號(hào),前面的只不過(guò)是type類型罷了。
細(xì)心一點(diǎn)的人應(yīng)該發(fā)現(xiàn)了,在“什么是指針”這一小節(jié)當(dāng)中,已經(jīng)表明了:指針的長(zhǎng)度跟CPU的位數(shù)相等,大部分的CPU是32位的,因此我們說(shuō),指針的長(zhǎng)度是32bit,也就是4個(gè)字節(jié)!注意:任意指針的長(zhǎng)度都是4個(gè)字節(jié),不管是什么指針?。ó?dāng)然64位機(jī)自己去測(cè)一下,應(yīng)該是8個(gè)字節(jié)吧。。。)
?于是:
Type *p;
izeof(p)的值是4,Type可以是任意類型,char,int, long, struct, class, int **…
以后大家看到什么sizeof(char*), sizeof(int *),sizeof(xxx *),不要理會(huì),統(tǒng)統(tǒng)寫(xiě)4,只要是指針,長(zhǎng)度就是4個(gè)字節(jié),絕對(duì)不要被type類型迷惑!
這種抽象機(jī)制使得程序使用的是虛擬存儲(chǔ)器,而不是直接操作和使用真實(shí)存在的物理存儲(chǔ)器。
所有的虛擬地址形成的集合就是虛擬地址空間。
最關(guān)鍵的是,每一個(gè)字節(jié)都有一個(gè)唯一的編號(hào),編號(hào)從0開(kāi)始,一直到最后一個(gè)字節(jié)。
如上圖中,這是一個(gè)256M的內(nèi)存,他一共有256x1024x1024 ?= 268435456個(gè)字節(jié),那么它的地址范圍就是 0 ~268435455 ?。
因此,在程序中使用的變量,常量,甚至數(shù)函數(shù)等數(shù)據(jù),當(dāng)他們被載入到內(nèi)存中后,都有自己唯一的一個(gè)編號(hào),這個(gè)編號(hào)就是這個(gè)數(shù)據(jù)的地址。
指針就是這樣形成的。
#include
int main(void)
{
? ?char ch = 'a';
? ?int ?num = 97;
? ?printf("ch 的地址:%p
",&ch); ? //ch 的地址:0028FF47
? ?printf("num的地址:%p
",&num); ?//num的地址:0028FF40
? ?return 0;
}
指針的值(虛擬地址值)使用一個(gè)機(jī)器字的大小來(lái)存儲(chǔ)。
也就是說(shuō),對(duì)于一個(gè)機(jī)器字為w位的電腦而言,它的虛擬地址空間是0~2w - 1 ,程序最多能訪問(wèn)2w個(gè)字節(jié)。
這就是為什么xp這種32位系統(tǒng)最大支持4GB內(nèi)存的原因了。
97的二進(jìn)制是 : 00000000 00000000 00000000 0110000 , 但使用的小端模式存儲(chǔ)時(shí),低位數(shù)據(jù)存放在低地址,所以圖中畫(huà)的時(shí)候是倒過(guò)來(lái)的。
num的類型是int,因此將被解釋為 一個(gè)整數(shù)。
而且在C語(yǔ)言中,并不是所有的內(nèi)存數(shù)據(jù)都有名稱,例如使用malloc申請(qǐng)的堆內(nèi)存就沒(méi)有。
因此num的地址是 0028FF40。內(nèi)存的地址用于標(biāo)識(shí)這個(gè)內(nèi)存塊。
C語(yǔ)言中的程序數(shù)據(jù)會(huì)按照他們定義的位置,數(shù)據(jù)的種類,修飾的關(guān)鍵字等因素,決定他們的生命周期特性。
實(shí)質(zhì)上我們程序使用的內(nèi)存會(huì)被邏輯上劃分為:棧區(qū),堆區(qū),靜態(tài)數(shù)據(jù)區(qū),方法區(qū)。
不同的區(qū)域的數(shù)據(jù)有不同的生命周期。
Type *p;
p++;
然后問(wèn)你p的值變化了多少。
其實(shí),也可以認(rèn)為這是在考編譯器的基本知識(shí)。因此p的值并不像表面看到的+1那么簡(jiǎn)單,編譯器實(shí)際上對(duì)p進(jìn)行的是加sizeof(Type)的操作。
這里注釋掉char一行的原因是因?yàn)閏out<<(char*)會(huì)被當(dāng)成字符串輸出,而不是char的地址)
2(sizeof(short))
4(sizeof(int))
4(sizeof(long))?????????
8(sizeof(long?long))?????????
4(sizeof(float))?????????
8(sizeof(double))?????????
12(sizeof(long?double))
喏,增加的值是不是sizeof(Type)呢?別的什么struct,class之類的,就不驗(yàn)證你,有興趣的自己去驗(yàn)證。
如果指針變量p1保存了變量 num的地址,則就說(shuō):p1指向了變量num,也可以說(shuō)p1指向了num所在的內(nèi)存塊 ,這種指向關(guān)系,在圖中一般用 箭頭表示。
int a ; //int類型變量 a
int *a ; //int* 變量a
int arr[3]; //arr是包含3個(gè)int元素的數(shù)組
int (* arr )[3]; //arr是一個(gè)指向包含3個(gè)int元素的數(shù)組的指針變量
//-----------------各種類型的指針------------------------------
int* p_int; //指向int類型變量的指針
double* p_double; //指向idouble類型變量的指針
struct Student *p_struct; //結(jié)構(gòu)體類型的指針
int(*p_func)(int,int); //指向返回類型為int,有2個(gè)int形參的函數(shù)的指針
int(*p_arr)[3]; //指向含有3個(gè)int元素的數(shù)組的指針
int** p_pointer; //指向 一個(gè)整形變量指針的指針
這里要強(qiáng)調(diào)2個(gè)屬性:指針的類型,指針的值。
int main(void)
{
? ?int num = 97;
? ?int *p1 ?= #
? ?char* p2 = (char*)(&num);
? ?printf("%d
",*p1); ? ?//輸出 ?97
? ?putchar(*p2); ? ? ? ? ?//輸出 ?a
? ?return 0;
}
數(shù)據(jù)的地址用于在內(nèi)存中定位和標(biāo)識(shí)這個(gè)數(shù)據(jù),因?yàn)槿魏?個(gè)內(nèi)存不重疊的不同數(shù)據(jù)的地址都是不同的。
一般指針變量的類型要和它指向的數(shù)據(jù)的類型匹配。
int add(int a , int b)
{
? ?return a + b;
}
int main(void)
{
? ?int num = 97;
? ?float score = 10.00F;
? ?int arr[3] = {1,2,3};
? ?//-----------------------
? ?int* p_num = #
? ?float* p_score = &score;
? ?int (*p_arr)[3] = &arr; ? ? ? ? ?
? ?int (*fp_add)(int ,int ) ?= add; ?//p_add是指向函數(shù)add的函數(shù)指針
? ?return 0;
}
-
數(shù)組名的值就是這個(gè)數(shù)組的第一個(gè)元素的地址。 -
函數(shù)名的值就是這個(gè)函數(shù)的地址。 -
字符串字面值常量作為右值時(shí),就是這個(gè)字符串對(duì)應(yīng)的字符數(shù)組的名稱,也就是這個(gè)字符串在內(nèi)存中的地址。
int add(int a , int b){
? ?return a + b;
}
int main(void)
{
? ?int arr[3] = {1,2,3};
? ?//-----------------------
? ?int* p_first = arr;
? ?int (*fp_add)(int ,int ) ?= ?add;
? ?const char* msg = "Hello world";
? ?return 0;
}
當(dāng)然使用通過(guò)它來(lái)操作(讀/寫(xiě))它指向的數(shù)據(jù)啦。
對(duì)一個(gè)指針解地址,就可以取到這個(gè)內(nèi)存數(shù)據(jù),解地址的寫(xiě)法,就是在指針的前面加一個(gè)*號(hào)。
int main(void)
{
? ?int age = 19;
? ?int*p_age = &age;
? ?*p_age ?= 20; ?//通過(guò)指針修改指向的內(nèi)存數(shù)據(jù)
? ?printf("age = %d
",*p_age); ? //通過(guò)指針讀取指向的內(nèi)存數(shù)據(jù)
? ?printf("age = %d
",age);
? ?return 0;
}
指針之間的賦值是一種淺拷貝,是在多個(gè)編程單元之間共享內(nèi)存數(shù)據(jù)的高效的方法。
int* p1 ?= & num;
int* p3 = p1;
//通過(guò)指針 p1 、 p3 都可以對(duì)內(nèi)存數(shù)據(jù) num 進(jìn)行讀寫(xiě),如果2個(gè)函數(shù)分別使用了p1 和p3,那么這2個(gè)函數(shù)就共享了數(shù)據(jù)num。
#ifdef __cplusplus
? ? #define NULL ? ?0
#else ? ?
? ? #define NULL ? ?((void *)0)
#endif
my_free調(diào)用了之后,p的值就變成了0(NULL),調(diào)用多少次free都不會(huì)報(bào)錯(cuò)了!
執(zhí)行結(jié)果同上面一樣,不會(huì)報(bào)段錯(cuò)誤:
不能對(duì)他們做解指針操作,否則程序會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤,導(dǎo)致程序意外終止。
壞指針是造成C語(yǔ)言Bug的最頻繁的原因之一。
下面的代碼就是錯(cuò)誤的示例。
void opp()
{
? ? int*p = NULL;
? ? *p = 10; ? ? ?//Oops! 不能對(duì)NULL解地址
}
void foo()
{
? ? int*p;
? ? *p = 10; ? ? ?//Oops! 不能對(duì)一個(gè)未知的地址解地址
}
void bar()
{
? ? int*p = (int*)1000;
? ? *p =10; ? ? ?//Oops! ? 不能對(duì)一個(gè)可能不屬于本程序的內(nèi)存的地址的指針解地址
}
如果想要完整的提取指向的數(shù)據(jù),程序員就必須對(duì)這個(gè)指針做出正確的類型轉(zhuǎn)換,然后再解指針。因?yàn)椋幾g器不允許直接對(duì)void*類型的指針做解指針操作。
雖然從字面上看,void的意思是空,但是void指針的意思,可不是空指針的意思,空指針指的是上面所說(shuō)的NULL指針。
void指針實(shí)際上的意思是指向任意類型的指針。任意類型的指針都可以直接賦給void指針,而不需要進(jìn)行強(qiáng)制轉(zhuǎn)換。
例如:
Type a, *p=&a;
(Type等于char, int, struct, int *…)
void *pv;
pv=p;
void free(void*ptr);
char*malloc(size_t sz);
實(shí)際上設(shè)置成
Type*malloc(size_t sz);
也是完全正確的,使用void指針的原因,實(shí)際上就像前面說(shuō)的,void指針意思是任意指針,這樣設(shè)計(jì)更加嚴(yán)謹(jǐn)一些,也更符合我們的直觀理解。如果對(duì)前面我說(shuō)的指針概念理解的童鞋,肯定明白這一點(diǎn)。
typedef struct
{
? ?char name[31];
? ?int age;
? ?float score;
}Student;
int main(void)
{
? ?Student stu = {"Bob" , 19, 98.0};
? ?Student*ps = &stu;
? ?ps->age = 20;
? ?ps->score = 99.0;
? ?printf("name:%s age:%d
",ps->name,ps->age);
? ?return 0;
}
int main(void)
{
? ?int arr[3] = {1,2,3};
? ?int*p_first = arr;
? ?printf("%d
",*p_first); ?//1
? ?return 0;
}
(實(shí)質(zhì)上所有指針都支持遞增遞減 運(yùn)算 ,但只有在數(shù)組中使用才是有意義的)
int main(void)
{
? ?int arr[3] = {1,2,3};
? ?int*p = arr;
? ?for(;p!=arr+3;p++){
? ? ? ?printf("%d
",*p);
? ?}
? ?return 0;
}
int main(void)
{
? ?int arr[3] = {1,2,3};
? ?int*p = arr;
? ?printf("sizeof(arr)=%d
",sizeof(arr)); ?//sizeof(arr)=12
? ?printf("sizeof(p)=%d
",sizeof(p)); ? //sizeof(p)=4
? ?return 0;
}
這就意味著:這種數(shù)據(jù)傳遞是單向的,即從調(diào)用者傳遞給被調(diào)函數(shù),而被調(diào)函數(shù)無(wú)法修改傳遞的參數(shù)達(dá)到回傳的效果。
void change(int a)
{
? a++; ? ? ?//在函數(shù)中改變的只是這個(gè)函數(shù)的局部變量a,而隨著函數(shù)執(zhí)行結(jié)束,a被銷(xiāo)毀。age還是原來(lái)的age,紋絲不動(dòng)。
}
int main(void)
{
? ?int age = 19;
? ?change(age);
? ?printf("age = %d
",age); ? // age = 19
? ?return 0;
}
但是如果返回值有其它用途(例如返回函數(shù)的執(zhí)行狀態(tài)量),或者要回傳的數(shù)據(jù)不止一個(gè),返回值就解決不了了。
void change(int* pa)
{
? ?(*pa)++; ? //因?yàn)閭鬟f的是age的地址,因此pa指向內(nèi)存數(shù)據(jù)age。當(dāng)在函數(shù)中對(duì)指針pa解地址時(shí),
? ? ? ? ? ? ? //會(huì)直接去內(nèi)存中找到age這個(gè)數(shù)據(jù),然后把它增1。
}
int main(void)
{
? ?int age = 19;
? ?change(&age);
? ?printf("age = %d
",age); ? // age = 20
? ?return 0;
}
#include
void swap_bad(int a,int b);
void swap_ok(int*pa,int*pb);
int main()
{
? ?int a = 5;
? ?int b = 3;
? ?swap_bad(a,b); ? ? ? //Can`t swap;
? ?swap_ok(&a,&b); ? ? ?//OK
? ?return 0;
}
//錯(cuò)誤的寫(xiě)法
void swap_bad(int a,int b)
{
? ?int t;
? ?t=a;
? ?a=b;
? ?b=t;
}
//正確的寫(xiě)法:通過(guò)指針
void swap_ok(int*pa,int*pb)
{
? ?int t;
? ?t=*pa;
? ?*pa=*pb;
? ?*pb=t;
}
相反,我們防止這個(gè)目標(biāo)數(shù)據(jù)被改變。傳遞指針只是為了避免拷貝大型數(shù)據(jù)。
typedef struct
{
? ?char name[31];
? ?int age;
? ?float score;
}Student;
//打印Student變量信息
void show(const Student * ps)
{
? ?printf("name:%s , age:%d , score:%.2f
",ps->name,ps->age,ps->score); ?
}
另外我們?yōu)槭裁匆褂弥羔樁皇侵苯觽鬟fStudent變量呢?
而傳遞變量的指針卻快很多,因?yàn)樵谕粋€(gè)平臺(tái)下,無(wú)論什么類型的指針大小都是固定的:X86指針4字節(jié),X64指針8字節(jié),遠(yuǎn)遠(yuǎn)比一個(gè)Student結(jié)構(gòu)體變量小。
typedef int(*compare)(const void *x, const void *y);
這個(gè)時(shí)候,compare就是參數(shù)為const void *, const void *類型,返回值是int類型的函數(shù)。例如:
用typedef來(lái)定義的好處,就是可以使用一個(gè)簡(jiǎn)短的名稱來(lái)表示一種類型,而不需要總是使用很長(zhǎng)的代碼來(lái),這樣不僅使得代碼更加簡(jiǎn)潔易讀,更是避免了代碼敲寫(xiě)容易出錯(cuò)的問(wèn)題。 強(qiáng)烈推薦各位在定義結(jié)構(gòu)體,指針(尤其是函數(shù)指針)等比較復(fù)雜的結(jié)構(gòu)時(shí),使用typedef來(lái)定義。
在程序載入到內(nèi)存后,函數(shù)的機(jī)器指令存放在一個(gè)特定的邏輯區(qū)域:代碼區(qū)。
既然是存放在內(nèi)存中,那么函數(shù)也是有自己的指針的。
void echo(const char *msg){ ? ?printf("%s",msg);}int main(void){ ? ?void(*p)(const char*) = echo; ? //函數(shù)指針變量指向echo這個(gè)函數(shù) ? ?p("Hello "); ? ? ?//通過(guò)函數(shù)的指針p調(diào)用函數(shù),等價(jià)于echo("Hello ") ? ?echo("World"); ? ?return 0;}
(原子類型是不可再分割的類型,如int, short , char,以及typedef包裝后的類型)
int main(){ ? ?int a = 1; ? ?int const *p1 = &a; ? ? ? ?//const后面是*p1,實(shí)質(zhì)是數(shù)據(jù)a,則修飾*p1,通過(guò)p1不能修改a的值 ? const int*p2 = ?&a; ? ? ? ?//const后面是int類型,則跳過(guò)int ,修飾*p2, 效果同上 ? int* const p3 = NULL; ? ? ?//const后面是數(shù)據(jù)p3。也就是指針p3本身是const . ? const int* const p4 = &a; ?// 通過(guò)p4不能改變a 的值,同時(shí)p4本身也是 const ? int const* const p5 = &a; ?//效果同上 ? return 0;}typedef int* pint_t; ?//將 int* 類型 包裝為 pint_t,則pint_t 現(xiàn)在是一個(gè)完整的原子類型int main(){ ? int a ?= 1; ? const pint_t p1 = &a; ?//同樣,const跳過(guò)類型pint_t,修飾p1,指針p1本身是const ? pint_t const p2 = &a; ?//const 直接修飾p,同上 ? return 0;}
如果被訪問(wèn)的數(shù)據(jù)被拷貝了,在每個(gè)單元中都有自己的一份,對(duì)目標(biāo)數(shù)據(jù)的操作相互不受影響,則叫做深拷貝。
指針常用在C語(yǔ)言中,而引用,則用于諸如Java,C#等 在語(yǔ)言層面封裝了對(duì)指針的直接操作的編程語(yǔ)言中。
有些機(jī)器同時(shí)支持大端和小端模式,通過(guò)配置來(lái)設(shè)定實(shí)際的端模式。
//測(cè)試機(jī)器使用的是否為小端模式。是,則返回true,否則返回false
//這個(gè)方法判別的依據(jù)就是:C語(yǔ)言中一個(gè)對(duì)象的地址就是這個(gè)對(duì)象占用的字節(jié)中,地址值最小的那個(gè)字節(jié)的地址。
bool isSmallIndain()
{
? ? ?unsigned int val = 'A';
? ? unsigned char* p = (unsigned char*)&val; ?//C/C++:對(duì)于多字節(jié)數(shù)據(jù),取地址是取的數(shù)據(jù)對(duì)象的第一個(gè)字節(jié)的地址,也就是數(shù)據(jù)的低地址
? ? ?return *p == 'A';
}
往期好文合集
??最 后
??
?
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!