当前位置: 首页 > news >正文

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

目录

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,否则未定义

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

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

 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 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样
下列文件
test.c
#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];
};
如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。这样工程量很大。
我们可以用条件编译,在头文件中写下下列代码。
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者写

#pragma once

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

http://www.dtcms.com/a/271827.html

相关文章:

  • 广告匹配策略的智能化之路:人工智能大模型的方法和步骤
  • 多模态大语言模型arxiv论文略读(156)
  • vivo Pulsar 万亿级消息处理实践(3)-KoP指标异常修复
  • 快速上手MongoDB与.NET/C#整合
  • 【AI大模型】LLM模型架构深度解析:BERT vs. GPT vs. T5
  • searxng 对接openweb-UI实现大模型通过国内搜索引擎在线搜索
  • 搜索引擎vs向量数据库:LangChain混合检索架构实战解析
  • 计算机视觉 之 数字图像处理基础
  • 基于 SpringBoot + Vue 的 IT 技术交流和分享平台的设计与实现
  • TCP-与-UDP-协议详解:原理、区别与应用场景全解析
  • 北斗舞动在线监测装置:电力安全的“智慧守护者”
  • SpringMVC @ExceptionHandler 典型用法
  • 了解去中心化金融在现代经济中的作用——安全交易新时代
  • 编写bat文件自动打开chrome浏览器,并通过selenium抓取浏览器操作chrome
  • 双指针-18.四数之和-力扣(LeetCode)
  • linux系统---ISCSI存储服务
  • Language Models are Few-Shot Learners: 开箱即用的GPT-3(二)
  • 节点小宝:手机图片备份至电脑功能实测体验
  • 同一类型,每条数据,执行不同逻辑
  • 偏振相机,偏振图像是怎么样的
  • WebGPU了解
  • 智能体决策机制深度剖析:ReAct、Plan-and-Execute与自适应策略
  • 云蝠智能VoiceAgent重构企业电话客服体系
  • PLC框架-1.3.2 报文750控制汇川伺服的转矩上下限
  • 【前缀和 BFS 并集查找】P3127 [USACO15OPEN] Trapped in the Haybales G|省选-
  • XSS(跨站脚本攻击)
  • RabbitMQ 消息队列:从入门到Spring Boot实战
  • Java 枚举详解:从基础到实战,掌握类型安全与优雅设计
  • 7-语言模型
  • CRT 不同会导致 fopen 地址不同