C/C++ Bug&Pitfalls
这篇文章也是长期性的一个积累,对于本人个性化的一些使用C/C++造成的失误进行记录。
1. Printf 中格式化使用造成的失误:
int main() { char* str="-1234"; printf("string is %lld\nstr length is %d\n", str2Number(str), strlen(str)); //printf("string is %ld\nstr length is %d\n", str2Number(str), strlen(str)); getchar(); return 0; }
如上,str2Number返回的是long long 类型,如果使用注释掉的那一种printf的话,则后者显示为-1,尽管strlen(str)值仍为5。
另注:lld --》long long int, ld --》long int, llf --》 long double,lf --》 double,注意printf在输出前应该强制类型转换,而不是默认的。
上述为Linux下, 对于VC6.0,lld应改为I64d,long long int 应改为_int64,__int64。
2. 在使用赋值操作:
int num = 0xFFFFFFFF int num = 0x80000001
所赋的值都是补码,而不是源码或者反码。
3. float和double类型的长度
二者分别为4字节,8字节(与机器位宽无关)。
float: 8bit指数位(-128~127),23bit尾数位,1位符号位。
double: 11bit指数(-1023~1024),52bit尾数为,1位符号位。
float范围:-2^128 ~ 2^127。
double范围:-2^1024~2^1027。
float精度:2^23 = 8388608,故为7位,至少6位。
double精度:2^52 = 4503599627370496,故为16位,至少15位。
另注意:long double的宽度仍为8字节(Win,x86/x64)。
4. 干脆写出所有类型在x86及x64(均为VS编译器)下的长度:
类型 | x86(字节数) | x64(字节数) |
char/unsigned char | 1 | 1 |
short/unsigned short | 2 | 2 |
int/unsigned int | 4(system) | 4 |
long/unsigned long | 4 | 4 |
long long/unsigned long long | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
long double | 8 | 8 |
char*(T*) | 4 | 8 |
5. 左移右移
C存在逻辑右移和算术右移两种,左移则只有逻辑左移。
逻辑移动:移动后,新出现的位补“0”;
算术移动:移动后,新出现的位补“最高有效位的值”
Java则进行了修改:1)Java没有无符号数;2)Java中”>>“为算术右移,”>>>“为逻辑右移。
问题:
C语言标准没有指定使用哪种类型右移,普适的应用情况是:对无符号数,右移补“0”,即使用逻辑右移。对有符号数,右移补”符号位“,即使用算术右移。
例子,全为有符号数,移动4位(16):
变量 | 算术右移 | 逻辑右移 | 除法 | 逻辑左移 |
01100011(99) | 00000110(6) | 00000110(6) | 6。1875 | 00110000(48)(99 *16 % 256) |
10010101(-107) | 11111001(-7) | 00001001(9) | -6.6875 | 01010000(80)(-107 * 16 % 256) |
11111111(-1) | 11111111(-1)(无限循环!<移动0-7位仍为-1>) | 00001111(15) | -0.5 | 11110000(-16)(-1 * 16 % 256) |
10000000(-128) | 11111000(-8) | 00001000(8) | -8 | 00000000(0)(-128 * 16 % 256) |
根据上面可以发现以下几条编程准则:
1)避免对有符号数进行左移操作,会丢掉符号位,变成求余结果(且结果正负不可预测,见”-107“与”-1“)。同样尽量不对有符号数右移,因为可能出现无限循环。
2)不管正负数,除法取整的方式均是向下取整(取靠近浮点数但比浮点数小的结果)
3)-1的右移操作不会改变其值。
4)当我们试着把一个超出范围的数赋给该变量时,存在以下几种情况:
- 超出范围的unsigned(X)赋给unsigned(Y,上限为0 ~ YMax),则Y = X % YMax
- 超出范围的signed(X)赋给signed(Y,上限为-(YMax/2 + 1) ~ (YMax/2)),则 Y = X % (YMax/2)
- 负数(X)赋给unsigned(Y),由于对于unsigned类型说,负数总是超出其范围,所以Y = X %YMax
6. malloc,free,new,delete
1)配对关系要满足
2)free以及delete完后,需要将指针置为NULL/nullptr,否则,下次再free或delete时,会报错
3)不用在free及delete前检查指针是否为NULL/nullptr,free/delete会在内部自己检查
4)”不能多次free/delete同一区域“这种说法不准确,前提是置为NULL/nullptr的话,就可以多次free/delete
7. 不安全的C库函数
1) void* memcpy(void* dst, const void* src, size_t len)
- 目的地址和源地址相同时会发生不可预测行为
- len有长度限制为int
- len指的是拷贝字节长度,而不是拷贝项数
推荐使用:
void* memmove(void* dst, void* src, unsigne len):其在重叠时仍能正确运行。
2)char* strcpy(char* dest, char* src)
- 目的地址和源地址相同时会发生不可预测行为
- 拷贝越界问题
推荐使用:
char* strncpy(char* dest, char* src, unsigned len),会指定从src中要拷贝的项数;
error_t strncpy_s(char* dest, size_t len, char* src, size_t maxCount),maxCount为要拷贝的字节数,len为src中字节数,内部还会比较maxCount与src长度,取其较小的作为拷贝字符数目。其返回的是错误代码,防止返回错误指针。
8. sizeof 与 strlen的区别
1)参数区别:
sizeof接受的参数为类型名或者变量名,返回结果表征的是该类型实例或者变量所由编译器分配的内存字节数。
strlen接受char*的参数,当将数组名传给strlen时,其退化为char*。
2)计算方法区别:
sizeof计算参数所占有的内存大小,strlen计算截止到'\0'为止(不包括'\0')的字节数,比如下例:
char str[] = {'a', 'b', '\0', 'c', 'd'}; // sizeof(str)结果为5,而strlen(str)结果为2。
3)附加c语言字符串及字符串数组的pitfall:
char str[] = {'a', 'b', 'c', 'd', 'e'}; // 不提倡,因为这样当将str看做字符串时,没有'\0'结束符,strlen(str)会出错。建议采用下列方式:
char str[] = {'a', 'b', 'c', 'd', 'e', '\0'}; // 提倡,人为加入'\0' ,或:
char str[6] = {'a', 'b', 'c', 'd', 'e'}; // 提倡,编译器自动在尾部加入'\0',但需要预留给至少一个位置给编译器添加'\0',否则如下编译器无法添加'\0':
char str[5] = {'a', 'b', 'c', 'd', 'e'}; // 或者直接赋予字符串,如下:
char str[] = "abcde" // 编译器自动添加'\0',sizeof(str)结果为6, 而strlen(str)结果为5,或者直接将字符串赋给char*,如下:
char* str = "abcde" // 编译器自动添加'\0',sizeof(str)结果为4(因为此时sizeof计算的是一个char型指针所占内存字节数),strlen(str)结果为5。
9. sscanf格式匹配
1)函数原型
int sscanf(const char *buffer,const char *format,[argument ]...);
2)Format解释
Format可为一个或多个:{%[*] [width] [{h | I | I64 | L}]type | ' ' | '\t' | '\n' | 非%符号},其中:
- '*'表示,略过后面紧接着的类型的字符
- width表示字符宽度
- {h | I | I64 | L}表示三个类型任选一个,由于被[]包裹,所以可以有也可以没有
- type就是常见的"%d", "%s"之类
- | ' ' | '\t' | '\n' | 非%符号表示这四种格式控制符,并且可以没有
特别注意的是:
- %[a-z] 表示匹配a到z中任意字符,贪婪性(尽可能多的匹配)
- %[aB'] 匹配a、B、'中一员,贪婪性
- %[^a] 匹配非a的任意字符,并且停止读入,贪婪性
举例:
%64[^\n]表示读入64个字符宽度的输入,贪婪的读,读到'\n'则不读,读到64个了也不读。可以用于控制输入带空格字符串,并以'\n'结束输入。
10. C/C++ void&void*&NULL&nullptr
1)void
C/C++中,void表示空类型,其不表示任意类型,而只是表示不存在的类型。所以:
void func(void)表示没有返回值,没有参数。
C++中,void func()表示没有参数,但C中,void func()却表示可有拥有任意数目任意类型参数。
所以,标准而通用的写法是,对于没有参数,写void,对于没有返回值,也要写void。
2)void*
尽管void表示的是空类型,但void*却可以表示任意类型的指针。因为C/C++这种静态语言,定义变量就需要分配内存,不同类型分配的内存不同,但指针类型则统一为4字节(32位系统),8字节(64位)。
所以,void*可以用于表示任意类型的指针。但其应用限制为:
- 给另一个void*指针赋值
- 与另一个void*指针进行比较
- 向函数传递void*参数或者从函数返回void*参数
一定要注意限制不可以的应用为:
- 对void*解引用
- 操作void*所指向的对象
- 对void*进行算术操作
3)NULL & nullptr
C中为(void*)0,表示空指针,一般指向0地址。
C++中为0,因为C++中不支持void*向其它类型指针的转换,C则采用此机制实现函数重载。比如:
C中可以写FILE* p = NULL,或者FILE* p = (void*)0,C++中这样写就会报错,必须写为FILE* p = (FILE*)0,或者FILE* p = NULL, 或者干脆 FILE* p = 0;
C++中需要注意的是,对于重载函数func(int a, type* b)和func(int a, int b),不要以为func(3, NULL)调用的是func(int a, type* b)形式,其实NULL = 0,其使用的是func(int a, int b)。
但当我们引入nullptr(C++11)后,nullptr就切实表示为一个空指针,可以指向任意类型的指针,那么func(3, nullptr)则调用的是func(int a, type* b)形式,从而避免我们出错。