C语言的程序环境和预处理详解

news2024/12/26 2:19:18

 

目录

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

二、编译和链接详解

2、1 翻译环境

2、2 编译过程详解

2、3 执行环境 

三、预处理详解

3、1 预定义符号

3、2 #define

3、2、1 #define定义的符号

3、2、2 #define 定义宏

3、2、3 #define 替换规则

3、3 宏和函数的对比

3、4 条件编译

3、5 头文件的包含

3、5、1 头文件被包含的方式

3、5、2 嵌套文件包


标题:C语言的程序环境和预处理详解

作者:@Ggggggtm

寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景

  我们平常写的代码都是通过编译器来运行的。我们有没有想过编译器是怎么将代码转化为各种指令最后输出结果呢?这篇文章会详细解释编译器的运行的整个过程的细节,希望会对你有所帮助。

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

  我们可以简单认为编译器把代码首先进行翻译,然后再执行。所以在ANSIC的任何一种实现中,存在两个不同的环境:

  • 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
  • 第2种是执行环境,它用于实际执行代码。
  那编译器具体是怎么翻译和执行的呢?这就要看编译和链接的过程了。我们接着往下看。

二、编译和链接详解

2、1 翻译环境

  我们先来看一下整个的翻译过程,翻译环境大致可分为以下三个步骤:

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
  具体我们也可结合下图一起理解:
  我们再具体看其中的编译和执行的细节。

2、2 编译过程详解

  我们先来看一段代码:

sum.c
int g_val = 2016;
void print(const char *str) 
{
    printf("%s\n", str);
}

test.c
#include <stdio.h>
int main()
{
    extern void print(char *str);
    extern int g_val;
    printf("%d\n", g_val);
    print("hello bit.\n");
    return 0; 
}

  我们可以看到上述代码中有两个源文件,分别是:sum.c 和 test.c。在对上述代码进行编译的时候,具体又分为以下步骤:

  1. 预编译(预处理)。主要是处理预处理指令,有头文件的包含#include、定义符号的替换和删除#define、注释的删除等等。
  2. 编译。把C语言代码翻译成汇编代码。其中有语法分析、词法分析、语义分析、符号汇总。
  3. 汇编。把汇编代码翻译成二进制指令,同时形成符号表。 
  4. 链接。符号表的合并和重定位、合并段表。

  在上述编译的过程中,第2点的符号汇总是指讲全局变量函数名称当作符号汇总,然后再汇编阶段将汇总的全局变量函数名称与其地址形成一个符号表。最后,由于有多个源文件会生成多个符号表,在链接阶段对符号表进行合并和重定位。链接完后会生成可执行程序。

  上述代码的编译过程中的符号表生成如下图:

  注意,多个源文件隔离编译,生成各自的符号表。最后会在链接时对符号表进行汇总和重定位。

2、3 执行环境 

  上述讲述了编译和链接后生成可执行文件,那么我们再看一下程序执行的过程:

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

三、预处理详解

3、1 预定义符号

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

  上述的符号均为预处理符号。在文件的预处理阶段均会被替换成相应的数据。我们结合以下代码理解。

#include<stdio.h>
int main()
{
	printf("line:%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	return 0;
}

  上述代码的运行结果为,如下图:

3、2 #define

3、2、1 #define定义的符号

  我们直接看#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__ )

  我们对上述的例子进行一一解释。第一个就是用 MAX 代替了 1000。第二个我们在使用register 关键字时会感到很麻烦,因为这个关键字太长了。于是用了 reg 代替了regisert。第三个其实是死循环。第四个效果更加明显。当我们使用switch语句时,可能经常忘记break,于是用CASE 代替了 break;case。第五个就很简单,直接代替了一个打印语句。

  注意,define定义标识符的时候,在最后不要加上 ; 。因为define定义标识符时进行替换的,加上 ; 时可能会出现意想不到的错误。

3、2、2 #define 定义宏

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

  语法:#define name( parament - list ) stuff
  其中的 parament -list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中。
  注意: 参数列表的左括号必须与name 紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff 的一部分。
  我们举一个例子,代码如下: 
#define SQUARE( x ) x * x
int main()
{
	printf("%d", SQUARE(5));
	return 0;
}

   将上面的代码进行预处理后,打印的是5*5的值。我们再来看一段代码:

#define SQUARE( x ) x * x
int main()
{
	//printf("%d", SQUARE(5));
	int a = 5;
	printf("%d\n", SQUARE(a + 1));
	return 0;
}

   我们的本意是想打印出a+1的平方,但是结果并非如此。结果如下图:

  我们来分析一下替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:

printf ("%d\n",a + 1 * a + 1 )。自然而然,结果就是11。

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

在宏定义上加上两个括号,这个问题便轻松的解决了:
#define DOUBLE( x)   ( ( x ) * ( x ) )

3、2、3 #define 替换规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。
  注意:
  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

3、3 宏和函数的对比

  宏通常被应用于执行简单的运算。 比如在两个数中找出较大的值。
#define MAX(a, b) ((a)>(b)?(a):(b))
  那么为什么不用函数呢?其有如下两个原因:
  • 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹
  • 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
    用于 > 来比较的类型。 宏是类型无关的。

  当然,宏和函数对比也是有不足的,有如下几点:

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

  更加具体的宏和函数的对比总结如下表格:

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

3、4 条件编译

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

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

#include <stdio.h>
#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; 
}

   条件编译指令有很多,我们来看一下常见的条件编译指令: 

1.
#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、5、1 头文件被包含的方式

  头文件的包含方式有两种:

  • 本地文件包含。如: #include "filename" 。
    查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
  • 库文件包含。如: #include <filename.h> 。
    查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。这样是不是可以说,对于库文件也可以使用 “” 的形式包含?答案是肯定的,可以 但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

3、5、2 嵌套文件包

  如果出现如下场景:

  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

  预处理指令的内容就讲解到这里,希望以上内容对你有所帮助ovo~ 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CUDA中的底层驱动API

文章目录CUDA底层驱动API1. Context2. Module3. Kernel Execution4. Interoperability between Runtime and Driver APIs5. Driver Entry Point Access5.1. Introduction5.2. Driver Function Typedefs5.3. Driver Function Retrieval5.3.1. Using the driver API5.3.2. Using …

Springboot扩展点之BeanPostProcessor

前言 Springboot&#xff08;Spring&#xff09;的扩展点其实有很多&#xff0c;但是都有一个共同点&#xff0c;都是围绕着Bean和BeanFactory&#xff08;容器&#xff09;展开的&#xff0c;其实这也很好理解&#xff0c;Spring的核心是控制反转、依赖注入、面向切面编程&…

西湖论剑 2023 比赛复现

WEB real_ez_node 在 route/index.js 中&#xff1a; router.post(/copy,(req,res)>{res.setHeader(Content-type,text/html;charsetutf-8)var ip req.connection.remoteAddress;console.log(ip);var obj {msg: ,}if (!ip.includes(127.0.0.1)) {obj.msg"only for…

【设计模式之美 设计原则与思想:面向对象】13丨实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?

面向对象分析&#xff08;OOA&#xff09;、面向对象设计&#xff08;OOD&#xff09;、面向对象编程&#xff08;OOP&#xff09;&#xff0c;是面向对象开发的三个主要环节。在前面的章节中&#xff0c;我对三者的讲解比较偏理论、偏概括性&#xff0c;目的是让你先有一个宏观…

电脑重装系统注册表恢复方法

​今天讲关于大家的电脑在遇到一些故障的时候&#xff0c;以及电脑用久了之后会卡顿&#xff0c;那么这时候大家一般都会给电脑重装系统。重装系统之后却发现自己电脑里的注册表不见了&#xff0c;重装系统后怎么恢复注册表?小编就带着大家一起学习重装系统注册表恢复到底是怎…

【博客615】通过systemd设置cgroup来限制服务资源争抢

通过systemd设置cgroup来限制服务资源争抢 1、场景 我们的宿主机上通常会用systemctl来管理一些agent服务&#xff0c;此时我们需要限制服务的cpu&#xff0c;memory等资源用量&#xff0c;以防止服务之前互相争抢资源&#xff0c;导致某些核心agent运行异常 2、systemd与cgro…

生成树协议 — STP

目录 一、环路的出现 1、广播风暴&#xff1a; 2、MAC地址表翻滚&#xff1a; 二、生成树 1、定义&#xff1a; 2、生成树使用的算法&#xff1a; 三、802.1D 1、BPDU&#xff1a; 2、TCN—拓扑变更消息&#xff08;也是BPDU&#xff09;&#xff1a; 3、部分名词&am…

【Python小游戏】某程序员将套圈游戏玩儿到了巅峰,好嗨哟~Pygame代码版《牛牛套圈》已上线,大人的套圈游戏太嗨了,小孩勿进。

前言 世上选择那么多。 关注栗子同学会是您最明智的选择哦。 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝白嫖源码福利&#xff0c;请移步至CSDN社区或文末公众hao即可免费。 “幸运牛牛套圈圈”套住欢乐&#xff0c;圈住幸福&#xff0c;等你来挑战&#xf…

用OpeAI API打造ChatGPT桌面端应用

用OpeAI API打造ChatGPT桌面端应用 自从《如何用ChatGPT高效完成工作》这篇文章火了之后&#xff0c;我在公司内部分享了一下”摸鱼“的先进经验&#xff0c;激发起广大同事一起”摸鱼“的热情。但是注册ChatGPT账号非常麻烦&#xff0c;既要Science上网&#xff0c;又要海外手…

Spark环境搭建

文章目录Spark 概述Spark 发展历史使用现状官网介绍流行原因组成模块Spark环境搭建-Local模式(本地模式)Spark环境搭建-Standalone(独立集群)Spark环境搭建-Standalone-HA(高可用)Spark环境搭建-Spark-On-Yarn两种模式Spark 概述 Spark 发展历史 2009年诞生2014年成为Apache顶…

Java笔记-线程中断

线程的中断 1.应用场景&#xff1a; 假设从网络下载一个100M的文件&#xff0c;如果网速很慢&#xff0c;用户等得不耐烦&#xff0c;就可能在下载过程中点“取消”&#xff0c;这时&#xff0c;程序就需要中断下载线程的执行。 2.常用中断线程的方法&#xff1a; 1.使用标…

Canvas鼠标滚轮缩放以及画布拖动(图文并茂版)

Canvas鼠标滚轮缩放以及画布拖动 本文会带大家认识Canvas中常用的坐标变换方法 translate 和 scale&#xff0c;并结合这两个方法&#xff0c;实现鼠标滚轮缩放以及画布拖动功能。 Canvas的坐标变换 Canvas 绘图的缩放以及画布拖动主要通过 CanvasRenderingContext2D 提供的 …

C++设计模式(13)——装饰模式

亦称&#xff1a; 装饰者模式、装饰器模式、Wrapper、Decorator 意图 装饰模式是一种结构型设计模式&#xff0c; 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 问题 假设你正在开发一个提供通知功能的库&#xff0c; 其他程序可使用它向用户发…

注册ChatGPT的辛酸血泪史,不能算教程的教程

注册ChatGPT的血泪史 2月份了&#xff0c;改论文降重了&#xff0c;所以想搞个ChatGPT玩玩&#xff0c;本以为有渠道能顺序上车&#xff0c;但是看了很多教程&#xff0c;也进了很多交流群&#xff0c;都喵的(要不是卖号&#xff0c;租体验&#xff0c;liar)&#xff0c;所以自…

java ssm高校教材管理平台 idea maven

设计并且实现一个基于JSP技术的高校教材管理平台的设计与实现。采用MYSQL为数据库开发平台&#xff0c;SSM框架&#xff0c;Tomcat网络信息服务作为应用服务器。高校教材管理平台的设计与实现的功能已基本实现&#xff0c;主要学生、教材管理、学习教材、教材入库、教材领取、缴…

C语言 大数加法 大数乘法

最近刷题&#xff0c;总遇到大数加法&#xff08;浮点数&#xff09;和乘法问题(阶乘)&#xff0c;总结一下思路。 大数乘法主要思想&#xff1a;编程实现竖式乘法&#xff08;小学时候学的列竖式计算乘法&#xff09;创建一个很大的数组&#xff0c;用于存储大数的每一位。&am…

Android Q WiFi 代码框架

同学,别退出呀,我可是全网最牛逼的 WIFI/BT/GPS/NFC分析博主,我写了上百篇文章,请点击下面了解本专栏,进入本博主主页看看再走呗,一定不会让你后悔的,记得一定要去看主页置顶文章哦。 1 wifi 架构图 备注:在Android 9.0中,WiFi的状态处理在WifiStateMachine中进行,到…

c语言数据结构-图的遍历

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 定义&#xff1a; 两种遍历方法&#xff1a; 深度优先搜索&#xff08;DFS&#xff09;&#xff1a; …

ElasticJob-Lite架构篇 - 认知分布式任务调度ElasticJob-Lite

前言 本文基于 ElasticJob-Lite 3.x 版本展开分析。 如果 Quartz 集群中有多个服务端节点&#xff0c;任务决定在哪个服务端节点上执行的呢&#xff1f; Quartz 采用随机负载&#xff0c;通过 DB 抢占下一个即将触发的 Trigger 绑定的任务的执行权限。 在 Quartz 的基础上&…

从0到1一步一步玩转openEuler--10 openEuler基础配置-设置kdump

10 openEuler基础配置-设置kdump 文章目录10 openEuler基础配置-设置kdump10.1 设置kdump10.1.1 设置kdump预留内存10.1.1.1 预留内存参数格式10.1.2 预留内存推荐值10.1.3 禁用网络相关驱动10.1 设置kdump 本节介绍如何设置kdump预留内存及修改kdump配置文件参数。 10.1.1 设…