3.17 数组

数组在内存中连续分配,编译器通过地址计算实现数组元素的访问。

一维数组

内存布局

一维数组的元素在内存中连续存放,占用空间为 N × sizeof(T) 字节。

// C 代码
char   A[4];   // 4 × 1 = 4 字节
short  B[4];   // 4 × 2 = 8 字节
int    C[4];   // 4 × 4 = 16 字节
long   D[4];   // 4 × 8 = 32 字节
内存布局(以 long D[4] 为例):
地址   内容
D      D[0]  ← 低地址
D+8    D[1]
D+16   D[2]
D+24   D[3]  ← 高地址

元素访问

// C 代码
long array[N];
 
long get_array(long *arr, long index) {
    return arr[index];
}
# 汇编实现
get_array:
    # %rdi = arr, %rsi = index
    movq    (%rdi, %rsi, 8), %rax   # rax = arr[index]
    ret

地址计算arr + index × 8

通用公式

对于类型 T 的数组 T A[N],访问 A[i] 的地址为:

&A[i] = A + i × sizeof(T)

多维数组

行优先存储

C 语言使用行优先(row-major)顺序存储多维数组:

// C 代码
long A[M][N];   // M 行 N 列
内存布局:A[0][0] A[0][1] ... A[0][N-1] A[1][0] A[1][1] ... A[M-1][N-1]
          ├────── 第 0 行 ──────┤ ├────── 第 1 行 ──────┤

二维数组元素访问

// C 代码
long get_element(long A[M][N], long i, long j) {
    return A[i][j];
}
# 汇编实现
get_element:
    # %rdi = A, %rsi = i, %rdx = j
    # 计算 A + i × N × 8 + j × 8
    imulq   $N, %rsi, %rsi     # rsi = i × N
    addq    %rsi, %rdi         # rdi = A + i × N × 8
    movq    (%rdi, %rdx, 8), %rax  # rax = A[i][j]
    ret

地址公式&A[i][j] = A + i × (N × sizeof(T)) + j × sizeof(T)

多维数组的行指针

// C 代码
long get_row(long A[M][N], long i) {
    return (long)&A[i];
}
get_row:
    # %rdi = A, %rsi = i
    leaq    (%rdi, %rsi, N*8), %rax  # rax = A + i × N × 8
    ret

元素大小与步长

元素的大小决定了数组访问的步长(stride):

类型大小元素 i 的地址步长
char1arr + i1
short2arr + i × 22
int4arr + i × 44
long8arr + i × 88
double8arr + i × 88
int*8arr + i × 88

固定大小数组 vs 指针

// 固定大小数组
long A1[4];           // 栈上分配 32 字节
// A1 的类型是 long[4],不是指针
 
// 指针
long *A2;              // 栈上分配 8 字节(指针)
A2 = malloc(32);       // 堆上分配
 
// 参数声明——实际是指针
void func(long A[4]) { // 等价于 long *A
    // sizeof(A) == 8,不是 32!
}

关键区别

  • sizeof(A1) = 32(整个数组)
  • sizeof(A2) = 8(指针)
  • 函数参数中的数组声明退化为指针

数组的循环处理

// C 代码:数组求和
long array_sum(long *arr, long n) {
    long sum = 0;
    for (long i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}
# 汇编实现(Guarded-Do 模式)
array_sum:
    movq    $0, %rax            # sum = 0
    movq    $0, %rcx            # i = 0
    cmpq    %rsi, %rcx          # 比较 i 和 n
    jge     .Ldone              # if (i >= n) done
 
.Lloop:
    addq    (%rdi, %rcx, 8), %rax  # sum += arr[i]
    addq    $1, %rcx            # i++
    cmpq    %rsi, %rcx          # 比较 i 和 n
    jl      .Lloop              # if (i < n) continue
 
.Ldone:
    ret

指针版本优化

编译器常将数组索引转化为指针操作:

# 优化版本(使用指针而非索引)
array_sum_opt:
    movq    $0, %rax            # sum = 0
    leaq    (%rdi, %rsi, 8), %rcx  # end = arr + n
 
.Lloop:
    cmpq    %rcx, %rdi          # 比较 arr 和 end
    jae     .Ldone              # if (arr >= end) done
    addq    (%rdi), %rax        # sum += *arr
    addq    $8, %rdi            # arr++
    jmp     .Lloop
 
.Ldone:
    ret

变量长度数组(VLA)

C99 支持变量长度数组,数组维度在运行时确定。

// C 代码:VLA 作为函数参数
long sum_vla(long n, long A[n][n]) {
    long sum = 0;
    for (long i = 0; i < n; i++) {
        for (long j = 0; j < n; j++) {
            sum += A[i][j];
        }
    }
    return sum;
}

VLA 的地址计算需要在运行时动态计算行步长:&A[i][j] = A + i × n × 8 + j × 8

数组的栈分配

void stack_array(long n) {
    long local_arr[4];    // 固定大小,编译时分配
    long vla_arr[n];      // VLA:运行时分配
}
stack_array:
    pushq   %rbp
    movq    %rsp, %rbp
    # 固定大小数组:subq $32, %rsp  (4 × 8 = 32)
    # VLA:动态计算大小后分配
    leaq    (, %rdi, 8), %rax      # rax = n * 8
    subq    %rax, %rsp             # 动态分配 VLA 空间
    # ...
    movq    %rbp, %rsp
    popq    %rbp
    ret

指针数组 vs 多维数组

// 真正的多维数组(连续内存)
long A[3][4];           // 固定每行 4 列,连续 96 字节
 
// 指针数组(每行可独立分配)
long *B[3];             // 3 个指针
B[0] = malloc(4 * 8);   // 每行可不同长度
B[1] = malloc(8 * 8);
B[2] = malloc(2 * 8);
 
// 访问差异
long x = A[1][2];       // *(A + 1*4*8 + 2*8)
long y = B[1][2];       // *(*(B + 1*8) + 2*8) 两次访存

相关笔记

    • 寻址模式在数组访问中的应用
    • 数组地址的计算
    • 结构体数组的内存布局
    • 数组元素的对齐要求

链接到