初学者对编译和链接的学习笔记(含预编译详解)
目录
1.翻译环境和运行环境
2.翻译环境由两大过程组成:编译和链接
2.1预处理(预编译)(.c->.i)(详解在下文)
2.2编译(.i->.s->.o)
2.2.1词法分析
2.2.2语法分析
2.2.3语义分析
2.4链接(解决一个项目中,多文件,多模块相互调用的问题)
3.运行环境
1.预定义符号
2.#define定义常量
3.#define定义宏(可以理解成特殊的比较“死板”的函数)
4.带有副作用的宏
5.宏的替换规则
7.#和##
7.1#运算符
7.2##运算符(记号粘合)
8.#undef
9.条件编译
12.头文件被包含
12.1头文件被包含方式
12.1.1本地文件
12.1.2库文件包含
12.2嵌套⽂件包含
1.翻译环境和运行环境
翻译环境:代码源在这里被转换成可执行的机器指令(二进制)。
运行环境:用于实际执行代码
2.翻译环境由两大过程组成:编译和链接
如下:
- 多个.c⽂件单独经过编译器,编译处理⽣成对应的⽬标⽂件。
- 在Windows环境下的⽬标⽂件的后缀是 .obj ,Linux环境下⽬标⽂件的后缀是 .o
- 多个⽬标⽂件和链接库⼀起经过链接器处理⽣成最终的可执⾏程序。
- 链接库是指运⾏时库(它是⽀持程序运⾏的基本函数集合)或者第三⽅库。
Windows系统是高度集成的,很多细节观察不到,一般用Linux环境观察。(需要自行配置)
编译过程大致如下:
2.1预处理(预编译)(.c->.i)(详解在下文)
gcc编译环境下,x想观察.i文件,使用预编译指令:
gcc -E test.c -o test.i
- 将所有的 #define 删除,并展开所有的宏定义
- 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif
- 处理#include 预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进⾏的,也就是说被包含的头⽂件也可能包含其他⽂件
- 删除所有的注释
- 添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等
- 保留所有的 #pragma 的编译器指令,编译器后续会使⽤
2.2编译(.i->.s->.o)
过程包括,词法分析,语法分析,语义分析
编译指令:
gcc -S test.i -o test.s
2.2.1词法分析
此过程代码被输入扫描器,进行词法分析,将代码中的字符转换成一系列有意义的记号,方便进行语法分析。
2.2.2语法分析
语法分析器,对记号进行语法分析,根据语法规则产生语法树(以表达式为节点,理顺表达式方便语义分析)
2.2.3语义分析
检查代码是否有意义,比如变量是否声明,类型是否兼容等
2.3汇编(将汇编代码转换成机器可执行的二进制指令)
指令
gcc -c test.s -o test.o
2.4链接(解决一个项目中,多文件,多模块相互调用的问题)
3.运行环境
程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成(单片机)。 程序的执⾏便开始。
接着便调⽤main函数。 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程 ⼀直保留他们的值。
下面的内容是对预编译的详细补充
1.预定义符号
c中有一些可以直接使用的预定义符号(它们是在预编译阶段被处理的)
__FILE__ //进⾏编译的源⽂件__LINE__ //⽂件当前的⾏号__DATE__ //⽂件被编译的⽇期__TIME__ //⽂件被编译的时间__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
例
printf("file:%s line:%d\n", __FILE__, __LINE__);
2.#define定义常量
#define name stuff
例
#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字
//register的作用是把指定变量放到寄存器中,增加运行速率,但是register只起建议性作用,要不要把变量放进去由系统决定
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
#define CASE break;case //在写case语句的时候⾃动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏
符)。注意空格的位置,容易出错
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
#define定义标识符的低吼,不要加;
在进行文本替换的时候容易重复导致出错
例
#define MAX 1000;if(condition)max = MAX;
elsemax = 0;
这边if和else之间有了两条语句,其中一个是由于重复导致的空语句,而if在没有{ }的情况下默认只能跟一个语句,语法错误
3.#define定义宏(可以理解成特殊的比较“死板”的函数)
#define name( parament-list ) stuff
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。
例
#define SQUARE( x ) x * x
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
上述代码等价于
printf ("%d\n",a + 1 * a + 1 );
由于运算符的优先级发生了逻辑错误,所以在使用宏的时候尽量给参数加上括号
#define SQUARE(x) (x) * (x)
这样就不会发生上述问题。
4.带有副作用的宏
x+1;//不带副作⽤x++;//带有副作⽤
#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);
上述代码就体现出有副作用的宏对代码的影响,后置++先用再加,运行完之后,y和x的值已经变了,得不到原来的值
-
z = 9
-
x = 6(只在比较时自增一次)
-
y = 10(比较和结果各自增一次)
5.宏的替换规则

7.#和##
7.1#运算符
#define PRINT(n) printf("the value of "#n " is %d", n);
int a=10;PRINT(a);//printf("the value of ""a" " is %d", a);打印:the value of a is 10
7.2##运算符(记号粘合)
int int_max(int x, int y)
{return x > y ? x : y;
}float float_max(float x, float y)
{return x > y ? x : y;
}
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \return (x>y?x:y); \
}
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{//调⽤函数int m = int_max(2, 3);printf("%d\n", m);float fm = float_max(3.5f, 4.5f);printf("%f\n", fm);return 0;
}
体会这种奇妙的用法,生成一个函数模板,只需要提供相应的类型就能生成相应的函数。
8.#undef
#undef NAME//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
9.条件编译
用来决定是否将⼀条语句(⼀组语句)编译或者放弃。
#include <stdio.h>
#define __DEBUG__
int main()
{int i = 0;int arr[10] = {0};for(i = 0; i < 10; i++){arr[i] = i;#ifdef __DEBUG__printf("%d\n", arr[i]);//为了观察数组是否赋值成功。#endif //__DEBUG__}
return 0;
}
常用的条件编译指令
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分⽀的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
12.头文件被包含
12.1头文件被包含方式
12.1.1本地文件
#include "filename"
查找策略:
12.1.2库文件包含
#include <filename.h>
查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误
12.2嵌套⽂件包含
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
test.h
void test();
struct Stu
{int id;char name[20];
};
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__
或者写
#pragma once
这样头文件只会被编译一次了