C预处理器和C函数库

来源:互联网 发布:数据分析平台 功能 编辑:程序博客网 时间:2024/06/17 01:43

预处理器(Preprocessor)

预处理器在程序被compile之前进行处理,它将程序中的符号缩写按照指示代替。预处理器可以按要求将其他文件包含进来,并可以选择哪些代码能够让compiler看到。预处理器并不了解C语言,它基本上只是将一些文字转换成另一些文字


翻译(translate)程序的第一步:

首先,在跳转至预处理之前,compiler要将程序经过一些翻译处理。compiler要将源代码中出现的字符映射到源字符集

接着,compiler会找到后面接了newline的"\"字符并删除它们(此处的newline是指Enter键在代码文件中产生的新的一行,而不是指"\n")。这样做是因为预处理表达式的长度必须是一个逻辑行,但一个逻辑行可以是多个物理行

接着,compiler将文本切割成一系列的预处理标记(token)和一系列的空白和注释(token是通过空格、tab或断行分隔的群组)。此时,每个注释将会被一个空格字符代替

最后,程序便可以进入预处理阶段,预处理器会寻找潜在的预处理指令(在一行的开头由"#"指出)


#define

预处理指令可以出现在源文件的任何位置,定义从它出现的位置生效一直到文件结尾。预处理器会一直运行到"#"后的第一个newline,也就是说指令在长度上只能有一行(注意这里的行是逻辑行,通过"\"可以将一个逻辑行断成多个物理行)

每一个#define的逻辑行有三个部分:

第一个部分是#define;

第二个部分是你所选择的缩写,叫做宏(macro)。表示数值的宏叫做object-like macro,C还有function-like macro。宏的定义可以包含其他宏

第三个部分叫做replacement list或body,可以是任意字符串,甚至可以是一个C表达式。当预处理器在程序中找到一个定义了的宏时,它会用body进行替换(但有一个例外,就是当宏被双引号引起时,内容将变成宏的名字本身而不是宏所替换的body),这个过程叫做宏扩展(macro expansion);

如果想用宏定义字符或者字符串而不使用body的真实含义,要用单引号(')或双引号(")引起


一般来说,宏的body通常被当作是一系列token而不是一个字符串,二者的区别是body中会被当作有多少空白

如果你将LIMIT定义为20,后来又在同一文件中将其定义为25,这叫做重定义常量(redefining a constant)。重定义的结果取决于重定义的规定,ANSI C标准规定只有当新定义的内容与原有定义相同时才是合法的。如果有想要进行重定义的常量,使用const关键词结合scope规则可能是更好的选择


通过使用参数,可以创建一个function-like macro,它有一个或多个参数,而这些参数会出现在替代部分。

例:#define MEAN(X, Y) (((X)+(Y))/2)


#define SQUARE(x) x*x

在宏被调用时,宏定义中的X会被调用时所使用的参数替代,所以可以使用SQUARE(2);

x = 5;

SQUARE(x+2);    // ==17

因为预处理器不会进行计算,它只是进行字符的代替,只要出现x就用x+2代替,所以结果变成了x+2*x+2 == 5+2*5+2 == 17。当程序在运行时,函数调用会将参数的值传给函数;而宏调用只是在compile之前将参数作为字符(token)传给程序;二者是不同时间的不同处理。要想修正这个问题,在宏定义时将X括起即可,即:#define SQUARE(x) (x)*(x)

但是这并不能解决所有问题,如:100 / SQUARE(2)会变成100 / (2) * (2) == 100。要解决这个问题,要将body整体用括号括起,即

#define SQUARE ((x)*(x))

所以,要尽量多的使用括号以保证操作是以正确的顺序运行的

尽管如此,SQUARE(++x)会变成++x*++x,如果x=5,结果可能会是6*7==42(也可能是7*7==49)。实际上,这种调用方式的结果是未定义的,并且++x被执行了两次,x的值从5变成了7,尽管程序看起来只执行了一次x的自加。要解决这个问题的方法是不要将"++"用作宏的参数。


C允许在字符串中使用宏的参数字符本身。在function-like macro的替代部分,"#"符号能将token在预处理时转换成字符串。例如,如果x是宏的参数,那么#x就是将这个参数的名字转换成字符串"x"。这个过程叫做stringizing

例:

#define P(x) printf("The square of " #x " is %d\n", ((x)*(x)))...int y = 5;P(y);P(2+4);

结果是:

The square of y is 25

The square of 2+4 is 36

注意,#x不在双引号中,ANSI C的字符串会将双引号内的字符串和#x结合到一起。如果#x被放到了双引号中,则输出的结果将会是

The square of #x is 25


类似"#"操作符,"##"操作符可以用在function-like macro的替代部分,也可以用在object-like macro的替代部分。"##"操作符可以将两个token结合成一个token,例:

#define XNAME(n) x ## n

则XNAME(4)将会变成x4


除了函数可以不定参数个数外,C99/C11也支持不定参数个数的宏。将宏定义参数列表的最后一个参数设成"...",则预定义好的宏__VA_ARGS__就可以被用在替代部分来表明什么会替代"..."。例:

#define PR(...) printf(__VA_ARGS__)...PR("Howdy");PR("weight = %d, shipping = %.2f\n", wt, sp);


宏和函数

1. 多次使用时,函数占用的空间小。例如使用同一个宏20次,则在代码中会插入20行;而即便使用了20次函数程序中也会只存储一个函数副本

2. 调用函数时,程序控制会交给函数,函数完成后将控制交回给main,这会花费更多的时间

3. 宏不必考虑参数的变量类型



#include

代码中的#include指令会被所包含文件的内容替代。尖括号(<>)表示从标准系统目录中寻找,双引号("")表示从当前目录中寻找。包含头文件不一定会大量增加程序的大小,头文件的大部分内容只是compiler所需要的产生最终代码的信息,而不是要添加到最终代码的内容。头文件通常包括的内容有:

1. manifest constant(明式常量):如EOF,NULL等

2. 宏函数:例如getchar()通常被定义为getc(stdin)

3. 函数声明

4. 结构体模板定义:例如stdio.h定义了FILE结构体,包含文件和buffer的相关信息

5. 类型定义

6. 还可以定义多个文件共享的external变量

7. 定义file scope,internal linkage的const变量或数组。const可以避免被改变,static保证每个文件都会有自己的一份变量或数组的副本



预处理器提供了许多可以帮助程序员创建能够通过改变一些#define的值而将代码从一个系统移植到另一个系统的指令。#undef可以取消之前的#define定义;#if,#ifdef,#ifndef,#else,#elif和#endif允许使用不同的选择compile代码;#line允许重置行和文件信息,#error允许发出错误讯息;#pragma允许给compiler下达指令

#undef

取消之前的#define定义,例:

#define LIMIT 400

#undef LIMIT

然后可以对LIMIT重新进行#define定义。即使LIMIT尚未被#define定义过也可以使用使用#undef,例如你想使用一个名字进行#define定义但却不确定这个名字是否之前使用过,择可以#undef它以保证安全

注意#define宏的scope从它被定义开始,到文件结尾或遇到#undef结束(先遇到的那个),并且如果宏是由头文件引入的,则#define的位置和#include的位置也有关系。一些预定义的宏,例如__DATE__,__FILE__等是不能#undef的


条件编译(conditional compilation)

可以使用其他指令来创建条件编译,即可以通过它们告诉compiler根据条件在compile时选择接受或忽略哪些信息或代码


#ifdef,#else和#endif

例:

#ifdef MAVIS                  // 如果MAVIS已被定义    #include "horse.h"    #define STABLES 5#else    #include "cow.h"    #define STABLES 15#endif                        // 有没有#else都要写

可以嵌套


#ifndef

和#ifdef用法相同,只不过判断后面的identifier是否未定义,通常用于当包含多个头文件时避免同一个宏被重复定义,因为可能每个头文件都包含了定义


#if和#elif

用法和C中的if类似,后接逻辑表达式,例:

#if SYS == 1    #include "ibm.h"#elif SYS == 2    #include "mac.h"#else    #include "general.h"#endif

可以使用【#if defined (IBM)】代替【#ifdef IBM】,这里"defined"是一个预处理操作符,如果参数已被定义返回1,否则返回0。它的优点是可以与#elif合用,进而可以使程序有更好的可移植性。例:

#if defined (IBM)    #include "ibm.h"#elif defined (MAC)    #include "mac.h"#else    #include "general.h"#endif


预定义的宏


C99还定义了__func__,它存储了当前使用函数的函数名,因此它具有function scope。因为宏必须具有file scope,所以__func__只是C语言预定义的identifier而不是预定义的宏


#line和#error

compiler使用行号和文件名(可有可无)来引用compile时遇到的错误。行号通常是现在的输入行,文件名是现在的输入文件

#line允许重置由__LINE__和__FILE__报告的行号和文件名,文件名要用双引号括起。#line通常被用来产生引用自源代码的错误信息,而不是引用自生成程序。例:

#line 1000             // 将现在的行号设为1000#line 10 "cool.c"      // 将行号设为10,文件名设为"cool.c"


#error可以让预处理器发出所包括的错误信息,如果可以,compile过程应该终止,例:

#if __STDC_VERSION__ != 201112L    #error Not C11#endif


#pragma

允许在源代码中放置compiler指令。通常每一个compiler都有自己的pragma(编译注释),它们可能用来控制automatic变量使用的内存空间等。C99提供了_Pragma预处理操作符,可以将字符串转化成pragma,如:

_Pragma("nonstandardtreatmenttypeB on") == #pragma nonstandardtreatmenttypeB on

因为前者不使用"#",所以可以用作宏定义的一部分,如:

#define PRAGMA(X) _Pragma(#X)#define LIMRG(X) PRAGMA(STDC CX_LIMITED_RANGE X)...LIMRG(ON);

然而,下面的方法是错误的:

#define LIMRG(X) _Pragma(STDC CX_LIMITED_RANGE #X)

问题在于它依赖字符串合并,然而compiler直到预处理结束后才会进行字符串合并(前面的方法中,#X就是#X,第二个define中用的是X的值而非#X;而下面的方法则需要将括号内前面的内容和#X合并成一个字符串)


_Pragma操作符同时实现了"destringizing"的工作,即字符串中跳脱的符号会被转化成所表示的字符,例:

_Pragma("use_bool \"true \"false") == #pragma use_bool "true " false



Generic Selection(泛型选择)(C11)

generic programming(泛型编程)指代码不固定某个类型,但当类型确定后可以转换成对应类型的代码。C11增加了一种新的表达式,叫做泛型选择表达式(generic selection expression),它可以根据表达式的类型选择值。泛型选择表达式并不是预处理语句,但它通常用作#define宏的一部分

泛型选择表达式的形式是:_Generic(x, int: 0, float: 1, double: 2, default: 3)

这里"_Generic"是C11的关键词,他后面的括号中包含了若干个用逗号分开的项。第一项是一个表达式,剩下的所有项是类型加冒号(:)加一个值。第一项的类型如果匹配后面某项的类型,则表达式的值便被设为该类型冒号后的数值。例如若x是double型,则整个表达式的值便是2。泛型选择表达式有点类似switch语句,不过匹配的是类型而不是值

例1:

#define SQRT(X) _Generic((X),\    long double: sqrtl, \    float: sqrtf, \    default: sqrt)(X)
例2:

#define SQRT2 _Generic((X), \   long double: sqrtl((X)), \   float: sqrtf((X)), \   default: sqrt((X)) \)
例1与例2的区别在于,例2中每一个对应的类型是一个函数的调用,所以_Generic表达式是一个函数调用;而例1中_Generic表达式是函数的名字,函数的名字被函数的地址代替,所以_Generic表达式的值是一个指向函数的指针,因为_Generic表达式后面接了(X),所以【函数指针(参数)】就用给出的参数调用了指向的函数
C99标准添加了tgmath.h头文件,它定义了类似上例中的泛型宏(type-generic macro)。如果math.h头文件中的函数定义了float,double和long double型,tgmath.h文件就会产生一个和double型版本函数名字相同的泛型宏。例如它定义了sqrt()宏,根据参数类型可以使用sqrtf(),sqrt()和sqrtl()函数。如果想使用原函数而非泛型宏的话,需要将函数名用括号括起,如:
#include<tgmath.h>...float x = 44;double y;y = sqrt(x);     // 使用宏,相当于sqrtf(x)y = (sqrt)(x);   // 使用函数sqrt()



内联函数(inline function)(C99)

通常,函数的调用是有开销的,要花时间来建立调用,传递参数,跳到函数代码,返回等,但通过使用宏将代码内联可以避免这些开销。C99和C11添加了新的方法,叫做内联函数(inline function),它的定义是:“将一个函数变成内联函数表明对这个函数的调用会尽可能得快,实现的程度取决于具体的定义实现”。所以将一个函数定义成内联函数可能会使compiler用内联代码代替函数,或用其他的方式进行优化,也可能没有效果。

标准提出有internal linkage的函数可以被内联,并且对内联函数的定义和函数的使用必须在同一个文件中。最简单的方法就是在static标识符前添加"inline"关键词。因为内联函数没有单独的代码block,所以不能获取它的地址(事实上可以获取它的地址,但compiler就会产生一个非内联的函数)。

内联函数应该短小。对于长函数来说,调用它的时间比执行函数体的时间要短,所以使用内联不会有太大性能上的提升。如果同一个程序的多个源代码文件要使用同一个内联函数,则最方便的方法是将这个内联函数定义到一个头文件中,然后将这个头文件包括在每一个需要内联函数的源代码中即可

C,不像C++,允许在一个程序中,在源文件(如1.c)中定义一个与在其他源文件(如2.c)中的内联函数相同的external函数(如square(double x))。在除这两个文件外的其他文件(如3.c)中可以声明一个inline但是没有static的相同的函数,则第三个文件中compiler可以自由选择使用使用1.c或2.c中的函数定义,例:

// 1.cinline static double square(double x) {return x*x;}// 2.cdouble square(double x) {return x*x;}// 3.cinline double square(double x) {return x*x;}

_Noreturn函数(C11)

C11添加了另一个函数关键词"_Noreturn"来表示那些完成后并不返回调用它的函数的函数(例如exit()函数)。



C函数库

使用C函数库的三种方法:

1. 自动访问

2. 文件包含:如果函数被定义成一个宏,则可以用#include将包含它定义的文件包含进来

3. 库包含:注意这个过程与包含头文件不同。头文件提供了函数的声明或prototype;库选项则告诉系统到哪里寻找函数代码


通用工具库(General Utilities Library)

exit()和atexit()

在main()函数返回时会自动调用exit()。在执行完atexit()添加的函数后,exit()函数会清空所有的输出流,关闭所有打开的流,并且关闭由调用标准IO函数tmpfile()所产生的临时文件。然后它会将控制交回给主机环境,如果可能的话汇报终止状态(termination status)。Unix程序通常使用0来表示成功退出(否则为非0),但并不适用于所有系统。ANSI C定义了名为"EXIT_SUCCESS"和"EXIT_FAILURE"的更具有可移植性的宏。在ANSI C环境下,在非递归的main()函数中使用exit()和return效果相同,但是exit()在用在除main()之外的其他函数中时也可以终止程序

ANSI标准增加了一些新的特性,最重要的一个就是可以指明exit()执行时可以调用哪些函数。atexit()函数可以指明在exit时执行的函数,它的参数是函数指针。atexit()函数并不会执行exit(),它只是表明在执行exit()还要执行哪些函数。ANSI保证至少可以添加32个函数,每一个函数都要使用单独的atexit()添加,并且最后添加的函数会被最先执行。只要exit()被调用,即使不是显式调用,atexit()添加的函数也会被执行


qsort()函数

C中快速排序算法的实现是qsort(),它可以处理data object的数组,prototype是:

void qsort (void* base, size_t nmemb, size_t size, int (*compar)(const void *, const void *) );

第一个参数是指向要被排序数组的指针;第二个参数是要被排序的项目数;因为qsort()第一个参数是void pointer,所以它无法的值数组中每个元素的大小,第三个参数的作用就是告知qsort()每个元素的大小;第四个元素是一个函数指针,指向比较函数,它的作用是告知qsort()如何进行排序。这个比较函数要有两个参数,分别指向两个要进行比较的项目;如果第一个项目应该排在第二个项目后,则返回正整数;如果相同返回0;如果第一个项目应排在第二个项目之前则返回负整数。

然而,要比较两个被指向的值要解引用(dereference)一个指针。假设要排序的数组是double型,那么就需要将指针解引用成double型,但qsort()传入的指针类型是指向void的指针。要解决这个问题,就必须要在比较函数中声明合适的指针类型并将它们初始化成传入参数的值。例:

...qsort(arr, LEN, sizeof(double), comp);...int comp(const void* p1, const void* p2){   const double* a1 = (const double *)p1;   const double* a2 = (const double *)p2;   ...}

C和C++处理pointer-to-void的方式是不同的。二者都可以将任何类型的指针赋给void *,但是C++要求在将其他类型的指针赋给void *指针时进行类型转换,而C没有类似的要求。上例中的指针转换对C来说不是必须的,但是这样可以更好的移植到C++


Assert Library

assert library是帮助debug程序的库,头文件是assert.h。它包含一个叫做assert()的宏,宏的参数是一个int表达式。如果表达式的值是false,assert()宏会将错误信息写到stderr并且调用abort()来终止程序(abort()在stlib.h中)。目的是为了检查程序中某个位置的某种情况是否成立,并且如果不成立的话则使用assert()终止程序。如果assert()终止了程序,它会显示未通过的检查,包括这个检查的文件名和行号。

相比于自行输出信息并abort(),assert()的优点是它可以自动识别文件和问题发生的行号,并且可以在不改变代码的情况下开启或关闭assert()宏。如果不再需要assert(),只要在#include<assert.h>的前一行加入#define NDEBUG,则assert()就会被关闭


assert()表达式是run-time检查,C11添加了_Static_assert声明可以进行compile-time检查(在包含了assert.h头文件后可以使用static_assert代替,C++使用static_assert)。它有两个参数,第一个是常量表达式,第二个是字符串。如果第一个表达式的值是0则compiler会显示字符串的信息并终止compile。例:

_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");
_Static_assert被当作一个声明语句,所以它既可以出现在一个函数中,也可以出现在一个函数外。但如果将上例放在main()中,则只会当编译结束并执行程序的时候才会进行错误警告,而这是低效率的


memcpy()和memmove()

这两个函数包含在<string.h>库中,都提供了移动数组的功能,它们的prototype是:

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);

void *memmove(void * s1, const void * s2, size_t n);

二者都能够将n个byte从s2指向的位置复制到s1指向的位置并返回s1的值。区别主要在于restrict产生的效果,memcpy()会假设两个内存区域没有重叠,而memmove()则不会这样假设,所以s2的数组会被先复制到一个临时buffer中然后才拷贝到目标地址。因为指针类型是pointer-to-void,所以要用第三个参数来决定移动数据的大小,例如要移动10个double变量,则第三个参数应该是10*sizeof(double)。memcpy()和memmove()都不知道也不在意数据类型,它们只是将一定byte的数据从一个地方复制到另一个地方


不定参数个数:stdarg.h

stdarg.h能够使函数接受不固定个数的参数。要使用它必须做到以下几点:

1. 用"..."提供函数prototype

2. 变量列表除了"..."之外至少还要有一个变量,并且"..."必须在最后

3. 在函数定义中创建一个va_list类型的变量

4. 使用va_start宏初始化参数列表的变量

5. 使用va_arg宏访问参数列表

6. 使用va_end宏进行清理


"..."前的最后一个参数叫做"parmN",传入这个参数的值是"..."部分所表示的参数个数,例如:

double sum(int lim, ...);sum(4, 13.1, 1.04, 18.21, 117.8);
在stdargs.h头文件中定义的va_list类型的变量包含了"..."部分的所有参数。一个不定参数个数函数的定义应该这样开头(这里lim就是parmN):

double sum(int lim, ...){     va_list ap;

然后,函数需要使用va_start()宏将参数列表中"..."部分的变量复制到va_list类型的变量中。va_start()宏有两个参数:va_list型变量和parmN参数。上例续:

va_start(ap, lim);

下一步就是获得参数列表的具体内容,需要使用宏va_arg()。它有两个参数:va_list型变量和类型名。第一次调用它时,它会以第二个参数给出的类型返回va_list变量中的第一个项目;第二次调用时会返回第二个,以此类推

double var1;var1 = va_arg(va_list, double);

一定要小心的是,变量类型一定要与变量数值匹配,因为赋值时的自动类型转换在这里并不会发生。最后,需要通过宏va_end()进行清理,比如它可能会释放存放参数所分配的动态空间。va_end()的参数是va_list型变量

va_end(ap);

因为va_arg()不能再返回已经返回过的参数,所以最好存放一份它的副本。C99增加了一个进行这个处理的宏,叫作va_copy()。它的两个参数都是va_list型变量,行为是将第二个参数复制到第一个中


一个完整的不定参数个数函数的例子:

double sum(int lim, ...){     va_list ap;     double total = 0.0;     int i;     va_start(ap, lim);     for(i = 0; i < lim; i++)          total += va_arg(ap, double);     va_end(ap);     return total;}



0 0