C语言进阶--程序的编译(预处理动作)+链接

article/2025/8/13 13:48:09

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

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

第一种是翻译环境:将源代码转换为可执行的机器指令(0/1);

第二种是执行环境:用于实际执行代码。

2.详解编译+链接

2.1翻译环境

程序编译过程:

在这里插入图片描述

linux系统中的编译器gcc生成的目标文件:xxx.o

(不同的参数)

2.2编译本身也分为几个阶段

在这里插入图片描述
预编译/预处理:(文本操作)

  • 头文件的包含(#include)
  • #define定义符号的替换,删除定义的符号
  • 删除注释
  • ···

编译:把C语言代码转换成汇编代码(包括语法分析、词法分析、符号汇总、语义分析)

汇编:把汇编代码转换成二进制指令(机器指令);将编译时的符号汇总形成符号表

链接:合并段表;符号表的合并和重定位

gcc test.c -E -o test.ivim /usr/include/stdio.h    

在这里插入图片描述

gcc test.c -E -o test.i

在这里插入图片描述

gcc test.i -S  
//生成test.s

符号汇总:

在这里插入图片描述

gcc test.s -c
//生成test.o目标文件(二进制文件)

在这里插入图片描述
Linux下的可执行程序是elf格式。

2.3运行环境

程序执行的过程:

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

3.预处理详解

3.1预定义符号

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

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

#include <stdio.h>
int main()
{int i = 0;FILE* pf = fopen("log.txt", "w");if (pf == NULL){perror("fopen");return EXIT_FAILURE; //1//EXIT_SUCCESS; //0}for (i = 0; i < 10; i++){fprintf(pf, "file:%s line=%d date:%s time:%s i=%d", __FILE__, __LINE__, __DATE__, __TIME__, i);}fclose(pf);pf = NULL;return 0;
}

在这里插入图片描述

3.2#define

3.2.1#define定义标识符(不加分号)
#define name stuff
#include <stdio.h>#define MAX 1000
#define STR "hello bit"int main()
{int m = MAX;printf("%d\n", MAX); //1000printf("%s\n", STR); //hello bitreturn 0;
}

续行符
在这里插入图片描述

3.2.2#define定义宏

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

#define name(parament-list) stuff

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

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

#include <stdio.h>#define SQUARE(X) X*Xint main()
{int r = SQUARE(5); printf("%d\n", r); //25return 0;
}

上面写法有缺陷eg:

#include <stdio.h>#define SQUARE(X) X*Xint main()
{int r = SQUARE(5+1);  //5 + 1 * 5 + 1printf("%d\n", r); //11return 0;
}

修正:不吝啬括号

#include <stdio.h>#define SQUARE(X) ((X)*(X))int main()
{int r = SQUARE(5+1);  //((5 + 1) * (5 + 1))printf("%d\n", r); //36return 0;
}
3.2.3#define替换规则

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

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

注意:

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

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

#include <stdio.h>#define M 100
#define DOUBLE(X) ((X)+(X))int main()
{DOUBLE(M+2);  //(100+2)//((100+2)+(100+2))return 0;
}
3.2.4#和##

如何把参数插入到字符串中?
在这里插入图片描述

#include <stdio.h>#define PRINT(N) printf("the value of "#N" is %d\n", N)
int main()
{int a = 10;PRINT(a); //printf("the value of ""a"" is %d\n", a);int b = 10;PRINT(b);return 0;
}

在这里插入图片描述

#include <stdio.h>#define PRINT(N, FORMAT) printf("the value of "#N" is "FORMAT"\n", N)
int main()
{int a = 10;PRINT(a, %d); int f = 3.14f;PRINT(f, %lf);return 0;
}

在这里插入图片描述
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。

#include <stdio.h>#define CAT(Class, Num) Class##Num
int main()
{int Class106 = 100;printf("%d\n", CAT(Class, 106)); //100//printf("%d\n", Class106);return 0;
}
3.2.5带副作用的宏参数

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

#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 5; //6 7int b = 4; //5int m = MAX(a++, b++); //int m = ((a++)>(b++)?(a++):(b++));6      5   > 4   ?6printf("m=%d ", m); //6printf("a=%d b=%d\n", a, b); //7 5return 0;
}
3.2.6宏和函数对比

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

eg:在2个数中找出较大的一个,为什么不用函数来完成这个任务呢?

原因:

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

2.更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏可以适用于整型、长整型、浮点型等可以>来比较的类型。宏是与类型无关的。

函数调用(参数传参、栈帧创建)——>计算——>函数返回

宏的缺点:

1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2.宏是没法调试的。

3.宏由于类型无关,也就不够严谨。

4.宏可能会带来运算符优先级的问题,导致程序容易出错。

宏有时候可以做函数做不到的事情。比如宏的参数可以出现类型,但是函数不可以。

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

一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的习惯是:把宏名全部大写;函数名不要全部大写

3.2.8#undef

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

#undef name
#define M 100#include <stdio.h>int main()
{printf("%d\n", M); //100
#undef M    printf("%d\n", M); //errorreturn 0;
}

3.3命令行定义

linux下gcc编译器实现:

在这里插入图片描述
编译指令

gcc test.c -D SZ=10

3.4条件编译

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

在这里插入图片描述
常见的条件编译指令:

#if 常量表达式//···
#endif
#include <stdio.h>#if 0
int main()
{printf("hehe\n");return 0;
}
#endif

在这里插入图片描述
2.多个分支的条件编译

#if 常量表达式//···
#elif 常量表达式//···
#else 常量表达式//···
#endif
#define MAX 3#include <stdio.h>int main()
{
#if M<5printf("hehe\n");
#elif M==5 printf("haha\n");
#elseprintf("heihei\n");
#endifreturn 0;
}

预编译代码:

在这里插入图片描述
3.判断是否被定义

#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol
//#define MAX 100#include <stdio.h>int main()
{
#if !defined(MAX)printf("max\n");
#endifreturn 0;
}
#define MAX 100#include <stdio.h>int main()
{
#ifdef MAXprintf("max\n");
#endifreturn 0;
}
//#define MAX 100#include <stdio.h>int main()
{
#ifndef MAXprintf("max\n");
#endifreturn 0;
}

4.嵌套指令

#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif

3.5文件包含

防止头文件被多次重复的包含。

方案1:

#ifndef __TEST_H__
#define __TEST_H__
···
#endif    

在这里插入图片描述
方案2:

#pragma once

在这里插入图片描述

#include <stdio.h> //库文件包含
#include "test.h" //本地文件包含

<>和""的区别:查找的策略不同。

<>查找策略:直接去库目录下查找;

""查找策略:先去代码所在的路径下查找;如果找不到,再去库目录下查找。

Linux环境的标准头文件的路径:

/usr/include

VS环境的标准头文件的路径:(不同版本有差异)

C:\Program Files(x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

4.其他预处理指令

#error
#pragma
#line
···
不做介绍,自己了解,参考《C语言深度解剖》
#pragma pack()在结构体部分介绍。    

百度笔试题(offsetof宏的实现)

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明。
在这里插入图片描述

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

总结

今天就暂且更新至此吧,期待下周再会。如有错误还请不吝赐教。希望对您学习有所帮助,翻页前留下你的支持,以防下次失踪了嗷。

作者更新不易,免费关注别手软。


http://www.hkcw.cn/article/ZnSNsCLtbn.shtml

相关文章

GCA解码大脑因果网络

格兰杰因果分析&#xff08;Granger Causality Analysis,GCA&#xff09; 是一种测量脑区之间有效性连接&#xff08;effective connectivity&#xff09;的成熟方法。利用多元线性回归分析一个时间序列的过去值是否能正确预测另一个时间序列的当前值&#xff0c;可以用来描述脑…

H5S 大华SDK带图报警类型及热成像报警支持

目前很多应用都希望报警带对应的图片&#xff0c;比如控制中心在弹报警框的时候需要有一张图片让人工更快的做出判断&#xff0c;下面介绍使用大华SDK 的带图报警功能。 大华SDK支持接入设备带图报警&#xff0c;并且支持热成像通道报警&#xff0c;设置订阅事件并吧协议端口设…

(javaSE)Java数组进阶:数组初始化 数组访问 数组中的jvm 空指针异常

数组的基础 什么是数组呢? 数组指的是一种容器,可以用来存储同种数据类型的多个值 数组的初始化 初始化&#xff1a;就是在内存中,为数组容器开辟空间,并将数据存入容器中的过程。 数组初始化的两种方式&#xff1a;静态初始化&#xff0c;动态初始化 数组的静态初始化 初始化…

Java数据结构——八大排序

排序 插⼊排序希尔排序直接选择排序堆排序冒泡排序快速排序归并排序计数排序 排序的概念 排序&#xff1a;就是将一串东西&#xff0c;按照要求进行排序&#xff0c;按照递增或递减排序起来 稳定性&#xff1a;就是比如排序中有两个相同的数&#xff0c;如果排序后&#xff0c…

【Linux】Linux文件系统详解

目录 Linux系统简介 Linux常见发行版&#xff1a; Linux/windows文件系统区别 Linux文件系统各个目录用途 Linux系统核心文件 系统核心配置文件 用户与环境配置文件 系统运行与日志文件 Linux文件名颜色含义 Linux文件关键信息解析 &#x1f525;个人主页 &#x1f52…

2023年6月6级第一套第一篇

虽然&#xff0c;不重要题干定位到主句信息了&#xff0c;往下走&#xff0c;看强调什么信息看最后一句&#xff0c;优先看主干信息&#xff0c;先找谓语然后找主语和宾语&#xff0c;也是和人有关&#xff0c;后面出现的名词信息是修饰部分&#xff0c;非主干信息不看 A选项&…

Langchaine4j 流式输出 (6)

Langchaine4j 流式输出 大模型的流式输出是指大模型在生成文本或其他类型的数据时&#xff0c;不是等到整个生成过程完成后再一次性 返回所有内容&#xff0c;而是生成一部分就立即发送一部分给用户或下游系统&#xff0c;以逐步、逐块的方式返回结果。 这样&#xff0c;用户…

代谢组数据分析(二十六):LC-MS/MS代谢组学和脂质组学数据的分析流程

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包依赖包安装包加载需要的R包数据下载以及转换mzML数据预处理代谢物注释LipidFinder过滤MultiABLER数据预处理过滤补缺失值对数变换数据标准化下游数据分析总结系统信息参考介…

常量指真,指针常量 ,

const int*p&#xff1b;//const int 值不能变 指向可以变 int *const p&#xff1b;//const p 指向不可以变 值能变

智能指针unique

什么是智能指针&#xff1a; 就像是一个自动管家 帮你管理内存 自动清理不需要的内存 防止内存泄漏 unique_ptr 的特点&#xff1a; 独占所有权&#xff1a;一个资源只能被一个 unique_ptr 管理 不能复制&#xff1a;只能移动 自动释放&#xff1a;当 unique_ptr 被销毁…

并发执行问题 下

这段例子 是让S3 在S2后面运行 写完数据 通知后 另一个进程 竞争使用资源 独占资源 shell解释器 科学语言才有并发语句语言 C语言没有 使用多线程和多进程实现并发运行

[JS逆向] 福建电子交易平台

博客配套代码发布于github&#xff1a;福建电子交易平台 相关知识点&#xff1a;[爬虫知识] 密码学&#xff1a;通往JS逆向路上必会的一环 相关爬虫专栏&#xff1a;JS逆向爬虫实战 爬虫知识点合集 爬虫实战案例 此案例目标为对福建省电子公共服务平台逆向&#xff0c;并爬…

Mask_RCNN 环境配置及训练

目录 一、Mask_RCNN代码及权重 1、源码下载 2、权重获取 二、环境配置 1、创建虚拟环境 2、安装必要的包 三、测试环境 1、使用coco 2、使用balloon 四、测试 1、使用coco 2、使用balloon 一、Mask_RCNN代码及权重 均从github获取&#xff0c;以下是相关链接&#…

72.编辑用户消息功能之前端实现

大体设想 我想实现的一个功能是在用户发出的消息下面有一个图标是编辑&#xff0c;按下那个图标之后&#xff0c;用户可以修改对应的那个消息&#xff0c;修改完成点击确认之后&#xff0c;用户下面对用的那个AI的回答可以重新生成 之前已经介绍了后端实现&#xff0c;这篇博…

第303个Vulnhub靶场演练攻略:Thales1

Thales1 Vulnhub 演练 “Thales”是 Vulnhub 上的夺旗挑战赛。MachineBoy 开发了这款机器&#xff0c;功不可没。https://www.vulnhub.com/entry/thales-1,749/在本教程中&#xff0c;我们将学习如何利用 Tomcat 应用程序管理器实例中的漏洞获取系统访问权限&#xff0c;以及如…

vscode + cmake + ninja+ gcc 搭建MCU开发环境

vscode cmake ninja gcc 搭建MCU开发环境 文章目录 vscode cmake ninja gcc 搭建MCU开发环境1. 前言2. 工具安装及介绍2.1 gcc2.1.1 gcc 介绍2.1.2 gcc 下载及安装 2.2 ninja2.2.1 ninja 介绍2.2 ninja 安装 2.3 cmake2.3.1 cmake 介绍2.3.2 cmake 安装 2.4 VScode 3. 上手…

GNSS终端授时之四:高精度的PTP授时

我们在GNSS终端的授时之三&#xff1a;NTP网络授时中介绍了NTP网络授时的基本原理。我们知道了NTP授时的精度跟网络环境相关&#xff0c;即使在局域网中NTP授时的精度也只能到ms级别。如果广域网&#xff0c;经过多级交换机&#xff0c;路由器&#xff0c;由于传输路径和延时的…

Amazon Augmented AI:人类智慧与AI协作,破解机器学习审核难题

在人工智能日益渗透业务核心的今天&#xff0c;你是否遭遇过这样的困境&#xff1a;自动化AI处理海量数据时&#xff0c;面对模糊、复杂或高风险的场景频频“卡壳”&#xff1f;人工审核团队则被低效、重复的任务压得喘不过气&#xff1f;Amazon Augmented AI (A2I) 的诞生&…

OS10.【Linux】yum命令

目录 1.安装软件的几种方法 直接编译源代码,得到可执行程序 使用软件包管理器 2.yum yum list命令 参数解释 yum install命令 yum remove命令 下载链接存放的位置 扩展yum源 实验:安装sl小火车命令 sl命令的选项 方法1:man sl 方法2:读源代码 3.更新yum源 查看…

网络协议的原理及应用层

网络协议 网络协议目的为了减少通信成本&#xff0c;所有的网络问题都是传输距离变长的问题。 协议的概念&#xff1a;用计算机语言来发出不同的信号&#xff0c;信号代表不同的含义&#xff0c;这就是通信双方的共识&#xff0c;便就是协议。 协议分层&#xff08;语言层和…