sprintf系列函数
sprintf用于按格式生成字符串,并保存进指定的缓冲区中。
char buffer[50];
sprintf(buffer, "%s, %d", "Hello", 50);
printf("%s\n", buffer);
/*
"Hello, 50"
*/
sprintf可以保存动态生成的字符串,这是其方便之处。但不便之处在于,我们必须事先指定缓冲区的大小,且缓冲区的大小必须足够大。
asprintf可以有效地解决须事先确定大小的问题。
char *result;
int len = asprintf(&result, "%s, %d", "Hello", 25);
printf("'%s'\n", result); // 'Hello, 25'
printf("%d\n", len); // 9
free(result);
asprintf函数会在堆上自动分配一个足够大的空间以存储所生成的字符串。其第一个参数为一个字符串指针的地址,生成字符串后,asprintf将该参数指向新生成的字符串。asprintf返回新生成字符串的长度。因为在堆上动态分配内存,因此使用后须释放内存。
现在,我们想要编写一个函数,它将根据格式字符串及数量可变的参数,来生成并返回相应的字符串,而不仅仅用于终端的输出。
因涉及到可变长参数,我们需使用vasprintf来实现此目标。
#include <stdio.h>
#include <stdarg.h>
char *gen_fmt_str(char *fmt, ...) {
char *pstr;
va_list vaList;
va_start(vaList, fmt);
vasprintf(&pstr, fmt, vaList);
va_end(vaList);
return pstr;
}
int main() {
char *pstr = gen_fmt_str("%s %d %s", "I eat", 5, "apples every day.");
printf("The result is: '%s'\n", pstr);
free(pstr);
}
上面代码,我们调用gen_fmt_str函数来根据我们特定需求来生成并返回一个字符串,然后再进一步在其他场合使用该字符串,尽管上面仅是简单地将其打印出来。
gen_fmt_str是一个支持可变长参数的函数。在stdarg.h中定义了va_list, va_start, va_arg, va_end等几个宏,用于读取可变长参数。在该函数中,可变长参数的前面必须至少有一个具名的参数。
一般情况下,在支持可变长的函数内,我们应使用va_arg来解开每个参数。但该宏必须明确数据类型。
va_list vaList;
va_start(vaList, fmt);
char *first_arg = va_arg(vaList, char *);
int second_arg = va_arg(vaList, int);
va_end(vaList);
可见,这种方式缺乏灵活性。而vasprintf可在支持可变长参数的函数内直接使用可变长参数而无需逐一解开这些参数。
va_list vaList;
va_start(vaList, fmt);
vasprintf(&pstr, fmt, vaList);
va_end(vaList);
自动生成字符串,这个功能比较酷。但在C语言中,如何存储这个自动生成的字符串,却是一个相对比较棘手的问题。
对程序员来讲,最理想的地方是存储于字符串字面值池中。例如:
char *s = "Hello";
字面值池中的字符串,我们可以直接使用,且无需手工释放内存,非常方便。但这种方式只能通过上面的字面值来生成,也即必须手工逐字符来输入。它是由编译器来辅助实现的,不是C语言的规范。
我们可以拷贝:
char *src = "Hello";
char buffer[20];
strcpy(buffer, src);
但需提前分配足够的内存空间。这是在栈中使用需考虑的问题。
全局变量、静态变量?还是脱离不了需提前分配足够内存空间的问题,且污染性强。
综上考虑,我还是倾向于使用在堆上动态分配内存的字符串,但需及时释放内存。
自动内存管理
结合在《高级篇》中实现的链表,只需对gen_fmt_str函数修改一个地方,我们可以轻松实现自动管理及自动释放内存的功能。下面是完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "linkedlist.h"
LinkedList auto_mem_list;
void app_clean_up() {
freeList(&auto_mem_list, freeMem);
printf("Good bye!");
}
void print_str(void *data) {
char *s = data;
printf("%s\n", s);
}
void reg_atexit(void (*fptr)(void)) {
if (atexit(fptr) != 0) {
printf("Error! Can't register function at exit!");
exit(1);
}
}
char *gen_fmt_str(char *fmt, ...) {
char *pstr;
va_list vaList;
va_start(vaList, fmt);
vasprintf(&pstr, fmt, vaList);
va_end(vaList);
addNode(&auto_mem_list, pstr);
return pstr;
}
void init_mem_manager() {
reg_atexit(app_clean_up);
initLinkedList(&auto_mem_list);
}
int main() {
init_mem_manager();
gen_fmt_str("%s, %s.", "Hello", "Mike");
gen_fmt_str("%d + %d = %d", 5, 2, 7);
printList(&auto_mem_list, print_str);
}
先运行程序,终端输出:
Hello, Mike.
5 + 2 = 7
Freeing dynamic data in node, at: 0x600000650080
Freeing node, at: 0x600002a54040
Freeing dynamic data in node, at: 0x600000650100
Freeing node, at: 0x600002a54050
Good bye!
第一段打印出我们自动生成的两个字符串。第二段是程序在退出前自动清理内存时所打印的信息。可以看到,在堆上所创建的两个字符串的内存空间,以及链表管理所需额外的内存空间,均自动得以释放。
代码很清爽,思路很简单。程序一开始,先注册好程序退出前要执行的函数app_clean_up,这是清理内存的守护者。同时,我们初始化了一个链表。
而在生成动态存储的字符串时:
char *gen_fmt_str(char *fmt, ...) {
char *pstr;
va_list vaList;
va_start(vaList, fmt);
vasprintf(&pstr, fmt, vaList);
va_end(vaList);
addNode(&auto_mem_list, pstr);
return pstr;
}
在返回结果字符串前,通过调用addNode函数将该指针添加进链表中。剩下的问题,链表会帮助我们自动跟踪及管理。
虽然gen_fmt_str返回具体的字符串指针,但我们并不使用它们来打印信息。因为有链表的存在,我们通过调用链表的打印功能来实现:
printList(&auto_mem_list, print_str);