Skip to content

C 语言的秘密武器:那些你必须知道的标准库函数 (2) (附示例)

各位C语言的探索者们,欢迎回到我们的标准库函数之旅! 👋

第一篇 文章中,我们一起了解了 stdio.h (基础输入输出)、string.h (基础字符串处理)、math.h (数学)、stdlib.h (通用工具基础) 和 ctype.h (字符处理) 的一些核心函数。它们构成了C语言编程的基石。

但是,C语言标准库的强大远不止于此!在实际开发中,我们经常需要处理更复杂的数据结构、与文件交互以及更精细地控制内存。

今天,我们将深入探讨更多C语言标准库的函数,特别是大家关心的动态内存管理以及必不可少的文件输入输出

老规矩,开始之前,记得在你的C文件顶部包含相应的头文件哦!

1. 精准掌控内存:stdlib.h 动态内存管理进阶

在第一篇文章中我们提到了 malloc()free() 用于动态分配和释放内存。这里我们详细介绍它们,并加入另外两个同样重要的伙伴:calloc()realloc()

  • malloc(size_t size): 分配 size 字节的内存块。分配的内存内容是不确定的(可能是随机值)。成功时返回指向分配内存起始位置的指针,失败时返回 NULL
  • free(void *ptr): 释放之前由 malloc, calloc, realloc 分配的内存。重要: 只能释放动态分配的内存,且不要重复释放或释放 NULL 指针(free(NULL) 是安全的)。
  • calloc(size_t num, size_t size): 分配一个能容纳 num 个大小为 size 的元素的内存块,并将所有字节初始化为。成功时返回指针,失败时返回 NULL。非常适合为数组分配内存并清零。
  • realloc(void *ptr, size_t new_size): 重新调整之前由 malloc, calloc, realloc 分配的内存块的大小为 new_size
    • 如果 ptrNULL,则行为类似于 malloc(new_size)
    • 如果 new_size 是 0 且 ptrNULL,则行为类似于 free(ptr)
    • realloc 可能会在原地扩展内存,也可能分配新的内存块并将旧数据复制过去,然后释放旧块。它返回新块的指针(可能与旧指针相同或不同)。
    • 重要: realloc 失败时返回 NULL但原始的 ptr 仍然有效。因此,通常的做法是将 realloc 的结果赋给一个临时指针,检查是否为 NULL,如果不是再更新原始指针。

为什么需要动态内存?

有时候,你在编写代码时不知道需要多少内存(比如用户输入的数量、从文件读取的数据量)。静态分配(如 int arr[10];)大小是固定的。动态分配让你可以在程序运行时根据需要申请内存,极大地提高了程序的灵活性。

小示例 (stdlib.h - 内存管理):

c
#include <stdio.h>
#include <stdlib.h> // 引入通用工具库

int main() {
    int *arr1 = NULL;
    int *arr2 = NULL;
    int *arr3 = NULL;
    int original_size = 3;
    int new_size = 5;

    // calloc 示例:分配并清零一个整数数组
    printf("使用 calloc 分配 %d 个整数并清零...\n", original_size);
    arr1 = (int*)calloc(original_size, sizeof(int));

    if (arr1 == NULL) {
        fprintf(stderr, "calloc 分配失败!\n");
        return 1;
    }

    printf("arr1 (calloced) 初始值: ");
    for (int i = 0; i < original_size; i++) {
        printf("%d ", arr1[i]); // 应该都是 0
    }
    printf("\n");

    // malloc 示例:分配一个整数数组(内容不确定)
    printf("使用 malloc 分配 %d 个整数...\n", original_size);
    arr2 = (int*)malloc(original_size * sizeof(int));

     if (arr2 == NULL) {
        fprintf(stderr, "malloc 分配失败!\n");
        // 在这里也应该释放 arr1,虽然本例后面会集中释放
        free(arr1);
        return 1;
    }

    printf("arr2 (malloced) 初始值 (不确定): ");
     for (int i = 0; i < original_size; i++) {
        // 打印 malloc 分配的内存内容,通常是垃圾值
        printf("%d ", arr2[i]);
        // 为了后续 realloc 示例,给 arr2 赋值
        arr2[i] = (i + 1) * 10;
     }
     printf("\n");
     printf("arr2 赋值后: ");
     for (int i = 0; i < original_size; i++) {
        printf("%d ", arr2[i]);
     }
     printf("\n");


    // realloc 示例:重新分配 arr2 的大小
    printf("使用 realloc 将 arr2 大小从 %d 调整到 %d 个整数...\n", original_size, new_size);
    // **重要:** 将 realloc 结果赋给临时指针
    int *temp_ptr = (int*)realloc(arr2, new_size * sizeof(int));

    if (temp_ptr == NULL) {
        fprintf(stderr, "realloc 失败!原始 arr2 仍然有效!\n");
        // realloc 失败,arr2 指向的内存块没变,内容也没变
        // 你仍然可以使用并最终释放 arr2
        // 在这个例子中,为了演示 free,我们直接跳到 free 步骤
    } else {
        printf("realloc 成功。新 arr2 的值 (可能包含旧数据): ");
        arr3 = temp_ptr; // realloc 成功,更新原始指针
        // 访问新分配的内存
        for (int i = 0; i < new_size; i++) {
             // 前 original_size 个元素可能保留旧值
            printf("%d ", arr3[i]);
        }
        printf("\n");

         // 新增部分未初始化,需要自己赋值或处理
         if (new_size > original_size) {
             printf("初始化新增的 %d 个元素...\n", new_size - original_size);
             for (int i = original_size; i < new_size; i++) {
                 arr3[i] = (i + 1) * 10;
             }
              printf("arr3 赋值后: ");
              for (int i = 0; i < new_size; i++) {
                printf("%d ", arr3[i]);
              }
              printf("\n");
         }
    }


    // 释放所有动态分配的内存
    printf("\n释放内存...\n");
    free(arr1);
    printf("arr1 释放成功。\n");

    // realloc 成功时,free 原来的 arr2 是不安全的,因为 realloc 可能已经释放了它。
    // 如果 realloc 失败,arr2 仍然需要被 free。
    // 这里的逻辑依赖 realloc 是否成功。假设 realloc 成功,我们应该 free arr3 (它现在是 arr2 的新地址)
    if (arr3 != NULL) { // 只有 realloc 成功时 arr3 才非 NULL
       free(arr3);
       printf("arr3 (原 arr2 的新地址) 释放成功。\n");
    } else if (arr2 != NULL) { // 如果 realloc 失败,arr3 是 NULL,但 arr2 仍需释放
       free(arr2);
       printf("arr2 释放成功 (realloc 失败)。\n");
    }


    printf("所有动态内存已释放。\n");

    return 0;
}

重要提示: 动态内存管理是C语言的强大之处,也是最容易出错的地方(内存泄漏、野指针、重复释放)。务必小心!

2. 文件 I/O:与磁盘上的文件交互 (stdio.h)

除了标准输入输出,stdio.h 还提供了强大的文件操作功能。这让你能读取、写入、创建文件。

  • FILE *fopen(const char *filename, const char *mode): 打开一个文件。
    • filename: 文件名(包括路径)。
    • mode: 打开模式字符串,如 "r" (读), "w" (写,覆盖原有内容), "a" (追加), "rb" (读二进制), "wb" (写二进制), "ab" (追加二进制), "r+" (读写), "w+" (读写,覆盖), "a+" (读写,追加)。
    • 成功时返回一个指向 FILE 结构的指针(文件流),失败时返回 NULL务必检查返回值!
  • int fclose(FILE *stream): 关闭一个文件流。会刷新缓冲区数据到文件并释放相关的系统资源。成功时返回0,失败时返回 EOF
  • int fprintf(FILE *stream, const char *format, ...): 格式化输出到指定的文件流,用法类似 printf
  • int fscanf(FILE *stream, const char *format, ...): 从指定的文件流中格式化读取,用法类似 scanf
  • int fgetc(FILE *stream): 从文件流中读取一个字符。
  • int fputc(int character, FILE *stream): 向文件流中写入一个字符。
  • char *fgets(char *str, int size, FILE *stream): 从文件流中读取一行字符串(最多 size-1 个字符),并存储到 str 中。安全函数。
  • int fputs(const char *str, FILE *stream): 向文件流中写入一个字符串。
  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream): 从文件流中读取 nmemb 个大小为 size 的数据项到 ptr 指向的缓冲区。常用于二进制文件。
  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream): 向文件流中写入 nmemb 个大小为 size 的数据项,数据来源于 ptr 指向的缓冲区。常用于二进制文件。
  • int fseek(FILE *stream, long offset, int origin): 移动文件流的读写位置。origin 可以是 SEEK_SET (文件开头), SEEK_CUR (当前位置), SEEK_END (文件末尾)。
  • long ftell(FILE *stream): 获取文件流的当前位置。

小示例 (stdio.h - 文件 I/O):

c
#include <stdio.h> // 引入标准输入输出库 (包含文件 I/O)
#include <stdlib.h> // 用于 exit 或返回非零错误码

int main() {
    FILE *file_ptr = NULL; // 声明文件指针

    // --- 写入文件示例 ---
    file_ptr = fopen("example.txt", "w"); // 以写入模式打开文件,如果文件存在会清空

    if (file_ptr == NULL) {
        perror("打开 example.txt 用于写入失败"); // 打印错误信息
        return 1; // 返回错误码
    }

    fprintf(file_ptr, "Hello from C!\n"); // 写入一行文本
    fprintf(file_ptr, "This is the second line.\n");
    fputc('A', file_ptr); // 写入一个字符
    fputc('\n', file_ptr);

    fputs("This is a line using fputs.\n", file_ptr); // 写入一个字符串

    if (fclose(file_ptr) == EOF) {
        perror("关闭 example.txt 失败");
        return 1;
    }
    printf("数据已写入 example.txt\n");


    // --- 读取文件示例 ---
    file_ptr = fopen("example.txt", "r"); // 以读取模式打开文件

    if (file_ptr == NULL) {
        perror("打开 example.txt 用于读取失败");
        return 1;
    }

    printf("\n读取 example.txt 的内容:\n");
    char buffer[100];
    // 使用 fgets 逐行读取
    while (fgets(buffer, sizeof(buffer), file_ptr) != NULL) {
        printf("%s", buffer); // fgets 读取的字符串包含换行符,所以直接打印
    }

    // 检查文件读取是否是因为到达文件末尾 (EOF) 还是发生错误
    if (feof(file_ptr)) {
        // 读取直到文件末尾是正常的
    } else if (ferror(file_ptr)) {
        perror("读取 example.txt 发生错误");
        fclose(file_ptr); // 发生错误也要尝试关闭文件
        return 1;
    }


    if (fclose(file_ptr) == EOF) {
        perror("关闭 example.txt 失败");
        return 1;
    }
    printf("文件读取完成。\n");


    // --- 追加文件示例 ---
    file_ptr = fopen("example.txt", "a"); // 以追加模式打开文件

    if (file_ptr == NULL) {
        perror("打开 example.txt 用于追加失败");
        return 1;
    }

    fprintf(file_ptr, "--- Appended line at %s ---\n", __TIME__); // 追加一行文本,包含当前时间

    if (fclose(file_ptr) == EOF) {
        perror("关闭 example.txt 失败");
        return 1;
    }
    printf("数据已追加到 example.txt\n");


    return 0; // 成功退出
}

3. 更强大的字符串与内存操作:string.h & 内存函数

除了第一部分介绍的基础函数,string.h 还提供了一些操作内存块的函数,它们不关心是否是null终止的字符串,而是按字节进行操作。

  • strncpy(char *dest, const char *src, size_t n): 复制 src 的前 n 个字符到 dest注意: 如果 src 的长度小于 ndest 的剩余部分会用零填充;如果 src 的长度大于等于 ndest 不会自动以null终止,除非 n 恰好是 src 的长度且包含终止符。
  • strncat(char *dest, const char *src, size_t n): 将 src 的前 n 个字符追加到 dest 的末尾,并在结果末尾添加一个null终止符。
  • strncmp(const char *s1, const char *s2, size_t n): 比较 s1s2 的前 n 个字符。
  • strchr(const char *s, int c): 在字符串 s 中查找字符 c 第一次出现的位置。返回一个指向该字符的指针,或在未找到时返回 NULL
  • strrchr(const char *s, int c): 在字符串 s 中查找字符 c 最后一次出现的位置。返回一个指向该字符的指针,或在未找到时返回 NULL
  • memset(void *s, int c, size_t n): 将 s 指向的内存块的前 n 个字节设置为指定的值 c (以 unsigned char 形式)。常用于清零 (memset(buffer, 0, sizeof(buffer));) 或初始化。返回 s
  • memcpy(void *dest, const void *src, size_t n): 从 src 指向的内存区域复制 n 个字节到 dest 指向的内存区域。注意: destsrc 区域不能重叠。返回 dest
  • memmove(void *dest, const void *src, size_t n): 功能类似 memcpy,但可以正确处理 destsrc 区域重叠的情况。返回 dest

小示例 (string.h - 扩展):

c
#include <stdio.h>
#include <string.h> // 引入字符串处理库

int main() {
    char buffer[20];
    char src[] = "Hello World";
    char dest[20] = "Greeting: ";

    // strncpy 示例 (注意潜在的非null终止问题)
    printf("使用 strncpy 复制前 5 个字符...\n");
    strncpy(buffer, src, 5); // 复制 "Hello"
    buffer[5] = '\0'; // 手动添加null终止符,因为 n=5 小于 src 长度
    printf("strncpy 结果: \"%s\"\n", buffer); // 输出 "Hello"

    printf("使用 strncpy 复制前 15 个字符...\n");
    // n=15 大于 src 长度,buffer 会被 "Hello World\0" 填充,剩余空间清零
    strncpy(buffer, src, 15);
    printf("strncpy 结果 (n > strlen): \"%s\"\n", buffer); // 输出 "Hello World"


    // strncat 示例
    printf("\n使用 strncat 连接前 5 个字符...\n");
    // 将 src 的前 5 个字符 "Hello" 追加到 dest "Greeting: " 后面
    strncat(dest, src, 5);
    printf("strncat 结果: \"%s\"\n", dest); // 输出 "Greeting: Hello"


    // strchr 和 strrchr 示例
    char path[] = "/home/user/document/file.txt";
    char *first_slash = strchr(path, '/'); // 查找第一个 '/'
    char *last_dot = strrchr(path, '.');   // 查找最后一个 '.'

    if (first_slash) {
        printf("\n第一个 '/' 的位置: %ld\n", first_slash - path);
    }
    if (last_dot) {
        printf("最后一个 '.' 的位置: %ld\n", last_dot - path);
    }


    // memset 和 memcpy 示例
    char data[10];
    printf("\n初始化内存块为 'X'...\n");
    memset(data, 'X', sizeof(data)); // 将 data 的所有字节设置为 'X'
    printf("memset 结果: ");
    for(int i = 0; i < sizeof(data); i++) printf("%c", data[i]);
    printf("\n");

    char source_data[] = "ABCDEF";
    char destination_data[10];
    printf("使用 memcpy 复制 \"%s\"...\n", source_data);
    // 将 source_data 的前 6 个字节复制到 destination_data
    memcpy(destination_data, source_data, strlen(source_data) + 1); // +1 为了复制 null 终止符
    printf("memcpy 结果: \"%s\"\n", destination_data);

    return 0;
}

4. 其他实用工具箱成员 (stdlib.h & errno.h)

stdlib.h 中还有一些其他非常实用的函数。同时,了解如何检查库函数调用是否成功也很重要,这时 errno.h 就派上用场了。

  • qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)): 对数组进行快速排序。
    • base: 指向数组第一个元素的指针。
    • nmemb: 数组元素的数量。
    • size: 每个元素的字节大小。
    • compar: 指向一个比较函数的指针,该函数接受两个指向元素的 const void * 指针,根据第一个元素相对于第二个元素的大小关系返回负数、零或正数。
  • system(const char *command): 执行操作系统命令。注意: 调用外部命令可能存在安全风险,特别是当 command 来源于不受信任的输入时。
  • getenv(const char *name): 获取指定名称的环境变量的值。
  • int errno: 在发生错误时,某些库函数会设置这个全局变量。它的值表示错误类型。需要 #include <errno.h>
  • void perror(const char *s): 打印一条描述 errno 当前值的错误信息,前面可加上自定义消息 s:
  • char *strerror(int errnum): 返回一个指向错误信息字符串的指针,该字符串描述了错误号 errnum (通常就是 errno 的值)。需要 #include <string.h>#include <errno.h> (C99)。

小示例 (stdlib.h & errno.h):

c
#include <stdio.h>
#include <stdlib.h> // 引入通用工具库
#include <string.h> // 用于 strerror (C99+ 也可用 errno.h)
#include <errno.h>  // 引入错误码库

// qsort 需要的比较函数
int compare_integers(const void *a, const void *b) {
    int arg1 = *(const int*)a;
    int arg2 = *(const int*)b;
    // 如果 arg1 < arg2 返回负数
    // 如果 arg1 == arg2 返回 0
    // 如果 arg1 > arg2 返回正数
    // (arg1 > arg2) - (arg1 < arg2) 是一个简洁的实现
    return (arg1 > arg2) - (arg1 < arg2);
    // 也可以直接 return arg1 - arg2; 但需要注意整数溢出风险,不过对于一般 int 范围通常没问题
}

int main() {
    // qsort 示例
    int numbers[] = {5, 2, 8, 1, 9, 4};
    int count = sizeof(numbers) / sizeof(numbers[0]);

    printf("排序前: ");
    for (int i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    // 对整个数组进行排序
    qsort(numbers, count, sizeof(int), compare_integers);

    printf("排序后: ");
    for (int i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");


    // system 示例 (取决于操作系统)
    printf("\n执行系统命令 'ls' (或 'dir' on Windows)...\n");
    // int status = system("ls"); // Linux/macOS
    int status = system("dir"); // Windows
    if (status == -1) {
         perror("执行系统命令失败");
    } else {
        printf("系统命令执行完毕,返回状态码: %d\n", status);
    }


    // getenv 示例 (获取PATH环境变量的值)
    char *path_env = getenv("PATH"); // 或 "Path" on Windows
    if (path_env) {
        printf("\nPATH 环境变量的值是:\n%s\n", path_env);
    } else {
        printf("\n未找到 PATH 环境变量。\n");
    }

    // errno 和 perror/strerror 示例 (演示文件打开失败)
    FILE *non_existent_file = fopen("non_existent_file.txt", "r");
    if (non_existent_file == NULL) {
        printf("\n尝试打开不存在的文件失败。\n");
        printf("errno 的值是: %d\n", errno); // 打印错误码
        perror("附加信息:"); // 打印基于 errno 的系统错误信息,前面加 "附加信息:"
        printf("strerror 描述:%s\n", strerror(errno)); // 打印基于 errno 的错误字符串
    } else {
        printf("意外:成功打开了 non_existent_file.txt\n");
        fclose(non_existent_file);
    }


    return 0;
}

结语

至此,我们已经探索了C语言标准库中大量常用且强大的函数,包括基础I/O、字符串处理、数学计算、动态内存管理、文件操作以及一些通用的实用工具。

标准库是C程序员的宝库。理解并熟练使用这些函数,将极大地提升你的编程效率和代码质量。请记住,查阅官方文档是学习标准库的最佳方式,它可以提供函数最准确、最详细的说明。

希望这个系列的文章能为你打开C语言标准库的大门,让你更有信心去编写更复杂、更强大的C程序!

多动手实践,不断尝试,你会在C语言的世界里越走越远!

下次见! 👋