15.程序环境和预处理

目录

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

编译和链接

预处理


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

在ANSI C的任意一种实现中,存在两种不同的环境。翻译环境和执行环境。

翻译环境:在这个环境中,将源代码转换为可执行的机器指令。

执行环境:用于实际代码的执行。

源文件通过编译器形成目标文件。目标文件和链接库通过连接器形成可执行程序。

组成一个程序的每个源文件通过编译过程分别转换成目标代码。

每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。

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

编译和链接

编译分为3个部分,分别是:预编译、编译、汇编

预编译:将后缀为.c文件转换为.i文件。主要进行的操作是将头文件包含,将注释删除或用空白代替,#define定义的符号替换等的文本操作。(gcc -E test.c -o test.i)

编译:将后缀为.i文件转换为.s文件。把c语言代码转换为汇编代码。包括语法分析,词法分析,语义分析,符号汇总。(gcc -S test.i ---> test.s)

汇编:将后缀为.s文件转换为.o文件。将汇编代码转换为二进制指令。形成符号表。(gcc -c test.s -->test.o)

链接:是将多个.o文件链接为一个可执行程序,包括合并段表、符号表的合并和重定位。(gcc test.o -o test)

 查看编译期间每一步发生了什么:

1.预处理 选项 gcc -E test.c -o test.i, 预处理完成之后停下来,预处理产生的结果都放在test.i文件中。

2.编译 选项 gcc -S test.c ,编译完成之后停下来,结果保存在test.s 中。

3.汇编 选项 gcc -c test.c 汇编完成之后停下来,结果保存在test.o中

程序执行的过程:

1.程序必须载入内存中。

在有操作系统的环境中:一般由这个操作系统完成。

在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

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

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

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

预处理

预定义符号:定义符号都是由语言内置的

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

#define定义的标识符

语法:

#define name staff
// 定义name为staff
#include <stdio.h>
#define MAX 100                // 把max定义为100
#define reg register           // 为regiseter这个关键字创建一个简短的名字
#define do_fover for(; ;)      // 用形象的符号来替换实现
#define CASE break;case        // 在每次写case时,自动加上break

int main()
{
	printf("%d\n", MAX);
	return 0;
}

#define 定义的宏

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

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

宏的声明方式:

#define	name(parament-lise) stuff
// 左括号必须与name紧邻
#include <stdio.h>
#define SUM(x, y) x + y
int main()
{
	printf("%d\n", SUM(2, 3)); // SUM(2, 3) == 2 + 3
	printf("%d\n", 10 * SUM(2, 3)); // 10 * SUM(2, 3) == 10 * 2 + 3 = 23
	return 0;
}

#include <stdio.h>
#define SUM(x, y) ((x) + (y))
int main()
{
	printf("%d\n", SUM(2, 3)); // SUM(2, 3) == 2 + 3
	printf("%d\n", 10 * SUM(2, 3)); // 10 * SUM(2, 3) == 10 * ((2) + (3)) = 50
	return 0;
}
// 由#define定义的宏多加括号

#define的替换规则:

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

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

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

注:

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

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

 #和##

#:把一个宏参数变成对应的字符。

#include <stdio.h>
#define PRINT(n) printf("the value of "#n" is %d\n", n)

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	PRINT(a);
	PRINT(b);
	PRINT(c);
	return 0;
}

#include <stdio.h>
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE)

int main()
{
	int a = 10;
	char b = 'a';
	PRINT("%d", a);
	PRINT("%d", a + 2);
	PRINT("%c", b);
	return 0;
}

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

必须是合法的标识符,否则其结果是未定义的。

#include <stdio.h>
#define CAT(C, NUM) C##NUM

int main()
{
	int twentyone = 21;
	printf("%d\n", CAT(twenty, one));
	return 0;
}

带副作用的宏参数

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

#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main()
{
	int x = 2;
	int y = 3;
	int z = MAX(x++, y++);
	// (2++) > (3++)    ❌
	//  x 3      y 4
	//            4++
	//  x = 3, y = 5, z = 4
	printf("x = %d\ny = %d\nz = %d\n", x, y, z);
	return 0;
}

运行结果:

 宏和函数的对比

宏相比于函数的优势:

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

2.函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之,这个宏可以适用于整型、长整型、浮点型等类型。宏于类型无关。

宏相比于函数的劣势:

1.每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。

2.宏是没有办法调试。

3.宏由于类型无关,不够严谨。

4.宏可能带来运算符优先级的问题,导致程序出错

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

命名约定:

宏全部大写

函数名不要全部大写 

#undef: 用于移除一个宏定义

#include <stdio.h>

#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
	int m = MAX(2, 3);
	printf("%d\n", m);
	return 0;
}

#include <stdio.h>

#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
#undef MAX
	int m = MAX(2, 3);
	printf("%d\n", m);
	return 0;
}

 

命令行定义:允许在命令行中定义符号,用于启动编译过程。

        当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候。 例如:在不同的机器位数确定不同的数组大小。

#include <stdio.h>

int main()
{
	int arr[ARRAY_SIZE];// 可以在编译的过程中使用命令行参数来给数组的大小赋值
	for (int i = 0; i < ARRAY_SIZE; i++) {
		arr[i] = i;
	}
	for (int i = 0; i < ARRAY_SIZE; i++) {
		printf("%d ", arr[i]);
	}
	return 0;
}

条件编译:在编译一个程序时,将一条语句编译或者放弃编译。

常见的条件编译的指令:

1.

#if 常量表达式

//

#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       

#include <stdio.h>

#define M 0
int main()
{
	int i = 0;
	int n = 10;
	for (i = 0; i < 10; i++) {
#if M
		printf("%d ", i);
#endif
		printf("1 ");
	}
	return 0;
}

#include <stdio.h>

#define M 2
int main()
{
	int i = 0;
	int n = 10;
	for (i = 0; i < 10; i++) {
#if M
		printf("%d ", i);
#endif
		printf("1 ");
	}
	return 0;
}

 

#include <stdio.h>

#define M 150
int main()
{
#if M < 100
	printf("less\n");
#elif M == 100
	printf("equal\n");
#elif M > 100 && M < 200
	printf("middle\n");
#else
	printf("more\n");
#endif
	return 0;
}

#include <stdio.h>
#define M 0
int main()
{
#ifdef M
	printf("Mdefine1\n");
#endif

#if defined(M)
	printf("Mdefine2\n");
#endif

#ifndef M
	printf("Mnodef1\n");
#endif

#if !defined(M)
	printf("Mnodef2\n");
#endif
	return 0;
}

#include <stdio.h>

int main()
{
#ifdef M
	printf("Mdefine1\n");
#endif

#if defined(M)
	printf("Mdefine2\n");
#endif

#ifndef M
	printf("Mnodef1\n");
#endif

#if !defined(M)
	printf("Mnodef2\n");
#endif
	return 0;
}

 

 头文件包含的方式:

本地文件包含:用双引号

        查找策略:现在源文件所在目录下查找,如果头文件未找到,编译器就在标准位置查找头文件。

库文件的包含:用<>

        查找策略:直接去标准路径下查找,找不到,返回错误

 防止一个文件被多次包含:

在每个头文件写

1.

#ifdef __TEST_H__

#deine __TEST_H_

//

#endif

2.

#pragma once

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值