C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
作者前言
🎂 ??????🍧🍧🍧🍧🍧🍧🍧🎂
?🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂
程序环境和预处理
环境
一个程序的运行要经历两个环境分别是
(1)翻译环境:在这个环境中的源码被转变成可执行的机器指令
(2)执行环境,用于实际执行代码
源码
图中的就是源码,源码存放在.c后缀的文件里
我们在运行这个文件的时候就会生成一个exe可执行文件,这个可执行文件是经过一系列的翻译得来的(翻译环境),exe可执行文件里面的内容是二进制指令
当我们运行这个exe可执行文件,就会显示打印的内容(执行环境)
下面我将围绕这个进行
所以我理解成一个图例
编译环境
编译环境分为编译和链接
源文件通过编译(经过编译器cl.exe) 编译成目标文件obj ,然后目标文件通过链接器和链接库进行链接,成就了一个可执行程序
编译
当我们查看对应的文件
会生成一个obj文件(二进制文件),这个就是day27_1.c生成的,经过cl.exe编译器处理
需要注意的是,在windos环境下生成的是obj文件,如果是在Linux里面生成 的就是.o文件
在这个编译的主要还要分成三部分
预处理(预编译)、编译和汇编
预处理
在vs2019里面找到这个界面,我们把这个预处理到文件更改为是,然后运行出来,过程会报错,这个正常,我们查看文件情况
后缀.i的文件就是预处理完的文件,
,当我们查看一下里面的内容,到文件末尾找到
那如果我们在Linux环境下运行gcc,看看
gcc -E test1.c -o test1.i #-E代表是生成预处理文件,-o是指定到哪个文件
我们查看一下
发现里面的情况大致和在windows环境下的一样,
所以我们知道
预处理:
(1)把注释替换成立空格
(2)头文件的包含处理
(3)#define 的符号替换
我们知道 有#的符号的代码 可以认为是预处理指令,如 #include #define 这些都是在预处理的阶段进行的
编译
我们在Linux系统下操作
gcc -S test1.i -o test1.s
然后查看里面的内容
就会发现这里是汇编代码
总结:
把C语言代码翻译成汇编代码,过程很复杂,要进行词法分析, 语法分析 和语句分析 和符号汇总(汇总的都是全局的)
汇编
把汇编代码翻译成二进制指令生成了.o文件(目标文件),也生成了一个符号表(一个.c文件产生一个符号表)
gcc -c test1.s -o test1.o
或者
gcc -c test1.s
当我们查看这个文件的时候
链接
这个就是我们的链接器了
我们需要链接的就是.obj文件和链接库
链接库:会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
在Linux系统中gcc编译器生成的目标文件和二进制的可执行文件都是按照elf这种文件的形式组织的
链接过程是把所有的目标文件.o进行合并(合并段表),也会进行符号表的合并
符号表的合并和重定向
一个文件写了add函数,另外一个文件引入这个函数,
总结:
(1)合并段表
(2) 符号表的合并和重定向
可执行程序
gcc test1.o -o test
运行这个代码就会生成一个可执行程序
或者我们可以
gcc test1.c -o test1.out
可以直接生成可执行程序
运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。 - 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。 - 终止程序。正常终止main函数;也有可能是意外终止。
预处理详解
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
在vs2019中__STDC__ 不支持
#include<stdio.h>
#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可以
#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定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
#define a 100
#define M(a,b) (a+b)
int main()
{
printf("%d", M(a, 2));
return 0;
}
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意: - 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#和##
我们知道在字符串中有#define定义的符号是不会替换的
#
使用 # ,把一个宏参数变成对应的字符串
#define print(a, format) printf("the value of " #a " is " format "\n", a)
#include<stdio.h>
#define PRINT(a, format) printf("the value of " #a " is " format "\n", a)
int main()
{
int b= 10;
PRINT(b, "%d");
return 0;
}
#会把宏参数变成一个字符串,不会进行转换
##
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
#include<stdio.h>
//#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<stdio.h>
//#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++,下面预处理的代码如下
可以简单明了的看到a的值变化了两次
注意:我们传入参数要思考好,不然就会引起一些不必要的后果
宏和函数的比较
宏:
1.通常被应用于执行简单的运算
宏仅仅只有运算,
如果使用函数,不仅要为函数创建栈帧,参数的传递,还有运算, 最后函数返回,这个就会很费时间
2.宏比函数在程序运算的规模和速度更胜一筹
3.函数的参数要声明类型,而宏不是
函数:
1.宏每调用一次就会替换一次,如果宏很长,即不好写,也不好直观
2.宏不能调试,
3.宏的类型无关,也会不严谨
4.宏可能会造成一些运算顺序问题,戴上()频繁
建议:逻辑简单,使用宏,逻辑复杂使用函数
函数和宏的命名规则
把宏名全部大写
函数名不要全部大写
undef
移除一个宏定义
#include<stdio.h>
#define M 100
int main()
{
#undef M
printf("%d", M);
return 0;
}
M不存在了,这里就会报错.
命令行定义
在linux系统中
#include<stdio.h>
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<stdio.h>
#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<stdio.h>
//#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<stdio.h>
//#define a 100
#define M 0
int main()
{
#if defined(M)
printf("定义了");
#endif
printf("哈哈哈");
return 0;
}
#include<stdio.h>
//#define a 100
#define M 0
int main()
{
#ifdef M
printf("定义了");
#endif
printf("哈哈哈");
return 0;
}
#if defined(M) 等同于 #ifdef M
#include<stdio.h>
//#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"
会先源文件的目录下查找头文件,如果找不到就会到标准位置找(库函数的目录)(标准库位置)
(2)包含标准库的方式
#include<xxx.h>
直接到标准库里面找,找不到就报错
Linux环境的标准头文件的路径:
/usr/include
嵌套文件包含
我们在引入头文件可能会引入多次相同的文件
为了防止重复引用
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
这段代码是经典的头文件保护机制,用于防止同一头文件被多次包含。当 TEST_H 没有被定义时,会定义它并包含头文件内容,否则直接跳过。
或者
#pragma once
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!