在前面编写简单的c运行库(一)中主要实现了调用main函数前的初始化、获取参数和环境变量、退出程序等工作。接下来我们真正实现c标准库中的一些函数(主要是文件操作、字符串操作函数)。不过我们对这些函数的实现力争简单,对于效率方面考虑的不是很多,因为目的主要还是学习神秘的库是怎么实现的。
1 文件操作
c中的标准I/O库都是带有缓存的,我们在这里为了实现的简单,将缓存省略了,直接包装了有关文件操作的系统调用。现在我们直接看文件打开的函数:
1 static int open(const char *pathname, int flags, int mode) 2 { 3 int ret; 4 5 __asm__ volatile( 6 "int $0x80" 7 :"=a"(ret) 8 :"0"(5),"b"(pathname),"c"(flags),"d"(mode) 9 ); 10 if (ret >= 0) 11 return ret; 12 return -1; 13 }
open函数中直接调用了嵌入汇编调用了系统调用。对于系统调用的返回值,如果是负数,直接返回-1,否则直接返回。这个函数是系统调用的一个包装,本质其实就是个系统调用。然后我们在open函数的基础上实现c标志库函数中的fopen函数。
1 FILE *fopen(const char *path, const char *mode) 2 { 3 int fd = -1; 4 int flags = 0; 5 int access = 00700; /*创建文件的权限*/ 6 7 if (strcmp(mode, "w") == 0) 8 flags |= O_WRONLY | O_CREAT | O_TRUNC; 9 if (strcmp(mode, "w+") == 0) 10 flags |= O_RDWR | O_CREAT | O_TRUNC; 11 if (strcmp(mode, "r") == 0) 12 flags |= O_RDONLY; 13 if (strcmp(mode, "r+") == 0) 14 flags |= O_RDWR | O_CREAT; 15 fd = open(path, flags, access); 16 return (FILE *)fd; 17 }
由于我没有像标志I/O库那样实现缓存,所以我直接把FILE定义为int型,这样我们用FILE就相当于用了文件描述符。从上面的代码中可以知道我设置了文件的创建权限只有文件创建者有读写执行的权限,还有就是我只实现了以只读、只写、读写方式打开文件,对于追加等方式没有实现。然后函数read、fread和write、fwrite都可以用相同的方式实现,还有fputc,fputs也是已一样的。
2 输出函数
I/O函数中比较麻烦的要属实现printf、fprintf这些可变参数的函数,当然这些函数都是调用vfprintf函数实现的,所以只要实现了vfprintf函数,其它的函数实现就比较简单了。
首先来看下我实现的vfprintf函数代码:
1 int vfprintf(FILE *stream, const char *format, va_list ap) 2 { 3 int n = 0, flag = 0, ret; 4 char str[20]; 5 6 while (*format) 7 { 8 switch (*format) 9 { 10 case '%': 11 if (flag == 1) 12 { 13 fputc('%', stream); 14 flag = 0; 15 n ++; 16 } 17 else 18 flag = 1; 19 break; 20 case 'd': 21 if (flag == 1) 22 { 23 itoa((int)va_arg(ap, int), str, 10); 24 ret = fputs(str, stream); 25 n += ret; 26 } 27 else 28 { 29 fputc('d', stream); 30 n ++; 31 } 32 flag = 0; 33 break; 34 case 's': 35 if (flag == 1) 36 { 37 ret = fputs((char *)va_arg(ap, char *), stream); 38 n += ret; 39 } 40 else 41 { 42 fputc('s', stream); 43 n ++; 44 } 45 flag = 0; 46 break; 47 case '\n': 48 /*换行*/ 49 fputc(0x0d, stream); 50 n ++; 51 fputc(0x0a, stream); 52 n ++; 53 break; 54 default: 55 fputc(*format, stream); 56 n ++; 57 } 58 format ++; 59 } 60 return n; 61 }
vfprintf主要麻烦的是对格式化字符串的分析,我们在这里使用一种比较简单的算法:
(1)定义模式:翻译模式/普通模式
(2)循环整个格式字符串
a) 如果遇到%
i 普通模式:进入翻译模式
ii 翻译模式: 输出%, 退出翻译模式
b) 如果遇到%后面允许出现的特殊字符(如d和s)
i 翻译模式:从不定参数中取出一个参数输出,退出翻译模式
ii 普通模式:直接输出该字符串
c) 如果遇到其它字符(除\n):无条件退出翻译模式并输出字符
d) 如果遇到'\n'字符,如果直接输出是不能达到换行的效果的,必须要同时输出回车换行才行
从上面的实现vfprintf的代码中可以看出,并不支持特殊的格式控制符,例如位数、进度控制等,仅支持%d与%s这样的简单转换。真正的vfprintf格式化字符串实现比较复杂,因为它支持诸如“%f”、“%x”已有的各种格式、位数、精度控制等。我觉得上面实现的代码已经充分的展示了vfprintf的实现原理和它的关键技巧,所以没有必要一个一个的都实现。现在来实现printf的就简单多了,下面是printf的实现代码:
1 int printf(const char *format, ...) 2 { 3 int n; 4 va_list ap; 5 6 va_start(ap, format); 7 n = vfprintf(stdout, format, ap); 8 va_end(ap); 9 return n; 10 }
对于可变参数的编程,我已经在c语言中的可变参数编程中详细的讲过了,包括它的实现原理。所以只要了解了可变参数的编程,对于实现printf函数来说就真的没什么难度了,纯粹就是调用vfprintf函数而已。如果实现了printf函数,那么对于实现scanf、fscanf也是同样的原理。
附件:文件操作