最近在看APUE,不愧是经典,看一点就收获一点。但是感觉有些东西还是没说清楚,需要自己动手验证一下,结果发现需要用gcc,就了解一下。

  有时候,你在代码里面引用了一个函数但是没有包含相关的头文件,这个时候gcc报的错误比较诡异,一般是这样:【math.c:6:25: 警告:隐式声明与内建函数‘sin’不兼容 [默认启用]】。这个错误网上大量博客都在说需要包含XXX.h文件,但是没有人解释这个错误信息为什么这样表达。什么是隐式声明,什么是内建函数,我就纠结了。

  

  隐式声明函数的概念网上有相关的资料,有兴趣的同学可以自行查阅,这里简要的提一下。如果你调用了一个函数a,但是gcc找不到函数a的定义,那就默认帮你定义一个函数a,大概如下。

  int a(XXX){return XXX}

  显然这个不是件好事,因为,有时候gcc这样做会发现问题,提示这个错误,如果你用了这样的语句int i = a(XX);这样的话gcc是不会报错的,具体的行为我也没有深入研究。C语言后来的标准都慢慢放弃了隐式声明函数,C++里面会直接报错。

  内建函数,讲这个的资料就比较少。最后是在gcc的官方文档里面看到了相关的介绍,我也没有时间去细究只是看了几段话,再结合一些帖子里面的只言片语,大概得出如下推测。。

  顾名思义,内建函数就是一个系统或者工具提供的默认就能用的函数。这里面可以有两种理解,可以是gcc支持的c语言默认让你用这些函数,这些是gcc-c的内建函数;还有一种理解就是gcc指定的函数,gcc允许你使用这些函数。官方文档里面说gcc的内建函数大多是为了对代码进行优化,所以我更倾向于后一种理解。我觉得gcc的内建函数可以认为是gcc提供的一些类似预处理功能,以C函数的形式提供给编程人员使用,就是说看着是c函数,其实最后跟c语言没关系。比如下面的例子里面会用到,如果代码里面直接有sin(1)这样的调用,那gcc会直接算出sin(1)的值,然后在生成代码的时候直接使用这个值,而不会使用call sin命令调用sin函数。这就是所谓的优化(还有其他类型的优化,这个只是其中一种情况)。

  官方文档里面说gcc的内建函数主要分两类,一类以_builtin_为前缀,一类没有前缀。后者往往与某一个标准库的函数相对应,如sin,printf,exit。当编译器认为可以对相关的代码进行优化的时候(比如上面提到的直接得出某个结果,比如忽略没有意义的计算等等),会直接进行优化,而这些函数就相当于gcc的内置函数了。

  上面对内置函数进行了也说明,不知道我表达清楚没有,下面讲几个具体的例子。

  一、不连接libm的情况下使用sin函数

  file:math.c。

  1. #include <stdio.h>
  2. #include <math.h>
  3.  
  4. int main(){
  5. //int i = 1;
  6. //printf("sin(1)=%f.\n", sin(i));
  7. printf("sin(1)=%f.\n", sin(1));
  8. return 0;
  9. }

  这个代码可以直接gcc math.c -o math.out。然后./math.out直接执行。

  输出结果:sin(1)=0.841471.

  习惯了window编程的同学可能觉得没什么,但是在linux编程中是有问题的。gcc中,include <math.h>这条语句只是将math.h(标准库头文件)文件包含进math.c(我们的例子文件)中来,但是math.h中只有sin函数的声明,并没有sin函数的定义。正常而言,使用了math.h中声明的函数,就需要在编译(准确说是连接)的时候指定实现了math.h中函数声明的库,这里math.h对应标准库libm.a和libm.so。前者为静态库,后者为动态库。你可以这样理解,所有的.h文件是不需要编译的(如果被include,直接就相当于插入到了代码中),所有的.c文件都需要编译。.h文件中只是定义一个函数的形式,而不管这个函数具体做什么,比如sin函数需要一个double型的参数,执行完后返回一个double型的值。对汇编和编译原理有所了解的同学都应该懂,这样就可以暂时的编译一个调用了sin函数的.c文件,而不管sin函数具体怎定义了,直到生成汇编源代码。最后编译成汇编源代码大概就是

  push XXX //参数压栈

  call sin

  mov XXX XXX 或者pop XXX //获取返回值。

  有个函数声明,编译器就知道参数压栈怎么压,同时也知道返回的时候怎么获取返回值。

  但是代码最后还是要执行的,也就是说生成了汇编源代码还不行,还要把汇编源代码汇编成机器代码。这个时候,没有sin函数具体的代码,编译器没办法继续将汇编源代码汇编成机器代码,只能停留在这里。编译一份代码的最后一步就是连接。连接会将所有指定的.c文件编译的结果连接在一起。如上所述,libm.a和libm.so实现了sin,要想上面的代码能够运行,需要将libm.a(这里面只用到静态链接库)和math.c(示例代码)的编译结果连接起来。

  说了半天编译器的事,如果你听不明白上面的内容,那估计就不用往下看了,先补充一下相关的知识再说。

  总而言之,在gcc中如果代码使用了math.h中声明的函数,不但要在代码里include <math.h>,还需要编译的时候指定连接libm.a。理解了这点,就知道为什么上面的例子使用"gcc math.c -o math.out"很奇怪了。言归正传,为什么这个例子不需要连接libm.so。

  一开始,我以为是gcc编译器比较智能,能自动识别sin是math.h中的函数,然后自动连接libm.a。或者gcc默认就连接libm.a,但是网上并没找到这样的资料。直到看到一个帖子也是问类似的问题,有一个回答的人大意如下:gcc会对代码进行优化,但是优化也是基于gcc能够确定这个优化是没问题的。比如把sin(1)替换为sin(1)的真实值,这个就可以,因为代码里面使用sin(1)的目的99.9999999%是要计算sin(1)的值,而这个值是确定的,那gcc就在编译的时候算好,运行的时候就不用再算了。为了验证这点,可以使用gcc -S math.c -o math.s命令查看gcc将math.c编译成的汇编源代码(-S指定编译行为停止在生成汇编源代码阶段)。

  1. .file "math.c"
  2. .section .rodata
  3. .LC1:
  4. .string "sin(1)=%f.\n"
  5. .text
  6. .globl main
  7. .type main, @function
  8. main:
  9. .LFB0:
  10. .cfi_startproc
  11. pushq %rbp
  12. .cfi_def_cfa_offset
  13. .cfi_offset , -
  14. movq %rsp, %rbp
  15. .cfi_def_cfa_register
  16. subq $, %rsp
  17. movabsq $, %rax
  18. movq %rax, -(%rbp)
  19. movsd -(%rbp), %xmm0
  20. movl $.LC1, %edi
  21. movl $, %eax
  22. call printf
  23. movl $, %eax
  24. leave
  25. .cfi_def_cfa ,
  26. ret
  27. .cfi_endproc
  28. .LFE0:
  29. .size main, .-main
  30. .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
  31. .section .note.GNU-stack,"",@progbits

  注意main函数的内容,里面只有一个call printf,并没有call sin。同时注意到第17行有一个莫名其妙的数字$4605754516372524270。个人认为这个就是sin(1)的值经过莫中变化后的8进制代码。至于经过了什么变化我也说不清楚,这个值好像也不是sin(1)浮点结果的8进制,可能经过了一些运算,或者sin(1)的结果只是这个8进制值的一部分,这个有心的同学可以研究研究。不管怎么样,汇编代码里面没有call sin。说明sin(1)已经被优化了。

  同样是sin(1),在什么情况下gcc没办法优化呢?很简单,int i = 1; sin(i),这样gcc就没法优化了。虽然也是计算sin(1),但是gcc在编译代码的时候只知道求sin(i),但是他不知道i值是多少。为什么不知道?这个是编译优化的内容,有兴趣的同学可以了解一下。简单来说就是,有些变量的值在某些状态下是可以推导的,但是目前的技术能推导的情况不多,而且需要大量的编译处理才能推导,gcc对sin(i)这种情况大概是选择直接不推导。

  1. #include <stdio.h>
  2. #include <math.h>
  3.  
  4. int main(){
  5. int i = ;
  6. printf("sin(1)=%f.\n", sin(i));
  7. printf("sin(1)=%f.\n", sin());
  8. return ;
  9. }

  注意之前math.c的代码,将其中的注释去掉,就是现在math.c的代码。这个时候"gcc math.c -o math.out"就会报错:

    /tmp/ccYkhbgg.o:在函数‘main’中:
    math.c:(.text+0x15):对‘sin’未定义的引用
    collect2: 错误:ld 返回 1

  再看看汇编代码,注意这个时候到汇编的代码还是可以生成的,只是将汇编源程序会变成机器代码的时候,才发现call sin的sin函数没定义。

  1. .file "math.c"
  2. .section .rodata
  3. .LC0:
  4. .string "sin(1)=%f.\n"
  5. .text
  6. .globl main
  7. .type main, @function
  8. main:
  9. .LFB0:
  10. .cfi_startproc
  11. pushq %rbp
  12. .cfi_def_cfa_offset
  13. .cfi_offset , -
  14. movq %rsp, %rbp
  15. .cfi_def_cfa_register
  16. subq $, %rsp
  17. movl $, -(%rbp)
  18. cvtsi2sd -(%rbp), %xmm0
  19. call sin
  20. movsd %xmm0, -(%rbp)
  21. movq -(%rbp), %rax
  22. movq %rax, -(%rbp)
  23. movsd -(%rbp), %xmm0
  24. movl $.LC0, %edi
  25. movl $, %eax
  26. call printf
  27. movabsq $, %rax
  28. movq %rax, -(%rbp)
  29. movsd -(%rbp), %xmm0
  30. movl $.LC0, %edi
  31. movl $, %eax
  32. call printf
  33. movl $, %eax
  34. leave
  35. .cfi_def_cfa ,
  36. ret
  37. .cfi_endproc
  38. .LFE0:
  39. .size main, .-main
  40. .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
  41. .section .note.GNU-stack,"",@progbits

  这个时候有两个call printf,第一个call printf之前有一个call sin。第二个call printf前面还是没有call sin。

  gcc官方文档里面有一段话,大意是:对于内置函数,如果能对代码进行优化,gcc会优化代码,如果不能优化,往往就是直接调用同名的标准库函数。我的理解就是sin(1)能优化就给你优化了,sin(i)优化不了,就还是调用math.h中声明的sin函数。

  GCC includes built-in versions of many of the functions in the standard C library. These functions come in two forms: one whose names start with the __builtin_ prefix, and the other without. Both forms have the same type (including prototype), the same address (when their address is taken), and the same meaning as the C library functions even if you specify the -fno-builtin option see C Dialect Options). Many of these functions are only optimized in certain cases; if they are not optimized in a particular case, a call to the library function is emitted.

  修改的后代码编译时制定libm.a就可以,具体命令如下 gcc math.c -lm -o math.out。 -lxxx参数就是到相关目录中找libxxx.so和libxxx.a。这样就可以连接到libm.a了。

  gcc内建函数是可选的,我们可以在编译的时候指定不使用某些内建函数,gcc -fno-builtin-xxx。还是一开始的例子,使用命令:gcc -fno-builtin-sin math.c -o math.out。这次就会报错,因为我们指定不使用内建函数sin,那就会使用math.h中声明的sin函数,同时编译的时候并没有指定连接libm.a,这样就会报错:

    /tmp/ccKy8vEG.o:在函数‘main’中:
    math.c:(.text+0x11):对‘sin’未定义的引用
    collect2: 错误:ld 返回 1

  

  最初的问题,【math.c:6:25: 警告:隐式声明与内建函数‘sin’不兼容 [默认启用]】是什么意思?这个其实我自己也不清楚,我只是大概弄清楚了什么叫做隐式声明函数和内建函数。在论坛上有人这样回复:内建函数也是有原型的,当隐式声明和对应的内建函数的声明不一致的时候,可能会出问题,所以gcc就警告一下。

  最后一个默认启用是什么意思我就不清楚了,推测是使用内置函数。

  最后补充一个例子

  1. #include <stdio.h>
  2. //#include <math.h>
  3.  
  4. int main(){
  5. int i = ;
  6. printf("sin(1)=%f.\n", sin(i));
  7. //printf("sin(1)=%f.\n", sin(1));
  8. return ;
  9. }

  编译的时候使用 gcc -lm math.c -o math.out。会有【math.c:6:25: 警告:隐式声明与内建函数‘sin’不兼容 [默认启用]】警告,但是却还是能生成可执行文件,并且执行结果正确。这个例子中,我们没有包含math.h,所以sin肯定是一个隐式声明函数,会和内建函数不兼容,gcc发出警告,但是由于gcc无法优化sin(i),所以转而调用标准库的sin(这个调用应该是内置的,因为我们没有包含math.h,应该gcc自动调用math.c中sin函数)。同时连接的时候制定了-lm,连接成功。所以生成的可执行文件正常计算sin(1)。如果默认启用是使用隐式声明函数,那结果应该会有问题。

  好了,这些就是我对gcc内建函数的一些了解以及一些猜测,如有说的不好的地方,同学们见谅,如有说的不对的地方,欢迎指正。

关于gcc内置函数和c隐式函数声明的认识以及一些推测的更多相关文章

  1. gcc 内置函数

    关于gcc内置函数和c隐式函数声明的认识以及一些推测   最近在看APUE,不愧是经典,看一点就收获一点.但是感觉有些东西还是没说清楚,需要自己动手验证一下,结果发现需要用gcc,就了解一下. 有时候 ...

  2. GCC内置函数

    在C语言写的程序中,有时候没有包含头文件,直接调用一些函数,如printf,也不会报错,因为GCC内置和一些函数.如果包含了头文件,则去第三方库中链接这个函数,不再使用GCC内置的函数.每个编译器的内 ...

  3. php 内置的 html 格式化/美化tidy函数 -- 让你的HTML更美观

    php 内置的 html 格式化/美化tidy函数 https://github.com/htacg/tidy-html5 # HTML 格式化 function beautify_html($htm ...

  4. 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

    多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢? ...

  5. c语言中的隐式函数声明(转)

    本文转自:http://www.jb51.net/article/78212.htm 在c语言里面开来还是要学习c++的编程习惯,使用函数之前一定要声明.不然,即使编译能通过,运行时也可能会出一些莫名 ...

  6. 万恶之源:C语言中的隐式函数声明

    1 什么是C语言的隐式函数声明 在C语言中,函数在调用前不一定非要声明.如果没有声明,那么编译器会自己主动依照一种隐式声明的规则,为调用函数的C代码产生汇编代码.以下是一个样例: int main(i ...

  7. 2018-02-17 中文代码示例[译]Scala中创建隐式函数

    前言: 学习Scala时, 顺便翻译一下自己有兴趣的文章. 代码中所有命名都中文化了(不是翻译). 比如原文用的是甜甜圈的例子. 原文: Scala Tutorial - Learn How To C ...

  8. C语言的“隐式函数声明”违背了 “前置声明” 原则

    这个问题来源于小组交流群里的一个问题: 最终问题落脚在 : 一个函数在main中调用了,必须在main之前定义或者声明吗? 我在自己的Centos上做了实验,结果是函数不需要,但是结构体(变量也要)需 ...

  9. 深入探究js中的隐式变量声明

    前两天遇到的问题,经过很多网友的深刻讨论,终于有一个相对可以解释的通的逻辑了,然后我仔细研究了一下相关的点,顺带研究了一下js中的隐式变量. 以下文章中提到的隐式变量都是指没有用var,let,con ...

随机推荐

  1. 讲课笔记1——meta标签、表格标签

    图片属性:src(source): 图片的来源(路径),可以放置本地图片,也可以放网上的图片的url地址 [相对路径:        ./:当前目录            ../:跳出当前目录,到上一 ...

  2. jquery插件serializeFormToObject

    $.fn.serializeObject = function() { var o = {}; var a = this.serializeArray(); $.each(a, function() ...

  3. 解决IllegalBlockSizeException:last block incomplete in decryption异常

    解决IllegalBlockSizeException:last block incomplete in decryption异常分类: webkit android最近做个加解密的实现,虽然实现了, ...

  4. JavaScript中数据类型和typeof返回的数据类型

    除了上图,要注意三点:1.symbol是ES6中新增的数据类型 2.typeof(null)结果是Object 3.typeof(Object)和typeof(Array)的结果是function,因 ...

  5. var、let、const声明变量的区别

    let和var声明变量的区别:1.let所声明的变量只在let命令所在的代码块内有效.(块级作用域) for(let i=0;i<10;i++){ // ... } console.log(i) ...

  6. shell脚本,awk合并一列的问题。

    文件 file2内容如下:0 qwert1 asdfghjk2 asdjkl2 zxcvbn3 dfghjkll4 222224 tyuiop4 bnm 让第一列相等的合并成一行,不要第一列,也就是变 ...

  7. Codeforces Round #510 #B

    http://codeforces.com/contest/1042/problem/B 题意: 给出n种饮料,每种饮料还有一种或多种维生素(A或B或C),某人想集齐三种维生素,问最少需要花费多少? ...

  8. Python操作微信跳一跳

    “跳一跳”这个东西还是今天刚接触到的,看到了python群中有人再问“微信跳一跳的外挂有人写了没”,“早就有了”,“github”,“等着出个更详细的教程教程没看懂,主要没有用过adb”. 不过没关系 ...

  9. 【php】 布尔值判断

    当转换为 boolean 时,以下值被认为是 FALSE: 布尔值 FALSE 本身 整型值 0(零) 浮点型值 0.0(零) 空字符串,以及字符串 "0" 不包括任何元素的数组 ...

  10. python爬虫入门三:requests库

    urllib库在很多时候都比较繁琐,比如处理Cookies.因此,我们选择学习另一个更为简单易用的HTTP库:Requests. requests官方文档 1. 什么是Requests Request ...