引用:
原帖由 OwnWaterloo 于 2008-11-8 16:15 发表 
这例子还是有点问题, 在输入超过缓冲区大小后, 不会溢出, 但是会输入到下一个name中。
稍后改正 ……
char *
fgets (
char * str,
int num,
FILE * stream ); 是带有
缓冲区长度检查的C/C++
标准库函数。
强调
标准,意思是MSVC从7还是8开始, 提供了一套CRT安全版本,全都带有缓冲区长度检查。
首先,
绝对不能使用不带缓冲区长度检查的函数,
即使它们是标准库函数。
如果标准库函数中剩下的函数不能完成工作,
即使牺牲可移植性, 也要使用带有缓冲区检查的非标准扩展函数, 如上面提到的。
详细文档可以见
http://www.cplusplus.com/reference/clibrary/cstdio/fgets.html
这里对它的一些特点作点解释:
一. num 是缓冲区大小
char buf[ n ];
fgets(buf, sizeof(buf)/ sizeof(buf[0]) /*
不需要减1 */ , stdin ); 是可以的, 不会溢出
二. 终止行为
当fgets被调用时, 会从stream读取字符填充到str中, 直到以下情况之一:
1. 读取到换行符
'\n' , fgets将其填入str,并且继续填充1个
null-character '\0', 然后返回str
2. 已经读取了 num-1个非换行字符, (也就是 str[num-2] 被填充了) fgets将填充 str[num-1] = '\0'; 并返回 str
3. 文件结束 ,返回0,表示错误
也就是说, (只要调用者传参正确) 该函数
绝对不会溢出缓冲区
在没有发生错误的时候,保证str是一个
c-style-string,
不丢弃换行符。
三. 对于stdin
stdin相对于其他FILE* 有所区别, stdin一般不会end of file, (允许ctrl+z中断,并且输入ctrl+z ,或者重定向)
当stdin发生end of file时, 会将控制交给console, 继续读入数据。
所以需要注意 fgets(str,num, stdin);
之前是否残留有字符,
之后是否残留有字符。
比如:
/* 1 */ printf("input an integer: ");
/* 2 */ scanf("%d",&i);
/* 3 */ printf("input a string: ");
/* 4 */ fgets(buf, sizeof(buf)/sizeof(buf[0]), stdin);
/* 5 */ printf("integer=%d string=\"%s\"\n",i,buf);
假设在 2 处 的scanf导致控制转到console, 用户输入一个数之后, 必须敲回车, 完成输入, 就将在stdin中残留字符(0个或多个字符,1个换行符)。
而 4 处的fgets 取出的是这部分残留字符, 而不会将控制再次转到console, 这也许并不是希望的结果。
/* 6 */ printf("input 1st person's name: ");
/* 7 */ fgets(buf, sizeof(buf)/sizeof(buf[0]), stdin);
/* 8 */ printf("input 1st person's name: ");
/* 9 */ fgets(buf, sizeof(buf)/sizeof(buf[0]), stdin);
假设 7 处的fgets导致控制转到console, 用户输入的字符串长度超出buf大小, fgets将只取前一部分, 并且遗留字符在stdin中。
而 9 处的fgets不再将控制转到console, 而是直接取出上次遗留的字符。
也就是引用部分提到的问题,也许仍然不是希望的结果。
四. 怎么办?
“要将input做得very robust是很繁琐的。” —— 烧饼
确实是这样 ……
在C++中, 可以利用
std::
string, std::
getline, std::
istringstream 做到robust。
而C就更繁琐了 ……
下面也许是一个比较灵活的方案。
typedef int ( *
getline_callback )(
const char* str,
size_t len,
int more,
void* data );
int getline_c_version(
getline_callback callback ,
FILE* stream,
void* data) {
assert( callback && stream && "pre condition failed" );
char buf[ N ];
char* r = 0;
size_t len = 0;
int more = 0;
int cont = 0;
for ( ; ; ) {
if (0==fgets( buf, sizeof(buf)/sizeof(buf[0]), stream ) )
return
EOF;
len = strlen( buf ); /* 这里我觉得是fgets的一个严重失误, 如果设计成返回被填充的最后一个字符, 就立刻能得到长度,同时也可以返回0表示EOF, 为什么需要长度下面会说明 */
assert( 1<=len ); /* 在没有数据的时候 fgets将返回EOF,否则 至少有一个 '\n' */
more = (int)buf[ len-1 ] - (int)'\n'; /* 必须求出长度,测试最后一个字符是否是换行,才知道是读取完整行,还是缓冲区不够被截断 */
/* 在其他很多时候,也是需要长度的,比如马上需要malloc来复制一份。 而且fgets内部必然是知道长度的,为什么不返回给调用者呢 …… */
cont = callback( buf, len, more, data ) /* 回调,用户能得到字符串首地址,长度,该行是否有更多字符,以及用户传入的数据 */
if ( !more ) /* 读取完毕 */
return
GET_WHOLE_LINE;
if ( !cont ) /* 回调可以返回0 ,表示希望终止而非继续读取整行 */
return
USER_ABORT;
}
assert( !"shouldn't get here" );
return
ERROR;
}
之后可以定义一个 :
int
ignore(const char* /*str*/, size_t /*len*/, int /*more*/, void* /*data*/ ) {
return 1;
}
getline_c_version( ignore, stdin, 0 ); 就可以插在上面的 /* 2 */ 和 /* 3 */ 以及 /* 8 */ 和 /* 9 */之间。
对于需要固定长度的用户, 可以选择只取前面部分 :
typedef struct get_first_data_tag {
char* dst;
size_t len;
int get;
} get_first_data;
int
get_first(const char* str, size_t len, int /*more*/, void* data ) {
get_first_data* p = data;
if ( !p->get ) {
p->get = 1;
if ( p->len < len ) {
strncat( p->dst, str, p->len-1);
p->dst[ p->len-1 ] = '\0';
}
else {
strncat( p->dst, str, len );
p->len = len;
}
}
return 1;
}
char str[10];
get_first_data d;
d.dst = str;
d.len = 10;
d.get = 0;
getline_c_version( get_first, stdin, &d );
char* str = malloc( 20 );
get_first_data d;
d.dst = str;
d.len = 20;
d.get = 0;
getline_c_version( get_first, stdin, &d );
free( str );
如果需要取得整一行, 肯定需要用C实现一个可动态增长的结构:
typedef struct get_whole_data_tag {
std:: string s; // 由于功底薄弱 …… 这里用C++表示 ……
} get_whole_data;
int get_whole(const char* str, size_t len, int more, void* data ) {
get_whole_data* p = data;
p->s += str;
return 1;
}
get_whole_data d;
getline_c_version( get_whole, stdin, &d );
// do some work with d.s.c_str()
[
本帖最后由 OwnWaterloo 于 2008-11-12 03:16 编辑 ]