敞开成长之旅!这是我参与「日新方案 2 月更文挑战」的第 8 天,点击查看活动详情

一、程序的翻译环境和履行环境

ANSI C的任何一种完成中,存在两个不同的环境

第1种是翻译环境,在这个环境中源代码被转换为可履行的机器指令。 第2种是履行环境,它用于实践履行代码

咱们先来笼统地讲一下这两个环境,在第二模块再进行细讲

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 首要关于一个【test.c】的源文件来收,咱们要将代码履行的成果输出到屏幕上,就需求有一个可履行程序【.exe】
  • 在【.c】文件转变为【.exe】文件的这段进程叫做==翻译环境==,翻译环境分为编译和链接两部分,而关于编译来说,又能够进行细分为【预编译】、【编译】和【汇编】三个组成部分;当通过翻译环境之后,就会生成一个test.exe的可履行文件

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 此刻再到==运转环境==,通过将程序读入内存,调用仓库【stack】,存储函数的局部变量和回来地址,来计算出程序的运转成果,若是有打印句子就将成果打印在屏幕上

二、详解编译+链接

接下去咱们来详细说说翻译翻译环境,也便是【编译】+【链接】的部分

1、前言小知识

==上一模块说到过。每个源文件【.c】都会通过编译器处理生成方针文件。多个方针文件又会通过链接器的处理以及链接库链接生成可履行程序==

  • 可是必定有同学对这个链接库有所疑问,咱们来看一段代码

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到,关于这个咱们写C语言时常常运用的printf(),它就被称为是库函数,包括在stdio.h这个头文件中
  • 而关于库函数来说是存放在链接库里的。当程序里要运用来自外部的函数时,在链接时就应该把他们所依靠的链接库链接进来

  • 若是要讲到【编译】【链接】这两块内容,有一点知识你必定要知道,也便是咱们日常编写C/C++代码所运用的IDE——VS2019,它叫做【集成开发环境】,通常是包括了编辑编译链接调试这些功用,或许你会以为它便是一个软件,带有这些功用,这样理解其实是不行细致的
  • 关于编辑功用来说有【编辑器】
  • 关于编译功用来说有【编译器】,VS2019为cl.exe
  • 关于链接功用来说有【链接器】,VS2019为link.exe
  • 关于调试功用来说有【调试器】

==所以能够直接用【cl.exe】和【link.exe】进行编译和链接==

2、翻译环境【important】

接下去咱们正式来说说翻译环境中的【编译】和【链接】。在这一末节我,我将运用到Linux下的CentOS7这个操作系统进行解说会运用到Linux中的编辑器vim和编译器gcc

不在VS演示的原因是VS这个集成开发环境现已封装得足够完善了,需求通过一些调试窗口的调用才干观察到一些底层的细节,所以我打算在Linux的环境下进行解说。若是没有学习过Linux的同学能够来我的Linux专栏了解一下

2.1 编译

下面两个【.c】文件是咱们解说中需求运用到的

==add.c==

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}

==test.c==

#include "add.c"
int main(void)
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("c = %d\n", c);
	return 0;
}
  • 首要来Linux中看一下这两个文件

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 然后咱们能够通过vim来观看一下这两个文件中的内容

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 接下去能够通过gcc编译一下【test.c】这个源文件,就能生成【a.out】的可履行文件

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

① 预编译【进行宏替换】

  • 好,接下去咱们来说说预编译阶段,预编译也叫预处理。上面的【a.out】这个可履行文件是链接之后产生的文件,可是咱们不想让它这么快到链接阶段,到预编译阶段就能够停下来了,由于在运用gcc进行编译的时分要进行一个改变
  • 在gcc编译的时分,后边加上一个-E的选项,就能够使文件在编译的进程中预编译完就能够停下来了。后边的-o选项表明output输出的意思,也便是将预编译完毕后代码所呈现的内容放到【test.i】这个文件中去
gcc -E test.c -o test.i

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 然后咱们通过vim翻开看一下。可是进去之后你会看到一对很古怪的途径。此刻不必惧怕,在vim的【指令方法】下咱们能够直接按G,就能够直接跳到文件的结尾

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 然后就能够看到咱们了解的main函数了

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 此刻往上滑就能够看到这个预编译后的文件中有一堆的代码。其实这些代码都是头文件stdio.h中的内容,这儿的【test.i】仅仅将这个头文件翻开了罢了

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 咱们能够去usr/include/stdio.h这个文件中看看。从下方图中的确能够看到很了解的一些东西,假如你晚上滑就能够看到咱们在【test.i】中有看到过他们,所以就能够确定了这些的确便是stdio.h的翻开

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 可是预编译就仅仅将头文件翻开吗?当然不是,其实这个阶段还做了其他的事:stuck_out_tongue_winking_eye:
  • 现在我在原先【test.c】的文件中新增一些内容,加上一些注释和一个宏界说(后边讲)
  1 #include "add.c"
  2 
  3 //下面是一个宏界说
  4 #define MAX 100;
  5 
  6 int main(void)
  7 {                                                                                         
  8     int m  = MAX;
  9     int a = 10;                                        
 10     int b = 20;                                        
 11                                                        
 12     int c = Add(a,b);                                  
 13     printf("ret = %d\n",c + m);                              
 14                                                        
 15     return 0;                                          
 16 }   
  • 然后再对这个文件进行预编译然后翻开就能够看到咱们在编译之前加的注释就没有了,又能够观察到在main函数中的m = MAX就被替换成了m = 100,由于咱们在前面界说了MAX为100

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

所以咱们能够得出在预编译阶段编译器会履行的作业

  • 翻开头文件
  • 注释的删去
  • 宏界说的符号替换

② 编译【生成汇编】

  • 接下去咱们来看看编译阶段会做什么作业。已然gcc在编译的时分能够在【预编译】之后停下来,那也能够在编译之后停下来,只需在gcc后加一个-S即可。这儿咱们对上面预编译之后产生的【test.i】去进行一个编译
gcc -S test.i
  • 在编译之后就能够发现多出了一个【.s】为后缀的文件,咱们用vim翻开看看

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到,是一对咱们看不懂的东西,可是这相比二进制文原本其实仔细看是能够看出点猫腻,假如你学过《编译原理》这门课程的话其实就完全看得懂,由于许多都是一些基本指令和寄存器,所以就能够看出这是【汇编代码】

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 所以在程序进行==编译==的的时分就会进行如下四步操作。也便是将C语言的代码转换为汇编代码
  • 语法剖析
  • 词法剖析
  • 语义剖析
  • 符号汇总

上面这些东西你能够不必知道,这些都是在《汇编原理》中进行学习的,比较偏向底层

  • 不过关于【符号汇总】这一小块我能够在这儿讲一讲。咱们在看完汇编这一进程后再来看看:point_down:

③ 汇编【生成机器可辨认代码】

  • 程序在通过预编译编译之后,就来到了【汇编】阶段,在这个阶段中完毕后就会生成一个【test.o】的方针文件,关于这个方针文件来说咱们在VS中进行编译之后也是能够看得到的,它叫做【test.obj】

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

要怎么生成这个【test.o】呢?也是相同,修正gcc的编译方法即可。这次是加上-c选项哦:smile:

gcc -c test.s
  • 然后就生成了这个【test.o】的方针文件

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 咱们仍是相同去翻开看看.。可是能够看到,都是一堆乱码(其实这是二进制代码,看不懂很正常)

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 其实关于之前生成过的【a.out】咱们也能够进行一个浏览

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到,关于【a.out】来说呈现的也是一对二进制代码,那咱们是都能够对它们做一个联系呢?
  • 这个时分就能够讲讲我上面说到过的符号汇总了,其实关于【test.o】和【a.out】这两个文件来说都归于可履行文件,而关于可履行文件来说都是由二进制代码组成的,由于编译器对咱们上面编译时产生的汇编代码进行了一个符号汇总,那关于汇编代码来说咱们现已是有点心生忌惮了,那将它们进行汇总之后其实就变成了上面这个容貌╮(╯▽╰)╭
  • 可是你看不懂不必定代表计算机看不懂,不要忘了计算机能够辨认便是【二进制底代码】,所以在Linux中咱们有特定的一个软件能够查看它,叫做==readelf==,即阅览文件格局为elf的,所以能够看出这两个文件的格局其实为elf,这种文件格局会将会将文件中的内容分成一个个的段,这么一段一段的组成其实就变成了一张【表】的方法,这便是所谓的符号表,对符号进行汇总也就会构成一个表格的姿态
  • 那现在咱们就能够运用==readelf==来读取解析一下这个二进制文件了

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到,关于这个软件来说和gcc相同,也是需求带一些指令选项的,其实在Linux中绝大多数的指令都是有着许多的指令选项的,带上不同的指令选项就能够呈现出不同的作用,假如想了解的能够看看我的这篇文章——>Linux常见指令汇总
  • 接着仔细观察就能够发现里面有一个选项为-s,后边对这个选项的描绘是Display the symbol table 显现符号表,因而决断挑选它
readelf -s test.o

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

最终咱们就能够得出在汇编阶段编译器会完结的作业

  • 将汇编指令转换为二进制指令(需求特定的文本阅览器)
  • 构成符号表(没错,就这个功用)

2.2 链接【生成可履行文件或库文件】

终所以到链接链接阶段了,完毕了上面的编译阶段后,咱们再来看看链接阶段会做些什么

  • 在这一块,咱们要将上面的代码做一个修正,现在要在加一个【.h】的头文件,将【add】函数进行一个分割

==add.h==

#pragma once
#include <stdio.h>
//以下是一个宏界说
#define MAX 100
int Add(int x, int y);

==add.c==

int Add(int x, int y)
{
	return x + y;
}

==test.c==

#include "add.h"
int main(void)
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("c = %d\n", c);
	return 0;
}
  • 在上面咱们都是对一个源文件进行编译,由于在【test.c】中我包括了【add.c】的文件,可是现在适当所以Add()这个函数现已独立出去了,它有自己的专属【.h】头文件,因而咱们在运用gcc进行编译的时分要带上两个源文件,由于咱们在运用gcc进行编译的时分,需求告知它咱们的代码都写在哪里了
gcc add.c test.c
  • 若是写成下面这样,gcc便辨认不出Add()函数!!!
gcc test.c
  • 来看一下运转成果,产生的a.out。接着履行一下这个可履行文件,便是咱们最终的成果了

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】


  • 当然,除了对【add.c】和【test.c】进行编译外,咱们也能够对【add.o】和【test.o】这两个方针文件进行编译。相同对这两个文件进行上一模块的【编译】作业

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 通过如上的一步步设置操作,就能够看到咱们履行【a.out】文件和【my_out】文件的输出成果都是相同的,均为130,由于他们都是通过链接之后的可履行文件

  • 看完了这些,信任你必定也想知道在链接阶段gc编译器对两个方针文件做了什么

    C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 上面这些其实便是在进行一个兼并段表的操作,而且将两个方针文件中的符号表进行一个重定位的操作,假定【add.o】这个方针文件中的Add函数名的地址为0x100,【test.o】的方针文件中从【add.h】中获取到Add的函数名为0x000,然后还有main函数的函数名的地址为0x200

  • 由于两个方针文件中的有重复的函数名Add,所以会进行一个==符号表的重定位操作==,取那个有用的地址0x100,当一切段表都兼并完后便构成了一个【可履行文件】。

—— 这便是完整的编译 + 链接进程

3、运转环境

接着咱们来聊聊程序的运转环境,这一块的话由于内容过于复杂,有太多底层的细节,因而不在这儿解说

程序履行的进程:running:

  1. 程序有必要载入内存中。在有操作系统的环境中:一般这个由操作系统完结。在独立的环境中,程序 的载入有必要由手艺组织,也或许是通过可履行代码置入只读内存来完结。
  2. 程序的履行便开端。接着便调用main函数
  3. 开端履行程序代码。这个时分程序将运用一个运转时仓库(stack),存储函数的局部变量和回来 地址。程序同时也能够运用静态(static)内存,存储于静态内存中的变量在程序的整个履行进程 一向保存他们的值。能够看看我的这篇文章——> 函数栈帧的树立和毁掉全进程
  4. 终止程序。正常终止main函数;也有或许是意外终止

==说了这么多,咱们来梳理一下==

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

三、预处理详解

1、预界说符号

在C语言中,有一些预界说的符号,当咱们需求查询当时文件的相关信息时,就能够运用这个预界说符号

__FILE__      //进行编译的源文件
__LINE__     //文件当时的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时刻
__STDC__    //假如编译器遵从ANSI C,其值为1,否则未界说
  • 咱们能够到VS中来看看
int main(void)
{
	printf("%s\n", __FILE__);		//进行编译的源文件
	printf("%d\n", __LINE__);		//文件当时的行号
	printf("%s\n", __DATE__);		//被编译的日期
	printf("%s\n", __TIME__);		//被编译的时刻
	//printf("%s\n", __STDC__);		//因VS2019没有遵从ANSI C --> 报错
	system("pause");
	return 0;
}

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 别的的STDC咱们能够到Linux中来瞧瞧

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 这儿就能够看出关于Linux来说是遵从ANSI C,所以打印出来的值就为1

2、#define【⭐】

讲预处理,那#define肯定要将,这个信任咱们都用到过

2.1 #define界说标识符

  • 首要来说说#define怎么去界说标识符
语法:
 #define name stuff
  • 首要这个应该是咱们最了解的,那便是界说一个MAX,其值为1000
#define MAX 1000
  • 这个是为 register这个关键字,创立一个简略的姓名、
#define reg register
  • 下面这个或许你就没见过了,这儿是界说一个死循环。也便是咱们在写代码的时分,直接写do_forever;那就表明此为一个死循环
#define do_forever for(;;)
  • 下面这个可谓是咱们的福音,咱们在写switch句子时分,都要写case子句,可是老会忘了写break;,然后形成了一个case穿透的作用。所以下面这个标识符的界说就使咱们在写case子句的时分,主动就能够把break句子加上,此刻就便利了许多
#define CASE break;case
  • 当然,假如咱们要替换的内容过长,也是能够的,比如说写个printf句子时,若是一行写不下了,能够在每行后边都假一个反斜杠【\】
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,  \
                          __DATE__,__TIME__ )

留意:在#define界说标识符的时分,后边不要加;

  • 举个很简单的比如,若是下面的1000后边加上了;,那么在程序运用的时分就会呈现过错
#define MAX 1000;
#define MAX 1000
  • 比如咱们来看看下面的场景

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到,关于加仍是没有分号【;】的状况,仍是很显着的,加了分号就会报错
  • 为什么呢?由于关于分号【;】而言,表明一条句子的完毕,此刻在预编译完毕后,MAX就会被替换成了1000;那此刻再加上MAX后边的【;】,此刻就会呈现两个分号,那便是两条句子,可是在这个if句子中咱们仅仅将其作为一句话来履行,所以没有加大括号{},所以这才产生了报错
  • 咱们能够到Linux中来详细看看。能够看到,的确是被替换成了1000;;

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

2.2 #define界说宏

#define除了界说标识符之外,还能够界说【宏】,它和函数很相似,也便是将==参数替换到文本中==

下面是宏的声明方法:

#define name( parament-list ) stuff
//其中的 parament-list 是一个由逗号离隔的符号表,它们或许呈现在stuff中

注:① 参数列表的左括号有必要与name紧邻,假如两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

  • 然后咱们来看看详细的【宏】该怎么去界说
/*宏*/
#define SQUARE(x) x * x
/*函数*/
int GetSquare(int x)		
{
	return x * x;
}
int main(void)
{
	int x = 5;
	//int ret = GetSquare(x);
	int ret = SQUARE(x);
	printf("ret = %d\n", ret);
	return 0;
}
  • 能够看到,关于求解一个数的平方,咱们若是运用函数去完结的话便是将需求求解的数字作为参数传入进入,然后在做一个回来值承受即可;
  • 可是关于宏界说而言,咱们不是这么去做的,这么咱们不需求指定回来值,不过函数名称仍是需求的, 关于形参中变量也无需界说类型,直接SQUARE(x)即可,而后边你只需记住怎么去运算就能够了吗,也就适当于咱们的函数体。在【预处理】完毕之后,就会进行一个宏的替换

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到的确是进行了宏替换,最终算出来的成果和函数算出来的成果也是相同的,均为25

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 可是这么去写宏界说其实是不对的,由于会存在一个==问题==

若是我在传值的时分这么写呢int ret = SQUARE(5 + 1);此刻在进行宏替换的时分就会替换成这样int ret = 5 + 1 * 5 + 1;此刻中心的1 * 5就会先进行一个运算,然后再和两头去进行一个相加,最终算出来的成果便是【11】,而不是咱们想要的【36】

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 这其实是运算的优先级问题,所以咱们在界说宏时面临这样的优先级问题应该对需求运算的内容外面加上括号
#define SQUARE(x) (x) * (x)		//加上括号避免运算优先级
  • 咱们来看看加上括号后的成果。能够看到的确就变成了咱们想要的【36】

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 可是别高兴得太早,关于这种加括号的行为可不能一了百了,咱们再来看看下面这个宏界说
#define DOUBLE(x) (x) + (x)		//计算一个数的两倍
  • 此刻我这样去传参的时分就会呈现问题
int ret = 10 * DOUBLE((5));		//10 * (5 + 5) = 100

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到运转成果算出来并不是咱们想要的100,而是在进行了宏替换之后运算出来为55
  • 咱们通过查看一下【预编译】后的test.i文件来看看是怎么进行宏替换的

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 很显着能够看到,在进行【预编译】后进行了宏替换,可是前面的10却和5先进行了一个相乘,算出来50后加上一个5,此刻就能够知道为什么算出来的值为【55】了
#define DOUBLE(x) ((x) + (x))		//在外面再加一个括号即可

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

2.3 #define替换规矩

在上面说完了#define去界说【标识符】和【宏】,咱们来总结一下#define的界说规矩

在程序中扩展#define界说符号和宏时,需求触及几个进程

  1. 在调用宏时,首要对参数进行检查,看看是否包括任何由#define界说的符号。假如是,它们首要

被替换。

  1. 替换文本随后被刺进到程序中原来文本的方位。关于宏,参数名被他们的值所替换。

  2. 最终,再次对成果文件进行扫描,看看它是否包括任何由#define界说的符号。假如是,就重复上述处理进程

==留意:==

  1. 宏参数和#define 界说中能够呈现其他#define界说的符号。可是关于宏,不能呈现递归。

  2. 当预处理器查找#define界说的符号的时分,字符串常量的内容并不被查找。

2.4 # 和 双#

讲到#define,正好我再来弥补一点很古怪的小知识,也便是# 和## 这两个用来辅助字符串进行衔接的

【#】 :把参数刺进到字符串中

【##】 :把两个字符串拼接在一起


  • 首要咱们来看看最简单的字符串拼接操作。能够看到下面两种操作都能够打印出hello world

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 所以咱们就发现关于字符串而言是具有主动拼接的功用。接下去咱们进入正题
  8     int a = 10;
  9     printf("the value of a is %d\n", a);
 10 
 11     char b = 'x';                                                                                     
 12     printf("the value of b is %c\n", b);
 13 
 14     float c = 3.14f;
 15     printf("the value of c is %f\n", c);
  • 关于上面这段代码,咱们能够别离打印出下面的三条句子,关于a, b, c三个变量别离有不同的数据类型

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 现在咱们要将其转换为【宏】来进行操作,该怎么做呢?对该怎么进行传参呢?
#define PRINT(value, format) printf("the value is " format "\n", value);
PRINT(a, "%d");
  • 能够看到,通过字符串的拼接,咱们完成了格局化打印的作用

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 可是能够看到少了点什么?是的,中心的【of a】不见了,可是咱们通过宏传参的值是一个整型,无法和字符串进行一个拼接,那此刻咱们就要运用到【#】,将==参数刺进到字符串中==
#define PRINT(value, format) printf("the value of "#value" is " format "\n", value);

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看到,原先的数值a,的确变成了字符串的方法与两头的"the value of "" is "进行了一个拼接,达到了咱们需求的作用

  • 接下去咱们来说说【##】的用法,它能够直接将把两个字符串拼接在一起
#define CAT(A, B) A##B
int CentOS = 7;
printf("%d\n", CAT(Cent, OS));
  • 能够看到,咱们运用##将CentOS拼接在了一起,而且我界说了CentOS = 7,因而打印出来便是【7】

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

2.5 带副作用的宏参数

当宏参数在宏的界说中呈现超过一次的时分,假如参数带有副作用,那么你在运用这个宏的时分就或许

呈现风险,导致不行猜测的成果。副作用便是表达式求值的时分呈现的永久性作用

  • 上面咱们学习了怎么运用#define去界说宏,在学习的进程中你应该也能感遭到,尽管宏比函数来的便利许多,可是在运用的时分却有许多要留意的小细节,就像加括号的问题,若是忽略的话就会导致最终的成果呈现问题
  • 咱们之前在学习变量递增的时分有说到过a++++a的差异,一个是后置++,另一个则是前置++,而它们与a+1又有所不同:point_down:

x+1;//不带副作用

x++;//带有副作用

  • 咱们通过详细的事例来看看。能够观察到,关于a + 1履行完后,a自身的值不会产生改变;可是在++c履行完后,c的值却产生了改变。

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 不仅如此,关于char ch = getchar()来说,会从缓冲区读取一个字符给到ch,但此刻缓冲区也会少了一个字符,这就形成了问题;关于fgetc()这样的文件操作来说在获取文件中一个字符后,文件指针就会往后偏移一位,此刻文件指针的改变就会导致咱们下一次读取的时分方位就会进行概念

所以咱们再来说说这种代码关于宏的损害

  3 #define MAX(a, b) ((a) > (b) ? (a) : (b))
  4 int main(void)
  5 {
  6 
  7     int a = 3;
  8     int b = 4;
  9     int max = 0;
 10 
 11     max = MAX(++a, ++b);
 12     printf("max = %d, a = %d, b = %d\n", max, a, b);  
  • 你能够算出上面这段代码的履行成果是多少吗?【5 4 5】【5 5 4】【6 5 4】【6 4 6】到底是哪个呢?

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 咱们能够进入【预编译】阶段看看宏界说是怎么替换的

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 能够看出,自身就会形成成果改变的前置++,若是在放入宏中,就会形成更多不变的要素。所以咱们平常在运用宏的时分必定要小心翼翼

2.6 宏和函数的比照

在学习了【宏】之后,你必定会疑问咱们该何时去运用,又该何时去运用函数呢?咱们再来比照一下它与函数之间的差异

  • 能够看到,关于下面这段代码,咱们去求解一个数的最大值运用了【函数】和【宏】两种方法,能够看到关于宏来说要写许多的括号,函数看起来愈加明晰美观一些,那为什么还要去运用函数呢?而要去运用【宏】
  1 #include <stdio.h>
  2 
  3 #define MAX(a, b) ((a) > (b) ? (a) : (b))
  4 
  5 int Max(int x, int y)
  6 {
  7     return (x > y ? x :y);
  8 }
  9 
 10 int main(void)
 11 {
 12     int a = 10;
 13     int b = 20;
 14 
 15     int max1 = MAX(a, b);
 16     printf("宏求解的最大值为:%d\n", max1);
 17 
 18     int max2 = Max(a, b);                                                                             
 19     printf("函数求解的最大值为:%d\n", max2);
 20     return 0;
 21 }

原因主要有以下三点Ⅲ

  1. 宏比函数在程序的规划和速度方面更胜一筹

    函数需求调用、计算、然后再回来,宏只需求进行计算即可

  • 咱们能够通过【反汇编】来看看其实就很显着能够看出【宏】在计算的时分的确比函数要来的快多了

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  1. 更为重要的是函数的参数有必要声明为特定的类型宏则与类型无关的

    函数只能在类型适宜的表达式上运用。反之这个宏怎能够适用于整形、长整型、浮点型等能够用于>来比较的类型。

  • 能够看到当咱们要修正宏的参数时,写字符型也能够能够的,甚至是一个表达式;可是关于函数来讲,就现已定死了,若是要进行一个修正,那么需求调用的函数形参类型也有必要进行一个修正

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  1. 宏有时分能够做函数做不到的作业
  • 咱们在学习动态内存拓荒的时分知道了怎么运用malloc()去动态请求内存。
  • 在请求整型数组的时分就要运用sizeof(int);在运用请求字符型数组的时分就要运用sizeof(char);在运用请求浮点型数组的时分就要运用sizeof(float);每次都要重新去写一下,其实是降低了开发功率。所以就有同学想到运用函数去进行一个封装,这样就能够做到仅仅修正一下就好了,可是呢又想到了函数无法传类型,所以又束手无策了。可是呢,此刻咱们的【宏】就能够完成这一块逻辑

  • 这么看下来宏好像真的蛮好的,可是在日常的开发中,咱们为什么仍是会运用函数呢?由于宏也具有它的缺点╮(╯▽╰)╭

宏的缺点

  1. 每次运用宏的时分,一份宏界说的代码将刺进到程序中。除非宏比较短,否则或许大幅度添加程序的长度。【宏能够运用反斜杠换到下一行继续写,能够像函数相同写许多】

  2. 宏是没法调试的。【这点是致命的】

  3. 宏由于类型无关,也就不行严谨。 【任何类型都能够传入】

  4. 宏或许会带来运算符优先级的问题,导致程容易呈现错。【加括号太麻烦了!!!】


讲完了函数和宏之后,感觉有点散乱,咱们通过表格来比照一下

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 通过这张表格,信任你对函数必定有了自己的一个理解

2.7 命名规矩

接下去咱们来讲讲关于【宏】和【函数】的一些命名规矩。由于关于函数和宏尽管存在许多的差别,可是呢在全体上还来仍是比较相似,在开发的进程中也或许会存在混淆。所以咱们在对它们进行命名的时分应该做一个规则

  • 把宏名悉数大写
  • 函数名不要悉数大写

3、#undef

功用:移除一个宏界说

语法:

#define NAME
//... 代码 —— 能够运用NAME
#undef NAME
//... 代码 —— 无法运用NAME
  • 来看看详细比如

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

4、指令行界说

  • 许多C 的编译器供给了一种才能,允许在指令行中界说符号。用于发动编译进程。

  • 例如:当咱们根据同一个源文件要编译出一个程序的不同版本的时分,这个特性有点用途。(假定一个当地需求一个正常大小的数组,咱们给正常的即可,可是另一个当地却需求很大的空间,此刻就不行用了)

==咱们来详细的事例中看看==

    1 #include <stdio.h>
    2 
    3 int main(void)
    4 {
E>  5     int a[sz];
    6     int i = 0;
E>  7     for(i = 0; i < sz; ++i)
    8     {
    9         a[i] = i;
   10     }
   11                                                                                                     
E> 12     for(i = 0; i < sz; ++i)
   13     {
   14         printf("%d ", a[i]);
   15     }
   16     printf("\n");
   17 
   18     return 0;
   19 }
  • 关于上面这段代码,很显着的过错能够看到程序发现了我没有界说这个sz,学习了宏界说后信任你应该知道该怎么去作了。可是呢通过上面我讲到的情景,若是咱们界说一个数组的大小,在这个程序中现已声明好了,那或许放到一些数据量大的当地就跑不过了(不考虑动态拓荒)
  • 所以此刻就能够运用到【指令行界说】这个东西了,也便是这个sz我不在这儿界说,而是放在指令行进行编译的时分去界说,指令如下所示
gcc -D sz=10 源文件   //这儿留意不能写成sz = 10,不能加空格

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 然后咱们就能够在编译的时分为sz界说不同的值了,使程序变得很有弹性

5、条件编译【✔】

接下去咱们来聊聊条件编译,关于这一块尽管咱们平常不怎么用,可是在实践的开发顶用得仍是比较多的,因而需求有一些了解

  • 日常咱们在编写程序的时分,都会写一些调试类的代码去检查自己的代码是否正确,在检查完后当这份代码用不到时你就会觉得 ——> 删去可惜,保存又妨碍

所以就有了咱们现在所讲的条件编译,一起来看看

 #if 常量表达式
  //...
 #endif
 //常量表达式由预处理器求值

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

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

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

//判断是否被界说
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

#ifdef#ifndef也是同理。其实你用#define MAX也是相同的,也算作界说了宏,不必定要给他赋值

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

//嵌套指令
#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
  • 这儿由于嵌套的种类太多,因而展现两个

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

看完了上面这四种条件编译的方法,信任你对此应该有了必定的了解。其实咱们在库中的一些源码里,也能够看到他们的身影

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

6、文件包括

咱们现已知道, #include 指令能够使别的一个文件被编译。就像它实践呈现于 #include 指令的当地相同

这种替换的方法很简单:

  • 预处理器先删去这条指令,并用包括文件的内容替换。这样一个源文件被包括10次,那就实践被编译10次

6.1 头文件被包括的方法

==本地文件包括==

#include "filename"
  • 【查找策略】:先在源文件地点目录下查找,假如该头文件未找到,编译器就像查找库函数头文件相同在规范方位查找头文件。假如仍是找不到,就提示编译过错 —— 简单来说,会查找两次
  • 这种包括一般都是咱们自己写的头文件

Linux环境的规范头文件的途径:

/usr/include

VS环境的规范头文件的途径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默许途径

留意依照自己的装置途径去找


==库文件包括==

#include <filename.h>
  • 这种头文件的咱们用的应该是最多的,例如stdio.hstdlib.h等等这种规范库中的头文件

那这个时分必定就会有同学疑问说:已然第一种方法会查找两次,关于库文件也能够运用 “” 的方法包括? 答案是:能够,可是没必要,这样做查找的功率就低了一些,关于任何头文件都要去查找两次,而且也不容易差异是库文件仍是本地文件了

【总结一下】:

:dart:用 #include <filename.h> 格局来引证规范库的头文件(编译器将从规范库目录开端查找,只查找一次) :dart:用 #include “filename.h” 格局来引证非规范库的头文件(编译器将从用户的作业目录开端查找,会查找两次)

6.2 嵌套文件包括

接下来说说关于嵌套文件的包括

  • 在咱们进行开发的时分,那代码都是上万行的,很这许多的.h.c文件,所以有许多.h的头文件就或许会被咱们重复包括,就像是下面这种状况

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  1. comm.h和comm.c是公共模块。

  2. test1.h和test1.c运用了公共模块。

  3. test2.h和test2.c运用了公共模块。

  4. test.h和test.c运用了test1模块和test2模块。

这样最终程序中就会呈现两份comm.h的内容。这样就形成了文件内容的重复

  • 那咱们要怎么去解决这个问题呢? 答:运用上面所学的【条件编译】
// test13.c                                                       
  1 #include "add.h"
  2 #include "add.h"
  3 #include "add.h"
  4 #include "add.h"
  5 #include "add.h"
  6 
  7 #include <stdio.h>                                                   8 int main(void)
  9 {
 10     printf("haha\n");
 11     return 0;
 12 }
  • 在于上面的test13.c文件中能够看到我包括了五次add.h这个头文件。可是我在头文件顶用到了条件编译,只需这个【TEST_H】被#define界说过了之后,那这个头文件就不会再被包括了
 //add.h
  1#ifdef __TEST_H__
  2   #define __TEST_H_
  3   //////////////////                                                 4   int Add(int x, int y);
  5   //////////////////
  6 #endif
  • 咱们能够来看一下【预处理】后的成果

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

  • 或许咱们还有别的一种方法能够使头文件被重复包括
#pragma once	//用得较多

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

四、其他预处理指令

不做介绍,自己去了解一下即可。 ①error ②pragma ③line

五、总结与提炼

来总结一下本文所学习的知识

  • 首要咱们了解了要生成一个程序需求通过【翻译环境】和【履行环境】,要点解说了一下翻译环境的全体进程,分为预编译编译汇编链接四部分,在每个小模块中,咱们都做了深入的了解和剖析,知道了在每个环节会做什么,会产生什么,会为下一个模块准备设么
  • 然后便是进入咱们【预处理】的学习,首要说到预界说符号,接着便是#define的各种翻开,这一模块要要点把握,尤其是关于宏和函数的差异,还记得我列了一张表格嘛:smile:;接着咱们又说到了#unde和指令行界说,这两个做一个了解即可。然后便是开发进程中被大量运用的条件编译,这一模块也是要要点把握。最终又讲了讲头文件的包括方法、怎么防止头文件被重复包括

以上便是本文所要讲述的一切内容,感谢您的阅览,假如疑问请于谈论区留言或许私信我:four_leaf_clover:

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】