[分析用][ANSI C]有关于 C指针和数组 的稍深入讨论

来源:互联网 发布:浙江师范行知学院官网 编辑:程序博客网 时间:2024/06/05 04:59

===================================================================================================================================

[0]导言 关于C的数组和指针

===================================================================================================================================  

        在C语言的学习过程中,我曾听到不止一个老师对我说过,

“数组是一个指针常量”

“二维数组就是一个指针数组,所以你可以把n维数组直接赋值给n级指针,多维数组会附加大量的指针变量耗损内存”

        而实质上,这些都是基于直觉的想当然的想法,我想,这也是大多数C语言初学者的思维误区(如果你有几万行的代码量还这么想我建议你把每天睡午觉的时间匀出来看会儿《C专家编程》和《C指针与陷阱》)。

        在开始分析C的数组与指针之间的关系与区别之前,首先容我简单回顾下C的指针和数组这两种数据类型(或者说数据结构)的相关基础知识。        那么,首先请容许将C提供的数据类型分为三种:

  • 简单类型(内置的数值类型如int,double等)
  • 复合类型(用户自定义的如struct,union类型)
  • 复杂类型(前两种类型配合修饰符组成的新类型,指针和数组属于该种)。

        这里我们只讨论复杂类型,C提供的格式修饰符有三个:

  • () 代表函数
  • * 代表指针
  • [] 代表数组

        复杂类型由简单类型和以上修饰符组合而成,用于一些简单类型的变量不能或者难以实现的特殊功能。下面是一些复杂类型定义变量的例子(0-0):

int foo(); //这是一个函数的声明式,不是一种数据类型,仅作对比int (*fun_ptr)(); //这是一个指向返回值为int,参数列表为空的函数的函数指针int *ptr; //这是指向一个int类型的一个指针(一级指针)int **pptr; //这是一个关于int二级指针int array[ 10 ]; //这是一个int数组(一维数组)int array_2[ 10 ][ 5 ]; //这是一个int二维数组int* ptr_array[ 10 ]; //这是一个储存指向int类型的指针的数组int (*array_ptr)[ 5 ]; //这是一个指向数组的指针(即数组指针,之后会讨论)

        其中,指针是一种储存内存地址的数据类型,一般的理解为指向某个数据的数据类型;而数组则是用于储存一组相同类型的变量的数据类型,可以看做一个变量的集合。这里,指针之所以叫做复杂类型,就是指针实际储存了两个信息:指向的数据类型和指向变量所处的内存地址(其中void*是不包含类型信息的)。相比简单类型,指针和数组可以执行下标(value[n])和解引用(*value)操作,可以通过对指针和数组变量执行这两种操作取得指针所指内存的变量或者数组内部的元素。(0-1):

int value = 0;int* pointer = &value;int array[ 10 ] = { 0 };

        此处array就是一个储存了10个int类型变量的一个集合,如果我们使用array[n](n从0到9)则会获得array这个数组变量第n+1个变量(取得是左值(不知道什么是左值和右值得请忽略))。而pointer则是一个指向int变量的指针,*pointer则会取得指向的变量,等同于直接使用value。

        同时,指针和数组也可以执行简单的算数操作(与整数加减),比如pointer+1可以获取&value加上sizeof(int)的地址值。而对于array+2则是获取array的第三个元素(array本身作为右值是数组首元素的地址)。这些便可以说是数组和指针的基本知识,那么下面我们就开始讨论我们要弄清楚的是什么样的问题。


===================================================================================================================================

[1] 开场的一维数组和一级指针

===================================================================================================================================

        首先在上面我也讲到了,数组和指针都是复杂数据,但他们是两种完全不同的数据类型,几乎可以说完全没有关系。数组负责储存一组相同类型的变量,指针负责储存指定类型变量的内存地址,通过数组和指针我们都可以间接的访问到数据本身,但他们本质是不同,数组是一块复合的数据结构,你甚至可以把它理解成一个特殊结构体,数据成员就是一组相同类型的变量,且内存空间是连续的(为何这么说后面会解释);而指针则是一个储存内存地址的变量,本质还是一个变量。

        那么是什么造成我们拥有“一维数组就是一级指针常量”的概念的呢?

  1. 首先也是最重要的一点:一维数组可以不经过任何强制转换赋值给一级指针!只要你把一维数组赋值给一级指针,一级指针就获得了数组首元素的地址,且直接引用数组名时获取的也是数组首元素的地址!
  2. 其次,让我们看看前面提到的对指针和数组可以进行的操作:简单算术运算和解引用,下标操作;而对一维数组和一级指针进行这些操作是没有区别的!获得结果是相同的!

        那么我们用下面的代码来验证一下我的说法(1-1):

#include <stdio.h>int main(void){        int array[ 5 ] = { 2, 3, 3, 3, 3 };        int* pointer = array;        printf("Address:\r\narray:%p\r\npointer:%p\r\n\r\n", array, pointer);        printf("Dereference:\r\n*array:%d\r\n*pointer:%d\r\n\r\n", *array, *pointer);        printf("Element0:\r\narray[0]:%d\r\npointer[0]:%d\r\n\r\n", *array, *pointer);        return 0;} 

        以下是输出结果:


        进一步测试我们还可以得出一个结论:对于一个数组或者指针进行*(obj+n)和obj[n]操作,即算术运算后解引用和直接下标解引用的结果是完全一样,这个结论可以算是正确的。而且返回刚才的问题,不难看出,对于一级指针和一维数组执行以上操作得到结果是完全一致的,而且如果尝试直接给数组名赋一个地址值,编译器会报错(强制转换的你赢了)。那么似乎可以得出“一维数组就是一级指针常量”的结论,是吗?

        下面请再容我用图片解释一下数组和指针的本质:

        我想说的是,数组不是一个独立的指针值,而是一个整体。何以见得?我们对分别对一维指针和一级指针sizeof一下就知道了。(1-2)

printf("Size:\r\nsizeof(array):%d\r\nsizeof(pointer):%d\r\n\r\n", sizeof(array), sizeof(pointer)); 

        输出结果如下:

        可见sizeof操作对于数组会获取整个数组(不包含数组头储存地址的部分)占用内存空间的字节大小,而对于指针则只会返回指针的字节大小而已(我的操作系统是64-bit的,32-bit的操作系统上指针大小是4)。而一维数组之所以可以赋值给一级指针,也可以进行指针的解引用操作,那是因为直接引用数组名时会获得数组的首元素地址值,而地址值是可以赋值给指针,也可以解引用的。所以总结如下:一维数组的行为跟一级指针基本类似;但根据sizeof的返回值可看出一维数组是一个整体,一维数组和一级指针本质上是两种不同的数据类型。

        当然,或许仅仅一个sizeof的区别不足以令你信服,那么下面才是重点,让我们来讨论下,二维数组与二级指针间的恩爱情仇。


===================================================================================================================================

[2] 重点的二维数组和二级指针

===================================================================================================================================

        上面我们说到了,对于一个数组或者指针进行*(obj+n)和obj[n]操作,即算术运算后解引用和直接下标解引用的结果是完全一样,对于一级指针和一维数组执行以上操作得到结果也是完全一致的。这是由于一维数组的数组头(数组名,但不是数组本身)和一个一级指 针常量的行为相同,对于数组的下标操作的本质就是:

  • 以数组头的地址值为基础向后移动n乘以数组元素的大小的字节得到元素的地址值,再在该地址处取出元素。

        这么一来,貌似数组和指针的行为就一致了(除了sizeof操作)。是这样吗?先不急着定下结论,我们来看看二维数组和二级指针。

        实践强于雄辩,我们来实践看看二维数组和二级指针之间的区别。这里我们先来定义一个二维数组和一个二级指针:(2-1)

int array_2d[ 5 ][ 5 ] = { { 0 } };int **ptr_2c = array_2d;

        在给二级指针初始化时,编译器给了个warning:

        可能坚信二维数组还是二级指针的人会不解编译器的做法,我们初始化时加个强制转换让编译器安静,我们继续。现在ptr_2c已经指向array_2d的首元素地址了,按理说,这个二级指针的行为应该与二维数组一致了。那么我们用如下代码随机初始化二维数组的元素并分别用二维数组和二级指针的两次下标操作来取出元素输出试试看:(2-2)

srand(time(NULL));for (i = 0; i < 5; ++i){         for (j = 0; j < 5; ++j){                 array_2d[ i ][ j ] = rand();         }}for (i = 0; i < 5; ++i){        for (j = 0; j < 5; ++j){                 printf("array_2d[%d][%d] = %d\r\n", i, j, array_2d[ i ][ j ]);         }}puts("=======================================================");for (i = 0; i < 5; ++i){         for (j = 0; j < 5; ++j){                printf("ptr_2c[%d][%d] = %d\r\n", i, j, ptr_2c[ i ][ j ]);         }}puts("=======================================================");


        数组输出了结果,指针还没输出程序就崩溃了,是不是莫名其妙?

        在这里我们就不麻烦调试君出场了,我们直接输出中间变量看看对array_2d和ptr_2c执行下标操作得到的地址是否一致:(2-3)

for (i = 0; i < 5; ++i){        printf("&(array_2d[%d]) = %p; &(ptr_2c[%d]) = %p\r\n",i, array_2d + i, i, ptr_2c + i);}puts("=======================================================");for (i = 0; i < 5; ++i){        for (j = 0; j < 5; ++j){                printf("&(array_2d[%d][%d]) = %p; &(ptr_2c[%d][%d]) = %p\r\n", i, j, array_2d[ i ] + j, i, j, ptr_2c[ i ] + j);        }}


        仔细看一下输出结果,你会倒吸一口凉气。(注:为了方便对比,我这次用了32位的编译器,地址值只有4字节)“嗯?怎么跟说好的不一样?”,╮(╯_╰)╭没错,首先是一维,执行简单算术操作时发生了不得了的事,数组每次向后面移动了20字节,但是指针只移动了4字节!但是静下来想想,有什么不对?二维数组array_2d的元素是一个包含五个int变量的一维数组,sizeof(int)*5刚好等于20字节,地址的移动本来就应该以20字节为单位。而对于二级指针ptr_2c,它指向的变量类型是一个int的指针(一级指针),sizeof(int*)的值就是4字节(32-bit的情况下),那么对其进行简单算术操作,地址的移动也是应该以4字节为单位。

        但是这还不是她们之间唯一的区别,我们再来看下面逐个元素的地址,你可以很快发现二维数组的所有元素的地址是连续的!而且array_2d的一维没有存放任何地址值,而是各个一维数组元素的起始地址!而指针这边……有些混乱,元素的地址几乎是随机分布的。但仔细分析下就会觉得这不奇怪,ptr_2c是二级指针,那么对其进行一次解引用理所应当会返回地址所指的元素——一个一级指针,但是等等,ptr_2c[n]所存的是一级指针变量?我们刚才说了,二维数组的所有元素的地址都是连续的,那么实质上ptr_2c[n]存的还是一个int变量,而ptr_2c的解引用就把一个array_2d所存的一个int变量的int值解释成了32-bit地址值!而从前面的代码(2-2)可见,数组的元素是用rand()随机初始化的!所以这个随机int值自然就成了一个一级指针所存的内存地址值,在对其进行简单算术操作时,就会自然地向后移动sizeof(int)也就是4字节。实际测试验证下我的猜测(2-4):

for (i = 0; i < 5; ++i){       printf("*(int*)((int)array_2d + %d * %d) = %08X\r\n", i, sizeof(int*), *(int*)((int)array_2d + sizeof(int*) * i));        //此处强制转换是为了保证array_2d移动相同的字节数到达ptr_2c[n]       for (j = 0; j < 5; ++j){               printf("&(ptr_2c[%d][%d]) = %p\r\n",i, j, ptr_2c[ i ] + j);       }}

       结果如图所示。可见实际情况与我的猜测是一致的。

       不过到这里你可能有问题了,二维数组的地址是连续的,但是*解引用和[]下标操作对于数组和指针,不是会返回根据所存地址值处的元素吗?对啊,就是这样的啊,但是二维数组的元素是一维数组啊,我说过,“数组和指针都是复杂数据,但他们是两种完全不同的数据类型,”,二维数组解引用返回了一维数组(返回的是数组而不是指针,可以用sizeof(array_2d[0])验证,验证类似代码1-2),但是一维数组本身就是一段内存,所以二维数组就返回了数组头所存地址处开始的一段内存,有什么不对?回过头来,虽然二维数组行为与二级指针大有不同,但是二维数组也是一段连续内存,这么说的话,二维数组在地址底层的行为是类似一级指针的!老规矩,上代码测试(2-5):

#include  <stdio.h>#include  <stdlib.h>#include  <time.h>int main(void){       int array_2d[ 5 ][ 5 ] = { { 0 } };       int *ptr_1c = (int*)array_2d;       size_t i = 0, j = 0;       srand(time(NULL));       for (i = 0; i < 5; ++i){              for (j = 0; j < 5; ++j){                     array_2d[ i ][ j ] = rand();              }       }       for (i = 0; i < 5; ++i){              for (j = 0; j < 5; ++j){                     printf("array_2d[%d][%d] = %06d; *(ptr_1c + %d*5 + %d) = %06d\r\n", i, j, array_2d[i][j], i, j, *(ptr_1c + i*5 + j));              }       }       return 0;}

       所以二维数组的结构要用图解是的话,如下所示:

        所以不能理解二维数组与二维指针的关系的人的最大的误区就在于,把一维数组理解成了一级指针!再重申一次,数组和指针是完全不同的两种数据类型!数组的核心是一段连续内存,是一个完整的数据结构对象,严格说数组头存储的数组首元素地址不是数组的一部分,而正是这个数组头使得数组的行为与指针接近(因为他们都是用于存放地址值的)。但是sizeof操作直接忽略了数组内唯一的一个指针(地址值),而且每个n维(n大于等于1)数组变量只有一个数组头地址值(作为元素的数组是不会存自己的头地址值的),算术运算和下标操作就是根据头地址加上下标数乘以元素大小的字节,所以C不检查数组越界,因为这两个操作不会涉及数组大小。所以可以总结一下:

  • int **ptr_2c就是int *(*ptr_2c),也就是说二级指针是指向指针的指针,对于解引用会返回地址处的指针(或者说把地址处的存放的值解释成指针并返回);
  • 而int array_2d[5][5]就是int (array[5])[5]二维数组则是包含数组的数组,内存是连续的,对于解引用操作是返回地址处的数组元素,里面除了数组头没有任何指针变量(也就是地址值),可以说行为类似一级指针;解引用和下标访问对于指针和数组的操作过程是不同的,对于指针,操作恒根据指针所存地址值取得变量;
  • 而对于数组,则如果元素是数组就返回数组(不根据地址值寻值),是一般变量(一维数组的情况)则行为同一级指针(即根据地址值寻值)。

===================================================================================================================================

[3] 数组指针

===================================================================================================================================

        到现在我们知道了二级指针和二维数组两者指向或者储存的元素是不同的。那么问题来了(山东济南找蓝翔!),如果我确实有需求要用指针间接操作一个二维数组怎么办?这个时候我们可以定义一个数组指针,顾名思义,数组指针所指向不再是简单或则复合类型,而是一个数组。定义方式是type (*ptr)[length](type为任意类型,ptr为数组指针变量标示符,length为所指数组长度,下文命名方式解释类似,不再赘述)(具体实例见上文0-0代码)。为什么这么定义呢?或者说为何type *ptr_array[length]为什么得到的是指针数组呢?我们回到之前的基础知识的部分,C提供了如下修饰符给用户用以定义复杂类型:

  • () 代表函数
  • *代表指针
  • []代表数组

        这里有个问题就是修饰符某种意义上也是个操作符,既然是操作符,那么一组操作符之间当然是有优先度的,而编译器正是根据修饰符优先度自顶而下判断变量类型。其中[]()同优先度(没有函数数组这种类型,但是有函数指针),同时存在时依照左优先的原则,逐个判断类型,而*的优先度低于[]()。但是比照算术操作符,你可以用()强制编译器无视优先度先识别某个修饰符。这么讲比较抽象,还是用代码解释:(3-1)

int *ptr_array[ 5 ] = { NULL };//由于[]的优先度高于*,所以编译器先识别[]判断ptr_array是一个数组,然后识别*判断ptr_array是一个int指针数组int (*array_ptr)[ 5 ] = NULL;//这里用了括号()强制编译器先识别了*判断array_ptr是一个指针,再识别[]判断array_ptr是一个指向(元素个数为5,类型为int的)数组的指针int *ptr_fun();//编译器识别了(),判断这是一个(返回值为int,参数为空的)函数,由于没有函数体,所以是函数声明int (*fun_ptr)() = NULL; //这里也用了括号()强制编译器先识别了*判断fun_ptr是一个指针,再识别()判断fun_ptr是一个指向(返回值为int,参数为空的)函数的指针int (*wrong_fun_ptr_array)()[5];/*这是个错误的类型,编译器会这么判断:先识别*,判断这是一个指针,再识别()判断这是这是一个函数指针,但是在之后判断[]就成了指向返回一个数组的函数的指针,而函数是不能返回数组的,故出错*/int (*fun_ptr_array[5])() = { NULL };//编译器先识别[],再是*,再是(),所以这是一个包含5个指向(返回值为int,参数为空的)函数的指针的数组int (*fun_ptr_array_2d[5][5])() = { { NULL } };//几乎同上,但是这是个包含5个上面类型数组的二维数组int (*(*ptr_fun_ptr_array)[5])() = NULL; //编译器先识别*,再是[],然后是*,最后是(),所以……这是一个指向包含5个指向(返回值为int,参数为空的)函数的指针的数组的指针

        所以我们用type (*ptr)[length]就可以定义出来一个数组指针(你要是认真看完上面的注释我建议你先去倒立两分钟清醒一下),这个指针指向数组,那么我们将刚才指向二维数组的二级指针改为数组指针再来进行上诉测试,看看结果:(3-2)

#include <stdio.h>#include <stdlib.h>#include <time.h>int main(void){        int array_2d[ 5 ][ 5 ] = { { 0 } };        int (*array_ptr)[ 5 ] = array_2d;size_t i = 0, j = 0;        srand(time(NULL));        for (i = 0; i < 5; ++i){                for (j = 0; j < 5; ++j){                array_2d[ i ][ j ] = rand();                }        }        for (i = 0; i < 5; ++i){                for (j = 0; j < 5; ++j){                        printf("array_2d[%d][%d] = %6d; array_ptr[%d][%d] = %6d\r\n",i, j, array_2d[ i ][ j ], i, j, array_ptr[ i ][ j ]);                }        }        puts("===================================================");        for (i = 0; i < 5; ++i){                printf("&(array_2d[%d]) = %p; &(array_ptr[%d]) = %p\r\n",i, array_2d + i, i, array_ptr + i);        }        puts("===================================================");        for (i = 0; i < 5; ++i){                for (j = 0; j < 5; ++j){                        printf("&(array_2d[%d][%d]) = %p; &(array_ptr[%d][%d]) = %p\r\n",i, j, array_2d[ i ] + j, i, j, array_ptr[ i ] + j);                }        }        return 0;}


         可见,这时数组和指针的行为几乎完全一致了!让我们再返回最开始的问题:

“数组是一个指针常量”

“二维数组就是一个指针数组,所以你可以把n维数组直接赋值给n级指针,多维数组会附加大量的指针变量耗损内存” 

        现在回过头来,让我们来想想为何有这种想法,原来是因为解引用或者下标操作是基于存放地址值的数组头完成的,这是如果数组储存的数据类型和指针指向的数据类型完全相同时,数组的行为几乎(除了直接sizeof求大小)和指针一致,所以一维数组和一级指针的行为相同,数组指针和多维数组又行为相同;这才是谬论的根本来源,但是上面我们经过一系列对二维数组和二级指针的测试和分析可以看出,从本质上来讲,指针和数组还是两种完全不同的数据类型



===================================================================================================================================

[4] 总结

===================================================================================================================================

        然后我们终于可以总结一下我们分析的结果了:

  1. 首先数组是一种完整的数据结构类型,而指针只是一个储存地址值的数据类型,它们类型不同,所以包含数组的数组(二维数组)是不能直接用指向指针的指针(二级指针)间接操作的,要用指针间接操作二维数组就必须用指向数组的指针(数组指针)完成;
  2. 不只是一维数组与一级指针行为相近,而是所有数组(1-n维数组)的行为都是与一级指针行为相近的!
  3. 数组之所以行为与指针相近,是因为数组包含储存数组首地址的数组头,而数组的行为大多基于数组头完成,而指针也是储存地址值的,这边造成数组与指针行为相近,进一步造成“数组就是指针”的谬论和误解。


===================================================================================================================================

[5] 补充:关于动态多维数组以及函数传参时的数组行为

===================================================================================================================================

        我们聊完数组与指针的关系,这里我们顺便其他相关的东西。

        首先我们就谈谈多维动态数组的申请。基于“二维数组就是一个指针数组,所以你可以把n维数组直接赋值给n级指针,多维数组会附加大量的指针变量耗损内存”思想的人,在申请动态多维数组时,多半是这么做的(以一个5*5的int二维数组为例):

  1. 申请一个包含5个int指针的动态数组;
  2. 给每个动态申请得来的int指针在申请包含5个int的动态数组。

       实现如下:

#include <malloc.h>int main(void){       int** stupid_dynamic_array = NULL;       size_t i = 0;//申请空间        stupid_dynamic_array = malloc(sizeof(int*) * 5);       for (i = 0; i < 5; ++i){              stupid_dynamic_array[ i ] = malloc(sizeof(int) * 5);       }//释放        for (i = 0; i < 5; ++i){              free(stupid_dynamic_array[ i ]);       }       free(stupid_dynamic_array);       return 0;} 

       但经过我们刚才的讨论应该不难发现,这种错误而且大费周章的多维数组申请方式就是对指针和数组关系理解不深的一个恶果。而真正的动态多维数组行为应该与静态的多维数组相同,所以应该只用声明一个数组指针即可:

#includeint main(void){       int (*true_dynamic_array)[ 5 ] = NULL;       size_t i = 0;//申请空间       true_dynamic_array = malloc(sizeof(int*) * 5 * 5);//释放       free(true_dynamic_array);       return 0;}

        对比上面由于误解指针与数组关系造成的代码,不难发现真正的多维数组申请方式要比上述方式更节约空间(没有指针变量)和时间(不用多做一层寻址操作)成本!



0 0
原创粉丝点击