C语言拯救者(程序的预处理、编译、链接与宏- -15)

注:由于VS2019是集成开发环境,不方便观察细节。我们使用Linxu gcc来演示编译和链接

目录

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

2. 编译和链接

2. 编译的几个阶段

2.1 预编译(预处理):

预处理阶段首先会进行头文件的包含

预处理阶段会把预处理指令转换,例如#include、define(完成替换)是一个预处理指令,同时删除注释

 2.2 编译

编译期间把C语言代码翻译成了汇编代码

 2.3 汇编

 2.4 链接

在链接期间,把跨多个文件的符号整合,这也是为什么在一个源文件中写的符号再另一个源文件中也可以使用。

2.5 运行环境

2.6  头文件和库文件的区别

3. 预处理详解

3.1 预定义符号

3.2 #define定义标识符

3.3 #define定义宏

3.4 写一个宏,求出x,y之间的较大值

3.5 写一个宏,求平方

3.6 #define 替换规则

 3.7 #和##操作符

4.1 带副作用的宏参数

4.2 宏和函数对比

4.3 #undef

4.4 命令行定义 

 4.5 条件编译

4.6 文件包含

4.7 嵌套文件包含


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

在C语言中,存在两个环境:

1.翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)。

2.执行环境,它用于实际执行代码。

2. 编译和链接

        

编译器

编译器,是一个根据源代码生成机器码的程序。        

厂商

C

C++

GNU

gcc

g++

LLVM

clang

clang++

2. 编译的几个阶段

在编译期间还分为了三个阶段,分别是预编译-->编译-->汇编

2.1 预编译(预处理):

我们首先在gcc底下创建一个test.c源文件和一个Add.c源文件

#include <stdio.h>

extern int Add(int, int);//申明外部符号

int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d\n", c);//30

	return 0;
}
#define _CRT_SECURE_NO_WARNINGS

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

通过指令:gcc test.c -E -o test.i

-E让test.c源文件程序在预编译阶段停下来,-o把程序运行结果放进test.i(预处理后生成的文件)文件中。

在test.i中你会发现屏幕上多出800多行代码,extern和main函数都和源文件一样,唯独#include <stdio.h>变成了800行代码

当我们打开头文件时,会发现和预编译期间多出来的代码相同

把头文件的相关内容包含到test.i中

预处理阶段首先会进行头文件的包含

测试预处理阶段还会做什么

#define Max 100
//定义Max值为100
#include <stdio.h>

extern int Add(int, int);//申明外部符号

int main()
{
    int z = Max;
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d\n", c);//30

	return 0;
}

 重复操作,打开test.i后发现注释和#define不见了,而且z直接被赋值成100

预处理阶段会把预处理指令转换,例如#include、define(完成替换)是一个预处理指令,同时删除注释

 


 2.2 编译

让程序在编译期间停下来指令:gcc test.i -S后生成test.s编译文件,同样对Add.i文件处理

打开test.s文件,会发现这其实是汇编代码

编译期间把C语言代码翻译成了汇编代码

在其中进行了1.语法分析  2.词法分析  3.语义分析4.符号汇总等操作  --《编译原理》 

         

符号汇总只会整理出全局符号,在main函数中整理出Add,main,在Add.c中会中整理出Add

 2.3 汇编

gcc test.s -c   --> 让程序在汇编期间停下生成了test.o文件  gcc Add.s -c -->同理

注意:windos环境下目标文件后缀为XXX.obj ,在Linxu环境下目标文件为XXX.o

目标文件是二进制

汇编这个过程把汇编指令转换成二进制指令

当我们通过一定方法去解读二进制指令后,打开符号表

在test.i中汇总了Add,main符号,在Add.i中汇总了Add符号

 符号汇总后,形成符号表,符号表同时记录了地址,如果我们屏蔽Add函数,在main函数中找不到有效的Add函数地址,便会在符号表里放入无意义的值,无法找到有效地址,main函数则获得了有效地址

 2.4 链接

链接需要做的:

1.合并段表(.o程序和.exe可执行程序都是同一种类型格式文件,需要合并相同段上的内容)

2.符号表的合并和重定位(Add符号出现了两次,其中main中的Add地址是无意义的,Add函数中的Add是有地址的,既然符号名相同,我们便合并成为一个符号,此时Add地址有意义,当我们需要找到对应函数通过地址寻找即可。)

同理,如果该符号地址无意义,无法通过地址寻找到有效的代码,程序便会在链接期间报错

如果我们在VS屏蔽了Add函数,可以发现程序在.obj链接阶段报错,或者写错了名字导致了无法把符号整理在一起,该符号还是无意义

在链接期间,把跨多个文件的符号整合,这也是为什么在一个源文件中写的符号再另一个源文件中也可以使用。


2.5 运行环境

程序执行的过程:

1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

2. 程序的执行便开始。接着便调用main函数。

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。

4. 终止程序。正常终止main函数;也有可能是意外终止。


2.6  头文件和库文件的区别

头文件:

在编程过程中,程序代码往往被拆成很多部分,每部分放在一个独立的源文件中,而不是将所有的代码放在一个源文件中运行时,编译器不知道代码用法是否正确,只有借助头文件中的函数声明来判断。

库文件:

有时候我们会有多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些共用的功能做成一个库,方便大家一起共享(规范化代码)。库中的函数可以被可执行文件调用,也可以被其他库文件调用。库文件又分为静态库文件和动态库文件。

        


3. 预处理详解

3.1 预定义符号

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

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

想获得当前程序执行的信息,便可以使用预定义符号 将信息计入到文件中,方便以后查看

int main()
{
	int i = 0;
	FILE* pf = fopen("log.txt", "a");
	if (pf == NULL)
	{
		return 1;
	}
	for (i = 0; i < 10; i++)
	{
		fprintf(pf, "name:%s file:%s line:%d date:%s time:%s i=%d\n", __func__, __FILE__, __LINE__, __DATE__, __TIME__, i);
	}

	fclose(pf);
	pf = NULL;

	return 0;
}


3.2 #define定义标识符

语法:
 #define name stuff
#define NUM 100
#define STR "abcdef"


int main()
{
	int num = 0;
	char* str = STR;
	
    预处理后直接把define的内容替换为:
    int num = 100;
    char* str = "abcdef";

	return 0;
}

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

由于#define是直接把内容完成替换,所以我们不需要在后面写; 否则替换过去便有可能出现问题

还可以把函数用define替换,省去了重复写函数的繁琐,但不便于调试(预处理阶段才替换我们无法调试)

// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
       date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ ) 


3.3 #define定义宏

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

#define name( parament-list ) stuff//宏的申明方式,阔号内部是参数列表

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

注意:

1.参数列表的左括号必须与name紧邻。

2.如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

3.4 写一个宏,求出x,y之间的较大值

错误写法: 答案虽然是对的,但是这种写法有风险,因为参数的值被传进去后,优先级无法保证

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

int main()
{
	int a = 10;
	int b = 20;

	int c = MAX(a, b);
	//int c = (a > b ? a : b);

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

	return 0;
}

正确写法:由于宏是直接替换,操作符的优先级可能会影响结果,为了避免代码出错,我们应该带上括号        

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

int main()
{
	int a = 10;
	int b = 20;

	int c = MAX(a, b);
	//int c = (a > b ? a : b);

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

	return 0;
}

3.5 写一个宏,求平方

错误的写法:发现答案错误

#define SQUARE(x) x*x

int main()
{
	int a = 9;
    //int r = SQUARE(a);没问题
	int r = SQUARE(a + 1);//有问题
	printf("%d", r);
	return 0;
}

为什么这个代码错误,我们替换过来后

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

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

int main()
{
	int a = 9;
	int r = SQUARE(a + 1);
	printf("%d", r);
	return 0;
}


3.6 #define 替换规则

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

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#define M 100

int main()
{
	printf("M is %d\n", M);

	return 0;
}


 3.7 #和##操作符

如果想封装一个函数,传递过来a,打印the value of a is a,传递过来b,打印the value of b is b

下面这个函数无法做到

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

 .那么如何把参数插入到字符串中?下面三个printf输出的是不是New World?

int main()
{
	char* p = "New ""World\n";

	printf("New World\n");
	printf("New ""World\n");
	printf("%s", p);
	return 0;
}

我们发现字符串是有自动连接的特点,于是我们可以修改宏,其中#作用是不将符号做任何替换,直接转换成对应字符串

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

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

 当然我们也可以再举一个例子

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

int main()
{
	int a = 20;
	double pai = 3.14;//类型不匹配

	PRINT(a, "%d");//手动传输类型
	PRINT(pai, "%lf");

	return 0;
}

## 的作用

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

#define CAT(name, num) name##num

int main()
{
	int cpp10 = 10;
	printf("%d\n", CAT(cpp, 10));//两个字符合成cpp10,最后打印结果是10

	return 0;
}


4.1 带副作用的宏参数

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

x+1;//不带副作用
x++;//带有副作用

下列代码中a,b,c分别是多少        

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

int main()
{
	int a = 5;
	int b = 8;
	int c = MAX(a++, b++);
	//int c = ((a++) > (b++) ? (a++) : (b++));
	printf("%d\n", c);//
	printf("%d\n", a);//
	printf("%d\n", b);//
	return 0;
}

结果可能跟我们想要的不同,参数副作用多次影响了后续求值


4.2 宏和函数对比

属 性 #define定义宏 函数
代 码 长 度 每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长

函数代码只出现于一个地方;每次使用这个

函数时,都调用那个 地方的同一份代码

执 行 速 度 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。

存在函数的调用和返回的额外开销,所以

相对慢一些

操 作 符 优 先 级 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。

函数参数只在函数调用的时候求值一次,它

的结果值传递给函 数。表达式的求值结果

更容易预测。

带 有 副 作 用 的 参 数 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果。

函数参数只在传参的时候求值一 次,

结果更容易控制。

参 数 类 型 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。宏由于类型无关,也就不够严谨。

函数的参数是与类型有关的,如 果参数的类

型不同,就需要不同的函数,即使他们执行

的任务是不同的。函数的参数必须声明为特

定的类型。 所以函数只能在类型合适的表达

式上使用。

调 试 宏是不方便调试的 函数是可以逐语句调试的
递 归 宏是不能递归的 函数是可以递归的

宏的参数可以出现类型,但是函数做不到。 

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

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

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

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

    


4.3 #undef

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

#define MALLOC(num ,type)  (type*)malloc(num*sizeof(type))
#include <stdlib.h>
int main()
{
	int*p = (int*)malloc(10 * sizeof(int));
	int*p2 = MALLOC(10, int);  
    //int *p2 = (int*)malloc(10*sizeof(int));

    #undef MALLOC
	MALLOC(20, char);//提示未定义

	return 0;
}


4.4 命令行定义 

当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假 定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大写。)

 举例:我们在源文件中并没有定义SZ

在linxu环境下,我们可以在编译过程中,通过命令行中指定SZ的值。用于启动编译过程,代码成功运行


 4.5 条件编译

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

 满足条件编译,不满足条件不编译

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


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

常量表达式

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#if 1+2//满足条件printf执行
		printf("%d ", arr[i]);
#endif//
        arr[i] = i+1;
#if 0//不满足条件printf执行
		printf("%d ", arr[i]);
#endif//


	}

	return 0;
}

 多分支条件编译

#define NUM 8
int main()
{
#if NUM==1
	printf("hehe\n");
#elif NUM == 2
	printf("haha\n");
#else
	printf("heihei\n");
#endif

	return 0;
}

查看符号是否被定义:

#define MAX 0

int main()
{
#if defined(MAX)//只要被定义即可,不在乎值
	printf("hehe\n");
#endif

#if !defined(MAX)//如果没有被定义执行,跟上面逻辑相反
	printf("hehe\n");
#endif

#ifdef MAX//等价于#if defined
	printf("hehe\n");
#endif

#ifndef MAX//如果没有被定义执行,跟上面逻辑相反
	printf("hehe\n");
#endif

	return 0;
}


4.6 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方 一样。这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。

#include <stdio.h> //标准库里的文件,使用<>


#include "test.h"  //包含自己的头文件,使用""
//1. 先在源文件所在目录下查找,找不到,走第2次查找
//2. 在标准位置查找头文件

头文件被包含的方式:

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

2.库文件包含(查找头文件直接去标准路径下去查找,如果找不到就提示编译错误)

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


4.7 嵌套文件包含

单个文件编译虽然方便,但也有缺点:
所有的代码都堆在一起,不利于模块化和理解。工程变大时,编译时间变得很长,改动一个地方就得全部重新编译。
因此,我们提出多文件编译的概念,文件之间通过符号声明相互引用。

 上面图例中,comm.h和comm.c是公共模块。 test1.h和test1.c使用了公共模块。 test2.h和test2.c使用了公共模块。 test.h和test.c使用了test1模块和test2模块。 这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

注意:如果多次包含头文件,预编译期间一次便增加几百几千行代码,会大大拖慢运行速度,导致编译器压力增大,我们开发代码时,无意间可能会多次包含头文件

为了规范代码,我们只需要在头文件中加入,下列代码意思:如果没有定义__TEST_H__,便define定义一个__TEST_H__(第一次包含头文件时,没见过符号,定义一个符号,下面代码参与编译,如果第二次包含,该符号已经被定义过了,跳过下面代码,这样无论你包含几次,实际编译只有一次)

#ifndef __TEST_H__
#define __TEST_H__

#endif

还有一种写法,在代码前加上这句话

#pragma once//以避免头文件的重复引入

《C语言深度剖析》中的预处理指令

猜你喜欢

转载自blog.csdn.net/weixin_63543274/article/details/124185019
c15
15