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

✨本篇我们讲:程序环境和预处理
❗ ❗ ❗ 超级干干干干货!!!


👩🏻‍🏫作者: 初入编程的菜鸟哒哒
📚系列文章目录:

一、TvT——C语言初阶の数据存储
二、TvT——C语言进阶の指针
三、TvT——C语言进阶の字符函数和字符串函数
四、TvT——C语言进阶の自定义类型:结构体,枚举,联合
五、TvT——C语言进阶の动态内存管理


📜刷题笔记:
1️⃣TvT——C语言进阶の刷题【1】:指针进阶


在这里插入图片描述

🥝1. 程序的翻译环境和执行环境

在ANSIC的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。(二进制指令)
第2种是执行环境,它用于实际执行代码

在这里插入图片描述

  • 通常来说运行环境就是操作系统提供的。
  • 编译环境就是编译器。

🥝2. 详解编译+链接

在这里插入图片描述

🥫2.1 翻译环境

在这里插入图片描述

❓这个图是怎么理解的呢

💌比如一个工程里面有很多个文件,比如test.c add.c sub.c
每个源文件都会通过编译器处理生成自己对应的目标文件(.obj 文件)
多个目标文件再加上链接库,经过我们链接器的处理
最终会生成一个可执行程序。

链接库就是库函数所依赖的库,或者第三方的库。
比如fread函数所依赖的库就是LIBC.LIBLIBCMT.LIBMSVCRT.LIB这三个库,链接的时候这些文件就会被链接到最后的可执行程序里面去。
在这里插入图片描述

🍻扩展知识:
❔那什么是编译器呢?
比如我用的VS2019
或者DEV C++ 、CodeBlocks、Clion
这些都叫做集成开发环境 - IDE
❔这些集成开发环境都集成了哪些功能呢?

  • 编辑 (敲代码用的那个编辑窗口)
  • 编译
  • 链接
  • 调试

在这里插入图片描述
以上是大致讲解了一下编译和链接。
在这里插入图片描述

❓那具体源文件是怎么变成可执行文件的呢?
接下来我们就详细的来讲一讲:

这里是引用
我们知道了大致的步骤是编译 - > 链接
但是编译里面又分别有几个步骤:

1. 预编译(预处理)
2. 编译
3. 汇编

链接里面就是链接
在这里插入图片描述
以下步骤我们用gcc来给大家演示

🎋2.1.1 编译

在这里插入图片描述
先演示 编译
1️⃣首先是预编译
预编译执行三个动作(进行了一些文本操作):

  1. 头文件的包含
    #include - 预处理指令
  2. define定义符号的替换
    #define - 预处理指令
  3. 注释的删除

💙1. 我们输入命令gcc test.c -E -o test.i :建立一个test.i的文件储存预处理之后的代码
add也同样操作
在这里插入图片描述
可以看到test.i里面有很多很多东西,很多行代码
❓为什么?
我们这里还可以看到#include<stdio.h>这行代码不见了,于是我们猜想上面的代码是stdio.h的代码
事实也的确如此
linux环境下,头文件放在 /usr/include 这个路径下
在这里插入图片描述
我们打开stdio.h看看里面的代码是不是test.i上面的那部分
在这里插入图片描述
✅答案是:一样的
所以我们验证了预处理会把头文件里面的代码包含进去。


💜2.我们给test.c代码加一条语句:#define MAX 100,并给z赋值等于MAX
在这里插入图片描述
然后我们预编译一下:
在这里插入图片描述
可以看到#define MAX 100这条语句已经没有了,并且z = MAX 也变成了z = 100
所以我们验证了第二点:在预处理阶段会完成#define的替换


💚3.我们再给test.c这个代码加上一行注释
在这里插入图片描述

然后我们预编译一下:
在这里插入图片描述
可以看到注释也没有了
所以我们验证了第三点:在预处理阶段会完成注释的删除
在这里插入图片描述
2️⃣第二步是编译
编译操作就是:
把C语言代码翻译成了汇编代码
这里面又包括几个步骤

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总 (💥较为重要)

我们输入命令gcc test.i -S,就会生成一个对应的test.s文件。
打开test.s看一下:
在这里插入图片描述
这里写的代码大家可能看不懂,但是我们可以了解一下,这里面放的代码是汇编代码
💥给大家重点说一下第四点:符号汇总
因为符号汇总在下一步汇编里是十分重要的

符号汇总顾名思义就是汇总符号的,代码中它会扫描里面的符号(不会汇总那些局部的符号,比如MAX(预处理的时候已经处理掉了),+- 这种也不会汇总)
比如扫描test.i里面有一个符号叫Add
这个时候就会汇总一个符号Add
往下走就继续汇总一个符号main
扫描add.i的时候就会汇总一个符号Add
在这里插入图片描述

有什么用呢?下一点汇编我们会讲到。
在这里插入图片描述
3️⃣第三步是汇编
编译操作就是:
汇编指令翻译成二进制指令
💥其中还有一个动作叫做 :形成符号表


我们输入命令gcc test.s -c,就会生成一个对应的test.o文件。
🍻扩展知识:
❔那什么是.o文件呢?
其实就是目标文件
在windows环境下目标文件的后缀是xxx.obj
在linux环境下目标文件的后缀就是xxx.o

了解了之后,打开test.o看一下:


汇编指令翻译成二进制指令

在这里插入图片描述
可以看到里面的代码变成了我们看不懂的一堆符号,这就是编译器把汇编指令翻译成了计算机可以理解的二进制指令,(💢目标文件是二进制的!)


🔆形成符号表
💌学习一个工具:
linux环境下 test .o 可执行程序的格式是elf
使用readelf工具可以看懂elf文件 (注:记得加上选项)
我们就看一下test.o的符号表
在这里插入图片描述
可以看见里面有Add,有main

再看一下add.o的符号表
在这里插入图片描述
❗哦!在test.oadd.o各自的符号表里面Add函数的值是不一样的!
这是因为在这一步两个文件还没有进行链接,test.o里面的Add其实是一个虚拟的值(无用的),因为此时还找不到真正的Add函数,而add.o里面的Add函数是真实存在的,所以有真实值

🎋2.1.2 链接

在这里插入图片描述
接下来我们演示 链接
❓那链接又到底干了什么事呢?

  1. 合并段表
  2. 符号表的合并和符号表的重定位

1️⃣合并段表: 这些.o文件按照.elf格式来执行的时候是把这些文件每个都分成了各种各样的段,最后这些段又合并在一起,简单理解合并段表就是这样的
在这里插入图片描述


2️⃣符号表的合并和符号表的重定位
就是把add的符号表和main的符号表合并在一起,并且有效地址覆盖无效地址
在这里插入图片描述
这就是为什么我们一个函数写到一个源文件里面,另外一个源文件也可以用。

最后借用比特鹏哥的图片,大家慢慢消化。
在这里插入图片描述
在这里插入图片描述

🥫2.2 运行环境

程序执行的过程

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

在这里插入图片描述

🥝3. 预处理详解

在这里插入图片描述

🥫3.1 预定义符号

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

这些预定义符号都是语言内置的。

🥜举个栗子:
在这里插入图片描述

在这里插入图片描述

🥫3.2 #define

在这里插入图片描述

🎋3.2.1 #define 定义标识符

语法: #define name stuff

🥜举个栗子:
在这里插入图片描述
提问:

define定义标识符的时候,要不要在最后加上 ; ?

比如:

#define MAX 1000;
#define MAX 1000

建议不要加上 ; ,这样容易导致问题。

比如下面的场景:

if(condition)
 max = MAX;
else
 max = 0;

🍳这里会出现语法错误。
在这里插入图片描述

🎋3.2.2 #define 定义标识宏

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

下面是宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中.

🥜举个栗子:
在这里插入图片描述
看,预处理后MAX(a,b)被替换了
在这里插入图片描述

注意:

  • 参数列表的左括号必须与name紧邻。
  • 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
  • 比如写成MAX (a,b)(a>b?a:b) 就会被认为MAX是一个符号,而后面的是MAX的内容,而不是一个宏

🥜再举个栗子:

#define SQUARE( x ) x * x

这个宏接收一个参数 x
如果在上述声明之后,你把SQUARE( 5 );置于程序中,预处理器就会用下面这个表达式替换上面的表达式:5 * 5

警告:

这个宏存在一个问题

观察下面的代码段:

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

乍一看,你可能觉得这段代码将打印36这个值。
事实上,它将打印11

❓ 为什么?

替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:printf ("%d\n",a + 1 * a + 1 );

这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值
在宏定义上加上几个括号,这个问题便轻松的解决了:

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

💥 提示:

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

在这里插入图片描述

🎋3.2.3 #define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

在这里插入图片描述

🎋3.2.4 #和##

如何把参数插入到字符串中?

首先我们看看这样的代码:
在这里插入图片描述
🤹🏻‍♀️可以看到这个地方的代码其实相似度是很高的,那有没有什么办法避免这种情况呢?

❔ 可不可以写一个函数实现这个功能呢?

写了一个这样的函数:

void print(int n)
{
	printf("the value of n is %d\n", n);
}

但我发现输出的时候怎么输出都是n is....而不是a is...或者b is...,我们这个函数没有完全实现我们想要的功能

于是我们可以用

#define PRINT(N) printf("the value of "#N" is %d\n", N)

❔ 怎么理解?
首先看这条语句:printf("the value of n" " is %d\n", N);,这里打印出的结果是正常的the value of n is N,所以可以知道printf语句中加入的两个双引号(实质是把一个字符串分成了两个字符串)是不影响输出结果的。
那我们来看一看#define PRINT(N) printf("the value of "#N" is %d\n", N)这条语句的实际结果:
在这里插入图片描述
宏PRINT实现了这个功能!!

💦Why?

原来 #的功能就是: 把一个宏参数变成对应的字符串 (把a变成了“a”,这样这段代码就变成了#define PRINT(N) printf("the value of ""a"" is %d\n", N),打印三段字符串而已)

在这里插入图片描述
##的作用

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

🥜举个栗子:
在这里插入图片描述
##的作用就是把class105合在一起变成class105,所以打印出来的值就是class105的值:105

💢注:

这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
在这里插入图片描述

🎋3.2.5 带副作用的宏参数

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

🥜举个栗子:

int main()
{
	int a = 2;
	int b = ++a;
	return 0;
}

这种写法就是有副作用的写法,因为得到ba的值发生了变化

🥜再举个栗子:

#define MAX(x, y)  ((x)>(y)?(x):(y))

int main()
{
	int a = 5;
	int b = 8;
	int c = MAX(a++, b++);

	printf("%d\n", c);
	printf("%d\n", a);
	printf("%d\n", b);


	return 0;
}

❔请问大家: 这里输出的结果是?

✅答案: 9 6 10

因为替换了之后是 c = ((a++)>(b++)?(a++):(b++))
先比较a 和b,这时a = 5b = 8,结果是b大,此时后置++生效,a = 6b = 9,因为结果是b大,所以执行后面的c = (b++),此时b = 9,所以c = 9,赋值结束之后后置++生效,b = 10

这里就可以看出副作用产生了多次影响,就会产生跟我们预期有所差异的代码。
在这里插入图片描述

🎋3.2.6 宏和函数对比

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

#define MAX(x, y)  ((x)>(y)?(x):(y))

💫那为什么不用函数来完成这个任务?

原因有二:

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

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

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

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

🥜举个栗子:

#define MALLOC(num ,type)  (type*)malloc(num*sizeof(type))

int main()
{
	int*p = (int*)malloc(10 * sizeof(int));
	int*p2 = MALLOC(10, int);
	return 0;
}

宏和函数的一个对比

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

❓那有没有一个办法可以把宏的优点和函数的优点结合在一起呢?

在C99标准下,有一个inline内敛函数 — 结合了函数的优点和宏的优点。


命名约定

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

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


在这里插入图片描述

🥫3.3 #undef

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

#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

在这里插入图片描述

🥫3.4 命令行定义

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

🥜举个栗子(linux环境下):

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

这里我们没有定义ARRAY_SIZE
我们编译的时候输入编译指令

gcc -D ARRAY_SIZE=10 programe.c

这条指令就是编译的时候定义ARRAY_SIZE=10,这个代码依旧可以执行,并且结果是正确的(打印 0 - ARRAY_SIZE-1

在这里插入图片描述

🥫3.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
🎈比如说:

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

🥜举个栗子:
想执行这段printf("%d ", arr[i]);代码的时候使#if后面的值为
在这里插入图片描述
不想执行这段printf("%d ", arr[i]);代码的时候使#if后面的值为
在这里插入图片描述
常见的条件编译指令:

1️⃣基本的条件编译
#if 常量表达式
//…
#endif
//常量表达式由预处理器求值。
如:
#define __ DEBUG__ 1
#if __ DEBUG__
//…
#endif

2️⃣ 多个分支的条件编译
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif

🥜举个栗子:
在这里插入图片描述

3️⃣ 判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

🥜举个栗子:
定义了MAX在这里插入图片描述
未定义MAX在这里插入图片描述
#ifdef#if !defined功能一样在这里插入图片描述
#ifndef#if defined功能一样在这里插入图片描述
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 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
在这里插入图片描述

🎋3.6.1 头文件被包含的方式:

  • 本地文件包含
#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误。

🎀先在源文件所在目录下查找的意思是:

比如这个通讯录项目,我就在这个test.ccommunication.c所在的目录下寻找communication.h如果找不到再去库函数头文件位置查找
在这里插入图片描述

Linux环境的标准头文件的路径
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

注意按照自己的安装路径去找。

  • 库文件包含
#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
这样是不是可以说,对于库文件也可以使用 “ ” 的形式包含?
答案是肯定的,可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

在这里插入图片描述

🎋3.6.2 嵌套文件包含

如果出现这样的场景:
在这里插入图片描述
comm.hcomm.c是公共模块。
test1.htest1.c想使用公共模块的头文件。
test2.htest2.c也想使用公共模块的头文件。
最后test.htest.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

如何解决这个问题?

✅答案:条件编译。

每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
int Add(int x,int y);//头文件的内容
#endif  

💦解释一下这个代码:
如果没有定义__TEST_H__,就定义一下__TEST_H__,并且执行下面的语句(int Add(int x,int y);),如果已经定义过了__TEST_H__,下面的语句就不会执行。

或者每个头文件的开头写:

#pragma once

就可以避免头文件的重复引入。
在这里插入图片描述

🥝总结

这里给大家推荐《高质量C/C++编程指南》中附录的考试试卷(很重要)
笔试题:

  1. 头文件中的 ifndef/define/endif是干什么用的?
  2. #include <filename.h>#include "filename.h"有什么区别?

在这里插入图片描述

“莫愁千里路,自有到来风。”
我们顶峰相见!
  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值