C语言之标准库中的常用api
标准库
C语言的标准库:ANSI C 定义了 15 个头文件,所有编译器必须支持,是C语言的标准库。
在之前学习的常用API,就是标准库中的API,有些因为功能不太常用所以没有涉及到,在这里进行补充
标准库中的每个头文件包含的大致功能:
- assert.h:定义了一个宏函数 assert(),用于在程序中添加断言
- ctype.h:包含用于字符处理的函数,例如判断字符的类型、大小写转换等
- errno.h:定义了全局变量 errno,用于表示发生的错误代码
- float.h:包含了浮点数的特性,例如浮点数的范围、精度等
- limits.h:包含了基本类型的取值范围,例如整型和字符型的最大最小值等
- locale.h:包含了处理本地化的函数,例如格式化数字、日期和时间等
- math.h:包含了数学函数,例如三角函数、指数函数、对数函数等
- setjmp.h:定义了用于跳转的非局部跳转函数 setjmp() 和 longjmp()
- signal.h:定义了处理信号的函数,例如注册信号处理函数、发送信号等
- stdarg.h:定义了处理变长参数的函数,例如使用可变参数的函数printf()
- stddef.h:定义了一些常用的类型和宏,例如 NULL、size_t 等
- stdio.h:包含了输入输出函数的声明,例如文件的读写、格式化输入输出等
- stdlib.h:包含了一些常用的函数,例如内存分配、字符串转换、随机数等
- string.h:包含了字符串处理函数的声明,例如字符串拷贝、字符串比较等
- time.h:包含了时间和日期函数的声明,例如获取当前时间、时间格式化等
- unistd.h:包含了一些系统调用函数的声明,例如文件操作、进程控制等
操作字符串 string.h
操作字符串的API都位于string.h文件中,其中还涉及到了一些操作内存的API,C语言可以直接通过操作内存来操作字符串
内存复制 memcyp
- 签名:
void *memcpy(void *dest, const void *src, size_t n);
- 参数
- dest:指向目标内存区域的指针,即要将数据复制到的位置。
- src:指向源内存区域的指针,即要复制数据的起始位置。
- n:要复制的字节数。
内存移动 memmove
位于string.h文件中,可以用来进行内存复制,将一段内存中的字符串复制到另一段内存中,如果源区域和目标区域的地址有重叠,memmove函数可以保证这种复制是安全的。
- 签名:
void *memmove (void *__dest, const void *__src, size_t __n)
- 参数:
- 参数1:指向目标区域的指针
- 参数2:指向源区域的指针
- 参数3:复制的长度
- 返回值:返回被移动的字符串
- 注意事项:当源和目标内存区域重叠时,memmove()函数会先将数据复制到一个临时缓冲区,然后再从缓冲区复制到目标内存区域,以确保正确的结果。因此,在涉及到重叠内存区域的情况下,应该使用memmove()函数而不是memcpy()函数。
案例:
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[]) {
char str[] = "Hello World!";
printf("%s\n", str); // Hello World!
char * str2 = memmove(&str[5], &str[6], 7);
printf("%s\n", str); // HelloWorld!!
printf("%s\n", str2); // World! 返回被移动的字符串
return 0;
}
案例2:使用strcpy函数,在内存区域重叠的两块内存间进行字符串复制
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[]) {
char str[] = "Hello World!";
char str2[] = strcpy(&str[5], &str[6]); // 报错:error: invalid initializer
return 0;
}
字符串比较 strcmp strncmp
strcmp:字符串比较,如果相同,返回0
- 签名:
int strcmp(const char *str1, const char *str2);
strncmp:可以指定长度,只比较指定长度内的字符
- 签名:
int strncmp(const char *str1, const char *str2, int len);
查找子字符串 strstr strchr strrchr
strstr函数:在字符串中寻找某个子字符串第一次出现的位置,返回指向该位置的指针。
- 签名:
const char *strstr (const char *__haystack, const char *__needle)
- 参数:
- 参数1:目标字符串
- 参数2:子字符串
- 返回值:指向该位置的指针,可以通过返回的指针减去原先的指针,计算出字符出现的位置
案例:
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[]) {
char *p_str = "Hello World!";
char *p_str2 = strstr(p_str, "Wo");
printf("%s\n", p_str2); // World!
return 0;
}
strchr:用于在一个字符串中查找指定字符的第一次出现位置。该函数返回一个指向该字符的指针,如果找不到字符,则返回 NULL。
- 签名:
char *strchr(const char *str, int c);
- 参数:str 是要搜索的字符串,c 是要查找的字符。
- 返回值:返回一个指向该字符的指针
案例:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!";
char *result;
result = strchr(str, 'o'); // o, World!
printf("result == %s\n", result);
if (result != NULL) {
// 这里是两个字符串指针相减
printf("Found at index %ld\n", result - str); // Found at index 4
} else {
printf("Not found\n");
}
return 0;
}
strrchr:用于在一个字符串中查找指定字符的最后一次出现位置。该函数返回一个指向该字符的指针,如果找不到字符,则返回 NULL。
- 签名:
char *strrchr(const char *str, int c);
- 参数:str 是要搜索的字符串,c 是要查找的字符
- 返回值:返回一个指向该字符的指针
案例:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!";
char *result;
result = strrchr(str, 'o');
printf("result == %s\n", result); // orld!
if (result != NULL) {
// 这里是两个字符串指针相减
printf("Found at index %ld\n", result - str); // Found at index 8
} else {
printf("Not found\n");
}
return 0;
}
字符串复制 strdup
strdup函数:位于string.h文件中,它用于复制一个字符串,为其分配足够的动态内存,并返回这段内存的指针。它不是 C 标准库的一部分,但通常在 POSIX 兼容的系统中作为非标准扩展提供,并且可以通过包含 <string.h> 头文件来使用。然而,在某些编译器或平台上,strdup 可能不可用或需要特定的宏定义来启用。
- 签名:
char *strdup (const char *__s)
- 参数:字符串
- 返回值:字符串的拷贝,这里的拷贝是深拷贝,改变原来的字符串不会影响到拷贝后的字符串
在引用string.h前定义宏:#define _GNU_SOURCE
案例:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[]) {
char str[] = "hello world";
// 字符串复制
char *str1 = strdup(str);
printf("str: %s\n", str);
printf("str1: %s\n", str1);
printf("str的内存地址:%p\n", &str);
printf("str1的内存地址:%p\n", &str1);
str[1] = 'b';
printf("str: %s\n", str);
printf("str1: %s\n", str1);
free(str1);
return 0;
}
字符串拼接 strcat
strcat函数:字符串拼接,必须要保证目标字符串有足够的空间,否则会内存溢出,它会找到目标字符串的’\0’,然后向后拼接字符,再移动’\0’到末尾
- 签名:
char *strcat (char *dest, const char *src)
- 参数:
- 参数1:目标字符串,它必须有足够的空间
- 参数2:要被拼接的字符
- 返回值:指向目标字符串的指针,即dest的值
案例:
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[]) {
int bufLen = 1024;
char buf[bufLen];
memset(buf, 0, bufLen);
char *a = strcat(buf, "Hello");
char *b = strcat(buf, "World");
printf("%s\n", buf); // Hello World
return 0;
}
操作字符 ctype.h
操作字符的API位于ctype.h文件中,它是C语言标准库的一部分,它提供了一些与字符分类和字符转换相关的函数和宏定义。
ctype.h提供的功能:
- 字符分类函数:一系列的is函数,用于判断一个字符是否属于某个特定的字符类别,如字母、数字、空白字符等。一些常见的is函数包括isalpha、isdigit、isspace等。
- 字符转换函数:用于将字符转换为其他字符,如大小写转换。一些常见的转换函数包括tolower、toupper等。
ctype.h中的函数和宏定义只适用于ASCII字符集,对于其他字符集可能需要使用其他的函数和库进行处理。
判断一个字符是不是控制字符 iscntl
使用iscntl函数,它位于ctype.h文件中
#include <stdio.h>
#include <unistd.h>
#include <ctype.h>
// 判断用户输入的字符是不是控制字符,用户可以在控制台输入esc、backspace等尝试。
int main() {
char c;
while (1) {
printf("请输入:");
scanf("%c", &c);
// 只接收用户输入的单个字符,剩余字符全部丢弃
while(getchar() != '\n');
// 判断用户输入的字符是不是控制字符
if (iscntrl(c)) {
printf("%d\n", c);
} else {
printf("%d (%c)\n", c, c);
}
if (c == 'q') {
break;
}
}
return 0;
}
异常处理 errno.h
errno.h:C语言标准库的一部分,用于处理和报告运行时发生的错误,它提供的功能:
- 全局变量errno:errno是一个全局变量,用于存储最近发生的错误代码。一些标准库函数在发生错误时会将错误代码设置到errno中。可以使用errno变量来获取和判断发生的错误类型。
- 错误代码宏定义:errno.h提供了一些与错误类型相关的宏定义,用于表示不同的错误代码。一些常见的宏定义包括EACCES(访问权限错误)、EINVAL(无效参数错误)、ENOMEM(内存分配错误)等。
打印错误信息 perror()函数
用于打印错误消息,将标准错误中定义的错误值errno解释为错误消息并打印到标准错误输出流stderr。它会主动获取当前发生的错误
案例:
#include <stdio.h>
int main(int argc, char const *argv[]) {
FILE * fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("文件打开失败:"); // 文件打开失败:: No such file or directory
}
return 0;
}
获取错误信息 strerror函数
案例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[]) {
FILE *fp = fopen("CC.txt", "r");
if (fp == NULL) {
printf("错误:%s\n", strerror(errno)); // 错误:No such file or directory
// perror("错误:");
}
return 0;
}
时间处理 time.h
时间日期相关的API,通常需要关注的这几个功能:时间戳、当前时间、时间格式化、时间计算
注意:时间戳是不带时区信息的,但是将时间戳转换为时间时会根据时区来进行转换,这个时候就带时区信息了
时间日期相关的API,在time.h文件中
获取当前时间戳
使用time函数来获取当前时间戳。
time函数:
- 签名:
time_t time(time_t *);
- 参数:一个time_t类型的指针,可以是NULL,如果不为NULL,time函数会把时间戳写到参数中
- 返回值:time_t,实际上上long类型的别名,返回一个时间戳
#include <stdio.h>
#include <time.h>
// 获取当前10位的时间戳
int main(int argc, char const *argv[]) {
// 方式1
time_t now = time(NULL);
printf("当前时间戳:%ld\n", now);
// 方式2
time_t now2;
time(&now2);
printf("当前时间戳:%ld\n", now2);
return 0;
}
将时间戳转换为时间
使用localtime函数,将时间戳转换为时间
localtime函数:
- 签名:
struct tm *localtime(const time_t *);
,函数接收一个time_t类型的指针,返回一个tm类型的指针
tm:一个结构体,存储了时间中的各个部分,例如年月日、时分秒。
struct tm {
int tm_sec; /* seconds after the minute [0-60] */
int tm_min; /* minutes after the hour [0-59] */
int tm_hour; /* hours since midnight [0-23] */
int tm_mday; /* day of the month [1-31] */
int tm_mon; /* months since January [0-11] */
int tm_year; /* years since 1900 */
int tm_wday; /* days since Sunday [0-6] */ // 星期
int tm_yday; /* days since January 1 [0-365] */
int tm_isdst; /* Daylight Savings Time flag */
long tm_gmtoff; /* offset from UTC in seconds */
char *tm_zone; /* timezone abbreviation */
};
案例:
#include <stdio.h>
#include <time.h>
int main(int argc, char const *argv[]) {
// 获取当前时间戳
time_t now;
time(&now);
// 将时间戳转换为当前时区下的时间
struct tm *current_time_ptr = localtime(&now);
// 获取时间中的年月日、时分秒
int year = current_time_ptr->tm_year + 1900; // tm_year表示从1900到现在过了多少年
int month = current_time_ptr->tm_mon + 1; // tm_mon从0开始,需要加1
int day = current_time_ptr->tm_mday;
int hour = current_time_ptr->tm_hour;
int minute = current_time_ptr->tm_min;
int second = current_time_ptr->tm_sec;
int week = current_time_ptr->tm_wday;
if (week == 0) {
week = 7;
}
char * zone = current_time_ptr->tm_zone;
printf("当前时间:%d-%d-%d %d:%d:%d 星期%d 时区 %s\n", year, month, day, hour, minute, second, week, zone);
return 0;
}
格式化时间
使用strftime函数来格式化时间
strftime函数:
- 签名:
size_t strftime(char * __restrict, size_t, const char * __restrict, const struct tm * __restrict) __DARWIN_ALIAS(strftime);
- 参数:
- 参数1:目标字符串,strftime函数会把格式化的结果写入到目标字符串中
- 参数2:目标字符串的长度
- 参数3:格式化替身符,用于格式化日期,例如:“%Y-%m-%d %H:%M:%S”,获取当前年月日
- 参数4:时间戳
- 返回值:返回格式化好的字符串的长度
案例:
#include <stdio.h>
#include <time.h>
int main(int argc, char const *argv[]) {
// 获取当前时间戳
time_t now;
time(&now);
// 获取当前时间
struct tm * current_time_p = localtime(&now);
// 格式化当前时间
char str[64] = {};
strftime(str, sizeof(str), "%Y-%m-%d %H:%M:%S 星期%w 时区%Z", current_time_p);
printf("当前时间:%s\n", str);
return 0;
}
时间计算
difftime()函数接受两个 time_t 类型的时间作为参数,计算 time1 - time2 的差,并把结果转换为秒。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int main(int argc, char const *argv[]) {
// 时间戳1
time_t now;
time(&now);
// 休眠5秒
sleep(5);
// 时间戳2
time_t now2;
time(&now2);
// 时间戳2 - 时间戳1
double diff = difftime(now2, now);
printf("%ld - %ld = %lf", now2, now, diff);
return 0;
}
内存管理 stdlib.h
标准库头文件,提供了许多与常见函数和类型相关的功能
stdlib.h提供的功能:
- 内存分配和释放:包括函数如malloc、calloc、realloc和free,用于动态分配和释放内存。
- 字符串转换和操作:包括函数如atoi、atof和itoa,用于字符串和数值之间的转换,以及一些字符串操作函数如strcpy、strcat和strlen。
- 随机数生成:包括函数如rand和srand,用于生成伪随机数。
- 程序终止和环境控制:包括函数如exit和system,用于终止程序的执行和执行操作系统命令。
- 排序和搜索:包括函数如qsort和bsearch,用于排序和搜索数组。
- 环境变量操作:包括函数如getenv和putenv,用于获取和设置环境变量。
- 定义了一些常用的数据类型,如size_t和NULL
C语言的内存管理,分为两个部分,一部分是由系统管理的,一部分是由用户手动管理的。
- 系统管理的内存:栈内存,栈内存是由系统管理的,函数在运行时入栈,运行结束后出栈,局部变量被分配到栈内存中,由系统负责管理栈内存
- 用户手动管理的内存:堆内存,用户申请的内存位于堆内存中
内存的申请和释放
malloc函数:用于分配内存,该函数向系统要求一段内存,系统就在“堆”里面分配一段连续的内存块给它
- 函数签名:
void *malloc(size_t __size);
接收一个整数作为参数,指定字节数,返回一个无类型指针,如果内存分配失败,返回NULL
free函数:释放内存
- 函数签名:void free(void *);
案例1:申请4字节的内存,存储一个int类型的变量
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
// 分配4字节的内存
void * p = malloc(4);
printf("%p\n", p); // 0x120e067a0
// 在之前申请的内存上存放一个int类型的值
int * int_p = (int *)p;
*int_p = 10;
printf("%d\n", *int_p ); // 10
// 释放内存
free(p);
// 释放内存后再次使用释放后的内存
// 可以使用,但是不推荐,因为系统很有可能把这段内存分配给其它变量,从而导致当前变量失效,
// 程序运行出错。
*int_p = 12;
printf("%d\n", *int_p);
return 0;
}
案例2:为一个int类型的数组申请内存,数组长度是10位
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
int n = 10;
// 为一个int类型的数组申请内存,数组长度是10位
int * p = (int *)malloc(sizeof(int) * n);
printf("指针指向的内存地址:%p\n", p); // 0x1226067a0
printf("指针的内存地址:%p\n", &p); // 0x16b42b300
// 初始化数组,通过指针来操作数组
for (int i = 0; i < n; i++) {
p[i] = i * 5;
}
// 遍历数组
for (int i = 0; i < 10; i++) {
printf("p[%d] = %d\n", i, p[i]);
}
// 释放内存
free(p);
return 0;
}
calloc函数:向系统申请内存,它会初始化申请好的内存,把所有元素全部初始化为0,
- 函数签名:
void *calloc(size_t __count, size_t __size);
接收两个参数,第一个参数是元素个数,第二个参数是元素大小
realloc函数:用于修改已经分配的内存块的大小,可以放大也可以缩小,返回一个指向新的内存块的指针。
- 函数签名:
void *realloc(void *__ptr, size_t __size);
接收两个参数,第一个参数是已经分配好的内存块指针,第二个参数是该内存块的新大小
内存初始化:memset
设置程序退出时要执行的动作 atexit
案例:
#include <stdio.h>
#include <stdlib.h>
void ex() {
printf("程序退出\n");
}
// 设置在程序退出时执行的函数
int main(int argc, char const *argv[]) {
atexit(ex);
for (int i = 0; i < 5; i++) {
printf("i = %d\n", i);
}
return 0;
}
atoi函数 字符串转换为数字
atoi函数:将一个存储了数字的字符串转换为数字,定义在<stdlib.h>
头文件
- 签名:
int atoi (const char *__nptr)
案例:
#include <stdio.h>
#include <stdlib.h>
int main() {
char *s1 = "123";
char *s2 = "-456";
char *s3 = "abc";
char *s4 = "2147483649"; // 超过int最大值
printf("str1 to int: %d\n", atoi(s1)); // 123
printf("str2 to int: %d\n", atoi(s2)); // -456
printf("str3 to int: %d\n", atoi(s3)); // 0, 因为"abc"不能转换为整数
printf("str4 to int: %d\n", atoi(s4)); // -2147483647,未定义的行为
return 0;
}
unistd.h
提供了和操作系统交互相关的功能,是POSIX标准的一部分,通常在类Unix系统上使用。
unistd.h提供的功能:
- 文件访问:unistd.h提供了函数如open、close、read、write等,用于打开、关闭、读取和写入文件。它还定义了一些常量用于文件访问权限和文件描述符。
- 进程控制:通过使用fork、exec、exit和wait等函数,unistd.h可以管理进程的创建、执行、终止和等待。
- 目录操作:通过使用chdir、mkdir和rmdir等函数,unistd.h可以进行目录切换、创建和删除操作。
- 系统调用:unistd.h提供了许多与系统调用相关的函数,如sleep、getpid、getuid和getgid等,用于获取当前进程的ID、用户ID和组ID等信息。
- 系统配置:通过使用sysconf、pathconf、confstr等函数,unistd.h可以获取系统的一些配置参数和限制。
unistd中声明的标准输入、标准输出、标准错误输出的文件标识符:
- 标准输入:
#define STDIN_FILENO 0
- 标准输出:
#define STDOUT_FILENO 1
- 标准错误输出:
#define STDERR_FILENO 2
常用操作
printf系列函数
printf函数
用于将格式化后的字符串打印到标准输出中
- 签名:
int printf(const char *, ...);
- 参数:
- 参数1:格式字符串,指定了后续参数如何被格式化并插入到字符串中,格式字符串的核心是格式替身符,如 %d 表示整数、%s 表示字符串、%f 表示浮点数等。
- 参数2:可变参,零个或多个要被格式化并插入到输出字符串中的参数
- 返回值:格式字符串格式化后的长度
案例:
#include <stdio.h>
int main(int argc, char const *argv[]) {
int len = printf("a:%s\n", "b"); // a:b
printf("返回值:%d\n", len); // 4
return 0;
}
sprintf函数
格式化字符串并将结果写入到字符串缓冲区中
- 签名:
int sprintf(char *__restrict, const char *__restrict, ...);
- 参数:
- 参数1:存储格式化后的结果
- 参数2:包含格式替身符的格式字符串
- 参数3:可变参,零个或多个要被格式化并插入到输出字符串中的参数
- 返回值:格式化后的字符串长度
案例1:
#include <stdio.h>
int main(int argc, char const *argv[]) {
char buf[16];
int len = sprintf(buf, "a:%s", "zs");
printf("%s\n", buf); // a:zs
printf("%d\n", len); // 4
return 0;
}
案例2:内存溢出的情况,格式化后的字符串比字符串缓冲区的长度更长
#include <stdio.h>
int main(int argc, char const *argv[]) {
char buf[8];
sprintf(buf, "Hello: %s", "张三");
printf("%s\n", buf); // Segmentation fault
return 0;
}
snprintf函数
将格式化字符串的结果写入到字符串缓冲区中,它和sprintf函数的功能基本一致,但是snprintf函数会对写入的结果做长度校验,避免出现内存溢出的情况
- 签名:
int snprintf(char *__restrict, size_t, const char *__restrict, ...)
- 参数:
- 参数1:字符串缓冲区
- 参数2:字符串缓冲区的长度
- 参数3:格式化字符串
- 参数4:可变参
- 返回值:格式化字符串被格式化后的长度
案例:
#include <stdio.h>
int main(int argc, char const *argv[]) {
char buf[10];
snprintf(buf, sizeof(buf), "Hello: %s", "张三");
// snprintf函数做长度校验,确保不会出现内存访问异常
printf("%s\n", buf); // Hello: ?
return 0;
}
vsnprintf函数
用于将格式化的字符串输出到字符数组中,是snprintf函数的可变参版本
- 签名:
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
- 参数:
- str:指向输出的字符数组的指针。
- size:指定输出字符数组的最大长度。
- format:格式化字符串,包含了要输出的文本以及格式控制符。
- ap:va_list类型的变量,用于传递可变参数列表。
- 返回值:函数返回生成的字符数量,不包括终止符\0,如果生成的字符数量超过了size,则返回按照size进行截断后的字符数量。如果发生错误,函数返回负数
#include <stdio.h>
#include <stdarg.h>
void formatStr(char buf[], int buf_len, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vsnprintf(buf, buf_len, fmt, args);
va_end(args);
}
int main(int argc, char const *argv[]) {
char buf[80];
char *name = "张三";
int age = 18;
formatStr(buf, 80, "name: %s, age: %d", name, age);
printf("buf: %s\n", buf); // buf: name: 张三, age: 18
return 0;
}
scanf系列函数
scanf函数
从标准输入中读取并解析用户输入,将用户输入存储到指定变量中
- 签名:
int scanf(const char *, ...)
- 参数:
- 参数1:格式字符串,定义了用户的输入应该如何被解读
- 参数2:可变参,一个或多个指向变量的指针,这些变量用于存储输入的数据。对于格式字符串中的每个格式替身符,用户需要提供一个相应类型的变量的指针。
- 返回值:返回解析到的变量的个数
案例:
#include <stdio.h>
int main() {
int number = 0;
printf("请输入一个数字:"); // 没有换行符,数据在缓冲区中,不会被输入到屏幕上
fflush(stdout); // 强制刷新输出缓冲区,以确保提示被打印到屏幕上
scanf("%d", &number);
printf("你的输入:%d\n", number);
return 0;
}
使用scanf函数时的注意事项:
- 当读取字符串时,scanf 默认以空白字符(空格、制表符或换行符)作为字符串输入的分隔符。因此,它无法用于读取含有空白字符的字符串。
- scanf 不检查数组的边界,当读取字符串时,如果输入的字符超过了数组的容量,会发生缓冲区溢出,这可能导致程序崩溃或安全漏洞。为了避免这种情况,应该使用 fgets 函数或指定最大宽度的 %s 格式,例如 %49s 用于50字符的数组。
- 输入时,如果用户输入的数据类型与格式说明符不匹配,scanf 可能会导致未定义的行为。为了提高程序的健壮性,应当始终检查scanf的返回值。
sscanf函数
从字符串中,依据指定的格式,解析值,并把解析结果存储到变量中
- 签名:
int sscanf(const char *, const char *, ...)
- 参数:
- 参数1:一个指针,指向要被解析的字符串
- 参数2:格式字符串,指定了如何从要被解析的字符串中解析数据
- 参数3:可变参,一个或多个指向变量的指针,存储解析结果
- 返回值:解析到的变量的个数
sscanf函数的注意事项:
- 字符串的读取默认以空白字符作为分隔符,且%s格式说明符不会检查目标缓冲区的边界。为了避免潜在的缓冲区溢出问题,应当限制输入的长度,例如使用 %49s 来限制读取到的字符数
- 输入字符串的格式必须与格式字符串匹配,否则可能会导致未定义的行为或者错误的解析结果
案例1:
#include <stdio.h>
int main(int argc, char const *argv[]) {
char buf[] = "18 19 aabb";
int v1, v2;
char s1[8];
int len = sscanf(buf, "%d %d %s", &v1, &v2, s1);
printf("v1: %d, v2: %d, v3: %s\n", v1, v2, s1); // v1: 18, v2: 19, v3: aabb
printf("len: %d\n", len); // 3
return 0;
}
案例2:sscanf函数无法从过于复杂的字符串中解析出数据
#include <stdio.h>
int main(int argc, char const *argv[]) {
char buf[] = "你好,张三,你的年龄是18";
char name[8];
int age;
sscanf(buf, "你好,%s,你的年龄是%d", name, &age);
printf("name: %s\n", name); // name: 张三,你的年龄是18 // 这里解析出错了,name应该是"张三"
printf("age: %d\n", age); // age: 65535 // 这里解析错了,age应该是18
return 0;
}
正则表达式
regex.h
在C语言中使用正则表达式,只需要在文件中引入regex.h即可
regex.h:POSIX(Portable Operating System Interface 可移植操作系统接口)标准中定义的一个头文件,它提供了一套用于正则表达式的API。这些API允许程序员在C程序中执行正则表达式匹配、替换等操作。不过,需要注意的是,并不是所有系统都默认支持regex.h,它主要存在于遵循POSIX规范的Unix-like系统中,如Linux。
入门案例:正则表达式的基本使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>
int main(int argc, char const *argv[]) {
char *s = "Hello World";
char *pattern = "e(.*)o";
int result = 0;
int bufLen = 64;
char errBuf[bufLen];
// 编译正则表达式
regex_t regex;
result = regcomp(®ex, pattern, REG_EXTENDED);
if (result != 0) {
regerror(result, ®ex, errBuf, bufLen);
printf("正则表达式编译错误:%s\n", errBuf);
return;
}
// 执行正则表达式
int matchLen = 10; // 表示最多支持匹配10次
regmatch_t match[matchLen];
result = regexec(®ex, s, matchLen, match, 0);
if (result != 0) {
regerror(result, ®ex, errBuf, bufLen);
printf("正则表达式执行错误:%s\n", errBuf);
return;
}
// 释放正则表达式实例
regfree(®ex);
// 查看匹配结果
if (result == REG_NOERROR) {
// 匹配成功
int i;
for (i = 0; i < matchLen; i++) {
regmatch_t ma = match[i];
if (ma.rm_so == -1) {
break;
}
int mLen = ma.rm_eo - ma.rm_so;
char *m = malloc(mLen + 1);
memcpy(m, &s[ma.rm_so], ma.rm_eo - ma.rm_so);
m[mLen] = '\0';
printf("匹配到的字符串:%s\n", m);
free(m);
}
} else if (result == REG_NOMATCH) {
// 匹配失败
printf("匹配失败\n");
}
return 0;
}
regex.h中的API
regcomp函数:编译正则表达式
- 签名:
int regcomp(regex_t *preg, const char *regex, int cflags);
- 参数:
- preg:一个regex_t结构体指针。
- regex:正则表达式字符串
- cflags:标志位,只能是下列四个特殊的值或它们的或运算
- REG_EXTENDED:使用POSIX扩展正则表达式语法解释的正则表达式。如果没有设置,基本POSIX正则表达式语法。
- REG_ICASE:忽略字母的大小写。
- REG_NOSUB:不存储匹配的结果。
- REG_NEWLINE:对换行符进行“特殊照顾”,后边详细说明。
- 返回值:
- 0:表示成功编译;
- 非0:表示编译失败,用regerror查看失败信息
regcomp在什么情况下会编译失败:
- 无效的正则表达式:如果提供的正则表达式模式包含语法错误或不被支持的操作符,regcomp会失败。例如,未闭合的括号、不正确的反向引用、未知的元字符等。
- 超出资源限制:如果正则表达式过于复杂(例如,嵌套过多的量词或过深的递归),可能导致内部状态爆炸,消耗过多的内存或栈空间,从而引发失败。
- 正则表达式太长:某些实现可能限制了正则表达式的最大长度,超出了这个长度也会导致编译失败。
- 不支持的特性:不同的实现可能支持正则表达式的特性有所不同,如果使用了某个实现不支持的特性,也可能导致编译失败。
- 参数错误:如果传递给regcomp的参数无效,如regex_t指针为NULL,或者size_t nsub参数的值过大,也会导致失败。
regexec函数:执行正则表达式。
- 签名:
int regexec(const regex_t *preg, const char *string, size_t nmatch, regmatch_t pmatch[], int eflags);
- 参数:
- preg:编译好的正则表达式
- string:要匹配的字符串
- nmatch:第四个参数是一个数组,这个参数代表数组大小
- pmatch:存储匹配结果的数组,数组的数据类型是 regmatch_t结构体。要注意的是,数组中的第一个元素是整体匹配,第二个元素是分组匹配,也就是小括号中的匹配。
- eflags:通常写0即可,有两个可选值,REG_NOTBOL,不将 “^” 视为字符串的开头,REG_NOTEOL,不将 “$”视为字符串的结尾。
- 返回值:0:表示成功编译; 非0:表示编译失败,用regerror查看失败信息
regexec在什么情况下会报错:
- 未初始化的正则表达式结构:如果在调用regexec之前没有成功调用regcomp来初始化regex_t结构,或者regex_t结构体因某种原因被破坏,regexec可能会失败。
- 无效的正则表达式句柄:传递给regexec的regex_t句柄无效(比如,已被regfree释放或未正确初始化),会导致执行失败。
- 内存不足:在执行匹配过程中,如果遇到极端情况导致需要的内存资源超过系统可提供的,可能会导致执行失败。
- 正则表达式引擎内部错误:尽管罕见,但正则表达式引擎内部错误也可能导致执行失败,这通常是库本身的bug或与特定输入的兼容性问题。
- 匹配操作被中断:在多线程环境中,如果另一个线程中断了当前执行匹配操作的线程,也可能导致regexec提前返回错误。
regmatch_t结构体:
typedef struct {
regoff_t rm_so; // 值如果不为-1,表示匹配的子串在字符串中的起始偏移量
regoff_t rm_eo; // 表示匹配的字串在字符串的结束偏移量
} regmatch_t;
regfree函数:用来释放regcomp编译好的内置变量。
- 签名:
void regfree(regex_t *preg)
- 参数:preg,指向结构体的指针。
regerror函数:获取regex.h中的错误信息
- 签名:
size_t regerror (int errcode, const regex_t *preg, char *errbuf, size_t __errbuf_size)
- 参数:
- 参数1:errcode,regcomp或regexec的返回值
- 参数2:preg,指向regex_t结构体的指针
- 参数3:errbuf,缓冲区,存储错误信息
- 参数4:errbuf_size,缓冲区的长度
regex_t:存储正则表达式的结构体,就使用而言,很少用到这个结构体中的变量,所以在这里不做介绍
pcre库
pcre:Perl Compatible Regular Expressions,一个用C语言编写的正则表达式函数库
安装pcre库:yum install -y pcre pcre-devel
入门案例:
#include <stdio.h>
#include <string.h>
#include <pcre.h>
#define OVERCOUNT 128
int main(int argc, char const *argv[]) {
char *s = "Hello World";
char *pattern = "e(.*)o";
int errOffset = 0;
const char *errPtr = NULL;
int ovector[OVERCOUNT] = {0};
pcre *re = pcre_compile(pattern, PCRE_EXTENDED, &errPtr, &errOffset, NULL);
if (re == NULL) {
printf("编译错误:offset %d, %s\n", errOffset, errPtr);
return -1;
}
int result = pcre_exec(re, NULL, s, strlen(s), 0, 0, ovector, OVERCOUNT);
if (result > 0) {
int i;
for (i = 0; i < result; i++) {
int start = ovector[2 * i];
int end = ovector[2 * i + 1];
int len = end - start;
char *m = malloc(len + 1);
memcpy(m, &s[start], len);
m[len] = '\0';
printf("i = %d, 匹配结果 %s\n", i, m);
free(m);
}
} else {
printf("匹配失败\n");
}
pcre_free(re);
return 0;
}
注意,编译时,要加上 -lpcre
参数,表示链接pcre库。
pcre库中的API
pcre_compile函数:编译正则表达式
- 签名:
pcre *pcre_compile(const char * pattern, int options, const char ** errorptr, int *erroffset, const unsigned char *tableptr)
- 参数:
- 参数1:正则表达式字符串
- 参数2:预定的选项,指定正则表达式的特性,通常是PCRE_EXTENDED,使用扩展的正则表达式,也可以是其它
- 参数3:一个指向字符串的指针,存储编译失败时的错误信息
- 参数4:整数指针,编译失败时,存储正则表达式中的错误字符
- 参数5:自定义的字符转换表,通常设置为NULL
- 返回值:编译成功后,返回 pcre结构体 类型的指针,编译失败后返回NULL,同时在参数3和参数4中提供错误信息
pcre_exec函数:
- 签名:
int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, int length, int startoffset, int options, int *ovector, int ovecsize)
- 参数:
- 参数1:一个指向pcre结构体的指针
- 参数2:一个指向pcre_extra结构体的指针,用于提供额外的匹配选项,可以传入NULL
- 参数3:要匹配的字符串
- 参数4:要匹配的字符串的长度
- 参数5:从目标字符串开始搜索的偏移量,默认是0
- 参数6:匹配时的额外选项,通常是0
- 参数7:一个整数数组,存储匹配结果,存储的是每个捕获组的开始索引和结束索引
- 参数8:代表参数7的数组大小
- 返回值:如果匹配成功,返回匹配到的子模式的数量,包括整个匹配作为第一个子模式,如果没有匹配到内容,返回-1
正则表达式 (?: 什么意思?
在正则表达式中,(?:…) 是一个非捕获组(non-capturing group)的语法。这意味着它像普通括号(…)一样,用于定义一组子表达式,以便应用量词(如*、+、?)或执行诸如|(或)等操作,但与普通括号不同的是,它不会捕获匹配的内容,也就是说,不会为这部分匹配创建一个可以后续引用的捕获组。
通常,在正则表达式中使用圆括号(…)不仅用来分组,还意味着要记住这个分组匹配的内容,可以通过\1, \2, … 等后向引用的方式来引用这些被捕获的内容。而非捕获组(?:…)则不对匹配的内容进行编号和存储,这对于那些只需要分组功能而不关心捕获结果的场景非常有用,它还可以避免不必要的内存消耗和提高匹配效率。
例如,在URL匹配的正则表达式中,(?:[a-zA-Z0-9-]+\.)+ 这个非捕获组用于匹配子域名部分,因为不需要单独引用每个子域名,只是需要它们作为一个整体来满足匹配条件。
数组扩容 memmove函数
C语言中的数组不支持扩容,如果想要实现类似于数组扩容的功能,可以使用指针来操作一段内存。
静态数组和动态数组:
- 静态数组:在编译时分配固定空间的数组,例如,
int arr[10]
,这里的arr就是一个静态数组- 静态数组如果声明在方法中,存储在栈上,
- 静态数组如果声明在方法外或者使用static变量修饰时,存储在程序的全局存储区或静态存储区,它们的生命周期贯穿整个程序的执行期间
- 动态数组:使用malloc等内存分配函数来初始化的数组,例如,
int *arr = malloc(10 * sizeof(int))
,这里的arr本质上g就是一个动态数组,只不过它是通过指针来操作的。
总结:静态数组才是语法意义上的数组,动态数组在语法上是一个指针。
案例:使用指针来操作一段内存,实现扩容功能
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
typedef struct point {
int x;
int y;
} Point;
// memmove函数,移动指针,指针指向一个数组
int main(int argc, char const *argv[]) {
// 初始化数组
int pArrLen = 3;
Point *pArr = malloc(pArrLen * sizeof(Point));
pArr[0] = (Point) {1, 2};
pArr[1] = (Point) {3, 4};
pArr[2] = (Point) {5, 6};
// 扩容并移动元素
int i = 1;
pArr = realloc(pArr, (pArrLen + 1) * sizeof(Point));
memmove(pArr + i + 1, pArr + i, (pArrLen - i) * sizeof(Point));
pArr[i] = (Point) {7, 8};
pArrLen++;
// 遍历数组
for (int j = 0; j < pArrLen; j++) {
printf("%d %d\n", pArr[j].x, pArr[j].y);
}
return 0;
}
案例:静态数组的内存移动
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
typedef struct point {
int x;
int y;
} Point;
// memmove函数,移动数组中的元素。将第一位以后的元素整体后移,注意,数组不扩容
int main(int argc, char const *argv[]) {
// 初始化数组
int pArrLen = 4;
Point pArr[pArrLen];
pArr[0] = (Point) {1, 2};
pArr[1] = (Point) {3, 4};
pArr[2] = (Point) {5, 6};
// 移动元素
int i = 1;
memmove(&pArr[i + 1], &pArr[i], (pArrLen - i) * sizeof(Point));
pArr[i] = (Point) {7, 8};
// 遍历数组
for (int j = 0; j < pArrLen; j++) {
printf("%d %d\n", pArr[j].x, pArr[j].y);
}
return 0;
}
阅读源码
printf函数中的__prinflike
printf函数的声明:int printf(const char * __restrict, ...) __printflike(1, 2);
__printflike
:本身是一个宏定义
#define __printflike(fmtarg, firstvararg) \
__attribute__((__format__ (__printf__, fmtarg, firstvararg)))
__printflike
的作用是向编译器提供提示,以便进行格式字符串的静态检查和类型检查。__printflike
是一个编译器扩展特性。
总结
这里是我学习C语言时使用过的函数,这里列举的API不必每个都看,仅供参考