初学者对编译和链接的学习笔记(含预编译详解)

目录

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)内存,存储于静态内存中的变量在程序的整个执⾏过程 ⼀直保留他们的值。
. 终⽌程序。正常终⽌main函数;也有可能是意外终⽌。

下面的内容是对预编译的详细补充

1.预定义符号

c中有一些可以直接使用的预定义符号(它们是在预编译阶段被处理的)

FILE // 进⾏编译的源⽂件
LINE // ⽂件当前的⾏号
DATE // ⽂件被编译的⽇期
TIME // ⽂件被编译的时间
STDC // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义

cpp 复制代码
printf("file:%s line:%d\n", __FILE__, __LINE__);

2.#define定义常量

define name stuff

cpp 复制代码
#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定义标识符的低吼,不要加;

在进行文本替换的时候容易重复导致出错

cpp 复制代码
#define MAX 1000;

if(condition)
    max = MAX;
else
    max = 0;

这边if和else之间有了两条语句,其中一个是由于重复导致的空语句,而if在没有{ }的情况下默认只能跟一个语句,语法错误

3.#define定义宏(可以理解成特殊的比较"死板"的函数)

#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏 (define macro)。
宏的声明

define name( parament-list ) stuff

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。

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

上述代码等价于

cpp 复制代码
printf ("%d\n",a + 1 * a + 1 );

由于运算符的优先级发生了逻辑错误,所以在使用宏的时候尽量给参数加上括号

cpp 复制代码
#define SQUARE(x) (x) * (x)

这样就不会发生上述问题。

4.带有副作用的宏

x+ 1 ; // 不带副作⽤
x++; // 带有副作⽤

cpp 复制代码
#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.宏的替换规则

1.在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2.替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
宏参数和#define 定义中可以出现其他#define定义的符号 。但是对于宏,不能出现递归(自己定自己)。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。如pringf(" ")
" "中的宏无法被替换
6.宏和函数的对比

7.#和##

7.1#运算符

#运算符将宏的⼀个参数 转换为字符串字⾯量
它仅允许出现在带参数的宏的替换列表中
#运算符所执⾏的操作可以理解为**"字符串化**"
例:

cpp 复制代码
#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##运算符(记号粘合)

可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符

cpp 复制代码
int int_max(int x, int y)
{
    return x > y ? x : y;
}
    float float_max(float x, float y)
{
    return x > y ? x : y;
}
cpp 复制代码
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{                              \
    return (x>y?x:y);              \
}
cpp 复制代码
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.条件编译

用来决定是否将⼀条语句(⼀组语句)编译或者放弃。
对于调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。

cpp 复制代码
#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;
}

常用的条件编译指令

cpp 复制代码
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 OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif

12.头文件被包含

12.1头文件被包含方式

12.1.1本地文件

include "filename"

查找策略:
先在源⽂件所在⽬录 下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。
如果找不到就提⽰编译错误。

12.1.2库文件包含

#include <filename.h>

查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误
对于库⽂件也可以使⽤ " " 的形式包含但是这样做查找的效率就低些 (因为要找两次),当然这样也不容易区分是库⽂件还是本地⽂件了。

12.2嵌套⽂件包含

已知,#include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样
下列文件
test.c

cpp 复制代码
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}

test.h

cpp 复制代码
void test();
struct Stu
{
    int id;
    char name[20];
};

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。这样工程量很大。
我们可以用条件编译,在头文件中写下下列代码。

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者写

cpp 复制代码
#pragma once

这样头文件只会被编译一次了