程序环境和预处理

目录

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

二 详解编译+链接

2.1 翻译环境

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

2.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 宏和函数对比

3.2.7 命名约定

3.3 #undef

3.4 命令行定义

3.5 条件编译

3.6 文件包含

3.6.1 头文件被包含的方式

3.6.2 嵌套文件包含


励志小模块

不要太在意过去,下次做好就行。


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


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

不同的编译器,对于缓冲区实现的方式不同。

ANSI C的任何一种实现中,存在两个不同的环境: 1 种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第2种是执行环境,它用于实际执行代码。

二 详解编译+链接

2.1 翻译环境

翻译环境分成两个部分:编译和链接;(C语言源代码通过编译生成目标文件(.obj/.o)(多个源文件单独通过编译器生成各自的目标文件),目标文件加上链接库通过链接器 链接成为可执行程序(.exe))

 编译过程:预编译、编译、汇编

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

预编译(预处理)——编译——汇编  (预编译也叫作预处理)

      预处理之后产生的结果都放在test.i文件中,编译完成之后产生的结果保存在test.s中,汇编完成之后产生的结果保存在test.o或者是test.obj(在linux中是.o;Windows中是.obj)

预编译:(1)进行头文件的展开(2)删除注释(3)#define定义的符号替换(例如:#define Max 100,在预编译结束之后,Max会被替换为100,并把#define这一行给删掉)
总而言之,就是进行一些文本操作。

编译(把C语言代码转换成汇编代码): (1)语法分析(2)词法分析(3)语义分析(4)符号汇总(全局符号:例如:函数名、main等)

汇编(把汇编代码转换成二进制的指令):(1)形成符号表(全局符号给一个地址,所有全局符号+地址形成符号表)

 目标文件(.o)加上链接库通过链接器 链接成为可执行程序(.exe))

linux中.o目标文件以及可执行文件的文件格式是elf

链接(编译之后生成.o文件,加上链接库通过链接器进行链接):(1)合并段表(2)符号表(不同文件的符号表)的合并以及重定义

链接的时候,多个目标文件进行链接的时候会通过符号表,查看来自外部的符号是否真实存在

2.3 运行环境

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

三 预处理详解

这里讲述的内容都是在预处理阶段

3.1 预定义符号

__FILE__       // 进行编译的源文件
__LINE__     // 文件当前的行号
__DATE__     // 文件被编译的日期
__TIME__     // 文件被编译的时间
__STDC__     // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义 (这个并不是所有的编译器都支持)

例子:

#include <stdio.h>
int main()
{
	printf("%s\n", __FILE__);//编译出来的结果,会显示运行文件的路径
	printf("%d\n", __LINE__);//编译出来的结果,显示该行的行数303
	printf("%s\n", __DATE__);//显示时间
	printf("%s\n", __TIME__);//显示日期
	return 0;
}

记录杂志:

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
	int i = 0;
	FILE* pf = fopen("log.txt", "w");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	for (i = 0; i < 10; i++)
	{
		fprintf(pf, "%s %s %s %d %d\n", __DATE__, __TIME__, __FILE__, __LINE__, i);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

3.2 #define

3.2.1 #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__ )

3.2.2 #define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏( macro )或定义宏(define macro )。
#define name( parament - list ) stuff 
其中的 parament - list 是一个由逗号隔开的符号,它们可能出现在 stuff 中。
注意: 参数列表的左括号必须与name 紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#include <stdio.h>
#define SQUARE(x) x * x     
 //这也是个替换,用后面的内容替换前面的宏

int main()
{
	int a = 5;
	printf("%d\n", SQUARE(a + 1));//打印结果为11
	return 0;
}

解析:打印结果,我们会理所应当的以为是36,但是结果却是11,因为代入后是a + 1* a+1,所以结果是11,要是想让结果为36,正确的应该是#define SQUARE(x)  ((x) * (x))

注意:定义宏,一定要注意括号,记得带上(大部分情况是需要带的,否则会出现优先级问题,导致想要的结果和预期的不一样)

比如上述代码,我们想要的是36,但是结果却是11

每一个部分带上括号,整体也要带上括号

3.2.3 #define 替换规则

先替换,后计算

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

3.2.4 #和##

使用 # 把一个宏参数变成对应的字符串
#include <stdio.h>
#define PRINT(n) printf("the value of " #n " is %d\n", n)
//#n相当于字符串("a","b"),插入到这个位置,三个都是字符串,//不加引号(;)

int main()
{
	int a = 10;
	PRINT(a);
	int b = 20;
	PRINT(b);
	printf("the value of " "b" " is %d\n", b);//the value of 是一个字符串,b是一个字符串,is %d\n 是一个字符串
	return 0;
}

 注意:字符串是有自动连接的特点

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

 

#include <stdio.h>
#define CAT(Class, num) Class##num

int main()
{
	int Class1 = 100;
	printf("%d\n", CAT(Class, 1));//Class1
	return 0;
}

打印结果:100

3.2.5 带副作用的宏参数

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

int main()
{
	int a = 2;
	int b = 0;
	b = ++a;//a = 3; b = 3//这个就有副作用
	a = 2;
	b = a + 1;//a = 2; b = 3 这个没有副作用
	return 0;
}
#include <stdio.h>
#define MAX(x, y) ((x) > (y)? (x): (y))
int main()
{
	int a = 3;
	int b = 5;
	int m = 0;
	m = MAX(a++, b++);//((a++) > (b++) ? (a++) : (b++))后置++,先使用后++ 3>5 a=4 b=6  m=5    b=7
	printf("%d\n", m);
	printf("a = %d, b = %d", a, b);
	return 0;
}

m=6 a=4 b=7

注意是替换

3.2.6 宏和函数对比

宏通常被应用于执行简单的运算
1.用于调用函数和从函数返回的代码可能比实际执行小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹
2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之   宏是类型无关的
缺点: 1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。2. 宏是没法调试的。(宏是替换)(把代码插入到相应的位置)3.宏由于类型无关,也就不够严谨4.宏可能会带来运算符优先级的问题,导致程容易出现错误。
宏的参数可以出现类型,但是函数不可以
#include <stdio.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p1 = (int*)malloc(10 * sizeof(int));
	int* p2 = MALLOC(10, int);
//p1 p2效果一样
	return 0;
}

3.2.7 命名约定

习惯:把宏名全部大写 函数名不要全部大写

3.3 #undef

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

int main()
{
#undef MAX
	int m = 0;
	m = MAX(2, 3);//此时,这个代码无法用MAX这个宏
	printf("%d\n", m);
	return 0;
}

3.4 命令行定义

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

3.5 条件编译

条件编译指令:在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃编译

比如:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。  

#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
#if 0
//1的时候就可以运行,不能是变量
		printf("%d\n", i);//此时,这个代码就不运行了
#endif
//这是预处理命令
		//其他
	}
	return 0;
}
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
// 常量表达式由预处理器求值。
2. 多个分支的条件编译
#if 常量表达式
      //...
#elif 常量表达式
      //...
#elif 常量表达式
      //...
#else
     //...
#endif
3. 判断是否被定义
(1)定义
#if defined(symbol)
      //...
#endif
例如:#define M 100;symbol就是指M
(2)定义
#ifdef symbol
      //...
#endif
(3)没有定义
#if !defined(symbol)
      //...
#endif
(4)没有定义
#ifndef symbol
      //...
#endif
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

3.6 文件包含

#include 指令可以使另外一个文件被编译。
预处理器先删除这条指令,并用包含文件的内容替换。 如果一个源文件被包含10 次,那就实际被编译 10 次。

3.6.1 头文件被包含的方式

(1)本地文件包含 #include "filename"   
  先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
(2)库文件包含 #include <filename.h>
  查找头文件直接去标准路径下去查找

库文件也可以用""的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

3.6.2 嵌套文件包含

如果文件被多次包含,这样就会造成文件内容的重复

解决办法:

(1)

#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
#endif   //__TEST_H__ 根据文件名改变

(2)

#pragma once

 以上两种办法可以避免头文件的重复引入

评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小刘同学啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值