C语言实现类似C#的格式化输出

C语言实现类似C#的格式化输出

在C#中,格式化输出可以使用索引占位符以及复合格式的占位符,可以不用关心后面参数是什么类型,使用起来非常方便,如下简单的示例:

Console.WriteLine("{2} {1} {0} {{{2}}}", "Hello, World!", 1, 8.8);

会输出:

8.8 1 Hello, World! {8.8}

规范可以参见:复合格式设置

在C语言标准库中使用的printf系列函数中,需要使用格式化字符串明确指定后面参数的类型,如果指定错误,可能会引发灾难! 那C语言,可以实现上述功能吗 ? 在C11泛型的加持下,使用关键字_Generic也可以实现上述功能了,下面笔者就一步步来实现,并且支持可以不指定索引,支持C标准格式化字符串。

一、实现效果

我们假定要实现的是printxprint,其使用的方式如下:

main.c:

#include "printx.h"

int main() {
  char a = 'A';
  unsigned char b = 130;
  int age = 25;
  unsigned int ui = 123456;
  long l = 12345678;
  unsigned ul = 123456789;
  long long ll = 98761234567890;
  unsigned long long ull = 9998761234567890;
  float f = 2.71828F;
  double pi = 3.14159;
  const char* name = "Witton";
  unsigned int hexv = 8192;
  auto x = '\n';

  printx("test\n");
  printx("Name: {{{}}}, Age: {}\n", name, age);
  printx("Age: {1}, Name: {}\n", name, age);
  printx("Name: {0}, Age: {1}\n", name, age);
  printx("Pi: {0:.2f}\n", pi);
  printx("Hex: {0:#x}, Again: {0:#08x}\n", hexv);
  printx("Swap order: {1}, {0}\n", "first", "second");
  printx("Char: {0}\n", (char)'A');
  printx("Repeat: {0} {0} {1:.1f} {}\n", age, pi, f);

  printx("1:{} 2:{} 3:{} 4:{} 5:{}, 6:{}, 7:{}, 8:{}, 9:{}, 10:{}\n", name, a,
         b, age, ui, l, ul, ll, ull, f);
  print(name, a, b, age, ui, l, ul, ll, ull, (char)'\n');
  return 0;
}

其运行结果为:

test
Name: {Witton}, Age: 25
Age: 25, Name: Witton  
Name: Witton, Age: 25  
Pi: 3.14
Hex: 0x2000, Again: 0x002000
Swap order: second, first
Char: A
Repeat: 25 25 3.1 25
1:Witton 2:A 3:130 4:25 5:123456, 6:12345678, 7:123456789, 8:98761234567890, 9:9998761234567890, 10:2.718280
Witton A 130 25 123456 12345678 123456789 98761234567890 9998761234567890

在这里插入图片描述

需要注意的是:C语言中字面字符的类型是int,而不是char,所以:

printx("Char: {0}\n", (char)'A');

中需要强制转换成char类型才能正常输出字母A。

在这里插入图片描述

二、统一类型

由于在调用的过程中传入的参数可能是各种数据类型,我们需要统一转换成一种自定义类型,在自定义类型中去标识实际的数据类型。

typedef enum {
  T_UNKNOWN,
  T_CHAR,
  T_BYTE,
  T_STRING,
  T_BYTES,
  T_INT,
  T_UINT,
  T_LONG,
  T_ULONG,
  T_LONGLONG,
  T_ULONGLONG,
  T_FLOAT,
  T_DOUBLE,
} EDataType;

typedef struct {
  EDataType type;
  union {
    char c;
    unsigned char b;
    int i;
    unsigned int u;
    long l;
    unsigned long ul;
    long long ll;
    unsigned long long ull;
    float f;
    double d;
    const char* s;
    const unsigned char* bs;
  } v;
} FmtArg;

static inline FmtArg make_char(char v) {
  return (FmtArg){T_CHAR, {.c = v}};
}

static inline FmtArg make_uchar(unsigned char v) {
  return (FmtArg){T_BYTE, {.b = v}};
}

static inline FmtArg make_string(const char* s) {
  return (FmtArg){T_STRING, {.s = s}};
}

static inline FmtArg make_bytes(const unsigned char* s) {
  return (FmtArg){T_BYTES, {.bs = s}};
}

static inline FmtArg make_int(int v) {
  return (FmtArg){T_INT, {.i = v}};
}
static inline FmtArg make_uint(unsigned int v) {
  return (FmtArg){T_UINT, {.u = v}};
}

static inline FmtArg make_long(long v) {
  return (FmtArg){T_LONG, {.l = v}};
}

static inline FmtArg make_ulong(unsigned long v) {
  return (FmtArg){T_ULONG, {.ul = v}};
}

static inline FmtArg make_longlong(long long v) {
  return (FmtArg){T_LONGLONG, {.ll = v}};
}

static inline FmtArg make_ulonglong(unsigned long long v) {
  return (FmtArg){T_ULONGLONG, {.ull = v}};
}

static inline FmtArg make_float(float v) {
  return (FmtArg){T_FLOAT, {.f = v}};
}

static inline FmtArg make_double(double v) {
  return (FmtArg){T_DOUBLE, {.d = v}};
}

static inline FmtArg make_unknown(void) {
  return (FmtArg){T_UNKNOWN};
}

有了自定义类型了,就可以声明具体实现的C函数了:

void printx_impl(const char* fmt, int arg_count, FmtArg* argv);

前面的fmt就是类似C#的格式符,也可以是nullptr,表明不需要格式字符串,自动依次使用参数;arg_count表明有几个参数,决定着后面参数argv的个数;argv为实际的参数信息。

而给用户调用的API,printxprint其实是一个宏,它类似如下声明:

#define printx(fmt, ...)
#define print(...)

我们需要在宏中调用实际工作的C函数printx_impl,在调用前需要将传入的参数转换成自定义数据类型FmtArg,并且准备好printx_impl函数需要的参数。

三、计算参数个数

如何计算宏参数...中包含的参数个数?
在C/C++中参数... 可能包含0个到多个参数,我们假定最多支持10个参数。为了计算宏参数个数,需要定义一个匹配或者说是取宏参数的宏:

#define GET_MACRO(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, n, ...) n

GET_MACRO的参数列表是:_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, n, ...,这里的...表示剩余参数,但在宏定义中,我们只取n

然后定义计算宏参数个数的宏:

#define COUNT_ARGS(...) \
  GET_MACRO(0, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

__VA_ARGS__中可能是0个参数,C/C++编译器有一种写法:##__VA_ARGS__,当是0个时,会把前面的逗号去掉,变为:

GET_MACRO(0, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

GET_MACRO第12个参数,结果为0

如果有多个参数,则依次填充。比如,参数1,2,3,变为:

GET_MACRO(0, 1, 2, 3, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

GET_MACRO第12个参数,结果为3

四、转换参数

通过前面的方法,我们知道了参数个数,接下来就是实现参数类型的转换了,要实现类型转换就需要知道是什么类型,C11提供了_Generic关键字,只有它才能识别类型,实现泛型。其语法参见:https://cppreference.cn/w/c/language/generic

定义如下宏来将参数转换成自定义类型FmtArg

#define MAKE_FMTARG(x)                    \
  _Generic((x),                           \
      char: make_char,                    \
      unsigned char: make_uchar,          \
      const char*: make_string,           \
      char*: make_string,                 \
      const unsigned char*: make_bytes,   \
      unsigned char*: make_bytes,         \
      int: make_int,                      \
      unsigned int: make_uint,            \
      long: make_long,                    \
      unsigned long: make_ulong,          \
      long long: make_longlong,           \
      unsigned long long: make_ulonglong, \
      float: make_float,                  \
      double: make_double,                \
      default: make_unknown)(x)

使用获取参数个数一样的方法,我们也可以逐个取参数,然后调用MAKE_FMTARG进行转换,先定义一组宏:

#define APPLY0(m, a) 0 // 这里定义为0,避免编译器警告
#define APPLY1(m, a) m(a)
#define APPLY2(m, a, ...) m(a), APPLY1(m, __VA_ARGS__)
#define APPLY3(m, a, ...) m(a), APPLY2(m, __VA_ARGS__)
#define APPLY4(m, a, ...) m(a), APPLY3(m, __VA_ARGS__)
#define APPLY5(m, a, ...) m(a), APPLY4(m, __VA_ARGS__)
#define APPLY6(m, a, ...) m(a), APPLY5(m, __VA_ARGS__)
#define APPLY7(m, a, ...) m(a), APPLY6(m, __VA_ARGS__)
#define APPLY8(m, a, ...) m(a), APPLY7(m, __VA_ARGS__)
#define APPLY9(m, a, ...) m(a), APPLY8(m, __VA_ARGS__)
#define APPLY10(m, a, ...) m(a), APPLY9(m, __VA_ARGS__)

#define APPLY(m, ...)                                                          \
  GET_MACRO(0, ##__VA_ARGS__, APPLY10, APPLY9, APPLY8, APPLY7, APPLY6, APPLY5, \
            APPLY4, APPLY3, APPLY2, APPLY1, APPLY0)(m, __VA_ARGS__)

这组宏支持0~10个参数,对每个参数调用mm可以是宏,也可以是函数。我们这里需要调用的是MAKE_FMTARG宏。

五、实现printxprint

现在可以写出printxprint宏的实现了:

#define printx(fmt, ...)                            \
  printx_impl(fmt, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

#define print(...)                                \
  printx_impl(0, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

(FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)}构建了一个FmtArg的数组,如果参数个数为0,则是(FmtArg[]){0}

六、扩展实现fprintxfprint

目前的API还只支持输出到标准输出设备stdout,无法输出到文件,比如想输出到日志文件,只需要添加一个FILE* fp参数即可,相应修改如下:

void printx_impl(FILE* fp, const char* fmt, int arg_count, FmtArg* argv);

#define printx(fmt, ...)                            \
  printx_impl(stdout, fmt, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

#define print(...)                                \
  printx_impl(stdout, 0, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

实现fprintxfprint宏:

#define fprintx(fp, fmt, ...)                   \
  printx_impl(fp, fmt, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

#define fprint(fp, ...)                       \
  printx_impl(fp, 0, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

七、源码

printx.h:

// copyright(C), author: Witton
// email: witton@163.com

#ifndef _PRINT_X_H_INCLUDE_
#define _PRINT_X_H_INCLUDE_

#include <stdio.h>

typedef enum {
  T_UNKNOWN,
  T_CHAR,
  T_BYTE,
  T_STRING,
  T_BYTES,
  T_INT,
  T_UINT,
  T_LONG,
  T_ULONG,
  T_LONGLONG,
  T_ULONGLONG,
  T_FLOAT,
  T_DOUBLE,
} EDataType;

typedef struct {
  EDataType type;
  union {
    char c;
    unsigned char b;
    int i;
    unsigned int u;
    long l;
    unsigned long ul;
    long long ll;
    unsigned long long ull;
    float f;
    double d;
    const char* s;
    const unsigned char* bs;
  } v;
} FmtArg;

static inline FmtArg make_char(char v) {
  return (FmtArg){T_CHAR, {.c = v}};
}

static inline FmtArg make_byte(unsigned char v) {
  return (FmtArg){T_BYTE, {.b = v}};
}

static inline FmtArg make_string(const char* s) {
  return (FmtArg){T_STRING, {.s = s}};
}

static inline FmtArg make_bytes(const unsigned char* s) {
  return (FmtArg){T_BYTES, {.bs = s}};
}

static inline FmtArg make_int(int v) {
  return (FmtArg){T_INT, {.i = v}};
}
static inline FmtArg make_uint(unsigned int v) {
  return (FmtArg){T_UINT, {.u = v}};
}

static inline FmtArg make_long(long v) {
  return (FmtArg){T_LONG, {.l = v}};
}

static inline FmtArg make_ulong(unsigned long v) {
  return (FmtArg){T_ULONG, {.ul = v}};
}

static inline FmtArg make_longlong(long long v) {
  return (FmtArg){T_LONGLONG, {.ll = v}};
}

static inline FmtArg make_ulonglong(unsigned long long v) {
  return (FmtArg){T_ULONGLONG, {.ull = v}};
}

static inline FmtArg make_float(float v) {
  return (FmtArg){T_FLOAT, {.f = v}};
}

static inline FmtArg make_double(double v) {
  return (FmtArg){T_DOUBLE, {.d = v}};
}

static inline FmtArg make_unknown(void) {
  return (FmtArg){T_UNKNOWN};
}

#define MAKE_FMTARG(x)                    \
  _Generic((x),                           \
      char: make_char,                    \
      unsigned char: make_byte,          \
      const char*: make_string,           \
      char*: make_string,                 \
      const unsigned char*: make_bytes,   \
      unsigned char*: make_bytes,         \
      int: make_int,                      \
      unsigned int: make_uint,            \
      long: make_long,                    \
      unsigned long: make_ulong,          \
      long long: make_longlong,           \
      unsigned long long: make_ulonglong, \
      float: make_float,                  \
      double: make_double,                \
      default: make_unknown)(x)

#define GET_MACRO(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, n, ...) n

#define COUNT_ARGS(...) \
  GET_MACRO(0, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

#define APPLY0(m, a) 0  // 这里定义为0,避免编译器警告
#define APPLY1(m, a) m(a)
#define APPLY2(m, a, ...) m(a), APPLY1(m, __VA_ARGS__)
#define APPLY3(m, a, ...) m(a), APPLY2(m, __VA_ARGS__)
#define APPLY4(m, a, ...) m(a), APPLY3(m, __VA_ARGS__)
#define APPLY5(m, a, ...) m(a), APPLY4(m, __VA_ARGS__)
#define APPLY6(m, a, ...) m(a), APPLY5(m, __VA_ARGS__)
#define APPLY7(m, a, ...) m(a), APPLY6(m, __VA_ARGS__)
#define APPLY8(m, a, ...) m(a), APPLY7(m, __VA_ARGS__)
#define APPLY9(m, a, ...) m(a), APPLY8(m, __VA_ARGS__)
#define APPLY10(m, a, ...) m(a), APPLY9(m, __VA_ARGS__)

#define APPLY(m, ...)                                                          \
  GET_MACRO(0, ##__VA_ARGS__, APPLY10, APPLY9, APPLY8, APPLY7, APPLY6, APPLY5, \
            APPLY4, APPLY3, APPLY2, APPLY1, APPLY0)(m, __VA_ARGS__)

#define printx(fmt, ...)                            \
  printx_impl(stdout, fmt, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

#define print(...)                                \
  printx_impl(stdout, 0, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

#define fprintx(fp, fmt, ...)                   \
  printx_impl(fp, fmt, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

#define fprint(fp, ...)                       \
  printx_impl(fp, 0, COUNT_ARGS(__VA_ARGS__), \
              (FmtArg[]){APPLY(MAKE_FMTARG, ##__VA_ARGS__)})

void printx_impl(FILE* fp, const char* fmt, int arg_count, FmtArg* argv);

#endif

printx.c:

// copyright(C), author: Witton
// email: witton@163.com

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "printx.h"

#if __STDC_VERSION__ >= 202311L
#ifdef NULL
#undef NULL
#define NULL nullptr
#endif
#endif

static void print_one_arg(FILE* fp, const char* fmt_spec, FmtArg arg) {
  if (!fmt_spec[0]) {
    switch (arg.type) {
      case T_CHAR:
        (void)fprintf(fp,"%c", arg.v.c);
        break;
      case T_BYTE:
        (void)fprintf(fp,"%hhu", arg.v.b);
        break;
      case T_INT:
        (void)fprintf(fp,"%d", arg.v.i);
        break;
      case T_UINT:
        (void)fprintf(fp,"%u", arg.v.u);
        break;
      case T_LONG:
        (void)fprintf(fp,"%ld", arg.v.l);
        break;
      case T_LONGLONG:
        (void)fprintf(fp,"%lld", arg.v.ll);
        break;
      case T_ULONGLONG:
        (void)fprintf(fp,"%llu", arg.v.ull);
        break;
      case T_FLOAT:
        (void)fprintf(fp,"%f", arg.v.f);
        break;
      case T_DOUBLE:
        (void)fprintf(fp,"%lf", arg.v.d);
        break;
      case T_STRING:
        (void)fprintf(fp,"%s", arg.v.s);
        break;
      default:
        (void)fprintf(fp,"<?(by witton)>");
        break;
    }
    return;
  }

  char real_fmt[64];
  (void)snprintf(real_fmt, sizeof(real_fmt), "%%%s", fmt_spec);
  switch (arg.type) {
    case T_CHAR:
      (void)fprintf(fp,real_fmt, arg.v.c);
      break;
    case T_BYTE:
      (void)fprintf(fp,real_fmt, arg.v.b);
      break;
    case T_INT:
      (void)fprintf(fp,real_fmt, arg.v.i);
      break;
    case T_UINT:
      (void)fprintf(fp,real_fmt, arg.v.u);
      break;
    case T_LONG:
      (void)fprintf(fp,real_fmt, arg.v.l);
      break;
    case T_LONGLONG:
      (void)fprintf(fp,real_fmt, arg.v.ll);
      break;
    case T_ULONGLONG:
      (void)fprintf(fp,real_fmt, arg.v.ull);
      break;
    case T_FLOAT:
      (void)fprintf(fp,real_fmt, arg.v.f);
      break;
    case T_DOUBLE:
      (void)fprintf(fp,real_fmt, arg.v.d);
      break;
    case T_STRING:
      (void)fprintf(fp,real_fmt, arg.v.s);
      break;
    default:
      (void)fprintf(fp,"<?(by witton)>");
      break;
  }
}

static inline void handle_printx_fmt(FILE* fp, const char* p,
                                     const char* end,
                                     int arg_count,
                                     FmtArg* argv) {
  char index_str[16] = {0};
  char spec[32] = {0};
  int idx = 0;

  const char* colon = memchr(p + 1, ':', end - (p + 1));
  if (colon) {
    size_t len_idx = colon - (p + 1);
    strncpy(index_str, p + 1, len_idx);
    strncpy(spec, colon + 1, end - (colon + 1));
  } else {
    strncpy(index_str, p + 1, end - (p + 1));
  }

  if (isdigit((unsigned char)index_str[0])) {
    idx = strtol(index_str, NULL, 10);
    if (idx >= 0 && idx < arg_count) {
      print_one_arg(fp, spec, argv[idx]);
    } else {
      (void)fprintf(fp, "<BAD_INDEX(by witton)>");
    }
  } else {
    (void)fprintf(fp, "<BAD_FORMAT(by witton)>");
  }
}

void printx_impl(FILE* fp, const char* fmt, int arg_count, FmtArg* argv) {
  if (NULL == fmt) {
    for (int i = 0; i < arg_count; ++i) {
      print_one_arg(fp, "", argv[i]);
      (void)putc(' ', fp);
    }
    return;
  }

  int used_args = 0;
  const char* p = fmt;
  while (*p) {
    if (p[0] == '{' && p[1] == '}' && used_args < arg_count) {
      print_one_arg(fp, "", argv[used_args++]);
      p += 2;  // 跳过 '}'
    } else if (*p == '{' && *(p + 1) != '{') {
      const char* end = strchr(p, '}');
      if (!end) {
        (void)putc(*p++, fp);
        continue;
      }
      handle_printx_fmt(fp, p, end, arg_count, argv);
      p = end + 1;
    } else if (*p == '{' && *(p + 1) == '{') {
      (void)putc('{', fp);
      p += 2;
    } else if (*p == '}' && *(p + 1) == '}') {
      (void)putc('}', fp);
      p += 2;
    } else {
      (void)putc(*p++, fp);
    }
  }
}

至此,我们可以像C#一样写格式化输出代码了,可以不担心格式符写错了。但是如果了使用自定义格式符,即类似{1:.1f}中有冒号后面标准C格式符,则依旧需要小心格式符是否写正确!

如果本文对你有帮助,欢迎点赞收藏!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值