我们经常写一个文件test.c
他是一个叫做源文件或者源程序的东西
这个test.c文件经过一个详细的处理会变成test.exe文件
test.exe可执行程序
过去我们没有关注这整个过程,我们只是关注这个过程中出现错误我们该怎么办?
实际上test.c经过(编译)(链接)才会变成test.exe文件
文件预处理就是解释(编译)(链接)这两个过程
---------前言
(文章内//比较多,文章编译时,有时候需要对齐,还有一些格式,没有//不好弄,给您带来不便,敬请谅解。)
延申:test.c—(编译)(链接)—>test.exe–(运行)–>
//我们先写一个正确的代码(随便写)
//int main()
//{
// int arr[10] = { 0 };
// int i = 0;
// for (i = 0; i < 10; i++)
// {
// arr[i] = i;
// }
// for (i = 0; i < 10; i++)
// {
// printf("%d ", arr[i]);
// }
// return 0;
//}
//例:你写的代码保存在桌面
//先不运行上述代码,从桌面打开今日写的代码文件,此时会出现一些小文件,自己截图
//退出文件
//运行代码,打开文件,此时会发现出来一个新的文件Debug
//Debug调试版本
//打开Debug文件会发现一个叫做test.exe的文件
//双击这个文件,则会运行这个文件,双击,此时会发现屏幕一闪而过
//退出上一级
//打开(和今日写的代码文件)一样的文件
//此时也会出现一个Debug文件,打开Debug这个文件
//此时需要关注一个文件夹(test.obj )
//这个过程会出现一些中间运行的文件,即:(编译链接)产生的东西
//test.c-- - (编译)(链接)—>test.exe
//test.exe可执行程序
//程序的翻译环境和执行环境
//在ANSI C的任何一种实现中,存在两个不同的环境。
//第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
//解释:桌面打开文件—>打开Debug文件—>用notepad++打开(test.exe)
//此时出现一大堆我们看不懂的内容,因为里面的内容是用二进制写的,
//即:test.exe里面是二进制信息(二进制文件)
//test.c-- - (编译)(链接)—>test.exe–(运行)–>
//test.c 里面是 :c代码。c代码是文本文件
//第2种是执行环境,它用于实际执行代码。
//通俗解释:
//test.c到test.exe之间的过程所依赖的整个环境被叫做:翻译环境
//test.exe–(执行)–> 所依赖的环境叫做运行环境
//翻译环境
//一个工程里面会有几个文件
//此时我们创建一个add.c源文件
//int main(int x, int y)
//{
// return x + y;
//}
总体流程:
//源文件------》编译器---------》目标文件--丨
// 丨
//源文件------》编译器---------》目标文件--丨
// 丨
//源文件------》编译器---------》目标文件--丨
// 丨
//链接库------》连接器《---------------------
// 丨
// v
// 可执行程序
//一个工程会有多个 (.c文件)对应上面图片的(源文件)
//任何一个源文件都会单独作为一个单元被编译器的单独处理会生成自己对应的目标文件
//翻译环境中的编译工作便是由编译器完成的
//test.obj叫做
//object 中文翻译:对象;目标
#include
//声明一下来自外部文件的函数
//extern int Add(int x, int y);
//int main()
//{
// int a = 10;
// int b = 20;
// int c = Add(a, b);
// printf("c = %d\n", c);
// return 0;
//}
//运行结果:c = 30
//此时从桌面打开今日代码–》打开和今日代码相同的文件–》打开Debug文件
//此时会看见(add.obj)和( test.obj)
//即:test.c 会生成 test.obj
// add.c 会生成 add.obj
//即:每个源文件经过编译器都会生成属于自己的目标文件(obj)
//然后(每个目标文件)都会和(链接库)经过(连接器)的处理会生成(可执行程序)
//test.c—(编译)(链接)—>test.exe–(运行)–>
//其中编译依赖编译器,编译走的整个过程叫做:编译
// 链接依赖连接器,链接走的整个过程叫做:链接
//链接器:linker
//官方解释:
//1.组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
//2.每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
//1.2.上面都有解释
//3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
//通俗解释:
//目标文件经常会使用一些库函数,这些库函数不一定都是源文件里面定义的,而是在标准库里面定义的
//例如:我们经常会使用一个叫做printf的函数,他所需要的头文件是#include
// 从MSDN中搜索printf,里面会有一个libraries(库)(静态库)(库文件)
// libraries下面会有三个库(都是.LIB)文件
// printf其实放在这三个库中
// 你使用printf,其实是使用这三个库
//即:你的目标文件需要使用printf函数,就是把(.LIB)文件这种(链接库)链接进来
//test.c----------------------------------------->test.exe–(运行)–>
//-------------翻译环境--------------------------
//【----编译(编译器)–】【—连接(连接器)–】
//—其中编译会生成–》目标文件
//其中编译本身也分为三个阶段【预编译】【编译】【汇编】
//此时需要 虚拟机
//创建一个文件test.c,然后写代码(随便写)
//定义一个全局变量
//#include
//int g_val = 1000;
//int main()
//{
// int i = 0;
// int arr[10] = { 0 };
// for (i = 0; i < 10; i++)
// {
// arr[i] = i;
// }
// for (i = 0; i < 10; i++)
// {
// printf("%d ", arr[i]);
// }
// return 0;
//}
//保存这个代码test.c,这个文件我们称之为:源文件
//1. 预处理 选项 gcc - E test.c - o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
//2. 编译 选项 gcc - S test.c 编译完成之后就停下来,结果保存在test.s中。
//3. 汇编 gcc - c test.c 汇编完成之后就停下来,结果保存在test.o中。
//此时输入:gcc test.c–》回车
//如果此时没有任何提示,则证明代码没有任何问题
//此时输入:ls
//此时会生成一个(a.out)文件
//a.out就是我们最终生成的可执行程序
//a.out是默认的,他的名字你可以自己随便修改
//修改名字:
//此时输入:gcc test.c -o test–》回车–》输入ls看一下
//此时会产生a.out test
//这里的a.out和test只是名字不一样,其内容是一样的
//此时我们返回原来的情况,为了不添麻烦
//输入:rm -f test–》回车–》输入ls
//此时便恢复正常,下一行出现:a.out test.c
//执行代码:
//输入:./a.out(执行当前目录的a.out文件)(. 叫做当前目录)
//回车,此时会出现结果:
// 0 1 2 3 4 5 6 7 8 9 【自己名字】(自己定义的)(不影响)
//test.c经过(gcc test.c)便会直接生成a.out
//此时输入:rm -f a.out–》回车–》输入:s–》回车
//此时我们便把a.out删除,
//输入:ls(显示)–》回车
//屏幕下一行显示为:
//test.c
//【1. 预处理 选项 gcc - E test.c - o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。】
//此时输入:gcc - E test.c–》回车(进行预编译/预处理)
//此时屏幕会出现一大堆不认识的(预处理之后产生的结果)
//此时生成的这些不好观察,我们可以把它放到文件里面去
//输入:gcc -E test.c > test.i
//(>叫做重定项)(重定项到一个文件里面)
//test.i是新建的文件
//预编译的过程中就会产生test.i维护的文件,此时默认产生这个文件(方便)
//回车–》输入ls–》
//此时便会产生一个test.i文件
//输入:vim test.i (打开test.i文件)
//此时面前会有855行代码,我们到最后几行去看
//841到855行代码是自己写的代码,上面的都不是
//此时输入: shift + ; 便会关闭
//观察自己写的代码和test.i文件里面的代码
//自己写的test.c中最前面几行中的#include
//则可以证明头文件在预处理中,便被已经被翻译
//预处理的过程
//1.#include 头文件的包含(将include里面的内容搬运到文件里面去)
//2.将#include里面的注释删除(我们在看test.i和include
// 还有test.c里面所有注释都删除
// 注释删除的方法:用空格替换
//3.#difine 预处理会将里面所以的值直接改成目标
//以上皆为文本操作
//【2. 编译 选项 gcc - S test.c 编译完成之后就停下来,结果保存在test.s中。】
//总体:test.i–》test.s文件
// 把c语言代码翻译成汇编代码
//具体步骤:1.语法分析 2.词法分析 3.语义分析 4.符号汇总
//符号汇总:一般汇总的符号都是函数名,全局变量之类的
//《程序员的自我修养》对具体不走里面的内容进行了详细分析
//输入:gcc - S test.i–》LS
//此时会生成test.s文件
//test.c–(预编译)gcc - E test.c–》test.i–(编译)gcc - S test.c --》test.s–。。。。。。
//输入:vim test.s(打开test.s文件)
//test.s文件里面放的是汇编指令,写的是汇编代码
//【3. 汇编 gcc - c test.c 汇编完成之后就停下来,结果保存在test.o中。】
//总体:把汇编代码转换成二进制指令;形成符号表
//输入:ls 看一下现在有什么文件
//test.c test.i test.s
//输入:gcc - c test.c --》回车–》ls–》
//此时会出现:test.c test.i test.o test.s
//虚拟机上面的test.o文件就是 我们电脑上面的test.obj文件
//test.o / test.obj 目标文件
//输入:vim test.o(打开)
//会出现一大堆我们看不懂的
//形成符号表:
//add.c文件
//int Add(int x, int y)
//{
// return x + y;
//}
//这个里面的符号Add
//test.c文件
//extern int Add(int x, int y);
//int main()
//{
// int a = 10;
// int b = 20;
// int c = Add(a, b);
// printf("c = %d\n", c);
// return 0;
//}
//这个里面的符号:Add main
//符号表格式:符号 地址
//test.o/test.obj文件经过链接形成test.exe文件(可执行程序)
//链接:1.合并段表 2.符号表的合并和符号的重定位
//1. 合并段表
//以.o为例
//test.o add.o文件是目标文件
//目标文件都有自己的格式:.o文件会把自己分成几个段(elf格式)
//合并段表会把对应的段链接在一起
// 2.符号表的合并和符号的重定位
//直接合并,有时候会遇见相同的符号,但是他们的地址不一样,此时我们要用(有效的)
//以上面代码为例:add.o和test.o文件里面的Add我们选择add.o
//因为test.o文件里面的Add,只是声明,没有实际应用
//add.c文件里面的符号表是: Add 有效地址
//test.c文件里面的符号表是:
//Add 无效地址
//main 有效地址
//当代码运行的时候,运行到符号的时候,他会自动去寻找这个符号的地址,如果没有(无效地址)则会运行错误
//运行环境
//程序执行的过程:
//1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
//解释:当你要执行代码的时候,双击便会运行,其实运行的过程是由操作系统把程序加载到内存里面去
// 如果没有操作系统,则需要手动完成,把程序放到内存里面去运行
//2. 程序的执行便开始。接着便调用main函数。
//3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
//4. 终止程序。正常终止main函数;也有可能是意外终止。
//预处理详解(预处理=预编译)
//预定义符号(本身就有,不是自己定义的)(自己定义的符号叫做:定义符号)
//例如:
//#define MAX 100 自己定义的符号
//预定义符号:
//FILE //进行编译的源文件
//LINE //文件当前的行号
//DATE //文件被编译的日期
//TIME //文件被编译的时间
//STDC //如果编译器遵循ANSI C,其值为1,否则未定义
//STDC 在 vc2013中不支持
//在linux中可以使用
//在linux输入
//#include
//int main()
//{
// printf("%d\n", __STDC__);
// return 0;
//}
//输出:1
//举例:
//int main()
//{
// printf("%s\n", __FILE__);
// printf("%d\n", __LINE__);
// return 0;
//}
//运行:此时会出现你现在写的代码的(绝对路径)
// printf("%d\n", __LINE__);这句代码所在的行数
//应用:写日记
//int main()
//{
// int i = 0;
// int arr[10] = { 0 };
// FILE* pf = fopen("log.txt", "w");
// for (i = 0; i < 10; i++)
// {
// arr[i] = i;
// fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\n",
// __FILE__, __LINE__, __DATE__, __TIME__, i);
// printf("%s\n", __FUNCTION__);//打印函数名称
// }
// fclose(pf);
// pf = NULL;
// for (i = 0; i < 10; i++)
// {
// printf("%d ", arr[i]);
// }
// return 0;
//}
//输出:10个main
// 0-9
//在外部打开会发现有一个log.c文件--》打开
//会出现一些日期,即:我们运行代码时的时间地址等等
//#define 预处理指令,是在预处理阶段处理的
//预处理指令:#define #include #pragma pack() #if #endif #ifdef #line(#开头的指令基本都是)
//1.#define 定义标识符
//2…#define 定义宏
//1.#define 定义标识符
//#define MAX 100
//int main()
//{
// int a = MAX;
// printf("%d\n", MAX);
// return 0;
//}
//可以定义任何东西 字符 数字 重命名 一段代码
//在define定义标识符的时候,不要在最后加上; ?
//解释:#define会把后面定义的所有内容替换过去,如果加入了,容易导致语法错误
//#define 定义宏
//#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)。
//下面是宏的申明方式:
//#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
//注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
//#define SQUARE(X) X*X
//int main()
//{
// int ret = SQUARE(5);
// printf("%d\n", ret);
// return 0;
//}
//输出:25
//#define SQUARE(X) X*X
//int main()
//{
// int ret = SQUARE(5 + 1);
// printf("%d\n", ret);
// return 0;
//}
//输出:11 5 + 1*5 +1 = 11
//#define SQUARE(X) (X)*(X)
//int main()
//{
// int ret = SQUARE(5 + 1);
// printf("%d\n", ret);
// return 0;
//}
//输出:36
//#define MAX 100
//#define DOUBLE(X) X+X
//int main()
//{
// int a = 5;
// int ret = 10 * DOUBLE(MAX);
// printf("MAX = %d\n", MAX);
// //int ret = 10* ((a)+(a));
// printf("%d\n", ret);
// return 0;
//}
//输出:MAX=100
// 1100
//#define MAX 100
//#define DOUBLE(X) ((X)+(X))
//int main()
//{
// int a = 5;
// int ret = 10 * DOUBLE(MAX);
// printf("MAX = %d\n", MAX);
// printf("%d\n", ret);
//
// return 0;
//}
//输出:MAX=100
// 2000
//提示:
//所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或
//邻近操作符之间不可预料的相互作用。
//#define 替换规则
//在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
//1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
//2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
//3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
//注意:
//1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
//2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
//字符串里面的符号不会被替换
//#和##
//#
//void print(int a)
//{
// printf("the value of a is %d\n", a);
//}
//int main()
//{
// int a = 10;
// int b = 20;
// print(a);
// print(b);
// return 0;
//}
//想输出:the value of 字符 is 字符所对应的数字
//上述方法(函数)绝对不可能解决这个问题
//int main()
//{
// printf("hello world\n");
// printf("hello " "world\n");
// printf("hel" "lo " "world\n");
// return 0;
//}
//输出都一样:hello word
//#define PRINT(X) printf("the value of " #X " is %d\n", X)
//int main()
//{
// int a = 10;
// int b = 20;
// PRINT(a);
// PRINT(b);
// return 0;
//}
//此时便达到我们的要求
//##的作用
//##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
//#define CAT(X, Y) X##Y
//
//int main()
//{
// int Class84 = 2019;
// //printf("%d\n", class84);
// printf("%d\n", CAT(Class, 84));
// //printf("%d\n", Class##84);
// //printf("%d\n", Class84);
// return 0;
//}
//带副作用的宏参数
//当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如:
//一般情况下的副作用举例:
//方法一:
//int main()
//{
// int a = 10;
// int b = a + 1;
// printf("%d ", a);
// printf("%d ", b);
// return 0;
//}
//输出:10 11
//方法二:
//int main()
//{
// int a = 10;
// int b = ++a;
// printf("%d ", a);
// printf("%d ", b);
// return 0;
//}
//输出:11 11
//解释:我们想要的到一个b=11,我们可以用两种方法,即:方法一和方法二
// 但是方法二会改变a的值,而方法一不会。
//带有宏参数的副作用举例:
//一:
//#define MAX(X, Y) ((X)>(Y)?(X):(Y))
//int main()
//{
// int a = 10;
// int b = 11;
// int max = MAX(a, b);
// printf("%d\n", max);//11
// printf("%d\n", a);//10
// printf("%d\n", b);//11
// return 0;
//}
//二:
//#define MAX(X, Y) ((X)>(Y)?(X):(Y))
//int main()
//{
// int a = 10;
// int b = 11;
// int max = MAX(a++, b++);
// //int max = ((a++) > (b++) ? (a++) : (b++));
// printf("%d\n", max);//12
// printf("%d\n", a);//11
// printf("%d\n", b);//13
// return 0;
//}
//解释://int max = ((a++) > (b++) ? (a++) : (b++));
// 1 2 3 4
//1和2 都会先用a和b,然后对a和b进行++,此时a=11,b==12.
//判断结果:a<b
//执行4,此时输出b,然后对b进行++,此时b=13
//函数和宏进行比较(性能):
//函数
//int Max(int x, int y)
//{
// return (x > y ? x : y);
//}
宏
//#define MAX(X, Y) ((X)>(Y)?(X):(Y))
//int main()
//{
// int a = 10;
// int b = 20;;
// int max = Max(a, b);
// printf("max = %d\n", max);
// max = MAX(a, b);
// printf("max = %d\n", max);
// return 0;
//}
//此时没有任何问题,都能进行比较
//函数
//int Max(int x, int y)
//{
// return (x > y ? x : y);
//}
//
//float Max2(float x, float y)
//{
// return (x > y ? x : y);
//}
宏
//#define MAX(X, Y) ((X)>(Y)?(X):(Y))
//int main()
//{
// float c = 3.0f;
// float d = 4.0f;
//
// float max = Max2(c, d);
// printf("max = %lf\n", max);
//
// max = MAX(c, d);
// max = ((c) > (d) ? (c) : (d));
// printf("max = %lf\n", max);
// return 0;
//}
//如果我们改变比较数字的类型,我们则需要重新写函数,而宏不需要
//如果我们把数字的类型搞错,我们用函数,则可以进行检查
//各有千秋
函数
//int Max(int x, int y)
//{
// return (x > y ? x : y);
//}
//
//float Max2(float x, float y)
//{
// return (x > y ? x : y);
//}
//
宏
//#define MAX(X, Y) ((X)>(Y)?(X):(Y))
//
//int main()
//{
// int a = 10;
// int b = 20;
// float c = 3.0f;
// float d = 4.0f;
// //函数在调用的时候
// //会有函数调用和返回的开销
// float max = Max2(c, d);
// printf("max = %lf\n", max);
//
// //预处理阶段就完成了替换
// //没有函数的调用和返回的开销
// max = MAX(c, d);
// //直接就翻译成:max = ((c) > (d) ? (c) : (d));
// printf("max = %lf\n", max);
// return 0;
//}
//先按 f11 --》出现很多文件--》关闭所有选项
//鼠标右键--》转到反汇编--》按f11一直走
// 当出现float max = Max2(c, d);我们观察这句代码下面的反汇编代码
//他执行的代码我们按f11会看到(注意行数)
//同理:运行到max = MAX(c, d);时,我们看反汇编里面运行了多少代码
//明显宏用的比函数的少,则宏效率更高
//宏相比较于函数的优点:
//1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
//2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
//3. 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
//【3. 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。】
//举例:
//#define SIZEOF(type) sizeof(type)
//int main()
//{
// int ret = SIZEOF(int);
// //int ret = sizeof(int);
// printf("%d\n", ret);//4
// return 0;
//}
//我们平常使用malloc时,特别别扭,int* p = (int*)malloc(10*sizeof(int));我们要计算大小,还要强制类型转换,感觉挺复杂的
//#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
//int main()
//{
// int* p = (int*)malloc(10*sizeof(int));
//
// int* p = MALLOC(10, int);
// //int* p = (int*)malloc(10 * sizeof(int));
// return 0;
//}
//宏相比较于函数的缺点:
//1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
//解释:宏可以定义一段很长的代码,当函数运行的时候,我们引用了这段宏很多次,那么这整个代码则会特别长
//2. 宏是没法调试的。
//3. 宏由于类型无关,也就不够严谨。
//4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
//函数传值的时候是先计算,然后再传,宏直接替换
//总结:
//属性 #define定义宏 函数
//代码长度 每次使用时,宏代码都会被插入到程序中。除了非常小的宏 函数代码只出现于一个地方;每次使用这
// 之外,程序的长度会大幅度增长 个函数时,都调用那个地方的同一份代码
//执行速度 更快 存在函数的调用和返回的额外开销,所以相对慢一些
//操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加 函数参数只在函数调用的时候求值一
// 上括号,否则邻近操作符的优先级可能会产生不可预料的后 次,它的结果值传递给函数。表达式
// 果,所以建议宏在书写的时候多些括号。 的求值结果更容易预测。
//带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参 函数参数只在传参的时候求值一次,
// 数求值可能会产生不可预料的结果。 结果更容易控制。
//参数类型 宏的参数与类型无关,只要对参数的操作是合法的,它就可 函数的参数是与类型有关的,如果参数的类型不同,
// 以使用于任何参数类型。 就需要不同的函数,即使他们执行的任务是不同的。
//调试 宏是不方便调试的 函数是可以逐语句调试的
//递归 宏是不能递归的 函数是可以递归的
//命名约定:
//一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:
//把宏名全部大写 函数名不要全部大写
//#undef
//这条指令用于移除一个宏定义。
//#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
//#define MAX 100
//int main()
//{
// printf("MAX = %d\n", MAX);
//#undef MAX
// printf("MAX = %d\n", MAX);
// return 0;
//}
//输入这段代码,我们可以明显看见第二个printf("MAX = %d\n", MAX);中的MAX下面有波浪线(有问题),则证明移除成功
//命令行定义
//许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
//用linux
//int main()
//{
// int arr[sz] = { 0 };
// 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 test.c -D sz=10
//运行:正常
//输出:0 1 2 3 4 5 6 7 8 9
//此时我们输入:gcc test.c -D sz=13
//运行:正常
//输出:0-12的数字
//条件编译:
//在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
//#define DEBUG
//int main()
//{
// int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
// int i = 0;
// for (i = 0; i < 10; i++)
// {
// arr[i] = 0;
//#ifdef DEBUG//如果DEBUG这个被定义过,则执行下面的代码
// printf("%d ", arr[i]);
//#endif
// }
// return 0;
//}
//int main()
//{
// int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
// int i = 0;
// for (i = 0; i < 10; i++)
// {
// arr[i] = 0;
//#ifdef DEBUG//如果DEBUG这个被定义过,则执行下面的代码
// printf("%d ", arr[i]);
//#endif
// }
// return 0;
//}
//对比上下代码,我们可以看见第二个代码下面的printf("%d ", arr[i]);明显便灰
//常见条件编译指令:
//1.
//#if 常量表达式
...
//#endif
常量表达式由预处理器求值。
//int main()
//{
// int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
// int i = 0;
//
// for (i = 0; i < 10; i++)
// {
// arr[i] = 0;
//#if 1+1//常量表达式,如果为真,则执行下面语句
#if 0 //此时则不会执行
// printf("%d ", arr[i]);
//#endif
// }
//
// return 0;
//}
//2.多个分支的条件编译
//#if 常量表达式
…
//#elif 常量表达式
…
//#else
…
//#endif
//个人理解:相当于if语句
//3.判断是否被定义 defined定义
//#if defined(symbol)
//#ifdef symbol
//#if !defined(symbol)
//#ifndef symbol
//都需要假如enfif结束
//#if defined(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次。
//1.头文件被包含的方式:
//(1)本地文件包含
//(2)库文件包含
//个人理解:使用库文件#include<库函数>
// 使用本地文件#include “自己的”
//查找策略:"" 先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
// 自己写的 库文件里面
// <> 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
//2.嵌套文件包含
//一个头文件我们有时候会引用许多次,这样会造成空间和时间的浪费
//解决方法:条件编译。
//例如:判断test.h是否已经被引用
//每个“头文件” 的开头写:
//#ifndef TEST_H
//#define TEST_H
头文件的内容
//#endif
//#pragma once
//头文件的内容