C语言程序环境和预处理

文章目录

  • 程序的翻译环境和执行环境
  • 详解编译和链接
    • 翻译环境
    • 编译本身也分为几个阶段
      • 预处理
      • 编译
      • 汇编
    • 链接
      • 段表
      • 符号表的合并
  • 预处理详解
    • 预定义符号
    • #define
      • #define 定义标识符
      • #define定义宏
      • #define替换规则
      • #和##
      • ## 的作用
      • 带副作用的宏参数
      • 宏和参数的对比
      • 宏和函数的一个对比
      • 命名约定
    • #undef
    • 命令行定义
    • 条件编译
    • 文件的包含
      • 头文件被包含的方式:
      • 嵌套文件的包含
  • 其他预处理指令

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

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

  • 第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
  • 第二种是执行环境,它用于实际执行代码。
    如图所示
    在这里插入图片描述

详解编译和链接

翻译环境

在一个工程目录下可能有多个源文件,它们是通过这样的方式产生联系的
第一步:组成一个程序的每个源文件通过编译过程分别转换成目标代码。
第二步:每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
第二步:链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
如图所示,可以直观的表现以上内容:
在这里插入图片描述

编译本身也分为几个阶段

编译可以分为预处理,编译,汇编。预处理可以生成后缀为.i的文件,编译可以生成后缀为.s文件,汇编可以生成后缀为.o或者.obj的文件,每个阶段会生成不同的文件,这些文件都是程序文件,因为它们都是存储程序使用的文件,最后生成的.o和.obj文件在翻译环境中的链接模块生成.exe文件后交给执行环境进行执行。就上上一个图片所示的那样,多个.o文件通过链接库和链接器链接在一起,生成了可执行程序。

预处理

由于我们想要在预处理阶段知道代码到底发生了什么,但是在vs环境里面看不到这些信息,那么我们就可以使用gcc编译器进行演示,使用vscode来编辑程序,通过gcc编译器编译源程序,可以看到各个阶段程序的变化。关于如何在vscode下配置c/c++执行环境,可以点下面的链接进行学习。
https://www.bilibili.com/video/BV1UK411C7xi/?spm_id_from=333.999.0.0
接下来我们演示,程序在预处理的时候到底发生了什么
使用gcc test.c -E -o test.i 命令来生成.i文件,去.i文件里面看到底发生了什么
在这里插入图片描述
我们可以发现在.i文件里面没有注释和MAX这个符号,而是把MAX的值替换了进去。那么我们就可以发现预处理主要的作用是,注释的删除、宏替换和包含头文件。.i文件里884行以上都是头文件的内容。

编译

编译会生成.s文件,作用是把一个.i 文件转换为汇编代码。我们来验证这样一个事情。
使用gcc test.c -S -o test.s 来观察这一现象。
在这里插入图片描述
上面的代码都是汇编代码,可以去查阅相关的资料学习汇编语言来读懂这些是什么意思,这里我就不在展开讲解了。

出了上面介绍的功能外,编译还有很多的功能,语法分析、词法分析、语义分析,符号汇总等功能,其中符号汇总我们会在之后的链接部分进行介绍。

汇编

把一个.s文件转换为.o也就是目标文件,汇编就是把一个汇编程序转换为二进制文件。
使用gcc test.c -C -o test.o 命令来验证这个功能。
在这里插入图片描述
上面的.o文件就是一个二进制的文件了。在汇编阶段还有一个特殊的功能那就是形成符号表,关于这个功能我们会链接部分详细介绍。

链接

链接的作用就是合并段表以及符号表的合并和符号表的重定位。

段表

在这里插入图片描述

以上的程序会生成两个.obj文件。在链接阶段进行链接,形成一个.exe文件也就是可执行程序文件,这三个文件存放的都是机器码也就是二进制代码。.obj文件或者.o文件是有格式的,以gcc编译器产生的.o文件为例。它的目标文件是elf格式的,那么什么是elf格式呢,下面是一个详细的解释:
elf格式文件的解释在这里插入图片描述

符号表的合并

这里我们可以用一张图大概的了解一下:
在这里插入图片描述
在编译阶段会进行符号汇总,把一些main,函数名称,全局变量的符号进行汇总,在汇编阶段会把这些符号形成一个符号表,符号表里包括符号和符号在内存中的地址,在链接阶段会把符号表合并,所谓合并就是把符号的地址进行匹配,如果匹配不上就会发生链接性错误,程序是不能被编译成功的。

总结:
在这里插入图片描述
在这里插入图片描述## 运行环境

程序执行的过程

  • 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  • 程序的执行便开始。接着便调用main函数。
  • 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  • 终止程序。正常终止main函数;也有可能是意外终止。

预处理详解

预定义符号

_FILE_ //进行编译的源文件
_LINE_ //文件当前的行号
_DATE_ //文件被编译的日期
_TIME_ //文件被编译的时间
_STDC_ //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的。
我们来看这些符号的使用:

int main()
{
	printf("file:%s line:%d date:%s time:%s", __FILE__, __LINE__, __DATE__, __TIME__);
	return 0;
}

运行结果:
在这里插入图片描述

#define

#define 定义标识符

语法:
#define name stuff
我们来看一下下面例子:

#define MAX1 1000;
#define MAX 1000
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n",__FILE__,__LINE__,__DATE__,\
__TIME__);
#include <stdio.h>
// int main()
// {
//     int a = 0;
//     if(1)
//     {
//         a = MAX1;
//     }
//     else
//     {
//         a = MAX;
//     }
//     return 0;
// }
int main()
{
    DEBUG_PRINT;
}
//预编译结果
int main()
{
    printf("file:%s\tline:%d\t date:%s\ttime:%s\n",".\\test.c",22,"Apr  5 2023","09:44:05");;
}

这里要注意,#define定义标识符的时候不能在后面加上分号;。 比如我们来看一个例子,会导致不必要的错误。

在这里插入图片描述

预编译后我们发现if条件判断语句里面出现了两条语句,如果不带大括号的话,会产生语法错误。
导致程序出错。因为if条件判断语句如果不带大括号的话,只能有一条语句,上面就有了一个赋值语句和空语句。

#define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的声明方式:
#define name(parament-list) stuff
其中的parament-list 是一个有逗号隔开的符号表,它们可能出现在stuff中。
我们来看一个例子:

#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
    int n = SQUARE(5);
    printf("%d\n",n);
}

执行结果:
在这里插入图片描述
那么我们再看一个代码:

int main()
{
    int a = 5;
    int num = SQUARE(a+1);
    //这里结果我们可以去分析一下,5+1是6,然后把6
    //替换过去,然后6*6=36,我们看看是不是
    //这里结果是11。
    printf("%d\n",num);
    return 0;
}

运行结果:
在这里插入图片描述
那么这是为什么呢,我们来看一下.i文件的最后几行:
在这里插入图片描述
我们可以看到,此时的num = a+1a+1;我们把a代入进去,结果就是11。我们必须把宏的定义那里加上括号,才不会被运算符的优先级所影响。
修改后的代码:
在这里插入图片描述
我们再把a代入进去,此时的num = ((5+1)
(5+1)),答案为36。
在这里插入图片描述
我们再看一个代码:
在这里插入图片描述
.i文件里的内容(预处理之后)
在这里插入图片描述
加上括号后的运行结果,这个结果才是我们想要的:

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

在这里插入图片描述
.i文件里的内容(预处理之后)
在这里插入图片描述

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

#define替换规则

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

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

注意:

  • 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
    当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#和##

我们如何把参数插入到字符串中,这里我们就用到了#和## 这两个预处理操作符。
我们可以看以下代码:

int main()
{
    printf("hello""bit");
    return 0;
}

运行结果是hellobit,这里我们可以得到一个结论,字符串是有自动连接的特点的。
我们可以思考一个问题,怎么样才能实现 :

the value of a is 10
the value of b is 20
the value of c is 30
这样的功能呢。

我们可以这样写代码:
在这里插入图片描述
这样只是实现了一个问题,那就是the value of (值)这样的样式,并没有实现上述的功能,但是我们可以去这样改造代码:
在这里插入图片描述
这样写代码我们就可以实现上面的功能了。
所以#的作用就是把宏参数转换为字符串。

## 的作用

把两个字符串转换为标识符然后连接在一起
我们来看一个代码:
在这里插入图片描述
可见## 的作用是可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。

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

带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性的效果。
x+1 //不带有副作用
x++ //带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。

#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
    //使用带有副作用的宏参数
    int x = 5;
    int y = 8;
    int z = MAX(x++,y++);
    printf("%d %d %d\n",x,y,z);
}

预编译之后的结果:

# 91 "test.c"
int main()
{

    int x = 5;
    int y = 8;
    int z = ((x++)>(y++)?(x++):(y++));
    printf("%d %d %d\n",x,y,z);
}

分析:第一个x++ 把x加成了6,而比较的时候还是用5来比较,第一个y++使用8比较,再++得到y = 9。由于x<y,所以执行y++ 先把9赋给z 然后再把y+1,得到y = 10;
所以,执行结果应该是 x = 6 y = 10 z = 9。

我们来看执行结果,果然是我们分析下的值
在这里插入图片描述

宏和参数的对比

宏通常被应用于执行简单的算。
比如以上的宏,在两个数中找最大值。
int z = ((x++)>(y++)?(x++):(y++));
为什么不用函数来完成这个任务?
原因有两个:

  • 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    所以宏比函数在程序的规模和速度方面更胜一筹。
  • 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型、长整型、浮点型等类型。
    宏是类型无关的。

宏的缺点:

  • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  • 宏是没法调试的。
  • 宏由于类型无关,也就不够严谨。
  • 宏可能会带来运算符优先级的问题,导致程序容易出错。

宏有的时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。比如下面的这段代码:

#define MALLOC(num,type) \
       (type*)malloc(num*sizeof(type))

int main()
{
    int* a = MALLOC(2,int);
    return 0;
}

预处理替换后:

# 103 "test.c"
int main()
{
    int* a = (int*)malloc(2*sizeof(int));
    return 0;
}

宏和函数的一个对比

在这里插入图片描述
c/c++ 引入了一个inline关键字,内联函数:具有了函数的优点,也具有宏的优点。

命名约定

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

  • 把宏名全部大写
  • 函数名不要全部大写

#undef

这条指令用于移除一个宏定义
在这里插入图片描述
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如: 当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特点有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大些)
我们来看这样一段代码:

int main()
{
    int arr[SIZE];
    int i = 0;
    for(i = 0;i<SIZE;i++)
    {
        arr[i] = i;
    }
    for(i = 0;i<SIZE;i++)
    {
        printf("%d ",arr[i]);
    }
    printf("\n");
    return 0;
}

通过这条指令 gcc -D SIZE=10 test.c -o test.exe 编译后,得出结果为:
在这里插入图片描述

条件编译

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

int main()
{
    #ifdef MAX
        printf("%d",10);
    #endif
    //判断是否被定义
    #ifndef MAX
        printf("%d",10);
    #endif
    //多分支结构
    #if 1
        printf("%d",10);
    #elif 1 
        printf("%d",10);
    #else
        printf("%d",10);
    #endif
    //嵌套结构
    #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
}

文件的包含

我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方,这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含的方式:

  • 本地文件的包含
    #include "filename"
    查找策略:先在源文件目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
  • 库文件包含
    #include <filename>
    查找头文件直接去标准路径下查找,如果找不到就提示编译错误。
    我们在使用时习惯各用各的,不会混合起来,因为那样会降低效率。

嵌套文件的包含

这里只需要明白一点,为了防止头文件被重复包含,我们可以使用条件编译来做到这一点,看下面这个代码:

#ifndef MAX
    #define MAX
    //一些函数声明,头文件的包含。
#endif
//如果没有定义MAX我们就定义MAX,再一次编译时,已经有了MAX所以就不会执行#ifndef里面的代码了

也可以使用#pragma once来避免头文件的重复引入。

其他预处理指令

#error
#pragma
#line

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/7296.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

FastestDet:比yolov-fastest更快!更强!全新设计的超实时Anchor-free目标检测算法

本篇文章转自于知乎——qiuqiuqiu,主要设计了一个新颖的轻量级网络! 代码地址:https://github.com/dog-qiuqiu/FastestDet 1 概述 FastestDet是设计用来接替yolo-fastest系列算法,相比于业界已有的轻量级目标检测算法如yolov5n, yolox-nano, nanoDet, pp-yolo-tiny, Fast…

CSS基础知识,必须掌握!!!

CSS基础知识Background&#xff08;背景&#xff09;CSS文本格式文本颜色文本对齐格式文本修饰文本缩进CSS中的字体字体样式字体大小CSS链接&#xff08;link&#xff09;CSS列表不同列表标项CSS列表项用图片作为标记CSS列表标记项位置CSS中表格&#xff08;table&#xff09;表…

Shell脚本之嵌套循环与中断跳出

1、双重循环 1.1 格式 #!/bin/bash for ((i9;i>1;i--)) do for ((j9;j>$i;j--)) do echo -n -e "$j$i$[$i*$j]\t" done echo done1.2 实例操作 2.1 格式 #!/bin/bash for ((a1;a<9;a)) dofor ((b9;b>a;b--))doecho -n " "donefor((c1;c<…

系统信息:uname,sysinfo,gethostname,sysconf

且欲近寻彭泽宰&#xff0c;陶然共醉菊花怀。 文章目录系统信息系统标识 unamesysinfo 函数gethostname 函数sysconf()函数系统信息 系统标识 uname 系统调用 uname()用于获取有关当前操作系统内核的名称和信息&#xff0c;函数原型如下所示&#xff08;可通过"man 2 un…

面向对象编程(基础)7:再谈方法(重载)

目录 7.1 方法的重载&#xff08;overload&#xff09; 7.1.1 概念及特点 7.1.2 示例 举例1&#xff1a; 举例2&#xff1a; 举例3&#xff1a;方法的重载和返回值类型无关 7.1.3 练习 **练习1&#xff1a;** 练习2&#xff1a;编写程序&#xff0c;定义三个重载方法并…

如何大批量扫描的发票进行ocr识别导出Excel表格和WPS表格

OCR技术&#xff1a;OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&#xff09;是将数字图像中的文字识别成字符代码的技术&#xff0c;在发票识别中应用广泛。通过OCR技术&#xff0c;可以将图片发票上的信息识别出来&#xff0c;并导出到Excel表格中…

3年测试越来越迷茫... 技术跟不上接下来是不是要被淘汰了?

这两天和朋友聊到了软件测试的发展&#xff1a;这一行的变化确实蛮大&#xff0c;从开始最基础的功能测试&#xff0c;到现在自动化、性能、安全乃至于以后可能出现的大数据测试、AI测试岗位需求逐渐增多。我也在软件测试这行摸爬滚打有些日子了&#xff0c;正好有朋友问我&…

晶振01——晶振分类和无源晶振的设计

晶振 晶振相当于人的心脏&#xff0c;能跳动&#xff0c;整个系统才是“活的”。 晶振常见有有源晶振、无源晶振。 有源晶振比较贵&#xff0c;但是需要外围电路少&#xff0c;供个电就能工作。 无源晶振价格便宜&#xff0c;匹配电路复杂些。 以无源晶振进行分析&#xff0c…

WCF手麻系统源码,手术室麻醉临床系统源代码,商业源码 有演示

手麻系统源码 手术麻醉系统源码 手术室麻醉临床信息系统源码 商业级源码&#xff0c;有演示&#xff0c;三甲医院临床应用多年&#xff0c;系统稳定。 文末获取联系&#xff01; 技术架构&#xff1a;C# .net 桌面软件 C/S版&#xff0c;前后端分离&#xff0c;仓储模式 开发语…

2.5.3 乘法

这段话告诉我们&#xff0c;在程序中有一条乘法运算语句。这个程序会让计算机帮助我们完成一个简单的数学问题&#xff1a;计算6乘以2。和我们平常做数学题一样&#xff0c;程序使用*号表示乘法运算。语句 “feet 6 * fathoms;” 可以这样理解&#xff1a;它会找到之前我们定义…

spring 随笔 async 1-源码走读

0. 这一块比较简单&#xff0c;还就内个无障碍阅读 不谈,放个调用栈的日志先 … // 我们自己写的 Async 注解的方法 simpleTest:31, TestAsyncTestService (cn.angel.project.angelmicroservicesample.test.service) invoke:-1, TestAsyncTestService$$FastClassBySpringCGLI…

手撕vector

文章目录一.vector的基本结构二.构造函数调用不明确三.迭代器失效&#xff08;其实是野指针问题&#xff09;a.扩容导致的迭代器失效b.意义不同四.深层次的深浅拷贝五.整体代码实现有了前面模拟实现string的经验&#xff0c;vector对大家来说也不会算很难。vector本质也就是一个…

【ORACLE】极速通关Oracle23c开发者免费版连接

前言 oracle23c开发者免费版已经于2023年4月4日(北京时间)推出&#xff0c;并且官方也公布了安装介质的下载地址&#xff0c;有RPM安装包、VM虚拟机、docker镜像&#xff08;下载链接见文末&#xff09;。 由于最近工作比较忙&#xff0c;暂时无法写一篇内容丰富的测试&#x…

springboot树形结构接口, 懒加载实现

数据库关系有父子id的, 作为菜单栏展示时需要用前端需要用到懒加载, 所谓懒加载就是接口有一个标志位isLeaf, 前端请求后通过该字段判断该节点是否还有子节点数据 创建数据库表 t_company_info结构有id和parentId标识, 用来表示父子关系 /*Navicat Premium Data TransferSourc…

大数据环境-云平台(阿里云)

由于电脑配置原因&#xff0c;无法在本地利用虚拟机搭建环境&#xff0c;因此使用云平台来当做学习的环境。 本节内容参考&#xff1a; 【2023新版黑马程序员大数据入门到实战教程&#xff0c;大数据开发必会的Hadoop、Hive&#xff0c;云平台实战项目全套一网打尽-哔哩哔哩】 …

72-Linux_线程同步

线程同步一.什么是线程同步二.线程同步的方法1.互斥锁(1)什么是互斥锁(2)互斥锁的接口(3)互斥锁的使用(例题)2.信号量(1)什么是信号量(2)信号量的接口(3)信号量的使用(例题)a.循环打印ABCb.:主线程获取用户输入,函数线程将用户输入的数据存储到文件中;3.读写锁(1)读写锁的接口(…

R语言|plot和par函数绘图详解,绘图区域设置 颜色设置 绘图后修改及图像输出

plot()函数 plot()函数是R中最基本的绘图函数&#xff0c;其实最简单、最基础的函数&#xff0c;这也就意味着其具有更多的可操作性。 plot(x,y,...) 在plot函数中&#xff0c;只需指定最基本的x和y轴对应数据即可进行图像的绘制&#xff0c;x和y轴数据分别为两个向量或者是只…

年薪40W测试工程师成长之路,你在哪个阶段?

对任何职业而言&#xff0c;薪资始终都会是众多追求的重要部分。前几年的软件测试行业还是一个风口&#xff0c;随着不断地转行人员以及毕业的大学生疯狂地涌入软件测试行业&#xff0c;目前软件测试行业“缺口”已经基本饱和。当然&#xff0c;我说的是最基础的功能测试的岗位…

WIN10無法再使用 IE 瀏覽器打开网页解决办法

修改 Registry&#xff08;只適用 Win10&#xff09; 微軟已於 2023 年 2 月 14 日永久停用 Internet Explorer&#xff0c;會透過 Edge 的更新讓使用者開啟 IE 時自動導向 Edge&#xff0c;其餘如工作列上的圖示&#xff0c;使用的方法則是透過「品質更新」的 B 更新來達成&am…

NoSQL与Redis五次作业回顾

文章目录1. 作业12. 作业23. 作业34. 作业45. 作业51. 作业1 要求&#xff1a; 在 VM 上安装 CentOS Linux 系统&#xff0c;并在 Linux 上通过 yum 安装 C编译器&#xff0c;对 Redis 进行编译安装。 观察 Redis 目录结构&#xff0c;使用 redis-server 启动服务器&#xff…
最新文章