C语言之编译链接

简介: C语言之编译链接

一、翻译环境和运行环境


在ANSI C的任何⼀种实现中,存在两个不同的环境


第一种是翻译环境,在这个环境中,源代码被转换为可执行的二进制指令。翻译环境即我们日常使用编译器,将一个 " xxx.c " 的文件最终变成一个 " xxx.exe " 的可执行文件的一个过程。


第二种是运行环境,它用于实际执行代码。运行环境一般是由操作系统对 " xxx.exe " 可执行文件进行解析执行的结果。

二、预编译


1.预定义符号

C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。

__FILE__ //进⾏编译的源⽂件 
__LINE__ //⽂件当前的⾏号 
__DATE__ //⽂件被编译的⽇期 
__TIME__ //⽂件被编译的时间 
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义 

2.#define

(1)#define定义常量

在define定义标识符的时候,不要在最后加上;

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

加了分号后,if和else之间就是2条语句,而没有大括号的时候if后边只能有一条语句。这⾥会出现语法错误。

(2)#define定义宏

#include <stdio.h>
#define MAX(x,y) (x>y?x:y)
 
int main() {
  int a = 1;
  int b = 5;
  int E = MAX(a, b);
  // 预处理后变成 int E = (1>5?1:5);
  printf("%d\n", E); 
  return 0;
}

(2.1)宏的陷阱

#include <stdio.h>
#define SQ(x) x*x
 
int main() {
 
  int a = 6;
  int e1 = SQ(a);
  // 预处理后:e1 = 6*6;  //36
 
  int e2 = SQ(a + 1);
  // 预处理后:int e2 = 6+1*6+1;  //8
  printf("%d\n", result1 );
  printf("%d\n", result2);
  return 0;
}
 

宏带来了运算符优先问题。由于 #define 在定义宏的时候,是直接对参数进行替换的。所以我们第二个预期为 " 49" 的结果,最终变成了 8.

#include <stdio.h>
#define SQ(x) (x)*(x)
 
int main() {
 
  int a = 6;
  int e1 = SQ(a);
  // 预处理后:e1 = (6)*(6);  //36
 
  int e2 = SQ(a + 1);
  // 预处理后:int e2 = (6+1)*(6+1);  //49
  printf("%d\n", result1 );
  printf("%d\n", result2);
  return 0;
}
 

可以利用加括号的方式,避免掉入宏的陷阱。解决优先级的运算符的问题

(2.2)带有副作用的宏参数

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
  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

(3)#define 定义宏和函数的区别

(4)#undef

#undef 用于移除一个宏定义,在err的上一行,就是移除了 MAX 这个宏。之后再使用的时,就会报错

#include <stdio.h>
#define MAX(x,y) (x>y?x:y)
 
int main() {
  int a = 1;
  int b = 25;
  int E = MAX(a, b);
  printf("%d\n", E);
  #undef MAX
  int E = MAX(a, b);  // err
  return 0;
}

(5)#和##

1.#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执行的操作可以理解为”字符串化“。有⼀个变量 int x = 10; 的时候,打印出 the value of x is 10 . 就可以写:

#define PRINT(n) printf("the value of "#n " is %d", n);

PRINT(x);当我们把x替换到宏的体内时,就出现了#x,而#x就是转换为

 printf("the value of ""x" " is %d", x);//预处理之后

2.## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称 为记号粘合 这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

#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;
}

3.头文件的包含

#include 头文件包含属于预编译的过程,它其实也是进行了相关的文本替换。但C语言 的头文件分为两种,第一种是和库相关的库文件;第二种是本地文件包含。


本地头文件的查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件。 如果找不到就提示编译错误。对于库⽂件也可以使用 “” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

#include<stdio.h> //库文件
#include"SList.h" //本地文件

#include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include指令的地方⼀样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 ⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。


如果xxx.h⽂件比较大,这样预处理后代码量会剧增。使用条件编译指令来防止多次调用xxx.h。  

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容 
#endif 

或者使用pragme

#pragma once
//文件

三、翻译环境


其实翻译环境是由编译和链接两个大的过程组成的,而编译又可以分解成:预处理(预编译)、编译、汇编三个过程

1. 预编译

预处理阶段主要处理那些源文件中#开始的预编译指令。如:#include,#define,处理的规则如下:

(1)将所有的#define 删除,并展开所有的宏定义。

(2)处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。

(3)处理#include预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置,这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。

(4)删除所有的注释

(5)添加行号和文件名标识,方便后续编译器生成调试信息等。或保留所有的#pragma的编译器指令,编译器后续会使用。经过预处理后的.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i⽂件中。所以当我们无法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认。

2.编译

编译过程就是将预处理后的文件进行⼀系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。


词法分析:将源代码程序被输⼊扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成⼀系列 的记号(关键字、标识符、字⾯量、特殊字符等)


语法分析:接下来语法分析器,将对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。

语义分析:由语义分析器来完成语义分析,即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分 析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息

3.汇编

汇编器是将汇编代码转转变成机器可执行的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根 据汇编指令和机器指令的对照表⼀⼀的进行翻译,也不做指令优化

4.链接

链接是⼀个复杂的过程,链接的时候需要把⼀堆文件链接在⼀起才生成可执行程序。 链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。 链接解决的是⼀个项目中多⽂件、多模块之间互相调用的问题。


目录
相关文章
|
1月前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
|
1月前
|
自然语言处理 编译器 Linux
【C语言篇】编译和链接以及预处理介绍(上篇)1
【C语言篇】编译和链接以及预处理介绍(上篇)
40 1
|
3月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
122 1
|
1月前
|
存储 自然语言处理 编译器
|
1月前
|
编译器 Linux C语言
【C语言篇】编译和链接以及预处理介绍(下篇)
【C语言篇】编译和链接以及预处理介绍(下篇)
32 1
【C语言篇】编译和链接以及预处理介绍(下篇)
|
1月前
|
自然语言处理 编译器 Linux
C语言中抽象的编译和链接原理
C语言中抽象的编译和链接原理
20 1
|
1月前
|
存储 C语言
【C语言篇】编译和链接以及预处理介绍(上篇)2
【C语言篇】编译和链接以及预处理介绍(上篇)
36 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
33 3
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
21 6
|
26天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10