网站首页 > 文章精选 正文
C标准库(Standard Library)提供了一系列预定义的函数和宏,涵盖了输入/输出、字符串处理、数学运算、内存管理、时间日期等常用功能。除了基础用法外,标准库中还包含一些高级或不常用的功能,掌握它们能够帮助开发者编写更高效、更灵活或能处理特殊情况的C代码。qsort/bsearch 提供了通用的排序和搜索能力,setjmp/longjmp 提供了一种非本地跳转(类似异常处理)的机制,而 va_list 系列宏则允许创建接受可变数量参数的函数。
本文将深入探讨这三组高级标准库功能的用法、原理和注意事项。
1. 通用排序与搜索:qsort和 bsearch
<stdlib.h> 头文件提供了两个强大的通用函数:qsort 用于对任意类型的数组进行排序,bsearch 用于在已排序的数组中执行二分搜索。
它们的通用性来自于使用了 void* 指针和回调函数(比较函数)。
1.1 qsort- 通用排序
void qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *));
- base:指向要排序数组的第一个元素的指针 (void*)。
- num:数组中元素的数量 (size_t)。
- size:数组中每个元素的大小(以字节为单位)(size_t)。
- compar:比较函数指针。该函数接收两个指向数组元素的 const void* 指针,并返回一个整数值来指示两个元素的相对顺序:
- 返回值 < 0:第一个元素应排在第二个元素之前。
- 返回值 = 0:两个元素相等(相对顺序不确定,qsort 不保证稳定性)。
- 返回值 > 0:第一个元素应排在第二个元素之后。
工作原理: qsort 内部实现了高效的排序算法(通常是快速排序或其变种),但它不知道如何比较具体类型的数据。它通过调用用户提供的 compar 函数来确定元素间的顺序。qsort 会将数组元素的地址(强制转换为 const void*)传递给 compar 函数。
示例:对整数数组和结构体数组排序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 比较整数 (升序)
int compare_int(const void *a, const void *b) {
int int_a = *(const int*)a;
int int_b = *(const int*)b;
return int_a - int_b;
}
// 定义结构体
typedef struct {
int id;
char name[20];
} Record;
// 比较 Record 结构体 (按 id 升序)
int compare_record_id(const void *a, const void *b) {
const Record *rec_a = (const Record*)a;
const Record *rec_b = (const Record*)b;
return rec_a->id - rec_b->id;
}
// 比较 Record 结构体 (按 name 字典序升序)
int compare_record_name(const void *a, const void *b) {
const Record *rec_a = (const Record*)a;
const Record *rec_b = (const Record*)b;
return strcmp(rec_a->name, rec_b->name);
}
void print_int_array(int arr[], size_t n) {
for (size_t i = 0; i < n; ++i) printf("%d ", arr[i]);
printf("\n");
}
void print_record_array(Record arr[], size_t n) {
for (size_t i = 0; i < n; ++i) printf("(ID: %d, Name: %s) ", arr[i].id, arr[i].name);
printf("\n");
}
int main() {
// 整数排序
int numbers[] = {40, 10, 100, 90, 20, 25};
size_t num_count = sizeof(numbers) / sizeof(numbers[0]);
printf("Original integers: "); print_int_array(numbers, num_count);
qsort(numbers, num_count, sizeof(int), compare_int);
printf("Sorted integers: "); print_int_array(numbers, num_count);
printf("\n");
// 结构体排序
Record records[] = {
{3, "Charlie"}, {1, "Alice"}, {2, "Bob"}
};
size_t rec_count = sizeof(records) / sizeof(records[0]);
printf("Original records: "); print_record_array(records, rec_count);
// 按 ID 排序
qsort(records, rec_count, sizeof(Record), compare_record_id);
printf("Sorted by ID: "); print_record_array(records, rec_count);
// 按 Name 排序
qsort(records, rec_count, sizeof(Record), compare_record_name);
printf("Sorted by Name: "); print_record_array(records, rec_count);
return 0;
}
注意事项:
- 比较函数的正确性至关重要。它必须为任意两个元素提供一致的排序关系(反对称性、传递性)。
- qsort 不保证稳定性。如果数组中存在相等的元素,它们在排序后的相对顺序可能改变。
- 传递给 qsort 的 size 参数必须是每个元素的确切大小。
1.2 bsearch- 通用二分搜索
void *bsearch(const void *key, const void *base, size_t num, size_t size, int (*compar)(const void *, const void *));
- key:指向要搜索的键值的指针 (const void*)。这个键值的类型和内存布局必须与数组元素兼容,以便比较函数能处理。
- base:指向已排序数组的第一个元素的指针 (const void*)。
- num:数组中元素的数量 (size_t)。
- size:数组中每个元素的大小(以字节为单位)(size_t)。
- compar:比较函数指针。签名与 qsort 的比较函数相同,但参数的意义略有不同:
- 第一个参数 (const void*) 始终是指向 key 的指针。
- 第二个参数 (const void*) 是指向数组中某个元素的指针。
- 返回值含义与 qsort 相同,用于指示 key 与当前数组元素的相对顺序。
返回值:
- 如果找到与 key 匹配的元素,返回指向该元素的指针 (void*)。
- 如果未找到匹配的元素,返回 NULL。
- 如果数组中有多个元素与 key 匹配,bsearch 返回哪一个是不确定的。
前提条件: bsearch 要求输入的数组 base 必须已经按照 compar 函数所定义的顺序排好序。
示例:在已排序的整数和结构体数组中搜索
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- 复用之前的 compare_int, Record, compare_record_id ---
// 比较整数 (升序)
int compare_int(const void *a, const void *b) {
int int_a = *(const int*)a;
int int_b = *(const int*)b;
return int_a - int_b;
}
// 定义结构体
typedef struct {
int id;
char name[20];
} Record;
// 比较 Record 结构体 (按 id 升序)
// 注意:bsearch 的比较函数第一个参数是 key
int compare_record_id_for_bsearch(const void *key, const void *element) {
int key_id = *(const int*)key; // 假设 key 就是一个 int ID
const Record *rec_element = (const Record*)element;
return key_id - rec_element->id;
}
// --- end of reuse ---
int main() {
// 搜索整数
int numbers[] = {10, 20, 25, 40, 90, 100}; // 已升序排列
size_t num_count = sizeof(numbers) / sizeof(numbers[0]);
int key_int = 40;
int not_found_key = 50;
int *found_int = (int*)bsearch(&key_int, numbers, num_count, sizeof(int), compare_int);
if (found_int) {
printf("Integer %d found at address %p, value %d\n", key_int, (void*)found_int, *found_int);
} else {
printf("Integer %d not found.\n", key_int);
}
int *not_found_ptr = (int*)bsearch(not_found_key, numbers, num_count, sizeof(int), compare_int);
if (!not_found_ptr) {
printf("Integer %d not found.\n", not_found_key);
}
printf("\n");
// 搜索结构体 (按 ID)
Record records[] = {
{1, "Alice"}, {2, "Bob"}, {3, "Charlie"} // 已按 ID 升序排列
};
size_t rec_count = sizeof(records) / sizeof(records[0]);
int key_id = 2;
// key 是要搜索的 ID (int 类型)
Record *found_rec = (Record*)bsearch(&key_id, records, rec_count, sizeof(Record), compare_record_id_for_bsearch);
if (found_rec) {
printf("Record with ID %d found: Name=%s\n", key_id, found_rec->name);
} else {
printf("Record with ID %d not found.\n", key_id);
}
return 0;
}
注意事项:
- 数组必须已排序,且排序使用的比较逻辑必须与传递给 bsearch 的比较逻辑一致。
- key 的类型和比较函数中对 key 的解引用方式必须正确匹配。
- 比较函数的第一个参数始终是 key,第二个参数是数组元素。
2. 非本地跳转:setjmp和 longjmp
<setjmp.h> 头文件提供了 setjmp 和 longjmp 两个函数,它们允许程序实现一种非本地跳转 (non-local jump),可以从一个函数内部直接跳转到另一个(通常是调用栈上层)函数的特定位置。这可以用于实现简单的错误处理机制(类似异常处理),或者在深层嵌套调用中快速返回。
警告: setjmp/longjmp 是一种强大但危险的机制。它绕过了正常的函数返回流程,可能导致资源泄漏(如未关闭的文件、未释放的内存)、栈状态不一致等问题。应谨慎使用,并充分理解其副作用。
2.1 setjmp- 设置跳转点
int setjmp(jmp_buf env);
- env:一个 jmp_buf 类型的变量(通常是一个数组或结构体,具体类型由实现定义)。jmp_buf 用于保存当前的执行环境(包括程序计数器、栈指针、寄存器等)。
- 行为:
- 直接调用时:setjmp 保存当前环境到 env 中,并返回 0。
- 通过 longjmp 返回时:当 longjmp(env, value) 被调用时,程序执行流会跳转回对应的 setjmp 调用点。此时,setjmp 再次返回,但返回值是 longjmp 传递的 value(如果 value 为 0,setjmp 会返回 1)。
2.2 longjmp- 执行跳转
void longjmp(jmp_buf env, int value);
- env:先前由 setjmp 保存的环境信息。
- value:一个非零整数,将作为对应 setjmp 的返回值。如果传递 0,setjmp 会返回 1。
- 行为:longjmp 恢复 env 中保存的环境,并将程序执行流强制跳转到对应的 setjmp 调用点。longjmp 本身从不返回。
限制与规则:
- 调用 longjmp 的函数必须是被(直接或间接)调用 setjmp 的函数所调用的。不能跳转到已经返回的函数的 setjmp 点。
- setjmp 必须在特定的上下文中调用,例如:
- 作为 if, switch, while, do-while, for 语句的控制表达式。
- 作为简单的赋值语句的右侧。
- 作为函数调用的参数。
- 作为独立的语句 setjmp(env);。 在其他复杂表达式中调用 setjmp 可能导致未定义行为。
- 重要: 在 setjmp 调用之后、longjmp 发生之前,那些非 volatile 的自动存储期变量(普通局部变量)的值是不确定 (indeterminate) 的。如果需要在 longjmp 后访问这些变量,应将它们声明为 volatile。
示例:使用 setjmp/longjmp 进行错误处理
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
jmp_buf error_handler_env;
// 可能触发错误的深层函数
void process_data(int data) {
printf("Processing data: %d\n", data);
if (data < 0) {
printf("Error: Invalid data encountered!\n");
longjmp(error_handler_env, 1); // 跳转到错误处理点,返回值为 1
}
if (data > 100) {
printf("Error: Data too large!\n");
longjmp(error_handler_env, 2); // 跳转,返回值为 2
}
// 模拟资源分配
FILE *temp_file = fopen("temp.txt", "w");
if (!temp_file) {
printf("Error: Cannot open temp file!\n");
longjmp(error_handler_env, 3); // 跳转,返回值为 3
}
printf("Temp file opened.\n");
// ... 使用文件 ...
printf("Processing successful for data %d.\n", data);
fclose(temp_file); // 正常路径下关闭文件
printf("Temp file closed.\n");
}
// 调用处理函数的上层函数
void run_processor() {
volatile int resource_allocated = 0; // 使用 volatile
FILE *log_file = NULL;
log_file = fopen("log.txt", "a");
if (log_file) {
resource_allocated = 1;
fprintf(log_file, "Processor started.\n");
}
// 正常处理流程
process_data(50);
process_data(150); // 这会触发 longjmp
process_data(20); // 这行不会执行
// 如果 longjmp 发生,这里的清理代码会被跳过!
if (resource_allocated) {
fprintf(log_file, "Processor finished normally.\n");
fclose(log_file);
}
}
int main() {
int error_code;
// 设置跳转点
error_code = setjmp(error_handler_env);
if (error_code == 0) {
// --- 正常执行路径 ---
printf("Entering normal execution path.\n");
run_processor();
printf("Normal execution finished without errors.\n");
} else {
// --- 错误处理路径 (由 longjmp 跳转而来) ---
printf("\n--- Error Handling Path ---\n");
printf("An error occurred! Error code: %d\n", error_code);
// 在这里进行集中的错误清理
// 注意:run_processor 中分配的 log_file 可能未关闭!
// 这就是 setjmp/longjmp 的危险之处,资源管理困难。
printf("Performing cleanup actions...\n");
// 无法安全地关闭 run_processor 中的 log_file
// 也无法安全地关闭 process_data 中可能未关闭的 temp_file
}
printf("\nProgram exit.\n");
return 0;
}
setjmp/longjmp 的替代方案:
- 返回错误码:最常用、最安全的错误处理方式。函数通过返回值告知调用者是否成功以及错误类型。
- 全局错误状态:如 errno,但管理复杂且不利于模块化。
- 特定于库的错误处理机制:许多库提供自己的错误处理回调或上下文。
- C++ 异常处理 (try/catch/throw):提供了更结构化、更安全的非本地跳转和资源管理(通过RAII)。
何时考虑使用 setjmp/longjmp?
- 实现协程或轻量级线程库。
- 在无法修改大量现有代码以传递错误码的情况下,添加一个集中的错误处理点(需极其小心资源管理)。
- 某些性能极其敏感的底层代码(但现代编译器优化通常使得返回错误码的开销很小)。
总的来说,尽量避免使用 setjmp/longjmp,优先选择返回错误码或其他更安全的机制。
3. 可变参数函数:va_list, va_start, va_arg, va_end
<stdarg.h> 头文件提供了一组宏,允许创建可以接受可变数量参数的函数,例如 printf 和 scanf。
核心概念:
- 函数声明中,固定参数之后使用省略号 ... 表示接受可变参数。
- 必须至少有一个固定参数,va_start 需要用最后一个固定参数来定位可变参数列表的起始位置。
- 使用 va_list 类型变量来存储可变参数列表的状态。
- 使用 va_start 初始化 va_list 变量。
- 使用 va_arg 按顺序、按类型访问可变参数。
- 使用 va_end 在访问完所有参数后清理 va_list。
宏定义:
- va_list:一种类型,用于声明一个指向参数列表的变量。
- void va_start(va_list ap, last_fixed_arg);:初始化 ap,使其指向 last_fixed_arg(最后一个固定参数)之后的第一个可变参数。
- type va_arg(va_list ap, type);:获取 ap 当前指向的可变参数的值,并将其类型视为 type,然后将 ap 更新为指向下一个可变参数。调用者必须知道每个可变参数的正确类型!
- void va_end(va_list ap);:结束可变参数的处理,进行必要的清理。必须在函数返回前调用。
- void va_copy(va_list dest, va_list src); (C99):复制可变参数列表的状态。允许在不破坏原始列表的情况下多次遍历参数。
示例:实现一个简单的求和函数
#include <stdio.h>
#include <stdarg.h>
// 计算可变数量整数的总和
// 第一个参数 count 指定了后面有多少个整数参数
int sum_integers(int count, ...) { // 至少一个固定参数 count
int total = 0;
va_list args; // 声明 va_list 变量
// 初始化 args,使其指向 count 后面的第一个可变参数
va_start(args, count);
// 循环读取 count 个整数参数
for (int i = 0; i < count; ++i) {
// 获取当前参数 (类型为 int),并将 args 指向下一个
int value = va_arg(args, int);
total += value;
}
// 清理 args
va_end(args);
return total;
}
// 示例:实现一个简单的 printf 风格的日志函数
void my_log(const char *level, const char *format, ...) {
va_list args;
printf("[%s] ", level);
va_start(args, format);
// 使用 vprintf,它是 printf 的变体,接受 va_list
vprintf(format, args);
va_end(args);
printf("\n");
}
int main() {
int s1 = sum_integers(3, 10, 20, 30); // 3 个可变参数
int s2 = sum_integers(5, 1, 2, 3, 4, 5); // 5 个可变参数
int s3 = sum_integers(0); // 0 个可变参数
printf("Sum 1: %d\n", s1); // 60
printf("Sum 2: %d\n", s2); // 15
printf("Sum 3: %d\n", s3); // 0
my_log("INFO", "Program started.");
my_log("DEBUG", "User ID: %d, Name: %s", 123, "Alice");
my_log("ERROR", "Failed to open file: %s, Error code: %d", "data.txt", 5);
return 0;
}
类型安全问题与传递机制:
- 类型安全缺失:va_arg(args, type) 中的 type 完全由调用者指定。如果指定的类型与实际传递的参数类型不匹配,结果是未定义行为。
- 如何知道类型和数量? 可变参数函数必须通过某种机制来确定参数的数量和类型:
- 格式字符串:像 printf 那样,使用格式说明符(如 %d, %f, %s)来指示后续参数的类型。
- 固定参数指定数量:如 sum_integers 示例,第一个参数明确告知后面有多少个可变参数(通常要求这些参数类型相同)。
- 哨兵值 (Sentinel Value):参数列表以一个特殊的、约定好的值结束(例如 NULL 指针)。
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
// 连接多个字符串,以 NULL 结束
char* concatenate_strings(const char *first, ...) {
va_list args;
size_t total_len = strlen(first);
const char *current;
// 第一次遍历计算总长度
va_start(args, first);
while ((current = va_arg(args, const char*)) != NULL) {
total_len += strlen(current);
}
va_end(args);
char *result = malloc(total_len + 1);
if (!result) return NULL;
strcpy(result, first);
// 第二次遍历进行拼接
va_start(args, first);
while ((current = va_arg(args, const char*)) != NULL) {
strcat(result, current);
}
va_end(args);
return result; // 调用者需要 free
}
// ... main 中调用 free(concatenated_string); ...
- 默认参数提升 (Default Argument Promotions):当调用可变参数函数时,某些类型的参数会自动提升:
- char, short (以及它们的 signed/unsigned 版本) 会提升为 int (或 unsigned int)。
- float 会提升为 double。 因此,在 va_arg 中永远不应使用 char, short 或 float 作为类型参数,而应使用提升后的类型 int 或 double。
注意事项:
- 必须包含 <stdarg.h>。
- 必须至少有一个固定参数。
- va_start 必须在第一次调用 va_arg 或 va_copy 之前调用。
- va_end 必须在函数返回前调用。
- 类型匹配是调用者的责任,错误会导致未定义行为。
- 注意默认参数提升。
4. 总结
- qsort 和 bsearch 提供了强大的通用排序和搜索功能,依赖于用户提供的比较函数回调,适用于各种数据类型。
- setjmp 和 longjmp 提供了非本地跳转机制,可用于错误处理或快速返回,但使用危险,易导致资源泄漏和状态不一致,应优先使用返回错误码等更安全的方法。
- 可变参数宏 (<stdarg.h>) 允许创建接受不定数量参数的函数,但缺乏类型安全,需要通过格式字符串、计数值或哨兵值等方式确定参数类型和数量,并注意默认参数提升。
掌握这些标准库的高级功能,可以在特定场景下提供更灵活或高效的解决方案,但也需要更深入地理解其工作原理和潜在风险,谨慎使用。
猜你喜欢
- 2025-05-14 嵌入式开发中宝藏级别的C语言代码,使用频率高,绝对值得珍藏
- 2025-05-14 嵌入式面试常问的16个C语言问题
- 2025-05-14 如何利用CAS技术实现无锁队列
- 2025-05-14 并发编程:从线程到协程的技术演进与实战指南
- 2025-05-14 嵌入式工程师竟然看不懂这些专业语句,那真别怪人说你菜
- 2025-05-14 CPU缓存一致性:从理论到实战
- 2025-05-14 Java 魔法类 Unsafe 详解
- 2025-05-14 为QML创建C++插件
- 2025-05-14 C++ Qt开发:运用QThread多线程组件
- 2025-05-14 教你用C来实现基于Mempool的内存池设计
- 05-14TS,TypeScript,Windows环境下构建环境,安装、编译且运行
- 05-14TypeScript 也能开发AI应用了!
- 05-14搞懂 TypeScript 装饰器
- 05-14前端小哥哥:如何使用typescript开发实战项目?
- 05-14在 React 项目中,一般怎么处理错误?
- 05-14react19 常用状态管理
- 05-14Vue3开发极简入门(2):TypeScript定义对象类型
- 05-14C#与TypeScript语法深度对比
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)