文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-11 17:39

关注

大部分 C 程序员都以为基本的整形操作都是安全的其实不然,看下面这个例子,你觉得输出结果是什么:

  1. int main(int argc, char** argv) { 
  2.     long i = -1; 
  3.  
  4.     if (i < sizeof(i)) { 
  5.          printf("OK\n"); 
  6.     } 
  7.     else { 
  8.          printf("error\n"); 
  9.     } 
  10.  
  11.     return 0; 

当一个变量转换成无符号整形时,i的值不再是-1,而是 size_t的最大值,因为sizeof操作返回的是一个 size_t类型的无符号数。在C99/C11标准里写道:

在C标准里面 size_t至少是一个 16 位的无符号整数,对于给定的架构 size_t 一般对应long,所以sizeof(int)和size_t至少相等,这就带来了可移植性的问题,C标准没有定义 short, int,long,longlong的大小,只是说明了他们的最小长度,对于 x86_64 架构,long在Linux下是64位,而在64位Windows下是32位。一般的方法是采用固定长度的类型比如定义在C99头文件stdint.h中的uint16_t,int32_t,uint_least16_t,uint_fast16_t等。

 

如果 int可以表示原始类型的所有值,那么这个操作数会转换成 int,否则他会转换成 unsigned int。下面这个函数在 32 位平台返回 65536,但是在 16 位系统返回 0。

  1. uint32_t sum() 
  2.     uint16_t a = 65535; 
  3.     uint16_t b = 1; 
  4.     return a+b; 

对于char 类型到底是 signed 还是 unsigned 取决于硬件架构和操作系统,通常由特定平台的 ABI(Application Binary Interface) 指定,如果是 signed char,下面的代码输出-128 和-127,否则输出 128,129(x86 架构)。

  1. char c = 128; 
  2. char d = 129; 
  3. printf("%d,%d\n",c,d); 

内存管理和分配

malloc 函数分配制定字节大小的内存,对象未被初始化,如果 size 是 0 取决与系统实现。malloc(0)返回一个空指针或者 unique pointer,如果 size 是表达式的运算结果,确保没有整形溢出。

  1. size_t computed_size; 
  2.  
  3. if (elem_size && num > SIZE_MAX / elem_size) { 
  4.     errno = ENOMEM; 
  5.     err(1, "overflow"); 
  6.  
  7. computed_size = elem_size*num; 

malloc不会给分配的内存初始化,如果要对新分配的内存初始化,可以用calloc代替malloc,一般情况下给序列分配相等大小的元素时,用calloc来代替用表达式计算大小,calloc 会把内存初始化为 0。

 

[[336373]]

realloc 用来对已经分配内存的对象改变大小,如果新的 size 更大,额外的空间没 有 被 初 始 化 , 如 果 提 供 给 realloc 的 指 针 是 空 指 针 , realloc 就 等 效 于malloc,如果原指针非空而 new size是0,结果依赖于操作系统的具体实现。

下面这段代码可以带你领会malloc,calloc,realloc,free的用法:

  1. #include  
  2. #include  
  3. #include  
  4. #include  
  5.  
  6. #define VECTOR_OK            0 
  7. #define VECTOR_NULL_ERROR    1 
  8. #define VECTOR_SIZE_ERROR    2 
  9. #define VECTOR_ALLOC_ERROR   3 
  10.  
  11. struct vector { 
  12.     int *data; 
  13.     size_t size
  14. }; 
  15.  
  16. int create_vector(struct vector *vc, size_t num) { 
  17.  
  18.     if (vc == NULL) { 
  19.         return VECTOR_NULL_ERROR; 
  20.     } 
  21.  
  22.     vc->data = 0; 
  23.     vc->size = 0; 
  24.  
  25.      
  26.     if (num == 0 || SIZE_MAX / num < sizeof(int)) { 
  27.         errno = ENOMEM; 
  28.         return VECTOR_SIZE_ERROR; 
  29.     } 
  30.  
  31.     vc->data = calloc(num, sizeof(int)); 
  32.  
  33.      
  34.     if (vc->data == NULL) { 
  35.         return VECTOR_ALLOC_ERROR; 
  36.     } 
  37.  
  38.     vc->size = num * sizeof(int); 
  39.     return VECTOR_OK; 
  40.  
  41. int grow_vector(struct vector *vc) { 
  42.  
  43.     void *newptr = 0; 
  44.     size_t newsize; 
  45.  
  46.     if (vc == NULL) { 
  47.         return VECTOR_NULL_ERROR; 
  48.     } 
  49.  
  50.  
  51.      
  52.     if (vc->size == 0 || SIZE_MAX / 2 < vc->size) { 
  53.         errno = ENOMEM; 
  54.         return VECTOR_SIZE_ERROR; 
  55.     } 
  56.  
  57.     newsize = vc->size * 2; 
  58.  
  59.     newptr = realloc(vc->data, newsize); 
  60.  
  61.      
  62.     if (newptr == NULL) { 
  63.         return VECTOR_ALLOC_ERROR; 
  64.     } 
  65.  
  66.      
  67.     vc->data = newptr; 
  68.     vc->size = newsize; 
  69.     return VECTOR_OK; 

 

[[336374]]

避免重大错误

使用未初始化的变量,C语言要求所有变量在使用之前要初始化,使用未初始化的变量会造成为定义的行为,这和C++不同,C++保证所有变量在使用之前都得到初始化,Java尽量保证变量使用前的得到初始化,如类基本数据成员会被初始化为默认值。

free错误对空指针调用 free,对不是由 malloc family 函数分配的指针调用 free,或者对已经调用 free 的指针再次调用 free。一开始初始化指针为NULL可以减少错误,GCC和Clang编译器有-Wuninitialized 选项来对未初始化的变量显示警告信息,另外不要将同一个指针用于静态变量和动态变量。

  1. char *ptr = NULL; void nullfree(void **pptr) { void *ptr = *pptr; assert(ptr != NULLfree(ptr); *pptr = NULL; } 

3.对空指针解引用,数组越界访问

对NULL指针或者free’d内存解引用,数组越界访问,是很明显的错误,为了消除这种错误,一般的做法就是增加数组越界检查的功能,比如Java里的array就有下标检查的功能,但是这样会带来严重的性能代价,我们要修改ABI(application binary interface),让每个指针都跟随着它的范围信息,在数值计算中cost is terrible。

4.违反类型规则

把int×指针cast成float×,然后对它解引用,在C里面会引发undefined behavior,C规定这种类型的转换需要使用memset,C++里面有个reinterpret_cast函数用于无关类型之间的转换,reinterpret_cast (expression)

防止内存泄漏

内存泄漏发生在程序不再使用的动态内存没有得到释放,这需要我们掌握动态分配对象的作用域,尤其是什么时候该调用free来释放内存,常用的集中方法如下:

  1. #include  #include  #define MAX_REF_OBJ 100  
  2. #define RC_ERROR -1 struct mem_obj_t{ void *ptr; uint16_t count; };  
  3. static struct mem_obj_t references[MAX_REF_OBJ]; static uint16_t  
  4. reference_count = 0;   
  5. uint16_t create(size_t size){ if (reference_count >= MAX_REF_OBJ)  
  6. return RC_ERROR; if (size){ void *ptr = calloc(1, size); if (ptr !=  
  7. NULL){ references[reference_count].ptr = ptr;  
  8. references[reference_count].count = 0; return reference_count++; } }  
  9. return RC_ERROR; } 
  1.  
  2. void* retain(uint16_t handle){ 
  3.  
  4. if(handle < reference_count && handle >= 0){ 
  5.     references[handle].count++; 
  6.     return references[handle].ptr; 
  7.     } else { 
  8.         return NULL
  9.     } 
  10.  
  11.  
  12. void release(uint16_t handle){ 
  13. printf("release\n"); 
  14.  
  15. if(handle < reference_count && handle >= 0){ 
  16.     struct mem_obj_t *object = &references[handle]; 
  17.  
  18.     if (object->count <= 1){ 
  19.         printf("released\n"); 
  20.     free(object->ptr); 
  21.     reference_count--; 
  22. else { 
  23.     printf("decremented\n"); 
  24.     object->count--; 
  25.         } 
  26.      } 

C++标准库有个auto_ptr智能指针,能够自动释放指针所指对象的内存,C++ boost库有个boost::shared_ptr智能指针,内置引用计数,支持拷贝和赋值,看下面这个例子:

  1. #include  
  2. #include  
  3. int main() 
  4.     // Basic useage 
  5.     boost::shared_ptr<int> p1(new int(10)); 
  6.     std::cout << "ref count of p1: " << p1.use_count() << std::endl; 
  7.     boost::shared_ptr<int> p2(p1); // or p2 = p1; 
  8.     std::cout << "ref count of p1: " << p1.use_count() << std::endl; 
  9.     *p1 = 999; 
  10.     std::cout << "*p2: " << *p2 << std::endl; 
  11.     p2.reset(); 
  12.     std::cout << "ref count of p1: " << p1.use_count() << std::endl; 
  13.     return 0; 

内存池,有利于减少内存碎片,看下面这个例子:

  1. #include  
  2. #include  
  3.  
  4. struct mem_pool_t{ 
  5. void* ptr;//指向内存池起始地址 
  6. size_t size;//内存池大小 
  7. size_t used;//已用内存大小 
  8. }; 
  9.  
  10. //create memory pool 
  11. struct mem_pool_t* create_pool(size_t size){ 
  12. mem_pool_t* pool=calloc(1,sizeof(struct men_pool_t)); 
  13. if(pool!=NULL){ 
  14. void* mem=calloc(1,size); 
  15. if(mem!=NULL){ 
  16. pool->ptr=mem; 
  17. pool->size=size
  18. pool->used=0; 
  19. return pool; 
  20.         } 
  21.     } 
  22. return NULL
  23.  
  24. //allocate memory from pool 
  25. void* pool_alloc(mem_pool_t* pool,size_t size){ 
  26. if(pool=NULL
  27.     return NULL
  28. size_t bytes_left=pool->size-pool->used; 
  29. if(size&&size<=bytes_left){ 
  30.     void* mem=pool->ptr+pool->used; 
  31.     pool->used+=size
  32.     return mem; 
  33.     } 
  34. return NULL; 
  35.  
  36. //release memory of the pool 
  37. void pool_free(mem_pool_t* pool){ 
  38. if(pool!=NULL){ 
  39. free(pool->ptr); 
  40. free(pool); 
  41.  } 

垃圾回收机制引用计数采用的方法是当内存不再需要时得到手动释放,垃圾回收发生在内存分配失败或者内存到达一定的水位(watermarks),实现垃圾回收最简单的一个算法是MARK AND SWEEP算法,该算法的思路是遍历所有动态分配对象的内存,标记那些还能继续使用的,回收那些没有被标记的内存。Java采用的垃圾回收机制就更复杂了,思路也是回收那些不再使用的内存,JAVA的垃圾回收和C++的析构函数又不一样,C++保证对象在使用之前得到初始化,对象超出作用域之后内存得到释放,而JAVA不能保证对象一定被析构。

 

[[336375]]

指针和数组

我们一般的概念里指针和数组名是可互换的,但是在编译器里他们被不同的对待,当我们说一个对象或者表达式具有某种类型的时候我们一般是说这个对象是个左值(lvalue),当对象不是const的时候,左值是可以修改的,比如对象是复制操作符的左参数,而数组名是一个const左值,指向地一个元素的const指针,所以你不能给数组名赋值或者意图改变数组名,如果表达式是数组类型,数组名通常转换成指向地一个元素的指针。

但是也有例外,什么情况下数组名不是一个指针呢?1.当它是sizeof操作符的操作数时,返回数组占的内存字节数2.当它是取地址操作&的操作数时,返回一个数组的地址

看下面这个例子:

  1. short a[] = {1,2,3}; 
  2. short *pa; 
  3. short (*px)[]; 
  4.  
  5. void init(){ 
  6.     pa = a; 
  7.     px = &a; 
  8.  
  9.     printf("a:%p; pa:%p; px:%p\n", a, pa, px); 
  10.  
  11.     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不一样,它就是一个指针,不是用来定位的。再比如:

  1. int a[10]; 
  2. int b[10]; 
  3. int *a; 
  4. c=&a[0];//c是指向数组a地一个元素的指针 
  5. c=a;//a自动转换成指向第一个元素的指针,实际上是指针拷贝 
  6. b=a;//非法的,你不能用赋值符把一个数组的所有元素赋给另一个数组 
  7. a=c;//非法的,你不能修改const指针的值 

 

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯