C标准库<stdarg.h>提供了一套宏支持函数f可变参数的实现,f中必须含有最后一个(从左到右,看系统对函数参数的入栈顺序)可知的arg。用以下几个工具实现:
va_start:在parg开始指向未知参数之前必须使用”va_start(parg, arg);”使parg指向指向第一个(与已知参数紧接着的一个)未知的参数。
va_arg:在” va_start(parg, arg);”语句后,每执行一次va_arg(parg,type)就会返回对应的type类型参数值,并将parg指向下一个未知参数。va_arg指向的参数不会重复。
va_end:在参数遍历完毕且在退出函数f之前必须执行一遍”va_end(parg)”做清理工作。
不同类型的机子之上,<stdarg.h>的实现可能有所不同。但提供的可用接口都是一致的。
《The C Programming Language》中调用printf()本身作为输出函数实现了一个小型的printf(),将这个函数原型定为:void minprintf(char *fmt, …)。fmt称为了函数的最后一个可知参数。
void minprintf(char *fmt, ...) { va_list ap; char *p, *sval; int ival; double dval; va_start(ap, fmt); for(p = fmt; *p; ++p){ if(*p != '%'){ putchar(*p); continue; } switch(*++p){ case 'd': ival = va_arg(ap, int); printf("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; case 's': for(sval = va_arg(ap, char *); *sval; ++sval){ putchar(*sval); } break; default: putchar(*p); break; } } va_end(ap); }
va_start让ap指向了未知参数的第一个参数。
va_arg计算出来要返回的目标参数(type类型)的步长并将其返回,并将ap指向下一个参数。va_arg返回的参数不会重复。
va_end在所有的操作结束后,趁函数未结束做清理工作。
检验一下自己是否将可变参数函数的任意一宏理解错误。
void my_add(int n, ...) { char i; int sum = 0; va_list ap; va_start(ap, n); for(i = 0; i < n; ++i){ sum += va_arg(ap, int); printf("%d ", sum); } va_end(ap); printf("\n"); }
在main(MinGW编译器)中调用” my_add(3,3, 2, 1); “语句,调试并运行程序得到一下结果:
从程序执行结果可以明确在MinGW编译器下:
va_start初始化ap指向的未知参数列表的第一个参数是最后一个参数的邻居。还有就是若va_arg在没有定位到指定类型的参数时,没有特别的返回值。故而都不知道va_arg有没有操作成功,捎带沮丧。
可变参数列表函数实现的头文件头文件<stdarg.h>提供给用户的核心功能是va_arg(ap, type)。此宏能够根据用户指定的type从左到右(猜:跟系统执行语句的顺序一致),查找到对应的属type类型的实参值并将其返回给用户。所以在未知参数type的情况下,是用不好<stdarg.h>的(因为va_arg在操作失败后没有特定的返回值),它们针对的对象是调用中的函数。所以,<stdarg.h>很大程度上适用于编写已知所有未知参数类型(type)情况的函数,如printf()、scanf()等,对于非此类函数,操用<stdarg.h>来使用的意义不大。
对于<stdarg.h>的实现,个人猜想为首先获取当前执行函数的地址实现。从此地址内的字符串中解析到参数从而实现va_list。再利用用户传递的最后一个可知参数实现va_start。在根据用户给的type实现va_arg。va_end清理的应该是存储函数字符串的堆内存吧。
书中遍历目录例是在unix的系统调用下完成的,在现在的linux系统之上也可以用这些系统调用函数。程序中调用的opendir()、readdir()函数属unix内核函数。系统调用函数的参数涉及的是一些关于unix文件属性的结构体。书中将函数、结构体都解释的很完整。没有安装unix/linux系统,按照系统调用的思路,通过调用windows API来完成windows之上的目录遍历。Codeblocks + MinGW组合就能够调用windows APIS。
Unix/Linux之上用来描述一个目录的结构体名为DIR,包含每个目录的“描述符”、“节点数”及“目录名”等属性,主要作为readdir()的参数。用名为stat的结构体来描述文件(这个文件有可能只是一个文件如.txt,.c;也有可能还是一个目录)的“节点数”、“当前文件是真正意义上的文件还是目录”、“大小”、“文件最后被访问的时间”、“文件最后被更改的时间”、“文件被创建的时间”等属性,主要作为stat()函数的参数。所有的结构体和函数都属于内核内容,被定义在相应的<sys/type.h>,<sys/stat.h>头文件中。
在windows之上,用结构体WIN32_FIND_DATA描述文件或目录属性。WIN32_FIND_DATA结构体包含“文件属性:文件 or目录等”、“文件被建立的时间”、“文件最后被访问的时间”、“文件最后被修改的时间”、“文件大小”、“文件名”等属性,供FindFirstFile()函数作为参数使用,在windows 7之上,需要包含的头文件为<windows.h>。
命令行参数”F:\\*”与函数实参”F:\\*”的区别:在命令行下F盘内有多少个子文件”F:\\*”就代表多少个参数,命令行会将通配符’*’代表的内容展开,每个子文件名会代表一个参数;函数实参”F:\\*”不会将通配符展开,程序要调用相关函数才能找到”F:\\”下找到’*’代表的所有子文件。
在windows之上的目录遍历同样可以调用系统内核函数windows APIS之FindFirstFile()及FindNextFile()。
HANDLE WINAPI FindFirstFile(_In_ LPCTSTRlpFileName, _Out_ LPWIN32_FIND_DATA lpFindFileData)是获取lpFileName确定的文件或目录的属性于lpFindFileData结构体中。若lpFileName以通配符的形式存在,则函数先找到第一个文件的属性于lpFindFileData结构体中,然后返回一个“文件搜索句柄”,此句柄供FindNextFile()函数作为参数查询下一个文件的依据(标识)。
BOOL WINAPI FindNextFile(_In_ HANDLEhFindFile, _Out_ LPWIN32_FIND_DATA lpFindFileData)函数的hFindFile参数是FindFirstFile()返回值-----“文件搜索句柄”。只有在目录集下,如F盘下具有众多文件,”F:\\*”传递给lpFileName后FindFirstFile就可以返回“文件搜索句柄”。FindNextFile以此“文件搜索句柄”继续搜索,将文件夹下搜索到的下一个文件或目录属性给lpFindFileData结构体返回,直到将F盘下所有的文件搜索完毕。
FindFirstFile()与FindNextFile()的组合可以将一个目录下的所有文件搜索到。以目录为对象,分析一下访问目录的递归过程:
对于递归,关键理解递归函数的递归过程,将递归过程和函数调用过程结合到一起,简单的递归操作还是很容易编写的。
代码:
#include <stdio.h> #include <stdlib.h> #include <Windows.h> int travle_dir(const char *path_name); int main(int argc, char *argv[]) { if(argc < 2) { printf("win_travle_directory USAGE: win_travle_directory dir_path1, ...\n"); return -1; } while(--argc) { //Travle the argument directory printf("\n--------Now travle %s dir--------\n", argv[argc]); travle_dir(argv[argc]); } return 0; } //Travle current directory int travle_dir(const char *path_name) { if(!path_name){ printf("NULL DIR\n"); return -1; } WIN32_FIND_DATA ffd; LPSYSTEMTIME ptime; FILETIME ftLocal; HANDLE hFind; LARGE_INTEGER filesize; char dir[MAX_PATH]; char cdir[MAX_PATH]; hFind = INVALID_HANDLE_VALUE; strcpy(dir, path_name); strcat(dir, "\\*"); hFind = FindFirstFile(dir, &ffd); if(INVALID_HANDLE_VALUE == hFind){ printf("%s opened failed\n", dir); return -1; } do{ if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){ //Travle child directory if(strcmp(ffd.cFileName, ".") && strcmp(ffd.cFileName, "..")){ strcpy(cdir, path_name); strcat(cdir, "\\"); strcat(cdir, ffd.cFileName); travle_dir(cdir); printf("--------%s <DIR>--------\n\n", ffd.cFileName); } }else{ //Filename printf(" %-25s", ffd.cFileName); //Size filesize.LowPart = ffd.nFileSizeLow; filesize.HighPart = ffd.nFileSizeHigh; printf(" %-10ld bytes\n", filesize.QuadPart); } }while(0 != FindNextFile(hFind, &ffd)); FindClose(hFind); return 0; }
核心是采用Windows API FindFirst()和FindNextFile()两个函数、递归思想及windows目录知识完成的程序。根据程序运行结果不断纠正程序即可,不用专门对应去学习每个模块(递归思想除外)。
程序执行结果:
在win32的cmd下执行。将检索到的文件的“文件名”和“大小属性”输出。如“演讲<DIR>”及“Princess…<DIR>”是”F:moves”下的目录,二者之上的内容是自己目录里面的所有文件。
程序中未对过多的异常进行处理,再次体现以下比较简单的递归思想。没有将文件的其它属性输出来,原因之一是cmd或者控制台的窗口太小,输出来也不一定好看。原因之二是有的windows APIs的用法依赖性比较大,需要狠狠的调试才能将windows API调试正确。现在的主要工作不是这个。
不同点:系统调用函数不同,描述文件结的结构体不同。
相同点:描述文件结构体包含的信息差不多。都会体现一点关于递归的思想,而且是程序实现的关键地方。
如果需要进一步体会遍历目录的思想,可以自动编写opendir()、readdir()或FindFirstFile()、FindNextFile()函数。只要理清了遍历目录和递归过程之间的联系,都可以将遍历目录的程序编写出来。当然,要将遍历目录的程序应用于实际,要考虑的其它因素(如异常、数据)甚至比程序功能实现要复杂得多。
C Note Over。