1、当我们使用 printf
打印字符串时,要用 printf("%s", s );
而不能用 printf( s );
假设传入参数为 char *s = "%x%x";
前者还能正常工作,后者就显然达不到预期的结果。
2、加法溢出
在二分查找的时候,可能经常会写int mid = ( left + right) / 2;
如果两个数比较大的话,就有溢出的风险,比如:
int left = 0x7FFF0002; // 2147418114
int right = 0x7FFF0006; // 2147418118
int mid = ( left + right) / 2; // -65532
可以改为 int mid = left + (right - left) / 2; // 2147418116
3、乘法溢出
在嵌入式设备中通常会用到 eMMC,它的一个 block 是 512
字节。假设有一块 16GB
的 eMMC,则总字节数就是 0x4 0000 0000
,超过了 32
位,总block 数是 0x200 0000
,还没超过32
位,
所以 block 数定义为unsigned int block_count;
是可以的。
但是在计算某个分区的字节数时,可能会写成:
unsigned long bytes = 512 * block_count;
或
unsigned long bytes = (unsigned long)(512 * block_count);
初看以为用了 64
位来保存结果没有问题,尤其是第二种写法具有迷惑性,两个 32
位数相乘,结果也会被截断为 32
位,必须在计算乘法之前把其中一个数提升为 64
位,结果才会是 64
位。
而常用的 eMMC 分区的大小都在几兆到一百兆之内,不会触发溢出,所以平时很难察觉,
一旦 block_count >= 0x80 0000
,则 bytes >= 0x1 0000 0000
,
也就是当出现一个分区达到 4GB
时就会出现溢出。
改为下面这2个都可以:
bytes = (unsigned long)512 * block_count;
bytes = 512 * (unsigned long)block_count;
不过还是需要确认该平台下,每一种类型具体表示多少位,然后写好各个类型的 typedef
,最终使用时,就不写 unsigned long
,而是自定义的 uint64_t
比较严谨,因为也有可能是 unsigned long long int
表示 64
位。
在Ubuntu环境上,在 /usr/include/stdint.h 文件中有定义好:
$ grep -C 1 uint64_t /usr/include/stdint.h
#if __WORDSIZE == 64
typedef unsigned long int uint64_t;
#else
__extension__
typedef unsigned long long int uint64_t;
#endif
包含头文件#include <stdint.h>
即可使用。
另外如果 debug 需要使用 printf
打印出来,也记得改用相应的 %lx
或 %llx
而非 %x
。
4、结构体传参
函数传结构体指针时,如果两边结构体名字不同,是会有 warning
的:
warning: passing argument 1 of ‘fun’ from incompatible pointer type [-Wincompatible-pointer-types]
并指出函数要求一个 struct C * 类型,却传了一个struct B * 类型:
note: expected ‘struct C *’ but argument is of type ‘struct B *’
gcc
加上 -Werror
之后,就变成了 error
:
error: passing argument 1 of ‘fun’ from incompatible pointer type [-Werror=incompatible-pointer-types]
但是如果两个结构体的名字相同,只是定义不同的话,就连 warning
都不会有了,很难发现。
删减后的代码如下:
A仓库的代码:
a.h:
struct A{int x;int new_member;};
struct B{int data;};
struct C{struct A a; struct B b;};
a.c:
#include <stdio.h>
#include "a.h"
void fun(struct C *p)
{
printf("%d \n", p->b.data );
}
B仓库的代码:
a_bak.h:
struct A{int x;};
struct B{int data;};
struct C{struct A a;struct B b;};
b.c:
#include "a_bak.h"
extern void fun(struct C *p);
int main(void)
{
struct C c = {0};
c.b.data = 1;
fun( &c );
return 0;
}
编译命令: gcc a.c b.c -Wall -Werror
可以看到两个头文件中的 struct A
的定义不同,定义为什么会不同呢?
实际情况是,两边的代码属于不同的模块,放在不同的git仓库上,如果B
仓库直接include
另一个仓库A
的头文件时,假设你只下载了B
的代码,就不能编译,所以需要拷贝一份A
的头文件和库文件到B
仓库里面,才能编译。但是这样如果A
更新了,B
还不知道,就导致两边头文件中结构体的定义不同了。
所以编译时需要区分两种情况:
一是 B
单独编译,来做 B
内部的 debug,这时候旧一点的头文件和库可能影响不大,可以使用本地的旧文件;
二是整个项目进行完整编译的情况,这时候不仅要等 A
编译完之后,把 A
生成的库文件覆盖到 B
目录,还需要把 A
的头文件也覆盖到 B
目录。或者 B
的 Makefile
下用条件语句区分不同情况下使用不同目录下的文件。总之库文件与头文件要记得保持一致。
这里出错是因为库文件使用了最新的,但头文件仍然使用旧的,所以最终运行时可以看到 printf
打印出来的值并不是代码中赋值的 1 ,而是一个随机值,每次运行都不同。
5、缓冲区溢出示例
看上去是个容易发现的错误,但我们平时编码的时候却不一定总能写对:
#include <stdio.h>
int main(void)
{
char buf[6];
printf("input:");
scanf("%s", buf);
printf("buf:%s...\n", buf);
return 0;
}
$ ./a.out
input:123456789
buf:123456789...
*** stack smashing detected ***: ./a.out terminated
Aborted
$
上面的代码中,buf 占 6 个字节,正好够输入一个"hello",但如果输入的单词长度大于 5,那么字符 c 就要被覆盖了,那如果把数组扩大一些呢,比如 1024 个字节,但是不管定多大都有溢出的可能,只需要简单的把标准输入重定向到一个大于 1KB 的文件就溢出了。
一个简单的解决办法是在 %s
中加一个数字 n
,来限制 scanf
读取的最大字符数为n
:
#include <stdio.h>
int main(void)
{
char buf[6];
printf("input:");
scanf("%5s", buf);
printf("buf:%s...\n", buf);
return 0;
}
$ ./a.out
input:123456789
buf:12345...
$
这样程序最多只读取 5
个字符,剩余的部分,程序会在随后的输入语句中读取。
如果是 double
型,则往往更容易被忽视,假设你把一个 double
型数据转换为字符串,用一个 32 字节的字符数组来保存,看上去足够,其实会有溢出的风险。
#include <string.h>
#include <stdio.h>
#include <float.h>
int main(void)
{
char buf[512];
sprintf(buf, "%lf", DBL_MAX);
printf("buf = %s\n", buf);
printf("len = %ld\n", strlen(buf));
return 0;
}
$ ./a.out
str = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000
len = 316
$
是316,你没有看错,double
可以有这么长。
更容易忽视的是,此时把"%lf"
改为"%30lf"
不能达到只输出30个字节的效果,30是输出最小宽度,而不是最大宽度。
改为 snprintf
函数可以达到目的。
#include <string.h>
#include <stdio.h>
#include <float.h>
int main(void)
{
char buf[32];
snprintf(buf, sizeof(buf), "%lf", DBL_MAX);
printf("buf = %s\n", buf);
printf("len = %ld\n", strlen(buf));
return 0;
}
$ ./a.out
buf = 1797693134862315708145274237317
len = 31
$
还有一个容易忽视的printf
与scanf
不同的是,scanf
返回值是成功读入的数据项数,而printf
不是输出变量的个数,而是输出的字符数(字符串结束字符'\0'
不算在内)。
6、异或
有时候为了省一个变量,会用 3
次异或来交换两个值,而且这有一个小陷阱就是,当 i 等于 j 时,结果为 0
,代码如下:
#include <stdio.h>
void swap(int arr[], int i, int j)
{
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
int main(void)
{
int a[] = {1, 2, 3, 4};
swap(&a[0], 0, 1);
printf("a[0]=%d, a[1]=%d\n", a[0], a[1]);
swap(&a[0], 2, 2);
printf("a[2]=%d\n", a[2]);
return 0;
}
$ ./a.out
a[0]=2, a[1]=1
a[2]=0
$
正常的交换是这样的:
a ^= b;
b ^= a;
a ^= b;
而 i 等于 j 的时候,实际上是这样的,两个变量用的是同一块空间:
a ^= a;
a ^= a;
a ^= a;
所以如果看到别人给异或运算交换的代码前面加了一个条件判断 if (i != j)
,不是为了节省一次运算,而是为了避免一个错误。别一不小心就异或自己 3 次。
网友评论