C语言数组专题:从一维到二维,吃透内存与指针

数组是 C 语言最核心的基础知识点,二维数组更是衔接一维数组、指针与函数的关键枢纽。本文由浅入深梳理一维到二维数组完整知识点,并总结高频易错点,帮你彻底学懂学透。


1. 一维数组(基础)

1.1 什么是一维数组

一维数组是C语言中最基础的线性数据结构,它是相同类型数据的连续内存集合

cpp 复制代码
//语法:类型 数组名[数组长度];
int arr[5]; // 定义了一个能存放5个int数据的数组

1.2 一维数组的初始化

  • 完全初始化:int arr[5] = {1,2,3,4,5};
  • 部分初始化:int arr[5] = {1,2}; // 未初始化元素自动为0
  • 省略长度初始化:int arr[] = {1,2,3,4,5}; // 长度由初始化列表决定

1.3 一维数组的访问与遍历

数组元素通过下标(索引)访问,下标从0开始:

cpp 复制代码
int arr[5] = {1,2,3,4,5};
// 访问元素
printf("%d\n", arr[0]); // 输出1
// 遍历数组
for(int i=0; i<5; i++){
    printf("%d ", arr[i]);
}

1.4 一维数组的内存存储

一维数组在内存中是连续存储的,数组名arr是数组首元素的地址:

cpp 复制代码
printf("%p\n", arr);    // 数组首地址
printf("%p\n", &arr[0]);// 和arr的值完全相同
// 元素地址连续
printf("%p\n", &arr[0]);
printf("%p\n", &arr[1]); // 地址相差一个int的大小(4字节)

2. 二维数组

二维数组可以理解为"数组的数组",即一个数组的每个元素又是一个一维数组。

2.1 定义与初始化

2.1.1 定义语法
cpp 复制代码
// 类型 数组名[行数][列数];
int arr[3][4]; // 3行4列的二维数组,可存放12个int数据
  • 行数:表示有多少个一维数组
  • 列数:每个一维数组的长度
2.1.2 初始化方式
  1. 完全初始化(分行初始化)
    int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
    2. 连续初始化(按行填充)
    int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
    3. 部分初始化

// 只初始化前两行,第三行自动补0
int arr[3][4] = {{1,2}, {3,4,5}};
4.省略行数初始化
// 行数由初始化列表的元素个数和列数决定
int arr[][4] = {1,2,3,4,5,6,7,8}; // 自动识别为2行4列

2.2 下标访问与遍历

2.2.1 下标访问

二维数组元素通过**数组名[行标][列标]**访问,行标和列标都从0开始:

cpp 复制代码
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
printf("%d\n", arr[0][0]); // 输出第一行第一列的元素1
printf("%d\n", arr[2][3]); // 输出第三行第四列的元素12
2.2.2 遍历二维数组

最常用的方式是双层循环

cpp 复制代码
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
// 外层循环控制行,内层循环控制列
for(int i=0; i<3; i++){
    for(int j=0; j<4; j++){
        printf("%d ", arr[i][j]);
    }
    printf("\n"); // 每一行结束换行
}

2.3 内存存储(行优先)

二维数组在内存中并不是"二维"的,而是按行优先的顺序连续存储的。

2.3.1 行优先存储规则

存储顺序是:先存第0行的所有元素,再存第1行,再存第2行......
比如上面的arr[3][4],在内存中的存储顺序是:
arr[0][0] → arr[0][1] → arr[0][2] → arr[0][3] → arr[1][0] → ... → arr[2][3]

2.3.2 地址验证
cpp 复制代码
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
printf("arr[0]地址:%p\n", arr[0]);
printf("arr[1]地址:%p\n", arr[1]); // 比arr[0]大16(4个int)
printf("arr[2]地址:%p\n", arr[2]); // 比arr[1]大16
printf("arr[0][0]地址:%p\n", &arr[0][0]);
printf("arr[0][1]地址:%p\n", &arr[0][1]); // 比arr[0][0]大4

3. 指针与二维数组(核心)

二维数组和指针的关系,是C语言的重点和难点,核心在于理解行指针和数组名的含义。

3.1 二维数组名的含义

  • arr: 二维数组名,是指向第一行的指针 ,类型为int (*)[4](指向包含4个int元素的一维数组的指针)
  • arr[0]: 二维数组第0行的数组名,是第0行首元素的地址 ,类型为int*
  • &arr:整个二维数组的地址,类型为int (*)[3][4]

3.2 下标与指针的等价关系

二维数组的下标访问,本质上是指针运算:

// 以下写法完全等价
arr[i][j]
*(arr[i] + j)
*(*(arr + i) + j)
理解:

  1. arr + i:指向第i行的地址(行指针运算,每次移动一整行的大小)
  2. *(arr + i):等价于arr[i],得到第i行的首元素地址
  3. *(arr + i) + j:第i行第j个元素的地址(普通指针运算,每次移动一个int的大小)
  4. *(*(arr + i) + j):解引用得到第i行第j个元素的值

3.3 行指针的定义与使用

行指针是专门用来指向二维数组某一行的指针:

cpp 复制代码
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
// 定义一个指向包含4个int元素的一维数组的指针(行指针)
int (*p)[4] = arr;

// 通过行指针访问元素
printf("%d\n", *(*(p + 1) + 2)); // 等价于arr[1][2],输出7

4. 二维数组作函数参数

在函数中处理二维数组时,有三种常见的写法,本质上都是传递行指针

4.1 三种传参方式

方式1:完整写法(明确行列数)
cpp 复制代码
void printArr(int arr[3][4], int row, int col){
    for(int i=0; i<row; i++){
        for(int j=0; j<col; j++){
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
// 调用
int main(){
    int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
    printArr(arr, 3, 4);
    return 0;
}
方式2:省略行数(必须指定列数)

函数参数中二维数组的行数可以省略 ,但列数必须指定

cpp 复制代码
void printArr(int arr[][4], int row, int col){
    // 函数体和上面一样
}
方式3:行指针写法(最本质)
cpp 复制代码
void printArr(int (*arr)[4], int row, int col){
    // 函数体和上面一样
}

4.2 注意事项

  • 为什么列数必须指定?因为编译器需要知道每行的大小,才能正确计算行指针的偏移量
  • 函数内部不能用sizeof(arr)/sizeof(arr[0])计算行数,因为arr此时是一个指针,不是数组

5. 二维字符数组(字符串数组)

二维字符数组,本质上是字符串的数组,常用于存储多个字符串。

5.1 定义与初始化

cpp 复制代码
// 定义一个能存放3个字符串,每个字符串最大长度为10的数组
char names[3][10] = {
    "zhangsan",
    "lisi",
    "wangwu"
};

等价于:

cpp 复制代码
char names[3][10] = {
    {'z','h','a','n','g','s','a','n','\0'},
    {'l','i','s','i','\0'},
    {'w','a','n','g','w','u','\0'}
};

5.2 访问与遍历字符串数组

cpp 复制代码
// 访问单个字符串
printf("%s\n", names[0]); // 输出zhangsan
// 遍历所有字符串
for(int i=0; i<3; i++){
    printf("%s\n", names[i]);
}

5.3 二维字符数组 vs 字符指针数组

|--------|---------------------|----------------------------|------------------|
| 类型 | 定义 | 内存特点 | 适用场景 |
| 二维字符数组 | char arr[3][10] | 所有字符串连续存储,每个字符串长度固定 | 字符串长度固定,可修改字符串内容 |
| 字符指针数组 | char *arr[3] | 存储的是字符串的地址,字符串本身存储在常量区 | 字符串长度不固定,只读字符串 |


6. 动态二维数组(拓展)

前面的二维数组都是静态数组,大小在编译时确定。如果需要在运行时确定大小,可以使用动态二维数组。

6.1 方式1:用一维数组模拟二维数组

cpp 复制代码
int rows = 3, cols = 4;
// 申请连续的内存
int *arr = (int*)malloc(rows * cols * sizeof(int));
// 访问元素 arr[i][j] → arr[i*cols + j]
for(int i=0; i<rows; i++){
    for(int j=0; j<cols; j++){
        arr[i*cols + j] = i*cols + j;
    }
}
// 使用完释放
free(arr);

6.2 方式2:指针数组实现动态二维数组

cpp 复制代码
int rows = 3, cols = 4;
// 1. 先申请行指针数组
int **arr = (int**)malloc(rows * sizeof(int*));
// 2. 为每一行申请内存
for(int i=0; i<rows; i++){
    arr[i] = (int*)malloc(cols * sizeof(int));
}
// 访问元素,和静态二维数组一样 arr[i][j] = ...
// 使用完释放,顺序和申请相反
for(int i=0; i<rows; i++){
    free(arr[i]);
}
free(arr);

6.3 方式3:用行指针实现连续存储的动态二维数组

cpp 复制代码
int rows = 3, cols = 4;
// 申请一整块连续内存
int (*arr)[cols] = (int(*)[cols])malloc(rows * cols * sizeof(int));
// 访问元素 arr[i][j] = ...,和静态二维数组完全一样
// 释放
free(arr);

7. 数组高频易错点总结

7.1 一维数组常见易错点

易错1:下标越界

C语言不做数组下标越界检查,编译不报错,运行随机崩溃、乱码、篡改其他变量。
int arr[3] = {1,2,3}; arr[5] = 99; // 严重越界,编译器不报警,运行隐患极大
避坑:循环边界严格写成 i < 数组长度,不要写成 i <= 长度。


易错2:数组名不能整体赋值

数组名是地址常量,不能直接赋值、不能自增。
int a[5], b[5]; a = b; // 错误 a++; // 错误
避坑:只能循环逐个元素赋值,或用 memcpy 内存拷贝。


易错3:函数内用sizeof求数组长度

数组传参退化为指针,sizeof得不到真实数组大小。
void fun(int arr[]) { // 永远是指针大小 4/8字节,得不到元素个数 int n = sizeof(arr) / sizeof(arr[0]); }
避坑:手动把长度当参数传入函数。


易错4:部分初始化默认补0只针对全局/局部数组

int arr[5] = {1}; // 等价 {1,0,0,0,0},未显式初始化元素自动置0


7.2 二维数组核心易错点

易错1:初始化时省略列数

行数可以省,列数绝对不能省
int arr[][] = {1,2,3,4}; // 直接编译报错 int arr[][2] = {1,2,3,4}; // 正确,自动2行2列
原因:编译器需要列数计算每行内存偏移,没有列数无法寻址。


易错2:分不清 arr arr[0] &arr

int arr[3][4];

  • arr:行指针,int (*)[4],+1 跳过一整行
  • arr[0]:首行首元素地址,int *,+1 跳过一个int
  • &arr:整个二维数组地址,int (*)[3][4]
    三者数值相同,类型和步长完全不同,是面试高频坑。

易错3:把二维数组传给函数随意省略列

void print(int arr[][]) // 错误
void print(int arr[][4]) void print(int (*arr)[4]) // 正确


易错4:混淆行指针与二级指针

int **p 不能直接接收 二维数组名
int arr[3][4]; int **p = arr; // 类型不匹配,编译警告,运行崩溃
正确接收只能用:int (*p)[4] = arr;


易错5:双层循环遍历颠倒行列

外行循环控制列、内层控制行,逻辑全乱,打印排版错乱。
固定规则:外层i行,内层j列。


7.3 二维字符数组 易错坑

易错1:二维字符数组与字符指针数组混用

char strs[3][10] = {"abc","123","xyz"}; char *p[3] = {"abc","123","xyz"};

  • strs[3][10]:内存可修改,每个字符串有固定空间
  • char *p[3]:指向字符串常量区,不能修改内容
    p[0][0] = 'A'; // 运行崩溃,常量区不可写

易错2:字符串不手动补 \0

逐字符赋值时,不主动加 \0,printf 打印乱码、越界乱走。
char s[10];
s[0] = 'a';
s[1] = 'b'; // 少了 s[2] = '\0'; 直接printf乱码、


7.4 动态二维数组 易错坑

易错1:malloc后不判空

内存申请失败返回NULL,直接使用程序崩溃。

cpp 复制代码
int **arr = (int**)malloc(n * sizeof(int*));
if(arr == NULL)
{
    perror("malloc fail");
    return -1;
}

易错2:内存释放顺序颠倒、漏释放

申请:先申请行指针数组 → 再逐行申请
释放:必须先逐行free → 再free行指针数组
顺序错、少free都会内存泄漏。


易错3:用一维模拟二维时下标算错

公式:i行j列 = i * 列数 + j
容易写成 i * 行数 + j,全部错位。

7.5 面试常考总结一句话

  1. 一维数组名是首元素地址,二维数组名是行地址
  2. 二维数组传参列数必固定,只能用行指针接收;
  3. 数组传参变指针,函数内不能用sizeof求长度;
  4. int** 不等于 二维数组指针,绝对不能互相赋值;
  5. 字符指针数组指向常量字符串,不可修改内容

总结

  1. 二维数组本质是"数组的数组",内存按行优先连续存储
  2. 数组名是行指针,arr[i][j]等价于*(*(arr+i)+j)
  3. 二维数组作函数参数时,列数必须指定,本质传递的是行指针
  4. 二维字符数组适合存储多个字符串,注意和字符指针数组的区别
  5. 动态二维数组有多种实现方式,根据场景选择合适的方式
相关推荐
Andya_net4 小时前
Spring | 深度剖析Spring Bean的生命周期:从加载到销毁的完整流程
java·spring·rpc
玛卡巴卡ldf4 小时前
【Springboot升级AI】(大模型部署)LangChain4j、会话记忆、隔离消失持久化问题、ollama、RAG知识库、Tools工具
java·开发语言·人工智能·spring boot·后端·springboot
Maiko Star4 小时前
Spring AI 多轮对话记忆(ChatMemory)保姆级教程:从内存版到 Redis 持久化
java·redis·spring
tjl521314_214 小时前
01C++ 类定义与访问控制(封装)
java·开发语言·c++
木木_王4 小时前
嵌入式Linux学习 | 数据结构 (Day04)链表升级(进阶优化 + 柔性数组原理 + 双向循环链表完整实现 + 高频面试深挖)
linux·数据结构·学习
无籽西瓜a4 小时前
【西瓜带你学Kafka | 第七期】Kafka 日志存储体系:保留清理、消息格式与分段刷新策略(文含图解)
java·分布式·后端·kafka·消息队列·mq
空中海4 小时前
第四章:Maven专家篇 — 企业级实践与 CI/CD 集成
java·maven
lifewange4 小时前
CNode API v1 完整接口文档(JSON 规范整理)
java·前端·json
阿Y加油吧4 小时前
二刷 LeetCode:152. 乘积最大子数组 & 416. 分割等和子集 复盘笔记
笔记·算法·leetcode