【程序的编译和预处理】源文件到可执行程序到底经历了什么?

简介: 【程序的编译和预处理】源文件到可执行程序到底经历了什么?

1.程序的翻译环境&2.执行环境

C语言程序实现的两种环境:

第一步:翻译环境--使得源程序转换为机器可执行的机器指令

第二步:执行环境--实现可执行代码

3cfd0b766315437db48f250a1aebfb0a.png

3.详解:程序的编译和链接(翻译环境)


fb060ba8385b45f391b1b705419d92c9.png

多个test.c文件,多个test.obj,生成一个test.exe

编译器介绍:



e96309efa5704d5a92391dd44a8767da.png


链接库:库文件里的库函数/第三方库


866eb4a4cf5848a99e0d0f4c70ff1bb4.png

296566b3c0ca49cc9dce0baf29e504af.png

85b72bea656e45e8b90d571b4e0dced3.png

4.预处理符号详解

4-1内置的预处理符号


328f7212109c448b817f12f15e1fac51.png

int main()
{
  for (int i = 0; i < 10; i++)
  {
    printf("name:%s\tfile:%s \tline:%d \tdate:%s \ttime:%s \ti:%d\n",__func__,__FILE__, __LINE__, __DATE__, __TIME__);
  }
  return 0;
}

ba0c5a59b7a74c30a68f551b2fd25164.png


5.预处理指令

5-1#define定义符号

1. #define NUM 100
2. #define STR "hello world"//字符串也可以使用预处理定义符号

e1299605f0824a068446174ef0e43815.png

5-2#define定义宏

#define MAX(x,y)  ((x)>(y)?(x):(y))
int main()
{
  int a = 10;
  int b = 20;
  int c = MAX(a, b);
  printf("%d\n", c);
  return 0;
}

注意:

  1. #define定义符号和宏的时候不要带分号
  2. 参数列表的左括号必须和name紧邻(函数可以,宏不可以)
  3. 写宏的时候,对于参数不要吝啬括号
#define NUM 100;//错误用例1
#define DOUBLE (x) x*x//错误用例2和3

5-3#define替换规则

#define M 100
#define DOUBLE(x) ((x)+(x))
int main()
{
  int a = DOUBLE(M);
  printf("%d\n", a);
  return 0;
}
//第一步:-替换M- int a=DOUBLE(100)
//第二步:-替换X- #define DOUBLE(100) 200
//第三步:-替换DOUBLE(100)- int a=200;

ce7fa425f3b54237a2de5b979fb9631c.png

6。#和##宏的妙用

6-1#

6-1-1例子1:单纯只是研究辅助打印的信息,没有考虑参数的类型

问:怎么把参数插入到一个字符串中?

想法2:函数
//void Print(int n)
//{
//  printf("the value of n is &d\n", n);
//}
//想法3:宏
//#define PRINT(N) printf("the value of N is %d\n",N)//想法3
//这个法子和想法2一样,字符串中的x都没法得到替换(字符串中的符号不会被直接替换)
//int main()
//{
//  int a = 10;
//  //printf("the value of a if %d\n", a);
//    Print(a);
//
//  int b = 20;
//  //:想法1:一个一个打
//  //printf("the value of b is %d\n", b);
//  PRINT(b);
//
//
//  return 0;
//}
//想法4:(最满足用户的做法)#
#define PRINT(N) printf("the value of "#N" is %d\n",N)
int main()
{
  //基石
  printf("hello world\n");
  printf("hello ""world\n");
  int a = 10;
  PRINT(a);
//等价于:printf("the value of ""a"" is %d\n",N);
  return 0;
}

#的作用是把N 变成"N",N 变字符串N


c027397161ba49a5b531186e0d3d79b6.png

a0004e85a13b439e9c0f5d4654bfb0e8.png

6-1-2:考虑到传入的参数的类型 (这使得我想到函数重载

#define PRINT(N) printf("the value of "#N" is %d\n",N)
int main()
{
  int a = 10;
  double pai = 3.14;
  PRINT(a);
  PRINT(pai);
  return 0;
}

6-2##

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

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

#define CAT(name,num) name##num
int main()
{
  int song100 = 105;
  printf("%d\n", CAT(song, 100));
//等价于printf("%d\n",song100)
  return 0;
}

dce7860757d9414d99ddd77c2c21fe59.png

这里我想解释一下一个东西:


b3e543f866a640999ed709f30704c12c.png

解释:先进行预处理(先合成了classi),再编译

6-3带有副作用的宏参数🌸

++在宏中的副作用

#define MAX(m,n) ((m)>(n)?(m):(n))
int main()
{
  //int a = 0;
  //int b = a + 1;
  //b = a++;//带有副作用的语句
  //带有副作用的宏参数
  int a = 10;
  int b = 20;
  int c = MAX(a++, b++);
  //相当于int c=(a++)>(b++)?(a++):(b++);
  //             11    21           22
  printf("%d\n", a);//11
  printf("%d\n", b);//22
  printf("%d\n", c);//21
  return 0;
}

原因:

  • 宏的参数是不带计算的替换的(函数的参数是带计算拷贝的)
  • 如果宏中有多份++就会执行多次
  • 0ea8d0f34cdc4b8eb86c72d89d3b738e.png

7.宏和函数的对比(蓝色标明考虑角度)

宏没有函数栈帧的开销,也没有了函数递归;

宏只是简单替换,没了类型检查,也产生了优先级和副作用,和无法调试的问题。

    宏和函数的对比                                                    


宏的优点:


没有函数调用和函数返回的开销

宏的参数与类型无关

宏的缺点:


宏是没有办法调试的

宏在使用不当,可能会带来运算符优先级和++的副作用问题

宏是没办法递归的


8.条件编译

应用:stdio.h头文件中好多这种东西,你要看得懂

#define NUM 1
int main()
{
  //#if-#else-#endif 分支的条件编译
#if 0
  printf("hehe\n");
#else
  printf("haha\n");
#endif
  //#if-#elif-(#else)-#endif 多分支的条件编译
#if NUM==1
  printf("1\n");
#elif NUM==2
  printf("2\n");
#else
  printf("0\n");
#endif
  //判断是否#define符号的两种方法
  //方法1:
#if defined(NUM)
  printf("1\n");
#endif
  //方法2:
#ifdef NUM
  printf("2\n");
#endif
  //判断是否#undefine符号的两种方法
  //方法1:
#if !defined(NUM)
  printf("1\n");
#endif
  //方法2:
#ifndef NUM
    printf("2\n");
#endif
    return 0;

9.预处理指令#include

9-1#include<stdio.h>和#inlcude"stdio.h"的区别

查找策略:


#include“include”:先在源文件的目录中查找,没找到再去目标库里查找


#include<stdio.h>:直接去目标库里查找


所以你的#include<stdio.h>可以写成#include"stdio.h"


但是你的contact.c中不能把#include"conta


ct.h"写成#include<contact.h>


05ce3c4a204f402d91019e26c2b4d3bd.png


推荐:

引用自己定义的头文件使用"""

引用库里的头文件使用<>

9-2防止头文件被重复包含的两种方法:(写在头文件里的)

多次包含了头文件的危害:平添了几千行代码,使得编译器处理起来压力大

方法1:


//test.c
#include<stdio.h>
#include"stdio.h"
#include<stdio.h>
//test.h
#ifndef __TEST_H__
#define __TEST_H__
#endif

方法2:

1. //test.c
2. #include<stdio.h>
3. #include<stdio.h>//无效,这一次头文件并没有被包含
4. 
5. #test.h
6. #pragma once

bdd35f4458a74492b2e1cd4614430108.png

10.面试题:宏实现offsetof

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明。

首先我们来看看offsetof:

作用:返回type类型的结构体中,member结构体变量的地址相对于结构体起始地址的偏移量
原型:size_ t offsetof(type,member)
头文件:#include<stddef.h>
第一个参数:type类型的结构体
第二个参数:结构体成员变量名memer
返回值:size_t,无符号整型,可使用%zd或%ud打印
单词;offset偏移 of   offsetof也就是...的偏移量
例子:
struct Str
{
  char c;
  int i;
  char t;
};
int main()
{
    struct Str s={ 0 };
  printf("%zd\n", offsetof(struct Str, c));//0
  printf("%zd\n", offsetof(struct Str, i));//4
  printf("%zd\n", offsetof(struct Str, t));//8
  return 0;
}
struct Str类型的结构体的起始地址:&(s.c)
成员变量名为c的地址:&(s.c)
则成员变量为c的地址相对于结构体的起始地址的偏移量offset==&(s.c)-&(s.c);

322a5e556bfd45829eba0749378ae0b2.png

这里我们假设其实地址就是0,偏移量就就是&(s.c)-0==&(s.c),也就是说每一个成员变量的地址就变成了偏移量。


使用宏实现offsetof:

struct Str
{
  char c;
  int i;
  char t;
};
#define OFFSETOF(type,member)  (size_t)&(((type*)0)->member)
int main()
{
  struct Str s={0};
  printf("%zd\n", OFFSETOF(struct Str, c));
  printf("%zd\n", OFFSETOF(struct Str, i));
  printf("%zd\n", OFFSETOF(struct Str, t));
  return 0;
}

223a5e9e423942a18c50197a7026bd08.png

目录
相关文章
|
2月前
|
Ubuntu JavaScript Java
程序从源代码到可执行程序
这篇文章介绍了从源代码到可执行程序的转换过程,包括预处理、编译、汇编和链接四个主要步骤,并提到了解释性语言、脚本语言和超文本语言的不同处理方式。
30 0
|
6月前
|
编译器 C语言
C语言编译详解:GCC分步编译与一次编译多个文件
C语言编译详解:GCC分步编译与一次编译多个文件
529 2
|
自然语言处理 Linux C语言
C语言代码到可执行文件的过程
我们写好的代码是不能直接运行的,想要运行需要预编译、编译、汇编、链接四大步骤变成可执行目标文件,本文将简单介绍这四大步骤。
|
自然语言处理 编译器 Linux
gcc编译的过程
GCC 仅仅是一个编译器,没有界面,必须在命令行模式下使用。通过 gcc 命令就可以将源文件编译成可执行文件。
177 0
|
自然语言处理 编译器 程序员
文件编译和预处理
文件编译和预处理
101 0
|
自然语言处理 编译器 Linux
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解(二)
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解
593 0
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解(二)
|
存储 Ubuntu Unix
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解(一)
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解
366 0
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解(一)
|
NoSQL 编译器 Linux
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解(三)
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解
317 0
【三、深入浅出GCC编译器】一个源文件到可执行文件是如何生成的:GCC编译工具链及编译参数详解(三)
|
编译器 C语言 C++
编译的四个过程-预处理、编译、汇编、链接
编译的四个过程-预处理、编译、汇编、链接
|
Linux C语言 C++
gcc编译的四个阶段解析
gcc编译的四个阶段解析
409 0
gcc编译的四个阶段解析