将 void 指针传递给 printf,其类型由格式字符串确定



我将简化我的情况,以便专注于实际问题。

假设我正在为printf编写一个名为print_data的掩护函数。用户调用print_data并传入单一格式字符串,例如"%.1f"以及表示数据的void *

void print_data(const char *format, void *data);

我的工作是接受这些论点并以某种方式将它们传递给printf

我的问题是 printf 需要一个而不是一个指针(字符串除外)。我无法确定用户传入的数据类型,只能自己手动读取格式字符串并相应地转换数据(例如,如果传递了"f",则转换为float)。

一个"神奇"的解决方案是以某种方式取消引用void*的能力,但这当然是不可能的。

不幸的是,我无法重组设计,因为问题不是这么简单,需要我收到void*和格式字符串。

我的问题几乎与给定指针和格式字符串的 printf 相同。浮点问题,除了看起来解决方案没有解决。

关于我如何实现这一目标的任何想法?

您必须分析函数中的格式字符串并使用适当的值类型调用printf。若要读取该值,可以将void指针强制转换为由转换说明符确定的相应类型。

下面是一个快速示例:

#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#define printf printf__
int printf(const char *, ...);
int print_data(const char *format, void *data) {
const char *p = format;
enum {
FMT_none = 0,
FMT_c   = 1,
FMT_i   = 2,
FMT_u   = 3,
FMT_f   = 4,
FMT_pc  = 5,
FMT_pv  = 6,
PREF_l  = (1 << 3),
PREF_ll = (1 << 4),
PREF_h  = (1 << 5),
PREF_hh = (1 << 6),
PREF_j  = (1 << 7),
PREF_z  = (1 << 8),
PREF_t  = (1 << 9),
PREF_L  = (1 << 10),
};
int fmt = FMT_none;

for (;;) {
int cur_fmt = FMT_none;
int prefix = 0;
p = strchr(p, '%');
if (!p)
break;
p++;  // skip the '%'
// skip the flag characters, width and precision
// note that invalid combinations will not be detected
// such as %..d or %.+d
p += strspn(p, " -#+0123456789.");
// parse the length modifier if present
switch (*p) {
case 'l':
p++;
prefix = PREF_l;
if (*p == 'l') {
p++;
prefix = PREF_ll;
}
break;
case 'h':
p++;
prefix = PREF_h;
if (*p == 'h') {
p++;
prefix = PREF_hh;
}
break;
case 'j':
p++;
prefix = PREF_j;
break;
case 'z':
p++;
prefix = PREF_z;
break;
case 't':
p++;
prefix = PREF_t;
break;
case 'L':
p++;
prefix = PREF_L;
break;
}
switch (*p++) {
case '%':
if (p[-2] != '%')
return -1;
continue;
case 'c':
cur_fmt = FMT_c;
break;
case 'd':
case 'i':
cur_fmt = FMT_i;
break;
case 'o':
case 'u':
case 'x': case 'X':
cur_fmt = FMT_u;
break;
case 'a': case 'A':
case 'e': case 'E':
case 'f': case 'F':
case 'g': case 'G':
cur_fmt = FMT_f;
break;
case 's':
cur_fmt = FMT_pc;
break;
case 'p':
cur_fmt = FMT_pv;
break;
default:
return -1;
}
if (fmt != FMT_none)
return -1; // more than one format
fmt = cur_fmt | prefix;
}
switch (fmt) {
case FMT_none:
return printf(format);
case FMT_c:
return printf(format, *(char *)data);
case FMT_c | PREF_l:
// the (wint_t) cast is redundant, omitted
return printf(format, *(wchar_t *)data);
case FMT_i:
return printf(format, *(int *)data);
case FMT_i | PREF_l:
return printf(format, *(long *)data);
case FMT_i | PREF_ll:
return printf(format, *(long long *)data);
case FMT_i | PREF_h:
return printf(format, *(short *)data);
case FMT_i | PREF_hh:
return printf(format, *(signed char *)data);
case FMT_i | PREF_j:
return printf(format, *(intmax_t *)data);
case FMT_i | PREF_z:
case FMT_u | PREF_z:
return printf(format, *(size_t *)data);
case FMT_i | PREF_t:
case FMT_u | PREF_t:
return printf(format, *(ptrdiff_t *)data);
case FMT_u:
return printf(format, *(unsigned *)data);
case FMT_u | PREF_l:
return printf(format, *(unsigned long *)data);
case FMT_u | PREF_ll:
return printf(format, *(unsigned long long *)data);
case FMT_u | PREF_h:
return printf(format, *(unsigned short *)data);
case FMT_u | PREF_hh:
return printf(format, *(unsigned char *)data);
case FMT_u | PREF_j:
return printf(format, *(uintmax_t *)data);
case FMT_f:
// the cast (double) is redundant, but useful to prevent warnings
return printf(format, (double)*(float *)data);
case FMT_f | PREF_l:
return printf(format, *(double *)data);
case FMT_f | PREF_L:
return printf(format, *(long double *)data);
case FMT_pc:
return printf(format, *(char **)data);
case FMT_pc | PREF_l:
return printf(format, *(wchar_t **)data);
case FMT_pv:
return printf(format, *(void **)data);
default:
return -1;
}
}

笔记:

  • 浮点格式的行为类似于scanf():如果data指向float则使用%f,如果指向double,则使用%lfl将被printf忽略,因为float值在传递给 vararg 函数时将转换为double

  • 此函数需要一个指向%c格式的char的指针,尽管printf需要一个将转换为unsigned charint

  • 此函数需要指向%lc格式的wchar_t的指针,尽管printf需要wint_t

  • 转换说明符%zdC 标准允许和%tu,但标准未定义相应的类型。对于负值,传递带有其他符号的类型并不严格正确,但不太可能造成问题。

关于我如何实现这一目标的任何想法?

解析格式 - 鉴于该格式不仅具有说明"acdefginopsuxAEFGX%"而且修饰"hlhhlljztL",因此有很多可能性。 这很快就会产生数十种甚至 100+ 种有效的原始组合。

存在简化:说明符"aefgAEFG"都是double或更宽的。"uoxX"unsigned或更宽。 我认为有效说明符/修饰符组合的数量可以归结为 (2(fp) +8(i) +8(u) +2(c) +2(s) +1(1))。

这里可能需要禁止说明符"%n"- 没有多大意义。

格式解析示例:如何检查两个格式字符串是否兼容?

不清楚 OP 想要用宽度精度做什么,就像在需要 1 个以上参数的"%*.*Lf"一样。


通过传递格式为printf 编写封面函数的替代方法,void *将不带格式进行打印,只需将对象转换为宏并使用_Generic来引导打印函数。 在格式化打印中进行了探索,无需使用 _Generic 指定类型匹配说明符。

正如链接问题的答案所指出的那样,您将不得不编写所有不同的演员表。在你的例子中,这是可行的,因为只有一个void*,所以强制转换的集合是有限的。

我最终使用了 https://github.com/mpaland/printf 提供的无依赖的printf库。

下载后,基本上就像更改接受"int"等类型的va_arg的所有实例一样简单,改为接受该类型的指针,然后取消引用该值。

例如

... = (int)va_arg(va, int)

已更改为

... = (int)*va_arg(va, int*)

我唯一需要更改的其他代码是关于双精度和浮点数,我必须专门检查是否传递了"lf"或"f"。幸运的是,因为库写得很好而且很容易理解,我注意到在"lf"的情况下已经为我设置了一个标志(即FLAGS_LONG)。

然后,我只需要检查该标志是否已设置,如果是,我将值解释为双精度,否则我将其解释为浮点数。

希望这有助于任何尝试实现类似内容的地方。

最新更新