企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
**肆** ***数组与指针(二)*** **数组与指针的纠葛** ***以指针的形式访问数组:*** 下标表达式: 后缀表达式[表达式] 在C语言中,根据定义,**表达式e1[e2]准确地对应于表达式*((e1)+(e2))。**因此,要求表达式e1[e2]的其中一个操作数是指针,另一个操作数是整数。且这两个操作数的顺序可以颠倒。 故: a[4] 等同于 4[a] 等同于 *(a+4) 编译器把所有的e1[e2]表达式转换成*((e1)+(e2))。 所以,以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了! **多维数组** 二维数组a[i][j] **编译器总是将二维数组看成是一个一维数组,而一维数组的每个元素又都是一个数组。** 多维数组定义的下标从前到后可以看做是 最宏观的维到最微观的维。例:三维数组a[i][j][k] 可理解为 共有i个大组,每个大组里有j个小组,每个小组里有k个元素。 故: a 表示为整个三维数组,其值为&a[0][0][0], &a+1为整个三维数组后面的第一个位置。(偏移整个三维数组的长度) a+1  为第二个大组的首位置处(偏移一个大组的长度)【数组名a代表的是数组首元素的首地址,即:第一个大组的首地址】 a[0]表示为三维数组的i个大组中的第一个大组【可看做一个二维数组】,其值为&a[0][0][0], &a[0]+1为第二个大组的首位置处(偏移一个大组的长度) a[0]+1为第一个大组中第二个小组的首位置处(a[0]可看做是一个二维数组名,故其代表的是第一个小组的首地址)(偏移一个小组的长度) a[0][0]表示为第一个大组中的第一个小组【可看做一个一维数组】其值为&a[0][0][0], &a[0][0]+1为第一个大组中第二个小组的首位置处(偏移一个小组的长度) a[0][0]+1为第一个大组中第一个小组的第二个元素位置处(偏移一个元素的长度) a[0][0][0]表示为第一个大组中的第一个小组中的第一个元素。其值为&a[0][0][0],a[0][0][0]+1为首元素值加1。(因为a[0][0][0]为元素值而不是地址) 数组的数组(即:二维数组名)退化为数组的(常量)指针,而不是指针的指针。 同理,**n维数组名退化为n-1维数组的(常量)指针。** 【**总结:指针代表的是谁的首地址 就以谁的长度为偏移单位。**】 【**规律:与定义比较,缺少几对方括号,就是几维数组的数组名**,如上例:a缺少3对方括号,即为3维数组的数组名(代表的是2维数组的地址);a[0]缺少2对方括号,即为2维数组的数组名(代表的是1维数组的地址);a[0][0]缺少1对方括号,即为1维数组的数组名(代表的是数组元素的地址)】 【数组名与整数相加,首先要转换成数组的首元素地址与整数相加,而首元素的存储大小就是整数的单位】 对多维数组的解析: 我们可以用上面那种从前到后的解析方式来思考,a:就表示整个多维数组。a[m]:就表示第m+1大组(大组即数组最大的维),a[m][n]:就表示第m+1大组中的第n+1小组。(小组即次大的维),以此类推,即多维数组的解析是层层细化的。 **◎☆指针数组与数组指针:** 指针数组:首先它是一个数组。数组的元素都是指针。它是“存储指针的数组”的简称。 数组指针:首先它是一个指针。它指向一个数组。它是“指向数组的指针”的简称。 例:int * p1[10]; //它是指针数组。(因为[]的优先级比*高,p1先与[]结合,构成一个数组的定义) int  (*p2)[10] ; //它是数组指针。(括号的优先级较高,*与p2构成一个指针的定义) 它指向一个包含10个int型数据的数组。 若有:int(*p)[10][5] ;  //则p指向一个int型的二维数组a[10][5]。 【**规律:数组指针,把定义中括号内的指针看成是一个普通的字母,则其表示的就是 数组指针所指的对象类型**】 ◎☆ ~~~ int a[5][5] ; int (*p)[4] ; p=a ; 问:&p[4][2]-&a[4][2]的值为多少? ~~~ 设二维数组的首地址为0,则a[4][2]为第5组的第3个位置(以后见到多维数组要这么想,不要总想着是几排几列的模式),因为int a[5][5];即有5组,每组有5个元素。故:&a[4][2]是(4*5+2)*sizeof(int). int (*p)[4] ; 指针指向一个含4个int型的元素的数组,故p[4]相对于p[0]向后移动了“4个int型数组”的长度,然后在此基础上再向后移动2个int型的长度(即,其步长按维度逐步递减,多维数组也可按此方式理解)。最后其值为(4*4+2)* sizeof(int) 最后**切记:地址值参与的加减运算(地址不能被乘),整数的单位是地址值代表的元素的存储大小!** &p[4][2]-&a[4][2]结果为-4。若分开比较&p[4][2]和&a[4][2]则相差4* sizeof(int)个字节** 【**◎☆规律:数组指针的连续解引用** 数组指针的定义提供了其逐次解引用时的偏移单位,例int (*p)[m][n][k],则意为:数组指针的第一次解引用的偏移单位是m*n*k个int型长度,再次解引用的偏移单位是n*k个int型长度,又一次解引用的偏移单位是k个int型长度,最后一次解引用的偏移单位是1个int型长度。它只能连续解引用4次。 故:p[2][3][4][5]与四维数组首地址相距(2*m*n*k + 3*n*k + 4*k + 5 )个int型长度】 故:**数组指针指向的是哪个数组,就可以把它当做那个数组的数组名来用。** 例:inta[3][10][5] ; int (*p)[10][5] ; p = a ; 则:p[1][2][3] == a[1][2][3] ;  p[1][2] ==a[1][2] 即:用数组指针访问数组和用数组名访问,效果是相同的。 **WHY?**以int(*p)[10][5]为例,它指向一个[10][5]的二维数组,故第一次解引用时以二维数组[10][5]的长度作为偏移单位,一次解引用后p[1]就是一个[10][5]二维数组了。(解引用就是提取出指针偏移后 指向的对象) 即为:一维数组[5]的首地址。故再次解引用就以一维数组[5]的长度作为偏移单位,二次解引用后p[1][2]就是一个[5]一维数组了,即是一维数组首元素的地址。所以三次引用后,偏移单位为1个元素。 **数组参数与指针参数:** 1,二维数组名做实参 ~~~ int main(void) { int a[4][5] ; ………. ……… fun(a); ………. } 被调函数: ①fun( inta[4][5] ) ②fun( inta[ ][5] ) ③fun( int(*a)[5] ) { ………. a[i][j]=………. ……… } ~~~ 以上三种方式皆可。无论是那种方式,它们只是写法不同,但编译器的处理方式相同,都把它们看做是一维数组指针。 因为二维数组名退化为一个一维数组指针,故是以一维数组指针的形式来传递二维数组的。 2,指针数组做实参 ~~~ int main(void) { int a[4][5] , i, *p[4] ; for(i=0;i<4; i++) p[i]= a[i] ; ………. fun(p); ………. } 被调函数: ①fun(int*q[4]) ②fun(int *q[]) ③fun(int **q) { ………. q[i][j]=……….//取出指针数组中的第i个元素(为指针),再偏移j个单位 //也可从双重指针的角度理解:[i]为第一次解引用,偏移量是i个指针的大小(因为双重指针指向的是指针变量),[j]为第二次解引用,偏移量是j个int型变量大小(因为此时指针指向的是一个int型变量:某组的首元素) ……… } ~~~ 以上三种方式皆可。无论是那种方式,写法不同,但编译器的处理方式相同,都把它们看做是二级指针。 因为指针数组名退化为数组首元素的地址,即二级指针,故是以二级指针的形式来传递指针数组的。 而多维数组名退化为次维数组的指针,即数组指针,故是以数组指针的形式来传递多维数组的。 【数组指针的连续解引用,其指针的步长对应数组的维度值 是逐渐减小的 多级指针的连续解引用,其指针的步长 前几次解引用的步长为1个指针的长度,最后一次解引用的步长为最终指向的对象长度。(操作系统常用多级指针在多张表中做查询操作)】 【C中函数实参与形参之间是传值引用的,所以你要改变这个值,就传递它的地址(无需多言)】 **函数指针**: 函数指针就是函数的指针。它是一个指针,指向一个函数。 (即函数在内存中的起始位置地址) 实际上,所有的函数名在表达式和初始化中,总是隐式地退化为指针。 例:int  r , (*fp)( ) , func( ) ;       fp= func ;      //函数名退化为指针       r= (*fp)( ) ;  //等价于r=fp( ) ; **无论fp是函数名还是函数指针,都能正确工作。因为函数总是通过指针进行调用的!** 例:int  f(int) ; //函数声明       int  (*fp)(int) = &f  ;//此取地址符是可选的。编译器就把函数名当做函数的入口地址。 //在引用这个函数地址之前,f函数应先声明。       int  ans ;       //以下三种方式可调用函数       ans= f(25) ; //函数名后的括号是“函数调用操作符”。       ans= (*fp)(25) ;       ans= fp(25) ; **函数名就是一个函数指针常量,函数调用操作符(即一对括号)相当于解引用** 函数的执行过程: 函数名首先被转换为一个函数指针常量,该指针指定函数在内存中的位置。然后函数调用操作符调用该函数,执行开始于这个地址的代码。 **再说强制类型转换:** ~~~ void fun() { printf("Call fun "); } int main(void) { void(*p)( ) ; *(int*)&p = (int)fun ; (*p)() ; return0 ; } ~~~ 参见前面文章的强制类型转换。强制类型转换只不过是改变了编译器对位的解释方法罢了。 *(int *)&p = (int)fun ;中的fun是一个函数地址,被强制转换为int数字。左边的(int*)&p是把函数指针p转换为int型指针。*(int *)&p = (int)fun ;表示将函数的入口地址赋值给指针变量p。(*p)( ) ;表示对函数的调用。 **函数指针数组:** 即是存储函数指针的数组。(有时非常有用) 例:char *(*pf[3])(char *) ; **函数指针的用途:** 1,**转移表**(转移表就是一个函数指针数组) 即可用来实现“菜单驱动系统”。系统提示用户从菜单中选择一个选项,每个选项由不同的函数提供服务。 【若每个选项包含许多操作,用switch操作,会使程序变得很长,可读性差。这时可用转移表的方式】 例:void(*f[3])(int) = {function1, function2, function3} ; //定义一个转移表       (*f[choice])( ) ; //根据用户的选择来调用相应的函数 2,**回调函数**(用函数指针做形参,用户根据自己的环境写个简单的函数模块,传给回调函数,这样回调函数就能在不同的环境下运行了,**提高了模块的复用性**) 【回调函数实现与环境无关的核心操作,而把与环境有关的简单操作留给用户完成,在实际运行时回调函数通过函数指针调用用户的函数,这样其就能适应多种用户需求】 例:C库函数中的快速排序函数       voidqsort(void *base, int nelem, size_t width, int  (*fcmp)(void*, void*) ); //base为待排序的数组基址,nelem为数组中元素个数,width为元素的大小,fcmp为函数指针。 这样,由用户实现fcmp的比较功能(用户可根据需要,写整型值的比较、浮点值的比较,字符串的比较 等)这样qsort函数就能适应各种不同的类型值的排序。 **使用函数指针的好处在于:** 可以将实现同一功能的多个模块统一起来标识,这样一来更容易后期维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦合度以及使接口与实现分开。 **函数指针数组的指针:**(基本用不到) 例:char *(*(*pf)[3])(char *) 这个指针指向一个数组,这个数组里存储的都是指向函数的指针。它们指向的是一种返回值为字符指针,参数为字符指针的函数。 [对于这种复杂的声明,《C和指针》《C专家编程》中有专门的论述。我的方法就是:从核心到外层,层层分析。先找到这个声明的核心,看他的本质是什么。就像本例,最内层的括号里是一个指针,再看外层来确定它是个什么指针。外层是一个3个元素的数组,再看这个数组的元素类型是什么。是一个函数指针。 故总体来说此声明是一个函数指针数组的指针。] **复杂指针的举例:** int* (*a[5])(int, char*); void (*b[10]) (void (*)()); doube(*)() (*pa)[9]; 让我们一层一层剥开它的心。 第1个、首先找到核心,即标识符a,[ ] 优先级大于“*”,a与“[5]”先结合。所以a是一个数组,这个数组有5个元素,每一个元素都是一个指针。再往外层看:指针指向“(int,char*)”,对,指向一个函数,函数参数是“int, char*”,返回值是“int*”。完毕! 第2个、首先找到核心:b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是“void(*)()”【 这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”】 返回值是“void”。完毕! 第3个、核心pa是一个指针,指针指向一个数组,这个数组有9个元素。再往外层看:每一个元素都是“doube(*)()”【也即一个指针,指向一个函数,函数参数为空,返回值是“double”】 **使用typedef简化声明:** 某大牛对typedef用法做过一个总结:“建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”。 举例: 例1,void (*b[10]) (void (*)()); typedef void (*pfv)();                        //先把上式的后半部分用typedef换掉 typedef void (*pf_taking_pfv)(pfv);     //再把前半部分用typedef换掉 pf_taking_pfv b[10];                          //整个用typedef换掉 跟void (*b[10]) (void (*)());的效果一样! 例2,doube(*)() (*pa)[9];  typedef double(*PF)();         //先替换前半部分 typedef PF (*PA)[9];            //再替换后半部分 PA  pa;         //跟doube(*)() (*pa)[9];的效果一样! **反思:** 1,我们为什么需要指针? 因为我们要访问一个对象,我们要改变一个对象。要访问一个对象,必须先知道它在哪,也就是它在内存中的地址。地址就是指针值。 所以我们有 函数指针:某块函数代码的起始位置(地址) 指针的指针:因为我要访问(或改变)某个变量,只是这个变量是指针罢了 2,为什么要有指针类型? 因为我们访问的对象一般占据多个字节,而代表它们的地址值只是其中最低字节的地址,我们要完整的访问对象,必须知道它们总共占据了多少字节。而指针类型即向我们提供这样的信息。 注意:一个指针变量向我们提供了三种信息**:** ①一个首字节的地址值 ②这个指针的作用范围(步长) ③对这个范围中的数位的解释规则(解码规则) 【编译器就像一个以步数测量距离的盲人。故你要告诉它从哪开始走,走多少步。】 3,强制类型转换的真相? 学过汇编的人都知道,什么尼玛指针,什么char,int,double,什么数组指针,函数指针,指针的指针,在内存中都尼玛是一串二进制数罢了。**只是我们赋予了这些二进制数不同的含义,给它们设定一些不同的解释规则,让它们代表不同的事物。**(比如1000 0000 0000 0001 是内存中某4个字节中的内容,如果我们认为它是int型,则按int型的规则解释它为-231+ 1;如果我们认为它是unsigned int ,则被解释为231+ 1;当然我们也可把它解释为一个地址值,数组的地址,函数的地址,指针的地址等) 如果我们使用汇编编程,我们必须根据上下文需要,用大脑记住这个值当前的代表含义,当程序中有很多这样的值时,我们必须分别记清它们当前代表的含义。这样极易导致误用,所以编译器出现了,让它来帮我们记住这些值当前表示的含义。当我们想让某个值换一种解释的方案时,就用强制类型转换的方式来告诉编译器,编译器则修改解释它的规则,而内存中的二进制数位是不变的(涉及浮点型的强制转换除外,它们是舍掉一些位,保留一些位) 4,涉及浮点型的强制转 详情参见《深入理解计算机系统》 5,难点 多维数组、数组指针、多级指针。 **抓住问题的核心:指针值是谁的地址,这个地址代表的是哪个对象。** 搞清楚这个问题,关于指针移动时偏移量(步长)的计算就不会出错。 指针类型只是C语言提供的一种抽象,来帮助程序员避免寻址错误。