1、字符串操作函数
1.1、初始化字符串
#include <string.h>
void *memset(void *s, int c, size_t n);
返回值:s指向哪,返回的指针就指向哪
memset 函数把 s 所指的内存地址开始的 n 个字节都填充为 c 的值。
通常 c 的值为0,把一块内存区清零,例如定义 char buf[10]。
如果它是全局变量或静态变量,则自动初始化为0(位于 .bss 段)
如果它是函数的局部变量,则初值不确定,可以用 memset(buf, 0, 10) 清零
由 malloc 分配的内存初值也是不确定的,也可以用 memset 清零。
1.2、获取字符串长度
#include <string.h>
size_t strlen(const char *s);
返回值:字符串的长度
该函数从 s 所指的第一个字符开始找 '\0' 字符,一旦找到就返回,返回的长度不包括 '\0' 字符在内
1.3、拷贝字符串
#include <string.h>
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
返回:dest指向哪,返回的指针就指向哪
strcpy 和 strncpy 函数:
拷贝以 '\0' 结尾的字符串, strncpy 还带一个参数指定最多拷贝多少个字节,此外, strncpy 并不保证缓冲区以 '\0' 结尾
memcpy 函数:
从 src 所指的内存地址拷贝 n 个字节到 dest 所指的内存地址
和 strncpy 不同, memcpy 并不是遇到 '\0' 就结束,而是一定会拷贝完 n 个字节
memmove函数:
也是从 src 所指的内存地址拷贝 n 个字节到 dest 所指的内存地址,虽然叫move但其实也是拷贝而非移动。但是和 memcpy 有一点不同, memcpy 的两个参数 src 和 dest 所指的内存区间如果重叠则无法保证正确拷贝,而 memmove 却可以正确拷贝。
#include <string.h>
char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);
返回值:dest指向哪,返回的指针就指向哪
strcat 和 strcpy 有同样的问题,调用者必须确保 dest 缓冲区足够大,否则会导致缓冲区溢出错误。
strncat 函数通过参数 n 指定一个长度,就可以避免缓冲区溢出错误。注意这个参数 n 的含义和 strncpy 的参数 n 不同,它并不是缓冲区 dest 的长度,而是表示最多从 src 缓冲区中取 n 个字符(不包括结尾的 '\0' )连接到 dest 后面。如果 src 中前 n 个字符没有出现 '\0' ,则取前 n 个字符再加一个 '\0' 连接到 dest 后面,所以 strncat 总是保证 dest 缓冲区以 '\0' 结尾
1.4、比较字符串
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
返回值:负值表示s1小于s2,0表示s1等于s2,正值表示s1大于s2
memcmp 从前到后逐个比较缓冲区 s1 和 s2 的前 n 个字节(不管里面有没有 '\0' )
strcmp 把 s1 和 s2 当字符串比较,在其中一个字符串中遇到 '\0' 时结束
strncmp 的比较结束条件是:要么在其中一个字符串中遇到 '\0' 结束(类似于 strcmp ),要么比较完 n 个字符结束(类似于 memcmp )
1.5、搜索字符串
#include <string.h>
char *strchr(const char *s, int c);
char *strrchr(const char *s, int c);
返回值:如果找到字符c,返回字符串s中指向字符c的指针,如果找不到就返回NULL
char *strstr(const char *haystack, const char *needle);
返回值:如果找到子串,返回值指向子串的开头,如果找不到就返回NULL
1.6、分割字符串
#include <string.h>
char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);
返回值:返回指向下一个Token的指针,如果没有下一个Token了就返回NULL
delim 是分隔符,可以指定一个或多个分隔符, strtok 遇到其中任何一个分隔符就会分割字符串
使用举例
#include <string.h>
#include <stdio.h>
int main(void)
{
char str[] = "root:x::0:root:/root:/bin/bash:";
char *token;
token = strtok(str, ":");
printf("%s\n", token);
while ((token = strtok(NULL, ":")) != NULL) {
printf("%s\n", token);
}
return 0;
}
第一次调用要把字符串首地址传给 strtok 的第一个参数,以后每次调用第一个参数只要传 NULL 就可以了, strtok 函数自己会记住上次处理到字符串的什么位置(这是通过 strtok 函数中的一个静态指针变量记住的)。
2、标准IO库函数
2.1、fopen/fclose
#include <stdio.h>
File *fopen(const char *path, const char *mode);
返回值:成功返回文件指针,出错返回NULL并设置errno
int fclose(FILE *fp);
返回值:成功返回0,出错返回EOF并设置errno
"r"
只读,文件必须已存在
"w"
只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节再重新写,也就是替换掉原来的文件内容
"a"
只能在文件末尾追加数据,如果文件不存在则创建
"r+"
允许读和写,文件必须已存在
"w+"
允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写
"a+"
允许读和追加数据,如果文件不存在则创建
2.2、perror和errno
#include <stdio.h>
void perror(const char *s);
char *strerror(int errnum);
返回值:错误码errnum所对应的字符串
perror 函数将错误信息打印到标准错误输出,首先打印参数 s 所指的字符串,然后打印:号,然后根据当前 errno 的值打印错误原因
2.3、以字节为单位的IO函数
#include <stdio.h>
int fgetc(FILE *stream);
int getchar(void);
返回值:成功返回读到的字节,出错或者读到文件末尾时返回EOF
int fputc(int c, FILE *stream);
int putchar(int c);
返回值:成功返回写入的字节,出错返回EOF
fgetc 函数从指定的文件中读一个字节, getchar 从标准输入读一个字节
fputc 函数向指定的文件写一个字节, putchar 向标准输出写一个字节
2.4、操作读写位置的函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
返回值:成功返回0,出错返回-1并设置errno
long ftell(FILE *stream);
返回值:成功返回当前读写位置,出错返回-1并设置errno
void rewind(FILE *stream);
把读写位置移动到文件开头
whence 参数的含义如下:
SEEK_SET
从文件开头移动 offset 个字节
SEEK_CUR
从当前位置移动 offset 个字节
SEEK_END
从文件末尾移动 offset 个字节
offset 可正可负
负值表示向前(向文件开头的方向)移动,如果向前移动的字节数超过了文件开头则出错返回
正值表示向后(向文件末尾的方向)移动,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸,从原来的文件末尾到 fseek 移动之后的读写位置之间的字节都是0
2.5、以字符串为单位的IO函数
include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s); //Man Page说明了,永远不要使用此函数
返回值:成功时s指向哪返回的指针就指向哪,出错或者读到文件末尾时返回NULL
int fputs(const char *s, FILE *stream);
int puts(const char *s);
返回值:成功返回一个非负整数,出错返回EOF
fgets 从指定的文件中读一行字符到调用者提供的缓冲区中,
gets 从标准输入读一行字符到调用者提供的缓冲区中,Man page说明了,永远不要使用此函数
fputs 向指定的文件写入一个字符串,
puts 向标准输出写入一个字符串
2.6、以记录为单位的IO函数
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE*stream);
返回值:读或写的记录数,成功时返回的记录数等于nmemb,出错或读到文件末尾时返回的记
录数小于nmemb,也可能返回0
fread 和 fwrite 用于读写记录,这里的记录是指一串固定长度的字节,比如一个 int 、一个结构体或者一个定长数组。
参数 size 指出一条记录的长度,而 nmemb 指出要读或写多少条记录,这些记录在 ptr 所指的内存空间中连续存放,共占 size * nmemb 个字节
fread 从文件 stream 中读出 size *nmemb 个字节保存到 ptr 中
fwrite 把 ptr 中的 size * nmemb 个字节写到文件 stream 中
2.7、格式化IO函数
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list
ap);
返回值:成功返回格式化输出的字节数(不包括字符串的结尾'\0'),出错返回一个负值
printf 格式化打印到标准输出
fprintf 打印到指定的文件 stream 中
sprintf 并不打印到文件,而是打印到用户提供的缓冲区 str 中并在末尾加 '\0' ,由于格式化后的字符串长度很难预计,所以很可能造成缓冲区溢出
snprintf 更好一些,参数 size 指定了缓冲区长度,如果格式化后的字符串长度超过缓冲区长度, snprintf 就把字符串截断到 size-1 字节,再加上一个 '\0' 写入缓冲区,也就是说 snprintf 保证字符串以 '\0' 结尾。 snprintf 的返回值是格式化后的字符串长度(不包括结尾的 '\0' ),如果字符串被截断,返回的是截断之前的长度,把它和实际缓冲区中的字符串长度相比较就可以知道是否发生了截断。
上面列出的后四个函数在前四个函数名的前面多了个 v ,表示可变参数不是以 ... 的形式传进来,而是以 va_list 类型传进来。
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
#include <stdarg.h>
int vscanf(const char *format, va_list ap);
int vsscanf(const char *str, const char *format, va_list ap);
int vfscanf(FILE *stream, const char *format, va_list ap);
返回值:返回成功匹配和赋值的参数个数,成功匹配的参数可能少于所提供的赋值参数,返
回0表示一个都不匹配,出错或者读到文件或字符串末尾时返回EOF并设置errno
scanf 从标准输入读字符,按格式化字符串 format 中的转换说明解释这些字符,转换后赋给后面的参数,后面的参数都是传出参数,因此必须传地址而不能传值。
fscanf 从指定的文件 stream 中读字符
sscanf 从指定的字符串 str 中读字符
后面三个以 v 开头的函数的可变参数不是以 ... 的形式传进来,而是以 va_list 类型传进来
2.8、C标准库的IO缓冲区
用户程序调用C标准I/O库函数读写文件或设备,而这些库函数要通过系统调用把读写请求传给内核,最终由内核驱动磁盘或设备完成I/O操作。
C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的 FILE 结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。
以 fgetc / fputc 为例,当用户程序第一次调用 fgetc 读一个字节时, fgetc 函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指向I/O缓冲区中的第二个字符,以后用户再调 fgetc ,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用 fgetc 时, fgetc 函数会再次进入内核读1K字节到I/O缓冲区中。
C标准库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。
另一方面,用户程序调用 fputc 通常只是写到I/O缓冲区中,这样 fputc 函数可以很快地返回,如果I/O缓冲区写满了, fputc 就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。
有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作,对应的库函数是 fflush , fclose 函数在关闭文件之前也会做Flush操作
#include <stdio.h>
int fflush(FILE *stream);
返回值:成功返回0,出错返回EOF并设置errno
C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时,不同类型的缓冲区具有不同的特性。
- 全缓冲
如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。
- 行缓冲
如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。
- 无缓冲
用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。
3、数值字符串转换函数
#include <stdlib.h>
int atoi(const char *nptr);
double atof(const char *nptr);
返回值:转换结果
long int strtol(const char *nptr, char **endptr, int base);
double strtod(const char *nptr, char **endptr);
返回值:转换结果,出错时设置errno
atoi 把一个字符串开头可以识别成十进制整数的部分转换成 int 型
atof 把一个字符串开头可以识别成浮点数的部分转换成 double 型
strtol 是 atoi 的增强版,主要体现在这几方面:
- 不仅可以识别十进制整数,还可以识别其它进制的整数,取决于 base 参数,比如 strtol("0XDEADbeE~~", NULL, 16) 返回0xdeadbee的值, strtol("0777~~", NULL, 8) 返回0777的值。
- endptr 是一个传出参数,函数返回时指向后面未被识别的第一个字符。例如 char *pos;strtol("123abc", &pos, 10); , strtol 返回123, pos 指向字符串中的字母a。如果字符串开头没有可识别的整数,例如 char *pos; strtol("ABCabc", &pos, 10); ,则 strtol 返回0, pos 指向字符串开头,可以据此判断这种出错的情况,而这是 atoi 处理不了的。
- 如果字符串中的整数值超出 long int 的表示范围(上溢或下溢),则 strtol 返回它所能表示的最大(或最小)整数,并设置 errno 为 ERANGE ,例如 strtol("0XDEADbeef~~", NULL, 16) 返回0x7fffffff并设置 errno 为 ERANGE 。
4、分配和释放内存函数
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
返回值:成功返回所分配内存空间的首地址,出错返回NULL
void free(void *ptr);
calloc 分配 nmemb 个元素的内存空间,每个元素占 size 字节,并且 calloc 负责把这块内存空间用字节0填充,而 malloc 并不负责把分配的内存空间清零
5、函数可变参数
一种方式是在参数列表传入一个Sentinel,例如NULL,就像execl(3)入参一样。
#include <stdio.h>
#include <stdarg.h>
void printlist(int begin, ...)
{
va_list valist;
char *p;
va_start(valist, begin);
p = va_arg(valist, char *);
while(p != NULL) {
fputs(p, stdout);
putchar('\n');
p = va_arg(valist, char *);
}
va_end(valist);
}
int main(void)
{
printlist(0, "hello", "world", "foo", "goo", NULL);
return 0;
}
printlist 的第一个参数 begin 的值并没有用到,但是C语言规定至少要定义一个有名字的参数,因为 va_start 宏要用到参数列表中最后一个有名字的参数,从它的地址开始找可变参数的位置。
实现者应该在文档中说明参数列表必须以 NULL 结尾