美文网首页
C语言陷阱

C语言陷阱

作者: 欧阳_z | 来源:发表于2020-06-28 16:57 被阅读0次

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 目录。或者 BMakefile 下用条件语句区分不同情况下使用不同目录下的文件。总之库文件与头文件要记得保持一致。
这里出错是因为库文件使用了最新的,但头文件仍然使用旧的,所以最终运行时可以看到 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
$

还有一个容易忽视的printfscanf不同的是,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 次。

相关文章

  • C语言陷阱

    最近一直做C编译器相关的开发,感觉该总结一下。以前一直以为对C已经足够熟悉了,结果被它奇葩的语法树震惊了。碰巧最近...

  • C语言陷阱

    1、当我们使用 printf 打印字符串时,要用 printf("%s", s ); 而不能用 printf( s...

  • C语言陷阱「词法陷阱 之字符与字符串」

    C语言陷阱【词法陷阱 之字符与字符串】 字符与字符串 C语言中的单引号' ',与双引号" ",含义不同。 用单引号...

  • 书籍推荐

    1.《C primer plus》----《C程序设计语言》-----《C和指针》、《C专家编程》、《C缺陷与陷阱...

  • C语言陷阱II

    相信以后还会有III,IIII,V…… 类型转换是有代价的 最近在写虚拟机解释引擎的时候遇到的这个问题。Java虚...

  • Java中的断言assert

    Java陷阱之assert关键字 一、概述 在C和C++语言中都有assert关键,表示断言。在Java中,同样也...

  • 有效使用C语言的建议

    如何有效地使用C语言? 避免C语言的陷阱,不要依赖编译器来检测代码中的问题。 使用工具来改进您的程序,尤其是lin...

  • C语言中的词法陷阱

    该文章为笔记,因此许多内容摘抄自《C陷阱与缺陷》。《C陷阱与缺陷》,全书不厚,但是感觉十分有提醒与启迪作用,值得阅...

  • C语言 宏陷阱与缺陷

    1. 不能忽视宏定义中的空格 #define f (x) ((x)-1) 上面的宏定义中展开后变成 (x) ((x...

  • C7. C语言“陷阱” 之 运算顺序

    在C语言中,某些运算符总是以一种已知的、规定的运算顺序对其操作数进行求值,而另外一些则不是这样。考虑如下表达式: ...

网友评论

      本文标题:C语言陷阱

      本文链接:https://www.haomeiwen.com/subject/nlpwfktx.html