【问题标题】:How to use formatting strings in user-defined functions?如何在用户定义的函数中使用格式化字符串?
【发布时间】:2019-07-19 14:28:17
【问题描述】:

我想编写一个函数,以类似于 printf/sprintf 使用格式化字符串的方式在 LCD 上打印字符。

【问题讨论】:

  • 您想要做什么与使用fprintf() 与打开的文件流有什么不同,以便您可以写入LCD?您想要与使用snprintf() 格式化字符串有何不同,然后您可以使用其他功能将其传送到 LCD? (或者,换句话说,您没有告诉我们您的情况?您将如何将信息发送到 LCD?)

标签: c printf stm32 lcd


【解决方案1】:

您可以使用 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 将具有格式化的字符串

【讨论】:

    【解决方案2】:

    您可以编写一个可变参数函数并将参数传递给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.
    【解决方案3】:

    此答案将所有其他答案中最好的部分合二为一。考虑到所有因素,我认为这是做到这一点的最佳方法,并将在展示示例后进行更详细的解释。

    总结:

    这是一个完整的示例,包括函数中的基本错误检查。在这里,我创建了一个类似于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;
    }
    

    ma​​in.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 应用程序),这是完美的方法。但是,对于微控制器,我强烈建议不要这样做,因为:

    1. 它是不确定的。每次调用free() 可能需要不同的(并且事先无法确定的)时间来完成。这是因为堆内存会随着时间的推移而变得碎片化。这意味着这种方法不适用于实时系统。
      • 有关 malloc()free() 的各种堆实现的更多信息,请查看 5 个堆实现,例如,FreeRTOS 在此处描述:https://www.freertos.org/a00111.html。在此页面中搜索“确定性”。
    2. 它是无限的。它将尝试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-indexfirst-to-check 参数使用一个数字。

    参数string-index指定哪个参数是格式字符串参数(从1开始),而first-to-check是第一个参数的编号检查格式字符串。

    由于非静态 C++ 方法具有隐式 this 参数,因此在为 string-indexfirst-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++?

    因此,将以上所有内容放在一起,您将获得理想的微控制器解决方案,正如我在上面介绍的那样。

    【讨论】:

      【解决方案4】:

      因为最常用的 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
      

      也可以使用 printfscanfstrftimestrfmon。) 参数 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 时生成警告,但是 没有属性就无法检查调用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-07-09
        • 2018-04-30
        • 2013-07-29
        • 1970-01-01
        • 2013-11-13
        • 1970-01-01
        • 1970-01-01
        • 2015-04-22
        相关资源
        最近更新 更多