上一篇文章中对多级指针做了简要分析,其实只有当指针与多维数组以及函数联合在一起使用时,麻烦才算真正到来。

零、数组与数组名

C语言中的数组的一般声明形式如下:

T arr_name[n]; /* T为类型,n为数组元素个数 */

内存布局角度来说,数组T arr_name[n]就是内存中连续的内存单元,每个内存单元的长度为sizeof(T),数组的起始内存单元地址为arr_name所在的内存地址, 同时也是数组第一个元素arr_name[0]的内存地址。

C语言数组的数组名(arr_name)有这样的特点:arr_name = &arr_name = *arr_name = 数组起始地址。见下面例子:

char a[5];

printf("a = %p\n", a);
printf("&a = %p\n", &a);
printf("*a = %p\n", *a);

输出结果:

a = 0xbfb146c0
&a = 0xbfb146c0
*a = 0xbfb146c0

C语言数组与指针有着紧密的联系。数组名本身的值就是数组的起始地址,有了地址,就有了指针存在的理由了。

1) 数组名可以被当作指针来用

    char a[5] = {1, 2, 3, 4, 5};
    printf("%d, %d, %d\n", *a, *(a+1), *(a+2)); // 输出1, 2, 3

   
    这种用法下,数组名相当于指向数组首地址的char*指针变量。

2) 数组名可以作为地址被赋值给兼容类型的指针变量
   
    char a[5] = {1, 2, 3, 4, 5};
    char *p = a;
    printf("%d, %d, %d\n", *p, *(p+1), *(p+2)); //输出1, 2, 3

3) 数组名不可以被当作指针变量来赋值

    char a[5] = {1, 2, 3, 4, 5};
    char b[5] = {6, 7, 8, 9, 0};

    a = b; //编译器提示错误:将‘char *’赋值给‘char[5]’时类型不兼容

    数组名与指针变量不同:指针变量有单独的存储空间,其存储空间内存储的是指向的内存单元的地址,但数组名只是个"代号"而已,其没有单独的存储空间,其所 在内存地址中存储的是数组第一个元素的元素值,而不是一个地址。或者说数组名代表的是一个值类型,char a[5]中的a可理解为是一个char[5]的值类型变量。将一个数组指针变量值赋值给一个值变量显然是不合逻辑的,也是非法的。

4) 考虑到效率,数组无法被按值传递给函数
   
    虽然数组名可以理解为一个值类型变量,但将数组名传递给函数时,传递的不是数组的全部,而只是数组的首地址,这显然是有效率方面考虑的。如果是传递数组的 全部,那碰到大数组时,这个mem copy的效率显然是不可接受的。但通过这个首地址,函数内部也是可以访问和修改数组中的所有元素的。
   
5) 函数形参中的数组变量将被转化为兼容类型指针变量对待

正如4)中所言,数组是以传址方式传入函数的。对于以数组变量作为形参的函数来说,在函数内部引用该参数时,会自动将该参数视为数组类型兼容的指 针变量,比如:
    char a[5] = {1, 2, 3, 4, 5};

    void foo(char a[5]) {
        printf("sizeof(a) = %d\n", sizeof(a));
    }

    这是一个经典的C语言“陷阱”。foo形参中变量a已经转化为一个char*类型指针了。对该指针变量进行sizeof操作,所得的 size仅是一个指针的长度(在32bit编译下是4),而不是a数组的长度(4 * 5)。

一、多维数组的理解

C语言中管数组的数组(的数组的…)称为多维数组,虽然高于二维的多维数组并不经常使用和遇见。

T multi_arr_name[i][j][k];

多维数组也是数组,根据数组的理解,多维数组也是内存中连续分配的内存单元,只是这些物理分配的内存单元被从逻辑上看成是“行”、“列”以及各种 维度罢了。《C专家编程》中有一种理解方法:将数组看成是一种向量,也就是某种对象的一维数组;当其元素为其他数组时,这个向量也就是我们所说的 多维数组。

我们来结合例子理解一下多维数组,从低维到高维度逐步理解:

1) 一维数组

char a[2];
这是一个向量,拥有两个元素,向量中的元素类型为char。可以理解为:

char a[2]; <=> (char) a[2];

2) 二维数组

char a[2][3];
这是一个向量,拥有两个元素,向量中的元素类型为char[3]。可以理解为:

char a[2][3]; <=> (char[3]) a[2];

3) 三维数组

char a[2][3][5];
这是一个向量,拥有两个元素,向量中的元素类型为char[3][5]。可以理解为:

char a[2][3][5]; <=> (char[3][5]) a[2];

4) N维数组

char a[i][j][k]…[z];
这是一个向量,拥有i个元素,向量中的元素类型为char[j][k]…[z]。可以理解为:

char a[i][j][k]…[z]; <=> (char [j][k]…[z]) a[i];

二、与数组类型兼容的指针类型

假设有下面这样一个数组:

char a[2][3];

我要声明一个可以指向该数组的指针变量,这个声明该如何书写呢?是 char *p[3]还是char (*p)[3]?按照上面对多维数组的理解:

char a[2][3]; <=> char[3] a[2];

这样我们只需构造出一个指向char[3]类型的指针即可,显然这样的指针声明是(char[3]) *p。哦,不对,这样的声明C编译器是不认的,乾坤大挪移!把(char[3])从中间劈开 => char *p[3],这样对么?这个是指向数组a的指针么?怎么越看越像是一个指针数组阿,char *p[3]<=> (char*) p[3]。哇,真的弄错了,改! 对了,刚才忘记了(char[3]) *p中还有一对括号呢,给*p穿上,=> char (*p)[3]。这回没错了,就是它了。

char a[2][3];
char (*p)[3];

p = a; /* 没有什么比这个还正确的了 */

再来一个三维数组的例子,这次简单直白点。

char a[2][3][5];

变形!=> (char[3][5]) a[2];
指针有了 => (char[3][5]) *p => char (*p)[3][5];

有了上面的例子分析,对于更高维度数组,你还不会声明其兼容的指针类型吗?

理解了多维数组兼容的指针变量的类型声明,那么将多维数组与函数结合在一起使用时,你就会得心应手了,在函数内部你看到的、能用到的就是多维数组 对应的兼容指针类型变量。

三、多维数组中的“隐式数组名”

在很多C语言书中,我们会经常看到这样的描述:对于多维数组char a[m][n][h],其中的某个元素a[i][j][k] <=> *(*(*(a + i) + j) + k)。这种等价形式是如何形成的呢?

第零小节的描述告诉我们:数组名是具有指针属性的,除了标准的下标引用方式外,还可以以指针的方式做指针运算以及访问元素,这就是 *(*(*(a + i) + j) + k)是合法的原因。

接下来我们来对*(*(*(a + i) + j) + k)做一次分解分析。鉴于一般形式不易理解和输出结果,我们用一个具体的例子来说明。

    char a[2][3][5] = {
        {
            {1, 2, 3, 4, 5},
            {6, 7, 8, 9, 10},
            {11, 12, 13, 14, 15},
        },

        {
            {21, 22, 23, 24, 25},
            {26, 27, 28, 29, 30},
            {31, 32, 33, 34, 35},
        }
    };

    char (*p)[3][5] = a;
    printf("a[1][2][3] = %d\n”, a[1][2][3]);
    printf("a addr = %p\n", a);
    printf("a + 1 = %p\n", a + 1);
    printf("*(a + 1) = %p\n", *(a + 1));
    printf("*(a + 1) + 2 = %p\n", *(a + 1) + 2);
    printf("*(*(a + 1) + 2) = %p\n", *(*(a + 1) + 2));
    printf("*(*(a + 1) + 2) + 3 = %p\n", *(*(a + 1) + 2) + 3);
    printf("*(*(*(a + 1) + 2) + 3) = %d\n", *(*(*(a + 1) + 2) + 3));

编译这个程序,执行输出:

a[1][2][3] = 34
a addr = 0xbfa0893e
a + 1 = 0xbfa0894d
*(a + 1) = 0xbfa0894d
*(a + 1) + 2 = 0xbfa08957
*(*(a + 1) + 2) = 0xbfa08957
*(*(a + 1) + 2) + 3 = 0xbfa0895a
*(*(*(a + 1) + 2) + 3) = 34

我们以*(*(*(a + 1) + 2) + 3)为例,再根据上面的输出结果,逐步拆解分析。

1) a + 1

a的等价指针类型是char (*p)[3][5]; 因此a + 1这个指针运算的结果相当于在数组a的起始地址开始向后移动sizeof(char [3][5])个字节。从输出结果来看,a + 1 = 0xbfa0894d = 0xbfa0893e + 15 = a addr +15也印证了这点。

2) *(a + 1)

通常指针的解引用操作会得到指针所指内存地址所在存储单元中存储的值。但上面的输出结果让我们产生疑问:

*(a + 1) = 0xbfa0894d == a + 1

在若干年前我的文章《挖掘一下C语言中的多维数组》中曾经探讨过这个问题,当时针对这个问题并未给出答案。这次对此问题我又有了新的认识。还记得我们在开篇中对数组名做的操作以及输出结果么:

char a[5];

a = 0xbfb146c0
&a = 0xbfb146c0
*a = 0xbfb146c0

也是a == *a。而这里同样是*(a + 1) == a + 1。通过这个对比我们得到一个大胆的推论:a + 1也可以看作是一个“数组名”,这是一个隐式数组名。只有这个解释看起来是合理的。

3) *(a + 1) + 2

a + 1这个隐式数组名对应的指针类型是char (*p)[5],因此 *(a+1) +2相当于从a + 1地址的开始再向后移动10(2 x 5)个字节,也就是0xbfa08957,输出结果也印证了这点。

4) *(*(a + 1) + 2)

我们又遇到了一个隐式数组名。*(*(a + 1) + 2) = 0xbfa08957 == *(a + 1) + 2。

5) *(*(a + 1) + 2) + 3

*(a + 1) + 2这个隐式数组名对应的指针类型是char *p,因此*(*(a + 1) + 2) + 3相当于从*(a + 1) + 2开始再向后移动3个字节,也就是0xbfa0895a,注意这个地址所在单元上存储的是一个char值。

6) *(*(*(a + 1) + 2) + 3)

如果将*(*(a + 1) + 2) + 3赋值给char *p,那么*(*(*(a + 1) + 2) + 3)就相当于*p,这个再简单不过了,34就是这个单元存储的char值。

© 2013, bigwhite. 版权所有.

Related posts:

  1. 简析多级指针解引用
  2. 也谈指针运算
  3. 也谈C语言变长参数
  4. 挖掘一下C语言中的多维数组
  5. 也谈字节序问题