C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)

作者前言

✨✨✨✨✨✨
​ 作者介绍:

作者id:老秦包你会,
简单介绍:
喜欢学习C语言和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨
个人主页::小小页面
gitee页面:秦大大

一个爱分享的小博主 欢迎小可爱们前来借鉴


程序环境和预处理

  • **作者前言**
  • 环境
  • 源码![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d80a5dc086fe48459f6a12aeb961ae24.png)
  • 编译环境
    • 编译
      • 预处理
      • 编译
      • 汇编
    • 链接
    • 可执行程序
  • 运行环境
  • 预处理详解
    • #define
    • #和##
      • \#
    • \##
    • 带有副作用的宏参数
    • 宏和函数的比较
  • 函数和宏的命名规则
    • undef
  • 命令行定义
  • 条件编译
    • #if .... #endif
    • 多分支语句
    • 判断是否被定义
  • 文件包含
  • 嵌套文件包含

环境

一个程序的运行要经历两个环境分别是
(1)翻译环境:在这个环境中的源码被转变成可执行的机器指令
(2)执行环境,用于实际执行代码

源码C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第1张图片

图中的就是源码,源码存放在.c后缀的文件里
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第2张图片
我们在运行这个文件的时候就会生成一个exe可执行文件,这个可执行文件是经过一系列的翻译得来的(翻译环境),exe可执行文件里面的内容是二进制指令
当我们运行这个exe可执行文件,就会显示打印的内容(执行环境)
下面我将围绕这个进行
所以我理解成一个图例

编译环境

编译环境分为编译和链接
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第3张图片
源文件通过编译(经过编译器cl.exe) 编译成目标文件obj ,然后目标文件通过链接器和链接库进行链接,成就了一个可执行程序

编译

当我们查看对应的文件
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第4张图片
会生成一个obj文件(二进制文件),这个就是day27_1.c生成的,经过cl.exe编译器处理
在这里插入图片描述
需要注意的是,在windos环境下生成的是obj文件,如果是在Linux里面生成 的就是.o文件
在这个编译的主要还要分成三部分
预处理(预编译)、编译和汇编

预处理

C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第5张图片
在vs2019里面找到这个界面,我们把这个预处理到文件更改为是,然后运行出来,过程会报错,这个正常,我们查看文件情况
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第6张图片
后缀.i的文件就是预处理完的文件,
,当我们查看一下里面的内容,到文件末尾找到
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第7张图片
那如果我们在Linux环境下运行gcc,看看

gcc -E test1.c -o test1.i #-E代表是生成预处理文件,-o是指定到哪个文件

在这里插入图片描述
我们查看一下
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第8张图片
发现里面的情况大致和在windows环境下的一样,
所以我们知道
预处理:
(1)把注释替换成立空格
(2)头文件的包含处理
(3)#define 的符号替换
我们知道 有#的符号的代码 可以认为是预处理指令,如 #include #define 这些都是在预处理的阶段进行的

编译

我们在Linux系统下操作

gcc -S test1.i -o test1.s

然后查看里面的内容
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第9张图片
就会发现这里是汇编代码
总结:
把C语言代码翻译成汇编代码,过程很复杂,要进行词法分析, 语法分析 和语句分析 和符号汇总(汇总的都是全局的

汇编

把汇编代码翻译成二进制指令生成了.o文件(目标文件),也生成了一个符号表(一个.c文件产生一个符号表)

gcc -c test1.s -o test1.o
或者
gcc -c test1.s 

当我们查看这个文件的时候
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第10张图片

链接

C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第11张图片
这个就是我们的链接器了
我们需要链接的就是.obj文件和链接库

链接库:会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

在Linux系统中gcc编译器生成的目标文件和二进制的可执行文件都是按照elf这种文件的形式组织的
链接过程是把所有的目标文件.o进行合并(合并段表),也会进行符号表的合并
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第12张图片
符号表的合并和重定向
一个文件写了add函数,另外一个文件引入这个函数,
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第13张图片
总结:
(1)合并段表
(2) 符号表的合并和重定向

可执行程序

gcc test1.o -o test

运行这个代码就会生成一个可执行程序
在这里插入图片描述
或者我们可以

gcc test1.c -o test1.out

可以直接生成可执行程序

运行环境

程序执行的过程:

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

预处理详解

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

在vs2019中__STDC__ 不支持

#include
#define M 100
int main()
{
	printf("%s\n", __FILE__);//查看当前的文件路径
	printf("%d\n", __LINE__);//查看这一行是第几行
	printf("%s\n", __DATE__);//查看当前日期
	printf("%s\n", __TIME__);//查看当前时间


	return 0;
}

在linux的gcc可以
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第14张图片

#define

(1)定义常量

#define M 100

(2)定义宏

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

#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

#define M(a,b) (a+b)

我们尽量在写宏的时候给替代的数值加个()
#define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

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

#和##

我们知道在字符串中有#define定义的符号是不会替换的

#

使用 # ,把一个宏参数变成对应的字符串

#define print(a, format) printf("the value of " #a " is " format "\n", a)
#include
#define PRINT(a, format) printf("the value of " #a " is " format "\n", a)
int main()
{
	int  b= 10;
	PRINT(b, "%d");
	return 0;
}

在这里插入图片描述
#会把宏参数变成一个字符串,不会进行转换

##

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

#include
//#define A 100
//#define M(a,b) (a+b)

#define ADD(num, value) int sum##num = value
#define print(num)  printf("%d ", sum##num)
int main()
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ADD(i, i);
		print(i);
	}
	return 0;
}

我们可以使用于创建一些文件名的地方,或者创建一些变量
在这里插入图片描述

带有副作用的宏参数

#include
//#define a 100
//#define M(a,b) (a+b)
#define ADD(a,b) (a >= b? a + b : 1)
int main()
{
	int a = 10;
	int b = 10;
	printf("%d\n", ADD(a++, b));
	printf("a = %d", a);

	return 0;
}

可以看到我们写了一段代码里面有宏,传入的值是a++,下面预处理的代码如下
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第15张图片
可以简单明了的看到a的值变化了两次
注意:我们传入参数要思考好,不然就会引起一些不必要的后果

宏和函数的比较

宏:
1.通常被应用于执行简单的运算
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第16张图片
宏仅仅只有运算,
如果使用函数,不仅要为函数创建栈帧,参数的传递,还有运算, 最后函数返回,这个就会很费时间
2.宏比函数在程序运算的规模和速度更胜一筹
3.函数的参数要声明类型,而宏不是

函数:
1.宏每调用一次就会替换一次,如果宏很长,即不好写,也不好直观
2.宏不能调试,
3.宏的类型无关,也会不严谨
4.宏可能会造成一些运算顺序问题,戴上()频繁

建议:逻辑简单,使用宏,逻辑复杂使用函数

函数和宏的命名规则

把宏名全部大写
函数名不要全部大写

undef

移除一个宏定义

#include
#define M 100
int main()
{
#undef M
	printf("%d", M);
	return 0;
}

M不存在了,这里就会报错.

命令行定义

在linux系统中

#include
int main()
{
	int arr[sz];
	int i = 0;
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d", arr[i]);
	}

	return 0;
}

在这里插入图片描述

条件编译

我们在写了一段代码发现,某些代码是不需要的,但是删除了可惜,因此我们在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

#if … #endif

#if 常量表达式
//...
#endif

相当于我们的if判断 ,没有else

#include
#define M 3
int main()
{
	int a = 1;
	scanf("%d", &a);
#if M==3
	printf("%d", a);
#endif
	printf("跳过");


	return 0;
}

如果M==3就执行printf(“%d”, a);,不是就不执行,

多分支语句

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
#include
//#define a 100
//#define M(a,b) (a+b)
#define M  10
int main()
{
#if M == 1
	printf("1 ");
#elif M == 2
	printf("2 ");
#else
	printf("3 ");
#endif
	printf("运行结束");

	return 0;
}

这个语句相当于我们的if的多分支语句

判断是否被定义

#include
//#define a 100
#define M 0
int main()
{
#if defined(M)
	printf("定义了");
#endif
	printf("哈哈哈");

	return 0;
}
#include
//#define a 100
#define M 0
int main()
{
#ifdef M
	printf("定义了");
#endif
	printf("哈哈哈");

	return 0;
}

#if defined(M) 等同于 #ifdef M

#include
//#define a 100
//#define M 0
int main()
{
#ifdef M
	printf("定义了");
#elif !defined(M)
	printf("没有定义");
#endif
	printf("哈哈哈");

	return 0;
}

#if !defined(M) 等同于 #ifndef M
defined() 函数用于检查某个标识符是否已经被 #define 定义

文件包含

我们知道#include可以引入头文件
有两种表示形式
(1)包含本地文件(自己写的文件,或者别人写的)
#include"xxx.h"
会先源文件的目录下查找头文件,如果找不到就会到标准位置找(库函数的目录)(标准库位置)
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)_第17张图片

(2)包含标准库的方式
#include
直接到标准库里面找,找不到就报错

Linux环境的标准头文件的路径:

/usr/include

在这里插入图片描述

嵌套文件包含

我们在引入头文件可能会引入多次相同的文件
为了防止重复引用

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif  //__TEST_H__

这段代码是经典的头文件保护机制,用于防止同一头文件被多次包含。当 TEST_H 没有被定义时,会定义它并包含头文件内容,否则直接跳过。
或者

#pragma once

你可能感兴趣的:(c语言,开发语言)