07 · 数组、结构体、指针与浮点汇编
来源:第 7 讲 pp.3~51(包括附加材料);小班第 7 讲;教材 §3.8~3.9、§3.11;2025 阶段卷第 18~22 题。
一维数组与指针算术#
T A[N] 为 N 个连续 T 对象分配 N·sizeof(T) 字节。&A[i] = base+i·sizeof(T),A[i]=*(A+i)。指针加 1 是增加一个所指对象的跨度,而不是一字节。
同一数组中的指针相减得到元素个数(类型 ptrdiff_t),不是字节数;跨不相关对象相减不满足 C 的定义。可以形成末尾后一个指针用于比较或循环终止,但不能解引用它。
数组对象不是指针变量。A 在很多表达式中退化为首元素指针,sizeof(A) 和 &A 是重要例外;作为函数形参的 int a[10] 调整为指针参数,所以在函数内 sizeof(a) 是指针大小。
二维、变长与多级数组#
C 的 T A[R][C] 是 R 个“含 C 个 T 的数组”,行优先:
&A[i][j] = base + (i*C+j)*sizeof(T)
A+i 的跨度 = C*sizeof(T)
&A+1 的跨度 = R*C*sizeof(T)
int A[3][5] 中 sizeof(A)=60、sizeof(*A)=20、sizeof(**A)=4、sizeof(&A)=8。这些数值默认本课 LP64。
int fixed(int a[3][5], size_t i, size_t j) { return a[i][j]; }
int variable(size_t n, int a[n][n], size_t i, size_t j) {
return a[i][j];
}
变长数组的列数要在运算时参与乘法;形参调整后的类型是指向整行的指针,不是 int**。固定列数常能用移位或 LEA 算行偏移。
int *rows[R] 是指针数组:先从表里读 rows[i],再通过所得指针读第 j 项,通常两次数据读取;各行可独立分配、不连续或不同长度。int A[R][C] 只需一次最终元素读取,行地址可直接算出。将连续二维数组强转 int** 不会自动变成行指针表。
类型声明与 sizeof 表#
从变量名出发,先结合括号内的结构,再按 []、() 优先于 * 读声明。
| 声明 | A 是什么 | sizeof A |
sizeof *A |
sizeof **A |
sizeof ***A |
|---|---|---|---|---|---|
int A[3][5] |
二维 int 数组 | 60 | 20 | 4 | 非法类型表达式 |
int *A[3][5] |
二维 int 指针数组 | 120 | 40 | 8 | 4 |
int (*A)[3][5] |
指向整个二维数组的指针 | 8 | 60 | 20 | 4 |
int (*A[3])[5] |
3 个指针,每个指向 5 个 int 的数组 | 24 | 8 | 20 | 4 |
int *(*A[2])[3] |
2 个指针,每个指向 3 个 int* 的数组 | 16 | 8 | 24 | 8 |
最后一行不是“指针指向指针再指向 int 数组”的随意层叠:括号与数组位置决定了元素类型。再解引用一级 ****A 才得到 int,sizeof 为 4。
sizeof(*p) 对非 VLA 类型通常不求值,因此 sizeof 不会真的解引用 p;但在普通值表达式中读 *p 需要有效对象。课件附表的 “Bad pointer” 是讨论实际访问风险,不应误读成所有 sizeof 都发生访存。VLA 相关 sizeof 可能运行时求值,要单独分析。
结构体布局与对齐#
编译器按声明顺序排列成员,不会为了省空间擅自重排。每个成员从满足其 alignment 的最小偏移开始;最终结构体大小向其最大成员对齐要求的整数倍取整,保证结构体数组每个元素也正确对齐。
align_up(x,a) = ceil(x/a)*a
成员偏移 = align_up(当前末尾, 成员对齐)
结构体大小 = align_up(最后成员末尾, 结构体对齐)
struct S { char c; int i; short s; double d; };
/* 本课 ABI:offset(c)=0, i=4, s=8, d=16, sizeof=24, align=8 */
c 后补 3 字节;s 后结束于 10,再补 6 字节使 d 在 16。如果改成 double d; int i; short s; char c;,偏移 0/8/12/14,总大小 16。这说明程序员改变顺序可节省空间;编译器不能偷偷改变对外布局。
数组和嵌套结构体作为完整成员参与布局。结构体整体起始地址必须满足所有成员的对齐,不能只满足第一个 char。x86 支持许多未对齐访问不意味着 C/ABI 可以忽略对齐;未对齐还可能跨缓存块并影响性能。#pragma pack、不同 ABI 和向量类型可能改变规则。
用 <stddef.h> 的 offsetof(struct S,i) 核对偏移,用 _Alignof 核对对齐;不通过手写空指针解引用技巧猜布局。
从汇编反推布局#
先看访问宽度判断候选类型,再看偏移;对数组维度列出约束而非凭一个地址猜答案。2025 阶段卷第 21 题:
str1: int x[A][B]; long y; y 在 184
str2: char array[B]; int t; short s[A]; long u;
t 在 8,u 在 32
得到 align_up(B,4)=8,所以 5≤B≤8;align_up(12+2A,8)=32,所以 7≤A≤10;align_up(4AB,8)=184,所以 AB∈{45,46}。唯一满足正整数范围的解是 A=9,B=5。注意尾部 y 的地址不是简单假定为 4AB,必须先考虑 padding。
链表读取中区分取成员地址与读成员值:leaq 8(%rdi),%rax 返回成员地址;movq 8(%rdi),%rax 读该处 8 字节;后面再 movq (%rax),%rax 才是一次额外指针追踪。
SSE / XMM 与 SIMD#
XMM 寄存器宽 128 位,标量运算只处理指定低位标量,packed 运算处理多个元素。YMM、ZMM 是更宽的扩展寄存器;本课代码主要看 XMM。
| 指令/后缀 | 意义 |
|---|---|
| ss / sd | scalar single / scalar double,单精度/双精度标量 |
| ps / pd | packed single / packed double,4 个 float / 2 个 double |
| movss / movsd | 移动浮点标量位模式 |
| addss/addsd、mulss/mulsd、sub、div 同族 | 相应精度标量运算 |
| cvtss2sd / cvtsd2ss | float ↔ double 数值转换 |
| cvtsi2sd / cvtsi2ss | 有符号整数转换成浮点 |
| cvttsd2si / cvttss2si | 浮点向零截断转换为有符号整数 |
| ucomiss / ucomisd | 设置浮点比较结果标志 |
| xorpd %xmm0,%xmm0 | 清零,形成 +0 位模式 |
移动位模式不是数值转换;movq 的整数/向量寄存器间形式也不等于 cvt。标量操作对寄存器其他位的效果与具体 SSE/AVX 编码有关,不只凭 ss/sd 后缀泛化。
浮点参数与比较#
double mix(long *a,double *b,float c):a 在 rdi,b 在 rsi,c 在 xmm0;指向浮点的指针仍走整数寄存器,只有浮点值走 XMM。若计算 *a+*b+c,先加载、转换为 double,再加,结果在 xmm0。
对于 ucomis 的逻辑左值与右值比较(AT&T ucomisd src,dst 比较 dst 对 src):
| 关系 | ZF | PF | CF |
|---|---|---|---|
| 大于 | 0 | 0 | 0 |
| 小于 | 0 | 0 | 1 |
| 相等 | 1 | 0 | 0 |
| 无序(NaN) | 1 | 1 | 1 |
因此不能仅用 ZF 断言浮点相等,还要排除 PF=1 的无序情形;编译器可能组合 setnp 等指令。OF/SF 清零,不能套整数 signed 比较条件。NaN 的异常细节还需区别 quiet/signaling 和比较指令种类。
自检#
能按类型一步步求 sizeof;能解释数组形参与对象的差异;能列结构体偏移表;能从汇编反推维度;能判断浮点指针和浮点值各在哪类寄存器;能解释 unordered。教材作业 3.66、3.67、3.68。