C语言-程序环境与预处理详解

C语言-程序环境与预处理详解

首先抛出几个问题:

1:我们都知道电脑或其它硬件设备都只认识二进制数据,那么我们编写的.c文件如何被计算机所识别?

2:写过C语言代码的都知道,如果程序中有错误,编译器会报错,那么错误主要有哪些?又是怎么被计算机发现的?

3:C语言的预处理操作都有哪些?宏定义和函数的主要区别在哪?

1、我们写的.c程序如何变为可执行程序?(.c.exe

废话不说,直接上图:

从图中我们可以看到.c文件到.exe文件之间主要存在预处理、编译、汇编和链接步骤。这里就这四个步骤再详细说明一下:

1:预处理

下文中专门针对预处理进行详细讲解,这里先不多说。

2:编译

编译环节主要是将各个.i文件进行语句分析及符号汇总。比如每行代码结尾没有加分号,或判断语句后没有将判断条件用括号括起来等。 如果有错,将直接报错。(如果程序中存在不符合C语言语法的问题,IDE都会在编译环节进行报错。并且这一环节会将C语言代码转化为汇编代码

(IDE:集成开发环境。 IDE的工作包含预处理、编译、汇编、链接以及调试器,我们使用的Visual Studio就是一款IDE)。

3:汇编

汇编的主要工作是将汇编代码转化为二进制文件(到这一步,我们用文本阅读器打开test.o文件,就已经无法阅读了,显示乱码),并且汇编也会形成符号表。

形成符号表:

4:链接链接的作用是合并符号表即符号表的重定位。(我们一个工程中会有很多个.c文件,每一个.c文件都会独立经过预处理、编译和汇编环节,每个.c都会生成一个目标文件(目标文件中包含各自.c文件中的符号表),然后在链接环节,链接器将全部的目标文件和相关链接库进行链接,生成.exe)。

如果程序中存在未定义的变量或函数,或者在某一个.c中调用其它文件中的变量或函数 但没有包含头文件,都属于链接错误,将在这一步报错。

过程如下图:

链接过程
编译器链接过程

.c到.exe的流程大概讲述完毕。实际上预处理、编译、汇编和链接都十分复杂,而我们在编写程序时,一般主要关注预处理过程,下文中就预处理进行详细讲解,其余过程都属于编译器的工作,这里不再做介绍。

2、预处理

预处理环节是编译器最先进行的一步,编写程序时我们时常调用一些预处理指令。这里主要介绍预处理的二个主要功能:

1:宏定义

宏定义只需记住一个词:替换。  (完全替换,连一个标点符号都不放过)

2:头文件中的应用(条件编译)

条件编译的主要作用是:增加程序的兼容性

 1:宏定义

重要的事情再说一遍:宏定义的作用方式就是替换,完全的替换。

(1)一些预定义的符号
 

一些预定义符号及功能
__FILE__进行编译的源文件(显示文件的属性信息)
__LINE__文件当前的行号(显示__LINE__所在的行号)
__DATE__文件被编译的日期(显示时间(年月日))
__TIME__文件被编译的时间(显示时间(时分秒))

实操:

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

结果为:

运行结果

这里说明一下这些预定义符号的作用:程序日志(在程序关键地方将这些关键信息进行输出(需要学习C语言文件操作的知识),达到调试的目的)。这个功能将在C语言-文件操作的博客中进行实现。

(2)宏定义

宏定义的格式为:#define A B      :作用是,程序中B往往书写较为复杂(名字过长等),这里定义一个A,在程序中直接书写A,就可以达到书写B的效果。

(1):替换

在预处理环节,程序中所有出现A的地方,全部被替换为B。

知识点1:程序中使用宏定义时,千万不要吝啬括号。

#define ADD(a,b) a+b
#define ADD1(a,b) ((a)+(b))
int main()
{
	int a = 5;
	int b = 3;
	printf("%d\n",10*ADD(a,b));  //运行结果为53
	printf("%d\n", 10 * ADD1(a, b));//运行结果为80
	return 0;
}

printf("%d\n",10*ADD(a,b));   在经过预处理环节后,程序变为:printf("%d\n",10* a + b);      

因为宏定义是完全替换,所以,宏定义时千万不要吝啬括号,因为括号的优先级是最高的。

知识点2:宏定义最后不要加分号

原因与知识点1一样,宏定义是完全替换。

如果#define ADD(a,b) ((a) + (b)); 

那么预处理后,printf("%d\n",10*ADD(a,b));  变为:printf("%d\n",10*((a) + (b)););       这里会出现语法错误。

(2):预处理环节不进行表达式的计算

这里插播一条知识点:我们的源文件最终会被编译器编程一个可执行文件(.exe文件)。

在没有运行.exe文件之前:全局变量、静态常量以及常量字符串在编译环节就已经在内存的静态区分配好了空间。

这里存在一个问题:如果我们只编译,但不运行,程序没有运行,怎么会在内存中分配空间呢?

原因:比如我们的Visual Studio这种IDE软件,我们编写好源文件,点击运行,此时IDE的预处理环节就会程序编译环节向内存的静态区申请空间。 如果,我们只是生成.exe文件而不运行,此时全局变量、静态常量以及常量字符串会被存储在.exe特定的节中,当.exe被点击运行后,全局变量、静态常量以及常量字符串会被直接加载到内存的静态区,而不需要额外的代码来向内存开辟空间。

运行.exe文件之后:程序运行之后,局部变量在定义时,程序会向内存的栈区申请内存,局部变量的生命周期结束后,程序将空间返还给栈区(自动的)。 如果是我们使用专门的函数进行动态内存开辟,此时程序向内存申请的空间是堆区,用完我们需要手动将堆区的空间进行返还

话有些多,直接上图:

有了上述的铺垫,我们知道,在预处理环节,程序中的表达式是不计算的,在程序运行中才计算表达式。

int main()
{
	char b = 0;
	int a = 5;
	printf("%d\n", sizeof(b = a + 2)); //运行结果为1
	printf("%d\n", b);          //运行结果为0
	return 0; 
}

sizeof是C语言中的一个关键字,其计算数据的大小(单位为字节)。

这段程序主要有二点:

(1)sizeof(b = a + 2))  由于sizeof为关键字,其起作用的阶段为程序的编译阶段,表达式不计算,所以b的值并没有变为7。(注意:b的结果与b的数据类型无关,b的值没变只是因为表达式没有被计算,即sizeof的作用是在程序的编译环节,而不是程序的运行环节)

(2)无论表达式是否计算(实际上没有计算),sizeof求的都是b所占空间的大小,b为char型,所占大小为1字节。

(3)#的骚操作

(1):#可以将参数插入字符串中

简而言之:一般而言,“  ”之中的是字符串,里面所有的字符不具值的属性,但对于转义字符和#后的字符,要另当别论。

废话不说,上代码:

(1)对于宏定义而言:

宏定义的本质是替换,所以:PRINT("%d",a+6);  等于 printf("the value of " "a+6" " is " "%d" "\n", a+6);   从这里可以看出,宏定义中使用#可以将参数变为字符串(#VALUE == "a+6")

这里需要注意二点:(1)printf("a""b"); 等于 printf("ab");  (2)#的作用仅仅在宏定义中起作用,不可在函数内调用,否则会出语法错误。并且在宏定义中,#VALUE是不可在""内部。

(2)对于转义字符而言:

转义字符不是字符,每个转移字符都有自身的功能。C语言中常用的转义字符如下:

C语言中常用的转义字符
转义字符功能
\?用于表示字符 ?
\'用于表示字符 '
\"用于表示字符 "
\\用于表示字符 \
\a警告字符,并使电脑蜂鸣器响一声
\b退格符
\f进址符
\n换行
\r回车
\t水平制表符
\v垂直制表符
\ddd用于表示1-3个八进制数
\xdd

用于表示2个十六进制数

(2):##可以将字符串合并为变量名

需要注意的二点:(1)##的作用也必须在宏定义中,不可在函数内部这样使用。(2)a##b 中 a和b合成后的变量ab必须是已定义的变量名,否则程序会报错。

(4)宏和函数的区别

宏:常被我们用于执行简单的运算

函数:常被我们用来实现复杂法功能,函数的语法较为严谨。

这里给出二者的详细区别:

宏与函数的区别
属性函数
代码长度每次使用时,宏都会插入程序中。函数只需要定义后,别的函数需要时,直接通过函数地址来调用函数。
执行速度较慢(函数的调用和返回需要耗时)
操作符优先级宏是直接插入程序之中,需要考虑插入处附近的代码,避免宏插入后产生语法错误函数是相互独立的,相互直接的调用通过函数地址来进行,不需要考虑那么多操作符优先级的问题
是否有副作用宏替换后,会导致一些变量的数值发生改变,这一点要注意函数直接传递数值,一个函数不会更改另一个函数内部变量的数值(除非通过指针)
参数类型宏的本质是替换,只有替换后程序语法正确,不需要在宏中考虑数据类型(这也是宏语法不严谨的原因)函数传参基本都需要知道参数的数据类型。 (非基本的情况:在C语言—指针文中有介绍)
调试不可调试(预处理阶段就替换完了,在程序运行阶段才有调试概念)可调试
递归不可递归可递归

宏和函数都是我们写程序时的基本功,要根据二者的区别,根据实际情况来决定使用宏还是函数。

2:条件编译

条件编译的目的:增加程序的兼容性

条件编译的使用场所:在头文件大量使用

常用的条件编译指令:

#undef:用于移除一个宏定义

#if  和#ifdef 和#ifndef     :判断语句(#if 与##ifdef效果相同,如果其后条件为真,则运行之后的代码。 #inndef 如果其后条件为假,则运行之后的代码)

#elif   :判断分支语句(与else的效果相似)

#endif  :判断结束语句(#if 与#endif 必须成对出现)

 

(1)导入头文件

导入头文件一般有二种方式:

(1):<stdio.h> 

这样导入头文件时,IDE直接去库文件所在的位置去查找,如果库文件没有,则报错。

对于我们直接写的头文件,其位置一般在工程所在目录下,并不在库函数的位置(库函数的位置是安装IDE时就已经确定的)。

(2):"stdio.h"

这样导入头文件,IDE先去工程所在目录下去查找,如果没有,再去库文件所在位置去找,如果都没有,则报错。

对于官方给定的库函数,使用这种方法导入,也可以成功,就是比较耗时。

(2)头文件的重复导入

对于一个头文件(特别是库函数),一个工程中会有很多个.c文件需要某个库函数,如果各自都导入,这样无疑会增加程序编译后的代码长度。

这是因为:每个.c在编译环节都是独立的,都需要各自导入头文件。但在链接环节,连接器将各个目标文件合并为一个.exe,所以,一个头文件在编译环节被导入很多次是无意义的。

解决办法:当我们在某个.c中需要调用一个头文件时,我们先判断一下这个头文件在工程中其它位置是否已被导入,如果已被导入,则不需要再次导入;如果没有被导入,则次.c中需要导入。

解决方案一:在我们编写头文件时加上下面的代码

#ifndef __ADD__    //如果__ADD__ 没有被定义

#define __ADD__ //定义__ADD__   (这属于无初始化的全局变量,因为我们不需要使用该变量的值,只需要将其定义即可。__ADD__ 在内存中被存储在BBS之中 )

#endif 

__ADD__仅仅是一个标识符,可以更改。

如果我们自己写的一个头文件add.h中加入上述代码后。其余.c要调用add.h时,首先要判断add.h是否已被导入工程中,如果没有则导入,并定于标识符__ADD__,当其它.c文件再导入add.h时就会发现__ADD__已被定义,则不再导入add.h。这样可以避免一个头文件被重复导入。

解决方案二:#pragma once

在我们自己写的头文件中加上#pragma once,即可保证头文件不会被重复导入。其作用效果与解决方案一一样。

(3)条件编译

假设在程序中,某部分代码使用的条件较为苛刻,但是某些情况下又需要。为了增加程序的兼容性,我们可以将这部分代码放到条件编译之下。需要时可以直接用,不需要时,程序不编译这部分代码,不会使代码冗余。

火速实操:

#define AD 1
int main()
{
#if AD
	printf("%d",1);
#elif AD==0
	printf("%d", 0);
#endif
	return 0;
}

条件编译的语法和if 、else if 的使用方法相似,判断语句都是常量表达式。唯一不同的是判断语句不需要使用括号括起来,并且表达式不需要在花括号内,结束时使用#endif ,而不是使用花括号结束。

3、结束语

宏定义最重要的概念的就是替换。记住这一点,我们在使用宏定义时,就会避免很多坑。本文叙述的小知识点较多,希望大家能从这里获取一些新知识。

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值