
前言本文面向编程零基础小白用生活化案例通俗讲解C语言中动态数组核心概念、组成要素与完整实操流程手把手演示动态数组的完整可运行代码示例。一、核心概念指针变量指的是可以储存某个内存地址的变量。一般来说指针变量并不储存某个有效数据而是指向某个存了有效数据的变量所在的地址可以指向一个变量的内存地址也可以指向一个结构体的内存地址甚至可以存另一个指针的的内存地址。但一般不会用它来储存有效数据。结构体结构体可以把一些变量“打包”成一个整体为的是在传递参数时可以通过直接传递结构体的指针从而将结构体内所有的变量一次性全部传入以方便使用、避免混淆。C语言的结构体在一定程度上有些类似 Java 的类class但是 Java 的类中可以有变量与函数方法C语言的结构体中只能有变量是不允许有函数的。内存分配在C语言中可以通过头文件 stdlib.h 中的 malloc 函数来申请分配一些内存然后将这些内存的地址赋值给一个指针。格式就像是//分配一块大小是一个 int 的内存//并将这块内存的地址赋值给指针 pint*p(int*)malloc(sizeof(int));//但 malloc 只负责给内存不负责放数据。内存里是垃圾值必须自己赋值。//通过指针为它指向的内存赋值*p10;//用完后记得用 free 函数释放这些内存free(p);//让 p 指向 null防止野指针pNULL;函数本质上是一段能重复调用的代码。C语言中的函数由 返回类型、函数名、参数列表、函数体、返回值 这五个部分组成。intplus(inta,intb){intcab;returnc;}在这个函数中int 是返回类型表示这个函数最终会返回一个整数。plus 是函数名也就是这个函数的名字。名字叫什么都可以除了关键字或用数字开头。函数名后面紧跟的括号以及括号中的内容就是这个函数需要的参数调用时必须传入与定义函数时相同类型、相同数量的参数。在大括号内的 “int c a b;” 就是这个函数的函数体了在调用这个函数时就会执行函数体。最后的 return c; 就是将 c 的值返回。定义一个函数并实际调用运行#includestdio.h//定义一个函数它需要两个整数作为参数intplus(inta,intb){//函数中为变量 c 进行了赋值intcab;//最后返回了变量 c 的值returnc;}//主函数程序执行入口intmain(){//调用 plus 函数为变量 i 赋值intiplus(15,20);//输出一下看看printf(%d,i);return0;}输出的结果就是35最后主函数是特殊的。在C语言中主函数的返回类型必须是 int函数名必须是 main参数列表必须是空的或者写(int argc, char *argv[])返回值一般写0。按理说可以写任何数字不过一般习惯返回0表示程序正常结束动态数组在C语言中数组的长度一旦确定便无法更改没有所谓的“加长”或“缩短”的操作。有些情况下如果数组拥有“扩容”的功能的话会方便很多比如我需要写一个哈希表自然会希望有一个可以自动扩容的数组。遗憾的是与 Java 不同C语言中并没有内置的官方动态数组给你用。但我们可以自己写。所谓的扩容功能我们可以通过新建一个比原来大的数组再把原来的数据全部拷贝过去最后让原来的指针指向新数组。不管过程怎么样我们的确实现了扩容的目的。之后还需要写添加、删减、获取数据等功能函数让使用者通过函数操作数组而不是直接操作内部数据安全、便捷地使用动态数组。二、指针的组成要素C语言的指针十分复杂。指针是什么指针是一种变量它一般被用来储存一块内存的地址。指针的类型指针是有类型区分的比如 int*整型指针或 char*字符指针甚至 void*无明确类型指针。指针的类型决定了它将以什么样的形式对指向内存的数据解引用、前后移操作、加减时走多远。比如对 int* 指针进行加减1时会往后走4个字节-1时会往前走4个字节。对 char* 指针1时会往后走1个字节-1时会往前走1个字节。而 void* 指针是例外因为并没有明确指向内存的数据是什么类型所以也不能进行增减操作、不能解引用。指针能做什么修改外部值调用函数时将外部的指针通过传入参数的方式传入函数中就可以在调用方函数后直接修改对应的外部变量数据。因为这本质上是将外部变量的内存地址传入了函数内部函数内外的指针指向的都是同一块内存修改的也是同一份数据。如果只是传入变量而非指针则相当于只是将变量复制了一份副本进入函数中这与外部对应的变量指向的并非同一块内存自然也非同一份数据。指针运算有明确类型的指针可以通过直接用 或 - 符号运算。比如一个字符数组char h[] “hello”;我们可以定义一个指针char *p h;然后通过 p 访问到字符 ‘e’也就是数组的第二位字符。指针也可以用大于号 、小于号 进行比较。通过对指针的运算也可以实现遍历数组的操作charh[]hello;//定义数组//定义指针char*ph;//这个操作会让指针指向数组的第一个位置//循环遍历for(inti0;i5;i){printf(%c,*(pi));}//另一种更老式、更接近底层的方式char*endh5;//定义一个指针表示数组末尾//直接通过比较指针来循环while(pend){printf(%c,*p);p;}当做数组使用数组本质上就是一串连续的内存。通过 malloc 函数申请分配一段连续的内存再将这段连续的内存第一格的地址赋值给一个指针变量本质上与创建数组一样。需要对这些内存进行操作时比如为其中一格赋值只需要指向那串内存的头指针就可以通过对指针增减来操作对应的位置。本质上与数组没有区别。多级指针指针可以指向指针定义多级指针时要加上两个*星号。通过这个操作我实现了可以储存任何数据类型的动态数组。原理是动态数组中每个格子都存 void*每个 void* 指向一个基本类型的指针比如一个 int*。然后这个声明了类型的指针再指向最终数据。这与 Java 中的 Object 数组十分相似不同的是Java 在解引用时会自动分辨类型但C语言中的 void* 做不到。三、完整实操案例#includestdbool.h//提供 bool、true、false#includestdio.h#includestdlib.h//动态数组结构体//data 存指针数组size 是当前元素个数len 是当前容量typedefstructArrayList{void**data;intsize;intlen;}ArrayList;//初始化数组指定初始容量ArrayList*arrayListCreate(intlen){//给结构体本身分配内存ArrayList*list(ArrayList*)malloc(sizeof(ArrayList));//给指针数组分配内存每个格子存一个 void*list-data(void**)malloc(len*sizeof(void*));list-size0;list-lenlen;returnlist;}//默认长度为10ArrayList*arrayListCreateDefault(){returnarrayListCreate(10);}//销毁数组//freeFn是用来释放每个元素的函数传 NULL 表示不释放元素voidarrayListDestroy(ArrayList*list,void(*freeFn)(void*)){//如果传了释放函数就逐个释放元素if(freeFn!NULL){for(inti0;ilist-size;i){freeFn(list-data[i]);}}//先释放里面的指针数组再释放结构体本身free(list-data);free(list);}//末尾添加voidarrayListAdd(ArrayList*list,void*value){//满了就扩容if(list-sizelist-len){intnewlenlist-len*2;void**newdata(void**)malloc(newlen*sizeof(void*));//把旧数据复制到新数组for(inti0;ilist-size;i){newdata[i]list-data[i];}//释放旧的指向新的free(list-data);list-datanewdata;list-lennewlen;}//放到末尾size 加1list-data[list-size]value;list-size;}//插入添加在 index 位置插入 valuevoidarrayListAddAt(ArrayList*list,intindex,void*value){//下标不合法就直接返回if(index0||indexlist-size){return;}//满了就扩容逻辑和 add 一样if(list-sizelist-len){intnewlenlist-len*2;void**newdata(void**)malloc(newlen*sizeof(void*));for(inti0;ilist-size;i){newdata[i]list-data[i];}free(list-data);list-datanewdata;list-lennewlen;}//从后往前挪给 index 腾出位置for(intilist-size-1;iindex;i--){list-data[i1]list-data[i];}//插入并 size 加1list-data[index]value;list-size;}//删除指定位置返回被删的指针void*arrayListRemoveAt(ArrayList*list,intindex){//下标不合法返回NULLif(index0||indexlist-size){returnNULL;}//先记住要删的void*removelist-data[index];//从前往后挪把后面的元素往前移一格for(intiindex1;ilist-size;i){list-data[i-1]list-data[i];}list-size--;returnremove;}//删除首个匹配的指针成功返回 true失败返回 falseboolarrayListRemove(ArrayList*list,void*value,void(*freeFn)(void*)){for(inti0;ilist-size;i){//泛型版只能比地址不能比内容if(list-data[i]value){//先释放元素再删除位置freeFn(list-data[i]);arrayListRemoveAt(list,i);returntrue;}}returnfalse;}//删除所有匹配的指针成功返回 true失败返回 falseboolarrayListRemoveAll(ArrayList*list,void*value,void(*freeFn)(void*)){bool removefalse;//从后往前删避免漏删for(intilist-size-1;i0;i--){if(list-data[i]value){freeFn(list-data[i]);arrayListRemoveAt(list,i);removetrue;}}returnremove;}//获取指定位置的指针void*arrayListGet(ArrayList*list,intindex){//下标不合法返回 NULLif(index0||indexlist-size){returnNULL;}returnlist-data[index];}//获取数组大小intarrayListSize(ArrayList*list){returnlist-size;}四、个人收获总结前段时间我在学习 Java 时也仿照 Java 标准库中的动态数组类写了一个动态数组用来学习所以这次用C语言练习时十分顺利。这次练习最难的一点是实现全类型通用。最开始我写的是 int 类型的动态数组先把功能都实现了再说。在 Java 中可以通过泛型或 Object 数组来实现全类型通用我用泛型实现了全类型通用的动态数组。但C语言中并没有泛型要实现全类型通用会比较麻烦但并非不可能。通过二级指针 void**实现了类似于 Java 中 Object 数组的效果数组中每一个位置都存二级指针 void**再通过二级指针指向我们需要的类型的指针最后再指向数据。不过需要注意的是在使用时需要先强制转型void* 并不能做到直接解引用。最后如果感兴趣可以看看 Java 版本的动态数组点击跳转Java数据结构基础笔记动态数组、链表