长文详解程序运行是个怎样的环境?预处理阶段在做什么?程序中我们不知道的一些事~(代码片段)

凛音Rinne 凛音Rinne     2023-01-13     264

关键词:

程序环境和预处理

老规矩笔记在gitee自取~:程序环境和预处理笔记
❤️欢迎喜欢学习C/C++的朋友互关一起努力!!❤️



一、程序的环境

在ANSI C(国际标准c语言)的任何一种实现中,存在两个不同的环境。

❄️翻译环境:在这个环境中源代码被转换为可执行的机器指令

❄️执行环境:它用于切实执行代码

下图详解(VS底下的编译器是cl.exe,连接器是link.exe

  • 每个文件(.c文件)都会经过编译器处理,变成目标文件(.obj)

  • 头文件(.h)的包含在预编译完成,生成文件(.i)

    #include/#define/#pragma等,是预编译口令

    注释也会在预编译阶段删除掉

  • 编译阶段完成,将c语言代码转化成汇编代码,生成汇编代码文件(.s)

    ❄️语法分析
    ❄️词法分析
    ❄️语义分析
    ❄️符号汇总(只汇总全局符号)

  • 汇编代码进行汇编操作,将汇编代码转化成二进制代码,生成符号表并且生成目标文件(类型linux/vs .o/.obj)

    此文件可以在debug目录(运行程序之后生成)下找到


    文件中数据的存储格式为elf格式

    readelf指令可以查看

  • 链接库包含库函数文件

  • 所有的目标文件+链接库通过链接器编译成可执行文件(.exe)

  • 链接各文件中操作:

    ⚡️合并段表
    ⚡️符号表的合并和符号表的重定位(相同的地方合并)

更深层次理解

☁️预处理:相当于根据预处理指令组装新的C/C++程序。

经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。

☁️编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。

☁️链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。


二、预处理符号

以下符号在预处理阶段处理

符号都是语言内置,无需包含文件

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

注意: __ STDC __ 在VS下不支持ANSI C所以未定义;而在Linux环境下,gcc对ANSI C支持


三、预处理指令#define

1. 定义标识符常量

#define MAX 100

int main()

    printf("%d", MAX);
    
    return 0;

预编译阶段,MAX已经变成了100

注意:

  • ⛄️标识符常量后不要加;,容易出现问题

  • ⛄️当预处理器/编译器中搜索不到#define定义的符号


2. 定义宏

与函数不同,宏是把参数替换到文本中

#define ADD(x, y) ((x) + (y))

int main()

    int a = 2;
    int b = 3;
    ADD(2, 3);//计算2+3
    
    return 0;

注意:

  • 🐳参数列表的左括号必须与宏名称紧邻

  • 🐳如果两者之间有任何空白存在,参数列表就会被解释为后面式子的一部分。

  • 🐳数值表达式进行求值的宏定义都应在外面加上括号,避免在使用宏时由于参数中的操作符或邻近操作符产生作用导致计算错误

  • 🐳宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归

  • 🐳注意++和–等有副作用的符号

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

    #define MAX(a, b) ( (a) > (b) ? (a) : (b) )
    
    int main()
    
        int x = 5;
        int y = 8;
        int z = MAX(x++, y++);
        printf("x=%d y=%d z=%d\\n", x, y, z);
    
        return 0;
    
    
    

    以这个三目操作符为例,参数a和b分别都出现了2次,三目操作符的意思是结果为真,返回a,结果为假,返回b

    判断语句:a++ > b++ ?

    这里比较的时候a和b是5和8

    比较完a和b是6和9

    返回结果:返回b的值,z被赋值为9

    宏结束后:返回b++结束后,y为10

    最后结果


3. #define和tpyedef区别

#define INT int*
typedef int* INT_T;
 
int main()

    INT a, b;//这里是int* a; int b;
    INT_T c, d;//这里是 int* a; int* b 

  • 🌼所以这里define仅仅是符号的替换,而typedef起作用效果的对象,却是之后的所有变量

  • 🌼🌼如果这里的INT只是int* 效果也是一样,所以typedef的优点也在于此

  • 🌼🌼🌼并且提醒大家以后定义变量尽量一行一个变量以防出错


4. 替换规则

在定义符号和宏的时候,需要注意:

  • 🐚在调用宏时,查看参数否包含任何由#define定义的符号,如果有将先被替换
  • 🐚对于符号,替换文本被插入到程序中原来文本的位置
  • 🐚对于宏,参数名被他们的值所替换
  • 🐚最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

5. # 和 ##

1. #

将一个宏参数变成对应的字符串

直接上一串代码体验:

我们想要实现printf的一个函数

int a = 1;
int b = 2;
printf("a的值是%d", a);
printf("b的值是%d", b);

//以上代码用函数实现
void print(int x)

    printf("x的值是%d", x);

但是打印出来是:x的值是1,x的值是2

实现不了我们想要的:a的值是1,b的值是2

define加上#就有奇效:

#define PRINT(x) printf(""#x"的值为%d", x)

int main()

    int a = 2;
    PRINT(a);
    
    return 0;

结果是:

原理是什么样的?

🌱🌱🌱因为字符串有相邻自动连接功能

printf("hi""hello");

而#相当于将宏参数x左右加上""

使得x变成一个符号,这就是将宏参数变成对应的字符串

实际上,上面的""#x"的值为x" 实则是 " " 、 ''x" 、 "的值为%d ",连接起来


2. ##

将两边的符号合成一个符号

#define PRINT(a, b) a##b

int main()

    int a = 1;
    int b = 0;
    int ab = 10;
    printf("%d", PRINT(a, b));//相当于打印ab

    return 0;

🌾🌾🌾注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义

不必过于深究,只要理解其作用就行


6. 宏与函数的优缺点

比较内容#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副 作用的参数求值可能会产生不可预料的结果函数参数只在传参的时候求值一 次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法 的,它就可以使用于任何参数类型函数的参数是与类型有关的,如 果参数的类型不同,就需要不同的函数,即使他们执行的任务是 不同的
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

命名约定:

  • 🎍宏命名全部大写
  • 🎍函数名可以不全大写

四、#undef 移除宏定义

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

五、命令行定义

有一些编译器提供了一种能力,允许在命令行中定义符号

比如在linux环境下,gcc编译器,使用指令,在预处理阶段给变量重新赋值

⛅️⛅️假定:某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大,我们需要一个数组能够大一点


六、条件编译

满足条件,代码参与编译,不满足就不参与

1. 简单条件编译

int main()

    int a = 1;
#if 0//这里写表达式或者符号(常量)
//#if a 这里是错误的,因为在预编译过程a还没创建
    //a创建的过程在运行的过程
    printf("%d", a);//什么都不打印
#endif
    
    return 0;


2. 判断是否定义

对于宏

//定义了
#if defined(symbol)
#ifdef symbol

//没有定义
#if !defined(symbol)
#ifndef symbol
//两种写法均可

代码感受:

#define MAX 0

int main()

    int a = 1;
#if defined(MAX)//测试定义了没有,与值无关
    printf("%d", a);
#endif
    
#if max//根据MAX的值判断
    printf("%d", MAX);
#endif
    return 0;

运行结果:


3. 多个分支的条件编译

#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif


4. 嵌套指令

#if defined(MAX)
 #ifdef OP1
 ADD1();
 #endif
 #ifdef OP2
 ADD2();
 #endif
#elif defined(MIN)
 #ifdef OP2
 DEL2();
 #endif
#endif

用法有点类似if/else 语句


七、头文件包含方式

1. 本地文件包含

#include "test.c"

🎓先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件

🎓如果找不到就提示编译错误


2. 库函数包含

#include <stdio.h>

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


3. 文件嵌套包含

可能存在一种,头文件在无意识中包含了两次,意味着预编译期间,会拷贝两份头文件内容

为了防止上述情况形成

我们用条件编译解决这个问题

#ifndef TSD
#define TSD
#include <stdio.h>
//头文件的内容
#endif

或者每个文件开头写

#pragma once

第二种方式最简单


c语言中程序的编译(预处理操作)+链接详解(详细介绍程序预编译过程)(代码片段)

...目录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.3宏和函数的对比4.... 查看详情

c语言预处理和程序环境

...译阶段 汇编阶段 链接阶段(不属于编译阶段)预处理详解预定义符号 #define#define定义标识符#define定义宏#define的替换规则 #和##的使用 带副作用的宏参数 宏和函数的对比一些命名的规则 #undef命令行的定义条件编译... 查看详情

bit-0-程序环境和预处理(代码片段)

程序环境和预处理一、程序的翻译和执行翻译环境编译分为几个阶段运行环境二、预处理详解define替换规则'#'和'##''##'的作用带副作用的宏参数宏和函数对比命名约定命令行定义条件编译文件包含一、程序的翻... 查看详情

怎样运行自己编好的java小程序?

参考技术A怎样运行自己编好的JAVA小程序?写个DOS批处理,javacNotepad.JavajavaNotepad~~~~~~~~~~~~~~~~~~~~怎样才能运行JAVA小程序?JDK+记事本是最简单的不过JDK需要配置环境变量之类的要想方便的话用Myeclipse参考下载地址:chinesedocument.kaifag... 查看详情

函数栈帧详解(代码片段)

...序在编译阶段发生的处理参考一下博客:程序环境和预处理总结一下就是编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机的内存中,因此每条指令都有... 查看详情

函数栈帧详解(代码片段)

...序在编译阶段发生的处理参考一下博客:程序环境和预处理总结一下就是编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机的内存中,因此每条指令都有... 查看详情

linux编译工具:gcc温习

...件。3.gcc编译程序的过程gcc编译程序主要经过四个过程:预处理(Pre-Processing)编译(Compiling)汇编(Assembling)链接(Linking)预处理实际上是将头文件、宏进行展开。编译阶段,gcc调用不同语言的编译器,例如c语言调用编译器cc... 查看详情

数据库设计的六个阶段详解

数据库设计的阶段数据库设计可以分为6个阶段1.系统需求分析阶段2.概念结构设计阶段3.逻辑结构设计阶段4.物理结构设计阶段5.数据库实施阶段6.数据库运行和维护阶段各阶段的任务系统需求分析对现实世界要处理的对象进行详... 查看详情

程序的编译

这里写目录标题1、程序的翻译环境和执行环境2、详解编译+链接2.1编译环境2.2编译的几个阶段2.3运行环境1、程序的翻译环境和执行环境在ANSIC的任何一种实现中,存在两个不同的环境。第1种是翻译环境,在这个环境中... 查看详情

c语言——程序环境和预处理

程序的翻译环境和执行环境编译+链接预处理一.程序的翻译环境和执行环境在ANSIC标准的任何一种实现中,存在两种不同的环境:翻译环境:该环境中源代码会被转换为可执行的机器指令 执行环境:其用于实际执行代码二.编... 查看详情

程序是怎样跑起来的第7章有感

读《程序是怎样跑起来的》第七章有感本章主要讲的是程序是在环境下运行的内容,首先操作系统和硬件决定了程序的运行环境,机器语言的编码被称为本地代码,程序员用C语言等编写的程序,在编写的阶段仅仅是文本文件,... 查看详情

程序的环境和预处理

1.程序的环境预编译1.头文件的包含2.#define的预处理指令的执行3.注释的删除编译汇编链接运行1.程序的环境写过无数代码的你是否想过你写的.c文件编译链接运行形成.exe文件的中间是怎样执行的吗?从.c文件到.exe文件到代码... 查看详情

java学习路线是怎样的?

...页重定向、Servlet3.0新增的注解支持、AJAX、responseText属性详解等。第三阶段,Java高级框架-SSH:Struts2异常处理、Struts2+Log4j集成、Struts2和JSON实例、Hibernate5、Hibernate集合映射、Hibernate组件映射、Spring4.0、SpringAOP+AspectJ框架、Spring与... 查看详情

程序环境和预处理(代码片段)

...a6;翻译环境(编译+链接)💦运行环境二、预处理详解💦预定义符号💦#define定义标识符💦#define定义宏💦#define替换规则💦#和##(奇怪的用法)💦带副作用的宏参数💦宏和函数的对比... 查看详情

node到底是个什么

...不深我还不知道,不过确实不浅。  2.Node的目标是帮助程序员构建高度可伸缩的应用程序,编写能够处理数万条同时连接到一个物理机的连接代码。处理高并发和异步I/O是Node受到开发人员的关注的原因之一。  3.Node本身运... 查看详情

程序环境和预处理(代码片段)

...a6;翻译环境(编译+链接)💦运行环境二、预处理详解💦预定义符号💦#define定义标识符💦#define定义宏💦#define替换规则💦#和##(奇怪的用法)💦带副作用的宏参数 查看详情

中断是个啥?(代码片段)

...意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。处理流程,大致如下图:那么,看了定义,也许有点理解。大概可以类比搬... 查看详情

万字长文详解hivesql执行计划(代码片段)

HiveSQL的执行计划描述SQL实际执行的整体轮廓,通过执行计划能了解SQL程序在转换成相应计算引擎的执行逻辑,掌握了执行逻辑也就能更好地把握程序出现的瓶颈点,从而能够实现更有针对性的优化。此外还能帮助开发者识别看... 查看详情