从第二章开始开始有难度了,单靠看书是不行的,许多知识点需要自己查阅相关资料方能理解。个人认为该章难度主要集中在几个API函数运用上
在windows编程中不能使用pirntf,但能使用sprintf,vsprintf系列
int sprintf( char *buffer, const char *format [,argument] ... );
int vsprintf( char *buffer, const char *format, va_list argptr );
两者区别在于如果需要写一个自定义的可变参数的函数,则应该用vsprintf,因为该函数有个指向格式化参数数组的指针(注意在写可变参数函数时一般定义为CDCEL,特别注意的是不能用__stdcall,因为__stdcall要由被调用者函数自动还原堆栈,而CDCEL是由调用者函数负责还原堆栈,详细可查阅函数命名调用约定),但在自几写的可变参数函数里如何得到格式化参数的指针呢,从书上p33发现作者运用了va_list,va_start,va_end宏,但看了书上的例子之后总觉得还是云里雾里,于是查看了<stdarg.h>关于宏的定义,"Yes,I got it",我不禁欢呼,原来在该头文件中主要定义了5个宏,下面分别阐述一下:
va_list宏:
书上p33的用法:va_list pArgs;
<stdarg.h>中的定义:
#ifndef _VA_LIST_DEFINED
#ifdef _M_ALPHA
typedef struct {
char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;
#else
typedef char * va_list;
#endif
#define _VA_LIST_DEFINED
#endif
pArgs为char*类型,是指向格式化参数的指针类型,pArgs作为结构体类型什么时候用到还不清楚。
va_start(ap,v)宏:
书上p33的用法:va_start(pArgs,szFormat)
<stdarg.h>中的定义:
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
定义_INTSIZEOF(v)宏主要是为了某些需要内存的对齐的系统.C语言的函数是从右向左压入堆栈的,下图是函数的参数在堆栈中的分布位置,看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是最后一个固定参数在堆栈的地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址。
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
va_end(ap)宏:
书上p33的用法:va_end(pArgs)
<stdarg.h>中的定义:
#define va_end(ap) ( ap = (va_list)0 )
该宏用来将指向格式化参数指针赋为NULL,来杜绝野指针。
va_arg(ap,t)宏:
用法:
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;
va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d/n", i, j);
return;
}
void main()
{
simple_va_fun(100,200);
}
<stdarg.h>中的定义:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
首先ap+=_INTSIZEOF(t),已经指向下一个参数的地址了.然后返回ap-_INTSIZEOF(t)的int*指针,这正是第一个可变参数在堆栈里的地址(如下图所示).然后用*取得这个地址的内容(参数值)赋给j。
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg后ap指向
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
_INTSIZEOF(n)宏:
<stdarg.h>中的定义:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,在va_start(ap,v)宏和va_arg(ap,t)宏定义中用到,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。
vsprintf和sprintf使用时有两个要注意的地方:
1.与printf一样当格式字符串与被格式化的变量不匹配时,可能会运行错误并可能造成程序崩溃。
2.用户的自定义的字符缓冲区必须足够大来存放结果,否则会造成缓冲区溢出(非法内存访问)。
所以由此引申出它们的第一个变种:最大长度版
int _snprintf( char *buffer, size_t count, const char *format [, argument] ... );
int _vsnprintf( char *buffer, size_t count, const char *format, va_list argptr );
它们比原来多了一个参数,该参数用来表明用户定义的缓冲区能够容纳的字符数,如果缓冲区已经到达count个字符,则剩余字符不予复制,如果剩余字符(不包括'/0')大于0,则该函数返回-1,否则返回总的copy字符数(不包括'/0'),所以说最大长度版相对比较安全,但效率相对较慢(在缓冲区能够容纳格式化字符数时至少是这样),以下是我做的一个试验程序:
1。#include <stdio.h>
void main()
{
char szBuffer1[2];
int iReturn1;
iReturn1=sprintf(szBuffer1,"abc"); //非法访问了两块内存,并赋值为c和'/0'
printf("szBuffer1=%s,iReturn1=%d/n",szBuffer1,iReturn1);
}
2。#include <stdio.h>
void main()
{
char szBuffer1[2];
int iReturn1;
iReturn1=_snprintf(szBuffer1,2,"abc"); //只赋值了a和b
printf("szBuffer1=%s,iReturn1=%d/n",szBuffer1,iReturn1);
}
但不是说这样就安全了,前提是我们一定要参数count传递正确:
1.如果count>缓冲区实际能容纳的字符数,则仍就可能造成缓冲区溢出。
2.如果count<缓冲区实际能容纳的字符数,则可能会造成没有完全利用内存空间。
前面的四个函数都要包括stdio.h头文件,属于c运行时函数,如果我只想用一个windows.h头文件怎么办,于是有了sprintf,vsprintf第二个变种:windows版
int wsprintfA( char *buffer, const char *format [,argument] ... );
int wvsprintfA( char *buffer, const char *format, va_list argptr );
他们功能上基本与sprintf和vsprintf等价,但不能处理浮点格式(搞不懂微软在干什么?-:))
所以上述三类函数各有优缺点,他们分别又有ascii和unicode以及通用(视编译设置而定)三种,可参看p33表2-1。值得注意的是在通用函数中如果要按unicode编译则要考虑两种情况,如果该函数是c语言运行时函数,则需要指定_UNICODE,而windows函数需要指定UNICODE。