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

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

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

        计算机是能够执行二进制指令的,但我们写出的C语言代码是文本信息,计算机不能直接理解,翻译环境的存在,就是将C语言的代码转换为二进制的指令,二进制指令放在可执行程序中;执行环境则用来执行二进制的代码

翻译环境

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

每个源文件都会单独经过编译器的处理,生成自己对应的目标文件,这个过程就叫编译

多个目标文件加上链接库经过链接器的处理,最后生成可执行程序,这个过程就叫链接

 

链接库:写代码时我们会使用库函数,库函数可以直接使用是因为它们都编译放在了一些静态库中,例如我们在vs编译器上写代码,在代码中使用了scanf函数,那么vs编译器(集成开发环境)会自动把静态库链接到程序中去。

补充:如vs2022集成开发环境,集成了编译器(cl.exe)、链接器(link.exe)、调试器

编译和链接

在翻译环境中,整个源代码转换为可执行机器指令的过程可分为两个:

一、编译       编译又可分为:

(1)预编译(预处理)

该阶段会处理:注释的删除#include等头文件的包含(展开)#define等符号的替换,这些都是文本操作。

#include#define这些被称为预处理指令,所有预处理指令都是在预处理阶段处理的

(2)编译

该阶段会检查语法错误,没有报错会把C语言代码翻译为汇编指令生成汇编代码(是指令级代码),还会进行语法分析、词法分析、语义分析、符号汇总等操作。

《编译原理》:编译器的原理

(3)汇编

该阶段会把汇编代码翻译为二进制的指令,交给CPU去执行,生成目标文件,目标文件中放的都是二进制的指令。

该阶段会形成符号表

二、链接

该阶段会合并段表合并和重定位符号表

该阶段会查看符号表

补充

Linux环境下,gcc编译产生的目标文件test.o可执行程序test都是按照 ELF 这种文件的格式来存储的

readelf工具能够识别elf格式的文件

test.o这样的文件,按照 ELF 这种文件的格式来存储时,会把文件分为一个个不同的段,每个段存放数据,如图所示

执行环境

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

运行环境

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

预处理

预定义符号

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

注意!!!:是左右各两个下划线_ _FILE_ _

这些预定义符号都是语言内置的。
int main()
{
	printf("%s\n", __FILE__);//__FILE__是文件名,即字符串
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __STDC__);//如果显示未定义,则说明当前编译器不支持ANSI C
	//预处理阶段就会被替换
	return 0;
}

#define

#define定义标识符

语法
#define name stuff   
例如:
#define M 100
#define STR "abc"
#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 MAX 100 ;   这种后面加有 

这样  int a=MAX;  就被替换为了   int a=100 ; ;
可能会出现问题,比如
#define M 100
int main()
{
    int b = 0;
	int c = 0;
	if (b > 5)
		b = M;  //  b=100;;  变成了两条语句  而if语句只能默认跟一条语句
	else    //这里就会出问题,else不知道和谁去匹配
		b = -1;
    
    return 0;
}	

#define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)。
宏的申明方式: 
#define name( parament-list ) stuff
其中的 parament - list 是一个由逗号隔开的符号表,它们可能出现在stuffff中。
#define MAX(x,y) ((x)>(y)?(x):(y))  
//x,y带括号是因为它们不是变量,而是参数,可能是一个表达式

int main()
{
	printf("%d", MAX(5, 56));
//预处理阶段替换为((5)>(56)?(5):(56))
	return 0;
}
注意:
1.参数列表的左括号必须与name 紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。
#define MAX(x,y) ((x)>(y)?(x):(y))
//        MAX后面没有空格才会认为MAX后面是它的参数
//例如:#define MAX (x,y) ((x)>(y)?(x):(y)), 会认为MAX替换为了(x,y) ((x)>(y)?(x):(y))
int main()
{
	printf("%d", MAX(5, 56));//调用的时候可以加空格,比如写作 printf("%d", MAX  (5, 56));
	return 0;
}

2.使用#define定义宏不要去吝啬使用括号

#define SQUARE(x) x*x //比如想定义为平方,但很容易出问题
//所以建议最好写成((x)*(x))
int main()
{
	int a = 3;

	printf("%d\n", SQUARE(a));//9

	printf("%d\n", SQUARE(a+2));//为什么是11而不是25?   
                                //因为预处理时变为了a+2*a+2,所以变成了11
	
return 0;
}
       所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

#define替换规则

在程序中扩展#define定义符号和宏时,需要涉及以下几个步骤:
1. 在调用宏时,首先对参数进行检查,查看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3. 最后,再次对结果文件进行扫描,查看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
#define SQUARE(x) ((x)*(x))
#define M 3
int main()
{
	int a = 3;
    int r=SQUARE(M+2);
    //这里有M,M首先被替换为3,
    //2.SQUARE(3+2)
    //3.预处理完后变为((3+2)*(3+2))
	return 0;
}
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#define M 10
int main()
{
    printf("M=%d\n",M);//前面的M是字符串常量的内容,不被搜索。
    //打印出来就是M=10
    return 0;

}

#和##

可以理解为预定义操作符,只能在宏中使用

#

C语言支持这种写法,可以得出结论:字符串是有自动连接的特点的。

int main()
{
	printf("hello world\n");
	printf("hello ""world\n");
	//这两段代码能打印出同样的hello world
	return 0;
}
如何把参数插入到字符串中?
#define PRINT(x,format) 	printf("the value of "#x" is "format, x)
//    使用 # ,可以把一个宏参数变成对应的字符串,比如 #x 会被预处理为 "x"
//    只有当字符串作为宏参数的时候才可以把字符串放在字符串中,就如这里的format
int main()
{
	int a = 10;
	printf("the value of a is %d\n", a);
	//能不能使用函数把它们封装起来统一打印?
	//答案是不能的。
	//但可以使用宏
	PRINT(a, "%d\n");

	int b = 20;
	printf("the value of b is %d\n", b);
	PRINT(b, "%d\n");

	float c = 3.14f;
	printf("the value of c is %f\n", c);
	PRINT(c, "%f\n");

	return 0;
}
1.只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
2.使用 # ,可以把一个宏参数变成对应的字符串。

##

## 可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define CAT(x,y)  x##y  //注意这里就不能乱加括号了!!!
int main()
{
	int Time_8_11 = 100;
	printf("%d\n", Time_8_11);
	printf("%d\n", CAT(Time, _8_11));//这样也可以实现打印Time_8_11的值

	return 0;
}
注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

带副作用的宏参数

何为副作用?

int main()
{
	int a = 10;
	//int b = a + 1;//b变成了11,a依旧是10
	int b = ++a;//不但b变成了11,而且a也变成了11
	//这个表达式就是带有副作用的表达式

	return 0;
}
       当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
#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++))
    //先会进行 a>b的判断  
    // 然后a++变成6,b++变成7 
	// 此时执行后面的 b++ ,会把7赋值给c,然后b再变为8         
	printf("%d\n", a);//6
	printf("%d\n", b);//8
	printf("%d\n", c);//7

	return 0;
}

对比宏和函数

宏相比函数的优势

宏通常被应用于执行简单的运算。比如找出两数中的较大值。
#define MAX(x, y) ((x)>(y)?(x):(y))
为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码,可能比实际执行这个小型计算工作所需要的时间更多。 所以 宏比函数 在程序 的规模和速度方面更胜一筹
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。而宏可以适用于整形、长整型、浮点型等各种能用> 来比较的类型。 宏是类型无关的。
//宏
#define MAX(x, y) ((x)>(y)?(x):(y))

//函数
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("a = %d\n", a);
	printf("b = %d\n", b);
	printf("c = %d\n", c);

	return 0;
}

宏相比函数的劣势

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的,调试在程序运行后,而宏的替换在预处理阶段。
3. 宏由于类型无关,所以不够严谨。
4. 宏可能会带来运算符优先级的问题,容易导致程序出错。

补充

宏的有些能力是函数绝对没有的
比如:宏的参数可以出现类型 ,但是函数做不到。
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))

int main()
{
//    int* p = (int*)malloc(126 * sizeof(int));
	//malloc函数使用起来较为繁琐麻烦
	//我们期望能够传类型 如 malloc(126, int);
	//但实际上函数是不能接收类型参数的,函数的参数不能传类型
	//这时我们可以使用宏
	int*p = MALLOC(126, int);
//宏的参数会直接替换进去:int* p = (int*)malloc(126 * sizeof(int));

	return 0;
}

总结

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

//函数
int Max(int x, int y)
{       //3     3
	return (x > y ? x : y);
}

int main()
{
	int c = MAX(1+2, 3);
	//int c = ((1+2)>(3)?(1+2):(3)) 宏的参数会直接替换进去

	c = Max(1+2, 3);//函数会先行计算,直接传 3和3
	return 0;
}

内联函数——inline

既然宏和函数各有优缺,那么存不存在集合两者优点的东西?

是存在的,在最新的语法中,有一种称为内联函数的东西,集合了二者的优点。

命名约定

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

//宏
#define MAX(x,y) ((x)>(y)?(x):(y))
//函数
int Max(int x, int y)
{
	return x > y ? x : y;
}
//有一个例外——offsetof是宏,但它是全小写
int main()
{

	return 0;
}

#undef

这条指令用于移除一个宏定义。  
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define MAX(x,y) ((x)>(y)?(x):(y))

int main()
{

	printf("%d\n", MAX(3, 6));//会打印6
#undef MAX
	printf("%d\n", MAX(3, 6));  //这里就会报错
//可以选择重新定义
#define MAX(x,y) 36
    printf("%d\n", MAX(3, 6));  //这就又可以正常使用了
	return 0;
}

命令行定义

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

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是如果另外一个机器内存大些,我们又需要一个数组能够大写。)

代码中虽然没有给SZ个值,但可以在编译时指定SZ的值是多少,使得程序正常执行

条件编译

满足某种条件,给代码进行编译;但不满足条件,就不进行编译。做到什么条件下编译什么样的代码(与 if  else 语句类似)。条件编译也是在预处理阶段进行。

常见的条件编译指令

1.单个分支的条件编译
#if 常量表达式
 ......
#endif
常量表达式由预处理器求值。   (if else是什么条件下执行什么样的代码)
int main()
{
#if 9>6  //#if后面的表达式为真,所以对下面的代码进行编译
	printf("hehe\n");//代码参与编译
	printf("hehe\n");
#endif

#if 0  //#if后面的表达式为假,所以对下面的代码不进行编译
	printf("hehe\n");   //预处理阶段直接删除
#endif
	return 0;
}
2.多个分支的条件编译
#if 常量表达式
 ......
#elif 常量表达式
  ......
#else
 ......
#endif
#define M 1
int main()
{
#if M==0
	printf("M=0\n");
#elif M==2
	printf("M=2\n");
#else
	printf("M=1\n");
#endif

	return 0;
}
3.判断是否被定义
#if defined(symbol)   或  #ifdef symbol
#if !defined(symbol)  或  #ifndef symbol
#define M 100
int main()
{
#if defined(M)
	printf("M被定义了\n");
#endif

#ifdef M
	printf("M被定义了\n");
#endif

#ifndef M
	printf("M没有被定义\n");
#endif

#if !defined(M)
	printf("M没有被定义\n");
#endif
	return 0;
}
4.嵌套指令
#define B 50
#define N 100
int main()
{

#if defined(A) 
#ifdef M 
	printf("AM被定义了\n");
#endif
#ifdef N 
	printf("AN被定义了\n");
#endif

#elif defined(B) 
#ifdef N 
	printf("BN被定义了\n");
#endif

#endif 
	return 0;
}
补充:花式注释
#if 0

int main()
{
	;
	return 0;
}

#endif
//上述代码就相当于被注释掉了
int main()
{
	printf("hehe\n");
	return 0;
}
适用场景
条件编译指令适用场景:跨平台性代码,使得代码适用于多种平台、场景

文件包含

头文件被包含的方式

C语言中头文件的包含有两种形式:

1.本地文件包含     

#inlcude " "       使用自己创建的头文件时

#include "filename" 
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
linux 环境的标准头文件的路径:
/usr/include
VS 环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include
2.库文件包含

#include  < >     使用库里面的头文件时

#include <filename.h>
查找策略:查找头文件会直接去标准路径下查找,如果找不到就提示编译错误。
既然如此,那么对于库文件是不是也可以使用  " "  的形式包含?
答案是肯定的
但是这样做查找的效率就低些,也不容易区分是库文件还是本地文件了。

嵌套文件包含

#include指令可以使另外一个文件被编译,就好像它真的出现在了#include指令的地方一样,展开头文件的本质是拷贝。

这种替换的方式很简单:预处理器先删除这条指令,再用包含文件的内容替换,相当于把文件内容直接拷贝过来。假如一个头文件被包含10次,那实际上它就被编译了10次。

比如出现这样的场景:

comm.h  和  comm.c  是公共模块。
test1.h  和  test1.c  使用了公共模块。
test2.h  和  test2.c  使用了公共模块。
test.h  和 test.c 使用了  test1  模块和  test2  模块。
这样最终程序中就会出现两份  comm.h  的内容,就会造成文件内容的重复。
又或是这样的场景:
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"


//test.h中声明了一个函数
int Add(int x,int y);

     可以看到预处理阶段,该函数声明出现了四次

解决方法:条件编译

1.比如刚刚的test.h头文件,可以这样写:

#ifndef __TEST_H__ //随意定义一个符号
#define __TEST_H__ 
   
   int Add(int x,int y);

#endif   

这样预处理时就会只有一个函数的声明了。

2.或者这样写:

#pragma once
   int Add(int x,int y);

与上面的写法效果相同。这样就可以避免头文件的重复引入了。

在VS的编译器上,创建头文件会自带    #pragma once

其它预处理指令

  • 31
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值