C语言----程序编译(预处理)

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言----程序编译(预处理)相关的知识,希望对你有一定的参考价值。

下面是从test.c到运行结果的大体过程

编译

1)预编译

gcc test.c -E > test.i
预处理后停止
完成文本操作

  1. 完成头文件包含
  2. #define定义的符号和宏的替换
  3. 去除注释

2)编译

gcc test.i -S
生成test.s的文件
把C语言代码转化为汇编代码
编译部分深入学习编译原理

  1. 语法分析
  2. 词法分析
  3. 语义分析
  1. 符号汇总

    1).把c程序所有的全局符号汇总(add.c中的全局符号是Addtest.c中全局符号为mainAdd)

3)汇编

PE ELF COFF概念

PEPortable Executable)意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE格式文件,微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)


ELFExecutable and Linkable Format)意为可执行与可链接格式,一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件,是Linux的主要可执行文件格式。,用来取代COFF
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定

gcc test.s -c
生成test.o文件,为ELF格式(类似与Windows下的.obj目标文件,为COFF格式)
把汇编代码转化为二进制指令(机器指令)

readelf -s add.oreadelf -s test.o分别查看汇总的全局符号
将编译中汇总的全局符号分别生成符号表:符号|地址**(分别是test.cadd.c)
| Add | 0x0000 | | | Add | 0x1008 |
| main| 0x1004 | |

链接

把多个目标文件(.o文件)进行链接
test.o+add.o->a.out(.out可执行文件也是ELF格式)

  1. 合并段表(每个ELF文件相当于一个段表)
  1. 符号表合并和重定位
    | Add | 0x1008 |
    | main| 0x1004 |
    Add地址有效相当于通过地址找到函数

运行环境

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须
    由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同
    时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

预定义符号(注意是两根杠

_ _ FILE_ _ :进行编译的源文
_ _ LINE _ _ :文件当前的行号
_ _ DATE _ _ :文件被编译的日期
_ _ TIME _ _ :文件被编译的时间
_ _ STDC _ _ :如果编译器遵循ANSI C,其值为1,否则未定义
_ _ FUNCTION _ _ :函数名
_ _ VA_ARGS _ _ :是系统预定义宏,被自动替换为参数列表


#define

预处理的替换,只会做最简单的浅层替换,而不会去考虑这个字符串是从哪里来的

1)#和##

字符串有自动连接的特点,如打印hello world

char* p = "hello ""world\\n";
printf("hello"," world\\n");
printf("%s", p);

只有当字符串作为宏参数的时候才可以把字符串放在字符串中

#define PRINT(FORMAT, VALUE)\\
printf("the value is "FORMAT"\\n", VALUE);
PRINT("%d", 10);

# 的用法:
# VALUE 会预处理为 " VALUE "
打印 the value of i+3 is 13

int i = 10;
#define PRINT(FORMAT, VALUE)\\
printf("the value of " #VALUE "is "FORMAT "\\n", VALUE);
PRINT("%d", i+3);

## 的用法:
##可以把位于它两边的符号合成,从而产生新的符号(词法层次),它允许宏定义从分离的文本片段创建标识符
sum##num += value;带入值变为sum5 += 10;

#define ADD_TO_SUM(num, value)
sum##num += value;
ADD_TO_SUM(5, 10);

2)#define

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。(语法规定,当一个宏遇到自己时,就停止展开)
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

#define CASE break;case //在写case语句的时候自动把 break写上


#define LOG( format, ... ) printf( format, _ _VA_ARGS_ _ )
LOG( "%s %d", str, count );
//变参宏


在define定义标识符时建议不要加上‘;’
当你不小心加上一个‘;’号时由于没有括号else会匹配最近的;语句,如下例子

#define MAX 1000;
if(condition)
	max = MAX;
else
	max = 0;

在宏定义上加上两个括号 #define SQUARE(x) (x) * (x)以防出现以下

#define SQUARE( x ) x * x
int a = 5;
printf("%d\\n" ,SQUARE( a + 1) );

替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了: printf ("%d\\n",a + 1 * a + 1 ); 所以他会打印11而不是想象的36

外面也要加上括号 #define SQUARE(x) ((x) + (x))以防出现以下

#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\\n" ,10 * DOUBLE(a));

乘法运算先于宏定义的加法,所以出现了printf ("%d\\n",10 * (5) + (5));打印55而非想象的100

宏参数的prescan
因为ADDPARAM( 1 ) 是作为PARAM的宏参数,所以先将ADDPARAM( 1 )展开为INT_1,然后再将INT_1放进PARAM

#define PARAM( x ) x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );

如果PARAM宏里对宏参数使用了#或##,那么宏参数不会被展开 如下:将被展开为"ADDPARAM( 1 )

#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );

3)宏与函数对比

宏的优点
宏的参数可以出现类型,但是函数做不到

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

#define MAX(a, b) ((a)>(b)?(a):(b))
对于此代码功能选用宏的原因

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
    函数转化为汇编代码会更多
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

宏的缺点

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错
  5. **带副作用的宏参数**
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\\n", x, y, z);

z = ( (x++) > (y++) ? (x++) : (y++));
x=6 y=10 z=9


命令行定义

当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,可以用命令行定义
如我们在程序中定义了一个 int array [ARRAY_SIZE];
gcc -D ARRAY_SIZE=10 programe.c(在命令行中输入ARRAY_SIZE的值)

条件编译

调试性的代码,不想删除,但不想执行,我们可以选择性的编译(不同于注释)

#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。

如:

#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

多个分支的条件编译

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

判断是否被定义

#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

嵌套指令

#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

文件包含

#include "filename"
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
linux环境的标准头文件的路径:/usr/include

#include <filename>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误

嵌套包含
避免重复包含我们可以使用:

  1. #ifndef _ _ TEST_H _ _
    #define _ _ TEST_H _ _
    //头文件的内容
    #endif //_ _ TEST_H_ _
  2. #pragma once

其他预处理指令

#error token-string
token-string:用户自定义的错误消息
当预处理器预处理到#error命令时将停止编译并输出用户自定义的错误消息

#line digit-sequence "filename"
digit-sequence:_ _ LINE _ _
"filename":_ _ FILE _ 的内容
命令#line改变
_ LINE _ _ FILE _ _的内容,它们是在编译程序中预先定义的标识符
The translator uses the line number and filename to determine the values of the predefined macros FILE and LINE

#pragma(详见MSDN)
alloc_text ,comment ,init_seg1 ,optimize
auto_inline ,component, inline_depth ,pack
bss_seg ,data_seg, inline_recursion ,pointers_to_members1
check_stack ,function ,intrinsic ,setlocale
code_seg, hdrstop ,message, vtordisp1
const_seg ,include_alias ,once ,warning

以上是关于C语言----程序编译(预处理)的主要内容,如果未能解决你的问题,请参考以下文章

C语言预处理 编译 汇编 链接四个阶段

#yyds干货盘点# C语言程序编译与预处理

C语言编译

c语言中的“宏”是指啥?

C语言预处理

C语言——程序环境和预处理