10个C库函数的那些坑
函数是C语言的基石,函数库是面向过程编程语言的重要组成部分。
通常,使用库函数只需要理解其接口即可,接口和注释通常会较友好地透露如何使用的细节,而一些库函数手册还会增加一些有代表性的实例,以加强用户对其理解。但一些库函数在使用时出错频率较高,原因是实现时在安全、易使用性、效率等方面需要有所取舍,要避坑及深入理解其原因,自然需要深入理解库函数背后实现的细节。
1 int scanf(char* format, ptrParaList) (stdio.h)
Read from standard input under control of the format string; equivalent to fscanf(stdin, char* format, args)
① 全部参数为指针(第一个参数是字符指针);
② 读取整型或浮点型数据时会忽略掉前面的空白字符(以空白字符(isspace())为分隔);
2 int fscanf(FILE* f, char* format, ptrParaList) (stdio.h)
Read from the file under control of the format string.
当试图读取文件中的一行以空白字符分隔的字符串时,字符串并不以空白字符做分隔,而是将空白字符做为普通字符一起读取。如果以空白字符分隔分隔的字符串长度固定,可以指定固定长度读取,否则需要先fgets(),然后使用strtok()来处理。sscanf()也是如此。例如要读取下面的文本:PKL071 Park-Lens 8 6.50 BLJ372 Ball-Joint 12 11.95 PKL073 Park-Lens 8 6.50 FLT014 Oil-Filter 23 7.95 DKP085 Disc-Pads 16 9.99 GSF556 Gas-Filter 9 4.50 FLT017 Oil-Filter 23 7.95
以下代码直接报错:typedef struct partRecord { char partNum[PartNumSize + 1]; char name[MaxName + 1]; int amtInStock; double unitPrice; } PartRecord; FILE *ftxt, *fbin; PartRecord part; if((ftxt = fopen("parts.txt", "r")) == NULL) { printf("Cannot open parts file "); return -1; } while(fscanf(ftxt, "%s %s %d %f", part.partNum, part.name, &part.amtInStock, &part.unitPrice) == 4) { printf("%s %s %d %f ", part.partNum, part.name,part.amtInStock, part.unitPrice); if(fwrite(&part, sizeof(PartRecord), 1, fbin) != 1) { printf("Error in writing file "); return -1; } }
以下代码没有正确读取:const int MAXCHAR = 128; char str[MAXCHAR]={0}; for(int i=0;i<7;i++) { fgets(str,MAXCHAR,ftxt); sscanf(str, "%s %s %d %f", part.partNum, part.name,&part.amtInStock, &part.unitPrice); printf("%s %s %d %f ", part.partNum, part.name,part.amtInStock, part.unitPrice); if(fwrite(&part, sizeof(PartRecord), 1, fbin) != 1) { printf("Error in writing file "); return -1; } }
以下代码OK:const int MAXCHAR = 128; char str[MAXCHAR]={0}; for(int i=0;i<7;i++) { fgets(str,MAXCHAR,ftxt); //sscanf(str, "%s %s %d %f", part.partNum, part.name,&part.amtInStock, &part.unitPrice); strcpy(part.partNum,strtok(str," ")); strcpy(part.name,strtok(NULL," ")); part.amtInStock = atoi(strtok(NULL," ")); part.unitPrice = atof(strtok(NULL," ")); printf("%s %s %d %f ", part.partNum, part.name,part.amtInStock, part.unitPrice); if(fwrite(&part, sizeof(PartRecord), 1, fbin) != 1) { printf("Error in writing file "); return -1; } }
3 char fgets(char s, int n, FILE* f) (stdio.h)
Read at most n-1 characters into the s array; stops at a newline, which is included in the array. The array is automatically terminated with a . char buf[6]; fgets(buf,6,stdin); // input:abcdef fputs(buf,stdout); // output:abcde
4 char* strcpy(char* d, char* s) (string.h)
Copy string s to string d
strcpy并没有边界检查,d所指缓冲区所有足够空间,否则会溢出。
5 char* strcmp(char* d, char* s) (string.h)
Compare string d to string s; return 0 if d>s
注意两字符串比较相等的结果是0而不是其它值。
6 char* strlen(char* d) (string.h)
Return the length of string d, not including the terminating NULL.
strlen函数的实现以" ‘为结束标志printf("%d ",strlen("s 16 end")); // 3
7 void* memcpy(void* d, void* s, int n) (string.h)
Copy n characters from s to d.
memcpy并没有检查d和s所指区域在复制时是否存在重叠情况,如果存在重叠,会出错。
memmove()有考虑重叠的情况。
8 void * memset(void *d, int c, size_t n); (string.h)
Fill d with n occurrence of c.
注意其其参数类型,以下操作通常没有问题:
① 以单字节数据(字符或零值)填充字符串;
② 以布尔值填充布尔数组;
③ 以零值填充数组;
以下操作通常不是预想的结果:
① 以非单字节数据填充数组;
② 以单字节数组(非零值)填充非字节数据的数组;
③ 以整型数据1填充整形数组;
原因是memset的实现是以字节为单元去填充内存空间的:/*** *char *memset(dst, val, count) - sets "count" bytes at "dst" to "val" * *Purpose: * Sets the first "count" bytes of the memory starting * at "dst" to the character value "val". * *Entry: * void *dst - pointer to memory to fill with val * int val - value to put in dst bytes * size_t count - number of bytes of dst to fill * *Exit: * returns dst, with filled bytes **********************************************************************/ void * __cdecl memset ( void *dst, int val, size_t count ) { void *start = dst; while (count--) { *(char *)dst = (char)val; dst = (char *)dst + 1; } return(start); }
9 char * strtok(char *string, const char *control ) (string.h)
细节:
① 分割字符串时,需要多次调用以依次返回被分割后的子串,子串结尾会附以" ";
② 非第一次调用strtok()时,其首参以NULL开始,其内部实现以一个静态变量记录了下次开始分割的目标字符串的位置;/*** *strtok.c - tokenize a string with given delimiters *Purpose: * defines strtok() - breaks string into series of token * via repeated calls. * ****************************************************************************/ /*** *char *strtok(string, control) - tokenize string with delimiter in control * *Purpose: * strtok considers the string to consist of a sequence of zero or more * text tokens separated by spans of one or more control chars. the first * call, with string specified, returns a pointer to the first char of the * first token, and will write a null char into string immediately * following the returned token. subsequent calls with zero for the first * argument (string) will work thru the string until no tokens remain. the * control string may be different from call to call. when no tokens remain * in string a NULL pointer is returned. remember the control chars with a * bit map, one bit per ascii char. the null char is always a control char. * *Entry: * char *string - string to tokenize, or NULL to get next token * char *control - string of characters to use as delimiters * *Exit: * returns pointer to first token in string, or if string * was NULL, to next token * returns NULL when no more tokens remain. *******************************************************************************/ char * strtok ( char * string, const char * control ) { unsigned char *str; const unsigned char *ctrl = control; unsigned char map[32]; int count; static char *nextoken; for (count = 0; count < 32; count++) map[count] = 0; /* Set bits in delimiter table */ do { map[*ctrl >> 3] |= (1 << (*ctrl & 7)); } while (*ctrl++); /* Initialize str. If string is NULL, set str to the saved * pointer (i.e., continue breaking tokens out of the string * from the last strtok call) */ if (string) str = string; else str = nextoken; /* Find beginning of token (skip over leading delimiters). Note that * there is no token iff this loop sets str to point to the terminal * null (*str == " ") */ while ( (map[*str >> 3] & (1 << (*str & 7))) && *str ) str++; string = str; /* Find the end of the token. If it is not the end of the string, * put a null there. */ for ( ; *str ; str++ ) if ( map[*str >> 3] & (1 << (*str & 7)) ) { *str++ = " "; break; } /* Update nextoken (or the corresponding field in the per-thread data * structure */ nextoken = str; /* Determine if a token has been found. */ if ( string == str ) return NULL; else return string; }
10 isblank() and isspace() (ctype.h)
int isblank ( int c );
Check if character is blank
Checks whether c is a blank character .
A blank character is a space character used to separate words within a line of text.
The standard "C" locale considers blank characters the tab character (" ") and the space character (" ").
Other locales may consider blank a different selection of characters, but they must all also be space characters by isspace.
int isspace ( int c );
Check if character is a white-space.
Checks whether c is a white-space character .
For the "C" locale, white-space characters are any of:
" "
(0x20)
space (SPC)
" "
(0x09)
horizontal tab (TAB)
" "
(0x0a)
newline (LF)
"v"
(0x0b)
vertical tab (VT)
"f"
(0x0c)
feed (FF)
"r"
(0x0d)
carriage return (CR)
Other locales may consider a different selection of characters as white-spaces, but never a character that returns true for isalnum.
11 其它
11.1 setbuf() 用来设置缓冲区特性,如果需要改变缓冲的特点和大小等,使用该调用。
11.2 fflush(stdin)、 fflush(stdout) 用来强制刷新缓冲区数据。如果需要在每次i/o操作前后,不希望缓冲中存在历史数据或者不期望的数据或者为了清除缓存等的时候使用。
11.3 setbuf(stdin,NULL);
把输入缓冲区设置为无缓冲,直接从流读取数据。
11.4 EOF的值是由fgetc()、fgets()动态设置,需要由此类函数调用后返回后的字符与EOF比较判断。
-End-