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
。- 如果
ptr
是NULL
,则行为类似于malloc(new_size)
。 - 如果
new_size
是 0 且ptr
非NULL
,则行为类似于free(ptr)
。 realloc
可能会在原地扩展内存,也可能分配新的内存块并将旧数据复制过去,然后释放旧块。它返回新块的指针(可能与旧指针相同或不同)。- 重要:
realloc
失败时返回NULL
,但原始的ptr
仍然有效。因此,通常的做法是将realloc
的结果赋给一个临时指针,检查是否为NULL
,如果不是再更新原始指针。
- 如果
为什么需要动态内存?
有时候,你在编写代码时不知道需要多少内存(比如用户输入的数量、从文件读取的数据量)。静态分配(如 int arr[10];
)大小是固定的。动态分配让你可以在程序运行时根据需要申请内存,极大地提高了程序的灵活性。
小示例 (stdlib.h - 内存管理):
#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):
#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
的长度小于n
,dest
的剩余部分会用零填充;如果src
的长度大于等于n
,dest
不会自动以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)
: 比较s1
和s2
的前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
指向的内存区域。注意:dest
和src
区域不能重叠。返回dest
。memmove(void *dest, const void *src, size_t n)
: 功能类似memcpy
,但可以正确处理dest
和src
区域重叠的情况。返回dest
。
小示例 (string.h - 扩展):
#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):
#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语言的世界里越走越远!
下次见! 👋