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


    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
    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
    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
    #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数组。 

  • 相关阅读:
    Sora - 探索AI视频模型的无限可能
    安徽首届道医传承十八绝技发布会在合肥成功举办
    【小沐学前端】Node.js实现基于Protobuf协议的WebSocket通信
    树的直径&
    【ICCV】PointDC,基于深度聚类的无监督3D场景语义分割,FaceChain团队联合出品
    直流充电桩测试仪的作用
    退火算法研究分析
    PyQt5学习一(环境搭建)
    系列文章|云原生时代下微服务架构进阶之路 - Spring Cloud
    前端设计模式
  • 原文地址:https://blog.csdn.net/weixin_40186813/article/details/126253253