C Primer Plus(6) 中文版 第10章 数组和指针 10.7 指针和多维数组

发布于:2023-01-10 ⋅ 阅读:(212) ⋅ 点赞:(0)

10.7 指针和多维数组 
int zippo[4][2]; /*内含int数组的数组*/
数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zip是这个内含两个int值的数组的地址。
*因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一个占据一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一地址,所以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]等价,这相当于zippo[0][0],即一个int类型的值。简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针就是双重间接(double indirection)的例子。
显然,增加数组维数会增加指针的复杂度。
/* zippo1.c --  zippo info */
#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[0]和*zippo完全相同,事实上确实如此。
使用两个间接运算符(*)或者使用两对方括号([])都能获得该值(这还可以使用一个*和一个[])。
要特别注意,与zippo[2][1]等价的指针表示法是*(*(zippo+2)+1)。看上去比较复杂,应最好能理解。下面列出了理解该表达式的思路:
zippo                   <---二维数组首元素的地址(每个元素都是内含两个int类型元素的一维数组)
zippo+2               <---二维数组的第3个元素(即一维数组)的地址
*(zippo+2)           <---二维数组的第3个元素(即一维数组)的首元素(一个int类型的值)地址
*(zippo+2)+1       <---二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)地址 
*(*(zippo+2)+1)   <---二维数组的第3个一维数组元素的第2个int类型元素的值,即数组的第3行第2列的值(zippo[2][1])
以上分析并不是为了说明用指针表示法代替数组表示法,而是提醒读者,如果程序恰巧使用一个指向二维数组的指针,而且要通过该
指针获取值时,最好用简单的数组表示法,而不是指针表示法。
图10.5以另一种试图演示了数组地址、数组内容和指针之间的关系。

 10.7.1 指向多维数组的指针
如何声明一个指针变量pz指向一个二维数组(如,zippo)?其声明如下:
int (*pz)[2]; //pz指向一个内含两个int类型值的数组
pz是一个指向一个数组的指针,该数组内含两个int类型值。
因为[]的优先级高于*,因此要在声明中使用圆括号。
int *pax[2]; //pax是一个内含两个指针元素的数组,每个元素都指向int的指针
指向二维数组的指针
/* zippo2.c --  zippo info via a pointer variable */
#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是一个指针,不是数组名,但是也可以使用pz[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:
zippo[m][n] == *(*(zippo + m) + n)
pz[m][n] = *(*(pz + m) + n)
10.7.2 指针的兼容性
指针之间赋值比数值类型之间的赋值要严格。不能在不同指针类型之间执行赋值操作。例如:
int n = 5;
double x;
int *p1 = &n;
double *pd = &x;
x = n; //隐式类型转换
pd = p1; //编译时错误
更复杂的类型也是如此。例如:
int *pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; //一个指向指针的指针
有如下的语句:
pt = &ar1[0][0];//都是指向int的指针
pt = ar1[0];     //都是指向int的指针
pt = ar1;         //无效,ar1是一个指向一维数组的指针
pa = ar1;        //都是指向内含3个int类型元素的指针 
pa = ar2;        //无效,ar2是一个指向一维数组的指针,但是一维数组中致函2的int类型的元素
p2 = &pt;        //都是指向int *的指针
*p2 = ar2[0];    //都是指向int的指针
p2 = ar2;        //无效
一般而言,多重解引用让人费解。例如:
int x = 20;
const int y = 23;
int *p1 = &x;
const int *p2 = &y;
const int **pp2;
p1 = p2; //不安全---把const指针赋给非const指针
p2 = p1; //有效---把非const指针赋给const指针
pp2 = &p1; //不安全---嵌套指针类型赋值
把const指针赋给非const指针不安全,因为这样可以使用新的指针改变const指针指向的数据。编译器在编写代码时,可能会发出警告,执行这样的代码是未定义的。但是把非const指针赋给const指针没有问题,前提是只进行一级解引用。
p2 = p1; //有效---把非const指针赋给const指针
但是进行两级解引用时,这样的赋值也不安全。例如:
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; //允许,但是这导致const限定符失效(根据第1行代码,不能通过**pp2修改它所指向的内容) 
*pp2 = &n; //有效,两者都声明为const,但是这将导致p1指向n(*pp2已被修改)
*p1 = 10;  //有效,但是这将改变n的值(但是根据第3行代码,不能修改n的值)
gcc和clang都给出指针类型不兼容的警告。当然,可以忽略这些警告,但是最好不要相信该程序运行的结果,这些结果都是未定义的。
C const和C++ const
C和C++中const的用法很相同,但是并不完全相同。区别之一是,C++允许声明数组大小时使用const整数,而C却不允许。区别之二是,C++的指针赋值检查更严格:
const int y;
const int *p2 = &y;
int *p1;
p1 = p2; //C++中不允许这样做,但是C可能只给出警告
C++不允许把const指针赋给非const指针。而C则允许这样做,但是如果通过p1更改y,其行为是未定义的。
10.7.3 函数和多维数组
处理二维数组的函数。一种方法是,利用for循环把处理一维数组的函数应用到二维数组的每一行。如下所示:
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]就是一维数组,可将其视为二维数组的一行。
然而,这种方法无法记录行和列的信息。用这种方法计算总和,行和列的信息并不重要。如果该函数要知道行和列的信息,可以通过声明正确类型的形参变量来完成,一般函数能正确地传递数组。可以这样声明函数的形参:
void somefunction( int (*pt)[4] );
另外,如果当且仅当pt是一个函数的形式参数时,可以这样声明:
void somefunction( int pt[][4] );
注意,第一个方括号是空的。空的方括号表明pt是一个指针。
演示了3种等价的原型语法
// array2d.c -- functions for 2d arrays
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int [][COLS], int );    // ok to omit names
int sum2d(int (*ar)[COLS], int rows); // another syntax
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;
}

/* 输出:

*/

列数内置在函数体中,但是行数靠函数传递得到。
注意,下面的声明不正确:
int sum2( int ar[][], int rows ); //错误的声明
编译器会把数组表示法转换为指针表示法。例如,编译器会把ar[1]转换成ar+1。编译器对ar+1求值,要知道ar所指向的对象的大小。
下面的声明:
int sum2( int ar[][4], int rows ); //有效声明
表示ar指向一个内含4个int类型值的数组,所以ar+1的意思是“该地址加上16字节”。如果第2对括号是空的,编译器就不知道该怎样处理。
也可以在第1对方括号中写上大小,如下所示,但是编译器会忽略该值:
int sum2( int ar[3][4], int rows ); //有效声明,但是3被忽略
与使用typedef相比,这种形式方便得多:
typedef int arr4[4];       //arr4是一个内含4个int的数组
typedef arr4 arr3x4[3]; //arr3x4是一个内含3个arr4的数组
int sum2( arr3x4 ar, int rows ); //与下面的声明相同
int sum2( ar[3][4], int rows ); //与下面的声明相同
int sum2( ar[3][4], int rows ); //标准形式
一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值:
int sum4d( int ar[][12][20][30], int rows ); 
因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。下面的声明与该声明等价:
int sum4d( int (*ar)[12][20][30], int rows ); //ar是一个指针
这里,ar指向一个12*20*30的int数组。