描述一下网站建设的基本流程图,四川移动网站建设,网站短时间怎么做权重,装饰公司用哪个招聘网站目录 1. 程序的翻译环境和执行环境
2. C语言程序的编译链接
2.1. 预处理
2.2. 编译
2.3. 汇编
2.4. 链接 3. 运行环境的简单介绍
4. 预定义符号介绍
5. 预处理指令 #define
5.1. #define定义标识符
5.2. #define定义宏
5.3. #define替换规则
6. 宏和函数的对比
1. …目录 1. 程序的翻译环境和执行环境
2. C语言程序的编译链接
2.1. 预处理
2.2. 编译
2.3. 汇编
2.4. 链接 3. 运行环境的简单介绍
4. 预定义符号介绍
5. 预处理指令 #define
5.1. #define定义标识符
5.2. #define定义宏
5.3. #define替换规则
6. 宏和函数的对比
1. 宏的优点
2. 宏的缺点
3. 宏和函数的对比
7. 预处理操作符#和##的介绍
7.1. #的作用
7.2. ##的作用
8. 命令约定
9. 预处理指令 #undef
10. 命令行定义
11. 条件编译
12. 文件包含 12.1. 头文件被包含的方式
1. 本地文件被包含的方式
2. 库文件被包含的方式
12.2. 嵌套文件包含 1. 程序的翻译环境和执行环境 在ANSI C的任何一种实现中存在两个不同的环境。 第一种是翻译环境在这个环境中我们的源代码会被转化为可执行的机器指令(即二进制指令)翻译环境是由编译器提供的(在vs下编译器为cl.exe链接器为link.exe) 第二种是执行环境它用来实际执行我们的代码(执行环境通常是操作系统提供的) 而我们在这里主要介绍一下翻译环境。 2. C语言程序的编译链接 不知道各位有没有好奇过我们写的C源文件是如何变成一个可以执行的二进制文件其实一个C源文件要变成一个可以执行的二进制文件需要经历两个过程编译和链接 什么叫编译呢什么叫链接呢 编译将一个程序的所有C源代码经由编译器的各种处理(隔离编译)形成一个可重定位的二进制文件在windows下具体为(*.obj)。但此时这个二进制文件是不可以直接被运行的它还需要经过链接处理。 链接 将一个程序中的所有可重定位的二进制文件经由链接器集中处理(集中链接)并且链接器会将程序需要的各种库(动态库、静态库)链接到程序中最后会形成一个可执行的二进制文件。 一个程序中的每个源文件都会通过编译过程(隔离编译)分别转化为可重定位的二进制文件。 链接器会将所有的二进制文件集中处理并将它们需要的各种库链接到一起形成一个可执行程序。 然而编译链接又可以被分为几个阶段 1、预处理 2、编译 3、汇编 4、链接 接下来我们用一个实例演示一下这几个过程 // add.c
extern int add(int x,int y)
{return x y;
}
// test.c
#include stdio.hextern int add(int x,int y);
// 宏定义
#define LEFT_VAL 20
#define RIGHT_VAL 10
// 条件编译
#if _LEFT_VAL
#define UP_VAL 30
#else
#define DOWN_VAL 40
#endifint main()
{// 注释//printf(haha\n);//printf(haha\n);//printf(haha\n);//printf(haha\n);printf(%d\n,add(LEFT_VAL,RIGHT_VAL));return 0;
} 2.1. 预处理 预处理过程会将C源程序进行一些列处理。具体如下 1、去掉注释 2、去掉条件编译 3、宏替换 4、头文件展开 // 让编译器预处理完后停下来,并把结果输出到test.i文件中
gcc -E test.c -o test.i 此时我们经过对比我们发现这个test.i文件有849行代码而我们写的test.c仅有22行,那么多出来的代码是谁的呢答案是stdio.h这个头文件中的内容。也就是说预处理会将头文件的内容展开其次我们发现我们源文件中的宏定义被替换了(宏常量直接被替换成对应的数字)而条件编译、以及注释都没见了。也就是说预处理不仅会展开头文件并且会将宏定义替换去掉注释以及条件编译。而像上面的 #include以及#define 就是预处理指令它们完成的都是一些文本操作。 2.2. 编译 编译又会做什么呢 //让编译器编译完后就停止,并将其结果重定向到test.s这个文件中
gcc -S test.i -o test.s 可以看到test.s其实就是一个汇编代码。也就是说在编译阶段编译器会将其C代码翻译成了汇编代码。这个过程包括1.语法分析 2.词法分析 3.语义分析 4.符号汇总在这里说一下符号汇总符号汇总就是将每个源文件(此时的源文件已经经历过了预处理)中的符号汇总到一起注意符号不会汇总。例如上面的两个源文件test.c和add.c。 2.3. 汇编
// 告诉编译器汇编完后就停下来,并将结果重定向到test.o文件中
gcc -c test.s -o test.o 此时这个文件就是一个可重定位的二进制文件在windows环境下可重定位的二进制文件类型为(*.obj)而在Linux环境下可重定位的二进制文件文件类型(*.o) 也就是说汇编过程会将汇编指令翻译成为我们的二进制指令。这个过程还会形成一个符号表。那么如何查看呢要知道二进制文件我们直接看是看不懂的在Linux下它的文件格式是elf(可执行程序和可重定位的二进制文件的格式都是elf)而在Linux下我们可以借助readelf工具来查看这个文件。如下readelf -s 可重定位的目标文件 就可以查看格式为elf文件的符号表 test.o的符号表如下 add.o的符号表如下 因为add的定义是在add.o文件中,test.o中只有其声明,故这里地址为NULLprintf同理,在stdio.h这个头文件中只有其声明,没有定义,故地址为NULL。这就是汇编过程每个源文件形成的符号表。 而此时由汇编形成的这些可重定位的二进制文件是不可以运行的。例如上面test.o这个二进制文件它只有add和printf的声明没有其定义这两个的函数地址为NULL此时无法运行这个文件。那么如何解决呢此时我们就需要进行链接链接会通过链接器将所有可重定位的二进制文件中的符号表进行汇总同时会将这些文件需要的库函数所在的各种库(例如C标准库第三方库等等)也链接进来形成一个可执行程序。 2.4. 链接 1. 合并段表 2. 将由汇编形成的符号表进行合并以及符号表的重定位。 3. 链接库 例如上面的编译过程中形成了两个符号表此时它们会进行合并以及重定位 经由链接过程此时这个可重定位的二进制文件就会形成一个可执行的二进制文件。 // 此时这个my_test就是一个可执行的二进制文件
gcc test.o add.o -o my_test 3. 运行环境的简单介绍 1. 程序必须载入内存中。在有操作系统的环境中一般这个由操作系统完成。在独立的环境中程序的载入必须由手工安排也可能是通过可执行代码置入只读内存来完成。 2. 程序的执行便开始。接着便调用 main 函数。 3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈 stack也就是我们所说的函数栈帧 存储函数的局部变量和返回地址。程序同时也可以使用静态static 内存存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。 4. 终止程序。正常终止 main 函数也有可能是意外终止。 4. 预定义符号介绍 __FILE__ --- 进行编译的源文件 __LINE__ --- 文件当前的行号 __DATE__ --- 文件被编译的日期 __TIME__ --- 文件被编译的时间 __STDC__ --- 如果编译器遵循ANSI C其值为1否则未定义 gcc编译器
void Test1(void)
{printf(file_name:%s\n,__FILE__);printf(file_line:%d\n,__LINE__);printf(file_date:%s\n,__DATE__);printf(file_time:%s\n,__TIME__);printf(STDC:%d\n,__STDC__);
} vs2013的sl.exe编译器: 相比之下 gcc编译器是对ANSI C的标准遵守的更全面一些的。 5. 预处理指令 #define
5.1. #define定义标识符 #define可以定义标识符本质上是一种替换机制。这个标识符会在预处理阶段就被替换掉。例如 #define COUNT 10
#define STR hehevoid Test2(void)
{// 这些宏定义的标识会在预处理阶段被替换掉// 例如这里本质上是:// int i 10;int i COUNT;// 这里是:const char* str hehe;const char* str STR; printf(%d\n%s\n, i, str);
} 注意由于#define定义标识符本质上是一种替换机制因此一般情况下不要在定义符号时加上 ; 例如下面的场景 // 如果你在这里添加了;
#define CAREFUL Dont add ;;void Test3(void)
{//那么本质上这个CAREFUL 就是 Dont add ;;,此时printf就会报错printf(%s\n, CAREFUL);
} #define在符合语法的前提下其用发还是蛮多的例如 #define stc staticvoid Test4(void)
{// 此时这个stc就是staticstc int i 10;
} 又比如 #define PRINTF printf(hehe\n)
// 注意: #defin的机制是一种替换机制
void Test5(void)
{// 此时这个PRINTF就是一个函数调用PRINTF;
}
5.2. #define定义宏 #define不仅可以定义标识符还可以定义宏宏是什么呢#define 机制包括了一个规定允许把参数替换到文本中这种实现通常称为宏macro或定义宏definemacro。 // 定义格式:
#define name( parament-list ) stuff
// 其中的parament-list是一个由逗号隔开的符号表, 它们可能出现在stuff中。
// 注意:参数列表的左括号必须与name紧邻.
// 如果两者之间有任何空白存在参数列表就会被解释为stuff的一部分 我们可以实现一个乘法的宏 #define MUL(x) x * xvoid Test6(void)
{int x 10;printf(ret: %d\n, MUL(x));
} 诶结果正确的但是如果是这样的呢 void Test7(void)
{printf(ret: %d\n, MUL(9 1));
} 此时看到这个结果有人就懵逼了什么情况我只是换了一种表现形式结果就不一样了注意我们应该牢记宏的机制是一种替换机制也就是说这个宏会被替换成如下的场景 printf(ret: %d\n, 9 1 * 9 1); 此时结果当然是19那么为了防止产生这种问题我们应该如何解决呢解决方案宏的实现我们要多用()如下 // 不要吝啬括号
#define MUL(x) (x) * (x)
void Test8(void)
{printf(ret: %d\n, MUL(91));// 上面的代码会被替换成如下代码://printf(ret: %d\n, (9 1) * (9 1));
} 此时才符合我们的预期 但有时候我们可能会遇到这种情况如下 #define DOUBLE(x) (x) (x)void Test9(void)
{int x 5;int ret 2 * DOUBLE(10) * DOUBLE(10);// 我们预期的结果应该是 2 * 20 * 20 800printf(ret: %d\n, ret);
} 为什么呢会出现这种现象呢原因是我们的宏写的有问题上面的这个表达式会被替换成如下形式 #define DOUBLE(x) (x) (x)void Test9(void)
{int x 5;int ret 2 * DOUBLE(10) * DOUBLE(10);// ret 2 * (10) (10) * (10) (10) 刚好是130printf(ret: %d\n, ret);
} 因此我们的宏应该做出如下改变 #define DOUBLE(x) ((x) (x))void Test9(void)
{int x 5;int ret 2 * DOUBLE(10) * DOUBLE(10);// 此时会被替换成如下形式// int ret 2 * ((10) (10)) * ((10) (10)); 此时才是800printf(ret: %d\n, ret);
} 总结在写宏的时候不要吝啬括号的使用 5.3. #define替换规则 在程序中扩展#define定义符号和宏时需要涉及几个步骤。 1. 在调用宏时首先对参数进行检查看看是否包含任何由 #define定义的符号。如果包含那么首先会将这些参数进行替换。 2. 替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值替换。 3. 最后再次对结果文件进行扫描看看它是否包含任何由 #define定义的符号。如果是就重复上述处理过程。 注意 1. 宏参数和 #define 定义中可以出现其他 #define定义的变量。但是对于宏不能出现递归。 2. 当预处理器搜索 #define定义的符号的时候字符串常量的内容并不被搜索。 6. 宏和函数的对比
1. 宏的优点 1. 宏的方式是替换不做计算也不做表达式的求解不需要建立函数栈帧对于某些短小的逻辑处理如果使用函数那么可能会存在函数栈帧的建立和销毁比函数逻辑处理更为复杂因此在某些情况下宏比函数在速度方面更胜一筹其处理过程也更为简单。例如 #define ADD(x,y) ((x) (y))int add(int x, int y)
{return x y;
}void Test16(void)
{int x 10;int y 20;int c ADD(x, y);c add(x, y);
}
1. 宏的处理 2. 函数的处理 可以看到函数的处理是十分复杂的其函数栈帧的建立与销毁比实际执行代码逻辑更为复杂。但是宏却的处理就更为简单。 2. 函数必须要求传参的时候要有类型但是宏没有这个要求。 例如上面我们写的ADD这个宏和add函数add函数已经限制了参数类型在不发生类型转换的前提下只能比较整形但是我的宏没有这个要求啊你传什么我就比较什么相比之下函数更为灵活。 3. 其次宏还可以传递类型但是函数是做不到的。例如 // type 就是你需要的数据类型
// size 代表多少个字节
#define GETMEMORY(type,size) (type*)malloc(size)
2. 宏的缺点 上面就是一些宏的优点但是它也有缺点例如 1. 每次使用宏的时候一份宏定义的代码将插入到程序中。除非宏比较短否则可能大幅度增加程序的长度。 2. 宏是没法调试的因为在预处理阶段宏就被替换了调试的是替换之后的代码。 3. 宏由于类型无关没有类型安全检查也就不够严谨。 4. 宏可能会带来运算符优先级的问题导致程序容易出现错。 3. 宏和函数的对比 属 性 #define定义宏函数代码长度 每次使用时宏代码都会被插入到程序中。除了非常小的宏之外程序的长度会大幅度增长 函数代码只出现于一个地方每次使 用这个函数时都调用那个地方的同 一份代码 执行速度更快 存在函数的调用和返回的额外开销 所以相对慢一些 操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里除非加上括号否则邻近操作符的优先级可能会产生不可预料的后果建议不要吝啬括号的使用 函数参数只在函数调用的时候求值一 次它的结果值传递给函数。表达式 的求值结果更容易预测 带有副作用的参数 参数可能被替换到宏的多个位置所以带有副作用的参数求值可能会产生不可预料的结果 函数参数只在传参的时候求值一次 结果更容易控制 参数类型 宏的参数与类型无关只要对参数的操作是合法的它就可以使用于任何参数类型 函数的参数是与类型有关的如果参 数的类型不同就需要不同的函数 即使他们执行的任务是相同的 调试宏是不支持调试的函数是可以调试的递归宏是不能递归的函数是可以递归的 7. 预处理操作符#和##的介绍
7.1. #的作用 #的作用使用 # 可以将一个宏参数变成对应的字符串。不过在这之前我们需要见一个东西 void Test11(void)
{printf(cowsay hello\n);// 下面打印的结果是什么呢printf(cowsay hello \n);
} 我们发现在printf中字符串是有自动连接的功能的。上面两个的打印其实是等价的。有了这个认识我们在来了解#的作用。 void Test12(void)
{int x 10;printf(the val of x is %d\n, x);int y 20;printf(the val of y is %d\n, y);
} 上面的两条打印是不是差异很小如果我想将其封装成一个函数呢即如果是x则打印上面的语句如果是y打印下面的语句。可是我们发现函数是很难做到这件事情的。例如 void print(int val)
{// 函数很难达到预期目的,中间这个val限制死了 printf(the val of val is %d\n, val);
} 但是我们的宏可以做到 //#val 的作用就是: 将这个宏参数不经过任何替换,直接把它转换为一个字符串,即 val
//此时如果宏参数是x,那么 #val 就相当于 x
//同理,如果宏参数是y,那么 #val 就相当于 y
//而我们之前说过,printf可以将多个字符串连接起来
#define PRINT(val) printf(the val of #val is %d\n,val);void Test13(void)
{int x 10;int y 20;PRINT(x);PRINT(y);
} 其实我们还可以这样玩上面的这个宏其实也很有限制它局限于打印整形但如果此时我想根据类型打印呢即你是整形我就用%d打印你是浮点型我就用%lf打印该如何实现呢 // 其中printf会将这五个字符串连接起来
// 此时我们就可以达到目的: 根据显示传递的打印格式,打印特定值
#define PRINT(val,format) printf(The val of #val is format \n,val);void Test14(void)
{int x 10;double PAI 3.14;PRINT(x,%d); PRINT(PAI, %lf);
} 总结#的作用可以把一个宏参数转换为对应的字符串 7.2. ##的作用 ##的作用可以将位于##两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。例如 void printhehe()
{printf(hehe\n);
}// 这个宏的作用: 将x和y合成为一个符号
#define CAT(x,y) x##yvoid Test15(void)
{// 此时CAT(print,hehe)的结果就是 printhehe// 而printfhehe是一个函数,调用这个函数CAT(print, hehe)();
} 注意这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。 例如 8. 命令约定 通常情况下宏名我们全部大写函数名不要全部大写。 9. 预处理指令 #undef #undef这条指令可以移除一个宏定义。例如 #define GETMEMORY(type,size) (type*)malloc(size)void Test17(void)
{int* ptr GETMEMORY(int, sizeof(int)*10);#undef GETMEMORY // 移除GETMEMORY这个宏int* str GETMEMORY(char, 5); // 此时就不认识这个GETMEMORY,编译报错int tmp 0;
}
10. 命令行定义 许多C的编译器提供了一种能力允许在命令行中定义符号。例如gcc编译器 #include stdio.hint main()
{int arr[sz] {0};for(size_t i 0; i sz; i){arr[i] i;}for(size_t i 0; i sz; i){printf(%d ,arr[i]);}printf(\n);
}可以看到上面的代码中的sz没有定义如果此时直接编译是会报错的。 但是我们可以利用命令行定义用于启动编译过程。例如 gcc -o my_test test.c -stdc99 -D sz10 //命令行定义 11. 条件编译 条件编译顾名思义满足条件就编译不满足条件就不编译。常见的条件编译指令如下 1. 格式如下 #if 常量表达式 如果符合条件,就进行编译 #endif 例如 void Test18(void)
{
#if 5printf(hehe\n); // 满足条件,进行编译
#endif#if 0printf(haha\n); // 不满足条件,不编译
#endif
} 注意哦每个#if都要有#endif与之配对。 2. 多个分支的条件编译格式如下 #if 常量表达式 //符合条件就编译,否则,不编译 #elif 常量表达式 //符合条件就编译,否则,不编译 #else //既不符合#if的常量表达式也不符合#elif的常量表达式那么就进行编译否则不编译 #endif 例如 #define NUM 1void Test19(void)
{
#if NUM 0printf(haha\n); // 当NUM 0时,进行编译
#elif NUM 1printf(hehe\n); // 当NUM 1时,进行编译
#elseprintf(heihei\n); // 当NUM ! 0 NUM ! 1时,进行编译
#endif // 最后要以#endif 结束
} 3. 判断是否被定义格式如下 #if defined(symbol) // 如果symbol被定义了那么编译否则不编译 #endif #ifdef symbol // 如果symbol被定义了那么编译否则不编译 #endif #if !defined(symbol) // 如果symbol没定义那么编译否则不编译 #endif #ifndef symbol // 如果symbol没定义那么编译否则不编译 #endif 例如 #define BLUESKYvoid Test20(void)
{
#if defined(BLUESKY)printf(hehe\n); // 如果BLUESKY定义了,那么编译
#endif#ifdef BLUESKYprintf(heihei\n); //如果BLUESKY定义了,那么编译
#endif#if !defined(BLUESKY)printf(haha\n); // 如果BLUESKY没定义,那么编译
#endif#ifndef BLUESKYprintf(xixi\n); // 如果BLUESKY没定义,那么编译
#endif
} 4. 嵌套定义 具体演示如下 #define PLANT
#define FLOWERvoid Test21(void)
{
#ifdef PLANT // 如果定义了PLANT,则编译#ifdef FLOWER // 如果定义了FLOWER,则编译printf(rose\n);#endif#ifdef GRASS // 如果定义了GRASS,则编译printf(green grass\n);#endif#elif defined(ANIMAL) // 如果定义了ANIMAL,则编译#ifdef LION // 如果定义了LION,则编译printf(lion\n);#endif
#endif
} 条件编译的运用非常广泛尤其是一些库实现。 12. 文件包含 12.1. 头文件被包含的方式
1. 本地文件被包含的方式
#include filename 本地文件的查找策略 第一步会先在源文件所在的目录下进行查找找到了就结束。如果没找到那么进行第二步。 第二步第二次查找编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。 // Linux下的标准库文件搜索路径:
/usr/include
//vs2013环境的标准头文件的路径:
E:\vs2013\Microsoft\VC\include
2. 库文件被包含的方式
#include filename 查找库的头文件直接会去标准路径下去查找如果找不到就提示编译错误。 对比这两种包含方式我们发现其实库文件也可以用 #include filename去查找但是会导致效率下降因为它首先会在源文件所在目录进行查找找不到然后会去标准路径下去查找不仅如此此时也不容易区分到底是本地文件还是库文件了因此我们还是建议区分开来本地文件就用filename的形式库文件就用filename的方式。 12.2. 嵌套文件包含 在以后的编写代码的过程中难免会出现头文件重复出现的情况。例如下面的情况 // add.h
int add(int x,int y)
{return x y;
}
// test.c
#include add.h
#include add.h
#include add.hint main()
{int a 10;int b 20;int c add(a,b);return 0;
}
// 我们看一下预处理后的结果
gcc -E test.c -o test.i 在预处理阶段头文件会被展开 如果此时相同的头文件头有多份并且假设每份的头文件代码量很多这样就会造成文件内容的大量重复如何解决这个问题呢 这时候我们的条件编译就派上用场了如下 // 方案一:
#ifndef __ADD_H_ // 如果没有定义 __ADD_H_,那么编译下列代码
#define __ADD_H_ // 定义__ADD_H_
int add(int x,int y)
{return x y;
}
#endif// 上面只是一种方式,或者可以用 #pragma once
// 方案二:
#pragma once
int add(int x,int y)
{return x y;
}// 上面的两种方式,就可以保证一份源文件中只会有一份这个头文件
//重新查看预处理后的结果
gcc -E test.c -o test.i 此时就只有一份add.h的内容了这就是防止头文件的重复引入。