当前位置: 首页 > 科技观察

你所不知道的C语言高级用法

时间:2023-03-14 20:41:37 科技观察

整数溢出与提升大多数C程序员认为基本的整数运算是安全的,其实不然。看下面的例子,你认为输出是什么:intmain(intargc,char**argv){longi=-1;if(iSIZE_MAX/elem_size){errno=ENOMEM;err(1,"overflow");}computed_size=elem_size*num;malloc不会初始化分配的内存。如果要对新分配的内存进行初始化,可以使用calloc代替malloc。通常,在给序列分配大小相等的元素时,使用calloc而不是用表达式计算大小,calloc会将内存初始化为零。realloc用于更改已分配内存的对象的大小。如果新大小更大,则不初始化额外空间。如果提供给realloc的指针是一个空指针,realloc就等同于malloc。如果原始指针不为空,newsize为0,则结果取决于操作系统的具体实现。“在失败的情况下,realloc应返回NULL并保持提供的内存对象完好无损。因此,不仅检查大小参数的整数溢出很重要,而且如果realloc失败,正确处理对象大小也很重要。#include#include#include#defineVECTOR_OK0#defineVECTOR_NULL_ERROR1#defineVECTOR_SIZE_ERROR2#defineVECTOR_ALLOC_ERROR3structvector{int*data;size_tsize;};intcreate_vector(structvector*vc,size_tnum){if(vc==NULL){returnVECTOR_NULL_ERROR;}vc->data=0;vc->size=0;/*checkforintegerandSIZE_MAXoverflow*/if(num==0||SIZE_MAX/numdata=calloc(num,sizeof(int));/*callocfaild*/if(vc->data==NULL){returnVECTOR_ALLOC_ERROR;}vc->size=num*sizeof(int);returnVECTOR_OK;}intgrow_vector(structvector*vc){void*newptr=0;size_tnewsize;if(vc==NULL){returnVECTOR_NULL_ERROR;}/*checkforintegerandSIZE_MAXoverflow*/if(vc->size==0||SIZE_MAX/2size){errno=ENOMEM;returnVECTOR_SIZE_ERROR;}newsize=vc->size*2;newptr=realloc(vc->data,newsize);/*reallocfaild;vectorstaysintactsizewasnotchanged*/if(newptr==NULL){returnVECTOR_ALLOC_ERROR;}/*uponsuccess;updatenewaddressandsize*/vc->data=newptr;vc->size=newsize;returnVECTOR_OK;}avoid一个主要的错误是使用未初始化的变量。C语言要求所有的变量在使用前都需要初始化。使用未初始化的变量将导致未定义的行为。这与C++不同。C++保证所有变量在使用前都已初始化。Java尽量保证变量在使用前被初始化,比如类的基本数据成员会被初始化为默认值函数,或者为已经调用过free的指针再次调用free。将指针初始化为NULL可以减少错误。GCC和Clang编译器有-Wuninitialized选项来显示未初始化变量的警告消息。另外,不要对静态变量和动态变量使用相同的指针。char*ptr=NULL;voidnullfree(void**pptr){void*ptr=*pptr;assert(ptr!=NULL)free(ptr);*pptr=NULL;}3.解引用空指针并访问数组越界取消引用NULL指针或释放的内存以及越界访问数组是明显的错误。为了消除此类错误,一般的方法是增加数组越界检查的功能。比如Java中的数组就有下标校验的功能,但这会带来严重的性能开销。我们需要修改ABI(applicationbinaryinterface),让每个指针都跟随它的范围信息,在数值计算上代价是非常可怕的。4.违反类型规则,将int×指针强制转换为float×,然后解引用,在C中会造成未定义行为。C规定这种类型转换需要使用memset,C++中有一个reinterpret_cast函数用于不相关的类型。reinterpret_cast(expression)之间的转换防止了内存泄漏。当程序不再使用的动态内存没有被释放时,就会发生内存泄漏。这就需要我们掌握好动态分配对象的范围,尤其是在调用free释放内存的时候。常用的集中式方法如下:程序启动时分配所需的堆内存,程序退出时将释放的任务交给操作系统。这种方法一般适用于程序运行后立即退出的情况。使用可变长度数组(VLA)如果你需要一个可变长度的空间并且作用域在一个函数中,可变长度数组可以帮助你,但也有一个限制,可变长度数组在一个函数中的内存大小function一般不会超过几百个Bytes,这个数字在C标准中并没有明确定义。最好在栈上分配内存。允许在堆栈上分配的最大VLA内存为SIZE_MAX。知道目标平台的堆栈大小可以有效防止堆栈溢出。使用引用计数引用计数是一种管理内存的好方法,尤其是当你不想让你定义的对象被复制时,每次赋值时引用计数加1,每次丢失引用计数减1一个参考。当引用计数等于0时,认为对象不再需要,我们需要释放该对象占用的内存。由于C没有提供自动析构函数,我们必须手动释放内存。看一个例子:#include#include#defineMAX_REF_OBJ100#defineRC_ERROR-1structmem_obj_t{void*ptr;uint16_tcount;};staticstructmem_obj_treferences[MAX_REF_OBJ];staticuint16_treference_count=0;/*creatememoryobjectandreturnhandle*/uint16_tcreate(size){if(reference_count>=MAX_REF_OBJ)returnRC_ERROR;if(size){void*ptr=calloc(1,size);if(ptr!=NULL){references[reference_count].ptr=ptr;references[reference_count].count=0;returnreference_count++;}}returnRC_ERROR;}/*getmemoryobjectandincrementreferencecounter*/void*retain(uint16_thandle){if(handle=0){references[handle].count++;returnreferences[handle].ptr;}else{returnNULL;}}/*decrementreferencecounter*/voidrelease(uint16_thandle){printf("release\n");if(handle=0){structmem_obj_t*object=&references[handle];if(object->count<=1){printf("released\n");free(object->ptr);reference_count--;}else{printf("decremented\n");object->count--;}}}C++标准库有一个auto_ptr智能指针,可以自动释放指针指向的对象的内存。C++boost库有一个boost::shared_ptr智能指针,内置引用计数,支持复制和赋值,参见下面的例子:“shared_ptr类型的对象具有获取指针所有权并共享该所有权的能力:一旦它们取得所有权,当最后一个指针的所有者释放该所有权时,指针的所有者组将负责删除该指针。”#include#includeintmain(){//Basicuseageboost::shared_ptrp1(newint(10));std::cout<<"refcountofp1:"<p2(p1);//orp2=p1;std::cout<<"refcountofp1:"<#includestructmem_pool_t{void*ptr;//指向内存池起始地址size_tsize;//内存池大小size_tused;//已用内存大小};//creatememorypoolstructmem_pool_t*create_pool(size_tsize){mem_pool_t*pool=calloc(1,sizeof(structmen_pool_t));if(pool!=NULL){void*mem=calloc(1,size);if(mem!=NULL){pool->ptr=mem;pool->size=size;pool->used=0;returnpool;}}returnNULL;}//allocatememoryfrompoolvoid*pool_alloc(mem_pool_t*pool,size_tsize){if(pool=NULL)returnNULL;size_tbytes_left=pool->size-pool->used;if(size&&size<=bytes_left){void*mem=pool->ptr+pool->used;pool->used+=size;returnmem;}returnNULL;}//releasememoryofthepoolvoidpool_free(mem_pool_t*pool){if(pool!=NULL){free(pool->ptr);free(pool);}}5.垃圾回收机制引用计数使用的方法是手动rel当不再需要内存时释放内存,当内存分配失败或内存达到某个水位线(watermarks)时发生垃圾回收,最简单的垃圾回收算法是MARKANDSWEEP算法,该算法的思想是遍历所有动态分配对象的内存,标记可以继续使用的,回收没有标记的。Java采用的垃圾回收机制比较复杂,其思想是回收那些不再使用的内存,JAVA的垃圾回收不同于C++的析构函数。C++保证对象在使用前先初始化,对象超出作用域后释放内存,而JAVA不能保证对象一定会被析构。指针和数组在我们一般的概念中,指针和数组名是可以互换的,但在编译器中它们的处理方式不同。当我们说一个对象或表达式具有某种类型时,我们通常是指该对象是一种类型。左值(lvalue),当对象不是const时,可以修改左值,比如对象是复制操作符的左参数,数组名是一个const左值,它指向一个指向某个元素的const指针,所以你不能赋值或意图改变数组名,如果表达式是数组类型,数组名通常被转换为指向元素的指针。但也有例外。什么情况下数组名不是指针?1、当为sizeof运算符的操作数时,返回数组占用的内存字节数。2.当它是地址运算&的操作数时,返回一个数组的地址看下面的例子:shorta[]={1,2,3};short*pa;short(*px)[];voidinit(){pa=a;px=&a;printf("a:%p;pa:%p;px:%p\n",a,pa,px);printf("a[1]:%i;pa[1]:%i(*px)[1]:%i\n",a[1],pa[1],(*px)[1]);}a是short类型的数组,pa是short类型的指针,px呢?px是一个指向数组类型的指针,在a赋值给pa之前,它的值被转换为指向数组首元素的指针,但是后面的a因为遇到了&运算符而没有被转换。数组下标a[1]相当于(a+1),和p[1]一样,也指向(p+1),但是两者还是有区别的,a是数组,它实际存放的是第一个元素的地址,所以数组a是用来定位第一个元素的,但是pa不一样,它是一个指针,不是用来定位的。又如:inta[10];intb[10];int*a;c=&a[0];//c是指向数组a元素的指针c=a;//a自动转换为指向第一个元素的指针实际上是指针的副本b=a;//非法,不能使用赋值运算符将一个数组的所有元素赋值给另一个数组a=c;//非法,不能修改值const指针的