栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C语言简易理解程序运行前经过的流程+预处理指令

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C语言简易理解程序运行前经过的流程+预处理指令

文章目录
  • 编译和运行
    • 预处理
    • 编译
    • 汇编
    • 链接
    • 运行阶段
  • 预定义符号
  • #define的使用
    • #define定义标识符
    • #define不要加上分号
    • #define实现宏
  • #和##的使用
  • 宏和函数的对比
  • 命令行定义
    • #undef
  • 条件编译
    • #if和#endif
    • #ifdef和#ifndef(no define)
    • #elif
  • #include中""和<>的区别
  • 如何避免重复包含头文件

编译和运行

我们都知道计算机是只认识二进制的,当我们写下这种文本代码时,计算机是怎么将这些文本转换成二进制让机器看的懂的。

经过两个大阶段:编译和运行
编译又分为了四个小阶段,分别为:预处理(预编译),编译,汇编,链接

这些过程在linux上可以清晰的看到会发生什么。
大概用文字描述一下:

预处理

在预处理阶段程序会做三件事情

  1. 把程序中的注释全部删除
  2. 头文件展开,比如stdio.h等等
  3. 把#define进行替换
编译

编译阶段干的最明显的事情就是把高级语言代码转换成汇编代码

包括:语法分析,词法分析,语义分析,符号汇总等等。这都是编译原理里面的内容。

这里简单说一下符号汇总,汇总的都是全局的符号,比如全局变量,函数等等。汇总的原因是为了在汇编阶段形成符号表(符号表是记录全局符号的地址的表格,相当于地图吧)

汇编

生成了目标文件
我们经常看见的.obj后缀的文件就是在这个阶段生成的
大致做两件事情

  1. 把汇编代码转换成机器语言(二进制)
  2. 形成符号表

下面这些就是符号表,就是记录符号的地址

链接

把所有目标文件全部通过链接器变成一个可执行程序(后缀是.exe)。如果目标文件里面含有库里面使用的函数,或者是程序员自己写的函数,也会在链接库里面链接起来。

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

一句话总结:函数在链接阶段被链接器把它与目标文件结合。

另外要做的两件事情:

  1. 合并段表
  2. 合并符号表
运行阶段
  1. 程序的执行便开始。接着便调用main函数。
  2. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同
    时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  3. 终止程序。正常终止main函数;也有可能是意外终止
预定义符号
__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
int main()
{
	printf("%s", __FILE__);
	return 0;
}

比如__FILE__就可以代表代码的文件名

#define的使用 #define定义标识符

常见用法是这样的:

#define MAX 1000

其实它还可以替换名字,例如:

#define INT int
#define do_forever for(;;)

只要替换后程序没有错误,就可以替换。

#define不要加上分号

如果加上了分号,可能会造成程序报错。
例如下面的例子:

#define MAX 1000;
printf("%d",MAX);
#define实现宏

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

例如:

#define MAX(X,Y) ((X)>(Y)?(X):(Y))

注意:宏的实现不要吝啬括号的使用,因为会很容易造成错误。具体例子就不举了。

#和##的使用

这两个符号很少见,也很有趣。

通过一个问题来引出#的使用方法
如何把参数插入到字符串中?

我们怎么让parametre是可变的呢?在第一次printf时代表a,在第二次printf时代表b。

int main()
{
	int a = 10;
	int b = 20;
	printf("the value of the parametre is %d", a);
	printf("the value of the parametre is %d", b);
	return 0;
}

这时候我们就要在宏里面使用#来解决这个问题。

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

这么写就可以了。#x把宏参数x变成了字符串x。

#define PRINT(x) printf("the value of the "#x" is %dn",x);

注:字符串是可以自动拼接的。
如:

printf("hello ""world");

这行代码会打印出hello world。

我们还可以用这个符号让宏更加美观
这样写会更加清晰。

#define PRINT(x) 
	printf("the value of the "#x" is %dn",x);

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

这个符号可以是任何类型的
比如下面这个例子:

#define CAT(a,b) a##b
int helloworld = 2021;
printf("%d",CAT(hello, world));

hello和world会被拼接成helloworld,这个变量刚好是等于2021.

甚至这样都可以

#define CAT(a,b) a##b
printf("%d", CAT(1,2));
//会打印12
宏和函数的对比

这个东西不用背,理解理解就好了。

  1. 宏: 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。函数:函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
  2. 宏:执行速度更快,因为它在预处理时就替换好了,不需要额外的开销。函数:存在函数的调用和返回的额外开销,所以相对慢一些
  3. 宏:宏要考虑优先级优先,要加很多括号。函数:函数不需要额外考虑优先级问题,结果容易预测。
  4. 宏:宏的参数是任意的,甚至可以传入类型。函数:函数的参数是固定的
  5. 宏:宏不可以调试。因为调试已经在程序运行阶段了,这时候宏早就消失了。函数:函数可以调试
  6. 宏:宏不可以递归。函数:函数可以递归。
命令行定义 #undef

#undef可以消除一个宏定义
这段代码会报错,因为PRINT这个宏已经被消除了。

#define PRINT(x) 
	printf("the value of the "#x" is %dn",x);
int main()
{
#undef PRINT(x)
	PRINT(a);
	PRINT(b);
}
条件编译 #if和#endif

这个和if语句使用方法一样。

如果满足条件就进入,不满足就不进入。
下面这段代码就是不会编译第一个main函数,因此程序不会报错。

#if 0
int main()
{
	return 0;
}
#endif

int main()
{
	return 0;
}

注:#if后面接的表达式一定要是常量表达式,不可以是变量

#ifdef和#ifndef(no define)

#ifdef代表的意思是,如果这个被定义了,就进入编译
#ifndef代表的意思是,如果这个没有被定义,就进入编译

定义了这个符号,因此就进入了main函数

#define __DEBUG__ 1
#ifdef __DEBUG__
int main()
{
	return 0;
}
#endif

没有定义这个符号,因此也进入main函数(这里是#ifndef)

#ifndef __DEBUG__
int main()
{
	return 0;
}
#endif
#elif

如下面这段代码,只会打印一个1

int main()
{
#if 1
	printf("1");
#elif 0
	printf("1");
#endif
}
#include中""和<>的区别

很简单,直接上结论。

双引号:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
尖括号:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用 “” 的形式包含? 答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

如何避免重复包含头文件

有两种方法:
第一种,这种比较简单

#pragma once

第二种

#ifdef __TEST.H__
#define __TEST.H__
#endif

原理:第一次包含头文件的时候,__TEST.H__没有被定义,因此进入编译,包含头文件。第二次包含头文件的时候,__TEST.H__已经被定义过了,这时候就不进入编译。因此不会重复包含头文件。

注:由于头文件在预编译阶段已经全部展开了,因此#define也会被展开几份,因此这么写是没有问题的。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/302686.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号