把知识记在小本本上

将零散的知识点放在一个集中的地方,不断递归重构,形成一套为己所用的知识系统。

博客首页 | 小本本首页

理解指针

指针是什么,举个栗子

我们隔壁的隔壁宿舍最近经营起了零食店。

我:来5包卫龙大面筋,送到413!

老板:么马达!来咧!

现在,假设这栋宿舍楼没有门牌号。

我:来5包卫龙大面筋,送到……额……4楼一上楼从最左边往右第13个宿舍!

老板:…… !!!???

其实指针就像门牌号一样,便于定位查找内存中的数据。

4GB的内存条有$2^{32}$个房间存数据,没有门牌号怎么找?从第一个开始数?哈哈。

在C语言中可以这样理解一个变量:

1
2
3
4
5
6
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字节)。

1
2
3
4
5
6
7
8
9
10
11
#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位环境下:

1
2
3
4
5
void*	8
char* 8
int* 8
float* 8
double* 8

32位机下:

1
2
3
4
5
void*	4
char* 4
int* 4
float* 4
double* 4

void *是个什么鬼

可以做这样的事:void *vp;

但是void*类型的指针不能被解引用,因为解引用后它不知道要访问多大的空间(int*解引用可以访问4字节的空间,char*解引用后可以访问1字节的空间)。

void*可以接收任意类型的指针,这样就可以用它来做一些接口方面的事,这就方便了许多。

指针类型

对于以下代码:

1
2
3
4
5
6
7
8
9
10
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位机器下运行结果为:

-64

在32位机器下运行结果为:

gcc main.c -m32

那么问题来了,既然占用大小都是一样的,为什么还要有这么多类型呢?

指针类型的作用

  • 指针类型决定了对指针解引用的时候有多大的权限(能操作几个字节)。比如:char*的指针解引用只能访问一个字节,而int*的指针解引用就能访问四个字节。
  • 指针类型决定了指针指向前或者向后一步有多大距离。

看下面的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
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看内存变化:

p

img

发现:i=0x11223344在执行*pi=0;后全部变为了0,c=0x11223344在执行*pc=0;后只有高位变成了0

说明:int*类型的指针解引用后,操作权限是4个字节(int的大小)。char*解引用后,操作权限仅为一个字节。

指针类型决定了对指针解引用的时候有多大的权限(能操作几个字节)。

再来看看第二个小点,对于以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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位机下有如下输出:

pic

可以看到:pi=pc=&n

但是:pi+1pc+1 不同,pi+1 - pi = 8,pc+1 - pc = 1

指针类型决定了指针指向前或者向后一步有多大距离。

指针的题目

1
2
3
4
5
6
7
8
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
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)
1
2
3
4
5
6
7
8
9
10
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;
}

在内存中:

指针笔试题3

1
2
3
4
5
6
7
8
int main()
{
int a[3][2] = { (0,1), (2,3), (4,5) };
int *p = a[0];
printf("%d\n", p[0]); // 输出1

return 0;
}

注意逗号表达式:运算结果为后面的值

所以:

4

1
2
3
4
5
6
7
8
9
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;
}

5

输出他们之间元素的个数。

因为p[4][2]的地址小于a[4][2]的地址,所以为-4,但是由于输出的时候,%p输出的是地址,也就是一个无符号的数,所以将-4的补码输出,%d正常输出。

1
2
3
4
5
6
7
8
9
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是一个二维数组,它的元素是一个一维数组。如下图:

6

1
2
3
4
5
6
7
8
9
int main()
{
char *a[] = {"work", "at", "360"};
char **pa = a;
pa++;
printf("%s\n", *pa); // 输出at

return 0;
}

7

1
2
3
4
5
6
7
8
9
10
11
12
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不会影响。[]的优先级大于*

8.1

**++cpp:先++,此时的cpp指向cp[1],解引用为c[2],再解引用即为POINT

8.2

*--*++cpp+3:经过上一步,cpp现在的指向如上图。

++,此时cpp指向cp[2],解引用即为cp[2],再--,此时改变了cp[2]的指向,他指向c[0],再解引用即为c[0],给c[0]+3,输出的结果为ER

8.3

*cpp[-2]+3:经过上一步,现在的指向如上图所示。

cpp[-2]指向了cp[0],解引用指向c[3]c[3]+3输出ST

cpp[-1][-1]+1:经过上一步并没有改变指针的指向。

cpp[-1][-1]代表c[1],再+1输出EW

指针和数组

数组名

除了以下两种情况外,一般情况下,数组名都代表数组首元素的地址。

数组名代表整个数组的情况:

  1. sizeof中的数组名(只出现数组名)代表整个数组。sizeof(arr)这里的数组名代表的是整个数组,但是sizeof(arr+0)代表首元素地址的大小。

  2. &arr代表整个数组。(实际上&arr表示的是数组的地址,而不是数组首元素的地址,数组的地址+1,会跳过整个数组的大小)

1
2
3
4
5
6
7
8
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);

输出:

1
2
3
4
5
6
7
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。

区分指针数组和数组指针

指针数组:是一个数组,数组的元素的指针。

数组指针:是一个指针,指向数组的指针。

从字面意思上来看就是谁在后就是什么东西。

1
2
int *p1[10];
int (*p2)[10];

int *p1[10]这里的p1先和[]结合,所以他是数组,是指针数组。

int (*p2)[10]p2先和*结合,是指针,是数组指针。

数组指针的使用

数组指针 指向数组,那么数组指针中存的就是数组的地址咯。

1
2
3
4
5
6
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;
}

但一般不这么使用。一个数组指针的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
}

数组传参,指针传参

  • 一维数组传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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;
    }
  • 二维数组传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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;
    }
  • 一级指针传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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;
    }
  • 二级指针传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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;
    }

函数指针

函数指针

函数名代表的是函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int test()
{
printf("ahoj\n");
return 0;
}

int main()
{
printf("test %p\n", test);
printf("&test %p\n", &test);
test();

return 0;
}

输出:

1
2
3
test    0x100000ef0
&test 0x100000ef0
ahoj

函数也是有地址滴,要保存函数地址,就要用到函数指针。

1
2
void (*pf1)();	// 函数指针,先和*结合,再与()结合。pf1是一个指针,指向一个无参数的函数,返回值为void
void *pf2(); // 返回值为void *的函数

到这里才理解了《C陷阱和缺陷》里的那段代码:(*(void (*)())0)();

这里的$0$被强制转换为void(*)(),函数指针=>解引用,0代表一个函数的地址(地址为0处的函数)

再来看个代码:void (*signal(int, void(*)(int)))(int);

这是一段函数声明,函数的返回值为:signal先与*结合=>是个函数指针,指向一个参数为(int, 函数指针类型)的一个函数。参数为:int。

对于上面的两行代码,太复杂,需要简化一下:

1
2
typedef void(*pfun_t)(int);	// pfun_t是一个函数指针,指向一个参数为int的函数
pfun_t signal(int, pfun_t); // signal的返回值是个函数指针,参数是int和一个函数指针

函数指针数组

数组用来存放相同类型数据的,那么把函数的地址存到一个数组中,这个数组就叫做函数指针数组

int (*parr[10])();

parr先和[]结合,说明parr是个数组,数组的内容是什么类型呢?是int (*)()类型的函数指针。

  • 函数指针数组的用途:转移表(例子:计算器)

    普通版:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    #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;
    }

    函数指针数组实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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;
    }

指向函数指针数组的指针

上面这个东西,首先是个指针,指向一个数组,数组的元素是函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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函数:

1
2
3
#include <stdlib.h>
void
qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));

返回值void,第一个参数是要排序的数组,第二个参数要排序数组元素的个数,第三个参数时每个元素的大小(所占字节数,比如int类型占4字节),第四个参数是一个比较大小用的回调函数(这个函数返回一个整数,参数为两个指针)。

qsort函数的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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这种类型的排序函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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》