[C] 在 C 语言编程中实现动态数组对象
对于习惯使用高级语言编程的人来说,使用 C 语言编程最头痛的问题之一就是在使用数组需要事先确定数组长度。
C 语言本身不提供动态数组这种数据结构,本文将演示如何在 C 语言编程中实现一种对象来作为动态数组。
/* Author: iFantastic@cnblogs */
基本的 C 数组
C 语言编程中声明一个基本数组如下:
int main() {
// 声明一个容纳 3000 个整数的数组
int my_array[];
}
以上代码做了两件事:
● 在栈区开辟内存空间。准确说来是在函数 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 = ;
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 < ) {
printf("Index %d out of bounds for vector of size %d\n", index, vector->size);
exit();
}
return vector->data[index];
} void vector_set(Vector *vector, int index, int value) {
// 使用 0 填充闲置在用内存空间。
while (index >= vector->size) {
vector_append(vector, );
} // 在指定数组位置保存指定整数。
vector->data[index] = value;
} void vector_double_capacity_if_full(Vector *vector) {
if (vector->size >= vector->capacity) {
// 翻倍数组大小。
vector->capacity *= ;
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 = ; i > ; i--) {
vector_append(&vector, i);
} // 我们指定在第 251 个位置保存一个整数 99999。
// vector 自动再次翻倍内部数组容积到 400 个位置,并将 99999 放到第 251 个位置。
// 另外将第 151 到 250 之间所有的位置用 0 进行填充。
vector_set(&vector, , ); // 读取第 28 个位置的整数值,该位置的整数应该是 173。
printf("Heres the value at 27: %d\n", vector_get(&vector, )); // 遍历当前 vector 内部数组所有实际在用的位置。
for (i = ; 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 :
vector[] =
vector[] =
vector[] =
...
vector[] =
vector[] =
vector[] =
vector[] =
...
vector[] =
vector[] =
可以看到这个动态数组大小为 251 个整数容量(实际可以保存 400 个整数),第 28 个位置值为 173,中间一段位置使用了 0 填充,第 251 个位置值为 99999。
数据结构中的平衡艺术
本文展示了如何实现一种底层数据结构,通过理解底层的实现过程,你可以更好的理解一些高级语言的行为以及为什么它们会有某些速度瓶颈。
调整本文中的数据结构 Vector 内部的数组大小是一种开销很大的操作,因为它需要调用 realloc() 函数。realloc() 函数会调整指针指向的那片内存空间的大小,并返回一个指向调整后内存空间的指针。如果当前内存区域没有足够的剩余空间来扩展当前的内存空间,那么 realloc() 会开辟一片新的内存区域,并且将指针指向的旧内存空间内容复制到新的内存空间,然后释放旧的内存空间,然后返回新的内存空间指针。
所以如果我们遇到当前内存区域不够扩展我们的数组时,我们不得不进行开销很大的复制操作。为了减少这种情况出现的可能性,我们每次扩展内存空间时总是翻倍地开辟新的内存空间,这种策略带来的副作用就是可能会造成内存空间的浪费,这就是一种根据内存空间与速度之间的平衡。
另外本文实现的数据结构只能保存整数类型对象。如果我们数据结构中使用的数组保存指向空对象的指针而不是整数,那么我们就可以保存任意类型的值。但这样的话,每次我们读取该数据结构保存的数据时,都要遭遇解指针所带来的瓶颈,这就是另一种灵活度与性能之间的平衡。
Ref.:
1. Implementing a dynamically sized array in C
[C] 在 C 语言编程中实现动态数组对象的更多相关文章
- (待续)C#语言中的动态数组(ArrayList)模拟常用页面置换算法(FIFO、LRU、Optimal)
目录 00 简介 01 算法概述 02 公用方法与变量解释 03 先进先出置换算法(FIFO) 04 最近最久未使用(LRU)算法 05 最佳置换算法(OPT) 00 简介 页面置换算法主要是记录内存 ...
- R语言编程中的常见错误
R语言编程中的常见错误有一些错误是R的初学者和经验丰富的R程序员都可能常犯的.如果程序出错了,请检查以下几方面. 使用了错误的大小写.help().Help()和HELP()是三个不同的函数(只有第 ...
- Linux下C语言编程中库的使用
零.问题 1. 为什么要用到库? 2. 我要用一个库,但是,尼玛命令行上该怎么写呢?或者说库文件如何使用? 3. Linux的库在那些地方? 4. 什么是静态库,什么是动态库,二者有啥区别? 5. 常 ...
- go语言之切片即动态数组
切片和数组的类型有什么不一样,我们可以打印一下,就可以知道两者的区别了,数组是容量的,所以中括号中有容量,切片的动态数组,是没有容量,这是数组和切片最大的区别 test8_4 := [20] int ...
- 关于C#中的动态数组ArrayList
在C#中,如果需要数组的长度和元素的个数随着程序的运行不断改变,就可以使用ArrayList类,该类是一个可以动态增减成员的数组. 一.ArrayList类与Array类的区别 ArrayList类实 ...
- 【Azure 环境】Azure Resource Graph Explorer 中实现动态数组数据转换成多行记录模式 - mv-expand
问题描述 想对Azure中全部VM的NSG资源进行收集,如果只是查看一个VM的NSG设定,可以在门户页面中查看表格模式,但是如果想把导出成表格,可以在Azure Resource Graph Expl ...
- 浅谈JavaScript和DOM中的类数组对象
JavaScript是一门弱类型语言,它的数据类型分为两大类:简单数据类型(5种:Undefined.Null.Boolean.Number.String)和复杂数据类型(1种:Object).Obj ...
- js中的类数组对象---NodeList
动态 NodeList 这是文档对象模型(DOM,Document Object Model)中的一个大坑. NodeList 对象(以及 HTML DOM 中的 HTMLCollection对象)是 ...
- JavaScript中的类数组对象
在javascript中,对象与数组都是这门语言的原生规范中的基本数据类型,处于并列的位置. 一般来说,如果我们有一个对象obj和一个数组a: obj["attr1"]; / ...
随机推荐
- 通过ZwQuerySystemInformation获取EPROCESS
google一下,发现很多都是直接通过ZwQuerySystemInformation通过11号获取进程结构SYSTEM_PROCESS_INFORMATION,对于详细的进程信息表达不够.所以想要通 ...
- 计算请假天数JavaScript方法
前言 最近,有这么个需求,给用户做个请假审批系统,要输入请假开始时间和结束时间,同时计算出请假天数,如果年假数量不够提示不能提交,如果年假数量够的话,就走审批工作流,审批通过以后,自动在年假上减去这个 ...
- Android典型界面设计(3)——访网易新闻实现双导航tab切换
一.问题描述 双导航tab切换(底部区块+区域内头部导航),实现方案底部区域使用FragmentTabHost+Fragment, 区域内头部导航使用ViewPager+Fragment,可在之前博客 ...
- logback身份证脱敏
logback身份证脱敏 学习了:https://shift-alt-ctrl.iteye.com/blog/2425469 https://blog.csdn.net/fywfengyanwei/a ...
- web,xml中关于filter的使用
从J2EE1.3开始,Servlet2.3规范中加入了对过滤器的支持.过滤器能够对目标资源的请求和响应进行截取.过滤器的工作方式分为四种,下面让我们分别来看看这四种过滤器的工作方式:1.request ...
- 使用Jenkins搭建持续集成(CI)环境
转自:http://www.cnitblog.com/luckydmz/archive/2012/01/03/77007.html 首先从官网http://jenkins-ci.org/下载 Java ...
- 源码版本管理工具 :TFS GIT
至于svn ..忽略不计了... 集中式代码管理 CVCS 模式:TFS 分布式代码管理 DVCS 模式:git 两者比较大的差别:tfs 只有一个中央仓储,其他副本都要与中央仓储进行更新.git ...
- Boinx FotoMagico for Mac(电子相册制作工具)破解版安装
1.软件简介 FotoMagico 是 macOS 系统上一款非常好用的电子视频相册制作工具,FotoMagico 被誉为 Mac 上的「会声会影」,我们可以使用这款软件快速的制作出精美的音乐视 ...
- dd测试硬盘性能
下面直接介绍几种常见的DD命令,先看一下他的区别~ dd bs=64k count=4k if=/dev/zero of=testdd bs=64k count=4k if=/dev/zero of= ...
- 6种常见的Git错误以及解决的办法
我们都会犯错误,尤其是在使用像Git这样复杂的东西时.如果你是Git的新手,可以学习如何在命令行上开始使用Git.下面介绍如何解决六个最常见的Git错误. Photo by Pawel Janiak ...