关于C标准库的学习就从这里开始了。一个声明如下,以后不再赘述:本系列学习笔记部分代码源自《The Standard C Library》,copyright(c)1992 by P.J.Plauger,Prentice-Hall出版社出版,这些代码已经获得授权使用。
第一个头文件assert.h。
#include <assert.h>
void assert(int expression);
头文件assert.h唯一的目的就是提供宏assert的定义。特别注意assert并不是一个函数,而是一个宏。而实际上expression是一个真(非0)或假(0)的表达式。当expression为非真时(也就是说和0相等),assert宏就按照实现定义的格式向标准错误流写入关于这个特定调用的失败信息(包括参数文本、源文件的名字、原文本行数)然后它调用abort函数异常结束程序。不过要记住assert只适合在调试程序的时候起作用,最好是用assert生成的断言作为文档保留下来。关于宏NDEBUG,如果在语句#include <assert.h>
之前写有#define NDEBUG
,那么assert就不会起作用,具体涉及到源代码的条件编译。所以,打开断言可以写:
#undef NDEBUG /**打开断言*/
#include <assert.h>
#undef MACRO_NAME /**取消名为MACRO_NAME的宏的宏定义*/
特别注意这是个良性的命令。也就是说不管这条语句之前有没有出现过MACRO_NAME,都是可以的,不会出现任何问题。
要关闭断言,可以写:
#define NDEBUG /**关闭断言,从此断言不再起作用*/
#include <assert.h>
注意一点,我们知道C标准库的头文件具有幂等性(即包含同一个头文件多次也只是编译第一次出现的头文件,之后的同名头文件会被忽略,这样做是为了避免多次包含同一个头文件出现多次编译相同头文件的情况发生。可以通过简单的编写两行代码解决这个问题,即通过设置保护宏。),但是对于assert.h来说,每次包含它的时候它的行为都会变化,并且assert.h会改变assert的定义来适应NDEBUG当前的定义(因为在assert.h的源代码中并没有设置保护宏,这是个特例)。
关于__FILE__
和__LINE__
,这是C标准规定的5个预定义内置宏中的2个,注意__FILE__
和__LINE__
分别是字符串和整型。不属于任何一个头文件,由编译器提供。分别表示所属的文件,和代码的行号。什么叫做保护宏,很简单,在每个头文件的开头写如下代码:
#ifndef MACRO_NAME /**如果之前没有定义MACRO_NAME,执行下面语句*/
#define MACRO_NAME
这样就能够保护头文件后续的代码,保证多次包含头文件也不会出现多次编译同一头文件。不过注意#ifndef有个对应的#endif结尾。
注意一个非常令人讨厌的问题是内置宏__LINE__
没有扩展成字符串字面量,它是一个十进制常量。将其转换需要一个额外的处理层。不过为何必须要两层,目前还未完全懂得。我们添加两个隐藏的宏_STR
和_VAL
来实现。_VAL
用它的十进制常量扩展来取代__LINE__
,_STR
则将十进制常量转换为一个字符串字面量。
#ifndef MY_ASSERT_H /**保护宏,避免多次包含头文件时多次编译*/
#define MY_ASSERT_H
#undef assert
#ifdef NDEBUG
#define assert(test) ((void)0)
#else
void _Assert(char *);
#define _STR(x) _VAL(x)
#define _VAL(x) #x
#define assert(test) ((test) ? (void)0 \
: _Assert(__FILE__":"_STR(__LINE__)" "#test))
#include <stdio.h>
#include <stdlib.h>
void _Assert(char *mesg)
{
fputs(mesg, stderr); /**输出相关信息到标准错误流*/
fputs(" -- assertion failed\n", stderr);
abort(); /**异常终止*/
}
#endif /**NDEBUG*/
#endif /**MY_ASSERT_H*/
自己实现的一个简单的assert.h头文件,要注意的是,我是用了保护宏的。其实函数_Assert的具体实现最好是不放在头文件中,而放在库中。但是为了简便索性写在一起。注意如下代码:
#define assert(test) ((test) ? (void)0 \
: _Assert(__FILE__":"_STR(__LINE__)" "#test))
这是一个典型的宏替换。注意在表达式中给参数test加上括号避免产生调用宏表达式很容易出现的副作用。这是个判断表达式,(test)是真吗(注意test本身就是判断表达式,即一个谓词,注意区分下文的宏表达式)?如果是,宏表达式的值就是(void)0;如果不是,就调用我们编写的函数_Assert,宏表达式的值就是_Assert的返回值,不过_Assert是void类型。这也能解释为什么要写强制转换(void)0,为了类型的匹配。_STR(__LINE__)
将__LINE__
转换为一个字符串字面量。注意这里的字符串连接的问题,这里会将括号里的内容连接成一个字符串传参给_Assert。注意#test,它的含义是生成test的字符串字面量。注意反斜杠’\’的作用,’\’常用在宏定义和字符串中,表示连接不同行的文本表示这是同一行。比如如下代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("EXIT_FAILURE = %d, \
EXIT_SUCCESS = %d", EXIT_FAILURE, EXIT_SUCCESS);
return 0;
}
‘\’连接了”EXIT_FAILURE = %d, “和”EXIT_SUCCESS = %d”,表示这是同一个字符串。如果去掉’\’编译器就会报错。当一行需要写的文本太多需要多行表示同一个字符串,就需要’\’的功能不然一行的代码太多就影响了可读性。