title: 指針
date: 2019-02-17 20:30:46
tags:
- C
categories:
- C
toc: true
理解指針
指針是什麼,舉個栗子
我們隔壁的隔壁宿舍最近經營起了零食店。
我:來5包衛龍大面筋,送到413!
老闆:麼馬達!來咧!
現在,假設這棟宿舍樓沒有門牌號。
我:來5包衛龍大面筋,送到……額……4樓一上樓從最左邊往右第13個宿舍!
老闆:…… !!!???
其實指針就像門牌號一樣,便於定位查找內存中的數據。
4GB的內存條有個房間存數據,沒有門牌號怎麼找?從第一個開始數?哈哈。
在C語言中可以這樣理解一個變量:
int main()
{
int a = 10;
int *b = &a;
return 0;
}
int a = 10;
就是a這個人從房地產商(系統)那裏買來一間房子,裏面放着自己的東西10,此時a這個人的地址是系統知道的,然後a把這個地址(&)告訴了他的好朋友b,並且給了b這間房子的鑰匙(*),這樣b就可以通過地址找到這個房子地址並且在房間裏取或者放自己的東西了。如果a不想讓b亂動家裏東西,可以和b說“不許亂動哦!”(加上const,嘿嘿)。
指針變量
這個其實挺好理解,在中文中,一般把強調的重點放在後面,指針變量是個變量。像這樣理解的還有:數組指針、指針數組。
指針變量是個變量,這個變量裏面存的是地址數據。
指針變量的大小:
不論是什麼類型(包括void *,這個下面細說~):
- 32位環境下,指針變量的大小是4字節。
- 64位環境下,指針變量的大小是8字節。
emmm,怎麼理解呢?
計算機給能訪問的內存地址是規範的長度,全0到全1,32位系統下可編址的範圍32個比特位(4字節),64位機器可編址的範圍爲64個比特位(8字節)。
#include <stdio.h>
int main(int argc, const char * argv[])
{
printf("void*\t%d\n", sizeof(void *));
printf("char*\t%d\n", sizeof(char*));
printf("int*\t%d\n", sizeof(int *));
printf("float*\t%d\n", sizeof(float*));
printf("double*\t%d\n", sizeof(double*));
return 0;
}
光說不頂用,來驗證下,在64位環境下:
void* 8
char* 8
int* 8
float* 8
double* 8
32位機下:
void* 4
char* 4
int* 4
float* 4
double* 4
void *是個什麼鬼
可以做這樣的事:void *vp;
但是void*類型的指針不能被解引用,因爲解引用後它不知道要訪問多大的空間(int*解引用可以訪問4字節的空間,char*解引用後可以訪問1字節的空間)。
void*可以接收任意類型的指針,這樣就可以用它來做一些接口方面的事,這就方便了許多。
指針類型
對於以下代碼:
int main(int argc, const char * argv[])
{
printf("char* : %lu\n",sizeof(char*));
printf("short* : %lu\n",sizeof(short*));
printf("int* : %lu\n",sizeof(int*));
printf("float* : %lu\n",sizeof(float*));
printf("double* : %lu\n",sizeof(double*));
printf("long double* : %lu\n",sizeof(long double*));
return 0;
}
在64位機器下運行結果爲:
在32位機器下運行結果爲:
那麼問題來了,既然佔用大小都是一樣的,爲什麼還要有這麼多類型呢?
指針類型的作用
- 指針類型決定了對指針解引用的時候有多大的權限(能操作幾個字節)。比如:
char*
的指針解引用只能訪問一個字節,而int*
的指針解引用就能訪問四個字節。 - 指針類型決定了指針指向前或者向後一步有多大距離。
看下面的這段代碼:
int main(int argc, const char * argv[])
{
int i = 0x11223344;
int *pi = &i;
*pi = 0;
int c = 0x11223344;
char *pc = &c;
*pc = 0;
return 0;
}
Debug看內存變化:
發現:i=0x11223344
在執行*pi=0;
後全部變爲了0,c=0x11223344
在執行*pc=0;
後只有高位變成了0
說明:int*類型的指針解引用後,操作權限是4個字節(int的大小)。char*解引用後,操作權限僅爲一個字節。
指針類型決定了對指針解引用的時候有多大的權限(能操作幾個字節)。
再來看看第二個小點,對於以下代碼:
int main(int argc, const char * argv[])
{
int n = 10;
int *pi = &n;
char *pc = (char*)&n;
printf("&n : %p\n", &n);
printf("pi : %p\n", pi);
printf("pi+1 : %p\n", pi+1);
printf("-----------------------\n");
printf("pc : %p\n", pc);
printf("pc+1 : %p\n", pc+1);
return 0;
}
有在64位機下有如下輸出:
可以看到:pi=pc=&n
但是:pi+1
和 pc+1
不同,pi+1 - pi
= 8,pc+1 - pc
= 1
指針類型決定了指針指向前或者向後一步有多大距離。
指針的題目
- 題
int main(int argc, const char * argv[])
{
int a[5] = {1,2,3,4,5};
int *p = (int *)(&a+1); // p指向5後面的那個地址
printf("%d,%d\n", *(a+1), *(p-1)); //2,5
return 0;
}
- 題
struct test
{
int Num;
char *pcName;
shortsDate;
char cha[2];
shortsBa[4];
}*p;
假設結構體test的大小爲20個字節,p的地址爲0x100000。
p + 0x1 = ? // p+1 => +20
(unsigned long)p + 0x1 = ? // 0x100001 (eg: int a = 0, a + 1 = 1)
(unsigned int *)p + 0x1 = ? // 0x100004 (加一個指針的大小,32位平臺下4)
- 題
int main()
{
int a[4] = {1,2,3,4};
int *p1 = (int *)(&a+1);
int *p2 = (int *)((int)a+1);
printf("%x,%x\n", p1[-1], *p2);
// p1[-1] 輸出 4
// *p2 這個編譯可以通過,但是運行錯誤
return 0;
}
在內存中:
- 題
int main()
{
int a[3][2] = { (0,1), (2,3), (4,5) };
int *p = a[0];
printf("%d\n", p[0]); // 輸出1
return 0;
}
注意逗號表達式:運算結果爲後面的值
所以:
- 題
int main()
{
int a[5][5];
int (*p)[4];//注意這裏!
p = a;
printf("%p,%d\n", &p[4][2]-&a[4][2], &p[4][2]-&a[4][2]); //輸出: -4的補碼,-4
return 0;
}
輸出他們之間元素的個數。
因爲p[4][2]
的地址小於a[4][2]
的地址,所以爲-4
,但是由於輸出的時候,%p
輸出的是地址,也就是一個無符號的數,所以將-4
的補碼輸出,%d
正常輸出。
- 題
int main()
{
int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
int *p1 = (int *)(&aa + 1);
int *p2 = (int *)(*(aa + 1));
printf("%d,%d\n", *(p1-1), *(p2-1)); // 輸出10,5
return 0;
}
&aa+1
跨過了整個數組aa
的長度,指向元素10後的地址。
aa+1
代表的跨過了一個aa
的元素,而aa
是一個二維數組,它的元素是一個一維數組。如下圖:
- 題
int main()
{
char *a[] = {"work", "at", "360"};
char **pa = a;
pa++;
printf("%s\n", *pa); // 輸出at
return 0;
}
- 題
int main()
{
char *c[] = { "ENTER", "NEW", "POINT", "FIRST" };
int **cp[] = { c+3, c+2, c+1, c };
char ***cpp = cp;
printf("%s\n", **++cpp); // POINT
printf("%s\n", *--*++cpp+3); // ER
printf("%s\n", *cpp[-2]+3); // ST
printf("%s\n", cpp[-1][-1]+1); // EW
return 0;
}
++、—
的優先級高於*
,*
的優先級高於+
。++cpp
會影響cpp
的值,但cpp+1
不會影響。[]
的優先級大於*
。
**++cpp
:先++
,此時的cpp
指向cp[1]
,解引用爲c[2]
,再解引用即爲POINT
。
*--*++cpp+3
:經過上一步,cpp
現在的指向如上圖。
先++
,此時cpp
指向cp[2]
,解引用即爲cp[2]
,再--
,此時改變了cp[2]
的指向,他指向c[0]
,再解引用即爲c[0]
,給c[0]+3
,輸出的結果爲ER
。
*cpp[-2]+3
:經過上一步,現在的指向如上圖所示。
cpp[-2]
指向了cp[0]
,解引用指向c[3]
,c[3]+3
輸出ST
。
cpp[-1][-1]+1
:經過上一步並沒有改變指針的指向。
cpp[-1][-1]
代表c[1]
,再+1
輸出EW
。
指針和數組
數組名
除了以下兩種情況外,一般情況下,數組名都代表數組首元素的地址。
數組名代表整個數組的情況:
-
sizeof
中的數組名(只出現數組名)代表整個數組。sizeof(arr)
這裏的數組名代表的是整個數組,但是sizeof(arr+0)
代表首元素地址的大小。 -
&arr
代表整個數組。(實際上&arr
表示的是數組的地址,而不是數組首元素的地址,數組的地址+1,會跳過整個數組的大小)
int arr[5] = {1,2,3,4,5};
printf("sizeof(arr) -> %d\n", sizeof(arr));
printf("sizeof(arr+0) -> %d\n", sizeof(arr+0));
printf("sizeof(&arr) -> %d\n", sizeof(&arr));
printf("arr -> %p\n", arr);
printf("&arr -> %p\n", &arr);
printf("arr+1 -> %p\n", arr+1);
printf("&arr+1 -> %p\n", &arr+1);
輸出:
sizeof(arr) -> 20
sizeof(arr+0) -> 8 // 64bit環境下指針的大小爲8字節,32bit環境下指針的大小爲4字節,這裏arr+0代表的是首元素地址
sizeof(&arr) -> 8 // 數組的地址大小(也是一個地址),佔8個字節。
arr -> 0x7ffeef8929a0
&arr -> 0x7ffeef8929a0
arr+1 -> 0x7ffeef8929a4 // 跳過了一個元素
&arr+1 -> 0x7ffeef8929b4 // 這裏跳過了整個數組的大小(20)
對指針+1,實際上加的是這個指針類型的大小,比如整型指針+1,地址+4。
區分指針數組和數組指針
指針數組:是一個數組,數組的元素的指針。
數組指針:是一個指針,指向數組的指針。
從字面意思上來看就是誰在後就是什麼東西。
int *p1[10];
int (*p2)[10];
int *p1[10]
這裏的p1先和[]
結合,所以他是數組,是指針數組。
int (*p2)[10]
p2先和*
結合,是指針,是數組指針。
數組指針的使用
數組指針 指向數組,那麼數組指針中存的就是數組的地址咯。
int main(int argc, const char * argv[])
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;
return 0;
}
但一般不這麼使用。一個數組指針的使用:
void init_arr(int (*arr)[5], int row, int col)
{
for(int i=0; i<row; i++)
{
for(int j=0; j<col; j++)
{
arr[i][j] = i+j;
}
}
}
void disp_arr(int (*arr)[5], int row, int col)
{
for(int i=0; i<row; i++)
{
for(int j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {0};
init_arr(arr, 3, 5);
disp_arr(arr, 3, 5);
return 0;
}
數組傳參,指針傳參
-
一維數組傳參
void test11(int arr[]){;} // ok void test12(int arr[10]){;} // ok void test13(int *arr){;} // ok void test21(int *arr[20]){;} // ok void test22(int **arr){;} // ok int main() { int arr1[10] = {0}; int *arr2[20] = {0}; //// 數組指針 test11(arr1); test12(arr1); test13(arr1); test21(arr2); test22(arr2); return 0; }
-
二維數組傳參
void test(int arr[3][5]){;} // ok void test(int arr[][]){;} // error! void test(int arr[][5]){;} // ok void test(int *arr){;} // error! void test(int* arr[5]){;} // error! void test(int (*arr)[5]){;} // OK! void test(int **arr){;} // error! int main() { int arr[3][5] = {0}; test(arr); return 0; }
-
一級指針傳參
void print(int *p, int size) { for(int i=0; i<size; i++) { printf("%d\n", *(p+i)); } } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9}; int *p = arr; int size = sizeof(arr)/sizeof(arr[0]); print(p, size); return 0; }
-
二級指針傳參
void test(int **p) { printf("%d\n", **p); } int main() { int n = 10; int *p = &n; int **pp = &p; test(&p); test(pp); return 0; }
函數指針
函數指針
函數名代表的是函數地址
int test()
{
printf("ahoj\n");
return 0;
}
int main()
{
printf("test %p\n", test);
printf("&test %p\n", &test);
test();
return 0;
}
輸出:
test 0x100000ef0
&test 0x100000ef0
ahoj
函數也是有地址滴,要保存函數地址,就要用到函數指針。
void (*pf1)(); // 函數指針,先和*結合,再與()結合。pf1是一個指針,指向一個無參數的函數,返回值爲void
void *pf2(); // 返回值爲void *的函數
到這裏才理解了《C陷阱和缺陷》裏的那段代碼:(*(void (*)())0)();
這裏的被強制轉換爲void(*)()
,函數指針=>解引用,0代表一個函數的地址(地址爲0處的函數)
再來看個代碼:void (*signal(int, void(*)(int)))(int);
這是一段函數聲明,函數的返回值爲:signal先與*結合=>是個函數指針,指向一個參數爲(int, 函數指針類型)
的一個函數。參數爲:int。
對於上面的兩行代碼,太複雜,需要簡化一下:
typedef void(*pfun_t)(int); // pfun_t是一個函數指針,指向一個參數爲int的函數
pfun_t signal(int, pfun_t); // signal的返回值是個函數指針,參數是int和一個函數指針
函數指針數組
數組用來存放相同類型數據的,那麼把函數的地址存到一個數組中,這個數組就叫做函數指針數組。
int (*parr[10])();
parr
先和[]
結合,說明parr
是個數組,數組的內容是什麼類型呢?是int (*)()
類型的函數指針。
-
函數指針數組的用途:轉移表(例子:計算器)
普通版:
#include <stdio.h> int 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 diiv(int a, int b) { return a/b; } int main() { int x, y; int input = 1; int ret = 0; while(input) { printf("=========\n"); printf("( 1. + )\n"); printf("( 2. - )\n"); printf("( 3. * )\n"); printf("( 4. \\ )\n"); printf("=========\n"); printf("choice>"); scanf("%d", &input); switch(input) { case 1: printf("<(a b)>"); scanf("%d%d", &x, &y); ret = add(x, y); break; case 2: printf("<(a b)>"); scanf("%d%d", &x, &y); ret = sub(x, y); case 3: printf("<(a b)>"); scanf("%d%d", &x, &y); ret = mul(x, y); case 4: printf("<(a b)>"); scanf("%d%d", &x, &y); ret = diiv(x, y); default: printf("error!\n"); break; } printf("ret = %d\n", ret); } return 0; }
函數指針數組實現:
int main() { int x, y; int input = 1; int ret = 0; int (*p[5])(int x, int y) = {0, add, sub, mul, diiv}; // 轉移表 while(input) { printf("=========\n"); printf("( 1. + )\n"); printf("( 2. - )\n"); printf("( 3. * )\n"); printf("( 4. \\ )\n"); printf("=========\n"); printf("choice>"); scanf("%d", &input); if((input<=4 && input>=1)) { printf("a b >"); scanf("%d%d", &x, &y); ret = (*p[input])(x,y); } else { printf("error!"); } printf("ret = %d\n", ret); } return 0; }
指向函數指針數組的指針
上面這個東西,首先是個指針,指向一個數組,數組的元素是函數指針。
void test(const char *str)
{
printf("%s\n", str);
}
int main()
{
// 函數指針pf
void (*pf)(const char *) = test;
// 函數指針的數組pfarr
void (*pfarr[5])(const char *str);
pfarr[0] = test;
// 指向函數指針數組pfarr的指針ppfarr
void (*(*ppfarr)[10])(const char *) = &pfarr;
return 0;
}
回調函數
回調函數就是一個通過函數指針調用的函數。如果把函數的地址作爲參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,稱爲這是一個回調函數。
回調函數不是由該函數的實現方法直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對事件或條件進行相應。
簡單認識qsort函數:
#include <stdlib.h>
void
qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
返回值void
,第一個參數是要排序的數組,第二個參數要排序數組元素的個數,第三個參數時每個元素的大小(所佔字節數,比如int類型佔4字節),第四個參數是一個比較大小用的回調函數(這個函數返回一個整數,參數爲兩個指針)。
qsort函數的使用:
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
int int_cmp(const void * p1, const void *p2)
{
// return (*(int *)p1 < *(int *)p2); //這樣只對正整數有效
int x = *(int *)p1;
int y = *(int *)p2;
if(x < y)
return -1;
else if(x == y)
return 0;
else
return 1;
}
int main(int argc, const char * argv[])
{
int arr[] = {11,33,22,-11,-22,-300,32,0};
qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_cmp);
for(int i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
使用冒泡排序模擬實現qsort這種類型的排序函數:
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
int int_cmp(const void * p1, const void *p2)
{
// return (*(int *)p1 < *(int *)p2); //這樣只對正整數有效
int x = *(int *)p1;
int y = *(int *)p2;
if(x < y)
return -1;
else if(x == y)
return 0;
else
return 1;
}
void swap(void *p1, void *p2, int size)
{
for(int i=0; i<size; i++)
{
char tmp = *((char *)p1+i);
*((char *)p1+i) = *((char *)p2+i);
*((char *)p2+i) = tmp;
}
}
void
myqsort(void *base,
int len,
int width,
int (*cmp)(void *p1, void *p2))
{
for(int i=0; i<len; i++)
{
for(int j=0; j<len-1-i; j++)
{
if( cmp((char *)base+j*width,(char *)base+(j+1)*width) > 0 )
{
swap((char *)base+j*width,
(char *)base+(j+1)*width,
sizeof(int));
}
}
}
}
int main(int argc, const char * argv[])
{
int arr[] = {11,33,22,-11,-22,-300,32,0};
qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_cmp);
for(int i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
參考:《C和指針》、《劍指offer》