摘要:指針變量可以存放基本類型數據的地址,也可以存放數組函數以及其他指針變量的地址。數組名表示的是整個數組所占的字節數。在這里數組指針的類型是。這也是我們可以用數組指針來接收二維數組數組名的原因。
目錄
相信很多小伙伴學c語言的時候都是談‘針’色變呀,我當年學的時候也可謂之吃力,因此寫了這篇文章,希望幫助更多的在指針的剝削下瑟瑟發抖的同學們徹底攻克這一煩人的內容,讓指針的壓迫成為過去時,讓更多的同學看完這篇文章后可以對著指針大喊:我!站起來了!
指針(Pointer)就是內存的地址,C語言允許用一個變量來存放指針,這種變量稱為指針變量。指針變量可以存放基本類型數據的地址,也可以存放數組、函數以及其他指針變量的地址。
int a=10; //定義一個變量aint* pa=&a;//定義一個指針pa指向a*pa=9; //通過對指針的解引用修改a的值
其中修改a的值得過程與 a=9 等價。
注意兩個‘*’的意義不同,第一個‘*’是與int連用的,表示指針類型是(int*),第二個"*"是解引用操作符。
在32位機器中,每一個指針的大小都是四個字節,直接給指針一個統一的類型比如ptr*不香嗎?那為什么還要有像(int*),(char*)等指針類型的區分呢,下面介紹指針類型的意義。
int a = 0x11223344;
首先我們定義一個16進制的數字a。
我們先找到a的地址,然后給他賦值,在內存中觀察到的是這樣的。
此時a的地址時0x0095F8FC,可以看到已經將16進制數字0x11223344放入了a中。
下面我將用不同類型的指針分別修改a的值,我們可以看看內存中a的值得變化效果。?
?1.int* 定義的指針
int* pa=&a;*pa=10;
當執行完*pa=10之后,a中放的內容為
?可以發現a的四個字節都發生了改變,并且這四個字節組成的數字是10。
2.char*定義的指針
char* pa=&a;*pa=10;
?最后a的值是這樣的
我們會發現只改變了一個字節的內容,改動的那個字節被賦值成10,但a的值并不是10。
總結一下就是int*型指針在解引用修改指向值時,修改的是和整型一樣大小的4個字節的內容,而char*型指針在解引用修改指向值時修改的是和字符型一樣大小的1個字節的內容。
int a = 10;int* pa = &a;char* pb = &a;printf("%p/n", pa);printf("%p/n", pb);printf("%p/n", pa + 1);printf("%p/n", pb + 1);
?這個代碼的輸出結果是
我們發現int*型指針+1后的結果多了4個字節,但char*型指針+1后的結果只多了1個字節。
int *p;*p=10;//編譯器會報錯
int arr[10];int i;int*p=arr;//數組名表示數組首元素地址for(i=0;i<=10;i++){*p=i;p++}
?由于數組中只有10個元素,但是指針已經訪問到第十個元素的下一個元素了,但該元素沒在定義的內存中,最終p成為野指針。
int* test(){int a=10;return &a;}int main(){int*p=test();printf("%d",*p);return 0;}
這樣一段代碼就發生了指針指向空間的釋放,當test()函數開始執行的時候,內存為它開辟空間,當執行結束之后,這塊空間會還給內存,即p接收的是不屬于我們已開辟空間的地址。
但是打印*p得到的依然是10,這是由于雖然之前為a開辟的空間已經還給了內存,但是空間中的內容是不變的,p接收了這段不在內存空間的地址,*p的值是10。
int* test(){int a=10;return &a;}int main(){int*p=test();printf("hehe");printf("%d",*p);return 0;}
但是如果我們在打印*p之前隨意引用一個函數,比如printf(),那么*p的結果就不一定會是10,這是因為,每新調用一個函數,內存都要為它分配空間,由于test()的空間已經釋放,所以為printf()分配的空間就有可能會覆蓋到原來為test()分配的空間上,從而導致對原來存儲a的空間內容的修改,使*p的值不再是10。
這個在上面指針類型的意義中講過,這里不多贅述。
指針減指針有一個前提就是要在同一塊空間內進行,例如在同一個數組內
int arr[10]={1,2,3,4,5,6,7,8,9,0};printf("%d",&arr[9]-&arr[0]};//9
指針減指針的結果是這兩個指針之間間隔的元素個數。打印的結果是9。
指針比較大小即為地址比較大小。
int arr[10]={1,2,3,4,5,6,7,8,9,0};int* p;for(p=arr[10];p>&arr[0];){*--p=0;}//由于是后置--,并沒有對arr[10]進行操作,所以可以這樣使用
其中p>&arr[0]為對指針的關系運算。
數組:一塊連續的空間,存放的是相同類型的元素,數組的大小與元素類型和個數有關。
指針:是一個變量,存放地址,大小是4(32位機器)或者8個(64位機器)byte。
數組名大多數情況表示的就是首元素地址。
int arr[10]={0};printf("%p",arr);printf("%p",&arr[0]);
這兩段代碼打印的結果是相同的,說明數組名表示的是首元素地址。
但是有兩個特例
int arr[10]={0};int sz=sizeof(arr)/sizeof(arr[0]);//10
這段代碼打印的結果是10,當sizeof與數組名結合的時候數組名表示整個數組。sizeof(數組名)表示的是整個數組所占的字節數。
int arr[10]={0};printf("%p/n",arr);printf("%p/n",&arr);printf("%p/n",arr+1);printf("%p/n",&arr+1);
打印出來的結果是這樣的,打印出來的arr和&arr是一樣的,但是+1之后,arr加了4個字節,但是&arr加了40個字節,說明?&arr中的arr表示的是整個數組。
這里只是另一種操作數組的方式。
int arr[10]={0};int* p=arr;int i;int sz=sizeof(arr)/sizeof(arr[0]);for(i=0;i
?其中*(p+i)與arr[i]是等價的。
指針數組,顧名思義就是存放指針的數組,我們可以類比:
int arr[10];//整型數組,存放整型的數組char ch[5];//字符數組,存放字符的數組int* parr[10];//指針數組,存放指針的數組
即數組中存放的全都是指針。舉個例子:
int a=10;int b=90;int c=40;int* parr[3]={&a,&b,&c};
將a,b,c的地址存放在了指針數組中。
再來看一段代碼:
#includeint main(){ int arr1[] = { 1,2,3,4,5 }; int arr2[] = { 2,3,4,5,6 }; int arr3[] = { 3,4,5,6,7 }; int* arr4[] = {arr1,arr2,arr3}; int i,j; for (i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { printf("%d ", arr4[i][j]); } printf("/n"); } return 0;}
?我們可以看到這段代碼的運行結果。注意printf后面的部分用法,arr4[i][j]并不是二維數組,假設我們要得到arr1中1這個元素,首先通過arr4[0]找到arr1的首地址,然后加0得到1的地址,再對其解引用,這一過程可以寫成*(arr4[0]+0),類比一維數組中*(arr+i)與arr[i]等價,則*(arr4[0]+0)與arr4[0][0]等價。所以才有了arr[i][j]這一用法。
數組指針與指針數組不同,前者強調的是指針,后者強調的是數組。
首先我們還是先進行一下類比:
int* p;//整型指針,即指向整型的指針char* pc;//字符指針,即指向字符的指針int(*p)[10];//數組指針,指向數組的指針
注意數組指針與指針數組的區分
int* p[10]表示的是p[10]是一個存放指針的指針數組,其中p是數組名。
int(*p)[10]表示的是p是一個指向有十個元素的數組的指針,且這十個元素都是整型。
在這里數組指針的類型是int(*)[10]。
1.一維數組傳參
來看這樣一段代碼
#includevoid print1(int* p, int sz){ int i; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); }}void print2(int(*p)[10], int sz){ int i; for (i = 0; i < 10; i++) { printf("%d ", *(*p + i)); }}int main(){ int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; int sz = sizeof(arr) / sizeof(arr[0]); print1(arr, sz); printf("/n"); print2(&arr, sz); return 0;}
?這段代碼用兩種不同的傳參方式,打印出來相同的結果。
向print1向傳的參數是數組首元素的地址。
向print2這個函數傳的參數是整個數組的地址,因為&數組名表示的是真個數組的地址。
print1接收的是首元素地址,只需要一個整型指針就可以接收。
print2接收的是整個數組的地址,所以需要用指針數組來接收。
重點講一下print2函數是如何將數組的所有元素成功打印的:
?重點在于這個部分,首先我們知道p是一個指向整個數組的指針。
規定:對指向整個數組的指針進行解引用的結果是整個數組的首元素的地址。
則*p是數組arr的首元素的地址,結合之前講的用指針操作數組,可以知道*(*p+i)表示的是數組中的元素,其中*p可以改寫成*(p+0),所以p[0]表示的也是數組首元素的地址,整個表達式可以改寫成這樣:p[0][i],這與上面的表達式是等價的。
對于一位數組,其實并不建議這么傳參,原因是比較繞,容易混淆。
?2.二維數組傳參
來看這樣一段代碼
#includevoid print1(int arr[3][5], int r, int c){ int i, j; for (i = 0; i < r; i++) { for (j = 0; j < c; j++) { printf("%d ", arr[i][j]); } printf("/n"); }}void print2(int(*p)[5], int r, int c){ int i, j; for (i = 0; i < r; i++) { for (j = 0; j < c; j++) { printf("%d ", *(*(p + i) + j)); } printf("/n"); }}int main(){ int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 }; print1(arr, 3, 5); printf("/n/n"); print2(arr, 3, 5); return 0;}
?我們可以看到,接收參數的方式不同,打印的結果是相同的,其中print1是常規操作,我們重點說一下print2這個函數。
?首先我們要知道:二維數組的數組名表示的是第一行的地址。
這也是我們可以用數組指針來接收二維數組數組名的原因。
?
整個問題的關鍵所在還是在這段代碼上。
我們已經明確p表示的是第一行的地址,那么根據指針運算的部分我們可以知道(p+i)表示的是第i行的地址,對(p+i)解引用,找到的是第i行第一個元素的地址,對這個地址+j表示的是這一行第j個元素的地址,再解引用,表示的就是第i行第j列的元素內容。?
?由于*(p+i)等價于p[i]所以整個表達式等價于p[i][j]。打印的結果是一樣的。
int arr[5];//一個整型數組int *parr[10];//一個指針數組,數組的每個元素都是指針int(*parr2)[10];//一個數組指針,這是一個指向含有十個整型元素的數組的指針int (*parr3[10])[5];//parr3是一個指針數組,數組中存放的是指針 //parr3中每一個元素指向含有5個整型元素的數組
int arr[10]={0};test(arr);
現在定義一個數組,以數組名為參數,形參的幾種形式
void test(int arr[]);void test(int arr[10]);void test(int *arr);//數組名是首元素地址可以用指針接收
若定義的值一個指針數組:
int *arr[10]={0};test(arr);
形參的形式可以是:
void test(int *arr[10]);void test(int **arr);//由于數組名時首元素地址,首元素是一個指針,能夠接收指針地址的是二級指針
int arr[3][5]={0};test(arr);
形參的形式可以是:
void test(int arr[3][5]);void test(int arr[][5]);//注意列不可以省void test(int (*p)[5]);//二維數組數組名表示的是第一行的地址
我們知道指針存放的是某一個元素的地址,但是指針依然是一個變量,指針也有它自己的地址。
int a=10;int*p=&a;printf("%d",*p);//10int**pp=&p;//定義二級指針pp,并使pp指向p的地址printf("%d",**p);//相當于兩次解引用,結果依然是10
那你可能會問了二級指針的地址是不是用int***類型的變量存儲呢,答案是肯定的,理論上來說是可以無限套娃的,但是用多了之后你會發現代碼會變得冗余可讀性變差,所以不建議無限套娃。
char ch="w";char*p =&ch;
這種形式和整型指針一樣,指針指向一個字符的地址。
char*pc="hello world";//hello world是一個常量字符串,存放在內存的常量區,不允許改printf("%c",*p);//打印出來的是第一個字符hprintf("%s",p);//打印出來的是整個字符串hello world
當指針指向字符串時,對它解引用得到的是字符串的第一個字符,當打印整個字符串時,將%c改成%s,不需要再解引用,這是由printf這個函數本身性質所決定的。
根據這一特征我們可以通過指針數組打印多組字符串。
int i;char* arr[3]={"abcd","efgh","ijkl"};for(i=0;i<3;i++){printf("%s",arr[i]);};
值得注意的是,此時的hello world是一個常量字符串,是無法通過*p修改的,如果加上*p="w"的話,編譯器會報錯。
所謂函數指針,就是可以指向函數的指針,函數和各類型變量一樣,也有自己的地址。
#includeint add(int a, int b);int add(int a, int b){ return a + b;}int main(){ printf("%p/n", add); printf("%p", &add);}
這段代碼打印的結果是
我們可以看到,無論是否在函數add前加上&,打印出來的結果是一樣的,這說明add和&add都代表著函數的地址。
既然函數有地址,那么一定有相應類型的指針變量去接收它的地址。并有相應的解引用操作。我們用函數操作和函數指針操作進行一下對比。
#includeint add(int a, int b);int add(int a, int b){ return a + b;}int main(){ int (*p)(int, int) = add; int ret = add(2, 3); printf("%d/n", ret); ret = (*p)(2, 3); printf("%d", ret); return 0;}
這一部分為函數指針的定義部分,前面講過,add可以表示函數的地址。這里的函數指針名為p,p前面的*代表它是一個指針,后面的(int,int)表示這個指針所指向函數的參數為兩個int型的參數,最前面的int表示的是這個指針所指函數的返回值是int型。該函數指針的類型為int(*)(int,int)。
?我們可以看到,這段代碼打印的結果依然是5,這里的p是一個指向函數add的指針,*對p解引用得到函數add,再向add傳參2和3,從而得到add的返回值5。
規定:在解引用函數指針時,*可以填多個,也可以不填。也就是說ret=(*p)(2,3)可以改寫成p(2,3),也可以改寫成(**p)(2,3)。
(*(void(*)())0)();
我們來分析一下這段代碼,分析這種復雜的代碼時一定要先找到每個括號的位置。
我們將它一層一層展開,首先是這一部分(void(*)())0,前面括號里面的void(*)()是一個沒有返回值,沒有參數的函數指針類型,后面的0是一個整型,在函數指針類型上加括號表示的是,將0這個整型強行轉換成函數指針類型。即0變成了一個函數指針。
現在的代碼被簡化成了(*0)(),其中0是函數指針類型,對其解引用找到了這個函數,由于0這個函數指針的類型是void(*)(),所以對0這個函數指針解引用時要傳遞的類型就是(),即原式是對0這個指針變量解引用的結果。
?void(*signal(int,void(*)(int)))(int);
再來看這樣一段代碼,signal(int,void(*)(int))這一段先不看先用a來代替,剩余的就是void(*a)(int)這樣一段內容,很明顯這是一個函數指針的定義,即a就是一個函數指針,即signal(int,void(*)(int))是一個函數指針,即signal是一個函數,它的返回值是一個函數指針,它的參數類型是整型int和函數指針類型void(*)(int)。
這樣一段代碼看起來很復雜,可以用typedef來簡化一下,我們可以先這樣定義:
typedef void(*p)(int) ,注意這不是定義了一個函數指針,而是將函數指針類型定義成了p,即p代表void(*p)(int)這一類型,這樣的話原代碼就可以改寫成p signal(int,p),比原來看起來會簡便多了。
函數指針可以簡化很多操作,尤其是對那種需要調用多個相似函數的程序有奇效。原理就是可以將每一個相同類型的函數放在函數指針數組中。
我們拿實現計算器作一個對比
首先是沒用到函數指針的計算器:
#includeint add(int a, int b){ return a + b;}int sub(int a, int b){ return a - b;}int mul(int a, int b){ return a * b;}int div(int a, int b){ return a / b;}void menu(){ printf("**1.add***/n"); printf("**2.sub***/n"); printf("**3.mul***/n"); printf("**4.piv***/n"); printf("**0.exit**/n");}int main(){ int input=0; menu(); do { printf("please input your choice!/n"); scanf("%d", &input); int a, b,ret; switch (input) { case 1:scanf("%d %d", &a, &b); ret=add(a, b); printf("%d/n", ret); break; case 2:scanf("%d %d", &a, &b); ret=sub(a, b); printf("%d/n", ret); break; case 3:scanf("%d %d", &a, &b); ret = mul(a, b); printf("%d/n", ret); break; case 4:scanf("%d %d", &a, &b); ret = div(a, b); printf("%d/n", ret); break; case 0:printf("exit!"); break; default:printf("error!/n"); break; } } while (input); return 0;}
這個程序很容易理解,用switch語句選擇不同的計算操作就能實現計算功能。然而這段代碼還是國語冗余,我們可以思考一下函數指針的方法。
#define _CRT_SECURE_NO_WARNINGS#includeint add(int a, int b){ return a + b;}int sub(int a, int b){ return a - b;}int mul(int a, int b){ return a * b;}int div(int a, int b){ return a / b;}void menu(){ printf("**1.add***/n"); printf("**2.sub***/n"); printf("**3.mul***/n"); printf("**4.piv***/n"); printf("**0.exit**/n");}int main(){ int input = 0; menu(); int (*p[5])(int, int) = { 0,add,sub,mul,div };//定義一個函數指針數組用來存儲四個函數的地址,0起到補位作用。 do { scanf("%d", &input); if (input == 0) { printf("exit/n"); break; } else if (input >= 1 && input <= 4) { int a, b, ret; scanf("%d %d", &a, &b); ret = (*p[input])(a, b);//對函數指針進行解引用找到對應的函數。 printf("%d/n", ret); } else { printf("error!/n"); } } while (input); return 0;}
?這樣一來的話判斷語句更簡便了,所需要的代碼量也變少了。得到的結果是相同的。
定義:回調函數就是一個通過函數指針調用的函數,如果你把函數的地址作為參數傳給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。
?仍然拿計算器這個程序舉例:
#includeint add(int a, int b){ return a + b;}int sub(int a, int b){ return a - b;}int mul(int a, int b){ return a * b;}int div(int a, int b){ return a / b;}void calc(int (*p)(int, int))//定義一個函數接收各函數地址{ int a, b, ret; scanf("%d %d", &a, &b); ret = (*p)(a, b); printf("%d/n", ret);}void menu(){ printf("**1.add***/n"); printf("**2.sub***/n"); printf("**3.mul***/n"); printf("**4.piv***/n"); printf("**0.exit**/n");}int main(){ int input = 0; menu(); do { printf("please input your choice!/n"); scanf("%d", &input); int a, b, ret; switch (input) { case 1:calc(add); break; case 2:calc(sub); break; case 3:calc(mul); break; case 4:calc(div); break; case 0:printf("exit!"); break; default:printf("error!/n"); break; } } while (input); return 0;}
這個方式也比第一種方式簡單的多,新定義了一個函數接收各個函數的地址,在每個case中只需要引用一個calc函數就可以了,這種方式利用函數指針解決了每個case中相同代碼較多的問題。
回調函數還是很重要的,剛剛看了幾個指針的面試題,都有涉及到回調函數,所以再給大家舉一個例子。
我們知道c語言中有許多我們還很少用到的庫函數,比如qsort函數,這是一個快速排序函數,和回調函數就有很大的聯系。這個函數的功能很強大,不僅能排序整數,甚至結構體便量也可以排序。這里只排整型變量,對排結構體感興趣的小伙伴可以研究一下呀。
首先我們先看一下這個函數的參數:
?
?這是msdn中給到的函數定義,我們逐個來介紹
1.void*base:定義了一個空類型的指針,空類型的指針可以指向所有類型的地址,但是不能解引用和進行指針加減運算。這里的base表示的是指向首元素的指針。
2.size_t num和size_t width:前者代表的是元素的個數,后者代表每個元素所占字節數。
3.int (__cdecl*compare)(const void*elem1,const void*elem2):這里定義了一個函數指針,所指向的是判斷相鄰兩個數據的大小的一個函數即傳了一個函數名,也是函數的地址。對這個函數也有一定的要求。這里的兩個數據也是先用的void*來接收的地址。
下面用一段代碼來展示一下這個函數怎么用:
#include#includeint compare(const void* e1, const void* e2){ return *(int*)e1 - *(int*)e2;}int main(){ int arr[] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), compare); int i = 0; for (i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0;}
?注意我們向qsort函數傳的參數
?compare函數的參數是兩個空類型的指針,傳入的是arr數組兩個相鄰數的地址,前面講過,void*類型是不能解引用的,所以我們要先將其強制轉換成int*類型,然后再解引用。
?英語差的我還特意百度了一下這段話的意思,就是說當e1>e2時,如果我定義的函數返回值大于0的話,那么e1就會排在e2的后面,反過來如果我定義的函數返回值小于0,那么e1就會排在e2的前面。
我們還是逐層取理解
int (*p)(int ,int)=add;//p是一個函數指針int (*p[4])(int,int)={add,sub,mul,div};//p是一個函數指針數組,每個元素存放一個函數地址int (*(*p)[4])(int, int);//p是一個指向函數指針數組的指針
這段代碼如何理解呢?首先看(*p)[4]這一部分,*與p結合表示p是一個指針,再與[10]結合,表示這個指針指向的是一個數組,該數組里有4個元素,在將(*p)[4]排除,剩余部分為int (*)(int,int),這一部分代表數組中每一個元素的類型,類型是函數指針類型,所以p是一個指向函數指針數組的指針。
感謝大家可以看到這里,嘔心瀝血了接近5天,總算是嘔完了,喜歡的別忘收藏一下,關注一下我啥的,不知道小伙伴們站起來了嗎,全文12000字,又破記錄了,以后還想寫一個指針的面試題的總結,加深一下這一塊內容的理解,學習指針調試的方面也很重要但這里沒多贅述,最后祝大家學業有成吧(比心)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/118804.html
摘要:萬字詳解與的用法數組名的意義一維數組用法字符數組用法的用法字符串數組用法的用法指針與字符串用法用法二維數組數組名的意義在講所有東西之前,需要先明確一個關鍵問題數組名,這里的數組名表示整個數組,計算的是整個數組的大小,單 ...
摘要:在位機器上,指針變量的大小為個字節。指針類型的強制類型轉換對指針變量進行強制類型轉換的一般形式將保存的類型指針強制轉換為類型指針后賦值給,其中還是為,沒有改變。 前言 大家好,我是努力學習的少年,今天這篇文章是專門寫關于指針的知識點,因為指針內容比較多,所以我將指針的這篇文章我將它分為...
目錄 ??? 一,寫在前面 二,為什么使用文件 1,原因 2,數據流 3,緩沖區(Buffer) 4,C語言中帶緩沖區的文件處理 5,文件類型 6,文件存取方式 三,什么是文件 1,程序文件 ?2,數據文件 3,文件名 四,文件的打開和關閉? 1,文件指針 ?2,文件的打開和關閉 五,文件的順序讀寫 1,功能 2,代碼實現 六,文件的隨機讀寫 1,fseek 2,ftell 3,rewind 七,...
摘要:需要注意的是用矩陣形式如行列表示二維數組,是邏輯上的概念,能形象地表示出行列關系。再次強調二維數組名如是指向行的。一維數組名如是指向列元素的。 哈嘍!這里是一只派大鑫,不是派大星。本著基礎不牢,地動山搖的學習態度,從基礎的C語言語法講到算法再到更高級的語法及框架的學習。更好地讓同樣熱愛編...
閱讀 853·2021-11-19 11:29
閱讀 3348·2021-09-26 10:15
閱讀 2854·2021-09-22 10:02
閱讀 2433·2021-09-02 15:15
閱讀 1970·2019-08-30 15:56
閱讀 2407·2019-08-30 15:54
閱讀 2903·2019-08-29 16:59
閱讀 635·2019-08-29 16:20