天涯的尽头是风沙,红尘的故事叫牵挂.....................................................................................
目录
前言
介绍了,程序环境和预处理方面的知识。敬请观看!
一、【程序环境的介绍】
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
1.1【翻译环境的介绍】
首先我们要清楚,一个代码写完以后是怎么成为一个可执行程序的。
以下是翻译环境中对(test.c)的大致处理情况
实际上,源代码在被编译器单独编译处理完之后,会首先生成对应的目标文件,(test.obj)
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。接下来了解一下一个test.c文件经过翻译环境到底经过了哪些操作:
这里主要讲一下,符号汇总,形成符号表,符号表的合并和重定位:
1.2【运行环境的介绍】
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。整个过程:
二、【预处理的讲解】
2.1.【预定义符号】
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义 __FUNCTION__//查看函数名
注:
补充:其实编译器在代码编译的时候,会对函数和变量名重命名的
//在C语言中重命名的规则基本就是:加_
//C++ 中会更加复杂//这些预定义符号都是语言内置的。
举例:
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; for (int i = 0; i < 10; i++) { printf("%d----file=%s,date=%s,time=%s,line=%d\n", arr[i], __FILE__, __DATE__, __TIME__, __LINE__); } return 0; }
2.2【#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 MAX 1000 int main() { printf("%d\n", MAX); int arr[MAX]; return 0; }
注:
在define定义标识符的时候,不要在最后加上 ‘; ’
这里如果+上‘ ;’容易出现语法问题。
2.3【#define 定义宏】
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff //其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。举例:
#define SQUARE(x) x*x int main() { printf("%d\n", SQUARE(8));//SQUARE(8)会被替换为8*8; printf("%d\n", SQUARE(1 + 7));//这时不会先把1+7的结果计算出来再替换,而是直接替换,也就是替换为1+7*1+7 //这里可以定义为#define SQUARE(x) (x)*(x),这样就会比较符合预期。 return 0; }
#define DOUBLE(x) (x)+(x) int main() { int a = 0; a = 10 * DOUBLE(4);//这里会把DOUBLE(4)替换为(4)+(4),则变为10*(4)+(4) //所以如果想要达到预期结果,应该定义为#define DOUBLE(x) ((x) + (x)) printf("%d\n", a); return 0; }
注:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
2.4【#define 替换规则】
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.5【#和##的讲解】
1.#的作用
使用 # ,把一个宏参数变成对应的字符串。
2.## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
#的使用举例:
#define print_format(num,format)\ printf("the value of " #num " is " format,num);//将变量名和变量类型当作参数传递,其中#的作用是把对应的变量名进行替换。 int main() { int a = 10; printf("the value of a is %d\n", a); int b = 20; printf("the value of b is %d\n", b); float f = 3.14f; printf("the value of f is %f\n", f); //这里如果我们想,实现不同类型的参数输出的结果不同,也就是说要实现用1行代码,完成上面3行代码的功能,首先用函数是不行的,因为参数的类型无法作为参数进行传递 //这里我们就可以考录用宏来实现,我们发现字符串是有自动连接的特点的。 //比如printf("hello world\n");和printf("hello" "world\n");输出的结果是一样的。 print_format(f, "%f\n"); return 0; }
##的使用举例:
int class110 = 2023; #define CAT(x,y) x##y//##能够使位于两端的符号合成1个符号。 int main() { printf("%d\n", CAT(class, 110)); return 0; }
2.6【带副作用的宏参数】
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
比如:
int main() { //代码1: int a = 10; int b = a + 1;//b得到11,a还是10 //代码2: a = 10; b = a++;//b虽然得到了11,可是a也变为了11,改变了a的值 //这里我们说代码2具有副作用。 return 0; }
#define MAX(X,Y) ((X)>(Y)?(X):(Y)) int main() { int a = 3; int b = 5; //int c = MAX(a++, b++); int c = Max(a++, b++); //int c = ((a++)>(b++)?(a++):(b++)); // 3 5 printf("%d\n", c);//6 printf("%d\n", a);//4 printf("%d\n", b);//7 return 0; }
2.7【宏和函数对比】
先看一个例子:
int Max(int x, int y) { return x > y ? x : y; } //宏的实现 - 2 #define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a = 0; int b = 0; //输入 scanf("%d %d", &a, &b); //较大值 int m1 = Max(a, b); printf("%d\n", m1); int m2 = MAX(a, b); //int m2 = ((a)>(b)?(a):(b)); printf("%d\n", m2); return 0; }
这里用宏的方式来实现更好,因为宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等,可以用于>来比较的类型。
宏是与类型无关的。宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:
宏的参数可以出现类型,但是函数做不到:
#define MALLOC(num, type) (type*)malloc(num * sizeof(type)) int main() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { //.... } int*p2 = MALLOC(10, int); if (p2 == NULL) { //.... } //MALLOC(10, flaot); return 0; }
函数调用的时间花费:
1.函数调用前准备(传参、函数栈帧空间的维护)
2.主要运算
3.函数返回:返回值的处理,函数栈帧的销毁总结:
以后功能比较简单的时候,可以采用宏来实现
如果功能比较复杂,建议使用函数来实现
以下是函数和宏的对比:
2.8【命名约定】
一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
1.把宏名全部大写
2.函数名不要全部大写
三、【条件编译】
3.1【#undef】
这条指令用于移除一个宏定义。
使用形式:
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
举例:
#define M 100 int main() { printf("%d\n", M ); #undef M printf("%d\n", M);//这里的M就不能正常使用了。 return 0; }
3.2【命令行定义】
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
3.3【条件编译的介绍】
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
类似于之前学过的if语句。
例子:
#define __DEBUG__ int main() { int arr[10] = { 0 }; int i = 0; for (i = 0; i < 10; i++) { arr[i] = i; #ifdef __DEBUG__//如果这里的DEBUG没有定义,则printf("%d ", arr[i]);不会被执行 printf("%d ", arr[i]); #endif } return 0; }
注意:
#if 等预处理指令是在预编译阶段就开始处理了,要注意使用对象。
举例:
int main() { int a = 2; #if a==2 printf("hehe\n"); #endif return 0; }
分析:
注:所以我们在使用时,一般判断的是常量表达式
常见的条件编译指令:
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
四、【文件包含】
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
4.1【包含方式】
1.本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
如果找不到就提示编译错误。2.库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
4.2【嵌套文件包含】
如果出现这样的场景:
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。如何解决这个问题?
答案:条件编译。每个头文件的开头写:
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif //__TEST_H__
或者:
#pragma once
就可以避免头文件的重复引入。
五、【其他的预处理指令】
#error #pragma #line ... 不做介绍,自己去了解。 #pragma pack()在结构体部分介绍
总结
本篇讲解了,源代码是怎样从文件变为可执行程序的,还有C语言中预处理相关知识的介绍。是C语言学习中的最后一篇内容了,在每篇的结尾,都有一首音乐,来帮助你排解学习中的疲劳,后续还有很多优质内容,敬请期待。最后,感谢观看!
.............................................................................................酒招旗风中萧萧,剑出鞘恩怨了
————《红尘客栈》