C语言之程序环境和预处理

目录

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

翻译环境

程序翻译过程

对于编译过程又分为3步

关于符号汇总和生成符号表

链接阶段

链接阶段做的事情

合并段表

符号表的合并和重定位

linux命令

运行环境

程序的执行过程

预处理

预定义符号

#define

#define定义标识符

#define定义宏

#define的替换规则

#和##

带副作用的宏参数

宏和函数的对比

#undef

命令行定义

条件编译

文件包含

关于#include

头文件被包含的方式

其他预处理指令 

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

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

  1. 翻译环境:在这个环境中代码被转换成可执行的机器指令
  2. 执行环境:用于实际执行代码

理解:由test.c经过翻译环境变成test.exe可执行程序的过程就是依赖我们的翻译环境(eg:vs2019-包含编译器与链接器)编译后就可以在我们的运行环境(windows/linux)中执行。

在这里插入图片描述

翻译环境

程序翻译过程

理解:工程中的每个源文件(后缀名为.c的文件-eg:test.c)都会单独经过编译器处理,最后都会各自生成目标文件(windows环境中后缀名为.obj的文件-eg:test.obj)当生成多个目标文件的时候,这些目标文件会一起经过链接器(链接器链接的时候还会把我们的一些链接库[库函数所在的静态库]链接进去),最后链接器会把这些目标文件以及这些链接库链接在一起生成我们的可执行程序(后缀名为.exe的文件-eg:test.exe)

翻译环境的翻译过程包含两大过程

  1. 编译过程(依赖我们的编译器)
  2. 链接过程(依赖我们的链接器)

对于编译过程又分为3步

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

预处理阶段完成的事

  • 完成了头文件的包含——#include
  • #define定义的符号和宏的替换
  • 注释的删除
  • 条件编译

编译期间:把C语言代码转换为汇编代码(期间进行了语法分析、词法分析、语义分析、符号汇总[只汇总全局的符号])

汇编期间:把汇编代码转换为二进制代码/机器指令(生成符号表)

关于符号汇总和生成符号表

以add.c和test.c为例

//add.c
int Add(int x, int y) {
	return x + y;
}
//test.c
extern int Add(int x, int y);
void main() {
	int a = 10;
	int b = 20;
	int ret = Add(a, b);
}
源文件符号汇总生成符号表
test.c

Add

main

Add——0x0000(无效地址,因为引入的)

main——0xxxxx(有效地址)

add.cAddAdd——0xxxxx(有效地址)

链接阶段

作用:把多个目标文件和链接库进行链接生成可执行程序

链接阶段做的事情

合并段表

汇编阶段后生成的.obj目标文件为elf格式,这种elf格式的文件是由几个段组成,如果有多个这样的目标文件(如:test.obj、add.obj他们之间具有某种关联),最终我们要把目标文件链接生成一个可执行程序(这种可执行程序的格式也是elf格式),今把多个文件进行连接时就需要把相同段合并到一起去。

符号表的合并和重定位

由上面可知符号表合并中有2个Add,一个main,并得知有一个Add为无效地址(舍弃)最终得到test.c的main和add.c的Add并把他们的有效地址进行保留。这就是符号表的合并和重定位

因此,未来在可执行程序中调用Add函数的时候通过有效地址便能找到该函数。

linux命令

预处理:gcc-E test.c -o test.i

说明:预处理完成之后就停下来,预处理之后产生的结果都放在test,i文件中

编译:gcc -S test.c

说明:编译完成之后就停下来,结果保存test.s中

汇编:gcc -C test.c

说明:汇编完成之后就停下来,结果保存在test.o中

运行环境

程序的执行过程

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

预处理

预定义符号

定义:在预处理阶段被处理的,已经定义好的这种符号,这些符号是可以直接拿来使用的

这些预定义符号都是语言内置的,可以用来打印日志

#include <stdio.h>
void Add() {
	printf("%s\n", __FUNCTION__);
}
void main() {
	//写日志好用
	printf("%s\n", __FILE__);//打印这句代码所在源文件的名字(包含绝对路径)
	printf("%d\n", __LINE__);//打印这句代码所在源文件的行号
	printf("%s\n", __DATE__);//打印此时的日期——May 25 2022
	printf("%s\n", __TIME__);//打印此时的时间——20:10:42
	printf("%s\n", __FUNCTION__);//打印此段代码所在的方法
	printf("%d\n", __STDC__);//如果支持ANSI C其值为1,但VS不支持
	Add();
}

#define

#define定义标识符

语法:#define name stuff

注意:stuff并不一定就是个数字,其可能是关键字,也可能是一段代码,#define name stuff本质就是把name替换为stuff(所以后面不能加;其不是一条语句)

#include <stdio.h>
#define M 100
#define I int
void main() {
	I a = M;
	printf("%d",a);//100
}

#define定义宏

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

宏的申明方式

#define name(parament-list) stuff

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

注意:

  • 参数列表的左括号必须与name相邻
  • 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#include <stdio.h>
#define SOUARE(x) x*x
void main() {
	printf("%d",SOUARE(3));//9
	printf("%d", SOUARE(3+1));//7——3+1*3+1——简单替换
	//本质是把SOURCE(x)替换为x*x只不过中间多了参数列表可以传参替换
}
#include <stdio.h>
#define SOUARE(x) ((x)*(x))
#define DOUBLE(x) (x)+(x)
void main() {
	printf("%d", SOUARE(3+1));//16
	printf("%d", 10*DOUBLE(4));//44
}

#define的替换规则

  1. 在调用宏时,首先对宏的参数进行检查,看看是否包含由define定义的符号,如果是,他们首先被替换
  2. 替换文本随后被插入到程序原来文本的位置,对于宏,参数名被他们的值替换
  3. 最后再次对结果文件进行扫描,看看他是否包含任何由#define定义的符号,如果是,就重复上述处理过程
#include <stdio.h>
#define M 100
#define MAX(x,y) ((x)>(y)?(x):(y))
void main() {
	int max = MAX(12, M);//M首先被替换
	printf("%d\n", max);
}

注意:

  • 宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归
  • 宏名在源程序中若用引号括起来,则预处理程序不对其做宏代换

#和##

#可以把参数插入到字符串中(用在预处理中的stuff位置)——转字符串

#include <stdio.h>
#define print(x) printf("the value of "#x" is %d",x);
void main() {
	printf("hello" "" " world\n");//hello world——字符串之间可以自动拼接
	int a = 10;
	print(a)//the value of a is 10
}
//#x在预处理中就代表x内容所对应的字符串即#x=="a"

##可以把左右两个符号合并成一个符号(用在预处理中的stuff位置)——拼接·

#include <stdio.h>
#define cat(x,y) x##y
void main() {
	int class1 = 30;
	printf("%d\n", cat(class, 1));//30
}

带副作用的宏参数

#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
void main() {
	int a = 5;
	int b = 8;
	int m = MAX(a++, b++);
	printf("a=%d b=%d\n", a, b);//a=6 b=10
	printf("m=%d", m);//m=9
}

宏和函数的对比

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

函数是可以递归的

#undef

用于移除一个宏定义

#undef name

//如果现存的一个名字需要被重新定义,那么他的旧名字首先应被移除

 注意:在宏中不可以对一个宏名重复定义,如果需再定义,需要移除之前的定义

#include <stdio.h>
#define M 12
void main() {
	printf("%d\n", M);//12
#undef M//移除M的定义
#define M 100
	printf("%d\n", M);//100
}

命名约定:宏名全部大写,函数名不全部大写 

命令行定义

如linux可在命令行定义参数

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

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

linux下执行
gcc test.c -D M=10 

条件编译

定义:条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率

我们设定条件,满足条件则编译,反之不编译

eg:调试性的代码,删除可惜,保留碍事,所以我们可以选择条件编译

//#ifdef
#include <stdio.h>
void main() {
//M、N最多定义一个如果都定义则以第一个为准
#ifdef M    //也可以写成#if defined(M)
	printf("定义了M\n");
#elif N
	printf("定义了N\n");
#else
	printf("都没定义啊\n");
	#endif
}
//#ifndef
#include <stdio.h>
void main() {
#define M 20
#ifndef M    //也可以写成#if !defined(M)
	printf("没定义M\n");
#else
	printf("定义了M\n");
	#endif
}
//#if 表达式
#include <stdio.h>
void main() {
	//1,2表达式最多有1个能为真,如果都为真则执行第一个表达式
#if 0//表达式1
	printf("1表达式为真");
#elif 1//表达式2
	printf("2表达式为真");
#else
	printf("都不为真");
	#endif
}

注意:条件编译也可以嵌套使用

文件包含

关于#include

  1. #include指令可以使另外一个文件被编译,就像它实际出现在#include指令的地方一样
  2. 这种替换方式很简单
  3. 预处理器先删除这条指令,并用包含文件的内容替换
  4. 这样一个源文件被包含10次,那么就实际被编译10次

头文件被包含的方式

本地文件包含

#include "filename"

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

使用:(实现加法)

//add.h头文件内
int Add(int x, int y);
//add.c源文件内
int Add(int x, int y) {
	return x + y;
}
//test.c源文件内
#include <stdio.h>
#include "add.h"
void main() {
	printf("%d\n", Add(2, 8));//10
}

库文件包含 

#include <filename>

查找策略:直接去库函数的头文件目录下查找

由此观之:本地文件包含相比库文件包含来说查找范围更大一些,不过为了效率,该去库文件目录下查找直接去库文件目录下查找,用库文件包含

嵌套包含

由此观之,comm.h头文件被包含2次 

如何防止一个头文件被多次包含

第一种

//防止一个头文件被多次包含

#pragma once

//add.h头文件内
#pragma once//即使被多次包含也只包含一份
int Add(int x, int y);

第二种

//条件编译方式如果包含就定义了参数,不能再次定义
#ifndef __TEST_H__    //名字随便起
#define __TEST_H__
int Add(int x, int y);
#endif

其他预处理指令 

//导入静态库sub.lib
#pragma comment(lib,"sub.lib")

#include <stdio.h>
//修改默认对齐数为2
#pragma pack(2)
struct Hand
{
	char c;
	int size;
	char q;
};
#pragma pack()//取消设置的默认对齐,还原默认
void main() {
	printf("%d\n", sizeof(struct Hand));//默认对齐数8时——12,默认对齐数2时——8
}

#error

#pragma

#line

……

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值