深入详解C语言数组:承上启下——从C语言数组基础到数据结构衔接
🔥个人主页:艾莉丝努力练剑
❄专栏传送门:《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题、洛谷刷题、C/C++基础知识知识强化补充、C/C++干货分享&学习过程记录
🍉学习方向:C/C++方向
⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平
前言:数组是C语言中最基础也是最最重要的数据结构,理解其内存布局和操作特性是学习更复杂数据结构的基础。从一维数组到二维数组,再到变长数组,C语言提供了不同层次的数组支持。虽然数组有局限性,但其简单高效的特性和顺序存储的特点,使其在适合的场景下仍然是首选的数据组织方式。
目录
一、一维数组:最基础的数据容器
1.1 定义与初始化
1.2 内存布局
1.3 数组与指针的关系
二、二维数组:表格型数据存储
2.1 定义与初始化
2.2 内存布局
2.3 访问方式
三、变长数组(VLA):C99引入的灵活特性
3.0 变长数组(VLA)到来前的C语言创建数组BeLike:
3.1 什么是变长数组(VLA)
3.2 变长数组(VLA)的代码演示
3.3 主要的注意事项
3.4 总结一下变长数组(VLA)
四、数组应用:二分查找算法
4.1 算法特性
4.2 代码演示
五、承上启下:衔接C语言数组与数据结构
5.1 顺序表(动态数组)
5.2 栈(数组实现,链表用的比较少)
5.3 顺序存储的二叉树
六、数组的局限性
C++的两个参考文档:
老朋友(非官方文档):cplusplus
官方文档(同步更新):cppreference
掌握数组:一维数组、二维数组、变长数组及简单的二分查找
一、一维数组:最基础的数据容器
一维数组概念:一维数组是C语言中最简单的数据结构,是一组相同类型元素的线性集合。
1.1 定义与初始化
关键点:
(1)数组长度必须是编译期常量(C99标准之前);
(2)数组下标从0开始;
(3)数组名在大多数情况下代表数组首元素的地址。
代码演示:
// 定义方式:类型 数组名[元素个数];
int scores[5]; // 声明一个包含5个整数的数组// 初始化方式
int primes[5] = {2, 3, 5, 7, 11}; // 完全初始化
int evens[10] = {2, 4, 6}; // 部分初始化,剩余元素自动初始化为0
int arr[] = {1, 2, 3}; // 省略长度,编译器自动计算
1.2 内存布局
内存布局:一维数组在内存中是连续存储的,每个元素占用相同大小的内存空间。
例如 int arr[3] 在32位系统(x86环境)中占用连续的12字节(3×4字节)。
1.3 数组与指针的关系
数组与指针关系的重要区别:
sizeof(arr) 返回整个数组的字节大小;
sizeof(p) 返回指针变量的大小(4个字节或8个字节)。
数组与指针的关系相关的代码演示:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 &arr[0]// 以下表达式等价
arr[2] == *(arr + 2) == *(p + 2) == p[2]
二、二维数组:表格型数据存储
引言:二维数组可以看作是"数组的数组",常用于表示矩阵或表格数据。
2.1 定义与初始化
初始化代码演示:
// 定义方式:类型 数组名[行数][列数];
int matrix[3][4]; // 3行4列的矩阵// 初始化方式
int grid[2][3] =
{{1, 2, 3}, // 第一行{4, 5, 6} // 第二行
};// 也可以连续初始化
int grid2[2][3] = {1, 2, 3, 4, 5, 6};
2.2 内存布局
内存布局:二维数组在内存中仍然是线性连续存储的,按行优先顺序排列。
比如 grid[2][3] 的内存布局为:
grid[0][0], grid[0][1], grid[0][2], grid[1][0], grid[1][1], grid[1][2]
如下图所示:
2.3 访问方式
代码演示:
// 常规访问
int val = grid[1][2]; // 获取第2行第3列元素// 指针方式访问
int val = *(*(grid + 1) + 2); // 等价于grid[1][2]
三、变长数组(VLA):C99引入的灵活特性
3.0 变长数组(VLA)到来前的C语言创建数组BeLike:
在C99标准之前,C语言在创建数组的时候,数组大小的指定只能使用常量、常量表达式,或者如果我们初始化数据的话,可以省略数组大小。
这样的语法限制,让我们创建数组就不够灵活,有时候数组大了浪费空间,有时候数组又小了不够用的。 这样能爽吗?不爽!如此受限制,跟这群虫豸语法在一起,怎么能搞好数组呢?
3.1 什么是变长数组(VLA)
概念:变长数组(Variable Length Array)允许使用变量指定数组长度。
3.2 变长数组(VLA)的代码演示
代码演示:
int n;
printf("请输入数组大小:");
scanf("%d", &n);int vla[n]; // 变长数组
3.3 主要的注意事项
这里我们应当注意以下几点:
1、VLA(变长数组)只能是局部变量,不能是全局变量或static变量;
2、VLA(变长数组)不能初始化;
3、内存分配在栈上,大数组可能导致栈溢出;
4、C11标准中VLA(变长数组)变为可选特性。
3.4 总结一下变长数组(VLA)
四、数组应用:二分查找算法
二分查找算法:二分查找是数组的经典应用,要求数组必须有序。
4.1 算法特性
二分查找算法的算法特性——
(1)时间复杂度:O(log n);
(2)空间复杂度:O(1);
(3)仅适用于有序数组。
4.2 代码演示
既然我们已经知道了二分查找算法的算法特性,就来通过一个实际的例子来感受一下二分查找吧!
int binary_search(int arr[], int size, int target)
{int left = 0;int right = size - 1;while (left <= right) {int mid = left + (right - left) / 2; // 防止溢出if (arr[mid] == target) {return mid;} else if (arr[mid] < target) {left = mid + 1;} else {right = mid - 1;}}return -1; // 未找到
}
五、承上启下:衔接C语言数组与数据结构
5.1 顺序表(动态数组)
顺序表是数组的扩展,可以动态增长——
typedef struct {int *data; // 指向动态数组的指针int capacity; // 当前容量int size; // 当前元素数量
} SeqList;void init(SeqList *list, int cap) {list->data = (int *)malloc(cap * sizeof(int));list->capacity = cap;list->size = 0;
}void append(SeqList *list, int value) {if (list->size >= list->capacity) {// 扩容操作list->capacity *= 2;list->data = (int *)realloc(list->data, list->capacity * sizeof(int));}list->data[list->size++] = value;
}
5.2 栈(数组实现,链表用的比较少)
栈的数组实现利用数组的尾部作为栈顶:
#define MAX_SIZE 100typedef struct {int data[MAX_SIZE];int top; // 栈顶指针
} ArrayStack;void push(ArrayStack *stack, int value) {if (stack->top < MAX_SIZE - 1) {stack->data[++stack->top] = value;}
}int pop(ArrayStack *stack) {if (stack->top >= 0) {return stack->data[stack->top--];}return -1; // 栈空
}
5.3 顺序存储的二叉树
完全二叉树可以用数组顺序存储——
// 对于索引为i的节点:
// 父节点:(i-1)/2
// 左孩子:2*i + 1
// 右孩子:2*i + 2int tree[7] = {1, 2, 3, 4, 5, 6, 7}; // 表示一个完全二叉树
六、数组的局限性
数组不可能是万能的,它主要存在以下几点不足——
(1)固定大小:传统数组大小在编译时确定(VLA【变长数组】除外);
(2)插入删除效率低:平均需要移动O(n)个元素;
(3)内存浪费或不足:静态分配难以精确控制。
正是这些局限性促使我们学习更高级的一些数据结构,比如链表、树等。
往期回顾:
深入详解C语言的循环结构:while循环、do-while循环、for循环,结合实例,讲透C语言的循环结构
掌握数组:一维数组、二维数组、变长数组及简单的二分查找
结语:本文内容到这里就全部结束了,非常感谢大家的阅读!不要忘记给博主“一键四连”哦!