程序环境和预处理(1)

文章目录

  • 目录
    • 1. 程序的翻译环境和执行环境
    • 2. 详解编译+链接
      • 2.1 翻译环境
      • 2.2 编译本身也分为几个阶段
      • 2.3 运行环境
    • 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.2.6 宏和函数对比

目录

  • 程序的翻译环境
  • 程序的执行环境
  • 详解:C语言程序的编译+链接
  • 预定义符号介绍
  • 预处理指令 #define
  • 宏和函数的对比
  • 预处理操作符#和##的介绍
  • 命令定义
  • 预处理指令 #include
  • 预处理指令 #undef
  • 条件编译

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

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

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。

计算机是能够执行二进制指令的,但是我们写出的C语言代码是文本信息,计算机不能直接理解。

翻译环境:C语言代码 —> 二进制的指令(放在可执行程序中)

执行环境:执行二进制的代码

2. 详解编译+链接

2.1 翻译环境

翻译环境

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

我们也可以通过代码来观察:

//add.cint Add(int x, int y)
{return x + y;
}
//test.c#include <stdio.h>extern int Add(int, int);int main()
{int a = 10;int b = 20;int c = Add(a, b);printf("%d\n", c);return 0;
}

目标文件和可执行程序

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

翻译环境详解

  1. 预处理阶段
    预处理阶段
  2. 编译阶段
    编译阶段
  3. 汇编阶段
    汇编阶段

解释一下符号汇总、符号表:
代码
ELF文件格式
readelf结果
在这个目标文件里,确实能看到一些符号(都是全局的


接下来,我们用两个文件来举例子:符号汇总解释

这有什么用呢?

如果没有定义Add函数,那么在链接的时候就定位不到这个函数,就会发生链接错误,生成不了可执行程序。

2.3 运行环境

程序执行的过程:

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

3. 预处理详解

3.1 预定义符号

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

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

举个例子:

#include <stdio.h>int main()
{printf("%s\n", __FILE__);printf("%d\n", __LINE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);//printf("%d\n", __STDC__);//当前VS是不支持ANSI	Creturn 0;
}

对代码进行预处理之后:
预定义符号

3.2 #define

3.2.1 #define 定义标识符

语法:
#define name stuff

#include <stdio.h>#define M 100
#define STR "abc"
#define FOR for(;;)
#define reg register//为 register这个关键字,创建一个简短的名字int main()
{printf("%d\n", M);printf("%s\n", STR);FOR;//死循环return 0;
}

预处理之后:
#define定义标识符


int main()
{int d = 0;switch (d){case 1:break;case 2:break;case 3:break;}return 0;
}

以上代码还可以这样写:

#define CASE break;caseint main()
{int d = 0;switch (d){case 1:CASE 2:CASE 3:}return 0;
}

#include <stdio.h>//如果定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\t\time:%s\n" , __FILE__, __LINE__,\__DATE__, __TIME__)int main()
{DEBUG_PRINT;return 0;
}

提问:

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

比如:

#define M 100;int main()
{int a = M;return 0;
}

预处理(1)
这样的代码看上去是没有问题的,但是这样写是非常容易出错的:

#define M 100;int main()
{int a = 0;int b = 0;if (a > 5)b = M;elseb = -1;return 0;
}

预处理(2)
if 语句后面默认只能跟一条语句,这里再加上一个 ; 就变成了两条语句,这就意味着下面的 else 不知道和谁匹配了。

因此,建议不要加上 ; ,这样容易导致问题。

3.2.2 #define 定义宏

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

下面是宏的申明方式:

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

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

#include <stdio.h>#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 2;int b = -2;int c = MAX(a, b);printf("c=%d\n", c);return 0;
}

预处理之后:
预处理(3)
提示:

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

预处理(4)
应该这样写:

#include<stdio.h>#define SQUARE(x) ((x) * (x))int main()
{int a = 3;int r = SQUARE(a + 2);printf("r=%d\n", r);return 0;
}

预处理(5)
应该这样写:

#include<stdio.h>#define DOUBLE(x) ((x) + (x))int main()
{int a = 3;int r = 10 * DOUBLE(a);printf("r=%d\n", r);return 0;
}
3.2.3 #define 替换规则

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

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

预处理(6)
注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#include<stdio.h>#define M 3int main()
{printf("M=%d\n", M);//M=3return 0;
}
3.2.4 #和##

如何把参数插入到字符串中?

首先我们看看这样的代码:

#include <stdio.h>int main()
{printf("hello world\n");//hello worldprintf("hello ""world\n");//hello worldreturn 0;
}

我们发现字符串是有自动连接的特点的。


我们想实现这样一个功能:

#include <stdio.h>int main()
{int a = 20;printf("the value of a is %d\n", a);int b = 15;printf("the value of b is %d\n", b);float f = 4.5f;printf("the value of f is %f\n", f);return 0;
}

我们可以用宏来实现(函数做不到):

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

代码中的 #n预处理“n”


##的作用:

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。

#include <stdio.h>#define CAT(x,y) x##yint main()
{int Class110 = 2024;printf("%d\n", CAT(Class, 110));//2024printf("%d\n", Class110);//2024return 0;
}

预处理(7)

3.2.5 带副作用的宏参数

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

int main()
{int a = 10;//int b = a + 1;//b=11, a=10int b = ++a;//b=11, a=11return 0;
}

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

MAX宏可以证明具有副作用的参数所引起的问题:

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

通常被应用于执行简单的运算:比如在两个数中找出较大的一个。

那为什么不用函数来完成这个任务?

原因有二:

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

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

宏的缺点:
当然和函数相比,宏也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。(代码如果特别长,那么编译的压力就会很大,因为在编译的时候会对代码做各种各样的处理,如:语法分析、词法分析等等)
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

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

#define MALLOC(num, type) (type*)malloc(num*sizeof(type))int main()
{int* p = (int*)malloc(126 * sizeof(int));//malloc(126, int);//errint* p = MALLOC(126, int);//int* p = (int*)malloc(126 * sizeof(int));return 0;
}

宏和函数的一个对比
宏和函数的区别

对于第三点的一个例子:

//1
#define MAX(x, y) ((x)>(y)?(x):(y))//2
int Max(int x, int y)
{         //5      6return (x > y ? x : y);
}int main()
{int c = MAX(2 + 3, 6);//int c = ((2+3)>(6)?(2+3):(6))c = Max(2 + 3, 6);return 0;
}

补充:
宏有自己的优势,当然也有劣势

函数也有自己的优势,也有劣势

能不能有一个函数既具有函数的好,也具有宏的好呢?

inline — 内联

内联函数

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring 源码阅读:深入探索内部工作机制 | 开源日报 No.180

xuchengsheng/spring-reading Stars: 1.2k License: NOASSERTION 深入 Spring&#xff0c;从源码开始&#xff01; 作者是一名有8年经验的 Java 后端开发人员&#xff0c;创建了 “Spring 源码阅读系列”&#xff0c;希望与大家共同探索 Spring 的内部工作机制。帮助程序员深…

RabbitMQ-消息队列:发布确认高级

18、发布确认高级 在生产环境中由于一些不明原因&#xff0c;导致 RabbitMQ 重启&#xff0c;在 RabbitMQ 重启期间生产者消息投递失败&#xff0c; 导致消息丢失&#xff0c;需要手动处理和恢复。于是&#xff0c;我们开始思考&#xff0c;如何才能进行 RabbitMQ 的消息可靠投…

Flink join详解(含两类API及coGroup、connect详解)

Flink SQL支持对动态表进行复杂而灵活的连接操作。 为了处理不同的场景&#xff0c;需要多种查询语义&#xff0c;因此有几种不同类型的 Join。 默认情况下&#xff0c;joins 的顺序是没有优化的。表的 join 顺序是在 FROM 从句指定的。可以通过把更新频率最低的表放在第一个、…

适用于高云FPGA的JTAG

目标板卡&#xff1a;小梅哥芯海无涯GOWIN高云ACG525(GW5A-LV25UG324) 1.软件要求&#xff1a;必须用商业版&#xff0c;因为教育版(V1.9.9Beta-4 Education)不支持此封装的GW5A。商业版需要上网申请License&#xff0c;此处提供D4D853392AD8.lic文件&#xff08;此方法为临时…

基于编译器的静态代码分析与软件开发效率、质量和性能

基于编译器的静态代码分析与软件开发效率、质量和性能 本文节选自《基础软件之路&#xff1a;企业级实践及开源之路》一书&#xff0c;该书集结了中国几乎所有主流基础软件企业的实践案例&#xff0c;由 28 位知名专家共同编写&#xff0c;系统剖析了基础软件发展趋势、四大基…

Android14之input高级调试技巧(一百八十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

Spring及工厂模式概述

文章目录 Spring 身世什么是 Spring什么是设计模式工厂设计模式什么是工厂设计模式简单的工厂设计模式通用的工厂设计 总结 在 Spring 框架出现之前&#xff0c;Java 开发者使用的主要是传统的 Java EE&#xff08;Java Enterprise Edition&#xff09;平台。Java EE 是一套用于…

ES坑-创建索引使用_下划线-黑马旅游搜不到

学ES的时候&#xff0c;星级过滤无效 找不到数据。 需要 但是我们在创建的时候使用的是keyword 通过研究发现&#xff0c;我们导入数据的时候应该默认的为starName 我get库时候发现有2个字段 所以通过star_name搜索因为都是空数据搜不到&#xff0c;而starName类型为text所以…

备战蓝桥杯————双指针技巧巧解数组2

利用双指针技巧来解决七道与数组相关的题目。 两数之和 II - 输入有序数组&#xff1a; 给定一个按升序排列的数组&#xff0c;找到两个数使它们的和等于目标值。可以使用双指针技巧&#xff0c;在数组两端设置左右指针&#xff0c;根据两数之和与目标值的大小关系移动指针。 …

【C++】类和对象---友元,内部类,匿名对象详解

目录 友元 友元函数 友元类 内部类 匿名对象 ⭐友元 友元提供了一种突破封装的方式&#xff0c;有时提供了便利。但是友元会增加耦合度&#xff0c;破坏了封装&#xff0c;所以 友元不宜多用。 友元分为&#xff1a;友元函数和友元类。 ⚡友元函数 先看一个问题&#x…

Android 解决后台服务麦克风无法录音问题

Android 解决后台无法录音问题 问题分析问题来源解决方案1. 修改清单文件:`AndroidManifest.xml`2. 修改启动服务方式3. 服务启动时创建前台通知并且指定前台服务类型参考文档最后我还有一句话要说我用心为你考虑黄浦江的事情,你心里想的却只有苏州河的勾当 问题分析 安卓9.…

架构篇36:微服务架构最佳实践 - 基础设施篇

文章目录 自动化测试自动化部署配置中心接口框架API 网关服务发现服务路由服务容错服务监控服务跟踪服务安全小结每项微服务基础设施都是一个平台、一个系统、一个解决方案,如果要自己实现,其过程和做业务系统类似,都需要经过需求分析、架构设计、开发、测试、部署上线等步骤…