對於習慣使用高級語言編程的人來說,使用 C 語言編程最頭痛的問題之一就是在使用數組需要事先確定數組長度。
C 語言本身不提供動態數組這種數據結構,本文將演示如何在 C 語言編程中實現一種對象來作為動態數組。
/* Author: iFantastic@cnblogs */
基本的 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.: