当前位置: 首页 > news >正文

丽水建设部门网站营销网站设计公司招聘

丽水建设部门网站,营销网站设计公司招聘,微信公众号做留言网站,站长工具seo综合查询方法文章目录 第14章 预处理器14.1 预处理器的工作原理14.2 预处理指令14.3 宏定义14.3.1 简单的宏14.3.2 带参数的宏14.3.3 #运算符14.3.4 ##运算符14.3.5 宏的通用属性14.3.6 宏定义中的圆括号14.3.7 创建较长的宏14.3.8 预定义宏14.3.9 C99中新增的预定义宏14.3.10 空的宏参数(C… 文章目录 第14章 预处理器14.1 预处理器的工作原理14.2 预处理指令14.3 宏定义14.3.1 简单的宏14.3.2 带参数的宏14.3.3 #运算符14.3.4 ##运算符14.3.5 宏的通用属性14.3.6 宏定义中的圆括号14.3.7 创建较长的宏14.3.8 预定义宏14.3.9 C99中新增的预定义宏14.3.10 空的宏参数(C99)14.3.11 参数个数可变的宏(C99)14.3.12 __func__标识符(C99) 14.4 条件编译14.4.1 #if指令和#endif指令14.4.2 defined运算符14.4.3 #ifdef指令和#ifndef指令14.4.4 #elif指令和#else指令14.4.5 使用条件编译 14.5 其他指令14.5.1 #error指令14.5.2 #line指令14.5.3 #pragma指令14.5.4 _Pragma运算符(C99) 问与答写在最后 第14章 预处理器 ——总有一些事用什么语言都不好表达而我们希望能通过程序把它表达出来。 前面的几章用到过#define与#include指令但没有深入讨论。这些指令以及我们还没有学到的指令都是由预处理器处理的。预处理器是一个小软件它可以在编译前处理C程序。C语言和C语言因为依赖预处理器而不同于其他的编程语言。 预处理器是一种强大的工具但它同时也可能是许多难以发现的错误的根源。此外预处理器也可能被错误地用来编写出一些几乎不可能读懂的程序。尽管有些C程序员十分依赖于预处理器我依然建议适度地使用它就像生活中的其他许多事物一样。 本章首先描述预处理器的工作原理14.1节并且给出一些会影响预处理指令14.2节的通用规则。14.3节和14.4节介绍预处理器最主要的两种能力宏定义和条件编译。而处理器另外一个主要功能即文件包含将留到第15章再详细介绍。14.5节讨论较少用到的预处理指令#error、#line和#pragma。 14.1 预处理器的工作原理 预处理器的行为是由预处理指令由#字符开头的一些命令控制的。我们已经在前面的章节中遇见过其中两种指令即#define和#include。 #define指令定义了一个宏——用来代表其他东西的一个名字例如常量或常用的表达式。预处理器会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时预处理器“扩展”宏将宏替换为其定义内容。 #include指令告诉预处理器打开一个特定的文件将它的内容作为正在编译的文件的一部分“包含”进来。例如代码行#include stdio.h指示预处理器打开一个名为stdio.h的文件并将它的内容加到当前的程序中。stdio.h包含了C语言标准输入/输出函数的原型。 预处理器在编译过程中的作用预处理器的输入是一个C语言程序程序可能包含指令。预处理器会执行这些指令并在处理过程中删除这些指令。预处理器的输出是另一个C程序原程序编辑后的版本不再包含指令。预处理器的输出被直接交给编译器编译器检查程序是否有错误并将程序翻译为目标代码机器指令。 可以通过一个程序来展现预处理器的作用 //预处理之前 #include stdio.h #define FREEZING_PT 32.0f #define SCALE_FACTOR (5.0f / 9.0f) int main(void) { float fahrenheit, celsius; printf(Enter Fahrenheit temperature: ); scanf(%f, fahrenheit); celsius (fahrenheit - FREEZING_PT) * SCALE_FACTOR; printf(Celsius equivalent is: %.1f\n, celsius); return 0; } /*********************************************************///预处理结束后 空行 空行 从stdio.h中引入的行 空行 空行 空行 空行 int main(void) { float fahrenheit, celsius; printf(Enter Fahrenheit temperature: ); scanf(%f, fahrenheit); celsius (fahrenheit - 32.0f) * (5.0f / 9.0f); printf(Celsius equivalent is: %.1f\n, celsius); return 0; }预处理器通过引入stdio.h的内容来响应#include指令。预处理器也删除了#define指令并且替换了该文件中稍后出现在任何位置上的FREEZING_PT和SCALE_FACTOR。请注意预处理器并没有删除包含指令的行而是简单地将它们替换为空。 正如这个例子所展示的那样预处理器不仅仅执行了指令还做了一些其他的事情。特别值得注意的是它将每一处注释都替换为一个空格字符。有一些预处理器还会进一步删除不必要的空白字符包括每一行开始用于缩进的空格符和制表符。 在C语言较早的时期预处理器是一个单独的程序它的输出提供给编译器。如今预处理器通常和编译器集成在一起而且其输出也不一定全是C代码例如包含stdio.h之类的标准头使得我们可以在程序中使用相应头中的函数而不需要把头的内容复制到程序的源代码中。然而将预处理器和编译器看作不同的程序仍然是有用的。实际上大部分C编译器提供了一种方法使用户可以看到预处理器的输出。在指定某个特定的选项GCC 用的是-E时编译器会产生预处理器的输出。其他一些编译器会提供一个类似于集成的预处理器的独立程序。要了解更多的信息可以查看你使用的编译器的文档。 注意!!预处理器仅知道少量C语言的规则。因此它在执行指令时非常有可能产生非法的程序。经常是原始程序看起来没问题使错误查找起来很难。对于较复杂的程序检查预处理器的输出可能是找到这类错误的有效途径。 14.2 预处理指令 大多数预处理指令属于下面3种类型之一: 宏定义。#define指令定义一个宏#undef指令删除一个宏定义。文件包含。#include指令导致一个指定文件的内容被包含到程序中。条件编译。#if、#ifdef、#ifndef、#elif、#else和#endif指令能根据预处理器可以测试的条件来确定是将一段文本块包含到程序中还是将其排除在程序之外。 剩下的#error、#line和#pragma指令是更特殊的指令较少用到。本章将深入研究预处理指令。唯一一个不会在这里详细讨论的指令是#include这个指令将在第15章介绍。 下面的规则适用于所有指令 指令都以#开始。#符号不需要出现在一行的行首只要在它之前只有空白字符就行。在#后是指令名接着是指令所需要的其他信息。在指令的符号之间可以插入任意数量的空格或水平制表符。例如下面的指令是合法的 # define N 100指令总是在第一个换行符处结束除非明确地指明要延续。如果想在下一行延续指令我们必须在当前行的末尾使用\字符。例如下面的指令定义了一个宏来表示硬盘的容量按字节计算 #define DISK_CAPACITY (SIDES * \ TRACKS_PER_SIDE * \ SECTORS_PER_TRACK * \ BYTES_PER_SECTOR)指令可以出现在程序中的任何地方。但我们通常将#define和#include指令放在文件的开始其他指令则放在后面甚至可以放在函数定义的中间。注释可以与指令放在同一行。实际上在宏定义的后面加一个注释来解释宏的含义是一种比较好的习惯 #define FREEZING_PT 32.0f /* freezing point of water */ 14.3 宏定义 从第2章开始使用的宏被称为简单的宏它们没有参数。预编译器还支持带参数的宏。本节先讨论简单的宏然后再讨论带参数的宏。在分别讨论它们之后我们会研究一下二者共同的特性。 14.3.1 简单的宏 简单的宏C标准中称为对象式宏的定义有如下格式: #define 标识符 替换列表替换列表是一系列的预处理记号本章中提及“记号”时均指“预处理记号”。 宏的替换列表可以包括标识符、关键字、数值常量、字符常量、字面串、运算符和标点符号。当预处理器遇到一个宏定义时会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中不管标识符在哪里出现预处理器都会用替换列表代替它。 请注意!!不要在宏定义中放置任何额外的符号否则它们会被当作替换列表的一部分。一种常见的错误是在宏定义中使用 #define N 100 /*** WRONG ***/ ... int a[N]; /* becomes int a[ 100]; */ 在上面的例子中我们错误地把N定义成两个记号和100。 在宏定义的末尾使用分号是另一个常见错误 #define N 100; /*** WRONG ***/ ... int a[N]; /* becomes int a[100;]; */这里N被定义为100和;两个记号。 编译器可以检测到宏定义中绝大多数由多余符号所导致的错误。但是编译器只会将每一个使用这个宏的地方标为错误而不会直接找到错误的根源——宏定义本身因为宏定义已经被预处理器删除了。 简单的宏主要用来定义那些被Kernighan和Ritchie称为“明示常量”manifest constant的东西。 我们可以使用宏给数值、字符值和字符串值命名。 #define STE_LEN 80 #define TRUE 1 #define FALSE 0 #define PI 3.14159 #define CR \r #define EOS \0 #define MEM_ERR Error: not enough memory 使用#define来为常量命名有许多显著的优点: 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则程序将包含大量的“魔法数”很容易迷惑读者。程序会更易于修改。我们仅需要改变一个宏定义就可以改变整个程序中出现的所有该常量的值。“硬编码”的常量会更难于修改特别是当它们以稍微不同的形式出现时。例如如果程序包含一个长度为100的数组它可能会包含一个0~99的循环。如果我们只是试图找到程序中出现的所有100那么就会漏掉99。可以帮助避免前后不一致或键盘输入错误。假如数值常量3.14159在程序中大量出现它可能会被意外地写成3.1416或3.14195。 虽然简单的宏常用于定义常量名但是它们还有其他应用: 可以对C语法做小的修改。我们可以通过定义宏的方式给C语言符号添加别名从而改变C语言的语法。例如对于习惯使用Pascal的begin和end而不是C语言的{和}的程序员可以定义下面的宏 #define BEGIN { #define END }我们甚至可以发明自己的语言。例如我们可以创建一个LOOP“语句”来实现一个无限循环 #define LOOP for (;;)当然改变C语言的语法通常不是个好主意因为它会使程序很难被其他程序员理解。 对类型重命名。在5.2节中我们通过重命名int创建了一个布尔类型 #define BOOL int虽然有些程序员会使用宏定义的方式来实现此目的但类型定义typedef7.5节仍然是定义新类型的最佳方法。 控制条件编译。如将在14.4节中看到的那样宏在控制条件编译中起到了重要的作用。例如在程序中出现的下面这行宏定义可能表明需要将程序在“调试模式”下进行编译并使用额外的语句输出调试信息 #define DEBUG 这里顺便提一下如上面的例子所示宏定义中的替换列表为空是合法的。 当宏作为常量使用时C程序员习惯在名字中只使用大写字母。但是并没有如何将用于其他目的的宏大写的统一做法。由于宏特别是带参数的宏可能是程序中错误的来源一些程序员更喜欢全部使用大写字母来引起注意而有些人则倾向于小写。 14.3.2 带参数的宏 带参数的宏也称为函数式宏的定义有如下格式 #define 标识符(x1, x2, ..., xn) 替换列表其中x1, x2,..., xn是标识符宏的参数。这些参数可以在替换列表中根据需要出现任意次。 请注意!!在宏的名字和左括号之间必须没有空格。如果有空格预处理器会认为在定义一个简单的宏其中(x1, x2, ..., xn)是替换列表的一部分。 当预处理器遇到带参数的宏时会将宏定义存储起来以便后面使用。在后面的程序中如果任何地方出现了标识符(y1, y2, ..., yn)格式的宏调用其中y1, y2, ..., yn是一系列记号预处理器会使用替换列表替代——使用y1替换x1y2替换x2以此类推。 例如假设我们定义了如下的宏 #define MAX(x,y) ((x)(y)?(x):(y)) #define IS_EVEN(n) ((n)%20)宏定义中的圆括号似乎过多但本节后面将看到这样做是有原因的。现在如果后面的程序中有如下语句 i MAX(jk, m-n); if (IS_EVEN(i)) i;//预处理器会将这些行替换为 i ((jk)(m-n)?(jk):(m-n)); if (((i)%20)) i;如这个例子所示带参数的宏经常用作简单的函数。MAX类似一个从两个值中选取较大值的函数IS_EVEN则类似一种当参数为偶数时返回1否则返回0的函数。 下面的宏也类似函数但更为复杂 #define TOUPPER(c) (a(c)(c)z?(c)-aA:(c)) 这个宏检测字符c是否在a与z之间。如果在的话这个宏会用c的值减去a再加上A从而计算出c所对应的大写字母。如果c不在这个范围就保留原来的c。ctype.h头文件23.5节中提供了一个类似的函数toupper它的可移植性更好。 带参数的宏可以包含空的参数列表如下例所示 #define getchar() getc(stdin) 空的参数列表不是必需的但这样可以使getchar更像一个函数。没错这就是stdio.h中的getchar。我们将在22.4节中看到getchar经常实现为宏也经常实现为函数。 使用带参数的宏替代真正的函数有2个优点: 程序可能会稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、复制参数的值等而调用宏则没有这些运行开销。 注意C99的内联函数18.6节为我们提供了一种不使用宏而避免这一开销的办法。宏更“通用”。与函数的参数不同宏的参数没有类型。因此只要预处理后的程序依然是合法的宏可以接受任何类型的参数。例如我们可以使用MAX宏从两个数中选出较大的一个数的类型可以是int、long、float、double等。 但是带参数的宏也有一些缺点 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表由此导致程序的源代码增加因此编译后的代码变大。宏使用得越频繁这种效果就越明显。当宏调用嵌套时这个问题会相互叠加从而使程序更加复杂。思考一下如果我们用MAX宏来找出3个数中最大的数会怎样 n MAX(i, MAX(j, k));//下面是预处理后的语句 n ((i)(((j)(k)?(j):(k)))?(i):(((j)(k)?(j):(k)))); 宏参数没有类型检查。当一个函数被调用时编译器会检查每一个参数来确认它们是否是正确的类型。如果不是要么将参数转换成正确的类型要么由编译器产生一条出错消息。预处理器不会检查宏参数的类型也不会进行类型转换。 无法用一个指针来指向一个宏。如在17.7节中将看到的C语言允许指针指向函数这在特定的编程条件下非常有用。宏会在预处理过程中被删除所以不存在类似的“指向宏的指针”。因此宏不能用于处理这些情况。 宏可能会不止一次地计算它的参数。函数对它的参数只会计算一次而宏可能会计算两次甚至更多次。如果参数有副作用多次计算参数的值可能会产生不可预知的结果。考虑下面的例子其中MAX的一个参数有副作用 n MAX(i, j);//预处理之后 n ((i)(j)?(i):(j)); 如果i大于j那么i可能会被错误地增加两次同时n可能被赋予错误的值。 请注意!!由于多次计算宏的参数而导致的错误可能非常难于发现这是因为宏调用和函数调用看起来是一样的。更糟糕的是这类宏可能在大多数情况下可以正常工作仅在特定参数有副作用时失效。为了自我保护最好避免使用带有副作用的参数。 带参数的宏不仅适用于模拟函数调用还经常用作需要重复书写的代码段模式。如果我们已经写烦了语句 printf(%d\n, i); 这是因为每次要显示一个整数i都要使用它。我们可以定义下面的宏使显示整数变得简单些 #define PRINT_INT(n) printf(%d\n, n)一旦定义了PRINT_INT预处理器会将 PRINT_INT(i/j); //预处理之后转换为 printf(%d\n, i/j);14.3.3 #运算符 宏定义可以包含两个专用的运算符#和##。编译器不会识别这两种运算符它们会在预处理时被执行。 #运算符将宏的一个参数转换为字面串。它仅允许出现在带参数的宏的替换列表中。#运算符所执行的操作可以理解为“串化”stringization这个词你在字典里肯定看不到。 #运算符有许多用途这里只来讨论其中的一种。假设我们决定在调试过程中使用PRINT_INT宏作为一个便捷的方法来输出整型变量或表达式的值。#运算符可以使PRINT_INT为每个输出的值添加标签。下面是改进后的PRINT_INT #define PRINT_INT(n) printf(#n %d\n, n)n之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字面串。因此调用 PRINT_INT(i/j); 会变为 printf(i/j %d\n, i/j); //C语言中相邻的字面串会被合并。因此上边的语句等价于 printf(i/j %d\n, i/j);当程序执行时printf函数会同时显示表达式i/j和它的值。例如如果i是11j是2的话输出为i/j 5。 14.3.4 ##运算符 ##运算符可以将两个记号如标识符“粘合”在一起成为一个记号。无须惊讶##运算符被称为“记号粘合”。如果其中一个操作数是宏参数“粘合”会在形式参数被相应的实际参数替换后发生。考虑下面的宏 #define MK_ID(n) i##n 当MK_ID被调用时比如MK_ID(1)预处理器首先使用实际参数这个例子中是1替换形式参数n。接着预处理器将i和1合并为一个记号i1。下面的声明使用MK_ID创建了3个标识符 int MK_ID(1), MK_ID(2), MK_ID(3); 预处理后这一声明变为: int i1, i2, i3;##运算符不属于预处理器最经常使用的特性。实际上想找到一些使用它的情况是比较困难的。为了找到一个有实际意义的##的应用我们来重新思考前面提到过的MAX宏。如我们所见当MAX的参数有副作用时会无法正常工作。一种解决方法是用MAX宏来写一个max函数。遗憾的是仅一个max函数是不够的我们可能需要一个实际参数是int值的max函数、一个实际参数为float值的max函数等等。除了实际参数的类型和返回值的类型之外这些函数都一样。因此这样定义每一个函数似乎是个很蠢的做法。 解决的办法是定义一个宏并使它展开后成为max函数的定义。宏只有一个参数type表示实际参数和返回值的类型。这里还有个问题如果我们用宏来创建多个max函数程序将无法编译。C语言不允许在同一文件中出现两个同名的函数。为了解决这个问题我们用##运算符为每个版本的max函数构造不同的名字。下面是宏的形式 #define GENERIC_MAX(type) \ type type##_max(type x, type y) \ { \ return x y ? x : y; \ }注意宏的定义中是如何将type和_max相连来形成新函数名的。 现在假如我们需要一个针对float值的max函数。下面是使用GENERIC_MAX宏来定义这一函数的方法 GENERIC_MAX(float)预处理器会将这行代码展开如下 float float_max(float x, float y) { return x y ? x : y; } 14.3.5 宏的通用属性 我们已经讨论过了简单的宏和带参数的宏现在来看一下它们都需要遵守的规则: 宏的替换列表可以包含对其他宏的调用。例如我们可以用宏PI来定义宏TWO_PI #define PI 3.14159 #define TWO_PI (2*PI)当预处理器在后面的程序中遇到TWO_PI时会将它替换成(2*PI)。接着预处理器会重新检查替换列表看它是否包含其他宏的调用在这个例子中调用了宏PI。预处理器会不断重新检查替换列表直到将所有的宏名字都替换完为止。 预处理器只会替换完整的记号而不会替换记号的片段。因此预处理器会忽略嵌在标识符、字符常量、字面串之中的宏名。例如假设程序含有如下代码行 #define SIZE 256 int BUFFER_SIZE; if (BUFFER_SIZE SIZE) puts(Error SIZE exceeded); 预处理后这些代码行会变为 int BUFFER_SIZE; if (BUFFER_SIZE 256) puts(Error: SIZE exceeded); 尽管标识符BUFFER_SIZE和字符串Error: SIZE exceeded都包含SIZE但是它们没有被预处理影响。 宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的它们不遵从通常的作用域规则。定义在函数中的宏并不是仅在函数内起作用而是作用到文件末尾。 宏不可以被定义两遍除非新的定义与旧的定义是一样的。小的间隔上的差异是被允许的但是宏的替换列表和参数如果有的话中的记号必须都一致。 宏可以使用#undef指令“取消定义”。#undef指令有如下形式: #undef 标识符//比如 #undef N会删除宏N当前的定义。如果N没有被定义成一个宏则#undef指令没有任何作用。#undef指令的一个用途是取消宏的现有定义以便于重新给出新的定义。 14.3.6 宏定义中的圆括号 在前面定义的宏的替换列表中有大量的圆括号。确实需要它们吗答案是绝对需要。如果我们少用几个圆括号宏有时可能会得到意想不到的而且是不希望有的结果。 至于在一个宏定义中哪里要加圆括号有2条规则要遵守: 首先如果宏的替换列表中有运算符那么始终要将替换列表放在括号中 #define TWO_PI (2*3.14159)。其次如果宏有参数每个参数每次在替换列表中出现时都要放在圆括号中#define SCALE(x) ((x)*10)没有括号的话我们将无法确保编译器会将替换列表和参数作为完整的表达式。编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。 为了展示为替换列表添加圆括号的重要性考虑下面的宏定义其中的替换列表没有添加圆括号 #define TWO_PI 2*3.14159 /* 需要给替换列表加圆括号 *///在预处理时语句 conversion_factor 360/TWO_PI; //会变成下面这样 conversion_factor 360/2*3.14159;除法会在乘法之前执行产生的结果并不是期望的结果。 当宏有参数时仅给替换列表添加圆括号是不够的。参数的每一次出现都要添加圆括号。例如假设SCALE定义如下 #define SCALE(x) (x*10) /* 需要给x添加括号 */ //假设有语句 j SCALE(i1); //预处理过程中语句会变成下面这样 j (i1*10); //由于乘法的优先级比加法高该语句等价于 j i10; //然而我们希望的是 j (i1)*10;请注意!!在宏定义中缺少圆括号会导致C语言中最让人讨厌的错误。程序通常仍然可以编译通过而且宏似乎也可以工作仅在少数情况下会出错。 14.3.7 创建较长的宏 在创建较长的宏时逗号运算符会十分有用。特别是可以使用逗号运算符来使替换列表包含一系列表达式。例如下面的宏会读入一个字符串再把字符串显示出来 #define ECHO(s) (gets(s), puts(s)) gets函数和puts函数的调用都是表达式因此使用逗号运算符连接它们是合法的。我们甚至可以把ECHO宏当作一个函数来使用 ECHO(str); /* 替换为 (gets(str), puts(str)); */ 如果不想在ECHO的定义中使用逗号运算符我们还可以将gets函数和puts函数的调用放在花括号中形成复合语句 #define ECHO(s) { gets(s); puts(s); } 遗憾的是这种方式并未奏效。假如我们将ECHO宏用于下面的if语句 if (echo_flag) ECHO(str); else gets(str); //将ECHO宏替换会得到下面的结果 if (echo_flag) { gets(str); puts(str); }; else gets(str); 编译器会将头两行作为完整的if语句 if (echo_flag) { gets(str); puts(str); } 编译器会将跟在后面的分号作为空语句并且对else子句抛出出错消息因为它不属于任何if语句。记住永远不要在ECHO宏后面加分号这样做就可以解决这个问题。但是这样做会使程序看起来有些怪异。 逗号运算符可以解决ECHO宏的问题但并不能解决所有宏的问题。假如一个宏需要包含一系列的语句而不仅仅是一系列的表达式这时逗号运算符就起不了作用了因为它只能连接表达式不能连接语句。解决的方法是将语句放在do循环中并将条件设置为假因此语句只会执行一次 do { ... } while (0)注意这个do语句是不完整的——后面还缺一个分号。为了看到这个技巧嗯应该说是技术的实际作用将它用于ECHO宏中 #define ECHO(s) \ do { \ gets(s); \ puts(s); \ } while (0) 当使用ECHO宏时一定要加分号以使do语句完整 ECHO(str); /* becomes do { gets(str); puts(str); } while (0); */ 14.3.8 预定义宏 C语言有一些预定义宏每个宏表示一个整型常量或字面串。如表14-1所示这些宏提供了当前编译或编译器本身的信息。 表14-1 预定义宏 名字描述__LINE__当前宏所在行的行号__FILE__当前文件的名字__DATE__编译的日期格式mm dd yyyy__TIME__编译的时间格式hh:mm:ss__STDC__如果编译器符合C标准C89或C99那么值为1 __DATE__宏和__TIME__宏指明程序编译的时间。例如假设程序以下面的语句开始 printf(Wacky Windows (c) 2010 Wacky Software, Inc.\n); printf(Compiled on %s at %s\n, __DATE__, __TIME__); 每次程序开始执行时程序都会显示如下的两行内容 Wacky Windows (c) 2010 Wacky Software, Inc. Compiled on Oct 23 2023 at 17:12:25这样的信息可以帮助区分同一个程序的不同版本。 我们可以使用__LINE__宏和__FILE__宏来找到错误。考虑被零除的定位问题。当C程序因为被零除而导致终止时通常没有信息指明哪条除法运算导致错误。下面的宏可以帮助我们查明错误的根源 #define CHECK_ZERO(divisor) \ if (divisor 0) \ printf(*** Attempt to divide by zero on line %d \ of file %s ***\n, __LINE__, __FILE__) //CHECK_ZERO宏应该在除法运算前被调用 CHECK_ZERO(j); k i / j;//如果j是0会显示出如下形式的信息 /* output: *** Attempt to divide by zero on line 9 of file foo.c *** */类似这样的错误检测的宏非常有用。实际上C语言库提供了一个通用的、用于错误检测的宏——assert宏(24.1节)。 如果编译器符合C标准C89或C99__STDC__宏存在且值为1。通过让预处理器测试这个宏程序可以在早于C89标准的编译器下编译通过14.4节会给出一个例子。 14.3.9 C99中新增的预定义宏 表14-2 C99中新增的预定义宏 名字描述__STDC_HOSTED__如果是托管式实现则值为1如果是独立式实现则值为0__STDC_VERSION__支持的C标准版本__STDC_IEC_559__如果支持IEC 60559浮点算术运算则值为1__STDC_IEC_559_COMPLEX__如果支持IEC 60559复数算术运算则值为1__STDC_ISO_10646__被定义为yyyymmL形式的整型常量意味着可以用wchar_t类型来存储ISO 10646标准所定义的以及在指定年月所修订和补充的Unicode字符 要了解__STDC_HOSTED__的意义需要介绍些新的名词。C的实现implementation包括编译器和执行C程序所需要的其他软件。C99将实现分为两种托管式hosted和独立式freestanding。托管式实现hosted implementation能够接受任何符合C99标准的程序而独立式实现freestanding implementation除了几个最基本的以外不一定要能够编译使用复数类型27.3节或标准头的程序。特别是独立式实现不需要支持stdio.h头。如果编译器是托管式实现则__STDC_HOSTED__宏代表常数1否则值为0。 __STDC_VERSION__宏为我们提供了一种查看编译器所识别出的C标准版本的方法。这个宏第一次出现在C89标准的Amendment 1中该文档指明宏的值为长整型常量199409L代表修订的年月。如果编译器符合C99标准其值为199901L。对于标准的每一个后续版本以及每一次后续修订宏的值都有所变化。 C99编译器可能也可能没有另外定义以下3种宏。仅当编译器满足特定条件时才会定义相应的宏: 如果编译器根据IEC 60559标准IEEE 754标准7.2节的别名执行浮点算术运算则定义__STDC_IEC_559__宏且其值为1。如果编译器根据IEC 60559标准执行复数算术运算则定义__STDC_IEC_559_COMPLEX__宏且其值为1。__STDC_ISO__10646__定义为yyyymmL格式如199712L的整型常量前提是wchar_t类型25.2节的值由ISO/IEC 10646标准包括指定年月的修订版本25.2节中的码值表示。 14.3.10 空的宏参数(C99) C99允许宏调用中的任意或所有参数为空。当然这样的调用需要有和一般调用一样多的逗号这样容易看出哪些参数被省略了。 在大多数情况下实际参数为空的效果是显而易见的。如果替换列表中出现相应的形式参数名那么只要在替换列表中不出现实际参数即可不需要替换。例如 #define ADD(x,y) (xy)//经过预处理之后语句 i ADD(j,k); //变成 i (jk);//而赋值语句 i ADD(,k); //则变成 i (k);当空参数是#或##运算符的操作数时其用法有特殊规定。如果空的实际参数被#运算符“串化”则结果为空字符串 #define MK_STR(x) #x ... char empty_string[] MK_STR(); //预处理之后上面的声明变成 char empty_string[] ;如果##运算符之后的一个实际参数为空它将被不可见的“位置标记”记号代替。把原始的记号与位置标记记号相连接得到的还是原始的记号位置标记记号消失了。如果连接两个位置标记记号得到的是一个位置标记记号。宏扩展完成后位置标记记号从程序中消失。考虑下面的例子 #define JOIN(x,y,z) x##y##z ... int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c); //预处理之后声明变成 int abc, ab, ac, c;漏掉的参数由位置标记记号代替这些记号在与非空参数相连接之后消失。JOIN宏的3个参数可以同时为空这样得到的结果为空。 14.3.11 参数个数可变的宏(C99) 在C89中如果宏有参数那么参数的个数是固定的。在C99中这个条件被适当放宽了允许宏具有可变长度的参数列表26.1节。这个特性对于函数来说早就有了所以应用于宏也不足为奇。 宏具有可变参数个数的主要原因是它可以将参数传递给具有可变参数个数的函数如printf和scanf。下面给出几个例子 #define TEST(condition, ...) ((condition)? \ printf(Passed test: %s\n, #condition): \ printf(__VA_ARGS__)) ...记号省略号出现在宏参数列表的最后前面是普通参数。__VA_ARGS__是一个专用的标识符只能出现在具有可变参数个数的宏的替换列表中代表所有与省略号相对应的参数。至少有一个与省略号相对应的参数但该参数可以为空。宏TEST至少要有两个参数第一个参数匹配condition剩下的参数匹配省略号。 下面这个例子说明了TEST的使用方法 TEST(voltage max_voltage, Voltage %d exceeds %d\n, voltage, max_voltage); //预处理器将产生如下的输出重排格式以增强可读性 ((voltage max_voltage)? printf(Passed test: %s\n, voltage max_voltage): printf(Voltage %d exceeds %d\n, voltage, max_voltage)); 如果voltage不大于max_voltage程序执行时将显示如下消息 Passed test: voltage max_voltage否则将分别显示voltage和max_voltage的值 voltage 125 exceeds 12014.3.12 __func__标识符(C99) C99的另一个新特性是__func__标识符。__func__与预处理器无关所以实际上与本章内容不相关。但是与许多预处理特性一样它也有助于调试所以在这里一并讨论。 每一个函数都可以访问__func__标识符它的行为很像一个存储当前正在执行的函数的名字的字符串变量。其作用相当于在函数体的一开始包含如下声明 static const char __func__[] function-name; 其中function-name是函数名。这个标识符的存在使得我们可以写出如下的调试宏 #define FUNCTION_CALLED() printf(%s called\n, __func__); #define FUNCTION_RETURNS() printf(%s returns\n, __func__); 对这些宏的调用可以放在函数体中以跟踪函数的调用 void f(void) { FUNCTION_CALLED(); /* displays f called */ ... FUNCTION_RETURNS(); /* displays f returns */ }__func__的另一个用法作为参数传递给函数让函数知道调用它的函数的名字。 14.4 条件编译 C语言的预处理器可以识别大量用于支持条件编译的指令。条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片段。 14.4.1 #if指令和#endif指令 假如我们正在调试一个程序。我们想要程序显示出特定变量的值因此将 printf函数调用添加到程序中重要的部分。一旦找到错误建议保留这些 printf函数调用以备后用。条件编译允许我们保留这些调用但是让编译器忽略它们。 下面是我们需要采取的方式。首先定义一个宏并给它一个非零的值 #define DEBUG 1宏的名字并不重要。接下来我们要在每组printf函数调用的前后加上#if和#endif #if DEBUG printf(Value of i: %d\n, i); printf(Value of j: %d\n, j); #endif在预处理过程中#if指令会测试DEBUG的值。由于DEBUG的值不是0因此预处理器会将这两个printf函数调用保留在程序中但#if和#endif行会消失。如果我们将DEBUG的值改为0并重新编译程序预处理器则会将这4行代码都删除。编译器不会看到这些printf函数调用所以这些调用就不会在目标代码中占用空间也不会在程序运行时消耗时间。我们可以将#if-#endif保留在最终的程序中这样如果程序在运行时出现问题可以通过将DEBUG改为1并重新编译来继续产生诊断信息。 一般来说#if指令的格式如下 #if 常量表达式#endif指令则更简单 #endif当预处理器遇到#if指令时会计算常量表达式的值。如果表达式的值为0那么#if与#endif之间的行将在预处理过程中从程序中删除否则#if和#endif之间的行会被保留在程序中继续留给编译器处理——这时#if和#endif对程序没有任何影响。 请注意#if指令会把没有定义过的标识符当作值为0的宏对待。因此如果省略DEBUG的定义测试 #if DEBUG会失败但不会产生出错消息而测试 #if !DEBUG 会成功。 14.4.2 defined运算符 14.3节中介绍过运算符#和##还有一个专用于预处理器的运算符——defined。当defined应用于标识符时如果标识符是一个定义过的宏则返回1否则返回0。defined运算符通常与#if指令结合使用可以这样写 #if defined(DEBUG) ... #endif仅当DEBUG被定义成宏时#if和#endif之间的代码会被保留在程序中。DEBUG两侧的括号不是必需的因此可以简单地写成 #if defined DEBUG因为defined运算符仅检测DEBUG是否有定义所以不需要给DEBUG赋值 #define DEBUG14.4.3 #ifdef指令和#ifndef指令 #ifdef指令测试一个标识符是否已经定义为宏 #ifdef 标识符#ifdef指令的使用与#if指令类似 #ifdef 标识符 当标识符被定义为宏时需要包含的代码 #endif严格地说并不需要#ifdef因为可以结合#if指令和defined运算符来得到相同的效果。换言之指令 #ifdef 标识符等价于 #if defined(标识符)#ifndef指令与#ifdef指令类似但测试的是标识符是否没有被定义为宏 #ifndef 标识符//等价于指令 #if !defined(标识符)14.4.4 #elif指令和#else指令 #if指令、#ifdef指令和#ifndef指令可以像普通的if语句那样嵌套使用。当发生嵌套时最好随着嵌套层次的增加而增加缩进。一些程序员对每一个#endif都加注释来指明对应的#if指令测试哪个条件 #if DEBUG ... #endif /* DEBUG */ 这种方法有助于更方便地找到#if指令的起始位置。 为了提供更多的便利预处理器还支持#elif和#else指令 #elif 常量表达式#else#elif指令和#else指令可以与#if指令、#ifdef指令和#ifndef指令结合使用来测试一系列条件: #if 表达式1 当表达式1非0时需要包含的代码 #elif 表达式2 当表达式1为0但表达式2非0时需要包含的代码 #else 其他情况下需要包含的代码 #endif 虽然上面的例子使用了#if指令但#ifdef指令或#ifndef指令也可以这样使用。在#if指令和#endif指令之间可以有任意多个#elif指令但最多只能有一个#else指令。 14.4.5 使用条件编译 条件编译对于调试是非常方便的但它的应用并不仅限于此。下面是其他一些常见的应用 编写在多台机器或多种操作系统之间可移植的程序。下面的例子中会根据WIN32、MAC_OS或LINUX是否被定义为宏而将三组代码之一包含到程序中 #if defined(WIN32) ... #elif defined(MAC_OS) ... #elif defined(LINUX) ... #endif一个程序中可以包含许多这样的#if指令。在程序的开头会定义这些宏之一而且只有一个由此选择了一个特定的操作系统。例如定义LINUX宏可以指明程序将运行在Linux操作系统下。 编写可以用不同的编译器编译的程序。不同的编译器可以用于识别不同的C语言版本这些版本之间会有一些差异。一些会接受标准C另一些则不会。一些版本会提供针对特定机器的语言扩展另一些版本则没有或者提供不同的扩展集。条件编译可以使程序适应于不同的编译器。考虑一下为以前的非标准编译器编写程序的问题。__STDC__宏允许预处理器检测编译器是否支持标准C89或C99。如果不支持我们可能必须修改程序的某些方面尤其是有可能必须用老式的函数声明见第9章末尾的“问与答”部分替代函数原型。对于每一处函数声明我们可以使用下面的代码 #if __STDC__ 函数原型 #else 老式的函数声明 #endif为宏提供默认定义。条件编译使我们可以检测一个宏当前是否已经被定义了如果没有则提供一个默认的定义。例如如果宏BUFFER_SIZE此前没有被定义的话下面的代码会给出定义 #ifndef BUFFER_SIZE #define BUFFER_SIZE 256 #endif临时屏蔽包含注释的代码。我们不能用/*...*/直接“注释掉”已经包含/*...*/注释的代码。然而我们可以使用#if指令来实现 #if 0 包含注释的代码行 #endif 将代码以这种方式屏蔽经常称为“条件屏蔽”。15.2节会讨论条件编译的另外一个常用用途保护头文件以避免重复包含。 14.5 其他指令 在本章的最后我们将简要地了解一下#error指令、#line指令和#pragma指令。与前面讨论过的指令相比这些指令更专业使用频率也低得多。 14.5.1 #error指令 #error指令有如下格式 #error 消息其中消息是任意的记号序列。如果预处理器遇到#error指令它会显示一条包含消息的出错消息。对于不同的编译器出错消息的具体形式也可能会不一样。格式可能类似 Error directive: 消息//或者 #error 消息遇到#error指令预示着程序中出现了严重的错误有些编译器会立即终止编译不再检查其他错误。 #error指令通常与条件编译指令一起用于检测正常编译过程中不应出现的情况。例如假定我们需要确保一个程序无法在一台int类型不能存储小于100000的数的机器上编译。允许的最大int值用INT_MAX宏23.2节表示所以我们需要做的就是当INT_MAX宏小于100000时调用#error指令 #if INT_MAX 100000 #error int type is too small #endif如果试图在一台以16位存储整数的机器上编译这个程序将产生一条出错消息 Error directive: int type is too small #error指令通常会出现在#if-#elif-#else序列中的#else部分 #if defined(WIN32) ... #elif defined(MAC_OS) ... #elif defined(LINUX) ... #else #error No operating system specified #endif14.5.2 #line指令 #line指令用来改变程序行的编号方法。正如你所期望的那样行的编号通常是按1, 2, 3, ...来进行的。我们也可以使用这条指令使编译器认为它正在从一个有不同名字的文件中读取程序。 #line指令有两种形式。第一种形式只指定行号 //#line指令形式1 #line nn必须是1~32767C99中是2147483647范围内的整数。这条指令导致程序中后续的行被编号为n、n1、n2等。 #line指令的第二种形式同时指定行号和文件名 #line n 文件指令后面的行会被认为来自文件行号由n开始。n和文件字符串的值可以用宏指定。 #line指令的一种作用是改变__LINE__宏可能还有__FILE__宏的值。更重要的是大多数编译器会使用来自#line指令的信息生成出错消息。例如假设下列指令出现在文件foo.c的开头 #line 10 bar.c现在假设编译器在foo.c的第5行发现一个错误。出错消息会指向bar.c的第13行而不是foo.c的第5行。为什么是第13行呢这是因为指令占据了foo.c的第1行因此对foo.c的重新编号从第2行开始并将这一行作为bar.c的第10行。 乍一看#line指令使人迷惑。为什么要使出错消息指向另一行甚至是另一个文件呢这样不是会使程序变得难以调试吗 实际上程序员并不经常使用#line指令。它主要用于那些产生C代码作为输出的程序。最著名的程序之一是yaccYet Another Compiler-Compiler它是一个用于自动生成编译器的一部分的UNIX 工具yacc的GNU版本称为bison。在使用yacc之前程序员需要准备一个包含yacc所需要的信息以及C代码段的文件。通过这个文件yacc生成一个C程序y.tab.c并合并程序员提供的代码。程序员接着按照正常方法编译y.tab.c。通过在y.tab.c中插入#line指令yacc会使编译器认为代码来自原始文件也就是程序员写的那个文件。于是任何编译y.tab.c时产生的出错消息会指向原始文件中的行而不是y.tab.c中的行。其最终结果是调试变得更容易因为出错消息都指向程序员编写的文件而不是由yacc生成的那个更复杂的文件。 14.5.3 #pragma指令 #pragma指令为要求编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。 #pragma指令有如下形式 #pragma 记号其中记号是任意记号。#pragma指令可以很简单只跟着一个记号也可以很复杂 #pragma data(heap_size 1000, stack_size 2000) #pragma指令中出现的命令集在不同的编译器上是不一样的。你必须通过查阅你所使用的编译器的文档来了解可以使用哪些命令以及这些命令的功能。顺便提一下如果#pragma指令包含了无法识别的命令预处理器必须忽略这些#pragma指令不允许给出出错消息。 C89中没有标准的编译提示pragma它们都是在实现中定义的。C99有3个标准的编译提示都使用STDC作为#pragma之后的第一个记号。这些编译提示是FP_CONTRACT23.4节、CX_LIMITED_RANGE27.4节和FENV_ACCESS27.6节。 14.5.4 _Pragma运算符(C99) C99引入了与#pragma指令一起使用的_Pragma运算符。_Pragma表达式可以具有如下形式 _Pragma (字面串)遇到该表达式时预处理器通过移除字符串两端的双引号并分别用字符和\代替转义序列\和\\来实现对字面串C99标准中的术语的“去串化”。表达式的结果是一系列的记号这些记号被当作pragma指令中的记号。例如 _Pragma(data(heap_size 1000, stack_size 2000)) 和 #pragma data(heap_size 1000, stack_size 2000)是一样的。 _Pragma运算符使我们摆脱了预处理器的局限性预处理指令不能产生其他指令。因为_Pragma是运算符而不是指令所以可以出现在宏定义中。这使得我们能够在#pragma指令后面进行宏的扩展。 现在来看一个GCC手册中的例子。下面的宏使用了_Pragma运算符 #define DO_PRAGMA(x) _Pragma(#x) 宏调用如下 DO_PRAGMA(GCC dependency parse.y)拓展后的结果 #pragma GCC dependency parse.y 这是GCC支持的一种编译提示。如果指定的文件本例中是parse.y比当前文件正被编译的文件还要新会给出警告消息。需要注意的是DO_PRAGMA调用的参数是一系列的记号。DO_PRAGMA定义中的#运算符会导致这些记号被串化为GCC dependency \parse.y\这个字符串随后作为参数传递给_Pragma运算符该运算符对其进行去串化操作从而得到包含原始记号的#pragma指令。 问与答 问1我看到在有些程序中#单独占一行。这样是合法的吗 答是合法的。这就是所谓的空指令它没有任何作用。一些程序员用空指令作为条件编译模块之间的间隔 #if INT_MAX 100000 # #error int type is too small # #endif当然空行也可以。不过#可以帮助读者看清模块的范围。 问2我不清楚程序中哪些常量需要定义成宏。有没有一些可以参照的规则 答一条首要的规则是除了0和1以外的每一个数值常量都应该定义成宏。字符常量和字符串常量有一点复杂因为使用宏来替换字符或字符串常量并不总能够提高程序的可读性。我个人建议在下面的条件下使用宏来替代字符或字面串(1) 常量被不止一次地使用(2) 以后可能需要修改常量。根据第二条规则我不会像这样使用宏 #define NUL \0尽管有些程序员会使用。 问3如果要被“串化”的参数包含或\字符#运算符会如何处理 答它会将转换为\\转换为\\。考虑下面的宏 #define STRINGIZE(x) #x 预处理器会将STRINGIZE(foo)替换为\foo\。 问4我无法使下面的宏正常工作 #define CONCAT(x,y) x##y 尽管CONCAT(a,b)会如所期望的那样得到ab但CONCAT(a,CONCAT(b,c))会给出一个怪异的结果。这是为什么 答这是那些连Kernighan和Ritchie都认为“怪异”的规则引起的。替换列表中依赖##的宏通常不能嵌套调用。这里的问题在于CONCAT(a,CONCAT(b,c))不会按照“正常”的方式扩展——CONCAT(b,c)首先得出bc然后CONCAT(a,bc)给出abc。在替换列表中位于##运算符之前和之后的宏参数在替换时不被扩展因此CONCAT(a,CONCAT(b,c))扩展成aCONCAT(b,c)而不会进一步扩展这是因为没有名为aCONCAT的宏。 有一种办法可以解决这个问题但不太好看。技巧是再定义一个宏来调用第一个宏 #define CONCAT2(x,y) CONCAT(x,y) 用CONCAT2(a,CONCAT2(b,c))就会得到我们所期望的结果。在扩展外面的CONCAT2调用时预处理器会同时扩展CONCAT2(b,c)。这里的区别在于CONCAT2的替换列表不包含##。如果这个也不行那也不用担心这种问题并不是经常会遇到。 顺便提一下#运算符也有同样的问题。如果#x出现在替换列表中其中x是一个宏参数其对应的实际参数也不会被扩展。因此假设N是一个代表10的宏且STR(x)包含替换列表#x那么STR(N)扩展的结果为N而不是10。解决的方法与处理CONCAT时的类似再定义一个宏来调用STR。 问5如果预处理器重新扫描时又发现了最初的宏名会如何处理呢如下面的例子所示 #define N (2*M) #define M (N1) i N; /* infinite loop? */预处理器会将N替换为(2*M)接着将M替换为(N1)。预处理器还会再次替换N从而导致无限循环吗 答一些早期的预处理器确实会进入无限循环但新的预处理器不会。按照C语言标准如果在扩展宏的过程中原先的宏名重复出现的话宏名不会再次被替换。下面是对i的赋值在预处理之后的形式 i (2*(N1));一些大胆的程序员会通过编写与保留字或标准库中的函数名同名的宏来利用这一行为。以库函数sqrt为例。sqrt函数23.3节计算参数的平方根如果参数为负数则返回一个由实现定义的值。我们可能希望参数为负数时返回0。由于sqrt是标准库函数我们无法很容易地修改它。但是我们可以定义一个sqrt宏使它在参数为负数时返回0 #undef sqrt #define sqrt(x) ((x)0?sqrt(x):0) 此后预处理器会截获sqrt的调用并将它替换成上面的条件表达式。在扫描宏的过程中条件表达式中的sqrt调用不会被替换因此会被留给编译器处理。注意在定义sqrt宏之前先使用#undef来删除sqrt定义的用法。在21.1节将看到标准库允许宏和函数使用同一个名字。在定义我们自己的sqrt宏之前先删除sqrt的定义是一种防御性的措施以防止库中已经把sqrt定义为宏了。 问6我在使用__LINE__和__FILE__等预定义宏的时候得到出错消息。我需要包含特定的头吗? 答不需要。这些宏可以由预处理器自动识别。请确保每个宏名的前后有两个下划线而不是一个。 问7区分“托管式实现”和“独立式实现”的目的是什么如果独立式实现连stdio.h头都不支持它能有什么用 答大多数程序包括本书中的程序都需要托管式实现这些程序需要底层的操作系统来提供输入/输出和其他基本服务。C的独立式实现用于不需要操作系统或只需要很小的操作系统的程序。例如编写操作系统内核时需要用到独立式实现这时不需要传统的输入/输出因而不需要 stdio.h。独立式实现还可用于为嵌入式系统编写软件。 问8我觉得预处理器就是一个编辑器。它如何计算常量表达式呢 答预处理器比你想的要复杂。虽然它不会完全按照编译器的方式去做但它足够“了解”C语言所以能够计算常量表达式。例如预处理器认为所有未定义的名字的值为0。其他的差异太深奥就不再深入了。在实际使用中预处理器常量表达式中的操作数通常为常量、表示常量的宏或defined运算符的应用。 问9既然我们可以使用#if指令和defined运算符达到同样的效果为什么C语言还提供#ifdef指令和#ifndef指令 答#ifdef指令和#ifndef指令从20世纪70年代就存在于C语言中了而defined运算符则是在20世纪80年代的标准化过程中加到C语言中的。因此实际的问题是为什么将defined运算符加到C语言中答案就是defined增加了灵活性。我们现在可以使用#if和defined运算符来测试任意数量的宏而不再是只能使用#ifdef和#ifndef对一个宏进行测试。例如下面的指令检查是否FOO和BAR被定义了而BAZ没有被定义 #if defined(FOO) defined(BAR) !defined(BAZ) 问10我想编译一个还没有写完的程序因此我“条件屏蔽”未完成的部分 #if 0 ... #endif编译的时候我得到了一条指向#if和#endif之间某一行的出错消息。预处理器不是简单地忽略#if指令和#endif指令之间的所有行吗 答不是的这些代码行不会被完全忽略。在执行预处理指令前先处理注释并把源代码分为多个预处理记号。因此#if和#endif之间未终止的注释会引起出错消息。此外不成对的单引号或双引号字符也可能导致未定义的行为。 写在最后 本文是博主阅读《C语言程序设计现代方法第2版·修订版》时所作笔记日后会持续更新后续章节笔记。欢迎各位大佬阅读学习如有疑问请及时联系指正希望对各位有所帮助Thank you very much
http://www.tj-hxxt.cn/news/216384.html

相关文章:

  • 网站推广制作做一个网站的市场价
  • 南宁建设厅官方网站网站建设php的心得和体会
  • 网站建设 北京天津造价信息网
  • 夏天做那些网站致富黄浦网站建设公司
  • 网站描述 修改淘宝优惠券网站开发
  • 专业做网站的公司 郑州适合个人做的外贸平台
  • 阿里云企业网站怎么建设轻量wordpress主题
  • 重庆做网站优化推广的公司东莞网站seo推广
  • 建站经验开发一个视频网站要多少钱
  • 巨野做网站的微信手机网站
  • 四模网站wordpress设置用户权限
  • 怎么判断网站好坏学校网站建设运行简介
  • 广州制作网站公司哪家好关键词优化公司如何选择
  • 建设教育局官方网站专门做微信小程序的公司
  • 网站定制开发成本广告联盟建设个人网站
  • 郑州网站排名服务鑫灵锐做网站多少钱
  • 59一起做网站安徽省建设部网站官网
  • 做招投标有哪些网站wordpress主题整站
  • 鹤壁做网站哪家便宜建设银行网站信息补充
  • 青海网站建设系统网站开发项目周报
  • 建设网站公司哪家技术好wordpress记录访问量
  • 无需登录网页小游戏网站为什么建设网站要年年交钱
  • 杭州购物网站建设开发企业小程序公司
  • 建站平台有哪些wordpress模板分享
  • wordpress商城建站教程抖音推广费用标准
  • 现在用什么工具做网站好国外做水广告网站大全
  • 建设部网站焊工证件查询wordpress推送服务器
  • 数字化档案馆及网站的建设企业logo标志设计免费
  • 网站策划方案如何做随县网站建设
  • 济南门户网站建设程序wordpress