数组 (Arrays)


章节概述

数组是 C 语言中组织同类型数据的核心机制——连续排列的内存块,通过下标快速访问。本章从一维数组的声明与初始化出发,深入二维和高维数组的内存布局(行主序 row-major),讲解变长数组(VLA,C99 特性)、数组退化为指针的核心概念(array-to-pointer decay)、数组越界的危险(C 语言不进行边界检查!)、多维数组在栈和堆上的内存分配,最后通过 mermaid 图表和汇编代码展示数组寻址的底层机制(base + index * sizeof(element))。理解数组在内存中的真实布局是理解指针、结构体、动态内存管理的基础。

核心主题:数组 = 连续内存块 + 首地址 + 偏移计算。CPU 通过 [base + offset * scale] 寻址模式实现数组访问——这也是为什么数组下标从 0 开始(偏移量 = 下标 × 元素大小)。C 语言不进行边界检查,意味着代码安全完全依赖程序员自律。


第一节:一维数组


1.1 声明、初始化、访问

#include <stdio.h>
 
int main() {
    // 声明(未初始化 → 内容不确定)
    int arr1[5];
 
    // 声明 + 初始化列表
    int arr2[5] = {10, 20, 30, 40, 50};
 
    // 部分初始化(剩余元素自动置 0)
    int arr3[5] = {1, 2};  // {1, 2, 0, 0, 0}
 
    // 自动推导长度
    int arr4[] = {1, 2, 3, 4, 5, 6};  // 长度为 6
 
    // 全零初始化
    int arr5[100] = {0};
 
    // C99 指定初始化器(designated initializer)
    int arr6[10] = {[2] = 5, [7] = 9};
    // arr6 = {0, 0, 5, 0, 0, 0, 0, 9, 0, 0}
 
    // 访问和修改
    arr2[0] = 100;          // 修改第一个元素
    int x = arr2[3];        // 读取第四个元素
 
    // 遍历
    for (int i = 0; i < 5; i++) {
        printf("arr2[%d] = %d\n", i, arr2[i]);
    }
 
    return 0;
}

1.2 数组在内存中的布局

graph LR
    subgraph "int arr#91;5#93; = {10, 20, 30, 40, 50}"
        A0["arr#91;0#93;: 10<br/>地址 0x1000"] 
        A1["arr#91;1#93;: 20<br/>地址 0x1004"]
        A2["arr#91;2#93;: 30<br/>地址 0x1008"]
        A3["arr#91;3#93;: 40<br/>地址 0x100C"]
        A4["arr#91;4#93;: 50<br/>地址 0x1010"]
    end
    A0 --> A1 --> A2 --> A3 --> A4
内存地址(假设 arr 起始于 0x1000):
0x1000: 0A 00 00 00  ← arr[0] = 10
0x1004: 14 00 00 00  ← arr[1] = 20
0x1008: 1E 00 00 00  ← arr[2] = 30
0x100C: 28 00 00 00  ← arr[3] = 40
0x1010: 32 00 00 00  ← arr[4] = 50

每个 int 占 4 字节 (sizeof(int) = 4)
地址递增步长 = sizeof(int) = 4

关键公式

arr[i] 的地址 = arr (基地址) + i × sizeof(元素类型)

1.3 数组名 = 指向首元素的指针(但不能被赋值)

#include <stdio.h>
 
int main() {
    int arr[5] = {10, 20, 30, 40, 50};
 
    // arr 的类型是 int[5](长度为 5 的 int 数组)
    // 但在大多数表达式中,arr 退化为 int*(指向首元素)
 
    printf("arr     = %p\n", (void*)arr);
    printf("&arr[0] = %p\n", (void*)&arr[0]);
    // 两者地址相同!
 
    printf("*arr    = %d\n", *arr);       // 10 (arr[0])
    printf("*(arr+2) = %d\n", *(arr + 2)); // 30 (arr[2])
 
    // arr 可以参与指针运算但不可被赋值
    int *p = arr;    // OK:指针 p 指向 arr 首元素
    // arr = p;      // 编译错误!数组名不是可修改的左值
 
    // 字符串字面量是特殊情况
    char str[] = "hello";   // OK:在栈上分配数组并拷贝字符串
    // str = "world";       // 编译错误!数组名不可被赋值
 
    return 0;
}

1.4 指针算术与数组下标的关系

#include <stdio.h>
 
int main() {
    int arr[] = {10, 20, 30, 40, 50};
 
    // arr[i] 等价于 *(arr + i)
    // 实际上,C 标准定义了 E1[E2] 等价于 (*((E1)+(E2)))
    // 这意味着...
 
    printf("arr[2]  = %d\n", arr[2]);    // 30
    printf("*(arr+2) = %d\n", *(arr+2)); // 30
 
    // 甚至以下写法也是合法的(但不要这样写!):
    printf("2[arr]  = %d\n", 2[arr]);    // 30!
 
    // 遍历数组的两种等价写法
    // 写法1:下标访问
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
 
    // 写法2:指针遍历
    for (int *p = arr; p < arr + 5; p++) {
        printf("%d ", *p);
    }
    printf("\n");
 
    return 0;
}

小节练习

选择题 1

int arr[5] = {1, 2}; 之后,arr[3] 的值是?

  • A. 未定义(垃圾值)

  • B. 0

  • C. 2

  • D. 编译错误

判断题 1

int arr[5]; arr = (int[]){1,2,3,4,5}; 可以给数组赋值。 ( )

  • 正确

  • 错误


第二节:多维数组


2.1 二维数组的声明和初始化

#include <stdio.h>
 
int main() {
    // 声明二维数组
    int matrix1[3][4];  // 3 行 4 列,未初始化
 
    // 完全初始化
    int matrix2[3][4] = {
        {1, 2, 3, 4},     // 第0行
        {5, 6, 7, 8},     // 第1行
        {9, 10, 11, 12}   // 第2行
    };
 
    // 部分初始化
    int matrix3[3][4] = {
        {1, 2},           // 第0行: {1, 2, 0, 0}
        {5},              // 第1行: {5, 0, 0, 0}
        {}                // 第2行: {0, 0, 0, 0}
    };
 
    // 平坦初始化(按行主序填充)
    int matrix4[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
 
    // 自动推导第一维长度
    int matrix5[][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };  // 自动得出 3 行
    // 注意:第二维必须指定!编译器需要知道每行有多少列
 
    // 访问元素
    printf("matrix2[1][2] = %d\n", matrix2[1][2]);  // 7
 
    return 0;
}

2.2 二维数组的内存布局(行主序 Row-Major)

graph TD
    subgraph "int matrix#91;3#93;#91;4#93; 内存布局"
        direction LR
        M0["0x1000<br/>matrix#91;0#93;#91;0#93; = 1"]
        M1["0x1004<br/>matrix#91;0#93;#91;1#93; = 2"]
        M2["0x1008<br/>matrix#91;0#93;#91;2#93; = 3"]
        M3["0x100C<br/>matrix#91;0#93;#91;3#93; = 4"]
        M4["0x1010<br/>matrix#91;1#93;#91;0#93; = 5"]
        M5["0x1014<br/>matrix#91;1#93;#91;1#93; = 6"]
        M6["0x1018<br/>matrix#91;1#93;#91;2#93; = 7"]
        M7["0x101C<br/>matrix#91;1#93;#91;3#93; = 8"]
        M8["0x1020<br/>matrix#91;2#93;#91;0#93; = 9"]
        M9["0x1024<br/>matrix#91;2#93;#91;1#93; = 10"]
        M10["0x1028<br/>matrix#91;2#93;#91;2#93; = 11"]
        M11["0x102C<br/>matrix#91;2#93;#91;3#93; = 12"]
    end
    M0 --> M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7 --> M8 --> M9 --> M10 --> M11

C 语言采用行主序(Row-Major Order):同一行的元素存储在连续的内存位置中。

matrix[i][j] 的地址 = matrix + (i × 列数 + j) × sizeof(element)
                   = matrix + (i × 4 + j) × 4
#include <stdio.h>
 
int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
 
    // 证明二维数组在内存中是连续的一维数组
    int *p = (int*)matrix;  // 或 &matrix[0][0]
    printf("平坦遍历: ");
    for (int i = 0; i < 12; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");  // 1 2 3 4 5 6 7 8 9 10 11 12
 
    // 等价于
    printf("matrix[%d][%d] 的替代写法:\n", 1, 2);
    // matrix[i][j] = *(*(matrix + i) + j)
    printf("*(*(matrix+1)+2) = %d\n", *(*(matrix + 1) + 2));
 
    return 0;
}

2.3 三维数组和更高维

#include <stdio.h>
 
int main() {
    // 三维数组:层 × 行 × 列
    int cube[2][3][4] = {
        {  // 第0层
            {1, 2, 3, 4},        // 第0层第0行
            {5, 6, 7, 8},        // 第0层第1行
            {9, 10, 11, 12}      // 第0层第2行
        },
        {  // 第1层
            {13, 14, 15, 16},
            {17, 18, 19, 20},
            {21, 22, 23, 24}
        }
    };
 
    printf("cube[1][2][3] = %d\n", cube[1][2][3]);  // 24
 
    // 内存布局:仍然是行主序的延伸
    // cube[layer][row][col] 的偏移 =
    //   layer × (rows × cols) + row × cols + col
    // = cube + (layer * 12 + row * 4 + col) * 4
 
    // 验证连续内存
    int count = 0;
    for (int l = 0; l < 2; l++)
        for (int r = 0; r < 3; r++)
            for (int c = 0; c < 4; c++)
                printf("%d ", cube[l][r][c] == ++count ? cube[l][r][c] : -1);
    printf("\n");  // 1..24 continuum
 
    return 0;
}

2.4 多维数组作为函数参数

#include <stdio.h>
 
// 传递二维数组 —— 必须指定所有维(除了第一维)
void print_matrix(int rows, int cols, int matrix[][4]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n");
    }
}
 
// 等价的指针版本
void print_matrix_ptr(int rows, int cols, int *matrix) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            // 手动计算偏移
            printf("%3d ", matrix[i * cols + j]);
        }
        printf("\n");
    }
}
 
int main() {
    int mat[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
 
    printf("二维数组版本:\n");
    print_matrix(3, 4, mat);
 
    printf("\n指针版本:\n");
    print_matrix_ptr(3, 4, &mat[0][0]);
 
    return 0;
}

小节练习

选择题 1

int arr[3][4] 在内存中占多少字节(64 位系统)?

  • A. 12 字节

  • B. 24 字节

  • C. 48 字节

  • D. 96 字节

选择题 2

二维数组传参时,下面哪个函数声明是正确的?

  • A. void func(int arr[][])

  • B. void func(int arr[3][])

  • C. void func(int arr[][4])

  • D. void func(int arr[3][4])


第三节:变长数组(VLA — Variable Length Array)


3.1 VLA 的概念(C99 特性)

VLA(变长数组)允许在运行时确定数组的大小,数组长度可以是一个变量(非编译时常量)。

#include <stdio.h>
 
int main() {
    int n;
    printf("输入数组大小: ");
    scanf("%d", &n);
 
    // VLA:数组大小在运行时确定
    int arr[n];  // 在栈上分配 n 个 int
 
    // 初始化和使用
    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;
    }
 
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
 
    // sizeof VLA:在运行时计算
    printf("sizeof(arr) = %zu 字节\n", sizeof(arr));
    // 运行时计算为 n * sizeof(int)
 
    return 0;
}

3.2 VLA 的限制和风险

#include <stdio.h>
 
int main() {
    int n;
 
    // 限制1:不能初始化(因为大小在编译时未知)
    // int arr[n] = {0};  // 编译错误!
 
    // 正确:声明后手动清空或逐个赋值
    n = 10;
    int arr[n];
    for (int i = 0; i < n; i++) arr[i] = i;
 
    // 限制2:不能作为全局数组(全局变量必须在编译时确定大小)
 
    // 风险1:栈溢出
    // 如果 n = 100000000,arr 占 ~400MB,远超栈大小 → 段错误
    // printf("输入 n: ");
    // scanf("%d", &n);
    // int huge[n];  // 可能是栈溢出!
 
    // 风险2:C11 将 VLA 变为可选特性
    // 部分编译器(如 MSVC)不支持 VLA
 
    // 风险3:VLA 作为函数参数的类型与指针相同
    void process(int rows, int cols, int mat[rows][cols]) {
        // 实际上 mat 仍然是指针
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                mat[i][j] = i * cols + j;
            }
        }
    }
 
    int rows = 3, cols = 4;
    int matrix[3][4];
    process(rows, cols, matrix);
 
    return 0;
}

3.3 VLA vs 动态内存分配

特性VLAmalloc/free
内存位置栈(stack)堆(heap)
生存期函数内(出作用域自动释放)手动管理(直到 free)
最大大小受栈大小限制(~8MB)受系统内存限制(GB 级)
性能极快(仅移动栈指针)较慢(需要堆管理)
可移植性C99 可选始终可用(C89 起)
可返回否(函数返回后栈被回收)
#include <stdio.h>
#include <stdlib.h>
 
// VLA 版本 —— 数组在函数返回后即消失
void fill_vla(int n) {
    int arr[n];
    for (int i = 0; i < n; i++) arr[i] = i;
    // arr 在这里被销毁
}
 
// malloc 版本 —— 数组持续存在直到 free
int *fill_malloc(int n) {
    int *arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) return NULL;
    for (int i = 0; i < n; i++) arr[i] = i;
    return arr;  // 安全返回,调用者负责 free
}
 
int main() {
    int *arr = fill_malloc(5);
    for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
    printf("\n");
    free(arr);  // 必须释放
    return 0;
}

关于动态内存管理(malloc/free)的详细讲解见 03_动态内存管理

小节练习

判断题 1

VLA(变长数组)在 C11 标准中是强制性要求。 ( )

  • 正确

  • 错误


第四节:数组越界与缓冲区溢出


4.1 C 语言不进行边界检查

#include <stdio.h>
 
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
 
    // 越界写入 —— C 不报错!
    arr[5] = 999;    // 写入了不属于 arr 的内存
    arr[10] = 888;   // 可能覆盖其他变量或返回地址!
 
    // 越界读取 —— C 不报错!
    printf("arr[5] = %d\n", arr[5]);  // 读取未知内存
    printf("arr[10] = %d\n", arr[10]); // 可能读取到其他变量的值
 
    // 后果:
    // 1. 未定义行为(UB)——可能崩溃,可能静默产生错误结果
    // 2. 覆盖邻近变量
    // 3. 覆盖返回地址(最危险!→ 安全漏洞的基础)
 
    int secret = 42;
    int buffer[4] = {0};
    // buffer[-1] 可能访问到 secret(取决于栈布局)
    printf("secret 附近的内存: buffer[-1] = %d\n", buffer[-1]);
 
    return 0;
}

4.2 缓冲区溢出是安全漏洞的根源

#include <stdio.h>
#include <string.h>
 
// 经典的安全漏洞示例(仅作教学用途)
void vulnerable(char *input) {
    char buffer[8];  // 只有 8 字节的缓冲区
    strcpy(buffer, input);  // 如果 input 长于 8 字节 → 溢出!
    printf("buffer: %s\n", buffer);
    // 溢出的数据会覆盖栈上的其他内容(包括返回地址!)
}
 
int main() {
    // safe: "hello" 只有 6 字节(含 \0)
    vulnerable("hello");
 
    // 危险!这个输入如果被攻击者控制,可能覆盖返回地址
    // 导致任意代码执行(buffer overflow exploit)
    // vulnerable("AAAAAAAAAAAAAAAAAAAAAAAA");
 
    return 0;
}
graph TD
    subgraph "栈布局(正常的函数栈帧)"
        direction TB
        S1["低地址<br/>buffer#91;8#93;: 8 字节"]
        S2["saved rbp: 8 字节"]
        S3["返回地址: 8 字节"]
        S4["高地址<br/>调用者的栈帧"]
    end
    S1 --> S2 --> S3 --> S4
    
    subgraph "溢出后的栈布局"
        direction TB
        O1["低地址<br/>buffer#91;0#93;"]
        O2["AAAAAAAA  ← 覆盖 buffer"]
        O3["AAAAAAAA  ← 覆盖 saved rbp"]
        O4["攻击者地址 ← 覆盖 返回地址!"]
        O5["高地址"]
    end
    O1 --> O2 --> O3 --> O4 --> O5

缓冲区溢出是 C 语言最著名的安全问题,也是 Rust 语言设计的核心动力之一。安全编程实践见本教程第二部分;Rust 的内存安全方案见 。

小节练习

选择题 1

C 语言中访问数组越界时会发生什么?

  • A. 编译错误

  • B. 运行时异常(如 Java 的 ArrayIndexOutOfBoundsException)

  • C. 未定义行为(UB)——结果不可预测

  • D. 自动返回 0


第五节:数组在汇编层的寻址


5.1 数组寻址的汇编指令

// C 代码
int sum_array(int arr[], int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

对应 x86-64 汇编(-O2 优化,简化版):

sum_array:
    testl   %esi, %esi       ; 检查 n 是否 ≤ 0
    jle     .L_ret_zero
    leal    -1(%rsi), %eax   ; eax = n - 1
    leaq    4(%rdi,%rax,4), %rdx  ; 计算结束地址
    movl    $0, %eax         ; sum = 0
.L_loop:
    addl    (%rdi), %eax     ; sum += *rdi (arr[i])
    addq    $4, %rdi         ; rdi += 4 (指针前移一个 int)
    cmpq    %rdx, %rdi       ; 是否已到数组末尾?
    jne     .L_loop
    ret
.L_ret_zero:
    xorl    %eax, %eax
    ret

关键汇编寻址模式

寻址模式示例含义C 等效
基址+偏移mov -8(%rbp), %eax访问 rbp - 8 处的值局部变量
基址+索引mov (%rdi,%rax,4), %edx访问 rdi + rax×4arr[i]
基址+索引+偏移mov 16(%rdi,%rax,8), %ecx访问 rdi + rax×8 + 16struct_arr[i].field
; arr[i] 的寻址:
; arr 在 rdi 中,i 在 rax 中
movl    (%rdi, %rax, 4), %eax   ; eax = arr[i]
;                ^^^^
;                比例因子 = sizeof(int)
 
; 等价于: eax = *(arr + i * 4)
 
; arr[i][j] 的寻址(假设每行 4 列,即 col=4):
; arr 在 rdi 中,i 在 rax 中,j 在 rcx 中
; 地址 = arr + (i * 4 + j) * 4 = arr + i * 16 + j * 4
leal    (%rcx, %rax, 4), %edx   ; edx = (i * 4 + j)   ; 注意:这里 i*4 因为 col=4
movl    (%rdi, %rdx, 4), %eax   ; eax = arr[i][j]

数组寻址的本质是通过 Scale-Index-Base(SIB)字节编码实现的高效地址计算。详细汇编寻址模式见 。

5.2 数组在循环中的栈遍历

// C 代码
int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += arr[i];
    }
    return sum;
}

栈帧布局(-O0,简化):

        ┌─────────────────────┐  高地址
        │   返回地址           │  rbp + 8
        ├─────────────────────┤
rbp →   │   旧的 rbp           │
        ├─────────────────────┤
        │   arr[4] = 50       │  rbp - 4
        │   arr[3] = 40       │  rbp - 8
        │   arr[2] = 30       │  rbp - 12
        │   arr[1] = 20       │  rbp - 16
        │   arr[0] = 10       │  rbp - 20
        ├─────────────────────┤
        │   sum = 0           │  rbp - 24
        ├─────────────────────┤
        │   i = 0             │  rbp - 28
        ├─────────────────────┤
rsp →   │   (低地址)          │
        └─────────────────────┘

小节练习

选择题 1

在 x86-64 汇编中,movl (%rdi,%rax,4), %eax 这条指令的 C 语言等价代码是?

  • A. eax = rdi + rax * 4;

  • B. eax = *(int*)(rdi + rax * 4);

  • C. eax = rdi[rax];

  • D. eax = &rdi[rax];


章节测试

一、判断题(正确选,错误选)

判断题 1

int arr[10]; 声明中,arr 本身是一个可修改的指针变量。 ( )

  • 正确

  • 错误

判断题 2

arr[i]*(arr + i) 是完全等价的。 ( )

  • 正确

  • 错误

判断题 3

C 语言运行时会自动检查数组越界。 ( )

  • 正确

  • 错误

判断题 4

二维数组 int arr[3][4] 在内存中按行主序(row-major)存储。 ( )

  • 正确

  • 错误

判断题 5

int arr[5] = {0}; 只把 arr[0] 初始化为 0,其他元素未初始化。 ( )

  • 正确

  • 错误

判断题 6

VLA(变长数组)可以用于声明全局变量。 ( )

  • 正确

  • 错误

判断题 7

多维数组作为函数参数时,除第一维外,其他所有维度大小必须指定。 ( )

  • 正确

  • 错误

判断题 8

sizeof(arr) 在函数内部能正确计算作为参数传递的数组的大小。 ( )

  • 正确

  • 错误

判断题 9

C 语言中 int w[10];sizeof(w) 返回 10 * sizeof(int),即 40(假设 int=4)。 ( )

  • 正确

  • 错误

判断题 10

数组在复制时(如 arr2 = arr1)会将所有元素复制到新数组。 ( )

  • 正确

  • 错误


二、选择题(单项选择题)

选择题 1

int arr[3][4]arr[1][2] 的地址偏移是?

  • A. arr + (1 + 2) * 4

  • B. arr + (1 * 3 + 2) * 4

  • C. arr + (1 * 4 + 2) * 4

  • D. arr + (2 * 3 + 1) * 4

选择题 2

以下哪个声明合法?

  • A. int arr[];(全局)

  • B. int arr[0];(全局)

  • C. int arr[] = {1,2,3};(全局)

  • D. int arr[.5];

选择题 3

以下代码输出什么?

int arr[] = {10, 20, 30, 40};
int *p = arr;
printf("%d\n", *(p + 2));
  • A. 10

  • B. 20

  • C. 30

  • D. 40

选择题 4

以下关于 VLA 的说法正确的是?

  • A. VLA 在 C89 标准中就已存在

  • B. VLA 的内存来自堆

  • C. VLA 的大小可以在运行时确定

  • D. VLA 可以用初始化列表 = {0}

选择题 5

数组越界写入最严重的后果是?

  • A. 编译错误

  • B. 运行时打印错误信息

  • C. 覆盖返回地址导致任意代码执行

  • D. 自动被操作系统阻止

选择题 6

int arr[3][4][5] 在内存中元素个数是?

  • A. 12

  • B. 20

  • C. 35

  • D. 60

选择题 7

以下数组中哪个不能自动推导长度?

  • A. int a[] = {1, 2, 3};

  • B. int b[][3] = {{1,2,3}, {4,5,6}};

  • C. int c[][3] = {};

  • D. int d[3][] = {{1,2}, {3,4}};

选择题 8

int arr[5]; printf("%zu", sizeof arr / sizeof arr[0]); 输出什么?

  • A. 4

  • B. 5

  • C. 20

  • D. 不确定

选择题 9

以下哪项是正确的三维数组 cube[2][3][4] 的内存偏移公式?

  • A. cube + (l * 3 + r * 2 + c) * 4

  • B. cube + (l * 12 + r * 4 + c) * 4

  • C. cube + (l + r + c) * 4

  • D. cube + (l * 3 * 4 + r * 4 + c) * 4

选择题 10

在 x86-64 汇编中访问 arr[i] 时,比例因子 scale 的值取决于什么?

  • A. 数组的首地址

  • B. 索引 i 的值

  • C. 数组元素的 sizeof(元素类型大小)

  • D. 数组长度


️ 动手练习题

练习题 1:矩阵转置

难度:

编写程序对 N×M 矩阵进行转置(行列互换):

  • 输入矩阵大小和元素
  • 输出转置后的矩阵(M×N)
  • 必须在原矩阵上”原地”转置(或使用额外数组)
  • 观察转置前后内存中的元素排列变化

力扣练习:力扣矩阵操作题

练习题 2:二维数组的综合操作

难度:

编写一个对学生成绩表的管理程序:

  • 使用二维数组存储 5 个学生的 4 门课成绩
  • 计算每个学生的总分和平均分
  • 计算每门课的最高分、最低分、平均分
  • 按总分排序(需要交换整行数据)

力扣练习:

  • 力扣数组比较题
  • 力扣多维数组操作题

练习题 3:杨辉三角

难度:

用二维数组生成并打印杨辉三角的前 N 行:

  • 使用 VLA 根据输入的 N 动态分配
  • 或者使用固定大小 + 限制 N ≤ 最大大小
  • 观察每一行的对称性
  • 使用 %6d 格式化输出使其美观

力扣练习:力扣杨辉三角题

练习题 4:汇编层数组访问

难度:

编写以下函数并用 gcc -S -O2 生成汇编:

  1. int sum_arr(int *arr, int n) — 指针遍历累加
  2. int sum_arr_idx(int arr[], int n) — 下标遍历累加
  3. int sum_matrix(int rows, int cols, int mat[][4]) — 二维数组累加

比较汇编代码:

  • 下标遍历和指针遍历的汇编是否相同?
  • 观察二维数组的地址计算指令(lea + SIB)
  • 验证 sizeof 被编译为立即数

参考 。

练习题 5:矩阵乘法

难度:

力扣练习:力扣矩阵运算题

用 C 语言实现后,额外实验:

  1. 实现三种循环顺序(i-j-k, i-k-j, j-i-k)的矩阵乘法
  2. clock() 计时,对 500×500 矩阵比较三种顺序的性能差异
  3. 解释为什么循环顺序影响性能(缓存局部性 / cache locality)
  4. 尝试使用 -O2 编译,观察编译器是否自动变换循环顺序