1可变参数列表函数实现
(1)原理
C标准库<stdarg.h>提供了一套宏支持函数f可变参数的实现,f中必须含有最后一个(从左到右,看系统对函数参数的入栈顺序)可知的arg。用以下几个工具实现:
- va_list:依次指向f的参数列表中的每一个未知的参数。在f内定义va_list parg。
-
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>的实现可能有所不同。但提供的可用接口都是一致的。
(2)程序例子
《The C Programming Language》中调用printf()本身作为输出函数实现了一个小型的printf(),将这个函数原型定为:void minprintf(char *fmt, …)。fmt称为了函数的最后一个可知参数。
[1]书中程序代码
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);
}
- 可变参数列表函数的声明形式为type function_name(typename, …)。未知参数列表用”…”表示。
-
va_start让ap指向了未知参数的第一个参数。
-
va_arg计算出来要返回的目标参数(type类型)的步长并将其返回,并将ap指向下一个参数。va_arg返回的参数不会重复。
-
va_end在所有的操作结束后,趁函数未结束做清理工作。
[2]自己的例子
检验一下自己是否将可变参数函数的任意一宏理解错误。
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有没有操作成功,捎带沮丧。
[3]总结
可变参数列表函数实现的头文件头文件<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清理的应该是存储函数字符串的堆内存吧。
2遍历目录
书中遍历目录例是在unix的系统调用下完成的,在现在的linux系统之上也可以用这些系统调用函数。程序中调用的opendir()、readdir()函数属unix内核函数。系统调用函数的参数涉及的是一些关于unix文件属性的结构体。书中将函数、结构体都解释的很完整。没有安装unix/linux系统,按照系统调用的思路,通过调用windows API来完成windows之上的目录遍历。Codeblocks + MinGW组合就能够调用windows APIS。
(1) 目录文件数据结构
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>。
(2) 命令行参数和函数实参的区别
命令行参数”F:\\*”与函数实参”F:\\*”的区别:在命令行下F盘内有多少个子文件”F:\\*”就代表多少个参数,命令行会将通配符’*’代表的内容展开,每个子文件名会代表一个参数;函数实参”F:\\*”不会将通配符展开,程序要调用相关函数才能找到”F:\\”下找到’*’代表的所有子文件。
(3) windows实现目录遍历
在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调试正确。现在的主要工作不是这个。
(4) Linux/windows目录遍历区别
不同点:系统调用函数不同,描述文件结的结构体不同。
相同点:描述文件结构体包含的信息差不多。都会体现一点关于递归的思想,而且是程序实现的关键地方。
如果需要进一步体会遍历目录的思想,可以自动编写opendir()、readdir()或FindFirstFile()、FindNextFile()函数。只要理清了遍历目录和递归过程之间的联系,都可以将遍历目录的程序编写出来。当然,要将遍历目录的程序应用于实际,要考虑的其它因素(如异常、数据)甚至比程序功能实现要复杂得多。
C Note Over。