当前位置: 首页 > news >正文

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(&regex, pattern, REG_EXTENDED);
    if (result != 0) {
        regerror(result, &regex, errBuf, bufLen);
        printf("正则表达式编译错误:%s\n", errBuf);
        return;
    }

    // 执行正则表达式
    int matchLen = 10;   // 表示最多支持匹配10次
    regmatch_t match[matchLen];
    result = regexec(&regex, s, matchLen, match, 0);
    if (result != 0) {
        regerror(result, &regex, errBuf, bufLen);
        printf("正则表达式执行错误:%s\n", errBuf);
        return;
    }

    // 释放正则表达式实例
    regfree(&regex);

    // 查看匹配结果
    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不必每个都看,仅供参考

http://www.dtcms.com/a/113285.html

相关文章:

  • 必刷算法100题之计算右侧小于当前元素的个数
  • 【算法竞赛】状态压缩型背包问题经典应用(蓝桥杯2019A4分糖果)
  • Linux数据库:【数据库基础】【库的操作】【表的操作】
  • [SAP SD] 常用事务码
  • Linux的/proc/sys/net/ipv6/conf/(all,default,interfaceName具体网络接口名称)/ 笔记250405
  • 国产系统统信uos和麒麟v10在线打开word给表格赋值
  • HTTP查询参数示例(XMLHttpRequest查询参数)(带查询参数的HTTP接口示例——以python flask接口为例)flask查询接口
  • ConstructorResolver
  • Day2-2:前端项目uniapp壁纸实战
  • HashMap 底层原理详解
  • C++学习之LINUX网络编程-套接字通信基础
  • JWT认证服务
  • [MySQL初阶]MySQL(9)事务机制
  • 基于springboot+vue的二手车交易系统
  • Supervisor的安装和使用
  • 0101安装matplotlib_numpy_pandas-报错-python
  • Business English Certificates (BEC) 高频词汇学习
  • 将MATLAB神经网络数据转换为C/C++进行推理计算
  • Linux网络状态监控利器:netstat与ping命令详解
  • Java的Selenium的特殊元素操作与定位之select下拉框
  • RocketMQ初认识
  • C,C++语言缓冲区溢出的产生和预防
  • 【2012】【论文笔记】太赫兹波在非磁化等离子体——
  • 【国产突围!致远电子ZXDoc如何打破Vector垄断,成为新能源车研发“神器”?】
  • Xshell Plus 6下载与安装
  • 【机器学习】机器学习工程实战-第4章 特征工程
  • LabVIEW商业软件开发注意问题
  • C语言-基础语法学习
  • 【Linux系统】linux下的软件管理
  • 大数据技术发展与应用趋势分析