【发布时间】:2019-07-19 14:28:17
【问题描述】:
我想编写一个函数,以类似于 printf/sprintf 使用格式化字符串的方式在 LCD 上打印字符。
【问题讨论】:
-
您想要做什么与使用
fprintf()与打开的文件流有什么不同,以便您可以写入LCD?您想要与使用snprintf()格式化字符串有何不同,然后您可以使用其他功能将其传送到 LCD? (或者,换句话说,您没有告诉我们您的情况?您将如何将信息发送到 LCD?)
我想编写一个函数,以类似于 printf/sprintf 使用格式化字符串的方式在 LCD 上打印字符。
【问题讨论】:
fprintf() 与打开的文件流有什么不同,以便您可以写入LCD?您想要与使用snprintf() 格式化字符串有何不同,然后您可以使用其他功能将其传送到 LCD? (或者,换句话说,您没有告诉我们您的情况?您将如何将信息发送到 LCD?)
您可以使用 sprintf 函数来格式化字符串并打印到 LCD。
char buffer[50];
int a = 10, b = 20, c;
c = a + b;
sprintf(buffer, "Sum of %d and %d is %d", a, b, c);
现在buffer 将具有格式化的字符串
【讨论】:
您可以编写一个可变参数函数并将参数传递给vsnprintf():
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void display(int foo, int bar, char const *format, ...)
{
va_list arglist;
va_start(arglist, format);
int length = vsnprintf(NULL, 0, format, arglist);
char *buffer = malloc(length * sizeof *buffer);
vsnprintf(buffer, length, format, arglist);
va_end(arglist);
puts(buffer);
free(buffer);
}
int main(void)
{
display(42, 13, "%s %d %f", "Hello", 99, 100.13);
}
【讨论】:
vsnprintf 的好用法!您的示例非常适合 PC,但不适用于内存受限或实时环境(例如微控制器)。我在这里更详细地解释:stackoverflow.com/a/54916870/4561887.
int length = vsnprintf(NULL, 0, format, arglist); 替换为int length = vsnprintf(NULL, 0, format, arglist) + 1;。 +1 是为空终止符留出空间所必需的,因为vsnprintf() 返回“如果 n 足够大,将写入的字符数,不包括终止空字符。” (来源:cplusplus.com/reference/cstdio/vsnprintf)。我在这里最终测试了它:onlinegdb.com/rySMA9BI4.
此答案将所有其他答案中最好的部分合二为一。考虑到所有因素,我认为这是做到这一点的最佳方法,并将在展示示例后进行更详细的解释。
这是一个完整的示例,包括函数中的基本错误检查。在这里,我创建了一个类似于printf 的函数,名为lcd_printf(),其工作方式与printf() 完全相同。它使用vsnprintf() 将格式化的字符串存储到静态分配的缓冲区中。然后,您可以将此缓冲区发送到我的评论指示的位置的 LCD 显示器。
lcd_print.h:
// For info on the gcc "format" attribute, read here under the section titled
// "format (archetype, string-index, first-to-check)":
// https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes.
int lcd_printf(const char * format, ...) __attribute__((format(printf, 1, 2)));
lcd_print.c:
#include "lcd_print.h"
#include <stdarg.h> // for variable args: va_list
#include <stdio.h> // for vsnprintf()
#include <limits.h> // for INT_MIN
// `printf`-like function to print to the LCD display.
// Returns the number of chars printed, or a negative number in the event of an error.
// Error Return codes:
// 1. INT_MIN if vsnprintf encoding error, OR
// 2. negative of the number of chars it *would have printed* had the buffer been large enough (ie: buffer would
// have needed to be the absolute value of this size + 1 for null terminator)
int lcd_printf(const char * format, ...)
{
int return_code;
// Formatted string buffer: make as long as you need it to be to hold the longest string you'd ever want
// to print + null terminator
char formatted_str[128];
va_list arglist;
va_start(arglist, format);
// Produce the formatted string; see vsnprintf documentation: http://www.cplusplus.com/reference/cstdio/vsnprintf/
int num_chars_to_print = vsnprintf(formatted_str, sizeof(formatted_str), format, arglist);
va_end(arglist);
if (num_chars_to_print < 0)
{
// Encoding error
return_code = INT_MIN;
return return_code; // exit early
}
else if (num_chars_to_print >= sizeof(formatted_str))
{
// formatted_str buffer not long enough
return_code = -num_chars_to_print;
// Do NOT return here; rather, continue and print what we can
}
else
{
// No error
return_code = num_chars_to_print;
}
// Now do whatever is required to send the formatted_str buffer to the LCD display here.
return return_code;
}
main.c:
#include "lcd_print.h"
int main(void)
{
int num1 = 7;
int num2 = -1000;
unsigned int num3 = 0x812A;
lcd_printf("my 3 numbers are %i, %i, 0x%4X\n", num1, num2, num3);
return 0;
}
@Harikrishnan points out you should use sprintf()。这是在正确的轨道上,是一种有效的,但不太通用和完整的方法。创建一个新的variadic function 使用vsnprintf(),就像@Swordfish 和我所做的那样,更好。
@Swordfish 对vsnprintf() 的正确用法做了一个fantastic demonstration,以便创建您自己的printf() 类似variadic function。他的例子(除了缺乏错误处理)是一个完美的模板,用于自定义printf() 类实现,它依赖于动态内存分配。他第一次调用vsnprintf(),使用NULL 目标缓冲区,只是确定他需要为格式化字符串分配多少字节(这是这个应用程序的一个巧妙且常用的技巧),他的第二个调用 vsnprintf() 实际上会创建格式化字符串。对于也具有大量 RAM 的 非 实时应用程序(例如:PC 应用程序),这是完美的方法。但是,对于微控制器,我强烈建议不要这样做,因为:
free() 可能需要不同的(并且事先无法确定的)时间来完成。这是因为堆内存会随着时间的推移而变得碎片化。这意味着这种方法不适用于实时系统。
malloc() 和 free() 的各种堆实现的更多信息,请查看 5 个堆实现,例如,FreeRTOS 在此处描述:https://www.freertos.org/a00111.html。在此页面中搜索“确定性”。malloc()格式化字符串所需的任何内存量。这很糟糕,因为它更容易发生堆栈溢出。在基于微控制器的安全关键系统上,需要严格防止堆栈溢出。一种首选方法是使用静态分配的内存,就像我所做的那样,具有固定的最大大小。 此外,它缺少 GCC 的“格式”属性,这是一个不错的选择(更多内容见下文)。
@P__J__ mentions GCC“格式”属性。我的示例也使用了这个。
如果使用 GCC 编译器或任何其他具有类似功能的编译器,强烈建议将其添加到您制作的任何自定义 printf()-like 函数中。
GCC documentation 在名为 format (archetype, string-index, first-to-check) 的部分下声明:
format 属性指定函数采用 printf、scanf、strftime 或 strfmon 样式参数,这些参数应根据格式字符串进行类型检查。
换句话说,它在编译时为您的自定义printf()-like 函数提供额外的保护和检查。这很好。
对于我们的例子,只需使用printf 作为archetype,并为string-index 和first-to-check 参数使用一个数字。
参数string-index指定哪个参数是格式字符串参数(从1开始),而first-to-check是第一个参数的编号检查格式字符串。
由于非静态 C++ 方法具有隐式 this 参数,因此在为 string-index 和 first-to 赋值时,此类方法的参数应从两个开始计算,而不是一个-检查。
换句话说,以下是应用于printf()-like 函数原型的该属性的一些有效示例用法:
在 C 中:
int lcd_printf(const char * format, ...) __attribute__((format(printf, 1, 2))); // 1 is the format-string index (1-based), and 2 is the variadic argument (`...`) index (1-based)
int lcd_printf(my_type my_var, const char * format, ...) __attribute__((format(printf, 2, 3))); // 2 is the format-string index (1-based), and 3 is the variadic argument (`...`) index (1-based)
int lcd_printf(my_type my_var, my_type my_var2, const char * format, my_type my_var3, ...) __attribute__((format(printf, 3, 5))); // 3 is the format-string index (1-based), and 5 is the variadic argument (`...`) index (1-based)
在 C++ 中:
int lcd_printf(const char * format, ...) __attribute__((format(printf, 2, 3))); // 2 is the format-string index (2-based), and 3 is the variadic argument (`...`) index (2-based)
int lcd_printf(my_type my_var, const char * format, ...) __attribute__((format(printf, 3, 4))); // 3 is the format-string index (2-based), and 4 is the variadic argument (`...`) index (2-based)
int lcd_printf(my_type my_var, my_type my_var2, const char * format, my_type my_var3, ...) __attribute__((format(printf, 4, 6))); // 4 is the format-string index (2-based), and 6 is the variadic argument (`...`) index (2-based)
在此处阅读我的其他答案:How should I properly use __attribute__ ((format (printf, x, y))) inside a class method in C++?。
因此,将以上所有内容放在一起,您将获得理想的微控制器解决方案,正如我在上面介绍的那样。
【讨论】:
因为最常用的 arm 编译器是 gcc,所以我只关注这个。编译器可以像 printf 一样检查格式和参数
__attribute__ ((format (printf...
来自 gcc 文档
格式(原型,字符串索引,首先检查) format 属性指定函数采用 printf、scanf、strftime 或 strfmon 样式参数,这些参数应该是 根据格式字符串进行类型检查。例如声明:
extern int my_printf (void *my_object, const char *my_format, ...) __attribute__ ((format (printf, 2, 3))); causes the compiler to check the arguments in calls to my_printf for consistency with the printf style format string argument我的格式。
The parameter archetype determines how the format string is interpreted, and should be printf, scanf, strftime or strfmon. (You也可以使用 printf、scanf、strftime 或 strfmon。) 参数 string-index 指定哪个参数是格式字符串 参数(从 1 开始),而 first-to-check 是 检查格式字符串的第一个参数。对于函数 无法检查参数(例如 vprintf), 将第三个参数指定为零。在这种情况下,仅编译器 检查格式字符串的一致性。对于 strftime 格式, 第三个参数必须为零。
In the example above, the format string (my_format) is the second argument of the function my_print, and the arguments to check start用第三个参数,所以格式正确的参数 属性是 2 和 3。
The format attribute allows you to identify your own functions which take format strings as arguments, so that GCC can check the调用这些函数以查找错误。编译器总是(除非 -ffreestanding 用于检查标准库函数 printf、fprintf、sprintf、scanf、fscanf、sscanf、strftime 的格式, 请求此类警告时的 vprintf、vfprintf 和 vsprintf (使用-Wformat),所以不需要修改头文件 标准输入法.h。在 C99 模式下,函数 snprintf、vsnprintf、vscanf、 vfscanf 和 vsscanf 也被检查。除了严格符合 C 标准模式下,X/Open 函数 strfmon 也按原样检查 printf_unlocked 和 fprintf_unlocked。请参阅控制 C 的选项 方言。 format_arg(字符串索引) format_arg 属性指定函数采用 printf、scanf、strftime 或 strfmon 样式函数的格式字符串,并且 修改它(例如,将其翻译成另一种语言),所以 结果可以传递给 printf、scanf、strftime 或 strfmon 样式 函数(格式函数的其余参数相同 就像它们对于未修改的字符串一样)。例如, 声明:
extern char * my_dgettext (char *my_domain, const char *my_format) __attribute__ ((format_arg (2))); causes the compiler to check the arguments in calls to a printf, scanf, strftime or strfmon type function, whose format string argument是对 my_dgettext 函数的调用,以与格式保持一致 字符串参数 my_format。如果 format_arg 属性没有 指定,所有编译器都可以在这样的格式调用中告诉 函数将是格式字符串参数不是恒定的; 这会在使用 -Wformat-nonliteral 时生成警告,但是 没有属性就无法检查调用。
【讨论】: