0


万字浅谈C语言指针

文章目录


前言

指针是C语言的一大特色,学会指针并且正确的使用这对日后的编程学习是大有裨益的。下面将会对C语言指针以及指针相关使用进行介绍。


一、指针是什么?

  1. 初学C语言的都知道,对于不同的类型的数据,都会用特定的数据类型去声明定义它。对不同数据的按照一定的意图进行合法操作处理就形成了一个程序。类比实现世界,工,农,兵,学,商这几类人组成了实现社会,并且在相应的岗位上各司其职维持整个社会的稳定运转。就像人是在社会这个大环境中生活,计算机的数据是在内存中存储着的。同时为了更加有效的利用内存,内存是被划分成了很多小的内存单元字节,每个字节单元都对应着一个地址编号,根据这个编号就能找到对应数据在内存的位置,进而操作处理这个数据。这个编号就好比实现生活中快递地址 ,快递小哥通过客户填写的地址派送商品。如果没有了地址了,如何在茫茫人海中找到客户并且把商品交给客户呢?总不能靠缘分吧!对于数据来说,它的数值可能会在操作处理过程中一直发生改变,但是它所占据的存储位置是不会改变的,就好像人会在 工农商学兵 这几个位置上不断变换角色,但是他们的身份证号码是不会改变的。因此指针数据类型便派上用场了。指针类型给数据处理带来了另一种可能。指针类型的变量,就是用来保存数据类型地址的。通过地址找到数据位置间接的操作它,而不是直接去操作它,这个好比你可以买张票去见异地恋的女友送她鲜花,也可以在美团下单将地址写成女友的。两种方式都达到了送花的目的。
    

写个代码示例
在这里插入图片描述

定义了一个整型数据n和整型指针变量p 先取出n的地址放进p中 通过解引用操作符
找到n的地址,并将该地址处的10间接改成12,于是n=12;现在n所在的地址处
放着12,然后将13直接赋值给n,n直接被改成了13.
上面两种方式都达到了修改n的值的目的
在上述代码中,对变量p进行定义声明时,int *这个符号表示的是变量p是整型指针类型的数据。不同的指针数据类型决定了指针变量的的步长。步长通俗将就是指针向前或者向后走一步有多大 

代码示例
在这里插入图片描述

在控制台中我们看以看到pc1 pc2两个指针变量中存放的内存地址编号,分别用整型打印和实际地址编号打印(%p是打印地址)其实无论以何种形式打印,pc1 pc1+1, pc2 pc2+1 之间的差值是一定的,因为输出的结果只是内存地址编号不同形式的表示。我们可以轻易算出pc1 和pc+1存放的内存编号之间是相差了4,pc2和pc2+1存放的地址编号之间查相差了1,pc1是int类型指针 pc2是char类型指针 int大小内存就是4字节 char 大小内存就是1字节,这就说明了指针变量的类型确定访问了地址的距离,这就是步长。一个内存编号对应一个字节大小的内存单元,假设char* p中存放的地址编号是0 那么p+4存放的地址编号就是4,因为char是一个字节大小,内存单元也是按一个字节划分的,如果把p假设是int* 那么p+4存放的地址编号就是16,因为int是4个字节大小。同时,指针相减就是两个指针之间相差的元素个数,pc1+1减去pc不就是1吗。他们之间确实相差一个元素,但是指针相减是相差的元素个数,这两个指针必须满足是相同类型的指针,而且有关联
说完了步长,接着说*解引用操作符也叫间接寻址运算符,当拿到某个变量的地址后,只需要用*就可以把这地址的变量进行修改。在使用指针变时,我们需要用& 取地址操作符将某个变量的地址取出来,那么*解引用操作符可以理解为&逆运算,&是取出地址,*是往地址里塞东西。 

说了这么这么多指针到底是什么呢?

以上是引入了指针数据类型的概念,但是指针的本质是地址,&取地址某个变量的地址,这个就可以称作指针。程序中的变量可能占有一个或者多个字节内存,把第一个字节的地址称为是变量的地址。
在这里插入图片描述
以i 为例,i在内存占据两个字节,i的地址就是1000.如果此时有个指针变量p存放i的地址,我们说p指向i。这就是指针。指针就是地址,指针变量是存储地址的变量。
在我们口头上经常说的指针其实指针变量。
以下文中提到指针也指的是指针变量。

二、指针数据类型的大小

1.我们知道指针变量里存放的是地址编号,编号也一种数据,既然指针变量存放的都是都是一组相同类型的数据,那么是不是所有类型的指针都是一样大呢?指针类型确实都是一样大,唯一需要注意的是在32位系统中,是4个字节大小,在64位中,则就为8个字节。

三.指针常见的使用

1.字符指针

代码示例:
在这里插入图片描述
我们可以看到将一个字面常量字符串str赋值给了字符指针s2 ,这里是把一个字符串放到s2中吗?实际上并不是的,指针变量是用来存放地址的,所以实际上s2里放的是字符串的首地址,通过这个首地址找到了字符串并打印出来。

再来看一段代码

在这里插入图片描述
我们知道字符串是直接无法比较大小的,那么这里是比较的什么呢?实际上这里的比较是地址,str1 str2是两个不同的变量,相同的常量字符串去初始化
不同的数组的时候就会开辟出不同的内存块,所以在内存中占据的位置也不一样,因此会输出not same;str3 str4里面保存的是hello bit 的首地址,但是hello bit是字面常量,C/C++会把常量字符串存储到单独的一个内存区域,当
几个指针在内存分配时,hello bit这个常量只有一个内存地址。str3 str 4里面保存的地址实际上是相同的,都是hello bit 常量的地址。
最后会输出 are same.
在这里插入图片描述

2.指针数组和数组指针

先简单说一下数组。数组(Array)就是一些列具有相同类型的数据的集合,这些数据在内存中依次挨着存放,彼此之间没有缝隙。这就是数组简单的定义。顺便提下数组名对于数组来说的含义,数组名其实是数组首元素的地址,但是有两个例外,就是取地址数组名&ArrayName,和sizeof(数组名),取地址数组名表示是取出整个数组的地址,sizeof(数组名)表示的是整个数组的大小,单位是字节。在C语言中数组的地址和数组首元素的地址在数值是其实是一样的,但是两种表示方式意义可是有很大的区别。
在这里插入图片描述

以图中的代码为例,arr是数组名,数组名是首元素的地址,用整型指针变量a1来存放数组首元素地址,&arr是取地址数组名表示的是取出整个数组的地址,所以此时的指针变量应该是一个指向有10个整型元素数组的指针,也就是数组指针,因此用a2来存放&arr,sizeof(arr)是求整个数组的大小,因此size_arr的大小肯定是40(字节),由于C语言中是用数组首元素地址表示的整个数组的地址,所以a1和a2存放的地址应该是一样的,但是a1是指向数组首元素地址,所以a1+1是指向数组第二个元素的,a2是指向整个数组的,所以a2+1是跳过整个数组的。我们看一下打印结果。

在这里插入图片描述
首先我们看到数组大小确实是40个字节,a1和a2存放的地址确实一样,a1+1和a1之间的差值就是4也就是一个整型的大小,a2和a2+1的差值是28 但是1地址是以16进制打印的,所以28转化成10进制就是8+2*16=40,也就是跳过了整个数组的大小。

回到指针数组,首先要搞清楚指针数组是数组,还是指针。毫无疑问,从语文角度来讲指针数组还是数组,实际上指针数组确实还是数组。指针数组,是用来存放指针的数组。简单声明一个指针数组 int * arr[5]这就是一个能够存放5个整型指针的数组,因为*结合性是低于[]的,所以int *arr[5]就是数组不是指针,这也符合指针数组的定义。有了前面的铺垫,那么数组指针就很好理解了,数组指针就是指向数组的指针。简单定义写成int (*arr)[5],arr先和*结合,说明arr是一个指针变量,然后指着指向的是一个大小为5个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。 

同时为了更好理解数组指针和指针数组,以下将介绍几种简单的代码示例
指针数组示例
在这里插入图片描述
a1这是一个指针数组,首先a1是数组,因为这是将arr放入a1中,所以a1只有1个元素,因此初始化的时候和普通数组是一样的。arr[0]访问了a1中第一个数组元素,但是数组元素是地址,所以拿到地址需要用*解引用操作符去访问对应地址的arr中的元素,因为数组中的元素不止一个,所以就得按首地址挨个访问。除了用 *解引用来访问,其实还可以[ ]来访问。

printf("%d ", (a1[i][j]));

再引入一个示例
在这里插入图片描述
如果想对多个一维数组进行管理,可以将一维数组的首元素地址放入指针数组a1中去,然后访问到对应一维数组首元素地址在进而通过数组首元素地址去挨个访问数组元素。这种方式类似于二维数组。

数组指针示例
在这里插入图片描述

数组指针,还是指针,是众多指针数据类型的一种,就如同整型指针是指向整型,数组指针是指向数组,a2是指向有3个整型元素的数组arr1的指针,因此a2是指针变量。通过a2解引用就可以访问到a2指向的地址,访问到地址后需要访问到对应数组地址的元素,也就是arr1地址中的元素,就需要再次解引用挨个访问数组元素。除了

printf("%d ", (*(*a2)+i));

写法外,其实还可以这样写

printf("%d ", (*a2)[i]);

其实不难理解,因为*和&是可以被看成逆运算的,a2=&arr1的,所以 * a2[i] = *&arr1[i] =arr1[i].

一维数组使用数组指针是不多见的,一般是用于二维数组
再引入一个示例

在这里插入图片描述

a2是指向二维数组的数组指针,但是为什么声明a2的时候不是int(*a)[2][3]呢?首先我们知道数组名的空间地址是连续的,二维数组也是如此。二维数组其实可以当作一维数组来看。

在这里插入图片描述
从图中可以看见数组a 中1 2 3后面接着排列的是 4 5 6,同时数组名既是数组首元素地址地址编号也是数组的地址编号,当取地址数组名时,指针a2实际指向二维数组首元素也就是二维数组第一行的地址,如果我们将二位数组的每一行当作是一维数组,a2+1指向的就是二维数组第二行,因为是把二维数组的每一行当作一维数组,每一行都是3个元素,所以a2的声明就是 int (*a2)[3],同时a2是指向第一行的地址,a2+1是指向第二行的地址,有了每一行的地址,只需要顺着每行地址再挨个访问每行的元素就行了,所以

printf("%d ", (*(*a2)+i)+j);

还可以写成

printf("%d ", a2[i][j]);

,其实我们再声明二维数组时,就会发先二维数组的行可以省略不写,但是列不能省略 类似于这样

int arr2[][3] = { 1,2,3 };

这是就是因为二维数组再空间排布上和一维数组类似,二维数组的每行都可以看出一个一维数组,只要确定了每列的元素个数,就确定了每行对应的一维数组的元素个数了。

数组传参

一维数组传参时,当函数实参时一维数组时,我们可以将函数形参设计成什么呢?首先,不管是什么数组,传参时都是数组名,也就是数组地址(数组首元素地址),对于一维数组来数,实参既然时一维数组,那形参也可以设计成一维数组,因为传参时,只是传的地址,并没有把整个数组传过去,所以形参可以写成与之对应的实参一维数组形式,也可以不写数组元元素个数,只表示形参是一维数组即可,举个简单例子,你可以把函数形参数组部分写做void test(int arr[ ]),也可以写做void test(int arr[5]),这都是可行的。除了这种传参方式还可以用整型指针来接收一维数组首元素的地址,因为根据数组首元素的地址可以挨个访问整个数组元素。

二维数组传参,当函数实参是二维数组时,与之对应的,我们可以将函数形参设计成二维数组,形参也可以省略数组的元素个数部分,就比如可以写成void test(int arr[ ] [ 5])这样,但是只能省略行,列是不能省略的。除了这种写法外还可以用数组指针来做为形参,用数组指针来访问二维数组的每一行,从而达到访问整个数组的目的。二维数组能用二级指针来接收吗,很明显不能,因为二级指针是存放一级指针变量地址的,那能用指针数组能接收吗,显然也不能,因为实参是地址,形参是数组,这完全是两种东西。

指针数组传参,用什么接收呢,指针数组就是一个数组,只不过数组元素都是指针,既然元素都是指针,那指针的地址肯定是用二级指针来接收。所以可以用二级指针来接收。除此之外,二级指针当函数参数可以接收二级指针实参和一级指针的地址。

3.函数指针

函数指针就是指向函数的指针,既然是指向函数的指针,那么该指针肯定就是用来保存函数地址,那么函数的地址这么表示呢?&取地址数组名就是数组的地址,那么&取地址函数名就是函数的地址,数组名是数组首元素的地址,但是函数不存在什么首元素之说,所以函数的地址就是函数名,&取地址函数名和函数名其实是等价的,都是函数的地址。知道了函数地址表示方法,但是函数指针类型怎么写呢?这个其实很简单 给出以下示例

在这里插入图片描述
对于p来说要想保证p是指针变量,所以p要先和*结合,因为p指向函数add,
add的形参是int 类型,返回值也是int类型,所以p前面的int表示是add函数的返回值,后面小圆括号两int表示的是add函数的形参部分。因此定义声明一个函数指针,需要表明该指针指向的函数的返回值类型和形参类型。当有了函数指针以后,怎么使用呢,只需要在该指针后面加上小圆括号填入对应的实参,然后对指针解引用即可,但函数指针就是存放的函数地址也就是函数名,那么不解引用像函数调用那样来使用函数指针可以吗?答案是可以的,所以上面的代码打印结果都是11.
在这里插入图片描述
知道了函数指针,那么来看看两段有趣的代码

//代码1(*(void(*)())0)();//代码2void(*signal(int,void(*)(int)))(int);

首先看看代码1,void(*p)()这很明显p是一个函数指针类型的指针变量,p指向的函数是返回值类型为viod,形参为空。当把p去掉以后就是void ( * )( ),这就是一种数据类型,数据类型加括号就是强制类型转换,类似于(int)3.5;将浮点数3.5转成整型。因为0是整型,所以是将0强制类型转化成函数指针类型,同时在对其解引用。后面单独的小圆括号其实就是通过函数指针来进行函数调用,因为函数参数是空,所以就是单独一个小圆括号。这段代码的含义就是将0转成void( * )( )类型的函数指针,同时在0地址处调用这个函数。同时解引用符号其实可以省略。这段代码出自于《c陷阱与缺陷》。

那接着看代码2,可以看到*没有和signal单独结合,所以signal肯定不是指针,把signal后面那一串拿出来,实际上就变成了,这样
在这里插入图片描述
那可以看出signal是函数,形参是整型类型和函数指针类型,但是唯独缺少了函数返回值,如果把图中最上面部分结合起来就是signal的函数返回值类型是函数指针类型。其实按照常理的写法应该是
void( * )(int) signal (int ,void( * )int),但是C语言的语法规定不能这样写,只能按照代码2的写法。所以代码2的含义就是一次函数的声明,函数名是signal,函数形参类型是整型和函数指针类型,函数返回值类型也是函数指针。对于这种繁琐的写法有没有简化的方法呢?其实可以用typedef来对类型进行重定义即可。在这里插入图片描述

函数指针数组

函数指针数组,本质还是数组,数组中的每个元素都是函数指针。
对于相同类型的函数指针可以将其放在函数指针中进行管理。
该怎么定义函数指针数组呢?举个简单的示例,数组arr中存放两个指向,函数返回值为整型,函数参数为整型的函数的函数指针,

int (*arr[ 2])(int ,int)

函数指针数组的应用

设计一个简单的整型计算器程序
大致思路
定义4个函数来实现加减乘除运算,同时可以利用循环实现函数的多次调用,使用者只是进循环一次进行选择,所以采用do while 循环,来实现。同时为了实现程序根据对应的选择做出不同的判断结果,循环内部用switch来处理不同的输入选择。同时可以设置简单的菜单进行简单的提示。

基于以上的思路,便有了如下的代码

#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>intadd(int a,int b){return a + b;}intsub(int a,int b){return a - b;}intmul(int a,int b){return a * b;}intdiv(int a,int b){return a / b;}voidmenu(){printf("*********************\n");printf("*****请选择**********\n");printf("****1.add   2.sub****\n");printf("****3.mul   4.div****\n");printf("********** 0.end*******\n");}intmain(){int input =0;int x =0;int y =0;int ret =0;do{menu();scanf("%d",&input);switch(input){case1:printf("请输入计算的两个数,并用空格隔开\n");scanf("%d %d",&x,&y);int ret =add(x, y);printf("%d\n", ret);break;case2:printf("请输入计算的两个数,并用空格隔开\n");scanf("%d %d",&x,&y);int ret =sub(x, y);printf("%d\n", ret);break;case3:printf("请输入计算的两个数,并用空格隔开\n");scanf("%d %d",&x,&y);int ret3 =mul(x, y);printf("%d\n", ret);break;case4:printf("请输入计算的两个数,并用空格隔开\n");scanf("%d %d",&x,&y);int ret
             =div(x, y);printf("%d\n", ret);break;case0:break;default:printf("输入错误,请重新输入\n");}}while(input);return0;}
虽然代码是写出来了,但是回过头来观察一下代码。在整个switch语句中实现函数调用时有着很多逻辑相似的代码。这样的代码太冗余了,有没有什么方法可以让代码高内聚,低耦合一点呢?

答案是可以的,首先可以发现实现加减乘除的函数的返回值类型和形参类型都是整型。所以可以将函数的地址放入一个函数指针数组,通过这个数组调用函数。那么以下代码将被改写如下形式。

intmain(){int input =0;int x =0;int y =0;int ret =0;int(*pa[5])(int,int)={0,add,sub,mul,div };do{menu();scanf("%d",&input);if(input ==0){break;}elseif(input >=1&& input <=4){printf("请输入两个整数并用空格隔开\n");scanf("%d %d",&x,&y);
            ret = pa[input](x, y);printf("%d\n", ret);}elseprintf("输入错误,请重新输入\n");}while(input);return0;}

可以看到,switc语句全部都被删除了,我们通过函数指针数组来实现函数的调用。为了使菜单中的选择与相应的函数对应,所以函数指针数组中放入5个元素,并且将0放入数组中,0放入数组中也不会有任何影响,只是占个位置而已,根本不会用到该数组中的0。同时用if语句进行相应的判断。这样使得代码的耦合度有所降低。

回调函数

函数指针数组应用说完了,接着就是函数指针的应用了。关于函数指针应用不得不提到回调函数。那什么是回调函数呢?

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。

以库函数中的qorst函数为例简单介绍一下回调函数,qorst函数是C语言中的数组排序函数,该函数是基于快速排序实现的。如果不了解该函数的,可以去官网https://cplusplus.com/查询。
在这里插入图片描述
在这里插入图片描述
qsort函数是可以对任意类型的元素排序,我们可以看到该函数是有4个参数,第一个参数是接收数组地址的,第二个是参数是接收数组元素个数的,第三个参数是接收元素大小的(单位字节),第4个参数是函数指针,这个函数指针指向的函数是使用者自己定义实现的,这个需要自己实现的函数是用来比较元素大小的,同时已经规定好了这个比较函数的返回值类型和参数类型,同时也说明了返回值的意义。p1如果大于p2就返回大于0的数,反之就返回小于0的数,如果两个数相等就返回0.
说了这么多那具体怎么做呢?看如下代码示例

#include<stdio.h>#include<assert.h>//对应引入头文件intcompare(constvoid* p1,constvoid* p2){return*(int*)p1-*(int*)p2;}intmain(){int arr[10]={3,4,5,2,7,8,6,0,9,1};int(*pa)(constvoid*,constvoid*)=compare;qsort(arr,10,sizeof(arr[0]), pa);for(int i =0; i <10; i++){printf("%d ", arr[i]);}return0;}

首先在使用qsort函数时必须得引入对应得头文件。关于比较函数的设计,我是参照官方文档给出的示例设计对应的形参和返回值。void*指针虽然可以存放任何类型元素的地址,但是在使用时必须得强制类型转化成你想使用的数据类型,不能直接用。关于比大小我直接让函数返回两个元素相减的值即可。上述代码的打印结果如下
在这里插入图片描述
确实是实现了数组元素排序。由于qsort有一个参数是函数指针,通过这个指针来调用比较函数,这就是回调函数的实际应用。

模拟实现qsort函数

我们可以试着模拟实现一个qsort函数,来加深理解。这里为了简单实现,我采用冒泡排序来实现qsort函数的排序功能。函数参数类型还是和qsort函数保持一致,但是大家思考过为什么qsort函数参数类型为什么这样设计呢?这个问题,在我们实现的过程中会一步步解答。

首先模拟实现psort函数的排序算法确定好了就是冒泡排序,我们以整型类型排序为例看看冒泡排序具体实现。
int arr[10]={3,4,5,2,7,8,6,0,9,1};int num =szieof(arr)/ arr[0];for(int i =0; i < num-1; i++){for(int j =0; j < num -1- i; j++){if(arr[j]> arr[j +1]){int tem = arr[j];
                arr[j]= arr[j +1];
                arr[j +1]= tem;}}}
我们知道冒泡排序外层循环控制冒泡趟数,内层循环是比较元素大小并交换元素位置。因为qsort函数是可以比较任何类型的元素的,所以在接收不同类型的数组地址时应该采用void*来接收地址,因为函数是不知道传入的数组地址是什么类型的,void*可以接收任何类型的数组地址,同时两个for循环是肯定对任何类型的元素都是固定的,都要控制趟数和比较大小交换位置,因此需要传入数组元素个数来控制循环,但是关于比较大小不能简单的用大于或者小于符号来判断了,传入的元素类型是不确定的,所以需要自定义比较函数,同时通过函数指针灵活的调用这个函数。为了能比较任何类型的元素,这个函数指针指向的函数的形参肯定也需要用定义成void* ,用来对任意元素进行比较。但是因为接收数组地址的形参是void*,但是void*l类型的指针是不能参与运算的,包括加减。因此必须将其强制类型转化,然后通过偏移量来找到对应元素的地址,但是数组元素类型不确定,所以强制类型转化成什么类型呢?其实,我们知道一个字节对应一个地址,char类型大小刚好是一个字节,我们将接收数组地址的形参强制类型转成char* ,然后将控制内层循环的j乘以数组元素大小,刚好就是对应地址的偏移量。其实不难理解,我们看到冒泡排序以整型数组为例时,直接用arr[j],arr[j+1]比较大小。但是现在引入函数指针传的是地址,同时void*接收的数组地址,&arr+j和&arr+j+1这种方式行不通。我们用采用char*按每个字节进行挨个进行偏移找到对应地址,确保了对任何类型数据都是有效的。我们最后一步就是关于元素交换了,因为元素类型是不确定的,所以引入第三个变量来实现元素的交换是不可取的。所以需要设计一个交换函数来实现对元素的交换。同时为了接收任意类型数据,我们采用void*来接收地址,接收了地址还要进行交换,但是同样是void*,不能运算,只能强制类型转换,为了确保对任意类型都能交换,我们还是采用上述的方法将对应地址强制类型转化成char*,在解引用,同时一个字节的一个字节的进行交换。

基于以上思路,写出如下代码

intcompare(constvoid* p1,constvoid* p2){return*(int*)p1 -*(int*)p2;}voidmy_swap(constvoid* p1,constvoid* p2,int szie_num){for(int i =0; i < szie_num; i++){char tem =*((char*)p1+i);*((char*)p1+i)=*((char*)p2+i);*((char*)p2+i)= tem;}}voidmy_qsort(constvoid* base,int num,int szie_num,int(*pa)(constvoid*,constvoid*)){for(int i =0; i < num-1; i++){for(int j =0; j < num -1- i; j++){if(pa((char*)base +j*szie_num,(char*)base+(j+1)*szie_num)>0){my_swap((char*)base + j * szie_num,(char*)base +(j +1)* szie_num,szie_num);}}}}

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过上面的层层分析,知道了qsort函数的形参类型为什么这样设计,主要就是能对任意类型的元素进行排序。

四.指针相关习题练习

1.一维数组

int a[]={1,2,3,4};1.printf("%d\n",sizeof(a));2.printf("%d\n",sizeof(a+0));3.printf("%d\n",sizeof(*a));4.printf("%d\n",sizeof(a+1));5.printf("%d\n",sizeof(a[1]));6.printf("%d\n",sizeof(&a));7.printf("%d\n",sizeof(*&a));8.printf("%d\n",sizeof(&a+1));9.printf("%d\n",sizeof(&a[0]));10.printf("%d\n",sizeof(&a[0]+1));

首先分析一下1.中sizeof单独放的是数组名所以1打印结果就是整个数组的大小,也就是16.
2中放的是数组名但是加了0,那就是数组首元素地址加0,还是数组首元素地址,所以2中计算的是地址的大小,那就是4或者8,
3中是对数组名解引用,数组名是数组首元素地址,所以对它解引用就是数组第一个元素,因此3中计算的大小就是4
4中是首元素地址加1,那还是地址,所以4中是地址大小,4或者8
5中数组元素,所以5中也是4
6中取的数组地址,本质还是地址,所以6中打印的也是4或8
7中解引用和取地址是可以抵消的,那就是相当于放的是数组名,那计算就是数组的大小,也就是16
8中是地址加1,那还是地址,所以是4或者8
9中是取地址,那还是地址,所以是4或者8
10中也是取地址加1,那还是地址,也就是4或者8

2.字符数组

char arr[]={'a','b','c','d','e','f'};1.printf("%d\n",sizeof(arr));2.printf("%d\n",sizeof(arr +0));3.printf("%d\n",sizeof(*arr));4.printf("%d\n",sizeof(arr[1]));5.printf("%d\n",sizeof(&arr));6.printf("%d\n",sizeof(&arr +1));7.printf("%d\n",sizeof(&arr[0]+1));8.printf("%d\n",strlen(arr));9.printf("%d\n",strlen(arr +0));10.printf("%d\n",strlen(*arr));11.printf("%d\n",strlen(arr[1]));12.printf("%d\n",strlen(&arr));13.printf("%d\n",strlen(&arr +1));14.printf("%d\n",strlen(&arr[0]+1));

简单分析一下,1中单独放的是数组名,所以1中打印结果是数组的大小,就是6
2中是数组名加0,就是首元素地址加0,那就还是地址,所以是4或者8
3中数组名解引用也就是对数组首元素地址解引用就是数组首元素,所以3中大小是1
4中是数组第二个元素,所以就是1.
5中是取地址数组名,本质还是地址,所以打印结果是4或者8.
6中是数组地址加1,还是地址,所以打印结果是4或者8.
7中是取地址加1,还是地址,所以打印结果是4或者8.
8是求字符串长度,但是整个数组中没有\0,所以打印结果是随机值。
9是数组首元素地址加0,还是数组首元素地址,因为没有\0,所以还是随机值而且随机值还和8一样。
10是数组首元素地址解引用就是数组首元素,也就是字符a,字符a会转成对应的ASCII值97,也就是会传入97所在的地址,这个地址是不能所以访问的这样会造成程序崩溃。
11和10中的例子一样,传入字符b对应ASCII值98的地址,造成程序崩溃。
12是取数组地址,数组的地址和数组首元素地址是一样的,而且数组里没有\0,所以也是随机值,而且随机值和8中的例子是一样的。
13取数组地址加1跳过整个数组,指向的是紧接着数组元素f后面的地址,也是随机值,并且和12的随机值相差6.
14是首元素地址加1,指向第二个数组元素的地址,也是随机值。

char arr[]="abcdef";1.printf("%d\n",sizeof(arr));2.printf("%d\n",sizeof(arr +0));3.printf("%d\n",sizeof(*arr));4.printf("%d\n",sizeof(arr[1]));5.printf("%d\n",sizeof(&arr));6.printf("%d\n",sizeof(&arr +1));7.printf("%d\n",sizeof(&arr[0]+1));8.printf("%d\n",strlen(arr));9.printf("%d\n",strlen(arr +0));10.printf("%d\n",strlen(*arr));11.printf("%d\n",strlen(arr[1]));12.printf("%d\n",strlen(&arr));13.printf("%d\n",strlen(&arr +1));14.printf("%d\n",strlen(&arr[0]+1));

简单分析一下,1中算的是数组大小,单位是字节,\0也会被计算在内也就是7
2是首元素地址加0,还是数组首元素地址,计算的是地址大小,所以是4或者8.
3是对首元素地址解引用就是首元素,算的是首元素的大小,也就是1字节
4是数组第二个元素,还是1
5取数组地址,计算的是地址大小,所以是4或者8.
6取数组地址加1,还是地址,所以是4或者8
7取数组首元素地址加1,还是地址,所以是4或者8
8是计算字符串长度,也就是6
9是首元素地址加0还是首元素地址,所以计算的还是6.
10是对数组首元素地址解引用也就是数组首元素,首元素会转换成对应ASCII值,然后将这个值的地址传入strlen,也就是97对应的地址,会造成程序崩溃
11是和10一样的,只不过是第二个数组元素对应ASCII值地址,造成程序崩溃。
12.取地址数组名和数组名都是同一个地址,所以计算的是字符串长度,还是6.
13. 取地址数组名加1跳过整个数组的地址,所以是随机值,这个随机值大小和和例8相差6.
14.数组首元素地址加1,是指向第二个数组元素地址,计算的是从第二个元素起字符串的长度,也就是5.

char* p ="abcdef";1.printf("%d\n",sizeof(p));2.printf("%d\n",sizeof(p +1));3.printf("%d\n",sizeof(*p));4.printf("%d\n",sizeof(p[0]));5.printf("%d\n",sizeof(&p));6.printf("%d\n",sizeof(&p +1));7.printf("%d\n",sizeof(&p[0]+1));8.printf("%d\n",strlen(p));9.printf("%d\n",strlen(p +1));10.printf("%d\n",strlen(*p));11.printf("%d\n",strlen(p[0]));12.printf("%d\n",strlen(&p));13.printf("%d\n",strlen(&p +1));14.printf("%d\n",strlen(&p[0]+1));

简单分析一下,1中放的是指针变量,指针变量的大小都是4或者8.
2中还是指针变量,所以结果是4或者8
3.是对指针解引用,p是指向常量字符串abcdef的首地址,所以也就是指向a,因此算的是a的大小也就是1.
4是等价于*(p+0)所以算的还是a的大小,还是1.
5是取地址p是对指针变量取地址,还是地址,所以是4或者8
6是对取地址加1还是地址,所以是4或者8
7是相当于取a的地址加1,还是取地址,所以是4或者8.
8.是求常量字符串的长度,所以是6
9是p+1地址处求字符串长度,也就是从字符串第二个字符起求字符串长度们就是5.
10.就是把第一个字符a转成ASCII值处的地址,也就是97处的地址,会造成程序崩溃。
11和10是等价的
12是对p取地址,通过这个地址可以找到p,p中放的是地址编号,地址编号以16进制表示,编号中可能可能会有0.所以是随机值。
13是对p取地址加1,这个地址位置也是随机的,不知道这个地址到底放着什么,打印结果也是随机值
14.取首元素地址加1就是第二个字符的地址,实际上就是计算从b起字符串的长度,也就是5.

二维数组

int a[3][4]={0};1.printf("%d\n",sizeof(a));2.printf("%d\n",sizeof(a[0][0]));3.printf("%d\n",sizeof(a[0]));4.printf("%d\n",sizeof(a[0]+1));5.printf("%d\n",sizeof(*(a[0]+1)));6.printf("%d\n",sizeof(a +1));7.printf("%d\n",sizeof(*(a +1)));8.printf("%d\n",sizeof(&a[0]+1));9.printf("%d\n",sizeof(*(&a[0]+1)));10.printf("%d\n",sizeof(*a));11.printf("%d\n",sizeof(a[3]));

简单分析一下,1中放的是数组名,所以计算的是二维数组的大小,也就是48
2中是第一行第一列的元素,所以计算的是元素大小,也就是4.
3中是a[0],a[0]是什么东西呢?我们知道如果想访问二维数组的第一行的元素,就是a[ 0 ][ j ] 访问第二行的元素就是a[ 1 ][ j ],二维数组是可以把每一行的元素可以当作一维数组来看的,实际上a[0],就是二维数组第一行首元素地址,第一行首元素的地址,类比一维数组,一维数组的首元素地址单独放在sizeof里是求整个数组的大小,第一行首元素地址单独放在sizeof中不就是求整个第一行的大小,也就是16.
4中并不是单独放入的a[0],类似于一维数组如果不是对a[0]取地址,就表示的不是第一行的地址而是第一行首元素的地址,这可以类比一维数组。a[0]+1是跳过一个int* 也就是指向第一行的第二个元素,本质还是地址。所以打印结果是4或者8.
5是对4解引用,也就是说5中放的是a[0][1],计算的是元素大小,也就是4
6是数组名加1,也就是数组首元素地址加1,二维数组的首元素是谁呢?首先我们把二维数组当作一维数组来看时,它的每一行就相当于一个元素,有3行,就有3个元素,那么首元素的地址就是第一行的地址,a+1就是第二个数组元素的地址,也就是第二行的地址,既然是地址也就是4或者8
7是对6解引用,6是指向第二行的地址,第二行的地址解引用就是a[1],也就是计算第二行的大小,也就是16
8.是&a[0]+1,&a[ 0 ]地址就是第一行首元素的地址,加一之后指向的是数组第一行第二个元素的地址,既然是地址那就是8或者4.
9是对8解引用,8是指向数组第二行地址, * ( &a[0] + 1)其实价于 *(a+0+1),也就是相当于a[1],所以就是求整个第二行的大小。
10.对数组名解引用就是等价于a[0],所以和例3一样计算的是整个第一行的大小,也就是16
11.求数组整个第四行的大小,但是a只有3行,这样会不会造成数组越界访问,其实sizeof中的表达式是不参与计算的,也就是没有运算效果,一个表达式,是有两个属性的,值属性和类型属性,sizeof是根据数据类型来计算大小,里面的值不会影响结果,同时里面的表达式式也没有操作效果。所以实际上这样写虽然是不符合规范的,但是不会造成数组越界。因为a数组前3行都是4个整型元素,哪怕是不存在第四行,但是还是沿用a[2]的数据类型,所以结果也是16.

指针笔试题

intmain(){int a[5]={1,2,3,4,5};int*ptr =(int*)(&a +1);printf("%d,%d",*(a +1),*(ptr -1));return0;}//程序的结果是什么

简单分析一下,取地址数组名表示的是取出整个数组的地址,想要保存整个数组的地址,应该用数组指针变量来保存,但是用强制类型转化,将其转转成了整型指针类型,那我们应该搞清楚ptr到底指向哪。首先取地址数组名+1,实际上是跳过整个数组,既然跳过了整个数组,那么ptr指向的就应该是数组最后一个元素往后一个整型的大小的位置,那么 ptr-1刚好就是5的位置,在对其解引用就是5.
数组名是首元素地址,首元素地址加1是访问第二个数组元素,就是2
打印结果就是2,5

structTest{int Num;char*pcName;short sDate;char cha[2];short sBa[4];}*p;//假设p 的值为0x100000。 如下表表达式的值分别为多少?//已知,结构体Test类型的变量大小是20个字节intmain(){ 
p =(structTest*)0x100000;printf("%p\n", p +0x1);printf("%p\n",(unsignedlong)p +0x1);printf("%p\n",(unsignedint*)p +0x1);return0;}

简单分析一下,0x表示的是16进制数,p+0x1和p+1是差不多的,因为p中放的地址是16进制形式的,所以用0x。前面提到过指针加减整数是指向向前或者向后的地址,向前一步走或者向后一步走的每步距离多大是取决于指针变量的数据类型的,p是开始时struct Test*类型的,也给出了struct Test的大小是20字节的,所以加1就跳过20个字节,是以地址(16进制)为打印结果的,所以在原来的地址编号上在加上16就行了,16转为10进制就是20.
我们看到第二个打印p被转成了无符号整型,也就是说p现在是整型。整型加1就是加1,也就是说p中放的地址编号加1.
第三个打印是p被转成了无符号整型指针,也就是说加1是跳过一个无符号整型大小个字节,所以p中的地址编号加4

intmain(){int a[4]={1,2,3,4};int*ptr1 =(int*)(&a +1);int*ptr2 =(int*)((int)a +1);printf("%x,%x", ptr1[-1],*ptr2);return0;}

简单分析一下,%x是16进制打印,第一个打印结果和例1类似,&a+1是跳过整个数组的指向的是4紧接着后面的地址,-1在对其解引用就是对4访问,打印结果就是4,第二个打印首先将a(首元素地址)强制类型转化,也就是a转成了整型,整型加1就是加1,现在a对应的地址编号就是在原来的基础上加了1,在将其转化成整型指针,实际上就是在指向首元素地址的基础上往后偏移了一个字节。整型在计算机中都是以补码存储的,存储就会涉及存储顺序的问题,就是字节大小端。我们知道数组地址都是从低到高的,不同的机器采用的存储顺序是不同的,以vs x86环境为例,采用的是小端存储。就是低地址放低位,高地址放高位。
在这里插入图片描述

#include<stdio.h>intmain(){int a[3][2]={(0,1),(2,3),(4,5)};int*p;
p = a[0];printf("%d", p[0]);return0;}

简单分析一下,可以看到数组中的元素是用小圆括号包起来了,并没有用花括号。所以数组中的是逗号表达式,a中实际上是放的1 ,3,5,并没有完全初始化,在之前的例题中有说过a[0]是第一行首元素的地址,也就是a[0][0]的地址,所以对p解引用就是a[0][0],打印结果就是1.

intmain(){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]);return0;}

简单分析一下,p是数组指针,指向有4个元素的一维数组,a是二维数组,但是把每行看做是一维数组,也就是说a是5个一维数组,每个一维数组都是有着5个元素。你也可以说a是有着5个元素的一维数组,每个元素都是a的每一行。现在把a给了p, p指向的是就是数组首元素地址,其实就是a[0][0]的地址,那么&p[4][2]是指向哪里得呢?

在这里插入图片描述

前面提到过指针相减是结果是相差的元素个数,其实地址相减不就是指针相减,因为指针里放的就是地址。两个地址之间相差4个元素。但是大的地址放在后面,小的地址放在前面,所以结果是-4,第一个打印结果以地址打印就是打印的-4的十六进制。第二个就是-4

intmain(){int aa[2][5]={1,2,3,4,5,6,7,8,9,10};int* ptr1 =(int*)(&aa +1);int* ptr2 =(int*)(*(aa +1));printf("%d,%d",*(ptr1 -1),*(ptr2 -1));return0;}

简单分析一下,取地址数组名是取整个数组的地址,加1后跳过整个数组,patr1-1现在指向的就是数组的末尾,就是10,所以第一个打印结果就是10,二维数组数组名,就是数组首元素地址,把二维数组当作一维数组,每个元素都代表二维数组的每一行,所以aa是第一行的地址,aa+1是第二行的地址,单独的每行就是一维数组,一维数组的地址是有数组首元素的地址表示的,所以第二行的地址就是数组第二行首元素的地址,就是6的地址,将6的地址传给ptr2,指针加减整型已经说了好几次了,就指向往后一个整型大小的地址1,就是指向5,在对其解引用就是5,打印结果就是5.

#include<stdio.h>intmain(){char*a[]={"work","at","alibaba"};char**pa = a;
pa++;printf("%s\n",*pa);return0;}

简单分析一下,a是指针数组,a中放的是3个字符串的地址。pa是二级指针,因为二级指针就是存放指针变量的地址,也就是存放地址的变量的地址。pa指向数组首元素地址,pa+1指向数组第二个元素的地址,数组第二个元素就是at的地址,对pa+1解引用就可以访问到at的地址,也就是说打印的结果就是at.

nt main(){char*c[]={"ENTER","NEW","POINT","FIRST"};char**cp[]={c+3,c+2,c+1,c};char***cpp = cp;printf("%s\n",**++cpp);printf("%s\n",*--*++cpp+3);printf("%s\n",*cpp[-2]+3);printf("%s\n", cpp[-1][-1]+1);return0;}

这题看着有些复杂,为了更好的梳理变量之间的关系,可以用画图的方式来处理

在这里插入图片描述
在这里插入图片描述

处理这种问题时要理清楚指向哪里的地址,解引用拿到是什么,运算过程中那哪一步改变了哪些值,哪些值是没有改变的。 将位置一 一对应起来。

五.总结

1.指针加减整数,指针相减,是穿插在题目中,没有单独拿出来。指针是C语言的精华,知识点很多,短短一篇文章是囊括不完的,在平时编程中需要多练习,多总结。本人能力有限,也不是写地面面俱到。
2,数组首元素地址和数组地址,虽然它们对应的地址编号是相同的,但是&取地址数组名和数组名所表示的意义可是完全不同的。这点是需要注意的,数组名是表示首元素地址,sizeof(数组名)是求整个数组的大小,&数组名表示取出整个数组的地址。
3.二维数组是可以当作一维数组的,将二维数组当作一维数组,它的每行看作一个元素。a如果是二维数组,a[0]就是第一行元素的首地址,a+1是整个第二行的地址。
4.sizeof只是计算数据大小,里面的式子是没有运算效果的。
5.数组和指针传参以及回调函数等,需要去实践练习。
6. 以上内容仅仅是我浅薄的看法,如有错误,欢迎指正!

标签: c语言 学习

本文转载自: https://blog.csdn.net/m0_61894055/article/details/126744192
版权归原作者 宗介@bit 所有, 如有侵权,请联系我们删除。

“万字浅谈C语言指针”的评论:

还没有评论