文档章节

C Primer Plus 第10章 数组和指针 10.7 指针和多维数组

idreamo
 idreamo
发布于 2016/08/06 07:30
字数 3474
阅读 47
收藏 0

指针和多维数组有什么关系?为什么我们需要知道它们之间的关系?函数是通过指针来处理多维数组的,因此在使用这样的函数之前,您需要更多的了解指针。假设有如下的声明:

int  zippo[4][2] ;  /*整数数组的数组*/

数组名zippo同时也是数组首元素的地址。在本例中,zippo的首元素本身又是包含两个int的数组,因此zippo也是包含两个int的数组的地址。下面从指针的属性进一步分析:

**因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]相同。另一方面,zippo[0]本身是包含两个整数的数组,因此zippo[0]的值同其首元素的地址&zippo[0][0]相同。简单地说,zippo[0]是一个整数大小对象的地址,而zippo是两个整数大小对象的地址。因为整数和两个整数组成的数组开始于同一个地址,因此zippo和zippo[0]具有相同的值 。

**对一个指针加1会对原来的数值加上一个对应类型大小的数值。在这方面zippo和zippo[0]是不一样的,zippo所指向对象的大小是两个int,而zippo[0]所指向对象的大小是一个int。因此zippo+1和zippo[0]+1的结果不同。

**对一个指针取值得到的是该指针所指向对象的数值。因为zippo[0]是其首元素zippo[0][0]的地址,所以*(zippo[0])代表存储在zippo[0][0]中的数值,即一个int数值。同样*zippo代表其首元素zippo[0]的值,但是zippo[0]本身就是一个int数的地址,即&zippo[0][0],因此*zippo是&zippo[0][0]。对这个表达式同时应用取值运算符将得到**zippo等价于*&zippo[0][0],后者简化后即为一个int数zippo[0][0]。简言之,zippo是地址的地址,需要两次取值才可以得到通常的数值。地址的地址或指针的指针是双重间接的典型例子。

显然,增加数组维度会增加指针的复杂度。

程序清单10.15

/*zippo1.c --有关zippo的信息*/
#include <stdio.h>
int main(void)
{
    int zippo[4][2]={{2,4},{6,8},{1,3},{5,7}};

    printf("zippo = %p,  zippo+1 = %p\n",
            zippo,       zippo+1);
    printf("zippo[0] = %p,  zippo[0]+1 = %p\n",
            zippo[0],       zippo[0]+1);
    printf("*zippo = %p,  *zippo+1 = %p\n",
            *zippo,       *zippo+1);
    printf("zippo[0][0] = %d\n",zippo[0][0]);
    printf("  *zippo[0] = %d\n",*zippo[0]);
    printf("    **zippo = %d\n",**zippo);
    printf("      zippo[2][1] = %d\n",zippo[2][1]);
    printf("*(*(zippo+2)+1)=%d\n",*(*(zippo+2)+1));
    return 0;
}

在一个系统上输出结果如下

zippo = 0022FF20,  zippo+1 = 0022FF28
zippo[0] = 0022FF20,  zippo[0]+1 = 0022FF24
*zippo = 0022FF20,  *zippo+1 = 0022FF24
zippo[0][0] = 2
  *zippo[0] = 2
    **zippo = 2
      zippo[2][1] = 3
*(*(zippo+2)+1)=3

输出显示出二维数组zippo的地址和一维数组zippo[0]的地址是相同的,均为相应的数组首元素的地址,它的值是和&zippo[0][0]相同的。

然而,差别也是有的,在我们系统上,int是4个字节长。前面我们讨论过,zippo[0]指向4字节长的数据对象,对zippo[0]加1导致它的值增加4。数组名zippo是包含两个int数的数组的地址,因此它指向8字节长的数据对象。所以,对zippo加1导致它的值增加8。

程序显示*zippo和zippo[0]是相同的,这点是正确的。另一方面,二维数组名必须再次取值才能取出数组中存储的数据

具体地:zippo[2][1]的等价指针符号表示为*(*(zippo+2)+1)。表10.2中分步建立了这个表达式:

分析*(*(zippo+2)+1)

zippo 第1个大小 为2个int的元素的地址
zippo+2 第3个大小为2个int的元素的地址
*(zippo+2) 第3个元素,即包含2个int值的数组,因此也是其第1个元素(int值)的地址
*(zippo+2) +1 包含2个Int值的数组的第2个元素(int值)的地址
*(*(zippo+2) +1) 数组第3行第2个int的值(zippo[2][1])

当您正好有一个指向二维数组的指针并需要取值时,最好不要使用指针符号,而应当使用形式简单的数组符号。

10.7.1  指向多维数组的指针

如何声明指向二维数组的指针变量pz?例如,在编写处理像zippo这样的数组的函数时,就会用到这类指针。指向int的指针可以胜任吗?不可以。这种指针只是和zippo[0]兼容。因为它们都指向一个单个的int值。但是zippo是其首元素的地址,而该首元素又是包含两个int值的数组。因此,pz必须指向一个包含两个int值的数组,而不是指向一个单个的int值。下面是正确的代码:

int (*pz) [2] ;  //pz指向一个包含2个int值的数组

该语句表明pz是指向包含2个int值的数组的指针。为什么使用圆括号?因为表达式中[]的优先级高于*。因此,如果我们这样声明:

int * pax[2] ;

那么首先[]与pax结合,表示pax是包含两个某种元素的数组。然后和*结合,表示pax是两个指针组成的数组。最后,用int来定义,表示pax是由两个指向int值的指针构成的数组。这种声明会创建两个指向单个Int值的指针。程序清单10.16显示了如何使用指向二维数组的指针。

程序清单10.16  zippo2.c

/*zippo2.c  --通过一个指针变量获取有关zippo的信息*/
#include <stdio.h>
int main(void)
{
    int zippo[4][2]={{2,4},{6,8},{1,3},{5,7}};
    int (*pz)[2];
    pz=zippo;

    printf(" pz = %p,  pz+1 = %p\n",
             pz,       pz+1);
    printf(" pz[0] = %p,pz[0]+1 = %p\n",
             pz[0],     pz[0]+1);
    printf("  *pz = %p,*pz+1 = %p\n",
               *pz,     *pz+1);
    printf("pz[0][0] = %d\n",pz[0][0]);
    printf("  *pz[0] = %d\n",*pz[0]);
    printf("    **pz = %d\n",**pz);
    printf("      pz[2][1] = %d\n",pz[2][1]);
    printf("*(*(pz+2)+1) = %d\n",*(*(pz+2)+1));
    return 0;
}

输出结果如下:

 pz = 0022FF1C,  pz+1 = 0022FF24
 pz[0] = 0022FF1C,pz[0]+1 = 0022FF20
  *pz = 0022FF1C,*pz+1 = 0022FF20
pz[0][0] = 2
  *pz[0] = 2
    **pz = 2
      pz[2][1] = 3
*(*(pz+2)+1) = 3

不同的计算机得到的结果可能有些差别,但是相互关系是一样的。尽管pz是一个指针,而不是数组名,仍然可以使用pz[2][1]这样的符号。

更一般地,要表示单个元素,可以使用数组符号或指针符号:并且在这两种表示中即可以使用数组名也可以使用指针:

zippo[m][n] == *(*(zippo+m)+n)

pz[m][n] == *(*(pz+m)+n)

10.7.2  指针兼容性

指针之间的赋值规则比数值类型的赋值更严格。例如,您可以不需要进行类型转换就直接把一个Int数值赋给一个double类型的变量。但对于指针来说这样的赋值是不允许的。

这些规则也适用于更复杂的类型。假设有如下声明:

int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **px;  //指针的指针

那么,有如下结论:

pt = &ar1[0][0];  //都指向 int
pt = ar1[0];      //都指向 int
pt = ar1;         //非法
pa = ar1;         //都指向int [3]
pa = ar2;         //非法
p2 = &pt          //都指向int *
*p2 = ar2[0]      //都指向int
p2 = ar2;         //非法

请注意,上面的非法赋值都包含着两个不指向同一类型的指针。例如,Pt指向一个int数值,但是ar1是指向由3个int值构成的数组。同样,pa指向由3个int值构成的数组,因此它与ar1的类型一致,但是和ar2的类型不一致,因为ar2指向由2个int值构成的数组。

后面的两个例子比较费解。变量p2是指向int的指针的指针,然而,ar2是指向由2个int值构成的数组的指针(简单一些说是指向int[2]的指针)。因此,p2和ar2的类型不同,不能把ar2的值赋给p2。但是*p2的类型是指向int的指针,所以它和ar2[0]是兼容的。前面讲过,ar2[0]是指向其首元素ar2[0][0]的指针,因此ar2[0]也是指向int的指针。

一般地,多重间接运算不容易理解。例如,考虑下面这段代码:

int *p1;
const int *p2;
const int **p2;
p1=p2;  //非法,把const指针赋给非const指针
p2=p1;  //合法,把非const指针赋给const指针
pp2=&p1;//非法,把非const指针赋给const指针

正如前面所提到的,把const指针赋给非const指针是错误的,因为您可能会使用新指针来改变const数据。但是把非const指针赋给const指针是允许的。这们的赋值有一个前提:只进行一层间接运算:

p2=p1;  //合法,把非const指针赋给const指针

在进行两层间接运算时,这样的赋值不再安全。如果允许这样赋值,可能会产生如下的问题:

const int **pp2;
int *p1;
const int n=13;
pp2=&p1;  //不允许,我们假设允许
*pp2=&n;  //合法,两者都是const,但同时会使p1指向n
*p1=10;  //合法,但这将改变const n的值

10.7.3  函数和多维数组

如果需要编写一个处理二维数组的函数,首先需要很好的理解指针以便正确声明函数的参数。在函数体内通常可以使用数组符号来避免使用指针 。

下面我们编写一个处理二维数组的函数,一种方法是把处理一维数组的函数应用到二维数组的每一行上,也就是如下所示这样处理:

int junk[3][4]={{2,4,5,8},{3,5,6,9},{12,10,8,6}};
int i,j;
int total=0;
for(i=0;i<3;i++)
  total+=sum(junk[i],4);  //junk[i]是一维数组

如果junk是二维数组,junk[i]就是一维数组,可以把它看作是二维数组的一行。函数sum()计算二维数组每行的和,然后由for循环把这些和加起来得到“总和”。

然而,使用这种方法得不到行列信息。要具有行列信息,需要恰当地声明形参变量以便于函数能够正确的传递数组。在本例中,数组junk是3行4列的int数组。如前面所讨论的,这表明junk是指向由4个int值构成的数组的指针。声明此类函数参量的方法如下所示:

void somefunction(int (*pt) [4]) ;

当且仅当pt是函数的形式参量时,也可以作如下的声明:

void somefunction(int pt[][4]) ;

注意到第一对方括号是空的。这个空的方括号表示pt是一个指针,这种变量的使用方法和junk一样。程序清单10.17中的例子就将使用上面两种声明的方法。注意清单中展示了原型语法的3种等价形式。

//array2d.c --处理二维数组的函数*/
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS],int rows);
void sum_cols(int [][COLS],int);      //可以省略名称
int sum2d(int (*ar)[COLS],int rows); //另一种语法形式
int main(void)
{
    int junk[ROWS][COLS]={
        {2,4,6,8},
        {3,5,7,9},
        {12,10,8,6}
    };

    sum_rows(junk,ROWS);
    sum_cols(junk,ROWS);
    printf("Sum of all elements = %d\n",sum2d(junk,ROWS));

    return 0;
}
void sum_rows(int ar[][COLS],int rows)
{
    int r ;
    int c ;
    int tot ;

    for (r=0;r<rows;r++)
    {
        tot=0;
        for(c=0;c<COLS;c++)
        tot+=ar[r][c];
    printf("row %d: sum = %d\n",r,tot);
    }
}
void sum_cols(int ar[][COLS],int rows)
{
    int r ;
    int c;
    int tot;

    for (c=0;c<COLS;c++)
    {
        tot=0;
        for (r=0;r<rows;r++)
            tot+=ar[r][c];
        printf("col %d: sum = %d\n",c,tot);
    }
}
int sum2d(int ar[][COLS],int rows)
{
    int r;
    int c;
    int tot=0;

    for(r=0;r<rows;r++)
    {
        for (c=0;c<COLS;c++)
            tot+=ar[r][c];
    }
    return tot;
}

这个函数可以在多种情况下工作,例如,如果把12作为行数传递给函数,则它可以处理12行4列的数组。这是因为rows是元素的数目;然而,每个元素都是一个数组,或者看作一行,rows也就可以看作是行数。

请注意ar的使用方式同mian()中junk的使用方式一样。这是因为ar和junk是同一类型,它们都是指向包含4个int值的数组的指针。

请注意下面的声明是不正确的:int sum2(int ar[][],int rows);  //错误的声明

回忆一下,编译器会把数组符号转换成指针符号。这就意味着,ar[1]会被转换成ar+1。编译器这样转换的时候,需要知道ar所指向对象的数据大小。下面的声明:

int sum2 (int ar[][4] , int rows ) ; //合法

就表示ar指向由4个Int值构成的数组,也就是16个字节长(本系统上)的对象,所以ar+1表示“在这个地址上加上16个字节大小”。如果是空括号,编译器将不能正确处理。

也可以像下面这样,在另一对方括号中填写大小,但编译器将忽略之:

int sum2 ( int ar [3][4] ,int rows ) ;  //合法,但3将被忽略

一般地,声明N维数组的指针时,除了最左边的方括号可以留空之外,其他都需要填写数值。

int sum4d (int ar [][12][20][30],int rows) ;

这是因为首方括号表示这是一个指针,而其他方括号描述的是所指向对象的数据类型。请参看下面的等效原型表示:

int sum4d ( int (*ar) [12][20][30] , int rows);  //ar是一个指针

此处ar指向一个12x20x30的int数组。

 

 

© 著作权归作者所有

共有 人打赏支持
idreamo
粉丝 14
博文 139
码字总数 224743
作品 0
青岛
产品经理
C Primer Plus 第10章 数组和指针 编程练习答案

1、修改程序清单10.7中的程序rain,使它不使用数组下标,而是使用指针进行计算(程序中仍然需要声明并初始化数组)。 2、编写一个程序,初始化一个double数组,然后把数组内容复制到另外两个...

idreamo
2016/08/14
160
0
C++ Primer 学习笔记(第三章:字符串、向量和数组)

C++ Primer 学习笔记(第三章:字符串、向量和数组) [TOC] 3.1 命名空间的声明 声明语句可以一行放多条。 位于头文件的代码,一般来说不应该使用声明。因为其内容会拷贝到每个使用该头文件的...

ShawnLue
2015/08/20
0
0
C Primer Plus 第10章 数组和指针 10.2 多维数组

例如,气象员要分析5年中每月的降水量数据,首先需要解决的问题是如何表示这些数据。可以使用60个变量。或者使用一个60个元素的数组,或者使用5个数组,每个数组12个元素。这些方法都比较笨拙...

idreamo
2016/07/23
26
0
C Primer Plus 第10章 数组和指针 10.9 复合文字

假设需要向带有一个int参量的函数传递一个值,您可以传递一个Int变量,也可以传递一个int常量,比如5。在C99标准出现之前,数组参数的情况是不同的:可以传递数组,但没有所谓的数组常量可供...

idreamo
2016/08/09
33
0
C++ primer第二次阅读学习笔记(第4章)

C++语言应尽量使用vector和迭代器类型,应避免使用低级的数组和指针,涉及良好的程序只有在强调速度时才在类实现的内部使用数组和指针。因此要向成为一个真正的C++程序员就要多使用vector和s...

长平狐
2012/10/08
59
0

没有更多内容

加载失败,请刷新页面

加载更多

WinDbg

参考来自:http://www.cnit.net.cn/?id=225 SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols ctrl + d to open dump_file Microsoft (R) Windows Debugger Version 6.12.0002.633......

xueyuse0012
27分钟前
2
0
OSChina 周五乱弹 —— 想不想把92年的萝莉退货

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @罗马的王:分享松澤由美的单曲《地球ぎ》 很久没看圣斗士星矢了 《地球ぎ》- 松澤由美 手机党少年们想听歌,请使劲儿戳(这里) @开源中国首...

小小编辑
54分钟前
10
1
springBoot条件配置

本篇介绍下,如何通过springboot的条件配置,控制Bean的创建 介绍下开发环境 JDK版本1.8 springboot版本是1.5.2 开发工具为 intellij idea(2018.2) 开发环境为 15款MacBook Pro 前言 很多时候,...

贺小五
今天
1
0
javascript source map 的使用

之前发现VS.NET会为压缩的js文添加一个与文件名同名的.map文件,一直没有搞懂他是用来做什么的,直接删除掉运行时浏览器又会报错,后来google了一直才真正搞懂了这个小小的map文件背后的巨大...

粒子数反转
昨天
1
0
谈谈如何学Linux和它在如今社会的影响

昨天,还在农耕脑力社会,今天已经人工智能技术、大数据、信息技术的科技社会了,高速开展并迅速浸透到当今科技社会的各个方面,Linux日益成为人们信息时代的到来,更加考验我们对信息的处理程...

linux-tao
昨天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部