全县网站建设管理工作会议召开,制作一个app软件需要多久,品牌搜索,重庆铜梁网站建设报价 #x1f4dd;个人主页#xff1a;Sherry的成长之路 #x1f3e0;学习社区#xff1a;Sherry的成长之路#xff08;个人社区#xff09; #x1f4d6;专栏链接#xff1a;C语言进阶 #x1f3af;长路漫漫浩浩#xff0c;万事皆有期待 文章目录1.编译与链接1.1 程… 个人主页Sherry的成长之路 学习社区Sherry的成长之路个人社区 专栏链接C语言进阶 长路漫漫浩浩万事皆有期待 文章目录1.编译与链接1.1 程序的翻译环境与执行环境1.2 翻译环境1.3 翻译阶段①.编译②.链接1.4 运行环境2. 预处理详解2.1 预定义符号2.2 #define2.2.1 #define 定义标识符2.2.2 #define 定义宏2.2.3 #define 替换规则2.2.4 带副作用的宏参数3. 宏与函数3.1 宏与函数对比3.2 宏与函数的命名约定4. 预处理操作符4.1 预处理操作符 # 4.2 预处理操作符 ## 5. 条件编译5.1 简述条件编译指令5.2 常见条件编译指令①. 单分支条件编译指令②. 多分枝条件编译指令③. 判断是否被定义④.嵌套指令7.总结1.编译与链接
1.1 程序的翻译环境与执行环境
在研究程序的编译与链接细节之前我们首先要了解我们程序的翻译以及执行环境我们要知道在 ANSI C 的任何一种实现中都存在着两种环境 翻译环境在该环境中我们所写下的 .c 等源代码将被转化成可执行的机器指令。 执行环境在该环境下将会真正执行经过转化后生成的 .exe 可执行文件。 1.2 翻译环境 在翻译环境中执行的操作简单来说可以分为三个步骤 一组成一个程序的每个源文件通过编译过程分别转换成目标代码.obj。 二每个目标文件由链接器捆绑在一起形成一个单一而完整的可执行程序。 三链接器同时也会引入标准C函数库中任何被该程序所用到的函数而且它可以搜索程序员个人 的程序库将其需要的函数也链接到程序中。 链接库指的是链接我们的工程中所引用的库函数所依赖的函数库以及各种第三方函数库。
1.3 翻译阶段
如果我们再细分下来翻译阶段又可以分为两个阶段即编译与链接
①.编译
编译阶段又可以细分为三个阶段 1.预编译(预处理)头文件的包含、#define 定义符号的替换、注释的删除均为文本操作。 2.编译将 C 语言的代码翻译成汇编代码其具体动作包含了语法分析、词法分析、语义分析、符号汇总等。 3. 汇编将汇编指令翻译为二进制指令具体动作包含了形成符号表等。
②.链接
连接阶段负责各个文件的链接相关操作其具体动作包括了 1.合并段表 2.合并符号表 3. 重定位符号表
1.4 运行环境
在这个环境下我们的程序就真正进入了运行阶段。我们程序的执行过程可以简述为 下面四个步骤 ①. 程序载入内存中在有操作系统的环境中该步骤通常由操作系统完成而在独立环境中则必须由我们自己手动完成独立环境中的程序也有可能通过执行可执行代码置入只读内存来完成。 ②. 调用 main 函数程序正式开始执行。 ③. 顺序执行程序代码 在这个阶段中我们的程序会使用一个运行时堆栈来存储函数的局部变量和返回地址。同时程序也可以使用静态内存来存储变量并且这些存储于静态变量中的变量在整个程序的执行过程中将始终保留它们的值。 ④. 终止程序一般情况下会正常终止 main 函数但我们的程序也有可能会意外终止。 2. 预处理详解
2.1 预定义符号
在 C 语言中有一些内置的预处理符号 __FILE__进行编译的源文件__LINE__文件当前的行号__DATE__文件被编译的日期__TIME__文件被编译的时间__STDC__如果编译器遵循ANSI C标准其值为1否则未定义这些符号都是语言内置的可以直接使用
#includestdio.h
int main()
{printf(进行编译的源文件\n);printf(%s\n, __FILE__);printf(文件当前的行号\n);printf(%d\n, __LINE__);printf(文件编译的日期\n);printf(%s\n, __DATE__);printf(文件编译的时间\n);printf(%s\n, __TIME__);return 0;
}2.2 #define
#define 的用处非常多就比如我们常用的定义标识符常量、定义宏等等而在这个过程中也有一些细节值得我们去注意。
2.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__) 同时可以思考一下在 #define 的最后要不要加分号 “ ; ” 最好不要添加这是因为有可能会导致语法错误
#define MAX 1000;
#includestdio.h
int main()
{int a 1;int max 0;if (a){max MAX;}printf(max %d\n, max);return 0;
}例如这个例子在预编译预处理阶段会进行 #define 定义标识符的替换于是 if 语句中的代码将会被替换为
将被替换为max (MAX;);
即max MAX;;此时我们发现经过符号替换后出现了语法错误。
2.2.2 #define 定义宏
在#define 的机制中包括了一个规定这个规定允许把参数替换到文本中这种实现通常称为定义宏或简称为宏 宏的申明方式为
#define name( parament-list ) stuff其中的 parament-list 是一个由逗号隔开的符号列表且可能出现在 stuff 中。 注意参数列表的左括号必须与 name 紧邻。如果两者之间有任何空白存在参数列表就会被解释为stuff的一部分。
例如我们定义一个宏 sqr 用来计算平方
#define sqr(x) x*x这个宏将会在执行时接受一个参数 x如果在上述声明之后将下面的代码置于程序中
sqr( 5 );则预处理器就会将上面的表达式替换为
5 * 5警告这个宏的书写仍是不严谨的随时可能导致错误的产生。 例如下面这样情况
#define sqr(x) x*x
#includestdio.h
int main()
{int a 5;printf(%d\n, sqr(a 1));return 0;
}看起来好像并没有什么问题但事实上这段代码在经过预编译的符号替换后将会变成
#define sqr(x) x*x
#includestdio.h
int main()
{int a 5;printf(%d\n, a 1 * a 1);return 0;
}原本想要计算 6 * 6却在预编译的符号替换后出现了错误打印了 5 1 * 5 1。
解决方法很简单只需要对宏定义进行简单修改即可
#define sqr(x) (x)*(x)
#includestdio.h
int main()
{int a 5;printf(%d\n, sqr(a 1));return 0;
}通过小括号的使用在预编译阶段这段代码将被替换为 printf(%d\n,(a1)*(a1));但是其实这样写还有可能出现下面这样的错误
#define sqr(x) (x)(x)
#includestdio.h
int main()
{int a 5;printf(%d\n, 10 * sqr(a));return 0;
}经过了与编译后又将会被替换为 printf(%d\n, 10 * (a)(a));由于操作符中 * 的优先级高于 计算结果又将出现差错。 解决方案是再使用一对小括号对宏定义进行简单修改即可
#define sqr(x) ((x)(x))
#includestdio.h
int main()
{int a 5;printf(%d\n, 10 * sqr(a));return 0;
}这时的代码在经过预编译后将会被替换为
printf(%d\n, 10 * ((a)(a)));总结 在用于对数值表达式进行求值的宏定义中都应该用这种方式加上括号以避免在使用宏时由于参数中操作符或邻近操作符之间不可预料的相互作用从而导致在经过预编译阶段的符号替换后出现错误。
2.2.3 #define 替换规则 #define 在进行符号替换时遵循以下规则 调用宏时首先对参数进行检查检查是否包含任何由 #define 定义的符号。如果有它们将首先被替换。替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值所替换。最后再次对结果文件进行扫描看看它是否包含任何由 #define 定义的符号。如果有就重复上述处理过程。 注意 1.宏参数和 #define 定义中可以出现其它 #define 定义的符号。但是对于宏绝对不能出现递归。 2. 当预处理器搜索 #define 定义的符号的时候字符串常量的内容并不会被搜索。
2.2.4 带副作用的宏参数
这里所说的副作用是指表达式求值的时候出现的永久性效果
x 1;//不带副作用
x;//带有副作用而这样的代码会通过下面这样的方式对结果造成影响
#define MAX(a,b) ((a)(b)?(a):(b))
#includestdio.h
int main()
{int x 5;int y 8;int z MAX(x, y);printf(x %d y %d z %d\n, x, y, z);return 0;
}这段代码的实际结果为
由这个例子我们可以得知当宏参数在宏的定义中出现超过一次的时候如果参数带有副作用那么在使用这个宏的时候就有可能导致不可预测的后果。
3. 宏与函数
3.1 宏与函数对比
宏 #define 常用于定义一些全局的宏常量等。宏还常常被应用于执行简单的运算例如比较两数的大小可以这样用
#define MAX(a,b) ((a)(b)?(a):(b))
#includestdio.h
int main()
{int a 5;int b 10;printf(MAX %d\n, MAX(a, b));return 0;
}为什么我们不使用函数来完成这个过程呢 在这里使用宏而不用函数的主要原因有两个 ①. 相比于函数宏在程序规模与速度方面更胜一筹由于函数在调用时牵扯到函数栈帧的创建与销毁等操作所以用于调用函数以及从函数返回的耗时可能比实际执行这个小型计算工作的耗时更多。 ②. 宏是类型无关的在声明、定义和使用函数时函数的参数必须是特定的类型所以函数只能在类型合适的特定表达式上使用。反观宏无论是整形、长整型还是浮点型都可以进行比较。 宏与函数相比也有劣势 ①. 宏只能处理简单运算我们知道在预处理阶段宏将会进行符号替换这就意味着每次在使用宏的时候一份宏定义的代码将插入到程序中。这样一来如果宏比较长就将会大幅度增加程序代码的长度。 ②. 宏是没有办法进行调试的。 ③. 由于宏具有类型无关的特点因此也不够严谨。 ④. 宏可能会带来运算符优先级的问题导致程序容易出错。 宏也可以做到函数做不到的事比如
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
#includestdio.h
#includestring.h
int main()
{MALLOC(10, int);//预处理器替换后(int*)malloc(10 * sizeof(int));return 0;
}3.2 宏与函数的命名约定
我们使用的宏定义函数与普通函数的使用语法很相似导致语言本身无法帮我们区分二者。所以我们通常对二者的命名进行约定 ①. 宏的名称全部大写。 ②. 函数名不要全部大写。 如此就可以帮助我们区分提升代码的可读性
4. 预处理操作符
int main()
{char* p Hellow world\n;//*p指向两个常量字符串printf(Hellow world\n);printf(%s, p);return 0;
}当我们将其编译运行发现打印结果均为“ Hellow world” 于是可以得知字符串具有自动连接的特点。那么我们是不是就可以写出下面这样的代码呢
#define PRINT(FORMAT,VALUE) printf(The value of VALUE is FORMAT\n,VALUE);
//可以使用宏来定义函数
//字符串The value of VALUE is 与\n将会自动连接
//构造函数等价于printf(the value of VALUE is %d\n,VALUE);
#includestdio.h
int main()
{int a 10;PRINT(%d, a 3);return 0;
}在这段代码中我们使用了宏来构造一个新的函数在这个我们自己构造出来的宏函数中其中的两个短字符串将会自动连接成为一整个长字符串并根据我们输入的参数进行打印。于是在这个过程中就衍生出了两个实用的预处理操作符# 与 ##。
4.1 预处理操作符 #
预处理操作符 # 的作用将宏参数转变成其对应的字符串。 这是什么意思呢例如下面这段代码中所使用的 #N
#define _CRT_SECURE_NO_WARNINGS 1
#define PRINT(N) printf(The value of #N is %d\n,N);
//N将被视为字符串N且字符串 N 在打印时打印的是字符串 N 的内容#includestdio.h
int main()
{int a 10;PRINT(a 3);return 0;
}在这段代码中#N 将会被处理为字符串 N 即 a 3 ,于是整个宏定义函数就相当于
printf(The value of a 3 is %d\n,VALUE);即相当于字符串The value of “、字符串a 3与字符串” is %d\n之间进行了自动连接成为了一整个长字符串。
4.2 预处理操作符 ##
预处理操作符 ## 的作用将位于其两边的符号合成为一个符号。并且它允许宏定义从分离的文本片段中创建标识符。
#define CAT(CLASS,NAME) CLASS##NAME
//宏定义函数的作用为将符号 CLASS 与符号 NAME 合并为符号 CCLASSNAME
#includestdio.h
int main()
{int class3xiaohong 888;int back CAT(class3, xiaohong);//使用宏定义函数 CAT 将符号 class3 与符号 xiaohong 合并为 class3xiaohong//而符号 class3xiaohong 表示的是值为 888 的 int 类型变量故用 int 类型变量 back 接收并打印或执行其他操作printf(%d\n, back);return 0;
}在这段代码中通过宏定义的预处理操作符 ## “将符号” class3 “与符号” xiaohong “进行了合并合并成了符号” class3xiaohong “接着我们看到在这之前我们就已经定义并初始化了 int 类型变量 class3xiaohong于是我们就可以使用 int 类型的变量对宏定义函数所合成的符号” class3xiaohong 所表示的值进行接收了。
5. 条件编译
5.1 简述条件编译指令
通过使用条件编译指令我们在编译一段代码时如果要将一条或一组语句编译或舍弃是很方便的。例如一些调试性的代码就可以通过使用条件编译指令来实现选择性的进行编译。
#includestdio.h
int main()
{int arr[10] { 0 };int i 0;for (i 0; i 10; i){arr[i] i 1;printf(%02d , arr[i]);
#if 0printf(\n);
#endif}return 0;
}在这段代码中包含在条件编译指令 #if 与 #endif 之间的换行打印是否执行就取决于条件编译指令的参数此时参数为 0 即为假则不进行换行打印 那么我们再将参数改为1即真
#includestdio.h
int main()
{int arr[10] { 0 };int i 0;for (i 0; i 10; i){arr[i] i 1;printf(%02d , arr[i]);
#if 1printf(\n);
#endif}return 0;
}这时条件编译指令参数为真于是执行其中的代码进行换行打印
5.2 常见条件编译指令
①. 单分支条件编译指令
#if 常量表达式//执行操作
#endif//或常量表达式由预编译器求值
#define __DEBUG__ 1#if __DEBUG__//执行操作
#endif②. 多分枝条件编译指令
#if 常量表达式//执行操作
#elif 常量表达式//执行操作
#elif 常量表达式//执行操作
...#endif③. 判断是否被定义
#if defined(symbol)//如果 symbol 被定义过则执行操作
#endif//或
#if !defined(symbol)//如果 symbol 没有被定义过则执行操作
#endif④.嵌套指令
#if defined(MAX)#if 1printf(%d\n, a b ? a : b);#endif
#elif defined(MIN)#if 0printf(%d\n, a b ? a : b);#endif
#endif通过灵活的嵌套使用条件编译指令就能实现某条或某组程序代码的选择性编译。
7.总结
今天我们了解了计算机的程序环境和预处理过程的相关知识,学习了掌握计算机预处理操作以及对程序编译的环境、编译链接过程以及很重要的预处理指令 #define 进行了研究并且对条件编译相关指令和宏与函数有了一定的了解和区分希望我的文章和讲解能对大家的学习提供一些帮助。 当然本文仍有许多不足之处欢迎各位小伙伴们随时私信交流、批评指正我们下期见~