背景基础
1.c语言库用c语言编写 其他语言则不同
早期语言的库是用汇编语言编写的 不同的计算机体系结构有不同的汇编语言 所以在移植性方面差一点 而c语言可以编写出高度可移植性的代码。
2.库
字符串 以第一个出现的空字符作为结束且包含该空字符的连续的字符序列
字母 字符集中的可以打印的字符
小数点 对浮点数和自护序列进行转换的函数所是使用的符号 用来指示珍重字符序列的小数部分的其实位置
3.标准头文件
每一个库函数都在一个头文件中生命 头文件的内容可以通过一个#include 预处理指令来使用 头文件生命了一组相关的函数 还包括一些必须的类型和一些附件的宏
标准头文件有
assert.h locale.h stddef.h ctype.h math.h stdio.h erro.h setjmp.h stdlib.h
float.h singnal.h string.h limits.h stdarg.h time.h
如果出现和上面的同名文件 且不作为语言实现的一部分 且在源文件包含的标准位置 则此种行为为未定义
头文件在一个给定的范围内被包含多次 与包含一次的效果相同;
如果要使用一个头文件 则应该包含在任何外部声明或者定义的外面 而且应该在第一次引用它声明的任何函数或对象 或者它定义的任何类型和宏之前被包含;
如果一个标识符在多个头文件中定义或者声明 那么第二个和后来的头文件可以在对这个标识符的第一次引用之后被包含;
程序在一个头文件被包含之前不应该有和当前定义的关键字重名的任何宏;
保留的标识符
a.下划线加一个大写字母或者两条下划线开头的标识符 都被保留做任何用途
b.下线开头的标识符通常都在普通标识符和标记命名空间方面作为文件作用域的标识符
c.后面的任何子句 包括库的展望中列出的每个宏名都被保留做任何用途 如果头文件被包含
d.后面的子句 包括库的展望 所有具有外部链接的标识符 都被保留为具有外部链接的标识符使用
e.后面的子句 包括库的展望 列出的文件作用域标识符 都被保留 作为一个相同命名空间中具有文件作用域的标识符来使用 只要它的任何一个相关头文件被包含了。
上面的标识符为保留的用处 如果一个程序声明或者定义了一个标识符 这个标识符和上下文环境中保留的标识符同名 则为未定义。
注意:
使用小数点字符的函数有
localeconv fprintf fscanf printf scanf sprintf sscanf vfprintf vprintf vsprintf atof 和strtod
头文件不一定是一个源文件 头文件名字中由尖括号括住的字符序列也不一定是有效的源文件名字;
具有外部链接的保留标识符包括 errno setjmp va_end;
因为宏名只要存在就会被代替,而和作用域和命名空间无关 所以如果相关头文件被包含 和任何保留的标识符的名字想匹配的宏名一定不能定义;
4 库函数的使用
a 如果函参无效 则未定义
b 如果函参为数组 则实际传递的是数组的指针 首地址
c 如果头文件的函数为该头文件的一个宏实现 则库函数不一定被显式的声明
d 移除任何宏定义的 指令 #undef 可以保证实际函数的引用
e 作为宏实现的库函数的任何调用都会一次展开为精确地计算它的每个参数代码 所以任何表达式作为参数都是安全的
f 所有展开为整型常量表达式的类似与对象的宏都使用与#if预处理指令
g 如果不引用一个头文件中的任何类型定义就可以声明一个库函数 那么显式或者隐式地声明这个函数都式允许的,并且不用包含它的相关头文件就可以使用它
h 如果一个接收可变参数列表的函数没有被声明 (显式地或者通过包含相关头文件) 那么它的行为未定义
i 一个函数的任何宏定义都可以通过用扩韩吧函数的名字括进来 局部地抑制它,因为这个名字后面没有跟着指示一个宏函数名展开的左括号
例如:
a 函数atoi 可以通过以下几种方式使用:
通过相关头文件的使用 可能生成一个宏展开
#include <stdlib.h> //上面的c 条
...
i=atoi(str);
b 通过相关头文件的使用 确实生成了一个真正的函数引用
#include <stdlib.h>
#undef atoi //显式的声明不用头文件中的atoi 上面的d条
..
i=atoi(str)
或者
#include <stdlib.h>
..
i=(atoi)(str)//此处有括号 对应i条
c 通过显式地声明
extern int atoi(const char *);
const char *str;
...
i=atoi(str);
d 通过隐式地声明
const char *str;
...
i=atoi(str);
一个实现必须为每个库函数提供一个实际的函数 即使它已经为该函数提供一个宏
标识符 BUILTIN_abs 可以用来说明函数abs内联代码的产生 因此 正确的头文件能为一个其代码生成器可以接受这个标识符的编译器之赐你个如下内容
#define abs(x) _BUILTIN_abs(x)
通过这种方式 如果一个用户希望保证像abs这样的给定库函数是一个真实的函数 可以这样编写
#undef abs
5 库的使用
重要的两点:
怎样使用库的头文件、
怎样在程序中创建库名
a 头文件的使用
头文件的特性
具有幂等性 多次包含头文件与只包含一次相同
相互独立 头文件正常工作不需要包含其他的标准头文件为前提
互相不包含
和文件级别的声明等同 先将标准头文件包含到你的程序中,然后才能使用该头文件已经定义或声明的东西 不能在声明中包含标准头文件 并且不能包含标准头文件之前用宏定义去替代关键字
书写约定:在程序中 开头部分要包含用到的头文件 在#include 指令之前只能有一句注释语句
自己定义的头文件不能与标准头文件重名 如果自定义的头文件用到标准头文件的声明或定义 需要在包含自定义头前 包含标准头
以小写字母开头 接着使用1-7个小写字母和数字 以.c作为c源文件名的后缀,.h作为头文件名的后缀
自定义头 需要用双引号包含 而不使用尖括号 只有引用标准头文件的时候才使用尖括号
例如
#include <stdio.h> //此处为标准头
#include "plot.h" //此处为自定义的头
命名空间
namespace 为了 避免重名 引起冲突 有了命名空间的概念 namespace xxx 此处xxx就是命名空间
标准库定义了200个外部名字 而且还保留了 某种类别的名字供库使用,除此之外的所有名字 编程者均可使用
a 函数中的大括号内的部分 引入了两个新的命名空间 一个是包含了所有作为类型定义 函数 数据对象和枚举
常量声明的名字,另一个则是包括了所有枚举,结构和联合的标记
b 定义的每个结果或者联合都引入了一个新的命名空间 包括所有成员的名字
c 声明的每个函数原型引入了一个一个新的命名空间 包括所有参数名
d 定义的每个函数引入一个新的命名空间 包括所有标号的名字
注意在一个给定的命名空间中 你只能以一种方式使用一个名字 ,如果翻译器识别出一个名字属于某个命名空间,它就看不到这个名字在另一个命名空间的其他用途。
使用过程中 命名时要注意所有的关键字和库名不能在使用到新的变量或函数命名中、
注意: a 以下划线开头且具有外部链接属性的函数和数据对象的名字 _abc或者_DEF
b 以两个下划线或一个下划线加一个大写字母开头的宏名,例如 _ _abc或者_DEF
6 库的实现
假设的成立
a 可以用一个同名的c源文件代替标准库的头文件
b 可以代替不完整的标准头文件
c 可以用包括了这个函数的常规定义的c源文件替换预定义的函数
d 可以一点一点地替换一些预定义的函数
e c源文件名应该至少有8个小写字母 后面是一个点和一个小写字母
f 外部名字可能把所有的字母和一种形式的字母相对应 也可能不是
编程风格
a 库中的每个科技那的函数都占据这单独的c源文件 文件名就是函数名 例如strlen就在文件strlen.c中
b 每个隐藏的名字都以一个下划线跟一个大写字母开头 如_Getint
c 库中隐藏的函数和数据对象通常占据以x开头命名的c源文件,例如xgetint.c 这样的文件可以包含多个函数或者数据对象 文件名源自它包含的某个函数或者数据对象的名字
d 代码排版需要统一 通常尽可能在嵌套里层的函数中声明数据对象 用公整的缩进来说明控制结构的嵌套 在每个函数里面的{ 左大括号后面写上一行注释
e 代码不包括register声明 因为他们不容易安排并且会使代码混乱 现代的编译器能比程序原更好地分配寄存器
f 一个库中的可见函数的定义中 函数名被一对括号括住 所有这样的函数声明都会被相应的头文件中的宏定义所掩盖 所以这对括号组织了翻译器识别和展开宏
g c源文件都以一个带边框的图的形式给出 图的说明中有文件的名字 有些比较大的文件会出现在两个页面中 这样图在每个页面的说明会提示 出现的代码是源文件的一部分
h 每个图显式的c源代码都使用只占4列的水平制表符来缩进 代码右边的注释都经过调整而没有换行 每个源文件的最后一行的结尾都用一个方框字符做标记
7 头文件的实现
幂等性 对于大多数的标准头文件可以使用宏保护 例如 可以通过有条件地最多包含一下内容 一次来保护<stdio.h>
#ifndef _STDIO_H //保留的名字
#define _STDIO_H
#endif
对于assert.h不能使用此种机制,受宏名NDEBUG的控制,每次包含此头文件时,该头文件关闭或者打开宏assert,取决于翻译器中的这个点是否定义了宏NDEBUG。
相互独立性:
a 有些名字会在多个头文件中定义 一个程序可以包含两个定义了相同名字的头文件而不出错
例如 size_t 就是其中一个例子 它是使用操作符sizeof所产生的类型 可以使用另一个宏保护来防止这种类型的多次定义
#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned int size_t;
#endif
宏NULL是另外一个例子 可以在任何一个想引入指向数据对象的空指针 的地方使用这个宏
定义的方式: #define NULL (void *) 0
在一个翻译单元中包含这样的宏定义的多个实例不会造成任何损失 允许宏的良性重定义 具有相同宏名的两个定义必须具有相同的记号序列 ,他们只能是记号之间的空白不同 没必要组织包含两个符合这种情况的定义
如果需要在多个地方提供相同的定义 需要两种解决办法
a 在多个地方编写相同的定义 定义改变时 就要找出这个定义的所有实例
b 把定义放在一个独立的头文件中 给这个文件起一个不会和程序员创建的文件名字冲突的名字 在每个用到该定义的头文件中包含这个头文件。
同义字
头文件<stdio.h>一定要包含一个va_list类型的同义字 有一个为宏保留的名字 在一个标准头文件内部表达这3个函数中的每个函数的原型
作为程序员 没有定义va_list类型就去使用这些函数是很困难的,这就以为着在任何想使用这些函数的时候 你可能会想包含头文件
<stdarg.h> 所以,这仍然是程序员的工作。编译器没必要在每次程序包含头文件<stdio.h>的时候都把头文件<stdarg.h>拉进来
文件级别的头文件
程序原必须在允许文件级别声明的地方包含一个标准头文件, 也即#include不能出现在另一个声明内的任何地方,大多数标准头文件必须包含一个
或多个外部声明。这些只在某些上下问环境中允许
库的测试
测试所有路径 :代码应该做什么 和 怎么做
规格确认
性能测试 a 测定与代码各种使用方式有关的参数
b 能被独立的用户执行
c 有可重现的结果
d 对 足够好 有合理的标准
e 对优于一般和卓越有令人信服的标准
设定测试计划
简单测试
a 打印出一个标准的额确认信息 退出时返回成功的状态来报告执行的正确性
b 识别出其他所有不可避免的输出 以把代码的读者遇到的混乱减到最小
c 提供有趣的独立与实现的信息 否则这些信息很难得到;
d 不输出其他的东西
通常的习惯是在每个头文件的名字前面加一个t来构建测试文件的名字 因此 tassert.c 是用来测试assert.h的
来保证宏assert做它应该做的工作 当一个诊断失败时会告诉你库的输出 当