引言
在C和C++开发中,我们经常会用到printf来进行字符串的格式化,例如printf("format string %d, %d", 1, 2);
,这样的格式化只是用于打印调试信息。printf函数实现的是接收可变参数,然后解析格式化的字符串,最后输出到控制台。那么问题来了,当我们需要实现一个函数,根据传入的可变参数来生成格式化的字符串,应该怎么办呢?
正文
可变参数
首先来一个可变参数使用示例,testVariadic
方法接收int行的可变参数,并以可变参数为-1表示结束。va_list用于遍历可变参数,va_start
方法接收两个参数,第一个为va_list
,第二个为可变参数前一个参数,下面的例子里该参数为a。
1 | /** |
运行结果
1 | ------testVari------ |
格式化字符串
好了,我们已经介绍了怎样实现一个接收可变参数的C函数,接下来介绍根据接收的可变参数来格式化字符串。这里介绍两种方式,第一种是利用宏定义,第二种通过函数的方式来实现。
通过宏定义的方式
en…让咱们先来看看第一个版本的宏,这个宏定义对于不熟悉宏的人来说可能看着有点费劲,不过不要怕,稍后会做解释,代码如下:
1 |
|
宏基础知识
首先需要介绍宏用到的知识:\
, 这个\
的作用是可换行定义宏,毕竟如果一行很长的宏可读性很差,使用方式在换行时加上\
即可。第二个是介绍(format, ...)
,这里的...
是预定义的宏,用于接收可变参数,就像是printf
函数一样。接着介绍##__VA_ARGS__
,同样的__VA_ARGS__
也是预定义的宏,表示接收到的...
传入的可变参数。##
的作用是用来处理未传入可变参数的情况,当没有传入可变参数的时候,编译器或通过优化将snprintf(NULL, 0, format, ##__VA_ARGS__);
优化为snprintf(NULL, 0, format);
。你可以理解为没有可变参数时,##
前的逗号,
与__VA_ARGS__
都被“干掉了”。
你一定会觉得困惑,为什么要写do-while
语句呢?这是为了宏的健壮性,如果使用宏的人像下面这样使用的话,就会出问题
1 |
|
上面的代码连编译都不会通过, 会报错如下:
如果手动展开这个宏的话,会变成这个样子,问题就显而易见了。但是如果if
语句加上了{}
的话,就不会有问题,可以看出规范写法是多么的重要🐶(皮一下很开心)。
1 | void test() |
加上do-while
以后就不一样,加上do-while后的代码如下:
1 |
|
预处理之后代码如下:
1 | //展开后的代码 |
好了,宏的基础知识就介绍这么多了,接下来进入正题。
代码解析
为了方便阅读,原谅我在这里再贴一遍宏定义的代码:
1 |
|
首先,介绍一下snprintf()
函数,此函数的定义如下:
1 | /** |
为了方便理解,使用方式是这个样子的:
1 | void testSnprintf() |
运行结果:
1 | ------testSnprintf------ |
snprintf
函数还有一个用法是__str
和__size
分别传入NULL和0,返回值会是格式化字符串的实际长度,可以通过这个方式来获取正确的格式化size,从而避免malloc多余的空间,造成空间浪费。同时返回的size是不包含结束符\0
的,所以真正写入要buffer时,需要对size + 1。
相信通过我的解释,你一定能看懂上面这段代码了吧。哦,对了malloc的代码一定要记得free(敲重点)。
到了这里,如果细心思考的同学一定会问?这个宏根本没有实际用途好不好,我要的是能够把格式化的字符串作为返回值返回的,仅仅打印直接用printf
不就好了。其实,这样的宏还是有作用的,比如说当你要记录日志时,你可以像这样使用:
1 |
|
要将结果字符串返回的话,需要用到GNU C的赋值扩展,使用方式如下:
1 | int a = ({ |
这段代码变量a最终值会是6。利用gnu这个扩展,将之前的宏改造一下就能实现我们的需求,改造完成后是这个样子的:
1 |
|
调用宏的代码:
1 | void testByMacro1() |
原谅我的啰嗦,malloc开辟的空间一定要记得free。运行结果:
1 | ------testByMacro1------ |
至此利用宏的方式就介绍完了。
通过函数的方式
老规矩先上代码
1 | char *myFormatStringByFun(char *format, ...) |
这里利用的是vsnprintf
函数,此函数的定义在stdio.h
中的定义如下:
1 | /** |
vsnprintf
的具体使用方式和之前介绍的snprintf
是差不多的,这里就不再详细介绍了,不大明白的同学可以看看上面的介绍。哦,对了,这两个函数都是定义在stdio.h这个头文件下的
接下来就是试一下我们封装的函数了
1 | void testByFun() |
运行结果:
1 | ------testByFun------ |
格式化字符串的方法差不多介绍完了,不知道善于思考的你有没想到直接用宏定义来调用我们封装的函数呢?我就在这直接给出宏定义和使用方式了
1 |
|
运行结果:
1 | ------testMyFormatStringByFunQuick------ |
C++版本
对了,最初实现是用的C++版本,这里使用的是泛型,代码是这个样子的:
1 | template< typename... Args > |
其实和C语言版本的没什么差别,只是多了泛型的东西而已,相信聪明的你一定能看懂,看不懂的话,就去看看C++的泛型知识吧,哈哈哈。
结语
终于介绍完了,你可以在这里下载代码。写博客是真的有点累人,不过对于最近被面试打击的我来说,写博客能够让我对知识理解的更加透彻,毕竟要自己认真思考后才能够写的明白(至少我觉得讲明白了,哈哈哈)。如果有什么说的不对的地方,还请指出,感谢你的阅读,thks。