c/c++ 开发中常见的坑

来源:互联网 发布:魅族官方网络授权店 编辑:程序博客网 时间:2024/04/29 11:15

http://blog.csdn.net/youyoushang/article/details/50032975


总结一下自己遇到的坑,吸取教训,避免第二次、第三次在同一个地方扑街。

语言类

  • 变量初始化问题

众所周知,局部变量在函数调用开始时创建,函数调用完成返回时“销毁”。值得注意的是,这里根本没有所谓的销毁、初始化的过程。局部变量的内存分配、回收是通过栈指针(esp)的减小、增大来完成的,注意函数栈从大地址向小地址增长。也就是说,函数栈只管分配和回收,至于这个地址空间里的内容, 它不关心,初始化工作要由程序员完成。同理,由malloc分配的空间,初始化工作也要由程序员完成。

使用未经初始化的变量,在写入值时,不会发生任何问题。但是在读取它的值时,就有可能发生错误。更要命的是,这种错误有可能在某段时间内、某个主机上(就是你的开发环境)一直不出现,一到测试环境、客户环境就会随机出现问题!由于局部变量的空间分配之前,可能被其他函数变量所用,这个空间的值刚好被置为0,这时再将这个空间分配给你的未初始化变量,就不会出现问题。而刚好在某一段时间内,函数调用流程是固定的,那么这个问题就“刚好”一直不会出现。但是我们不能一直跪地祈祷xx保佑不要出现问题,所以,记住这个血与泪的教训:定义变量时,一定要明确是否需要初始化。

问题示例:

[cpp] view plain copy
  1. #include <stdio.h>  
  2.   
  3. void main_loop(int argc, char **argv)  
  4. {  
  5.         int i;  
  6.   
  7.         while (i < argc) {  
  8.   
  9.                 i--;  
  10.         }  
  11. }  
  12.   
  13. int main(int argc, char *argv[])  
  14. {  
  15.         main_loop(argc, argv);  
  16.         return 0;  
  17. }  

初始化示例:

[cpp] view plain copy
  1. int i = 0;  
  2. int *p = NULL;  
  3. char buf[1024] = {0};  
  4. struct st_ipc ipc;  
  5. memset(&ipc, 0, sizeof(ipc));  

  • 实参合法性判断问题

在开发api给其他人用,或者自己封装api时,往往要注意实参的检查。最常见的是指针、数据范围的判断。这是增强程序健壮性的必备手段。而在每个函数入口检查参数,自然会有一堆重复代码。这时候宏的作用就体现出来了:

[cpp] view plain copy
  1. #ifdefine DEBUG_EN  
  2. #define DEBUG(fmt, args...)     \  
  3.         do {                    \  
  4.                 printf("DEBUG:%s-%d-%s "fmt, __FILE__, __LINE__, __FUNCTION__, ##args);\  
  5.         }while(0)  
  6.   
  7. #define ERROR(fmt, args...)     \  
  8.         do {                    \  
  9.                 printf("ERROR:%s-%d-%s "fmt, __FILE__, __LINE__, __FUNCTION__, ##args);\  
  10.         }while(0)  
  11. #else  
  12. #define DEBUG(fmt, args) do{}while(0)  
  13. #define ERROR(fmt, args) do{}while(0)  
  14. #endif  
  15.   
  16. #define IS_NULL(p)                      (p == NULL)  
  17. #define P_VALID_CHECK_ACT(p, act)       {if (IS_NULL(p)){DEBUG(#p" is NULL\n");act;}}  
  18. #define P_VALID_CHECK_RET(p, ret)       P_VALID_CHECK_ACT(p, return ret)  
如果参数非法,将提示出问题的代码文件、行数、所在的函数,方便定位问题。

使用方法:

[cpp] view plain copy
  1. int func(char *str)  
  2. {  
  3.         P_VALID_CHECK_RET(str, -1);  
  4.         ...  
  5.         return 0;  
  6. }  
  7.   
  8. int fuc1(char *str)  
  9. {  
  10.         int ret = -1;  
  11.         P_VALID_CHECK_ACT(str, goto ret);  
  12. ret:  
  13.         return ret;  
  14. }  

  • 赋值问题

这里主要是==和=的混用,比如条件判断时,误赋值的问题,比如:

[cpp] view plain copy
  1. if (a = 0 || c > d)  {  
  2. ...  
  3. }  

或者

[cpp] view plain copy
  1. int set(int *a)  
  2. {  
  3.        if(...)   
  4.                *a == 1;  
  5. }  
解决办法:
[cpp] view plain copy
  1. if (0 == a || c > d)  {  
  2. ...  
  3. }  

  • 内存问题

这是个常见的、烦人的、一两句话说不清的问题。主要是要注意几点:

1. 空间由谁释放、在哪里释放,释放之后不应该再访问那块空间,不能再次释放已经释放的空间。只能手动释放分配在堆上的空间。

[cpp] view plain copy
  1. #define SAFE_FREE(p)                    {if (p){free(p);p=NULL;}}  

2. 空间由谁分配,分配了多大空间,不能访问范围之外的空间。

3. 不要解引用为空的指针。在使用指针访问其成员之前,先检查指针是否为空。

网络通信类

  • tcp/udp

假设我们采用如下的tlv数据结构通信:

[cpp] view plain copy
  1. struct fs_tlv{  
  2.         u_int16_t type;  
  3.         u_int32_t length;  
  4.         char value[0];  
  5. }__attribute__((__packed__));  
  6. #define FS_TLV_HLEN sizeof(struct fs_tlv)  
  7.   
  8. struct fs_msg_h{  
  9.         u_int16_t type;  
  10.         u_int32_t length;  
  11.         u_int16_t id;  
  12.         char value[0];  
  13. }__attribute__((__packed__));  
  14. #define FS_MSG_HLEN     sizeof(struct fs_msg_h)  
一般情况下,一个完整的消息由消息头和数据构成(注意这里取消了内存对齐)。一般的处理是:数据准备好读取时,我们先读取消息头,确定后面的数据长度,再根据这个长度分配一个空间,再读取后面的数据。这样可以避免不必要的空间浪费。在使用tcp协议时,这样做完全没有问题。tcp是一种面向连接的、可靠的、基于字节流的通信协议。“字节流”意味着你甚至可以一个字节一个字节地读取数据,当然,考虑到效率问题,一般不这么做。

但如果通信协议是udp时,情况就不同了。你必须一次读取尽可能多的数据,如果你只读取了消息头(数据包的一部分),那么整个数据包都会丢失。第二次读取数据部分时,将出现问题。udp是用户数据报协议,有不提供数据包分组、组装和不能对数据包进行排序的缺点。使用udp意味着由应用层保证传输可靠性,值得注意的是,使用udp传输数据,数据包应该尽可能的小。

  • 大端小端

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,类似于字符串的存放。相反小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。大端格式更符合我们的习惯。

什么是数据的高字节?什么是低字节?什么是内存的高地址?低地址?

对于数字来说,比如十六进制的0x1234,数据的高字节指的是权值高的字节(显然1的权值高于4),12是高字节,34是低字节。内存地址大的为高地址,小的为低地址。这个数字用大端模式表示就是:12 34, 小端模式时:34 12。而对于字符串,是按照出现顺序,在左边的先发送,先收到。不存在大端小端转换的问题,因为字符串的解析,不依赖与字符串的长度,而是一个字节一个字节地解析。

网络通信采用的是网络字节序,具体就是大端格式。在发送、接收大于1字节的基本数据类型数据时,要注意使用ntoh、hton一类的函数转换字节序。

  • 结构体内存对齐

一般在通信编程中, 会采用结构体来封包、解包。这样可以避免复杂的指针运算、类型转换等操作。而不同的内存对齐方式,结构体的大小是不同的,结构体成员相对于结构体的起始地址也不相同。因为不同的对齐方式,可能在成员之间填充不同字节大小的空间。而采用内存对齐,主要是优化处理器访问,减少访问次数,提高速度。但是这个效率的提高,可能带来的是通信异常、不必要的空间浪费(填充字节),得不偿失,而且通信双方采用相同的对齐方式,这个也无法保证。所以一般在定义结构体时指定

[cpp] view plain copy
  1. __attribute__((__packed__)  
采用1字节对齐(就是不对齐)。
  • 加密解密

网络通信中经常会用到诸如MD5的加密api, 典型的加密流程:

[cpp] view plain copy
  1. #include <stdio.h>  
  2. #include "md5.h"  
  3.   
  4. int main(void)  
  5. {  
  6.         struct MD5Context md5;  
  7.         char s[] = "helloworld";  
  8.         unsigned char ss[16];  
  9.         int i = 0;  
  10.         char buf[128] = {0};  
  11.   
  12.         MD5Init(&md5);  
  13.         MD5Update(&md5, s, strlen(s));  
  14.         MD5Final(ss, &md5);  
  15.   
  16.         for (i = 0; i < sizeof(ss); i++)  
  17.                 sprintf(buf + 2*i, "%02X", ss[i]);  
  18.   
  19.         printf("%s\n", buf);  
  20.         return 0;  
  21. }  
输出结果:FC5E038D38A57032085441E7FE7010B0

这里有几个值得注意的地方:

  1. void MD5Update (MD5_CTX *context, unsignedchar *input, unsignedlong InputLen); 注意这里的InputLen为input字符串的长度, 而非占用的空间(多了'\0')。如果第11行的strlen换成sizeof的话,就悲剧了,加密结果总是错的。而很多人会想当然的用sizeof,因为大多数传入指针的函数确实是要求传入这个指针指向空间的大小。
  2. 参数input的类型是unsigned char, 可接受字符串、或二进制数。再次证明了第一点,第三个参数不可能是字符串长度。
  3. void MD5Final (unsignedchar *digest, MD5_CTX *context);生成最终的加密结果,注意结果digest的类型是unsigned char,不是字符串,不能直接解析为字符串,而是整数值!也就是说,这里使用unsigned char,只是当做了大小1字节的无符号整数来使用。
  4. 还是上面unsigned char的问题,如果ss定义为char类型,那么sprintf(buf, "%X", ss[i]);就极有可能出现问题,假如ss[i]的值大于7f,符号位就为1,而%X表示以无符号整数输出,char转换为usigned int将会发生符号扩展,结果会看见很多FF...所以这里使用了%02X,限制了只输出低2位16进制数,不足2位用0填充。

函数类

  • sizeof

数组名在函数传递参数过程中, 会退化为指针, 这个时候sizeof(数组名)得到的是这个指针变量占用的空间,而非数组占用的空间。

  • 可变参数的函数

vsprintf函数用于解析格式控制符一类的参数,有内存溢出的风险。曾遇到有人利用这个漏洞执行了恶意代码,拿到了设备控制权限。解决办法是利用vsnprintf代替。

  • system

这个问题其实不能归结于system函数的问题,应该是Linux shell(sh、bash)向某个程序传递命令行参数时要注意的问题。有时一个参数比如文件名中可能包含空格、中文字符,而一般多个参数是用空格隔开的,这种情况下就会造成参数传递错误。

[cpp] view plain copy
  1. static void custom_fload(void)  
  2. {  
  3.         char cmdline[1024] = {0};  
  4.         sprintf(cmdline, "ln -sf \"%s\" \"%s\"""Hello world""custom_f");  
  5.         system(cmdline);              
  6.         return;  
  7. }  
解决办法是,在可能出现空格、中文字符的参数前后加上双引号,明确双引号包围的字符为一个参数

一般情况下,我们调用system去执行一些零时的、不重要的命令。由于system会继承环境变量,在编写SUID/SGID 权限的程序时可能会遇到问题,此外,system所执行的命令是否执行成功的判断略繁琐,所以在使用时要多多注意。


编译类

  • c标准

曾经遇到过一个排查了很久才偶然解决的BUG, 为了支持某些新特性,在gcc编译时指定了-std=c99参数,编译时一切正常。遇到一个基于unix域协议的ipc函数调用时,发现通信过程中,某个结构体成员的值总是不正确。仔细排查发现,ipc函数调用在一个动态链接库中,是之前单独使用gcc默认c标准编译的(c89/c90)标准。去掉-std=c99参数之后,BUG消失。怀疑是不同c标准下产生的二进制代码不同, 在进程通信时,可能会带来兼容问题。

  • makefile

编辑一个目标下所执行的命令时,必须使用Tab键缩进!并且禁止编辑器使用空格代替Tab键!

  • 头文件

一定要在文件头尾使用宏防止头文件被重复包含

[cpp] view plain copy
  1. #ifndef _APP_IGD_AP_H_  
  2. #define _APP_IGD_AP_H_  
  3. .....  
  4.   
  5. #endif   

原创粉丝点击