最后更新于 .

各位C、C++开发的朋友们,有没有想过小小的printf也会有陷阱呢?这篇文章,我们就深入来探究一下(代码均在suse10 32位系统下编译测试通过)。 废话不多说,直接上代码:

int64_t a = 1;
printf("%d\n", a); 

结果是多少呢?当然是1,你可能会说。 我们来看一下结果:

1

果然是1!但是你会不会以为是 a 首先被自动转化成了 int 类型,然后输入为 1的呢? 如果真这么简单,本文到此也该结束了。我们换一个写法:

int64_t a = 1;
int b = 2;
printf("%d, %d\n", a, b);

这次的结果是多少呢?1 和 2?真的吗?我们来看一下结果:

1, 0

好吧,你可能该惊讶了。然而这个结果的确是对的。 如果你还是觉得不可相信,我们再来看一个代码:

uint32_t uin = 1;
printf("%llu\n", uin);

输入结果是:

13827625253599182849

这是个随机值。也许你会说,不对呀,应该是 1 呀? 这就涉及到 printf 的设计了,printf的第一个参数永远是字符串,他会解析每一个类似 %d 的结构,然后对指针做对应长度的偏移,如%d是4,%lld就是8。(为什么要偏移,请参看这里 从printf谈可变参数函数的实现) 所以,当执行如下代码

int64_t a = 1;
int b = 2;
printf("%d, %d\n", a, b);

实际上两个%d分别取得是 a 的低4字节和高4字节,从而分别是1和0(这里还涉及到大小端的问题,本机是小端存储)。 而对于

uint32_t uin = 1;
printf("%llu\n", uin);

也是一样的道理,先取到的uin作为低4字节,而高4字节则完全随机,从而得到了一个很大的随机数。 知道了这个原因,我们可以做一个测试:

uint32_t uin = 1;
uint32_t uin2 = 2;
printf("%llu\n", uin, uin2);

uint64_t uin3 = uin2;
uin3 = uin3 << 32;
uin3 += uin;
printf("%llu\n", uin3);

uin2比uin先入栈,所以uin2会在高位,uin会在低位。 如果按照我们所解释的,那两个结果应该完全一致,对不对?我们来看一下输出:

8589934593                                                                                                                                                  
8589934593

的确是一样的~ 看完这篇文章,当再看到类型不匹配的printf时,心里是不是会感觉更恐怖?哈哈

Pingbacks

Pingbacks已打开。

Trackbacks

引用地址

评论

  1. at2021

    at2021 on #

    学习了,不过还想请教博主,为什么printf在处理char和int的时候不会出现这种问题呢?

    Reply

    1. Dante

      Dante on #

      能给一下具体的代码?

      Reply

      1. at2021

        at2021 on #

        int64_t a = 1;
        int b = 2;
        printf("%d, %d\n", a, b);

        像这样的代码会出错,但是
        int a = 0x31;
        char b = 'c';
        printf("%c, %c\n",a, b);
        这段代码却不会出错
        运行结果是 1, c

        但是按照你的解释来看的话c应该是打印不出来的吧? 运行结果应该是1和一个空字符啊

        Reply

        1. Dante

          Dante on #

          http://blog.csdn.net/zdl1016/article/details/1698629

          看看这篇文章就明白了~

          Reply

    2. Dante

      Dante on #

      看看这篇文章应该有帮助:
      http://blog.csdn.net/zdl1016/article/details/1698629

      Reply

  2. 自由建客

    自由建客 on #

    你确定这段代码打印出来的是 1, 0 且编译器没有问题吗?
    int64_t a = 1;
    int b = 2;
    printf("%d, %d\n", a, b);
    更想不到你竟会如此分析!
    若真输出 1, 0 明眼人一看就知道是编译器或 libc 有问题。

    Reply

    1. 自由建客

      自由建客 on #

      抱歉!晕了,累了,该睡了。

      Reply

  3. 一念天堂

    一念天堂 on #

    这些东西在实际应用代码里面相当麻烦,比如很多日志采用这种格式字符串模式,当你更改变量类型的时候,往往会忘记同步更改格式字符,或者更有甚者,从32位平台迁移到64位平台,这就是噩梦,因为编译器帮不上忙。

    Reply

    1. Dante

      Dante on #

      对,而且非常不容易察觉。

      Reply

    2. 自由建客

      自由建客 on #

      编译器怎么帮不上了? gcc 不是有所谓的 attribute 吗,加 -Wall 就报了。

      Reply

  4. fanhe

    fanhe on #

    这问题是很经典的问题了, 第一个错了, 后面的全错.
    好习惯是做好强制类型转换.

    Reply

    1. Dante

      Dante on #

      对,关键是还不易察觉。

      Reply

  5. haohaolee

    haohaolee on #

    这是一个经典的类型不安全函数的例子。可惜printf家族是如此趁手,难以舍弃啊。
    希望C++11的可变模板参数能弄出个类型安全的printf来

    Reply

  6. szwe

    szwe on #

    表示ip的时候如果想用%d.%d.%d.%d的话就必须先用4个整形赋值原来是这个道理

    Reply

  7. ebencheung

    ebencheung on #

    @为什么printf在处理char和int的时候不会出现这种问题呢, 是因为printf在压栈的时候将uint8,char, short,unsigned short 转为4字节.

    @这是一个经典的类型不安全函数的例子
    gcc会检查 printf和scanf等内置函数, 会报warning.

    自定义的变参函数则要增加 __attribute__ ((format (printf, m,n))) 可以检查可变参数. 仅gcc, m 是format的位置, n是...的位置.从1开始数不是从0. 对应的还有format(scanf,m,n). 如果是成员函数则要把this计算进去.

    Reply

  8. ebencheung

    ebencheung on #

    @printf在压栈的时候将uint8,char, short,unsigned short 转为4字节.
    而scanf则不会, 再说scanf得到的是地址, 不知道多宽.所以没有自动转为4字节.
    char buf[4] = {0}
    sscanf("1-2-3-4","%d%d%d%d",buf[3],buf[2],buf[1],buf[0]); 将会输出不正确的结果, 原因是%d是4byte.而buf[0]只有1byte, buf[0]后面的3byte会被冲掉.

    Reply

    1. FG

      FG on #

      仔细看看sscanf函数的声明再发帖吧。

      Reply

  9. lxd

    lxd on #

    格式化I/O标准的跨平台处理方式应该是采用中定义的宏(C99)
    http://pubs.opengroup.org/onlinepubs/009604599/basedefs/inttypes.h.html

    Reply

  10. 识字

    识字 on #

    好久没逛博客了,呵呵,我来了。

    Reply

  11. 微博粉丝

    微博粉丝 on #

    博主,你既然这么懂博客,微博相比也玩吧。非常先关注你,从你这样比较好的博客里,可以看到很多适合经营微博的方法。

    Reply

  12. crazy

    crazy on #

    想问下,如果
    printf("%d"): 不写后面的参数,这打印出来的是啥呢?printf要偏移4个字节,是printf栈后面的4个字节?

    Reply

    1. Dante

      Dante on #

      先入栈的4个字节。

      Reply

  13. Lazy.monkey

    Lazy.monkey on #

    嗯。。。这个确实很难察觉!

    Reply

  14. FG

    FG on #

    作者你好,你用的编译器版本是什么?

    Reply

    1. FG

      FG on #

      代码在GCC4.6下运行结果是1,2

      #include
      #include

      int main()
      {
      int64_t a = 1;
      int b = 2;
      printf("%d %d\n", a, b);
      printf("%lu %lu\n", sizeof(a), sizeof(b));
      return 0;
      }

      ==============================
      编译:

      $ gcc test.c
      test.c: In function ‘main’:
      test.c:8:2: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int64_t’

      =====================================

      运行:

      $ ./a.out
      1 2
      8 4

      Reply

      1. Dante

        Dante on #

        嗯? 你是用64位机?

        Reply

      2. Dante

        Dante on #

        哦,看错了。。那就奇怪了,难道GCC的高版本做了优化么。。

        Reply

        1. FG

          FG on #

          呃,我果然用的是64位机。
          又用32位系统跑了一下,跟你原文中说的一模一样。
          受教了

          Reply

  15. seckcoder

    seckcoder on #

    为什么64位机跑出来的结果就是正确的呢?

    Reply

    1. fighterlyt

      fighterlyt on #

      原因很简单,因为64位机默认指针大小为64位,也就是说在汇编代码中,每个元素按照64位来取

      Reply

  16. x5miao

    x5miao on #

    按照C标准的规定,如果printf()的格式化字符串和后面的参数类型不匹配,则行为未定义。所以lz你的这个讨论应该是没有意义的吧,就像怎么讨论
    int i=1;
    int j=i+++i++;的结果都是没有意义的

    Reply

  17. yafei.zhang@langtaojin.com

    yafei.zhang@langtaojin.com on #

    请参考C99标准的 stdint.h 和 inttypes.h
    不要用这些山寨方法得到了错误的结论, 还以为自己很了解printf函数一样.

    Reply

  18. Jialin Wu

    Jialin Wu on #

    1
    2
    3
    int64_t a = 1;
    int b = 2;
    printf("%d, %d\n", a, b);
    这个代码的运行结果是 1 和 2:
    localhost:~ Jialin$ vim c.cpp
    localhost:~ Jialin$ g++ c.cpp
    c.cpp: In function ‘int main()’:
    c.cpp:8: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int64_t’
    c.cpp:8: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int64_t’
    localhost:~ Jialin$ ./a.out
    1,2

    Reply

发表评论