对于习惯使用高级语言编程的人来说,使用 C 语言编程最头痛的问题之一就是在使用数组需要事先确定数组长度。
C 语言本身不提供动态数组这种数据结构,本文将演示如何在 C 语言编程中实现一种对象来作为动态数组。
/* Author: [email protected] */
基本的 C 数组
C 语言编程中声明一个基本数组如下:
int main() { // 声明一个容纳 3000 个整数的数组 int my_array[3000]; }
以上代码做了两件事:
● 在栈区开辟内存空间。准确说来是在函数 main 的栈区空间开辟一个 3000 * sizeof(int) 个字节的内存空间。通过这种方式开辟的内存空间会在程序运行到当前区块终点时(对本例而言就是 main 函数的底部)被自动释放掉。
● 创建一个指针指向新开辟的内存区域,并将该指针赋给变量 my_array 保存。我们可以通过下标的方式来访问数组里的成员,例如 my_array[271] 可以访问到第 272 个成员。你也可以通过另一种方式来访问数组里的成员,即 *(my_array + 271)。
由此可以看出,C 语言的数组实质就是内存管理操作,下标索引只是一种语法糖。
C 语言的数组有两个雾区:
● 很难随着数据的增加自动扩大数组。事实是你可以使用 realloc 函数扩大开辟在堆区的数组大小,当然我们想要的是能自动调整大小的数组对象。
● 你可以索引到数组边界以外的区域。由于在 C 语言并不检查数组的边界,也就是说你的确可以访问数组边界以外区域的内存地址,例如 my_array[5000] 语法上是可行的。因为下标索引只是一种语法糖,它实际上所做的是从指针 my_array 开始向后移动 5000 次并读取它停在的那个内存地址所保存的数据。当你索引数据边界以外区域时相当于读取尚未分配的内存上的内容,但这不是你真的想要的,并且可能带来潜在的严重后果。
如果我们可以忍受一些速度和内存空间上的牺牲,那么我们可以通过实现某种数据结构作为所谓的 “动态数组”。本文我们将这种数据结构称为 Vector,但这种数据结构不能解决我们在操作数集时遇到的所有问题,它适合于向其中追加成员,但不适合做插入和删除操作,如果你需要大量的插入和删除操作,链表这种数据结构更能符合你的需求,但链表也有它的问题,我们就不在这里做过多讨论。
定义 Vector 对象
本文我们将创建一个容纳整数的 “动态数组”,让我们将这种数据结构命名为 Vector。首先我们使用一个头文件 vector.h 来定义数据结构 Vector:
// 首先定义一个常量,该常量表示 Vector 内部一个数组对象的初始大小。 #define VECTOR_INITIAL_CAPACITY 100 // 定义数据结构 Vector typedef struct { int size; // 数组在用长度 int capacity; // 数组最大可用长度 int *data; // 用来保存整数对象的数组对象 } Vector; // 该函数负责初始化一个 Vector 对象,初始数组在用长度为 0,最大长度为 VECTOR_INITIAL_CAPACITY。// 开辟适当的内存空间以供底层数组使用,空间大小为 vector->capacity * sizeof(int) 个字节。 void vector_init(Vector *vector); // 该函数负责追加整数型的成员到 vector 对象。如果底层的数组已满,则扩大底层数组容积来保存新成员。void vector_append(Vector *vector, int value); // 返回 vector 指定位置所保存的值。如果指定位置小于 0 或者大于 vector->size - 1,则返回异常。 int vector_get(Vector *vector, int index); // 将指定值保存到指定位置,如果指定位置大于 vector->size,则自动翻倍 vector 内部的数组容积直到可以容纳指定多的位置。// 扩大的数组中间使用 0 填满那些空位置。 void vector_set(Vector *vector, int index, int value); // 将 vector 内部数组容积翻倍。// 因为更改数组体积的开销是十分大的,采用翻倍的策略以免频繁更改数组体积。 void vector_double_capacity_if_full(Vector *vector); // 释放 vector 内部数组所使用的内存空间。 void vector_free(Vector *vector);
实现 Vector 对象
以下代码(vector.c)展示如何实现 Vector 数据结构:
#include <stdio.h> #include <stdlib.h> #include "vector.h" void vector_init(Vector *vector) { // 初始化 size 和 capacity。 vector->size = 0; vector->capacity = VECTOR_INITIAL_CAPACITY; // 为 vector 内部 data 数组对象申请内存空间 vector->data = malloc(sizeof(int) * vector->capacity); } void vector_append(Vector *vector, int value) { // 确保当前有足够的内存空间可用。 vector_double_capacity_if_full(vector); // 将整数追加到数组尾部。 vector->data[vector->size++] = value; } int vector_get(Vector *vector, int index) { if (index >= vector->size || index < 0) { printf("Index %d out of bounds for vector of size %d\n", index, vector->size); exit(1); } return vector->data[index]; } void vector_set(Vector *vector, int index, int value) { // 使用 0 填充闲置在用内存空间。 while (index >= vector->size) { vector_append(vector, 0); } // 在指定数组位置保存指定整数。 vector->data[index] = value; } void vector_double_capacity_if_full(Vector *vector) { if (vector->size >= vector->capacity) { // 翻倍数组大小。 vector->capacity *= 2; vector->data = realloc(vector->data, sizeof(int) * vector->capacity); } } void vector_free(Vector *vector) { free(vector->data); }
使用 Vector 对象
以下代码(vector-usage.c)展示如何使用 Vector 对象:
#include <stdio.h> #include "vector.h" int main() { // 声明一个新的 Vector 对象,并初始化它。 Vector vector; vector_init(&vector); // 初始化的 vector 内部数组最大保存 100 个整数。 // 现在我们将保存 150 个整数到 vector 对象中。 // vector 自动将内部数组容积扩大一倍达到最多可以保存 200 个整数,但实际只使用了 150 个位置。 int i; for (i = 200; i > 50; i--) { vector_append(&vector, i); } // 我们指定在第 251 个位置保存一个整数 99999。 // vector 自动再次翻倍内部数组容积到 400 个位置,并将 99999 放到第 251 个位置。 // 另外将第 151 到 250 之间所有的位置用 0 进行填充。 vector_set(&vector, 250, 99999); // 读取第 28 个位置的整数值,该位置的整数应该是 173。 printf("Heres the value at 27: %d\n", vector_get(&vector, 27)); // 遍历当前 vector 内部数组所有实际在用的位置。 for (i = 0; i < vector.size; i++) { printf("vector[%d] = %d\n", i, vector_get(&vector, i)); } // 释放 vector 对象内部数组。 vector_free(&vector); }
以上代码我们使用 Vector 这种数据结构来作为一个动态数组,一开始 Vector 大小(size)为 100 个整数容量,后来我们添加了 150 个整数,再后来我们又在第 251 个位置添加一个整数 99999。编译并运行以上代码:
$ gcc vector.c vector-usage.c $ ./a.out Heres the value at 27: 173 vector[0] = 200 vector[1] = 199 vector[2] = 198 ... vector[148] = 52 vector[149] = 51 vector[150] = 0 vector[151] = 0 ... vector[249] = 0 vector[250] = 99999
可以看到这个动态数组大小为 251 个整数容量(实际可以保存 400 个整数),第 28 个位置值为 173,中间一段位置使用了 0 填充,第 251 个位置值为 99999。
数据结构中的平衡艺术
本文展示了如何实现一种底层数据结构,通过理解底层的实现过程,你可以更好的理解一些高级语言的行为以及为什么它们会有某些速度瓶颈。
调整本文中的数据结构 Vector 内部的数组大小是一种开销很大的操作,因为它需要调用 realloc() 函数。realloc() 函数会调整指针指向的那片内存空间的大小,并返回一个指向调整后内存空间的指针。如果当前内存区域没有足够的剩余空间来扩展当前的内存空间,那么 realloc() 会开辟一片新的内存区域,并且将指针指向的旧内存空间内容复制到新的内存空间,然后释放旧的内存空间,然后返回新的内存空间指针。
所以如果我们遇到当前内存区域不够扩展我们的数组时,我们不得不进行开销很大的复制操作。为了减少这种情况出现的可能性,我们每次扩展内存空间时总是翻倍地开辟新的内存空间,这种策略带来的副作用就是可能会造成内存空间的浪费,这就是一种根据内存空间与速度之间的平衡。
另外本文实现的数据结构只能保存整数类型对象。如果我们数据结构中使用的数组保存指向空对象的指针而不是整数,那么我们就可以保存任意类型的值。但这样的话,每次我们读取该数据结构保存的数据时,都要遭遇解指针所带来的瓶颈,这就是另一种灵活度与性能之间的平衡。
Ref.:
1. Implementing a dynamically sized array in C