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倍。
/*
** multi_array_code_2.c.
*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
int i = 0, j = 0, k = 0;
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];
int (*r2)[5] = r + 1;
int *t2 = t + 1;
int num = (r2 - r) * 5 * sizeof(int);
int num_2 = (t2 - t) * 1 * sizeof(int);
printf( "sizeof(*r) = %zd, sizeof(*t) = %zd\n", sizeof(*r), sizeof(*t) );
printf( "r2 = %p, r = %p, t2 = %p, t = %p, num = %d, num_2 = %d\n",
r2, r, t2, t, num, num_2 );
return EXIT_SUCCESS;
}
输出:
/*
**编程挑战
*/
数组万岁!(哈哈,挺喜悦的呢)
int apricot[2][3][5];
int (*r)[5] = apricot[0];
int *t = apricot[0][0];
编写一个程序,打印出r和t的十六进制初始值(使用printf的%x转换符,打印十六进制值),对这两个指针进行自增(++)操作,并打印它们的新值。
#include <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>
#include <limits.h>
unsigned int 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 & (1u << i)) == 1 ? '1' : '0' );
}
printf( "\n" );
unsigned int res = float_to_bit( num2 );
printf( "res = %u\n", res );
for( i = flt_bit_size - 1; i >= 0; --i ){
printf( "%c", (res & (1u << i)) == 1 ? '1' : '0' );
}
printf( "\n" );
return EXIT_SUCCESS;
}
unsigned int 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};
/*
** doubly_dimension string array and string pointer array.
*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
const char *str = "beet";
const char *str_2 = "barley";
const char *str_3 = "basil";
const char *str_4 = "broccoli";
const char *str_5 = "beans";
char vegetable[][9] = {
"beet",
"barley",
"basil",
"broccoli",
"beans"
};
const char *vegetable_2[] = {
"beet",
"barley",
"basil",
"broccoli",
"beans"
}; /*没问题*/
int i;
int size = sizeof(vegetable) / sizeof(*vegetable);
int size_2 = sizeof(vegetable) / sizeof(*vegetable);
printf( "str = %p, str_2 = %p, str_3 = %p, str_4 = %p, str_5 = %p\n", str, str_2, str_3, str_4, str_5 );
for( i = 0; i < size; ++i ){
printf( "vegetable[%d] = %p, vegetable_2[%d] = %p\n", i, vegetable[i], i, vegetable_2[i] );
}
return EXIT_SUCCESS;
}
输出:
C专家编程 第9章 再论数组 9.6 C语言的多维数组相关推荐
- C专家编程 第10章 再论指针 10.7 使用指针创建和使用动态数组
使用指针创建和使用动态数组 当预先并不知道数据的长度时,可以使用动态数组.绝大多数具有数组的编程语言都能够在运行时设置数组的长度.它们允许程序员计算需要处理的元素的数目,然后创建一个刚好能容纳 ...
- C专家编程--读书笔记九 再论数组
第九章 一.知识点 1.所有作为函数参数的数组名总是可以通过编译器转换成指针. 然而,数组和指针在编译器处理时是不同的,在运行时的表示形式也是不一样的,并可能产生不同的代码.对编译器而言,一个数组就是 ...
- 《C专家编程》第二章——这不是Bug,而是语言特性
无论一门语言有多么流行或多么优秀,它总是存在一些问题,C语言也不例外.本章讨论的重点是C语言本身存在的问题,作者煞费苦心的用一个太空任务和软件的故事开头,也用另一个太空任务和软件的故事结尾,引人入胜. ...
- 二维数组转稀疏数组,写入文件后再读取文件,将内容转回二维数组
该方法模拟的是将棋盘的位置保存到稀疏数组中,降低存储的数据量,通过写入磁盘做持久化,再读入后恢复棋盘内容. package com.moson.sparsearray;import java.io.* ...
- C专家编程 第6章 运动的诗章:运行时数据结构 6.1 a.out及其传说
运动的诗章:运行时数据结构 编程语言理论的经典对立之一就是代码和数据的区别.有些语言(如Lisp)把二者视为一体.其他语言(如C语言)通常维持两者的区别.Internet蠕虫非常难以被人们所 ...
- C专家编程 第1章 C:穿越时空的迷雾 1.4 KR C
时间:1978 人物:Steve Johnson 事件:编写了pcc这个可移植的C编译器 影响:形成了一代C编译器的基础. C语言的演化之路如图1-2所示. 1973-3(早期的C)--->19 ...
- C专家编程 第11章 你懂得C,所以C++不再话下 11.1 初识OOP
C++之于C,就像Algol-68之于Algol ---David L.Jones 如果你觉得C++还不够复杂,那你知道protected abstract virtual ...
- C专家编程 第1章 C:穿越时空的迷雾 1.9 阅读ANSI C标准,寻找乐趣和裨益
阅读ANSI C标准,寻找乐趣和裨益 int foo(const char **p) { } 编译这段代码,编译器会发出一条警告信息: int main(int a ...
- C专家编程--读书笔记十 再论指针
第十章 一.知识点 1.C标准规定%s说明符的参数必须是一个指向字符数组的指针.所以如: char *p = NULL; printf("%s", p); 这是不正确的.NULL是 ...
- C专家编程 第8章 为什么程序员无法分清万圣节和圣诞节 8.2 根据位模式构筑图形
根据位模式构筑图形 图标(icon)或者图形(glyph)是一种小型的位模式映射于屏幕后产生的图像.一个位表示图像上的一个像素.如果一个位被设置,那么它所代表的像素就是"亮" ...
最新文章
- navicat 连接 mogodb 报错 requires authentication
- [你必须知道的.NET]第三十二回,,深入.NET 4.0之,Tuple一二
- 代码重构 防火墙 相关知识
- android notification自动消失,Notification点击事件和点击消失
- ubuntu 输入空白How to cd to folder name with spaces blank names (white space)
- python lxml模块解析html_用lxml解析HTML
- web pack的使用事项
- CSAPP-C1-计算机系统漫游
- bzoj 1132: [POI2008]Tro
- 百科知识 已知三角形三条边长,如何求解三角形的面积
- Mujoco xml建模
- 美容院预约管理系统管理员端用例测试
- 基于深度学习机器学习的中文期刊分类
- 标准单元库的corner简述
- 大陆引进《火影忍者》角色中文译名雷人出炉
- 深入浅出的web语义化理解
- 盘点中国未来最具潜力的IT培训学校前5名
- 对勾和叉怎么打_word文档中输入对号“√ ”和 叉号“×”的方法 word怎么打钩/打叉/半对半勾符号...
- OUC_软件工程_BLOG1
- StarRocks Parser 源码解析
热门文章
- python的十句名言_洗脑最厉害的10句名言名句
- Compose Modifier
- 前端 关于汇率的计算
- audio音频使用天坑
- FireFox更改缓存路径
- 尚来古籍——江氏族谱
- 使用php的curl爬去青果教务系统 课表(转)
- maven出现The server encountered an internal error () that prevented it from fulfilling this request.
- win7服务器未能登录怎么解决,win7系统出现User Profile Service服务未能登录的解决方法...
- SGG前台项目复习笔记