数组:将标量数据聚集成更大数据类型
C语言实现数组简单,容易翻译成机器代码
在机器代码中,这些指向数组元素的指针会被翻译成地址计算
优化编译器非常善于简化数组索引所使用的地址计算,这使得我们难以理解C语言代码与机器代码的对应关系
一. 基本原则
一维数组的声明:T A[N](T为数据类型,N为常数)
两个效果:在内存中分配L*N字节的连续区域;引入标识符A,用A来作为指向数组开头的指针,值为xA
数组元素i会存放在xA+i*L的地方
内存引用指令可以用来简化数组访问
movl (%rdx, %rcx, 4), %eax 执行地址计算xA+4i,并将结果存放在寄存器%eax中
伸缩因子1, 2, 4, 8覆盖了所有数据类型的大小
知识点回顾:
内存引用是操作数的一种
操作数一共有3种:
- 立即数(书写方式:‘$’后面跟一个用标准C表示法表示的整数,例如$-577,$0x1F)、
- 寄存器(书写方式为寄存器的名称ra,实际上操作的是寄存器里的值R[ra],将寄存器集合看成一个数组R,用寄存器标识符作为索引)、
- 内存引用(根据计算出来的地址Addr访问某个内存位置Mb[Addr],表示对存储在内存中地址Addr开始的b个字节值的引用)
- 计算地址的方式称为寻址模式,具体形式为:Imm(rb, ri, s) Imm表示一个立即数,rb称为基址寄存器,ri称为变址寄存器,s称为比例因子(s属于{1, 2, 4, 8})
- 计算公式:Imm+R[rb]+R[ri]*s
二. 指针运算
C语言允许对指针进行运算,计算出来的值会根据该指针引用的数据类型的大小进行伸缩,例如表达式p+i的值为xp+L*i
表达式2, 3, 6返回数组值,类型为数组元素的类型int,因此涉及4字节操作,例如指令movl和寄存器%eax
表达式1, 4, 5返回指针,类型为int*,因此涉及8字节操作,例如指令leaq和寄存器%rax
注意⚠️:表达式7可以计算同一个数据结构中的两个指针之差,结果的数据类型为long,值等于两个地址之差除以该数据类型的大小
三. 嵌套的数组
声明一个二维数组A
int A[5][3];//直接声明 等价于 typedef int row3_t[3];//嵌套声明 row3_t A[5];//可以立即为用A[5]去替换int row3_t[3]中的row3_t
这一部分的重点只有一个:
声明数组:T D[R][C];
则它的元素D[i][j]的内存地址为&D[i][j]=xD+L(C*i+j),L为数据类型T的大小
用行优先索引将多维数组映射到一维数组
考法:运用逆向工程,根据汇编代码确定R和C的值
四. 定长数组
定长数组的声明
#define N 16 typedef int fix_matrix[N][N];//将fix_matrix声明为16*16的整形数组
这部分主要讲了C语言编译器对定长多维数组的优化
例如计算两矩阵乘积的某一个元素,编译器会优化掉for循环所用的循环变量i,转换为指针的计算,左矩阵Aptr++,右矩阵Bptr+=N,并将for循环变为do-while循环,直到Bptr指向矩阵外时停止
例如将一矩阵的对角线上的元素赋值为val,编译器会优化掉for循环所用的循环变量i,转换为指针的计算,每次ptr+=N+1,并将for循环变为do-while循环,直到ptr指向矩阵外时停止
五. 变长数组
在历史上,C语言只支持大小在编译时就能确定的多维数组(对第一维有些例外)。那时候,程序员需要变长数组时不得不用malloc、calloc等函数为数组分配存储空间,并且需要程序员自己讲多维数组映射到一维数组,写入C语言程序。
ISO C99引入功能:数组的维度可以是表达式,在数组被分配的时候被计算出来
与定长数组的区别在于:必须用乘法指令imulq对i伸缩n倍,不能用一系列的移位和加法
利用访问模式的规律性来优化索引的计算
回到刚才的例子,计算两矩阵乘积的某一个元素,识别出访问数组元素的步长,生成的代码会避免直接使用内存地址公式的乘法计算(由于乘法计算较耗时,因此少去一步乘法计算会显著提高程序性能)