小熊奶糖(BearCandy)
小熊奶糖(BearCandy)
发布于 2025-10-25 / 4 阅读
0
0

二维数组的指针表示法 - 完整笔记

基本概念

数组定义

int A[3][4];  // 3行4列的二维数组

类型分析

  • A:二维数组名,类型为 int (*)[4](指向包含4个整数的数组的指针)
  • A[i]:第i行,类型为 int *(指向整数的指针)
  • A[i][j]:具体元素,类型为 int

正确的指针表示法

元素访问对照表

数组表示法 指针表示法 说明
A[0][0] *(*A) 第一行第一个元素 ✓
A[0][1] *(*A + 1) 第一行第二个元素 ✓
A[0][2] *(*A + 2) 第一行第三个元素 ✓
A[1][0] *(*(A + 1)) 第二行第一个元素 ✓
A[1][1] *(*(A + 1) + 1) 第二行第二个元素 ✓
A[i][j] *(*(A + i) + j) 通用公式 ✓

行指针对照表

表达式 等价形式 类型 含义
A[0] *A int * 第一行首地址
A[1] *(A + 1) int * 第二行首地址
A[i] *(A + i) int * 第i行首地址

常见错误分析

❌ 错误写法

A[0][1] = *(A + 1);  // 错误!

✅ 正确写法

A[0][1] = *(*A + 1);  // 正确

错误原因分析

  • A + 1:跳过一行(前进 4 × sizeof(int) 字节)
  • *(A + 1):得到第二行 A[1],而不是 A[0][1]
  • *(A + 1)A[1](指向第二行的指针)

内存布局理解

A → [A[0][0]][A[0][1]][A[0][2]][A[0][3]]  // 第一行
    [A[1][0]][A[1][1]][A[1][2]][A[1][3]]  // 第二行  
    [A[2][0]][A[2][1]][A[2][2]][A[2][3]]  // 第三行

指针运算规则

  • A + i:移动 i 行
  • *A + j:在第0行内移动 j 个元素
  • *(A + i) + j:在第i行内移动 j 个元素

记忆技巧

口诀

"外星(行)内存(列)"

  • 外层 * 对应行索引
  • 内层 * 对应列索引

推导步骤

  1. 确定行:A + i → 指向第i行
  2. 解引用行:*(A + i) → 得到第i行首地址
  3. 列偏移:*(A + i) + j → 指向第i行第j列
  4. 最终解引用:*(*(A + i) + j) → 得到元素值

实例验证

#include <stdio.h>

int main() {
    int A[2][3] = {{1, 2, 3}, {4, 5, 6}};
  
    // 正确示例
    printf("A[0][0] = %d, *(*A) = %d\n", A[0][0], *(*A));           // 1
    printf("A[0][1] = %d, *(*A + 1) = %d\n", A[0][1], *(*A + 1));   // 2
    printf("A[1][0] = %d, *(*(A + 1)) = %d\n", A[1][0], *(*(A + 1))); // 4
  
    // 错误示例对比
    printf("*(A + 1) = %p, A[1] = %p\n", *(A + 1), A[1]);  // 相同地址
    printf("*(A + 1)指向: %d (A[1][0])\n", **(A + 1));     // 4
  
    return 0;
}

总结

  • A[0][0]*(*A)
  • A[0][1]*(*A + 1)
  • A[i][j]*(*(A + i) + j)

关键:理解二维数组在内存中是按行存储的,指针运算时要分清行指针和列指针的差异。

二维数组内存布局 - 详细解析

核心结论

二维数组在内存中是连续存储的! 你的理解是正确的。

内存布局验证

示例代码

#include <stdio.h>

int main() {
    int A[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
  
    // 验证内存连续性
    printf("内存地址验证:\n");
    for(int i = 0; i < 3; i++) {
        for(int j = 0; j < 4; j++) {
            printf("&A[%d][%d] = %p, value = %2d\n", 
                   i, j, &A[i][j], A[i][j]);
        }
    }
  
    return 0;
}

典型输出结果

&A[0][0] = 0x7ffeeb0d6a00, value =  1
&A[0][1] = 0x7ffeeb0d6a04, value =  2  ← 相差4字节(int大小)
&A[0][2] = 0x7ffeeb0d6a08, value =  3
&A[0][3] = 0x7ffeeb0d6a0c, value =  4
&A[1][0] = 0x7ffeeb0d6a10, value =  5  ← 连续到第二行
&A[1][1] = 0x7ffeeb0d6a14, value =  6
&A[1][2] = 0x7ffeeb0d6a18, value =  7
&A[1][3] = 0x7ffeeb0d6a1c, value =  8
&A[2][0] = 0x7ffeeb0d6a20, value =  9  ← 连续到第三行
&A[2][1] = 0x7ffeeb0d6a24, value = 10
&A[2][2] = 0x7ffeeb0d6a28, value = 11
&A[2][3] = 0x7ffeeb0d6a2c, value = 12

内存布局图示

低地址 → [A[0][0]][A[0][1]][A[0][2]][A[0][3]][A[1][0]][A[1][1]][A[1][2]][A[1][3]][A[2][0]]... ← 高地址
         ↑第0行首           ↑第0行尾           ↑第1行首           ↑第1行尾           ↑第2行首
         连续存储!          连续存储!          连续存储!

为什么指针运算会有"行"的概念?

逻辑视图 vs 物理存储

  • 物理存储:绝对连续的内存块
  • 逻辑视图:编译器通过类型系统提供的抽象层次

类型系统的作用

int A[3][4];

// 不同的类型,相同的物理地址
printf("A     = %p (类型: int(*)[4])\n", A);      // 指向一行的指针
printf("A[0]  = %p (类型: int*)\n", A[0]);       // 指向一个元素的指针
printf("&A[0][0] = %p (类型: int*)\n", &A[0][0]);// 指向一个元素的指针

指针运算的差异

// 虽然地址相同,但指针运算不同
A + 1     // 前进一行的大小 (4 × sizeof(int) = 16字节)
A[0] + 1  // 前进一个元素的大小 (sizeof(int) = 4字节)
&A[0][0] + 1 // 前进一个元素的大小 (4字节)

连续性的实际应用

可以用一维方式遍历二维数组

// 方法1:传统二维遍历
for(int i = 0; i < 3; i++) {
    for(int j = 0; j < 4; j++) {
        printf("%d ", A[i][j]);
    }
}

// 方法2:利用连续性,一维遍历
int *p = &A[0][0];
for(int i = 0; i < 12; i++) {
    printf("%d ", p[i]);  // 或者 *(p + i)
}

内存拷贝

// 可以直接内存拷贝整个二维数组
int B[3][4];
memcpy(B, A, sizeof(A));  // 可行,因为内存连续

重要区别:数组 vs 指针数组

真正的二维数组(连续)

int A[3][4];  // 连续存储的12个int

指针数组(不连续)

int *B[3];    // 3个指针的数组
for(int i = 0; i < 3; i++) {
    B[i] = malloc(4 * sizeof(int));  // 每行可能在不同内存区域
}
// B[0][0], B[1][0] 可能不连续!

总结

  1. ✅ 二维数组在内存中绝对连续
  2. ✅ 所有元素按行优先顺序依次排列
  3. ✅ "行"的概念是类型系统的逻辑抽象,不是物理分隔
  4. ✅ 指针运算的差异源于类型信息,而非内存布局

你的理解完全正确!二维数组确实是连续存储的,指针表示法中的"行"概念是编译器为了类型安全而提供的抽象层。


评论