程序环境和预处理

目录

前言

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

二、详解编译+链接

1.翻译环境

2.运行环境

三、预处理详解 

1.预定义符号

2.#define

2.1#define定义标识符

2.2#define定义宏

2.3#define替换规则

2.4#和##

2.5带副作用的宏参数

2.6宏和函数对比

2.7命名约定

3.#undef

4.命令行定义

5.条件编译

6.文件包含

6.1头文件被包含的方式

6.2嵌套文件包含

四、其他预处理指令

总结


前言

  本篇文章主要讲程序环境和预处理的内容,本篇文章内容可能很长,但是很详细,欢迎大家阅读,下面我们就开始吧~


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

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

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境,它用于实际执行代码。

二、详解编译+链接

1.翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人
    的程序库,将其需要的函数也链接到程序中。

下面我们来举个例子,我们在gcc下建两个.c文件

写代码

//test.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);
    return 0;
}
//add.c

int Add(int a, int b)
{
    return a + b;
}

预编译

 使用预编译指令:gcc ____.c -E -o ____.i

 我们打开test.i看一下,我们可以看到有700多行,后面和test.c中的内容一致,前面是什么呢?是把头文件中的内容包含进去了吗?

 Linux环境下,头文件放在/usr/include这个路径下,我们打开可以看到有很多头文件。

 打开stdio.h,我们拉到最下面看一下

 和之前的test.i进行对比,可以看到有一样的函数

 我们再在test.c中增加两行代码:(#define和注释)

//test.c
#include<stdio.h>

extern int Add(int, int);

#define MAX 100
//定义MAX的值
int main()
{
    int z = MAX;
    int a = 10;
    int b = 20;
    int c = Add(a, b);
    printf("%d\n", c);
    return 0;
}

我们将test.c再次进行预编译,再次打开test.i,我们发现注释和#define都不见了,MAX也替换成了100。


编译

编译处理指令:gcc ____.i -S

 编译过程就是把C语言代码翻译成汇编代码。

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总

我们来简单了解一下符号汇总


汇编

汇编指令:gcc ____.s -c

 生成.o的目标文件,我们打开看一下,里面的内容我们是看不懂的,因为目标文件存的是二进制。 

Linux环境下:test.o可执行程序的格式是(elf)

readelf工具

我们直接使用readelf test.o,会出现很多东西,实际上我们使用readelf需要一些选项。

 其中-s是显示符号表

 


链接

 链接过程会:

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

 如果我们的add.c中没有写Add函数,在连接时,我们将无法解析外部符号Add


最后我们用一个图简单总结一下:

2.运行环境

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

三、预处理详解 

1.预定义符号

__FILE__      //进行编译的源文件

__LINE__     //文件当前的行号

__DATE__    //文件被编译的日期

__TIME__    //文件被编译的时间

__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的。
举个例子:
#include <stdio.h>

int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("name:%s   file:%s   line:%d   date:%s   time:%s   i=%d\n", __func__, __FILE__, __LINE__, __DATE__, __TIME__, i);
	}
	return 0;
}

运行结果展示

2.#define

2.1#define定义标识符

语法:#define name stuff

举例:

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,       \
                          __DATE__,__TIME__ )

预处理是define会直接进行替换,我们来举个例子

我们先在vs中写一段代码

#include <stdio.h>
#define NUM 100
int main()
{
	int num = NUM;
	return 0;
}

然后如下图操作

 再点击进行编译,我们可以看到出现了一个.i文件,我们将他打开,我们可以看到NUM已经被直接替换成了100。


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

比如:#define NUM 100;

我们可以自己测试一下,NUM直接替换成了100;,预编译后,出现两个;出现语法错误。

2.2#define定义宏

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

宏的申明方式:#define name( parament-list ) stuff

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

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


下面我们来举个例子

#include <stdio.h>
#define SQUARE( x ) x * x
int main()
{
	int num = SQUARE(5);
	return 0;
}

进行预编译,我们可以看到直接被替换成了5*5。


下面我们来改变一下代码

#include <stdio.h>
#define SQUARE( x ) x * x
int main()
{
	int a = 4;
	int num = SQUARE(a + 1);
	return 0;
}

直接把a+1赋给x,运行的结果是否和之前一样呢?答案是:否,我们预编译一下。结果是9,并不是25。


为了防止因为优先级,而出现这种错误,我们应该加上几个(),变成

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

这时我们的代码就不会出现错误。

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

2.3#define替换规则

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

注意:

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

2.4#和##

#

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

我们现在想进行这样一个操作

#include <stdio.h>

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

这两个打印十分相似,那我们怎么把他写成一条简单的代码呢?

这里我们要使用 #,把一个宏参数变成对应的字符串。

代码中的 #VALUE 会预处理器处理为: "VALUE" 。

所以我们应该这样写

#include <stdio.h>
#define PRINT(N) printf("the value of "#N" is %d\n", N)
int main()
{
	int a = 5;
	PRINT(a);
	int b = 10;
	PRINT(b);
	
	return 0;
}

运行结果展示,这样就达到了我们的目的。


##

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

举一个例子

#include <stdio.h>
#define CAT(name, num) name##num
int main()
{
	int No1 = 100;
	printf("%d\n", CAT(No, 1));
	
	return 0;
}

运行结果展示

 注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

2.5带副作用的宏参数

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

例如:

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

下面我们来举个例子

#include <stdio.h>
#define MAX(x, y) ( (x)>(y) ? (x) : (y) )
int main()
{
	int a = 2;
	int b = 3;
	int c = MAX(a++, b++);
	printf("%d\n", a);
	printf("%d\n", b);
	printf("%d\n", c);
	
	return 0;
}

输出的结果是什么呢?我们来编译一下。

 我们预编译看一下,因为参数多次使用,让我们的结果和预期的不一样,所以我们应该尽量避免使用带有副作用的参数。

2.6宏和函数对比

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

#define MAX(a, b) ((a)>(b)?(a):(b))

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

原因有二:

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

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

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

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

比如:

#include <stdio.h>
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p = MALLOC(10, int);
	
	return 0;
}

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

2.7命名约定

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

3.#undef

这条指令用于移除一个宏定义。
#undef NAME
// 如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

下面我们来举个例子,我们将MALLOC宏定义移除之后,在使用,会报错。

4.命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

下面我们在Linux环境下演示一下,我们先创建一个test.c文件,然后写入代码

#include <stdio.h>

int main()
{
	int arr[SZ];
	int i = 0;
	for (i = 0; i < SZ; i++)
	{
		arr[i] = i;
	}
	for (i = 0; i < SZ; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

编译指令:gcc -D ARRAY_SIZE=10 programe.c

我们可以看到生成了一个a.out的文件

 ./a.out执行

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

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

1.

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i + 1;
#if 1//如果真就执行,如果写的是"#if 0",如果假,不执行
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

我们改写成#if 0,预编译一下,我们来看一下,并没有printf那行代码


2.多个分支的条件编译

#include <stdio.h>
#define NUM 1
int main()
{
#if NUM == 1
	printf("NUM = 1");
#elif NUM == 2
	printf("NUM = 2");
#else
	printf("NUM = ?");
#endif

	return 0;
}

3.判断是否被定义

#include <stdio.h>
#define MAX 1//定义MAX
int main()
{
#if defined(MAX)//如果MAX被定义,打印Yeah!!
	printf("Yeah!!\n");
#endif

#if !defined(MAX)//如果MAX未定义
	printf("NO!!\n");
#endif

#ifdef MAX//另一种写法
	printf("Wow!!\n");
#endif

#ifndef MAX
	printf("no!!\n");
#endif

	return 0;
}

运行结果展示


 条件编译指令用途非常广泛

比如我们打开stdio.h这个头文件看一下

6.文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。

这种种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。

6.1头文件被包含的方式

  • 本地文件包含

#include "filename"

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

Linux环境的标准头文件路径:

/usr/include

VS环境的的标准头文件路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include(按照自己的安装路径去找)

  • 库文件包含

#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

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

6.2嵌套文件包含

在头文件包含这里可能会有一些小问题,我们来举一个例子

我们在Linux环境下建两个文件,test.c和test.h,写入代码

//test.c
#include "test.h"//多次引用自己的头文件
#include "test.h"
#include "test.h"

int main()
{
    return 0;
}
//test.h

int Add(int x, int y);

我们将test.c预编译到test.i中,打开test.i,头文件被拷贝了三次。


在我们写代码的时候,可能会出现这种情况。我们来举个例子

 那么我们应该怎么解决这个问题呢?我们只需要在我们的.h文件上进行一些改动

#ifndef __TEST_H__
#define __TEST_H__

int Add(int x, int y);

#endif

我们再次进行预编译,这次我们的test.h只被包含了一次

 还有一种写法,我们再来改一下我们的.h文件

#pragma once//可以达到和上面一样的效果

int Add(int x, int y);

四、其他预处理指令

#error

#pragma

#line

······

大家可以自己查询了解一下


总结

  以上就是今天要讲的内容,关于程序环境和预处理的内容就全部结束了,希望对大家有所帮助,感谢大家的阅读,如果有问题欢迎评论指出,会积极改正!!谢谢~

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值