• 【C语言从青铜到王者】第六篇·详解C指针


    本篇前言

    “指针是C语言的灵魂,精通指针也就基本精通了C语言。”
    指针确实非常重要,但是它只是一个工具,是帮助我们分配内存的工具,而了解指针,并且通过指针熟练的操作内存,这才是我们努力的方向。



    指针到底是什么?


    编程语言的本质是什么?

    我们可以把计算机当成一个外国人,而我们想和他沟通必须用同一种语言。虽然目前有些AI看似已经能够直接理解汉语,但是这些AI程序仍然是用计算机语言来编写的。也就是说,我们暂时并不能指望计算机来直接理解我们的语言,所以我们要想与之沟通,并让它为我们服务,就必须学习它的语言。

    那在C语言中,计算机是如何理解我们描述给它的问题的呢?

    首先,计算机是通过数据化处理来认识这个世界的。万事万物,在计算机眼里都是数据

    比如,有这样一个事实:“我有一个苹果的,它的价格是2元”

    我们把苹果的价格设为一个变量apple_price,那代码就可以写成:

    int apple_price = 2;
    
    • 1

    int apple_price = 2;这句话用我们的“人话”来说,就是“苹果的单价是2元”,而对于计算机来说,就是“在内存中开辟了一块4字节大小的空间,这个空间是专门划分给一个叫apple_price的变量的,这个变量的数据类型是整型,然后在这块空间中按照整型变量的存放规则放入了值为2(十进制)的二进制数00000000 00000000 00000000 00000010”

    内存是什么?

    刚刚那句“计算机说的话”用图像来表示就是:

    image.png

    那内存又是什么呢?我们可以把内存想象成一系列的储存0或1的“格子”(图中的所有格子都表示内存),每8个格子代表一个内存单元(图中的一行格子),每开辟一块变量的空间就是划定一段连续的格子(比如图中的灰色格子就是开辟一个int型变量的空间),用来存放新的01序列,这些新的01序列就会根据特定的规则存放数据,比如int型数据(有符号整型)就是需要32个格子,代表32位二进制序列,序列首元素代表符号,1正0负,其余的按照二进制规则表示数字

    而计算机能读懂的其实就是这一连串的01序列,也叫“机器语言”,我们今后所写的所有程序,实际上都会被翻译成成千上亿的0和1储存在这些格子中,我们操作变量,修改代码,本质上就是改变这些0和1的储存位置
    下图就是“计算机眼里的世界”
    image.png

    那看起来计算机做的工作就是不断的往空的格子里放0/1。而我们知道存放东西的目的是为了我们将来把它取出。而想要快速高效的取出就意味着这些0和1必须整整齐齐的按照一定顺序的摆放在格子里。这就像我们的快递柜一样,我们把包裹放入指定的有编号的柜子,将来再通过查找编号将其取出。

    所以继续思考一个问题:内存是如何有序的存放这一系列的0和1的呢?

    实际上内存也是一个大快递柜,它的每一个柜子都有编号,这个编号就是我们说的“内存的地址”

    每个内存单元的编号就是地址

    image.png

    内存会像上图一样给内存单元编号,第一个内存单元就是1号,第二个就是2号…但是问题是计算机有成千上亿的内存单元,而计算机又只认识二进制语言(0或1),所以我们是不能用十进制的数字来编号的,必须用二进制来表示

    C语言规定:在32位系统上,地址是32位二进制序列,在64位系统上,地址是64位二进制序列

    也就是说,在32位系统上,每个地址是32位二进制序列,所以最多可以表征232个内存单元,一个内存单元的大小是一个字节,也就是1B,210字节就是1KB,220字节就是1MB,230字节就是1GB,2^32字节就是4GB,所以32位系统最多可以有4GB的内存
    当然啦,随着科技的进步,我们对内存的需求远远超过了4GB。我们现在的电脑基本都是64位系统,也就是地址都是64位二进制序列,最多可以表征264个内存单元,也就是4×232GB的内存,这个数量在大多数场景下是够用的。当然对于为了满足某些特殊需求而设计出来的超级计算机,内存又会呈指数级增长了。

    我们在编译环境下查看地址时,一般都是16进制。这是因为32个0/1写出来过于繁琐,一般编译器展示给我们的序列都是8位16进制(64位编译器就是16位16进制)的序列
    image.png


    指针是什么?

    理解了上面的东西后,我们终于可以来说指针是什么

    看下面一段代码:

    #include
    int main()
    {
    	int a = 10;
    	int* pa = &a;
    	*pa = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    &a中的&是取地址操作符,可以把a的地址取出来(得到快递柜的编号)。int*是一种特殊的数据类型,这种类型的变量专门用来存放int型变量的地址

    所以int* pa = &a;的意思就是“把a所在内存的地址取出来放到pa中”。(把这个编号记下来)

    *pa解引用操作,就是通过pa中的地址找到这块地址所对应的内存空间(对着编号打开了这个快递柜)

    所以*pa = 20;的意思就是通过pa中存放的地址(也就是a的地址)找到这块地址所对应的内存空间(也就是a),然后把20放入这块空间(把20这个数字放入快递柜)

    所以此时a的值实际上已经变成了20(快递柜里的数字已经从10变成了20)

    上面这段程序中的变量pa就是指针

    指针就是一个存放地址的变量

    指针,也叫做指针变量,数据类型为指针类型的,专门用来存放地址的变量,大小在32位系统上为4字节,在64位系统上为8字节

    通过操作指针,我们就可以存放一块内存的地址,就可以很方便的通过地址来访问和管理这块内存。C语言能够通过指针直接管理内存,这也是C语言常常是底层编码语言的原因。


    指针类型

    指针变量的数据类型就是指针类型

    int*

    char*

    short*

    ...


    指针类型的大小

    指针变量是用来储存地址的,因为地址是32位(在32位机器上),所以指针类型的大小是4字节

    做个实验验证一下:

    #include
    int main()
    {
    	printf("%d\n", sizeof(int*));
    	printf("%d\n", sizeof(char*));
    	printf("%d\n", sizeof(short*));
    	printf("%d\n", sizeof(long*));
    	printf("%d\n", sizeof(float*));
    	printf("%d\n", sizeof(double*));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image.png


    指针类型决定解引用的访问权限

    不同指针类型的特殊性体现在两点:
    一就是指针类型决定了指针解引用的访问权限

    看下面的程序

    #include
    int main()
    {
    	int a = 0x11223344;
    	int* pa = &a;
    	*pa = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    将程序运行到图示位置
    在这里插入图片描述

    此时调出内存查看器发现a所在的内存中为:
    image.png
    继续运行
    image.png
    发现a所在的内存中变成了00 00 00 00
    image.png

    当我们修改一下程序,用char*型的指针接收地址时:

    #include
    int main()
    {
    	int a = 0x11223344;
    	char* pa = &a;
    	*pa = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行到图示位置
    image.png

    image.png

    发现只有一个字节的数据被改变了。也就是说,char*类型的指针只能访问一字节的内存。实际这是因为char类型的大小就是一字节。所以指针类型对应的数据类型的大小决定了其解引用时能够访问的内存大小


    指针类型决定指针的步长

    指针类型的第二个特殊性就是决定了指针“走一步走多远”,也就是指针的步长

    看下面这段程序

    #include
    int main()
    {
    	int arr[10] = { 0 };
    	int* p = arr;
    	char* pc = arr;
    	printf("%p\n", p);
    	printf("%p\n", p + 1);
    	printf("%p\n", pc);
    	printf("%p\n", pc + 1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果为
    image.png

    7C→80 4字节

    7C→7D 1字节

    差别的根源就是这两个指针的指针类型一个是int*一个是char*

    通过以上两个对指针类型的了解,我们可以用下面的写法来操作数组元素:

    #include
    int main()
    {
    	int arr[10] = { 0 };
    	char* p = arr;
    	int i = 0;
    	for (i = 0; i < 40 ; i++)
    	{
    		*(p + i) = i;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行一下看看
    image.png
    发现成功的逐字节的存入了数字123456…40(显示出来是16进制)
    image.png


    野指针

    指针指向的位置是不明确的指针就是野指针。

    野指针的出现会干扰我们对于内存的管理,所以我们需要了解野指针的成因然后规避野指针的出现


    野指针成因

    1.指针未初始化

    #include
    int main()
    {
    	int* p;
    	*p = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    局部变量指针变量p未初始化,默认为随机值

    此时再对p解引用,就是非法访问内存

    一般编译器也会直接报错

    2.指针越界访问

    #include
    int main()
    {
    	int arr[10] = { 0 };
    	int* p = arr;
    	int i = 0;
    	for (i = 0; i <= 10; i++)
    	{
    		*p = i;
    		p++;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    循环到第十一次时,指针就不指向数组了,此时解引用访问,就是非法访问内存,第十一个指针就是野指针

    3.指针指向的空间被释放

    #include
    int* test()
    {
    	int a = 10;
    	return &a;
    }
    int main()
    {
    	int* p = test();
    	*p = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    a是局部变量,出函数即被销毁,再次通过a的地址操作内存就是非法访问内存


    规避野指针的方法

    1.指针一旦使用必须初始化。不知道初始化什么地址就初始化为NULL空指针,明确知道初始化的值,那就直接取地址初始化。

    2.小心越界访问内存的行为。比如循环访问内存时,看清楚最后一次循环访问的内存有没有越界。

    3.指针指向空间一旦销毁就要把指针置成空指针。这个在设定局部变量指针时要特别注意。

    4.空指针不可以直接解引用访问,因为空指针指向的空间不存在于内存中。

    对于情况4,我们可以写下面的程序进行指针有效性的检查:

    if (p != NULL)
    {
       //操作
    }
    
    • 1
    • 2
    • 3
    • 4

    指针的状态只能是合法的可操作指针空指针,也就是说,指针变量里的地址必须有明确的指向


    指针运算

    讲到这里,对于指针本身的我们认识的差不多了。接下来我们要学习如何运用指针进行运算。

    指针+ - 整数

    指针±整数就是进行单位步长的移动(具体程序可以参考前面“指针类型决定指针的步长”)

    指针的关系运算

    指针的关系运算就是比较两个地址的大小。

    地址在一个数组里是随着数组下标的增加由低到高变化的

    比如下面的程序通过指针的关系运算来初始化数组元素:

    int main()
    {
    	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    	int* p = arr;
    	int* p_end = arr + 10;
    	while (p < p_end)
    	{
    		printf("%d\n", *p);
    		p++;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    指针 - 指针

    指针-指针就是两个指针之间的元素数量

    int main()
    {
    	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    	printf("%d\n", &arr[9] - &arr[0]);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结果是9,表明第10个元素和第1个元素之间有9个元素(不是8个)。

    注意:指向相同空间的指针才可以相减

    例如下面的程序利用这个技巧求字符串长度:

    int my_strlen(char* str)
    {
    	char* start = str;
    	while (*str != '\0')
    	{
    		str++;
    	}
    	return str - start;
    }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    const 修饰指针

    看一个简单的程序,用指针修改变量值

    #include
    int main()
    {
    	int num = 10;
    	int* p = &num;
    	*p = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果我们规定num的值不能改,按理说只需要给numconst把它变成常变量即可

    #include
    int main()
    {
    	const int num = 10;
    	int* p = &num;
    	*p = 20;
        printf("%d", num);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image.png

    结果还是改了。这是怎么回事呢?

    这种写法的问题在于我们不应该用const修饰num变量,因为const修饰的变量只是不能被直接赋值符=影响,但是仍然可以通过指针访问的形式修改它的值。

    自然而然的,既然不修饰num,那就只能修饰num的指针了

    下面介绍const修饰指针的两种不同的写法


    const 放在 * 前面

    表示指针指向的内存不能通过指针被改变:

    #include
    int main()
    {
    	int num = 10;
    	const int* p = &num;
    	*p = 20;
    	printf("%d", num);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image.png
    这个写法就是我们前面程序想要达到的效果。


    const 放在 * 和变量名之间

    表示指针变量p不能被更改:

    #include
    int main()
    {
    	int num = 10;
    	int a = 1;
    	int* const p = &num;
    	p = &a;
    	printf("%d", num);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image.png
    这种写法约束的是指针变量p,而不是整型变量num


    二级指针和多级指针

    指针变量的地址,存放到另一个指针变量中,第二指针变量就是二级指针,同理,第三指针变量就是三级指针

    二级指针的指针类型就是再加一个*,比如int型二级指针的数据类型就是int**,同理,三级指针是int***

    int main()
    {
    	int a = 10;
    	int* pa = &a;
    	int** ppa = &pa;//二级指针
    	int*** pppa = &ppa;//三级指针
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    再复杂的程序中可能会用到多级指针。暂时了解即可


    指针与字符串

    字符串与指针的结合有三个特殊点:

    • 1.字符串可以直接存入指针变量。此时指针中存入的是字符串首元素的地址,解引用所访问的也是字符串的首元素所在内存
    #include
    int main()
    {
    	char* ps = "I love C";
    	printf("%c\n", *ps);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image.png

    虽然解引用可以访问这块内存,但是我们是无法改变这块内存的值:

    image.png
    原因是这种写法实际上并没有开辟一块新的内存。对于常量12345…等数字和ABCD…abcd…等字母,计算机为了节省空间,直接给它们分配了固定的内存。而对于一个常量,计算机同样也只会开辟固定的内存来存储它们。这些内存都只可读不可写(写就是修改的意思)。

    2.打印字符串的首地址,可以直接获取整个字符串

    #include
    int main()
    {
    	char* ps = "I love C";
    	printf("%s\n", ps);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image.png

    做个实验:

    #include
    int main()
    {
    	char *ps = "I love C";
    	printf("%p", ps );
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    #include
    int main()
    {
    	char *ps = "I love C";
    	printf("%p", ps + 1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    #include
    int main()
    {
    	char *ps = "I love C";
    	printf("%p", ps + 2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    所以对于变量,计算机会开辟一块连续的内存用于存放,而对于常量,计算机选择的是直接访问固定位置的内存。

    3.常量字符串的地址相同

    #include
    int main()
    {
    	char* ps1 = "I love C";
    	char* ps2 = "I love C";
    	printf("%p\n", ps1);
    	printf("%p\n", ps2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image.png
    原理也是因为存入的是同样的常量字符串,所以直接访问了同一块内存,地址自然是一样的。


    指针与数组

    指针与数组的结合也有以下几个特殊点

    指针数组

    整型数组是由整型组成的数组,
    字符数组的由字符组成的数组,
    所以指针数组就是由指针组成的数组

    int main()
    {
    	int a = 0;
    	int* arr[10] = { &a };
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    arr就是指针数组

    指针数组的使用场景:

    下面这段程序就演示了怎么通过指针数组模拟二维数组:

    #include
    int main()
    {
    	int a[] = { 1,2,3,4,5 };
    	int b[] = { 2,3,4,5,6 };
    	int c[] = { 3,4,5,6,7 };
    	int* arr[3] = { a,b,c };
    	int i = 0;
    	for (i = 0; i < 3; i++)
    	{
    		int j = 0;
    		for (j = 0; j < 5; j++)
    		{
    			printf("%d ", *(arr[i] + j));
                //printf("%d ", arr[i][j]);
    		}
    		printf("\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image.png


    数组指针

    整型指针是指向整型的指针

    字符指针是指向字符的指针

    所以数组指针是指向数组的指针

    数组指针的指针类型写法有点复杂。看下面的程序

    #include
    int main()
    {
    	int arr[10] = { 10 };
    	int(*parr)[10] = &arr;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    parr就是数组指针,存放的是arr数组的地址

    数组的类型是int [10],所以int(* ) [10]就是这个数组指针的数据类型。注意数组指针的指针名要写在括号内

    上面这个例子是整型数组的指针,那么问题来了:指针数组的指针是什么呢?看下面的写法

    #include
    int main()
    {
    	int a = 0;
    	int* pa = &a;//pa是整型指针,存放a的地址
    	int* arr[1] = { pa };//arr是指针数组,存放的元素是指针pa
    	int* (*parr1)[1] = &arr;//parr1是指针数组arr的指针
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    parr1就是指针数组arr的指针。它是一个数组指针,这个数组的元素也是指针。
    再看这个东西:
    int (*parr2[10])[1] = { parr1 };
    这个东西又是什么呢?
    int (*parr2[10])[1]拆开成int (* )[1]parr2[10],前者是一个数组指针类型,表示的是1元素的int型数组的数组指针;后者parr2[10]表示这是一个有10个元素的数组,而我们并不清楚它的数据类型。所以,前者是一个数据类型,后者是一个缺少数据类型的数组。这俩合起来就是一个数组,它的数组名是parr2,它有10个元素,它的元素类型是int (* )[1],这个类型表明数组元素都是数组指针。所以,int (*parr2[10])[1]就是一个数组指针数组


    数组指针解引用

    看下面的对比程序:

    #include
    int main()
    {
    	int arr[10] = { 10 };
    	int(*parr)[10] = &arr;
    	printf("%p\n", *parr);
    	int a = 10;
    	int* pa = &a;
    	printf("%d\n", *pa);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    我们发现,对整型变量的指针解引用,得到的是这个整型变量的值,但是对数组的指针解引用,得到的却不是数组的值,而是一个地址

    我们再做实验验证一下这个地址是什么

    #include
    int main()
    {
    	int arr[10] = { 10 };
    	int(*parr)[10] = &arr;
    	printf("%p\n", arr);//数组名是数组首元素的地址
    	printf("%p\n", *parr);
    	printf("%p\n", *parr + 1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    这下彻底证明了,对数组指针解引用,得到的是数组首元素的地址


    可以使用数组指针来操作二维数组的值:

    #include
    void print(int(*arr)[5], int r, int c)
    {
    	int i = 0;
    	int j = 0;
    	for (i = 0; i < r; i++)
    	{
    		for (j = 0; j < c; j++)
    		{
    			printf("%d ", *(*(arr + i) + j));
    		}
    		printf("\n");
    	}
    }
    int main()
    {
    	int arr[3][5] = { {1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5} };
    	print(arr, 3, 5);
    	//arr拿到的是二维数组首元素的地址,也就是一维数组{1,2,3,4,5}的地址
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    image.png
    在这里插入图片描述

    冷知识

    若:

    int arr = { 0 }; int* p = arr;

    则:

    arr[2] == 2[arr] == p[2]

    这样的写法都不会报错

    原理是数组名是首元素地址,数组定义式的操作数满足交换律
    (不太好理解,可以不记)


    数组中的传值调用和传址调用

    一维数组传参:

    #include
    void test(int arr[])
    {}
    void test(int arr[10])
    {}
    void test(int* arr)
    {}
    void test2(int* arr[])
    {}
    void test2(int** arr)
    {}
    int main()
    {
    	int arr[10] = { 0 };
    	int* arr2[20] = { 0 };
    	test(arr);
    	test2(arr2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这五种函数的形式参数是否ok?

    先看前三种,

    int arr[]int arr[10]功能相同,意思是建立了一个新的形式参数的数组,用来存放实际参数数组的值,所以是ok的。这时传递的是主函数中整个数组的值。这是一种传值调用

    arr是数组名,也是数组首元素的地址,所以用int* arr这个整形指针接收地址,也是ok的。这是一种传址调用

    再看后两种

    int* arr2[20]是一个整型指针数组,所以形式参数用int* arr[]这个整形指针数组形参接收是ok的。这是一种传值调用

    arr2又是整形指针数组的首元素(一个整型指针)的地址,指针的地址应该用二级指针接收,所以int**arr[]也是ok的。这是一种传址调用

    也就是说,这段程序中所列写的五种写法都是ok的


    二维数组传参:

    #include
    void test(int arr[3][5])
    {}
    void test(int arr[][])
    {}
    void test(int arr[][5])
    {}
    //— — — — — — — — — — — — — —
    void test(int* arr)
    {}
    void test(int* arr[5])
    {}
    void test(int(*arr)[5])
    {}
    void test(int** arr)
    {}
    int main()
    {
    	int arr[3][5] = { 0 };
    	test(arr);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    先看前三种,前三种的参数都写成了二维数组形参的格式,这是一种传值调用
    但是二维数组的形参,可以省略行,不能省略列,所以第二种全省略int arr[][]是错误的

    再看后四种,后四种都写成了指针的形式,说明把arr看成了首元素的地址,这是一种传址调用。而我们要知道,二维数组首元素是第一行这个一维数组,所以arr实际上是这个一维数组的地址,所以必须用数组指针接收,所以int* arr这个整型指针是不行的,int* arr[5]根本就是个指针数组,所以肯定也不行,只有第三种int(*arr)[5]才是正确写法的数组指针,所以是可以的。而最后一种二级指针的写法当然也是不行的,这只是一个一级指针


    指针与函数


    函数指针

    整型指针是指向整型的指针

    字符指针是指向字符的指针

    数组指针是指向数组的指针

    所以函数指针是指向函数的指针,是存放函数地址的指针

    没错,函数,同样有地址


    函数的地址

    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int main()
    {
    	printf("%p\n", Add);
    	printf("%p\n", &Add);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image.png
    所以我们得到两个结论:

    1. &函数名 - 取出函数的地址

    2. 函数名 - 也是函数的地址

    类比思考:
    虽然这种情况和数组名很像,但是它们不完全一样
    数组名 != &数组名(数组名是数组首元素的地址,&数组名是数组的地址,步长不同)
    函数名 == &函数名(这俩是完全相同的)


    函数指针类型

    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int main()
    {
    	int (*pf)(int, int) = &Add;
    	printf("%p", pf);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    pf就是函数指针,它的类型是int (* )(int, int)
    所以函数指针类型就是函数返回值类型 (*函数指针名) (形式参数类型)
    因此,无参数返回值为空类型的函数指针类型是void(*)()


    函数指针解引用

    看下面的样例程序

    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int main()
    {
    	int (*pf)(int, int) = &Add;
    	int ret = 0;
    	ret = Add(3, 5);
    	printf("%d\n", ret);
    	ret = pf(3, 5);
    	printf("%d\n", ret);
    	ret = (*pf)(3, 5);
    	printf("%d\n", ret);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image.png
    第一行int (*pf)(int, int) = &Add;我们把Add函数的地址传入函数指针pf
    第三行ret = Add(3, 5);就是调用Add函数把3和5相加,所以打印出8,没问题
    第五行ret = pf(3, 5);,这就有点费解了,为啥函数指针也能当函数使用呢?
    第七行ret = (*pf)(3, 5);,这就更费解了,为啥对函数指针解引用也可以当函数使?

    从上面的实验,我们得知了一个事实:

    函数名 / 这个函数的地址 / 存放这个函数地址的指针
    这仨是一个东西,它们即代表函数的地址,又代表函数的函数名

    由于对于函数来说,函数名==函数地址==函数指针,这就会出现一个现象:函数指针不管解引用多少次仍然是它自身

    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int main()
    {
    	int (*pf)(int, int) = &Add;//Add == pf
    	int ret = (*****************pf)(3, 5);
    	printf("%d\n", ret);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    注意:解引用函数指针当函数名使用时,一定要加括号


    函数指针案例一:嵌入式系统中调用0地址处函数

    (*((void(*)())0))();
    
    • 1
    • 详解:
      1.void(*)()是一个无参数返回值为空类型的函数指针类型
      2.(void(*)())0是把0强制类型转换成函数指针类型
      3.*((void(*)())0)是把这个值为0的函数地址解引用
      4.前面我们说过,对函数地址解引用*函数指针可以当成函数名使用。所以(*((void(*)())0))()就是调用0地址处的函数,我们不需要知道这个函数的函数名,就可以直接通过函数指针调用。

    这种情况一般是“我们正在编写一个独立运行于某个微处理器上的C程序(嵌入式程序),想要运行这个C程序需要在程序开始时调用硬件自带的一系列相关子例程(比如开机程序、烧写bootloader等等),而对于嵌入式系统,这些子例程一般都是写在0地址处,所以我们可以直接在我们的程序中加上这段代码,计算机就会直接调用0地址处的子例程。
    当然除了嵌入式系统,一般的计算机很少直接对常量地址直接调用,因为pc(台式计算机)的编译器会自动的排列地址,直接这样暴力的调用地址是有内存泄漏的风险的。

    函数指针案例二:返回值类型是函数指针类型的函数指针

    void(* signal(int,void(*)(int)))(int);
    
    • 1
    • 详解
      1.signal后面直接跟了(数据类型)所以signal是函数指针名,这个函数的第一个参数类型是整型int,第二个参数是一个函数指针类型void(*)(int)
      2.既然signal是函数名,那么一定有函数返回值类型。除了signal以外的东西就是它的函数返回类型,void(* )(int)就是函数的返回类型
      3.所以signal是一个返回类型是void(*)(int)的,参数是(int,void(*)(int))的函数指针

    下面说如何简化这个代码

    typedef void(*pfun_t)(int) //这种写法就是对void(*)(int)的函数指针类型重命名为pfun_t
    
    • 1

    所以上面的代码就可以简洁的写成下面的形式

    pfun_t signal(int,pfun_t)
    
    • 1

    函数指针数组

    函数指针数组是一个数组,它的成员都是函数指针

    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int Sub(int x, int y)
    {
    	return x - y;
    }
    int main()
    {
    	int (*pfarr[2])(int, int) = { Add,Sub };
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    pfarr就是函数指针数组的数组名,它的元素个数是2个,数组元素的数据类型是int(*)(int,int)


    利用函数指针数组实现简易计算器:

    #include
    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int Sub(int x, int y)
    {
    	return x - y;
    }
    int Mul(int x, int y)
    {
    	return x * y;
    }
    int Div(int x, int y)
    {
    	return x / y;
    }
    void menu()
    {
    	printf("*********************\n");
    	printf("** 1.add     2.sub **\n");
    	printf("** 3.mul     4.div **\n");
    	printf("**      0.exit     **\n");
    	printf("*********************\n");
    }
    int main()
    {
    	//计算器 - 计算整型变量的加、减、乘、除
    	int x = 0;
    	int y = 0;
    	int ret = 0;
    	int input = 0;
    	do {
    		system("cls");
    		menu();
    		printf("请选择:\n");
    		scanf("%d", &input);
    		if (input >= 1 && input <= 4)
    		{
    			int (*pfarr[])(int, int) = { NULL,Add,Sub,Mul,Div };
    			//我们经常把这样的一个数组叫做“转移表”
    			printf("请输入两个操作数:");
    			scanf("%d %d", &x, &y);
    			ret = (pfarr[input])(x, y);
    			printf("ret = %d\n", ret);
    		}
    		else if (input == 0)
    		{
    			printf("退出程序\n");
    		}
    		else
    		{
    			printf("选择错误\n");
    		}
    		Sleep(2000);
    	} while (input);
    	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
    • 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

    指向函数指针数组的指针

    指向函数指针数组的指针是函数指针数组的指针

    int(*p1)(int,int);//函数指针
    int(*p2[4])(int,int);//函数指针的数组
    int(*  [4])(int,int);//这是函数指针数组类型
    (*p3)//这是指针类型
    int(*(*p3)[4])(int,int) = &p2;//函数指针数组的指针
    
    • 1
    • 2
    • 3
    • 4
    • 5

    回调函数

    把一个函数的地址传递给另一个函数,这个另一个函数就是回调函数

    回调函数实际上是一种非常好的帮助我们解耦函数功能的方式

    如果是普通的函数嵌套调用,我们无法更改嵌套调用的函数,需要手动的更改函数名

    而回调函数通过传递函数指针的方法,让我们在嵌套函数的时候能够同时嵌套一众相同类型的函数

    常见的用法是“回调函数+switch

    通过建立Calc回调函数编写计算器:

    #include
    int Add(int x, int y)
    {
    	return x + y;
    }
    int Sub(int x, int y)
    {
    	return x - y;
    }
    int Mul(int x, int y)
    {
    	return x * y;
    }
    int Div(int x, int y)
    {
    	return x / y;
    }
    void Calc(int(*pf)(int, int))
    {
    	int x = 0;
    	int y = 0;
    	printf("请输入两个操作数:");
    	scanf("%d %d", &x, &y);
    	printf("结果是 %d\n\n", pf(x, y));
    	return;
    }
    int main()
    {
    	int x = 0;
    	int y = 0;
    	int input = 0;
    	int ret = 0;
    
    	do {
    		printf("请选择:1.加法 2.减法 3.乘法 4.除法 0.退出\n");
    		scanf("%d", &input);
    		switch (input)
    		{
    		case 0:
    			printf("程序已退出\n");
    		case 1:
    			Calc(Add); break;
    		case 2:
    			Calc(Sub); break;
    		case 3:
    			Calc(Mul); break;
    		case 4:
    			Calc(Div); break;
    		default:
    			printf("输入错误,请重新输入!\n");
    		}
    	} while (input);
    	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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 详解:
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    至此,指针部分讲解完毕。本文信息密度较大,可能让各位初学者烧脑了。
    当然,这只是指针部分的知识环节,要想做到熟练掌握,还是需要我们在项目或者算法题中不断的融会贯通。加油,兄弟。

  • 相关阅读:
    安装Frida工具
    TTS前端原理学习 chatgpt生成答案
    OpenCV-基于阴影勾勒的图纸清晰度增强算法
    mysql服务器数据同步
    EdgeX Foundry 边缘计算平台对Modbus设备状态检测和远程控制
    docker之常用指令
    计算机中的第二个伟大发明(JMP/JMPR)
    mini LED显示屏—点胶测量
    ARM——综合作业
    简易开发一个app
  • 原文地址:https://blog.csdn.net/qq_51379868/article/details/126582104