【程序的编译(预处理操作)+链接】

简介: 【程序的编译(预处理操作)+链接】

程序的编译(预处理操作)+链接


1. 程序的翻译环境和执行环境

2. 详解编译+链接

2.1 翻译环境&&编译本身分成的几个阶段

2.2 运行环境

3. 预处理详解

3.1 预定义符号

3.2 #define

3.2.1 #define定义标识符

3.2.2 #define 定义宏

3.2.3 #define 替换规则

3.2.4 #和##

3.2.5 带副作用的宏参数

3.2.6 宏和函数的对比

3.2.7 命名约定

3.3 #undef

3.4 条件编译

3.5 文件包含

3.6.1 头文件被包含的方式:

3.6.2 嵌套文件包含

4. offsetof宏的实现

5. 总结:


1. 程序的翻译环境和执行环境


在ANSIC 的任何一种实现中,存在两个不同的环境


  • 第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
  • 第二种是执行环境,它用于实际执行代码。


2. 详解编译+链接


2.1 翻译环境&&编译本身分成的几个阶段


微信图片_20230221163750.png

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。


微信图片_20230221163846.png

微信图片_20230221163914.png


符号表一个程序最终只会变成一个符号表,因此,我们在合并符号表的时候需要去掉无效的符号,由于test.c的Add仅仅是声明作用,其不能真实的发挥作用,故,我们应保留add.c中的Add的符号,去掉test.c中的符号表。符号表最终会在链接中从符号表内部的符号地址从而引用出程序中的函数,因此符号表的重要性是显而易见的。


通过上述流程我们得到以下几个关键的编译步骤,即:


1.预处理 选项 gcc -E test.c -o test.i

  预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。


2.编译 选项 gcc -S test.c

  编译完成之后就停下来,结果保存在test.s中。


3.汇编 gcc -c test.c

  汇编完成之后就停下来,结果保存在test.o中。


2.2 运行环境


程序执行的过程:


1 .程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。


2 .程序的执行便开始。接着便调用main函数。


3 .开始执行程序代码。这个时候程序将使用一个运行的堆栈(stack,即函数栈帧),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。


4 .终止程序。正常终止main函数;也有可能意外终止。


3. 预处理详解


3.1 预定义符号


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

这些预定义符号都是语言内置的。


微信图片_20230221164249.png


3.2 #define


3.2.1 #define定义标识符


语法:

  • #define name stuff

举个列子:


#define MAX 1000
#define reg 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 MAX 100
#define STR "hello bit");
int main()
{
  int m = MAX;
  printf("%d\n", m);
  printf("%s\n", STR
  return 0;
}



#define不仅可以定义标识符,同样可以定义任何代码块,#define在预编译时就把define退换的标识符变成了替换的字符。


微信图片_20230221164414.png

提问:

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

  • 比如:

#define MAX 1000;

#define MAX 1000

建议不要加上 ; 这样容易导致问题。

比如下面的场景:


if(condition)
    max = MAX;//加上的话预处理会将MAX换成1000;,相当于多了一个;,造成语法错误
else
    max = 0;



3.2.2 #define 定义宏


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


下面是宏的申明方式:

#define name( parament-list ) stuff


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


注意:

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


如:

 #define SQUARE( X ) X * X

这个宏接收一个参数x.


如果在上述声明之后,把


SQUARE( 5 );



置于程序中,预处理器就会用下面这个表达式替换上面的表达式

5 * 5

注意:


这个宏存在一个问题:

观察下面的代码段:


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


乍一看,你可能觉得这段代码将打印36这个值。

事实上,它将打印11.

为什么?


替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:

printf (“%d\n”,a + 1 * a + 1 );


这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。


在宏定义上加上两个括号,这个问题便轻松的解决了:


#define SQUARE(x) (x) * (x)



这样预处理之后就产生了预期的效果:


printf("%d\n",(a + 1) * (a + 1) );

这里还有一个宏定义:

#define DOUBLE(x) (x)+(x)


定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。


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

这将打印什么值呢?


warning:


看上去,似乎是打印100,但事实上打印的是55.


我们发现替换之后:


printf("%d\n",10 * (5)+ (5) );

乘法运算先于宏定义的加法,所以出现了55.


因此,为了避免出现这种情况,应将stuff整体进行括号。


#define DOUBLE(x) ( (x) + (x) )

即所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。



3.2.3 #define 替换规则


在程序中扩展#define定义符号和宏时,需要涉及几个步骤。


  1. 1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,他们首先被替换。

  2. 2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

  3. 3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。


注意:


  1. 1.宏参数和#define定义中可以出现其他#define定义的符号。但对于宏,不能出现递归。

  2. 2.当预处理器搜索#define定义符号的时候,字符串常量的内容并不被搜索。


3.2.4 #和##

如何把参数插入到字符串中?


首先我们看看这样的代码:

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

我们发现字符串具有自动连接的特点。


  1. 那我们是不是可以写这样的代码?
#define PRINT(FORMAT, VALUE)\
    printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);

检验一下:


微信图片_20230224155110.png


确实可以这样。实际上,这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

  • 另外一个技巧是:

  • 使用 #把一个宏参数变成对应的字符串。


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

微信图片_20230221165025.png

即代码中的 #VALUE 会被预处理器处理为:

“VALUE”

##的作用


##可以把位于它两边的符号合成一个符号。


它允许宏定义从分离的文本片段创建标识符。


举个列子:

#define CAT(Class, Num) Class##Num
int main()
{
  int Class106 = 100;
  printf("%d\n", CAT(Class, 106));
  //printf("%d\n", Class106);
  return 0;
}


微信图片_20230224155231.png


3.2.5 带副作用的宏参数


当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性的效果。


#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
  //int m = MAX(2, 3);
  int a = 5;//6 7
  int b = 4;//5
  int m = MAX(a++, b++);
  //int m = ((a++) > (b++) ? (a++) : (b++));
     //6   //5    >  4    ? 6
  printf("m=%d ", m);//6
  printf("a=%d b=%d\n", a, b);//7 5
  return 0;
}

这样的带有自增性参数传参的时候虽然不会改变值,但是在接下来的步骤中会由于宏的条件发生不一样的运算结果。


3.2.6 宏和函数的对比


宏通常被应用于执行简单的运算。

比如在两个数中找出较大的一个。


#define MAX(a,b) ((a)>(b)?(a):(b))


原因:


1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。


2.更为重要的是函数的参数必须声明为特定的类型。

所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形,长整型,浮点型等可以用来比较的类型。宏是类型无关的。


宏的缺点: 当然和函数相比宏也有劣势的地方:


  1. 1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 2.宏是没办法调试的。
  3. 3.宏与类型无关,因此不够严谨。
  4. 4.宏可能会带来运算符优先级的问题,导致运算结果出现错误。

当然,宏也可以做到函数做不到的事情。比如:宏的参数可以出现类型,但是函数不能。


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


3.2.7 命名约定


一般来讲函数和宏的使用语法很相似,所以函数本身没法帮我们区分二者。那我们平时的一个习惯是:


  • 把宏名全部大写

  • 函数名不要全部大写


3.3 #undef

这条指令用于移除一个宏定义。

#undef NAME

//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。


#define MAX 100
#include<stdio.h>
int main()
{
   printf("%d\n",MAX);
   #undef MAX
   printf("%d\n",MAX);//此时这个位置的MAX未定义
   return 0;
}


3.4 条件编译


在编译一个程序的时候我们如果将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。


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


微信图片_20230224155543.png


微信图片_20230224155547.png

即当把宏注释掉,条件为假时,这条语句不参与编译,在预处理时已经消除了。


常见的编译指令有:


#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


它们都具有与上述相同的作用。


3.5 文件包含


我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。

这种替换方式很简单:

  • 预处理器先删除这条指令,并用包含文件的内容替换。
  • 这个源文件被包含10次,那就实际被编译10次。



3.6.1 头文件被包含的方式:

  • 本地文件包含
#include"filename"

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


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

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?

答案是肯定的,可以。


但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。


3.6.2 嵌套文件包含


如果出现这样的场景:


微信图片_20230221165800.png

如果出现这样的场景


comm.h和comm.c是公共模块。


test1.h和test1.c使用了公共模块。


test2.h和test2.c使用了公共模块。


test.h和test.c使用了test1模块和test2模块。


这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

那么解决这个问题的办法是什么?


用条件编译。


每个头文件的开头写:

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


或者:

#pragma once

就可以避免头文件的重复引入。


4. offsetof宏的实现


#include <stdio.h>
#include <stddef.h>
struct S
{
  char c1;
  int i;
  char c2;
};
#define OFFSETOF(type,m_name) (size_t)&(((type*)0)->m_name)
int main()
{
  struct S s = { 0 };
  printf("%d\n", OFFSETOF(struct S, c1));
  printf("%d\n", OFFSETOF(struct S, i));
  printf("%d\n", OFFSETOF(struct S, c2));
  //printf("%d\n", offsetof(struct S, c1));
  //printf("%d\n", offsetof(struct S, i));
  //printf("%d\n", offsetof(struct S, c2));
  return 0;
}


微信图片_20230221170104.png


5. 总结:


本篇文章主要介绍了程序编译执行的具体步骤及各种宏的实现,注意事项,好啦,以上就是本文的全部内容。

相关文章
|
27天前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
|
6月前
|
存储 自然语言处理 编译器
编译和链接(翻译环境:预编译+编译+汇编+链接​、运行环境)
编译和链接(翻译环境:预编译+编译+汇编+链接​、运行环境)
|
11月前
|
存储 自然语言处理 算法
程序的编译和链接
程序的编译和链接
47 0
|
6月前
|
自然语言处理 编译器 C语言
C语言程序编译和链接
在ANSI C的任何⼀种实现中,存在两个不同的环境。 第1种是翻译环境,在这个环境中源代码被转换为可执⾏的机器指令(⼆进制指令)。 第2种是执⾏环境,它⽤于实际执⾏代码。
29 0
|
6月前
|
存储 自然语言处理 编译器
C语言:编译和链接(从.c文件到输出结果的过程)
C语言:编译和链接(从.c文件到输出结果的过程)
|
6月前
|
编译器 Linux C语言
程序环境和预处理(含C语言程序的编译+链接)--2
程序环境和预处理(含C语言程序的编译+链接)--2
48 5
|
6月前
|
存储 编译器 程序员
程序环境和预处理(含C语言程序的编译+链接)--1
程序环境和预处理(含C语言程序的编译+链接)--1
42 0
|
存储 自然语言处理 编译器
程序编译和链接的过程/预处理符号和用法【C语言】
程序编译和链接的过程/预处理符号和用法【C语言】
69 0
|
自然语言处理 编译器 C语言
编译+链接和预处理
编译+链接和预处理
|
存储 自然语言处理 编译器
C语言进阶---程序的编译(预处理操作+链接)
本章主要介绍C语言的程序的编译,主要内容有:程序的翻译环境和执行环境、编译、链接细节剖析、运行环境、预处理详解。并且将一道百度面试题---模拟实现offsetof宏的功能。
C语言进阶---程序的编译(预处理操作+链接)