《c primer plus》c语言学习笔记整理(十六)-C预处理器和C库

第十六章 C预处理器和C库

C预处理器:

在程序执行前查看程序,根据程序中的预处理器指令,预处理器把符号缩写替换为器表示的内容。

1.明示常量:#define
指令可以出现在源文件任何地方,其定义从指令出现的地方到该文件末尾有效。预处理器从#开始运行,到后面第一个换行符为止,也就是说,指令的长度仅限于一行。
《c primer plus》c语言学习笔记整理(十六)-C预处理器和C库_第1张图片
预处理器不会进行实际的运算,运算的过程在编译进行,它只进行替换
宏定义还可以包含其他宏(一些编译器可能不支持这种功能)
一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换。如果替换的字符串中还包含宏,则继续替换这些宏,唯一例外是双引号中的宏,例如

printf ("Two: OW");

(1)记号
1)符号常量的特性:助记、易更改、可移植
2)可以把宏的替换体看作是记号型字符串,而不是字符型字符串
3)当替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不同,解释为字符型字符串,将把空格视为替换体的一部分,解释为记号型字符串,把空格视为替换体中各记号的分隔符。
4)C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开

#define SIX 2*3

有三个记号:2、*、3;

#define FOUR 2*2

只有一个记号:2*2序列
(2)重定义常量
1)只有新旧定义完全相同才允许重定义,否则有些实现会将其视为错误。一些实现允许重定义,但是给出警告。
2)具有相同的定义意味着替换体中的记号必须相同,且顺序也相同。例如:

#define SIX 2*3
#define SIX 2*3

这种是相同的,但是

#define sIX 2*3

这种不同
2.在define中使用参数
(1)用宏参数创建字符串:#运算符
1)一般不要在宏中使用递增或递减运算符
2)C允许在字符串中包含宏参数。在类宏函数的替换体中,#号作为一个预处理运算符,可以把记号转化为字符串。

#define PSQR(x) printf ("The square of " #x " is gd.\n", ( (x)*(x) ))

#x就是转化为字符串“X”的形式参数名,这个过程被称为字符串化
(2)预处理器黏合剂:##运算符
1)把两个记号组合成一个记号
2)##可用于对象宏的替换部分
(3)变参宏:…和__VA_ARGS__
1)格式:通过把宏参数列表中最后的参数写成省略号(即三个点…)来实现这一功能。预定义宏__VA_ARGS__可用于替换部分中,表明省略号代表什么。

#define PR(...) printf(__VA_ARGS__)

2)一些函数接数量可变的参数,stdvar.h头文件提供了工具
(4)宏和函数的选择
1)使用宏比普通函数更复杂一些,稍有不慎会有奇怪的副作用。一些编译器规定宏只能定义成一行,不过,即使编译器没有这个限制,也应该这么做。
2)宏和函数的选择实际上是时间和空间的权衡。宏生成内联代码,在程序中生成语句。函数调用无论多少次,程序中只有一份函数语句的副本,节省了空间。另一方面,程序的控制必须跳转到函数内,随后再返回主调函数,这显然比内联代码花费更多的时间。
3)对于简单的函数,程序员通常使用宏
4)宏的优点:不用担心变量类型(宏处理的是字符串,而不是实际的值)。因此能用int或float类型都可以使用宏。
5)宏的注意点:一是宏名中不允许有空格,但是在替换字符中可以有空格,二是用圆括号把宏的参数和整个替换体括起来,这样能确保被括起来的部分在表达式正确展开,如下:

forks =2*MAX (guests + 3, last);

三是用大写字母表示宏函数的名称(大写字母可以提醒程序员宏可能产生副作用)。四是如果打算用宏加快程序运行速度,那么首先要确定使用宏和使用函数是否会导致较大的差异,在程序中使用一次的宏无法明显减少程序的运行时间,在嵌套循环中使用宏更有助于提高效率。
五是假如你开发了一些方便的宏函数,如果使用#include指令,就不用这样做了。
3.文件包含:#include
4.其他指令
(1)#undef指令
用于取消已定义的#define指令。(如果想使用一个名称但是不确定之前是否已经使用过,为了安全起见,可以用#undef指令取消该名字的定义)
(2)
(3)条件编译
1)#ifdef、#else和#endif指令
通过使用这些指令告诉编译器根据编译时条件执行或忽略信息(代码块)
预处理器不识别用于标记块的花括号{},因此它使用#else(如果需要)和#endif来标记指令块。这些指令结构可以嵌套,也可以用这些指令标记C语句块。
2)#ifndef指令

  • 与1)的指令很类似,也可以和#else、#endif一起使用,但是他们的逻辑相反,#ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的变量。通常,包含多个头文件的时候,其中的文件可能包含了相同宏定义,#ifndef指令可以防止相同的宏被重复定义。
  • 在首次定义一个宏的头文件中用#ifndef指令激活定义,随后在其他头文件中的定义都被忽略。
  • 防止多次包含一个文件。
#ifndef THINGSH_
#define THINGS H
	/*省略了头文件中的其他内容*/endif
#endif
  • 用文件名作为标识符、使用大写字母、用下划线字符代替文件名中的点字符、用下划线字符做前缀或后缀(可能使用两条下划线)。
#ifndef _STDIO_H
#define STDIO_H 
//省略了文件的内容
#endif

3)#if和#elif指令

  • 用途:让程序更容易移植,改变文件开头部分的几个关键定义,即可根据不同的系统设置不同的值和包含不同的文件。
  • #if后面跟整型常量表达式,如果表达式非零,则为真。
#if SYS==1
	#include "ibmpc.h"
#elif SYS == 2
	#include "vax.h"
#elif SYS==3
	#include "mac.h"
#else
	#include "general.h"
#endif
  • 较新的编译器提供另一种方法测试名称是否已定义,即用#if defined(VAX)代替#ifdef VAX
#if defined (IBMPC)
	#include "ibmpc.h"
#elif defined (VAX)
	#include "vax.h"
#elif defined (MAC)
	#include "mac.h"
#else
	#include "general.h"
#endif
  • 如果在VAX机上运行这几行代码,那么应该在文件前面用下面代码定义VAX:
#define VAX

(4)预定义宏
1)C99标准提供了一个名为__func__的预定义标识符,它展开为一个代表函数名的字符串(该函数包含该标识符)。那么__func__必须具有函数作用域,而从本质上看宏具有文件作用域。因此__func__是c语言的预定义标识符,而不是预定义宏。
(5)#line和#error
1)作用:#line指令重置__Line__和__FILE__宏报告的行号和文件名。

#line 1000		//把当前行号置为1000
#line 10 "cool.c"		//把行号重置为10,把文件名重置为cool.c

#error指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用#error指令:

#if__STDC_VERSION__ != 201112L
#error Not C11

#endif

(6)#pragma
1)现在的编译器中,可以通过命令行参数或IDE菜单修改编译器的一些设置。#pragma把编译器指令放入源代码中。(例如让编译器支持C9X,在开发C99时,标准被称为C9X)

#pragma c9x on

2)C99还提供_Pragma预处理器运算符,该运算符把字符串转换为普通的编译指示

#Pragma ("nonstandardtreatmenttypeB on")

等价于以下指令:

3)由于该运算符不使用#符号,所以可以把它作为宏展开的一部分:

#define PRAGMA (X) Pragma (#X)
#define LIMRG (X) PRAGMA (STDC CX_LIMITED_RANGE X)

然后可以使用类似下面代码:

LIMRG (ON)

(7)泛型选择
1)泛型编程:那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。
2)泛型选择表达式:可根据表达式的类型(即表达式的类型是int、double还是其他类型)选择一个值。(C11标准)
3)泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常作为#define宏定义的一部分。
下面是一个泛型表达式的示例:

Generic (x, int: o, float: 1, double: 2, default: 3)

4)用法:第一个项是表达式,后面的每一个项都由一个类型、冒号、一个值组成,第一个项的类型匹配哪个标签,整个表达式的值就是该标签后面的值。
5)泛型选择语句和宏定义组合:

#define MYTYPE (X) Generic((X),\
	int: "int"\
	float : "float",\
	double: "double",\
	default: "other"\
)

5.内联函数
(1)C99新增inline关键字时,它是唯一函数说明符
(2)作用:把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用
(3)标准规定,具有内部链接的函数可以成为内联函数,内联函数的定义与调用该函数代码必须在同一个文件,最简单的方法就是使用函数说明符inline和存储类别说明符static。(通常内联函数定义在首次使用它的文件中,所以内联函数相当于函数原型)
(4)编译器查看内联函数的定义(也是原型),可能会用函数体中代码替换eatline()函数调用,也就是说,效果相当于在函数调用的位置输入函数体的代码

#include 
inline static void eatline()  //内联函数定义/原型
{
	while (getchar() !=n')
		continue;
}		
int main ()
{
	...
	eatline ();		//函数调用
	...
}

6._Noreturn函数(C11)
1)C11新增了第二个函数说明符_Noreturn,表明调用完成后,函数不返回主调函数。
2)这与void返回类型不同,void类型的函数在执行完毕后返回主调函数,只是它不提供返回值。
3)exit()函数是_Noreturn函数的一个示例,一旦调用exit(),它将不会返回主函数。
4)该说明符的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调函数,告诉用户以免滥用该函数,通知编译器可优化一些代码。
5.C库
最初并没有官方的C库,后来基于UNIX的C实现成为了标准。
(1)访问C库
1)自动访问
在一些系统中只需编译程序,就可使用一些常用的库函数。在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可完成。
2)文件包含
如果函数被定义为宏,那么可以通过#include指定包含定义宏函数的文件。通常,类似的宏放在合适名称的头文件中。
3)库包含
编译或链接程序的某些阶段,可能需要指定库选项,有些函数可能必须通过编译时选项显式指定这些库,库选项告诉系统去哪里查找函数代码。
(2)使用库描述
1)了解函数文档
2)stddef.h文件中包含size_t类型的typedef或#define定义。其他文件(包括stdio.h)通过包含stddef。h来包含这个定义。
3)ANSI C把指向void的指针作为一种通用指针,用于指针指向不同类型的情况。
4)C99/C11标准在以上的描述中加入了新的关键字restric
7.数学库
函数中涉及的角度都以弧度为单位
(1)三角问题
1)atan2()函数接受两个参数:x的值与y的值,这样通过检查x和y的正负号就可以得出正确的角度值。
2)pi的值通过计算表达式4*atan(1)得到
(2)类型变体
1)C标准专门为float类型和long double类型提供了标准函数,即在函数名前加上f或l前缀,比如sqrtf(),sqrtl()。
2)利用C11新增的泛型选择表达式定义一个泛型宏,根据参数类型选择最合适的数学函数版本。
(3)tgmath.h库(C99)
1)定义了泛型类型宏,与原来的double版本函数名同名,
2)如果编译器支持复数运算,就会支持complex.h头文件,其中声明了与复数运算相关的函数。如果提供这些支持,那么tgmath.h中的sqrt()宏也能展开为相应的复数平方根函数。
3)如果包含了tgmath.h,要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来:

#include 
...
	float x =44.0;
	double y;
	y= sgrt (x);	//调用宏,所以是sqrtf (x)
	y=(sqrt) (x) ;		//调用函数sqrt()

4)由于c语言奇怪而矛盾的函数指针规则,还也可以使用(*sqrt)()的形式来调用sqrt()函数
5)不借助C标准以外的机制,C11新增的_Generic表达式是实现tgmath.h最简单的方式。
8.通用工具库
(1)exit()和atexit()函数
1)main函数返回系统时将自动调用exit()函数,其中最重要的是可以指定在执行exit()时调用的特定函数。atexit()函数通过退出时注册被调用的函数提供这种功能,atexit()函数接收一个函数指针作为参数。
2)atexit()函数的用法:使用函数指针,只需要把退出时要调用的函数地址传递给atexit()即可。调用exit()函数时会执行这些函数。ANSI保证,在这个列表中至少可以放32个函数。最后调用exit()函数的时候,exit()会执行这些函数(执行顺序与列表中的函数顺序相反,即最后添加的函数最先执行)
3)atexit()注册的函数应该不带任何参数且返回类型为void,通常这些函数会执行一些清理任务。
4)main()结束后会隐式调用exit()
5)exit()函数的用法:执行完atexit()指定的函数后,会完成一些清理工作:刷新所有输出流,关闭所有打开的流和关闭由标准I/O函数tmpfile()创建的临时文件。然后exit()把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。0表示成功终止,非0表示终止失败。
6)UNIX返回的代码并不适用于所有的系统,ANSI C为了可移植性的要求,定义了一个EXIT_FALLURE的宏表示终止失败。
(2)qsort()函数
1)作用:快速排序算法在C实现中的名称是qsort()
2)函数原型:

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

3)用法:第一个参数为指针,指向排序数组的首元素,ANSI C允许把指向任何数据类型的指针强制转换为指向void的指针。第二个参数是待排序项的数量,函数原型把该值转化为size_t类型。第三个参数,待排序数组中每个元素的大小(由于该函数把第一个参数转换为void指针,qsort()不知道数组中每个元素的大小)。第四个参数,指向函数的指针(比较函数用于确定排序的顺序。该函数接受两个参数,分别指向待比较两项的指针。如果第一项的值大于第二项,比较函数返回正数,如果两项相同,返回0;第一项小于第二项,返回负数。qsort()根据给定的其他信息计算两个指针的值,然后把他们传递给比较函数)
4)qsort()要求指针指向void,要解决这个问题,必须在函数内部声明两个类型的指针,并初始化他们分别指向作为参数传入的值。
注:C和C++对待指向void的指针有所不同,在这两种语言中,都可以把任何类型的指针赋给void类型的指针,但是C++要求在把void *指针赋给任何类型的指针时必须进行强制转换。而C没有这样的要求。

const double *al = (const double *) pl;

9.断言库
assert.h头文件,用于辅助调试程序的小型库,由assert()宏组成,接收一个真心表达式作为参数。如果表达式求值为假(非0),assrt()宏就在标准错误流(stderr)中写入一条错误信息,并调用abort()函数终止程序(abort()函数的原型在stdlib.h头文件中)
(1)用法:
1)assert()宏是为了标识出程序中某些条件为真的关键位置,如果其中一个具体条件为假,就用assert()语句终止程序。
2)通常assert()的参数是一个条件表达式或逻辑表达式,如果assert()中止了程序,它首先会显示失败的测试、包含测试的文件名和行号。
3)优点:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果认为已经排除了程序的bug,就可以把下面这段宏定义在assert.h的位置前面。

#define NDEBUG

程序会禁用所有assert()语句。如果程序又出现问题,可以移除这条#define指令(或者把它注释掉),然后重新编译程序,这样就重新启动了assert()语句。
(2)_Static_assert(C11)
1)assert()表达式是在运行时进行检查。_Static_assert声明,可以在编译时检查assert()表达式。assert()可以导致正在运行的程序中止,_Static_assert()可以导致程序无法通过编译
2)用法:_Static_assert()接收两个参数,第一个参数是整型表达式,第二个参数是一个字符串。如果第一个表达式求值为0(或者False),编译器会显示字符串,而且不编译程序。
3)_Static_assert()被视为声明,因此,它可以出现在函数中,或者在这种情况下出现在函数外部。
4)_Static_assert()要求它的第一个参数是整型常量表达式,这保证了能在编译期求值。
5)两者区别:assert中作为测试表达式的不是常量表达式,要到程序运行时才求值。_Static_assert()要求它的第一个参数是整型常量表达式,这保证了能在编译期求值。
10.string.h库中的memcpy()和memmove()
(1)函数原型:

void *memcpy (void * restrict s1, const void * restrict s2, size t n);
void *memmove (void *sl, const void *s2, size_t n);

(2)作用:两个函数都从s2指向的位置拷贝n字节到s1指向的位置,而且都返回s1的值。
(3)区别:memcpy()的参数带关键字restrict,即memcpy()假设两个内存区域没有重叠,然而memmove()不作这样的假设,所以拷贝过程类似于先把所有字节拷贝到一个临时缓冲区,然后再拷贝到最终目的地。
(4)如果使用memcpy()两个区域重叠,结果是未定义的,意味着该函数可能正常工作,也可能失败。编译器不会在本不该使用memcpy()时禁止你使用, 程序员应该有责任确保两个区域不重叠。
(5)两个指针设计用于处理任何数据类型,所有它们的参数都是两个指向void的指针,c允许把任何类型的指针赋给void *类型的指针,因此函数无法知道待拷贝数据的类型,因此两个函数使用第三个参数指明待拷贝的字节数。
(6)memcpy()函数不知道也不关心数据的类型,它只负责从一个位置把一些字节拷贝到另一个位置(例如,从结构中拷贝数据到字符数组中)。而且,拷贝的过程中也不会进行数据转换。
11.可变参数:stdarg.h
(1)作用:让函数接收可变数量的参数
(2)用法:
1)提供一个使用省略号的函数原型
2)在函数定义中创建一个va_list类型的变量
3)用宏把该变量初始化为一个参数列表
4)用宏访问参数列表
5)用宏完成清理工作
(3)相关实例:

void fl(int n, ...);  //有效
int f2 (const char • s, int k, ...); //有效
char f3(char ci, ..., char c2);//无k,省略号不在最后
double f3(...);  //无效,没有形参

(4)注意事项
1)最右边的形参(省略号前一个形参)起着特殊的作用,标准中用parmN这个术语来描述该形参,传递给该形参的实际参数是省略号部分代表的参数数量。
2)声明在stdarg.h中的va_list类型代表一种用于储存形参对应的形参列表中省略号部分的数据对象。

double sum(int 1im. ...)
{
	va-list ap; //声明一个储存参数的对象

3)之后,该程序将定义在stdarg.h中的va_start()宏,把参数列表拷贝到va_list类型的变量中。该宏有两个参数:va_list类型的变量和parmN形参,va_list类型的变量是ap,parmN形参是lim。
4)访问参数列表的内容,这设计到了使用另一个宏va_arg()。该宏接收两个参数:一个va_list类型的变量和一个类型名。第一次调用va_arg()时,返回参数列表第一项目,第二次调用返回第二项,以此类推。表明类型的参数指定了返回值的类型。(传入的参数类型必须与宏参数类型相匹配)

double tic;
int toc;
...
tic=va_arg (ap, double); //检索第1个参数
toc = va_arg (ap, int); //检索第2个参数

5)最后要使用va_end()宏完成清理工作。(例如,释放动态分配用于储存参数的内存,该宏接收一个va_list类型的变量)

va_end (ap) ; //清理工作

6)调用va_end(ap)后,只有用va_start重新初始化ap后,才能使用变量ap。
7)因为va_arg()不提供退回之前参数的方法,所以有必要保存va_list类型变量的副本。C99新增了一个宏用于处理这种情况:va_copy()。该宏接收两个va_list类型的变量作为参数,它把第2个参数拷贝给第一个参数。(此时,即使删除了ap,也可以从apcopy中检索两个参数)。

va_list ap;
va_list apcopy;
double
double tic;
int toc;
va_start (ap, 1im);  //把ap初始化为一个参数列表表
va_copy (apcoрy, ap); //把apcopy作为ap的副本
tic =vaarg (ap, double) ; //栓索第1个参数
toc =va_arg (ap, int); //检索第2个参数

你可能感兴趣的:(C语言)