c语言编译过程详解,预处理,编译,汇编,链接(干货满满)

摘自:https://blog.csdn.net/weixin_41143631/article/details/81221777

楔子

我们在各自的电脑上写下代码,得明白我们代码究竟是如何产生的,不想了解1,0什么的,但这几个环节必须掌握吧。

我们的代码会经过这4个环节,从而形成最终文件,c语言作为编译语言,用来向计算机发出指令。让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动。

预处理, 展开头文件/宏替换/去掉注释/条件编译                      (test.i main .i)
编译,    检查语法,生成汇编                                                      ( test.s  main .s)
汇编,   汇编代码转换机器码                                                         (test.o main.o)
链接     链接到一起生成可执行程序                                              a.out
 

预处理

预处理如锲子中所言,是一种展开,下表是常用的一些预处理命令

还有下列几种预处理宏(是双下划线)

__LINE__ 表示正在编译的文件的行号
__FILE__表示正在编译的文件的名字__DATE__表示编译时刻的日期字符串,例如: "25 Dec 2007"
__TIME__ 表示编译时刻的时间字符串,例如: "12:30:55"
__STDC__ 判断该文件是不是定义成标准 C 程序
我的vs2013不是定义的标准c语言

宏函数很好用,是直接展开,在这我顺便说一下宏的好处和坏处。

宏优点1代码复用性2提高性能

宏缺点1 不可调试(预编译阶段进行了替换),2无类型安全检查3可读性差,容易出错。

这里附上《c和指针》中的一张表格,总结宏和函数十分到位,我就不多说了

宏函数很皮,#define定义一个比如判断大小,替换常量,很是方便。

不过我现在也就用下,#define ERROR_POWEROFF -1,#define _CRT_SECURE_NO_WARNINGS 1这样的和编译器有关的东西,不会去写宏函数,宏函数这东西,可读性特别差,在c++中,一般用const/枚举/内联去替代宏。

但是,define宏在某些方面真的是非常好用,我很推荐。

1.替代路径

#define ENG_PATH_1 C:Program Files (x86)

2.针对编译器版本不兼容报错

#define _CRT_SECURE_NO_WARNINGS 1

3.条件编译

#ifdef 标识符
程序段 1
#else
程序段 2
#endif

4.使用库中的宏

vc++中有许多有意思的宏,都是大牛们写出来的,真的是充满智慧,十分刁钻,怎么学也学不完,我个人担心出错就很少写宏,用函数代替了。在以后的博客中我会记录一些常用的,充作笔记。

emmm,当然,还有其他许多重要的预处理。

比如

include

#include <filename>

尖括号是预处理到系统规定的路径中去获得这个文件(即 C 编译系统所提供的并存放在指定的子目录下的头文件)。找到文件后,用文件内容替换该语句。如stdio.h

#include“filename”

“”则是预处理我们自己第三方的文件,如程序员小刘写的Date.h,我们就可以include“Date.h”

#error 预处理,#line 预处理,#pragma 预处理

#error 预处理指令的作用是,编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。

这个我没写过,但碰到过很多次,在编写mfc代码中,拉入控件时我加入密码框控件,OS编译时会自动弹出#error 提示我该编辑框为密码,注意明文问题

#line 的作用是改变当前行数和文件名称,如#line 28  liu 

目前我没使其派上用场,但了解为好。

#pragma 是比较重要且困难的预处理指令。

#pragma once 

这个的做用就是防止头文件多次包含

当然,还有另外一种风格,防止被包含,我同时给出来

是巧妙地利用了define宏

  1.  
    #ifndef _SOME_H
  2.  
    #define _SOME_H
  3.  
     
  4.  
     
  5.  
    ...//(some.h头文件内容)
  6.  
     
  7.  
     
  8.  
    #endif

变量的防止重复定义则利用extern,在头文件中不初始化只声明。引用该头文件即可,在链接过程中。就可以使用到这个变量。

(附:extern在c++中经常用于  extern "C"  告诉编译器下面是c语言风格)

#pragma warning

#pragma warning( disable : 4507 34; once : 4385; error : 164 )
等价于:
#pragma warning(disable:4507 34) // 不显示 4507 和 34 号警告信息
#pragma warning(once:4385) // 4385 号警告信息仅报告一次
#pragma warning(error:164) // 把 164 号警告信息作为一个错误。

另外还有

#pragma pack

使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
在#pragma pack (n)和#pragma pack ()之间的代码按 n 个字节对齐。

字节对齐,我将另起炉灶,在另外一篇博客中归纳总结。

#pragma pack(push) //保存当前对其方式到 packing stack
#pragma pack(push,n) 等效于
#pragma pack(push)
#pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按 n 字节对齐
#pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐

#运算符和##预算符

#define SQR(x) printf("The square of "#x" is %d. ", ((x)*(x)));

这段代码中#就是帮助x作为一个变量,表现出来,而不是一个简单的字母

如果有#,SQR(3)运算出来就是

The square of 3  is 9

如果没有# SQL(3)运算出来就是

The square of x  is 9

##预算符

##把两个语言符号组合成单个语言符号

编译

编译阶段是检查语法,生成汇编,这个属于程序员的必备知识,我们学习一门语言第一步就是知晓语法,其中比较生涩的有左值右值,指针的使用,内存的管理,数据结构的使用,这将会是一场持久战 ,贯穿在整个学习生涯。

在这里我截取优先级问题,这个可能会通过编译但是不一定达到程序员想要的结果。

在这里,我引用《c语言深度解剖》中的一张表格

汇编

  汇编代码转换机器码   这个阶段,非底层的程序员不需要考虑, 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。

链接

开头我引用一下百度百科的介绍

静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。

将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。该可执行文件会变大很多,一般是调用自己电脑上的。

静态库和应用程序编译在一起,在任何情况下都能运行,而动态库是动态链接,文件生效时才会调用。

很多代码编译通过,链接失败就极有可能在静态库和动态库这出现了纰漏,要视情况解决。缺少相关所需文件,就会链接报错。这个时候就要检查下本地的链接库是不是缺损。

原文地址:https://www.cnblogs.com/LiuYanYGZ/p/14296864.html