C语言进阶—程序环境和预处理

1.翻译环境

翻译环境的主要目标是把 test.c 转化为 test.exe,其中 test.c 需要先经过预编译转为 test.i ,然后再经过编译转为 test.s ,接着经过汇编翻译为 test.o ,最后再由链接器将目标文件 test.o 与其他目标文件、库函数文件等链接后生成可执行程序 test.exe。其中前三步由编译器完成,最后一步由链接器完成(这两个工具已经集成于VS中了),每个不同的源文件都需要分开编译,最后由链接器合并,下图很好的演示了整个翻译过程,当然更详细的在后面
在这里插入图片描述

1.1预编译

预编译阶段要干的事情:

  • 1.头文件的包含#include
  • 2.注释的删除
  • 3.#define符号的替换
    这些操作都是文本操作,结束后会形成一个test.i文件
    代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define MAX 100
//测试预编译阶段
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int x = 10;
	int y = 20;
	int z = Add(x, y);
	printf("%d\n", z + MAX);
	return 0;
}

test.i文件:
在这里插入图片描述

1.2编译

编译要干的事:
1.语法分析
2.词法分析
3.语义分析
4.符号汇总

这一阶段是把c语言代码转化成了汇编代码,生成test.s文件。此时代码我们已经看不懂了,文件格式为 elf,需要用其他工具来解析查看此文件,这里就不展示了。

1.3汇编

汇编阶段:
1.把已经生成的汇编指令转换成二进制指令
2.形成符号表

最终生成 .o 目标文件,此时的文件格式仍然为 elf
比如上面的代码,会生成这两个符号表:
在这里插入图片描述

1.4链接

1.合并段表
2.将符号表进行合并和重定位

由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序,在链接阶段可以发现被调用的函数未定义。

这些操作只能在Linux环境下看到,现在我们还没学,只需要理解就可以不需要操作
1.输入 gcc -E test.c -o test.i 可以把预编译阶段生成的代码放到 test.i 这个文件中
2.输入 gcc -S test.c -o test.s 可以将编译阶段生成的汇编代码放到 test.s 中
3.输入 gcc -c test.c -o test.o 可以把汇编阶段生成的二进制代码放到 test.o 中

1.5运行环境

程序执行的过程:

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

2.预处理

2.1预定义符号

FILE进行编译的源文件
LINE文件当前的行号
DATE文件被编译的日期
TIME文件被编译的时间
STDC如果编译器遵循ANSI C,其值为1,否则未定义这些预定义符号都是语言内置的

这些预定义符号都是语言内置的。
举个例子:

int main()
{
	printf("file:%s\n", __FILE__);
	printf("line:%d\n", __LINE__);
	printf("data:%s\n", __DATE__);
	printf("time:%s\n", __TIME__);
	return 0;
}

在这里插入图片描述

2.2#define

2.2.1#define定义标识符

#define 定义的符号,在翻译环境中的预编译阶段,会被替换。
我们可以用宏做一些简单的计算问题

#define ADD(x,y) ((x)+(y))//定义两个数相加
//这里注意#define是不管数据的类型的

我们在三子棋和扫雷中,还见过 #define 定义标识符常量,有效避免了大小固定的问题

#define ROW 3
#define COL 3	//#define 定义标识符常量

在这里我们可以发先#define定义的宏,符号等是不需要在后面加分号的

#define ROW 3
#define COL 3//err

2.2.2定义宏

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

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

#define SQUARE(x) x * x

把数据放进去

 SQUARE(5)//结果就是25

但是!!

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

乍一看这段代码输出结果是36
事实上呢?
替换文本时,a+1不会计算出来,会直接把x替换

#define SQUARE (a+1) a+1*a+1
//就是5+1*5+1=11

所以最后结果是11,所以我们需要给宏定义加上括号

#define SQUARE(x) (x) * (x)

这样问题就解决了!
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

2.2.3#define替换规则

来简单总结一下 #define 的替换规则:

  • 1.当宏在进行替换时,会对其中的参数进行检查,看是否有 #define 定义的符号,如果有的话,先优先替换参数
  • 2.替换文本会被插入到程序中原来文本的位置;对于宏,参数名被他们的值所替换
  • 3.最后,再对结果文件进行扫描,看看是否还有 #define 定义的符号,如果有的话,就重复上述步骤
    注意:
    1. 宏的参数和 #define 定义中可以出现其他 #define 定义的符号,也就是说#define 可以嵌套使用,但要合法。对于宏,不能使用递归
    1. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不会被搜索。

2.2.4#和##

#这个东西比较有意思,就是在宏定义中,把某个参数变成对应的字符串,再配合上 " " 号,就能 插入到后面的字符串中,比如下面这个例子,实现了全数据类型的打印

//奇葩预定义指令 #
//实现全类型数据打印
#define PRINT(format,value) printf("the value of "#value" is "#format"\n",value)
int main()
{
	int i = 10;
	PRINT(%d, i);
 
	char c = 'a';
	PRINT(%c, c);
 
	float f = 5.5f;
	PRINT(%.2f, f);
	return 0;
}

结果:the value of i is 10
   the value of c is a
   the value of f is 5.50
#这个东西配合上宏定义和字符串插入的特征,完成了一个函数无法实现的任务

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

#define SCP(A,B) A##B
int main()
{
	int HuGe = 100;
	printf("%d\n",SCP(Hu,Ge));
	return 0;
}

在这里插入图片描述
##就可以把He,Ge两个合成HeGe这个变量。以%d的形式打印出来就是100
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

2.2.5带副作用的宏参数

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

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

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

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
}

替换过后:

z = ( (x++) > (y++) ? (x++) : (y++));//这里x和y++后会保存

所以输出的结果是:

x=6  y=10  z=9

2.2.6宏和函数对比

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

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

那为什么不用函数来完成这个任务?
原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    所以宏比函数在程序的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。
    所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
    用于>来比较的类型。
    **宏是类型无关的,就是不用管是int,double,还是float类型它都可以接受。****

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

    1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
      的长度。
    1. 宏是没法调试的。
    1. 宏由于类型无关,也就不够严谨。
    1. 宏可能会带来运算符优先级的问题,导致程容易出现错。
      结论:
属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。只在函数调用的时候求值一次它的结果值传递给函数表达式的求值结果更容易预测
副作用参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

2.2.7命名约定

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

2.3.#undef

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

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
//如果不想用了
#undef MAX//这样MAX就会被移除

2.4.命令定义行

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。

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

在上面的代码中sz是不确定的,但我们可以在命令行中定义它。
编译指令:

//linux 环境演示
gcc -D sz=10 test.c    假设我们的c语言文件就是test.c
结果:0 1 2 3 4 5 6 7 8 9

2.5.条件编译

条件编译和if else语句工作原理很像的,满足条件就执行,不满足就不执行。

语法
#if      #endif
#if 后面跟条件表达式,当条件成立,后续代码才会编译
#endif 条件编译块结束的标志,每个 #if 都必须有一个 #endif 与之匹配

常见的条件编译指令:
1.单分支条件编译

int main()
{
#if 1 > 2
	printf("hello ");	//条件不成立,此条语句不参与编译
#endif
	printf("world\n");
	return 0;//结果:world
}

注意:有 #if 就要有 #endif ,二者互为彼此存在的必要条件
2.多分支条件编译
多分支就像if-else语句一样,增添了#else if#else

语法:
#if   #elif   #else   #endif
//其中,#if   #elif 后面都需要跟条件表达式

//如果前两个都为假,那就编译 #else 后的语句

// #endif 服务于 #if ,不可缺失

	#define MAX(x, y) ((x)>(y)?(x):(y))
	#define ADD(a,b) ((a)+(b))
	int main()
	{
		#if MAX
		pfintf("Yes\n");
		#elif ADD
		pfintf("Yes\n");
		#else
		printf("No\n");
		#endif
		
		#ifndef SCP//如果没有定义SCP就执行下面的的语句
		printf("Yes\n");
		#endif
	}

结果:
Yes
Yes
No
Yes
3.判断是否被定义

#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol

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

这就和if-else语句一样,可以嵌套使用

那这种条件编译在实际写代码中存在吗?答案是:存在,而且很频繁。

我们可以看到这是vs中stdio.h文件下的代码,里面几乎都是这样的条件编译,所以条件编译是很重要的

2.6.文件包含

2.6.1头文件被包含的方式

  • 本地文件包含----就是我们自己写的头文件

#include “game.h”
在这里插入图片描述
查找策略:先在我们自己的源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误。

我使用的是VS2013:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
在这里插入图片描述

  • 库文件包含

#include<stdio.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

其实库函数也可以用“ ”的方式来包含但这样效率比较低,而且不容易区分到底是本地文件还是库文件了

2.6.2嵌套文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
替换的方式:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
那我们就可能碰到一个头文件被引用多次的情况。一次两次还好,如果是一个大的C语言工程,代码动辄几十几百万行,那头文件的引用是需要避免重复引用的。

第一种方法:
使用条件编译

#ifndef __TEST_H__	//打个标记,如果是第一次被引用
#define __TEST_H__	//就会创建一个标识符,然后开始预处理头文件中的内容
 
//预处理头文件中的内容 
#endif
//等再次来到这个头文件中时已经定义了__TEST_H__就不会在处理头文件中的代码

第二种方法:

#pragma once
在头文件的首行放上这条代码就可以避免重复引用

在这里插入图片描述
我们可以看到stdio.h中就有这条代码。

到这程序和预处理就结束了,大家可以先收藏。哈哈,其中用到的Linux知识可以等到学了之后再来看看,相信再来看的时候会有新的收获!
大家一起努力!

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值