【C语言】程序环境,预处理,编译,汇编,链接详细介绍,其中预处理阶段重点讲解

news2024/11/14 20:47:58

目录

程序环境

翻译环境

1. 翻译环境的两个过程

2. 编译过程的三个阶段 

执行环境 

预处理(预编译) 

1. 预定义符号

2. #define 

2.1 用 #define 定义标识符(符号)

2.2 用 #define 定义宏 

2.3 #define 的替换规则 

2.4 # 和 ## 的用法

2.5 宏和函数

2.6 #undef

3. 命令行定义

4. 条件编译

5. 文件包含

5.1 两种头文件的包含

5.2 嵌套文件包含


程序环境

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

1. 翻译环境,在这个环境中源代码被转换为可执行的机器指令。

2. 执行环境,它用于实际执行代码。

.

我们写出的C语言代码是文本信息,计算机不能直接理解,计算机是执行二进制指令的,翻译环境负责将C语言代码转成二进制指令,执行环境负责执行二进制代码。


翻译环境

1. 翻译环境的两个过程

1. 一个工程可以有多个.c(源文件)文件,每个源文件都会单独经过编译器处理生成自己对应的目标文件(.obj),这个过程叫做编译。

2. 多个目标文件和链接库经过链接器的处理,最后生成可执行程序,这个过程叫做链接。

链接库的意思是链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

2. 编译过程的三个阶段 

翻译环境分为编译和链接两部分,编译又有预处理,编译,汇编三个阶段。

1. 预处理:gcc -E test.c -o test.i

预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。

2. 编译:gcc -S test.c

编译完成之后就停下来,结果保存在test.s中。

3. 汇编:gcc -c test.c

汇编完成之后就停下来,结果保存在test.o中。

.

符号

1. 查看符号,这些符号都是全局的。

2. 每个源文件自己编译阶段符号汇总,汇编阶段形成符号表(符号,对应地址),链接进行所有源文件的符号表合并(相同符号合并,函数定义的符号的地址为有效地址,函数声明的符号的地址为无效地址,选取有效地址)。

比如:下图main.c符号表中_sum地址是无效的,合并的时候选择sum.c符号表的_sum地址。


执行环境 

程序执行的过程:

1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排(例如单片机的烧录),也可能是通过可执行代码置入只读内存来完成。

2. 程序的执行便开始。接着便调用main函数。 

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)也就是函数栈帧,存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

4. 终止程序。正常终止main函数;也有可能是意外终止。


预处理(预编译) 

1. 预定义符号

以下预定义符号会在预处理阶段被替换。

__FILE__ 替换为当前进行编译的源文件名称

__LINE__ 替换为当前的行号

__DATE__ 替换为文件被编译的日期

__TIME__ 替换为文件被编译的时间

__STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义

.

2. #define 

2.1 用 #define 定义标识符(符号)

语法:

#define name stuff

例子:

用 MAX 代表1000。

#define MAX 1000

为 register 这个关键字,创建一个简短的名字。

#define reg register

用更形象的符号来替换一种实现。

#define do_forever for(;;)

在写 case 语句的时候自动把 break 写上。

#define CASE break;case

如果定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。

#define DEBUG_PRINT printf("file:%s\tline:%d\t\
                            date:%s\ttime:%s\n" ,\
                            __FILE__,__LINE__ ,\
                            __DATE__,__TIME__ )

提问:在define定义标识符的时候,要不要在最后加上;

答:不会直接报错,但没必要加,因为

1. 这只是单纯的替换,如果你加了分号,万一代码那边也写了分号,就会出现两个分号。

2. 不方便进行运算和逻辑执行,替换后你多一个分号有时候会影响原先代码的逻辑。

2.2 用 #define 定义宏 

语法:

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

#define name(parament-list) stuff 

用法:

#define MAX(x, y) (x>y ? x : y)

这个写法其实不够严谨,因为传进来的可能是多项式,所以尽量加上括号。

#define MAX(x, y) ((x)>(y) ? (x) : (y))

这个替换会发生在预处理阶段。


带副作用的宏参数 

x+1;//不带副作用
x++;//带有副作用

例子

#define MAX(a, b)  ( (a) > (b) ? (a) : (b) )
 
x = 5;
y = 8;
z = MAX(x++, y++);

z 就会替换为 z = ( (x++) > (y++) ? (x++) : (y++));

副作用就是表达式求值的时候出现的永久性效果。

2.3 #define 的替换规则 

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

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果有,那它们首先被替换,替换后的文本被插入到程序中原来文本的位置。

2. 然后参数被宏的值所替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果有,就重复上述处理过程。

注意:

1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是宏不能出现递归。

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

2.4 # 和 ## 的用法

1. # 的用法:把宏的参数插入到字符串中。

例子:

#define PRINT(n) printf("the value of n is %d\n", n)

int main()
{
    int a = 10;
    PRINT(a);
    
    int b = 20;
    PRINT(b);

    return 0;
}

问题:字符串中的n没有被替换。

解决办法:在 n 前面加一个 #

#define PRINT(n) printf("the value of "#n" is %d\n", n)

int main()
{
    int a = 10;
    PRINT(a);
    
    int b = 20;
    PRINT(b);

    return 0;
}

题外话:字符串的双引号是可以连接的。

比如:


2. ## 的用法:可以把位于它两边的符号合成一个符号。

例子:

将 a 和 b 连起来变成 ab。

#define CAT(x, y) x##y

int main()
{
    int ab = 10;
    printf("%d\n", CAT(a, b));
    printf("%d\n", ab);

    return 0;
}

2.5 宏和函数

宏的优点

1. 执行简单的运算时,选择用宏而不是函数。

原因:

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

函数的参数必须声明为特定的类型,宏是类型无关的,所以函数只能在类型合适的表达式上使用。

2. 宏的参数可以出现类型,但是函数做不到。

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

MALLOC(10, int);
//预处理器替换之后:
(int*)malloc(10*sizeof(int));

宏的缺点

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

2. 宏是没法调试的。

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

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


宏与函数对比


命名约定

1. 宏名全部大写

2. 函数名不要全部大写

2.6 #undef

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

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

例子:

3. 命令行定义

许多 C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例子:

 #include <stdio.h>
 int main()
 {
     int array [ARRAY_SIZE];
     int i = 0;
     for(i = 0; i< ARRAY_SIZE; i ++)
     {
         array[i] = i;
     }
     for(i = 0; i< ARRAY_SIZE; i ++)
     {
         printf("%d " ,array[i]);
     }
     printf("\n");
     return 0;
 }

编译指令:

gcc -D ARRAY_SIZE=10 programe.c

4. 条件编译

在编译一个程序的时候,条件编译指令可以将一条语句或一组语句进行编译或者放弃编译。

常见的条件编译指令:

1. 单分支的条件编译,常量表达式由预处理器求值。

 #if 常量表达式
    //...
 #endif
 
如:
#define __DEBUG__ 1

#if __DEBUG__
     //..
#endif

2. 多分支的条件编译

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

3. 判断是否被定义,各自有两种写法。

#if defined(symbol)
    //...
#endif

#ifdef symbol
    //...
#endif

#if !defined(symbol)
    //...
#endif

#ifndef symbol
    //...
#endif

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

5. 文件包含

5.1 两种头文件的包含

本地文件包含

#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误。


库文件包含

#include <filename.h>

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


题外话

1. 其实库文件包含也能用双引号,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

2. #include 指令可以使另外一个文件被编译,就像它实际出现于一样。

这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次。

5.2 嵌套文件包含

comm.h 和 comm.c 是公共模块。

test1.h 和 test1.c 使用了公共模块。

test2.h 和 test2.c 使用了公共模块。

test.h 和 test.c 使用了 test1 模块和 test2 模块。

这样最终程序中就会出现两份 comm.h 的内容,这样就造成了文件内容的重复。


解决办法有两种

1. 条件编译

#ifndef __TEST_H__

#define __TEST_H__
... //头文件的内容

#endif   

2. 头文件开头写:

#pragma once

这样就可以避免头文件的重复引入。

林宇恒/code_c - 码云 - 开源中国 (gitee.com)

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

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

相关文章

【学习笔记】决策单调性优化DP

背景 GDCPC还在发力&#xff0c;清华出题组出的牛客还是 4 题。 这次没有min25筛&#xff0c;不然我能5题&#xff08;bushi 除了一道用 prufer 序列的恶心 DP 外&#xff0c;还有一道DP题是一个状态难想&#xff0c;并且还需要决策单调性优化的DP&#xff0c;被认为是偏简单…

CTFHub XSS DOM 跳转

查看网页源代码 <script>var target location.search.split("")if (target[0].slice(1) "jumpto") {location.href target[1];} </script>注意&#xff01;当你将类似于 location.href "javascript:alert(xss)" 这样的代码赋值…

利用Qt实现调用文字大模型的API,文心一言、通义千问、豆包、GPT、Gemini、Claude。

利用Qt实现调用文字大模型的API&#xff0c;文心一言、通义千问、豆包、GPT、Gemini、Claude。 下载地址: AI.xyz 1 Qt实现语言大模型API调用 视频——Qt实现语言大模型API调用 嘿&#xff0c;大家好&#xff01;分享一个最近做的小项目 “AI.xyz” 基于Qt实现调用各家大模型…

Type-C PD芯片:边充电与数据传输同时进行LDR

在日新月异的科技浪潮中&#xff0c;电子产品不仅成为了我们日常生活不可或缺的一部分&#xff0c;更是推动着社会进步与变革的重要力量。随着智能设备的普及与多样化&#xff0c;一个能够高效融合充电与数据传输功能&#xff0c;同时保持广泛兼容性和安全性的接口标准显得尤为…

实战Transformers模型量化Facebook OPT

基于上篇文章的理论知识&#xff0c;本文主要讲述了实战Transformers模型量化&#xff1a;介绍Facebook OPT模型的量化过程和相关技术。 Transformers 模型量化技术&#xff1a;GPTQ Frantar等人发表了论文 GPTQ&#xff1a;Accurate Post-Training Quantization for Generat…

ARCH和GARCH模型

个人学习笔记&#xff0c;课程为数学建模清风付费课程 目录 一、为什么引入ARCH模型&#xff1f; 二、 ARCH模型 2.1概念 2.2适用情形 2.3条件异方差 2.4ARCH(1)模型和ARCH(q)模型 三、GARCH(p,q)模型 3.1ARCH(q)效应和GARCH(p,q)效应 3.2GARCH效应检验 3.2.1检验GA…

Variational Mode Decomposition (VMD) 详解与应用

Variational Mode Decomposition (VMD) 的详细介绍 VMD 是一种信号分解方法&#xff0c;旨在将复杂信号分解为若干个具有不同频率成分的模态。它的基本思想是通过变分优化的方式&#xff0c;得到一组模态信号&#xff0c;这些模态信号在频域上彼此分离。 1. 问题定义 假设我…

【C++】类和对象两个必看题

这两个题只有一句代码的差别。 看题目之前我先说一下怎么看汇编指令。 第一题&#xff1a;下面程序运行结果是&#xff1f; A.编译报错 B.运行崩溃 C.正常运行 #include <iostream> using namespace std; class A { public:void Print(){cout << "A::Pri…

正点原子imx6ull-mini-Linux驱动之异步通知实验(13)

在前面使用阻塞或者非阻塞的方式来读取驱动中按键值都是应用程序主动读取的&#xff0c;对于非 阻塞方式来说还需要应用程序通过 poll 函数不断的轮询。最好的方式就是驱动程序能主动向应 用程序发出通知&#xff0c;报告自己可以访问&#xff0c;然后应用程序在从驱动程序中读…

docker部署kkfileview文件在线预览服务

kkfileview文件在线预览服务部署使用 免费开源&#xff0c;功能强大&#xff0c;几乎支持日常见到的所有文件类型在线预览 目前支持的文件类型如下 支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx,xlam, xla 等 Office 办公文档支持 wp…

day12 多线程

目录 1.概念相关 1.1什么是线程 1.2什么是多线程 2.创建线程 2.1方式一&#xff1a;继承Thread类 2.1.1实现步骤 2.1.2优缺点 2.1.3注意事项 2.2方式二&#xff1a;实现Runnable接口 2.2.1实现步骤 2.2.2优缺点 2.2.3匿名内部类写法 2.3方式三&#xff1a;实现cal…

鸿蒙系统开发【网络-上传和下载(ArkTS)】基本功能

网络-上传和下载&#xff08;ArkTS&#xff09; 介绍 本示例使用ohos.request接口创建上传和下载任务&#xff0c;实现上传、下载功能&#xff0c;hfs作为服务器&#xff0c;实现了文件的上传和下载和任务的查询功能。 效果预览 使用说明 1.本示例功能需要先配置服务器环境…

BootStrap前端面试常见问题

在前端面试中&#xff0c;关于Bootstrap的问题通常围绕其基本概念、使用方式、特性以及实际应用等方面展开。以下是一些常见的问题及其详细解答&#xff1a; 1. Bootstrap是哪家公司研发的&#xff1f; 回答&#xff1a;Bootstrap是由Twitter的Mark Otto和Jacob Thornton合作…

go语言day21 goland使用gin框架、gorm框架操作mysql数据库redis数据库 使用宝塔创建redis数据库

GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly. gorm package - github.com/jinzhu/gorm - Go Packages go语言day20实现投票功能项目包-CSDN博客 gin框架标准项目结构&#xff1a; models&#xff1a;存放对应实体类和gorm包增删…

Godot的节点与场景

要深入的理解节点与场景&#xff0c;我们需要跳出这两个概念来看他。说的再直白一些godot本质就是一个场景编辑器&#xff01; 场景的概念应该在我们平时看电影看电视时会经常提到&#xff0c;比如某一个打斗的场景&#xff0c;这个场景可能会被设在某一个街道&#xff0c;那么…

RIP综合练习

要求&#xff1a; 1.合理使用IP地址划分网络&#xff0c;各自创建循环接口 2.R1创建环回172.16.1.1/24 172.16.2.1/24 172.16.3.1/24 3.要求R3使用R2访问R1环回 4.减少路由条目数量&#xff0c;R1,R2之间增加路由传递安全性 5.R5创建一个环回模拟运营商&#xff0c;不能…

Flink的DateStream API中的ProcessWindowFunction和AllWindowFunction两种用于窗口处理的函数接口的区别

目录 ProcessWindowFunction AllWindowFunction 具体区别 ProcessWindowFunction 示例 AllWindowFunction 示例 获取时间不同&#xff0c;一个数据产生的时间一个是数据处理的时间 ProcessWindowFunction AllWindowFunction 具体示例 ProcessWindowFunction 示例 Al…

CRMEB 电商系统安装及分析

CRMEB系统采用前后端分离技术&#xff0c;基于TP6Vue2.5Uniapp框架开发&#xff1b;支持微信小程序、公众号、H5、APP、PC端适配&#xff0c;数据同步&#xff01;是一套单商户新零售社交电商系统。 目录 安装 安装环境 安装过程 开始安装 安装检测 数据库配置 高级设置…

基于Cloudflare搭建私有Docker镜像源

周四原本不是发文的日子&#xff0c;主要因为两个原因&#xff1a; 第一个原因是总有人留言说 Docker 用不了&#xff0c;第二个原因是看了下上个月的阅读量&#xff0c;和之前比实在有点惨淡&#xff0c;除了文章总被人搬运外&#xff0c;我估计可能跟第一个原因多少还是有点…

计算机基础(Windows 10+Office 2016)教程 —— 第4章 计算机网络与Internet(上)

第4章 计算机网络与Internet 4.1 计算机网络概述4.1.1 计算机网络的定义4.1.2 计算机网络的发展4.1.3 计算机网络的功能4.1.4 计算机网络体系结构和TCP/IP 参考模型 4.2 计算机网络的组成和分类4.2.1 计算机网络的组成4.2.2 计算机网络的分类 4.3 网络传输介质和通信设备4.3.1 …