• C专家编程 第9章 再论数组 9.6 C语言的多维数组


    C语言的多维数组
    ANSI C标准表示:
    当几个“[]”修饰符连续出现时(方括号里面是数组的范围),就是定义一个多维数组。

    但所有其他语言都把这称为“数组的数组”
    哪些人的意思是C语言没有像其他语言那样的多维数组。

    在不同的语言中,“多维数组”的含义各有什么不同
    Ada语言标准明确说明数组的数组和多维数组是不一样的。
    Pascal语言标准明确说明数组的数组和多维数组是一样的。
    C语言里面只有一种别的语言称为数组的数组的形式,但C语言称它为多维数组。

    C语言的方法多少有点独特:定义和引用多维数组的唯一方式就是使用数组的数组。尽管C语言把数组的数组当做是多维数组,但不能把几个下标范围如[i][j][k]合并成Pascal式的下标表达式风格如[i,j,k]。如果你清楚地明白自己在做什么,也介意产生不合规范的程序,可以把[i][j][k]这样的下标值计算为相应的偏移量,然后只用一个单一的下标[z]来引用数组。当然,这不是一种值得推荐的做法。同样糟糕的是,像[i,j,k]这样的下标形式(由逗号分隔)是C语言合法的表达形式,只是它并非同时引用这几个下标(它实际上所引用的下标值是k,也是就逗号表达式的值)。C语言支持其他语言中一般称作“数组的数组”的内容,但却称它为多维数组。

    在C语言中,可以像下面这样声明一个10*20的多维字符数组:
    char carrot[10][20];
    或者声明一种看上去更像“数组的数组”形式:
    typedef char vegetable[20];
    vegetable carrot[10];
    不论那种情况,访问单个字符都是通过carrot[i][j]的形式,
    编译器在编译时会把它解析为*(*(carrot + i) + j)的形式。

    但C语言实际上只支持“数组的数组”。如果你的思维模式可以把数组看做是一种向量(即某种对象的一维数组,它的元素可以是另一个数组),就能极大简化编程语言中的这个相当复杂的领域。

    C语言中的数组就是一维数组
    当提到C语言中的数组时,就把它看做是一种向量(vector),也就是某种对象的一维数组,数组的元素可以是另一个数组。

    如何分解多维数组
    我们声明如下的多维数组:
    int apricot[2][3][5];
    int (*p)[3][5] = apricot;
    int (*r)[5] = apricot[i];
    int *t = apricot[i][j];
    int u = apricot[i][j][k];
    正常情况下,赋值发生在两个相同的类型之间,可以看到在“数组的数组的数组”中,每一个单独的数组都可以看作是一个指针。这是因为在表达式中的数组名被编译器当做“指向数组第一个单元的指针”。换句话说,不能把一个数组赋值给另一个数组,因为数组作为一个整体不能成为赋值的对象。之所以可以把数组名赋值给一个指针,就是因为这个“在表达式中的数组名被编译器当作一个指针”的规则。
    指针所指向的数组的维度不同,其区别会很大。使用上面例子中的声明:
    r++;
    t++;
    将会使r和t分别指向它们各自的下一个元素(两者所指向的元素本身都是数组)。它们所增长的步长是很不相同的,因为r所指向的数组元素的大小是t所指向的数组的元素大小的3倍。

    /*
    **编程挑战 
    */ 
    数组万岁!(哈哈,挺喜悦的呢)
    int apricot[2][3][5];

    int (*r)[5] = apricot[0];
    int *t = apricot[0][0];
    编写一个程序,打印出r和t的十六进制初始值(使用printf的%x转换符,打印十六进制值),对这两个指针进行自增(
    ++)操作,并打印它们的新值。
    #include
    #include

    int main( void ){
        int apricot[2][3][5];
        
        int (*r)[5] = apricot[0];
        int *t = apricot[0][0];
        printf( "r = %x, t = %x\n", r, t );
        r++;
        t++;
        printf( "r = %x, t = %x\n", r, t );
        
        return EXIT_SUCCESS;

    /* 输出:

    */ 

    内存中数组是如何布局的
    在C语言的多维数组中,最右边的下标是最先变化的,这个约定被称为“行主序”。由于“行/列主序”这个术语只适用于恰好是二维的多维数组,所以更确切的术语是“最右的下标先变化”。绝大部分语言都采用了这个约定,但Fortran却是一个主要的例外,它采用了“最左的下标先变化”,也就是“列主序”。在不同的下表变化中,多维数组在内存中的布局也不相同。事实上,如果把一个C程序的矩阵传递给一个Fortran程序,矩阵就会被自动转置---这是一个非常厉害的邪门秘笈,偶尔还真会用到。
    最低地址         <---->          最高地址
    C int a[2][3] a[0][0] a[0][1] a[0][2] a[1][0] a[1][1] a[1][2]  最右的下标先变化
    Fortran dim a(2, 3) a(1, 1) a(2, 1) a(1, 2) a(2, 2) a(1, 3) a(2, 3) 最左的下标先变化
                           图9-9 行主序vs列主序
    在C语言中,多维数组最大的用途是存储多个字符串。有人指出“最右边的下边先变化”在这方面具有优势(每个字符串中相邻的字符在内存中也相邻存储)。但在“最左边的下标先变化”的多维数组中,情况并不如此。

    如何对数组进行初始化
    在最简单的情况下,一维数组可以通过把初始值都放在一对花括号内来完成初始化。如果在数组的定义里未标明它的长度,C语言约定按照初始化值的个数来确定数组的长度。
    float banans[5] = {0.0, 1.0, 2.72, 3.14, 25.625};
    float honeydew[] = {0.0, 1.0, 2.72, 3.14, 25.625};
    只能够在数组声明时对它进行整体的初始化。之所以存在这个限制,并没有过硬的理由。多维数组可以通过嵌套的花括号进行初始化:
    short cantaloupe[2][5] = {
        {10, 12, 3, 4, -5},
        {31, 22, 6, 0, -5},

    int rhubarb[][3] = {{0, 0, 0}, {1, 1, 1},};
    注意,既可以在最后一个初始化值的后面加一个逗号,也可以省略它。同时,也可以省略最左边下标的长度(也可能是最左边的下标),编译器会根据初始化值的个数推断出它的长度。
    如果数组的长度比所提供的初始化值的个数要多,剩余的几个元素会自动设置为0。如果元素的类型是指针,那么它们被初始化为NULL;如果元素的类型是float,那么它们被初始化为0.0。在流行的IEEE754标准浮点数实现中(IBM PC和Sun系统都使用了这个标准),0.0和0的位模式是完全一样的。

    /*
    **编程挑战 
    */
    检查位模式
    写一个简单的程序,检查在你的系统中,浮点数0.0的位模式是否与整型数0的位模式相同。
    #include
    #include
    #include

    unsigned float_to_bit( float f );

    int main( void ){
        int int_size;
        int flt_size;
        int i; 
        int int_bit_size;
        int flt_bit_size;
        int num;
        float num2;
        
        num = 0;
        num2 = 0.0;
        int_size = sizeof(int);
        flt_size = sizeof(float);
        int_bit_size = int_size * CHAR_BIT;
        flt_bit_size = flt_size * CHAR_BIT;
        for( i = int_bit_size - 1; i >= 0; --i ){
            printf( "%c", (num & (1 << i)) == 1 ? '1' : '0' );
        }
        printf( "\n" );
        unsigned int res = float_to_bit( num2 );
        for( i = flt_bit_size - 1; i >= 0; --i ){
            printf( "%c", (res & (1 << i)) == 1 ? '1' : '0' );
        }
        printf( "\n" );
        
        return EXIT_SUCCESS;    

    unsigned float_to_bit( float f ){
        union{
            float f;
            unsigned u;
        } temp;
        temp.f = f;
        return temp.u;
    }

    /* 输出:

    */ 

    下面是一种初始化二维字符串数组的方法:
    char vegetable[][9] = {"beet",
                           "barley",
                           "basil",
                           "broccoli",
                           "beans"};
    一种非常有用的方法是建立指针数组。字符串常量可以用作数组初始化值。编译器会正确地把各个字符存储于数组中的地址。因此:
    char *avegetable[] = {"carrot",
                          "celery",
                          "corn",
                          "cilantro",
                          "crispy friend patatoes"}; /*没问题*/
    注意它的初始化部分与字符“数组的数组”初始化部分是一样的。只有字符串常量才可以初始化指针数组。指针数组不能由非字符串的类型直接初始化:
    int *weights[] = { /*无法成功编译*/
                        {1, 2, 3, 4, 5},
                        {6, 7},
                        {8, 9, 10} 
    }; /*无法成功编译*/
    如果想用这种方法对数组进行初始化,可以创建几个单独的数组,然后用这些数组名来初始化原先的数组。
    int row_1[] = {1, 2, 3, 4, 5, -1};
    int row_2[] = {6, 7, -1};
    int row_3[] = {8, 9, 10, -1};
    int *weight[] = {row_1, row_2, row_3};

  • 相关阅读:
    AttitudeFactor.h/AttitudeFactor.cpp
    探讨代理IP与Socks5代理在跨界电商中的网络安全应用
    【附源码】计算机毕业设计JAVA学校考务管理系统
    vue3使用element-plus
    1、Kafka 安装与简单使用
    R语言绘制PCA双标图、碎石图、变量载荷图和变量贡献图
    大语言模型相关工具使用链接
    前端面试题目(二十四)
    AlphaFold2无痛安装教程(超级详细)
    LeetCode - 207 课程表
  • 原文地址:https://blog.csdn.net/weixin_40186813/article/details/126090102