C语言之指针总结(1)
目錄
前言
一、指針簡介
1.什么是指針
2.指針變量的定義
3.指針變量的初始化
4.指針類型的意義
5.指針的大小
6.指針的運算
7.野指針
8.二級指針
9.指針表示法和數組表示法
二、指針數組和數組指針
1.指針數組
?2.數組指針
三、函數指針
四、函數指針數組
五、指向函數指針數組的指針
六、回調函數
前言
指針是C語言的靈魂,本文是對指針的一個大概總結。
一、指針簡介
1.什么是指針
一般來說,指針被認為是一個概念,是計算機內存地址的代名詞之一,而指針變量本身就是變量,存放內存的地址。在大多數情況下,不強調它們的區別,并且把指針變量簡稱為指針,但是實際上它們的意義不同。本文如果沒有說明,把指針和指針變量同等對待。
2.指針變量的定義
(1)定義指針變量的一般形式為:類型名* 指針變量名
①指針聲明符*表明聲明的變量時指針
②類型聲明符表明指針所指向對象的類型
例如:int* pi; //定義了一個指針變量pi,指向整型變量。char* cp; //定義了一個指針變量cp,指向字符型變量
注意:①°無論何種類型的指針變量,它們都是用來存放地址的,因此指針變量自身所占內存的大小和它所指向的變量數據類型無關,盡管不同類型的變量所占內存空間不同,但是不同類型指針變量所占內存空間大小相同。
? ? ? ? ? ②°指針聲明符*不是指針的組成部分,如:int* p;說明p是指針變量,而*p不是。
? ? ? ? ? ③°指針的類型和它所指向變量的類型必須相同。
3.指針變量的初始化
指針變量需要先賦值再使用,看下面賦值語句:
int i,*p;p = &i; p = 0; p = NULL; p = (int*)1732;①對于賦值的第一條語句:&把i的地址取出,賦給指針變量p,這是很常用的一種賦值方法
②對于賦值的第二、三條語句:NULL在stdio.h的文件中有定義,其值為0,這兩條語句把0賦給指針,代表該指針為空指針,不指向任何單元
③對于賦值的第四條語句:使用強制類型轉換(int*)來避免編譯錯誤,表示p指向的地址為1732的int型變量。但是我們不建議把絕對地址賦值給指針,NULL除外。
注意:
①在指針變量定義或者初始化時變量名前面的*,只表示該變量是一個指針變量,它不是間接訪問符
②不能用數值作為指針變量的值(0除外),int* p = 100;//error? ? ? ? ? ? ? ? ? ?int* p = 0;//ok
③可以用初始化了的指針變量給另一個指針變量作初始值
④把一個變量的地址作為初始化值賦給指針變量時,該變量必須在此之前已經定義。因為變量只有在定義后才被分配單元,它的地址才能賦給指針變量。
4.指針類型的意義
(1)指針的類型決定了指針向前或者向后走一步有多大(距離)。
地址是按字節編址的,在C中,指針加1指的是增加一個存儲單元。如:
#include <stdio.h> int main() {int n = 10;char *pc = (char*)&n;int *pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc + 1);printf("%p\n", pi);printf("%p\n", pi + 1); return ?0; }編譯運行該代碼,輸出如下:?
?
?可以看到char類型是1個字節,所以當指針pc+1時,指針pc只向前走了1個字節;而int類型是4個字節,當指針pi+1時,指針pi向前走了4個字節
(2)指針的類型決定了,對指針解引用的時候有多大的權限(能操作幾個字節)。
#include <stdio.h> int main() {int n = 0x11223344;char *pc = (char *)&n;int *pi = &n;*pc = 0; ? *pi = 0; ?return 0; } 調試該代碼,觀察其內存變化,如圖: 由圖可知:char* 的指針解引用就只能訪問一個字節,而 int* 的指針的解引用就能訪問四個字節。5.指針的大小
(1)在32位的機器上,假設有32根地址線,那么假設每根地址線在尋址的時候產生高電平(高電壓)和低電平(低電壓)就是(1或者0);則地址是32個0或者1組成二進制序列,那地址就得用4個字節的空間來存儲,所以一個指針變量的大小就應該是4個字節。 (2)那如果在64位機器上,如果有64個地址線,那一個指針變量的大小是8個字節,才能存放一個地址。 注意:一個小的單元的大小是1byte
6.指針的運算
(1)指針+-整數:
在指針類型的意義那里我們已經見過啦
(2)指針-指針?? ? 結果的絕對值表示它們之間相隔的數組元素的數目,前提:兩個指針指向同一塊區域
TIP:由此我們可以想到,指針-指針可以應用到我們自己模擬實現strlen()的功能。
?(3)指針的關系運算(兩個相同類型的指針)
注意:C語言標準規定:允許指向數組元素的指針與指向數組最后一個元素后面的那個內存位置的指針比較,但是不允許與指向第一個元素之前的那個內存位置的指針進行比較。(第二種方法實際在絕大部分的編譯器上是可以順利完成任務的,然而我們還是應該避免這樣寫,因為標準并不保證它可行)
7.野指針
(1)野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
(2)野指針的成因:
①指針未初始化
#include <stdio.h> int main() { int *p; //局部變量指針未初始化,默認為隨機值*p = 20;return 0; }②指針越界訪問
#include <stdio.h> int main() {int arr[10] = {0};int *p = arr;int i = 0;for(i=0; i<=11; i++){*(p++) = i; //當指針指向的范圍超出數組arr的范圍時,p就是野指針}return 0; }③指針指向的空間釋放(后續筆記再總結)
?(3)規避野指針的做法:
①指針初始化
②小心指針越界
③指針指向空間釋放即使置NULL
④避免返回局部變量的地址
⑤指針使用之前檢查有效性 如:看以下代碼: #include <stdio.h> int main() {int *p = NULL;//....int a = 10;p = &a;if(p != NULL){*p = 20;}return 0; }8.二級指針
(1)二級指針:指向指針的指針(指針變量也是變量,是變量就有地址,指針變量的地址存放在指針里面)
(2)一般定義為:類型名** 變量名
如:int** pa;//int* *pa,pa是一個指針,pa指向的變量是一個int*的變量
(3)二級指針的初始化:
int a = 10; int* pa = &a;//a的地址存在pa中,pa是一級指針 int** ppa = &pa;//pa的地址存在ppa中,ppa是二級指針??
?(4)二級指針的操作:
int a = 10, b = 20, t;int* pa = &a,* pb = &b, pt;int** ppa = &pa, ** pb = &pb, ppt;ppt = ppb; ppb = ppa; ppa = ppt;//①pt = pb; pb = pa; pa = pt;//②t = b; b = a; a = t;//③?部分變量的值變化如表:
| **ppa | **ppb | *pa | *pb | a | b | |
| 原 | 10 | 20 | 10 | 20 | 10 | 20 |
| ① | 20 | 10 | 10 | 20 | 10 | 20 |
| ② | 10 | 20 | 20 | 10 | 10 | 20 |
| ③ | 20 | 10 | 10 | 20 | 20 | 10 |
解析如下:
9.指針表示法和數組表示法
(1)一維數組
①arr[i]? ?等價于? ?*(arr+i),可以認為*(arr+i)的意思是“到內存的arr位置,然后移動i個單位,檢索存儲在那里的值”。
②arr + i? 等價于? &arr[i],前面我們說過數組名是數組首元素的地址,所以arr是該數組首元素的地址,首元素的地址+i得到的是第i個元素的地址,即&arr[i]
注意:不要混淆*(arr+2)和*arr+2;前者的意思是arr第2個元素的值,后者的意思是arr第0個元素的值+2
(2)二維數組
假設有定義:int a[3][2];
①a:數組名是數組首元素的地址。我們可以把二維數組a看成是由a[0],a[1],a[2]組成的一維數組,而a[0],a[1],a[2]各自又是一個一維數組。因此數組名a是a[0](一個內含兩個int值的數組)的首元素地址, a[0]是該數組首元素(a[0][0])的地址。所以我們可以知道兩個等價關系:a? 等價于? &a[0]; a[0]? ? 等價于? ?&a[0][0]
②a[0]是該數組首元素(a[0][0])的地址,所以,*(a[0])等價于a[0][0]的值;與此類似,*a代表該數組首元素(a[0])的值,而a[0]本身又是一個int類型的地址,所以該值的地址為&a[0][0],換言之,*a就是&a[0][0]。**a等價于*&(a[0][0]),因為*a就是首行首元素的地址,所以再對其進行解引用才能找到首元素的值。我們來捋一下:a即&a[0],? a[0]即&a[0][0], 因此,我們可以得出&a[0] 等價于?&&a[0][0]。其實二維數組名相當于一個二級指針,而a[0]相當于一級指針。注意:二級指針和二維數組名是兩碼事
③由于a[i]? 等價于? *(a+i),我們也可以得出a[i][j]? ?等價于? *( *(a+i) + j)或者*(a[i] + j)。
我們來剖析一下 *( *(a+i) + j)這個式子:a是該二維數組首元素的地址(第0行的地址),a+i就是第i行的地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?*(a+i)就是第i行首元素的地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??*(a+i)+j就是第i行,第j列的地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??*(*(a+i)+j)就是第i行,第j個元素
如:*(*(a+2)+1)就是第2行第1列(下標從0開始計數)的元素值,即a[2][1]。
說了這么多,我們上個表格來看看它們之間的等價關系:
| 二級指針 | 一級指針 | 數組元素 | ||||||
| a | &a[0] | &&a[0][0] | *a | a[0] | &a[0][0] | **a | a[0][0] | *(a[0]+0) |
二、指針數組和數組指針
1.指針數組
其實C語言中數組可以是任何類型的,如果數組的各個元素都是指針類型,用于存放內存地址,那么這個數組就是指針數組。
(1)一維指針數組定義的一般格式為:類型名* 數組名[數組長度]
如:int* arr[5];//arr是一個數組,有五個元素,每個元素是一個整形指針。該數組的類型是int* []。(去掉變量名就是類型)
(2)指針數組的初始化:
指針數組的各個元素是指針,用于存放地址,因此,我們可以用指針(地址)作為初始化內容,如:
//(1) int arr[] = {1,2,3}; int* parr[]={arr};//(2) char* color[5] = {"red", "blue", "yellow", "green", "black"}; //字符串常量實質上是一個指向該字符串首字符的指針常量?(3)指針數組和二級指針
char* color[5] = {"red", "blue", "yellow", "green", "black"};
①數組名是數組首元素(一個指向“red”的字符指針)的地址,所以color其實相當于一個二級指針
②color[1]等價于*(color+1):color+1是數組第1個元素的地址,*(color+1)訪問的是數組的第一個元素,它是一個字符指針,指向“blue”的首字符"b"
③*(*(color+2)+4):同理:*(color+2)訪問的是“yellow”的首字符“y”的地址,加4,向后偏移4,找到“o”的地址,再解引用,得到“o”
④(*color+1)[1]:*color實際上訪問的是"red"的首字符“r”的地址,加1,向后偏移1,找到"e"的地址,按照[]的訪問,向后再偏移1個字符的地址,訪問到“d”.這條表達式,它等價于*(*color+1+1)
⑤*color[3]+2:color[3]找到的是第3個元素的地址,*color[3]找到的是第三個元素的首字符“g”,字符"g"+2,實際上是"g"的ASCII碼值+2,得到105,105對應的是字符“i”。
?
?2.數組指針
(1)數組指針:可以指向數組的指針
如:int (*p)[10];//p先和*結合,說明p是一個指針變量,然后指針指向的是一個大小為10的數組,該數組的每個元素是一個整型。所以p是一個指針,指向一個數組,叫數組指針,該數組指針的類型為int(*)[10]。
我們可能會疑問:既然數組指針的類型是int (*)[10],數組指針的變量名為p,為什么不寫成int (*)[10] p;呢?其實是為了好看方便,所以寫成了int (*p)[10];數組指針的一般形式為:類型名 (*指針變量名)[數組長度]。類型名指的是指針指向的數組的元素的類型。
(2)&數組名和數組名
?①第一組的+4的解釋:arr是數組首元素的地址,因為arr的類型是int*,所以arr+1跳過4個字節
②第二組的+4的解釋:&arr[0]是數組首元素的地址,同樣的,&arr[0]的類型也是int*,所以&arr[0]+1也是跳過4個字節
③第三組的+40的解釋:&arr是數組的地址,它的類型是int(*)[10],一個指向大小為10的整型數組的指針,所以它+1,應該跳過一個數組的大小10*4=40。
(3)數組指針的使用
數組指針里面存放的是數組的地址。我們來看2個代碼:
①
?第一個for循環打印出來的是隨機值的原因如圖,解決方案為第二個for循環,我們可以把這個一位數組看成是二維數組的第一行,這樣*(p+0)訪問的是一維數組首元素的地址,然后(*(p+0))[i],就可以訪問后面的元素了,(*(p+0))[i]等價于p[0][i]。其實,這樣傳參大可不必,太容易出錯了,我們應該這樣傳參:print_arr(arr);然后用一級指針接收。
②
#include <stdio.h> void print_arr(int (*arr)[5], int row, int col) {int i;for(i=0; i<row; i++){for(j=0; j<col; j++){printf("%d ", arr[i][j]);}printf("\n");} } int main() {int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};print_arr(arr, 3, 5);return 0; } 數組名arr,表示首元素的地址,但是二維數組的首元素是二維數組的第一行,所以這里傳遞的arr,其實相當于第一行的地址,是一維數組的地址,可以數組指針來接收。 (4)我們再來看一句代碼的意思: int (*parr[10])[5];首先parr先和[]結合,說明它是一個數組,該數組有10個元素,每個元素是一個數組指針,該指針指向5個元素,每個元素的類型是int三、函數指針
1.函數指針定義的一般形式為:(類型名)(*變量名)(參數類型表)
類型名:指定函數的返回類型;變量名:指向函數的指針變量的名稱
如:int(*funp)(int,int);//定義了一個函數指針funp,它可以指向有兩個整型參數且返回值類型為int的函數,該函數指針的類型為int(*)(int,int),由于和數組指針相同的道理,我們不寫成int(*)(int,int) funp;
2.通過函數指針調用函數
(1)在使用函數指針之前,要先對它賦值。賦值時,將一個函數名賦給函數指針,但是該函數必須已經定義或聲明,且函數返回值的類型要和函數指針的類型一樣。(函數名和&函數名意義相同,都代表函數的地址)(函數指針用來存放函數的地址)
(2)函數調用的一般格式:
(*函數指針名)(參數表)
如:假設fun(x, y)已經有定義,現在要調用fun函數
int(*funp)(int,int) = fun;fun = (3,5); (*funp)(3,5); //兩者完全等價3.函數指針作為函數參數:
C語言的調用中,函數名或已賦值的函數指針也能作為實參,此時,形參就是函數指針,它指向實參所代表函數的入口地址,如:
int Add(int x,int y) {return x + y; } int main() {int (*pf)(int, int) = Add;int ret;ret = (*pf)(4, 5);printf("%d\n", ret);return 0; }編譯運行該代碼,輸出如下:
9
?其實ret = (*pf)(4,5);也可以寫成ret = pf(4,5);int (*pf)(int, int) = Add;Add可以賦值給pf并且不報任何警告,所以Add和pf可以說是等價,既然Add(4,5);那么我的pf也可以寫成這樣pf(4,5)。但是前者的寫法更好理解,pf指向Add的地址,解引用后找到函數的內容。
4.一些有趣的代碼:
(1)(*(void (*)())0)();//我們一層層的往外剝。把0強制轉換成“指向返回值為void的函數指針類型”,解引用0地址,就是去0地址處的這個函數,被調用的函數是無參,返回類型為void。該代碼為一次函數調用。其實,我們也可以簡化一下代碼,我們設fp是一個指向返回值為void類型的函數的指針,那么fp的聲明如下:void(*fp)();函數調用如下:(*fp)();最后我們用(void (*)())0來替換fp,即我們的(*(void (*)())0)();。
(2)void (*signal(int , void(*)(int)))(int);//這個其實是一個函數聲明,聲明的函數名是signal,signal函數有2個參數,第一個是int類型,第二個是void(*)(int)的函數指針類型,signal函數的返回類型是void(*)(int)。其實,這個代碼我們也可以簡化,看如下代碼:
typedef void(*pfun_t)(int); pfun_t signal(int, pfun_t); 解釋:把void(*)(int)重新命名為pfun_t,signal函數的返回類型是void(*)(int)即pfun_t。注意:不要寫成typedef void(*)(int)?pfun_t;這是錯的。四、函數指針數組
1.函數指針數組顧名思義就是存放函數指針的數組。
2.函數指針數組的定義:一下哪個代碼才是?
int (*parr1[10])(); int *parr2[10](); int (*)() parr3[10]; 只有第一個才是。parr1 先和 [] 結合,說明parr1是數組,數組的內容是int (*)() 類型的函數指針。該函數指針數組的類型是int(*[])()。 3.函數指針數組的初始化:既然函數指針數組存放的是函數指針,那么我們可以用函數指針(函數的地址)給函數指針數組初始化。見下面的函數指針數組的用途。 4.函數指針數組的用途:轉移表 #include <stdio.h> int Add(int x, int y) {return x + y; } int Sub(int x, int y) {return x - y; } ... int main() {//int (*pf)(int, int) = Add;//int (*pf)(int, int) = Sub;int (*pfArr[2])(int, int) = { Add,Sub };//初始化...return 0; }五、指向函數指針數組的指針
指向函數指針數組的指針:首先它是一個指針,指針指向一個數組 ,數組的元素都是函數指針。它放的是函數指針數組的地址。如: int (*pf)(int, int); //--pf是函數指針int (*pfArr[5])(int, int); //--pfArr是一個函數指針的數組
int (*(*ppfArr)[5])(int, int) = &pfArr;//-pfArr是一個指向函數指針數組的指針,它的類型是int (*(*)[5])(int, int)
六、回調函數
1.回調函數:一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進行響應。
2.常見的回調函數調用:qsort()--快速排序的庫函數,什么類型的數據都可以排序
(1)void*--無具體類型的指針,能夠接收任意類型的地址,缺點:不能進行運算。(因為無類型,你不知道它加減整數和解引用時跳幾個字節)
int a = 10; void* p = &a; p++;//error
(2)qsort()這個庫函數需要引用頭文件<stdlib.h>或<search.h>
其函數聲明為:void qsort( void *base, size_t num, size_t width, int (*cmp?)(const void *elem1, const void *elem2 ) );
①base:要排列的數據的開始
②num:要排序的元素個數
③width:一個元素的大小,單位是字節
④int (*cmp?)(const void *elem1, const void *elem2 ):cmp指向的是:排序時:用來比較2個元素的函數
compare( (void *) elem1, (void *) elem2 );qsort在排序過程中調用該函數一次或多次,該函數比較兩個元素并返回指定其關系的值,每次調用函數都會有返回值,如圖:
當elem1>elem2時,qsort將調換兩元素之間的位置,這是一個升序排序,如果想降序,可以轉換大于和小于的含義,即elem1<elem2時,qsort才調換兩元素之間的位置。
(3)舉例:比較整型數據的大小、結構體內容,看如下代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> struct Stu {char name[20];int age; }; //比較整型數據的大小 int cmp_int(const void* e1, const void* e2) {return *(int*)e1 - *(int*)e2;//e1,e2為void*,無法解引用,所以強制類型轉換int*//此處為升序,如果想降序,可以return *(int*)e2 - *(int*)e1; } //比較結構體內容 int cmp_by_name(const void* e1, const void* e2) {return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name); }int main() {int arr[] = { 1,5,8,9,0,2,3,7,4,6 };struct Stu s[3] = { {"張三",15},{"李四",30},{"王五",10} };int sz = sizeof(arr) / sizeof(arr[0]);int sz = sizeof(s) / sizeof(s[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);//將數組中元素排序qsort(s, sz, sizeof(s[0]), cmp_by_name);//按照名字排序return 0; }調試結果如下:
注意:cmp_int,cmp_by_name這兩個函數不是我們去調用的,而是我們把它的地址傳給qsort,由qsort調用的?
總結
以上是生活随笔為你收集整理的C语言之指针总结(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2辊张力辊张力计算公式
- 下一篇: C语言指针知识点小结