[C] 在 C 语言编程中实现动态数组对象

  对于习惯使用高级语言编程的人来说,使用 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

时间: 2024-10-16 04:42:33

[C] 在 C 语言编程中实现动态数组对象的相关文章

C#语言中的动态数组(ArrayList)模拟常用页面置换算法(FIFO、LRU、Optimal)

目录 00 简介 01 算法概述 02 公用方法 03 先进先出置换算法(FIFO) 04 最近最久未使用(LRU)算法 05 最佳置换算法(OPT) 00 简介 页面置换算法主要是记录内存的忙闲状态,为进程分配和释放内存.当主存的空间太小而无法装入所有的进程时,就需要在内存和硬盘之间进行调度操作. 多数操作系统只采用某种特定的页面置换算法进行置换,无法预先探测当前运行进程的页面访问模式,因此不能根据不同的页面访问模式,选用不同的页面置换算法.当然,如果能对不同的访问模式选取相应的页面置换算法,

R语言编程中的常见错误

R语言编程中的常见错误有一些错误是R的初学者和经验丰富的R程序员都可能常犯的.如果程序出错了,请检查以下几方面.? 使用了错误的大小写.help().Help()和HELP()是三个不同的函数(只有第一个是正确的).? 忘记使用必要的引号.install.packages("gclus")能够正常执行,然而Install.packages(gclus)将会报错.? 在函数调用时忘记使用括号.例如,要使用help()而非help.即使函数无需参数,仍需加上().? 在Windows上,路

C语言编程中的指针

单片机编程中,涉及指针的地方很多,需要多看多练习多总结.#include <string.h>#include <stdio.h>#include <stdlib.h>int main(void){unsigned int pInt32;//指向32位无符号整数的指针unsigned int UINT32_1;//32位的无符号整数unsigned int Array1_UINT32[]={1,9,0,1};//存储4个32位无符号整数的数组unsigned short

关于C#中的动态数组ArrayList

在C#中,如果需要数组的长度和元素的个数随着程序的运行不断改变,就可以使用ArrayList类,该类是一个可以动态增减成员的数组. 二.ArrayList类与Array类的区别 ArrayList类实际上是Array类的优化版本. ArrayList只能定义一维数组,Arrays可以定义多维数组. ArrayList的下限始终为0,Array可以定义自己的下限. ArrayList的元素都是object类型的,因此需要进行装箱和拆箱操作,内存分配的代价很高,而Array的元素通常是特定类型的.

js中的类数组对象---NodeList

动态 NodeList 这是文档对象模型(DOM,Document Object Model)中的一个大坑. NodeList 对象(以及 HTML DOM 中的 HTMLCollection对象)是一种特殊类型的对象. DOM Level 3 spec 规范 对 HTMLCollection 对象的描述如下: DOM中的 NodeList 和 NamedNodeMap 对象是动态的(live); 也就是说,对底层文档结构的修改会动态地反映到相关的集合NodeList 和 NamedNodeMa

JavaScript中的类数组对象

在javascript中,对象与数组都是这门语言的原生规范中的基本数据类型,处于并列的位置. 一般来说,如果我们有一个对象obj和一个数组a: obj["attr1"];    //取obj对象的attr1属性 a[1];   //取数组a中的第二个元素 但是,有些时候,也会将一个对象“伪装”成一个数组来用,我们把这种对象称为“类数组对象”,再此我们可以給它下一个定义,请看如下的代码: var a= {}; var i = 0; for(i=0; i<10 ; i++){ a[i

Javascript中的arguments数组对象

(1)在函数体中,可以直接使用arguments数组对象,这个数组的元素是调用此函数时实际传入的所有参数 (2)其实在调用函数时,传入的参数个数可以和声明函数时不同,传入不同类型的参数也会导致结果不符合预期 (3)由于js的函数的返回值和参数个数以及参数类型都很灵活,我们在调用的时候应该确保传入函数期望的参数 示例 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.

关于delphi XE7中的动态数组和并行编程(第一部分)

本文引自:http://www.danieleteti.it/category/embarcadero/delphi-xe7-embarcadero/ 并行编程库是delphi XE7中引进的最受期待的功能之一.下面是一个简单的并行编程例子:   procedure TFormThreading.Button1Click(Sender: TObject); var   tasks: array of ITask;   value: Integer; begin   value := 0;   t

各种语言编程中的注释总结

JAVA: 单行注释 // 多行注释 /*..........*/ 多行注释快捷键:Ctrl+/ 或者 Ctrl+Shift+C 或者 Ctrl+Shift+/ HTML: <!-- 注释内容 --> JSP中的HTML注释: <!-- 注释内容 --> JSP页面中的普通注释 <% // 注释内容 %>    <% /* 注释内容 */ %> .....