1

我正在尝试编写一个函数,它采用任意数量的参数格式化并将其打印到控制台。我希望函数具有以下签名

void formatted_text(...);

我希望以下列方式调用此函数。

formatted_text("String 1", 1, "String 2", 2.2);

现在的问题是,在函数中我不知道哪个参数属于哪个数据类型。一种解决方案是我可以要求用户将所有参数转换为字符串,然后将其传递给这样的函数

formatted_text("String 1", "1", "String 2", "2.2");

但使用起来不会那么方便。我可以做的另一件事是要求用户为每个参数提供数据类型,这又不是一个很好的解决方案。

有没有办法在不知道其数据类型的情况下将任何数据类型转换为字符串。我知道答案......,但你可能知道一些我不知道的事情。

处理这种需求之王的最佳方法是什么。

4

2 回答 2

2

C 数据类型本身并不是自描述的,就像它们通常在 Java、Python、PHP 等其他语言中一样。您必须提供一些元数据,以便formatted_text(...)知道如何解释您在堆栈上传递的字节。正如评论所提到的,您传递给的格式字符串(即第一个参数)printf是元数据可以采用的一种形式。

于 2013-02-01T01:41:17.987 回答
1

您的问题有两个部分:需要一个带有可变数量参数且方便的打印函数;并且想要一种打印任意用户定义数据类型的好方法。

对于第一部分,您可以编写一个“可变参数”函数,与printf(). 但是你有一个问题:没有办法计算有多少参数,而且你不知道参数的类型。您可以使函数更简单,而不是printf()说每个参数都必须是指向字符串的指针,并且最后一个参数必须始终是NULL指针;然后你可以编写一个简单的循环,直到它到达NULL. 谷歌搜索c varargs以了解更多信息。

如果您这样做,您可以创建一个宏,该宏始终扩展为以 aNULL作为最后一个参数调用您的函数,以减少用户忘记将NULL. 像这样的东西:

#define CONVENIENT_PRINT(...) \
    my_convenient_print_function(__VA_ARGS__, NULL)

当然,有趣的部分是一些编译器在上述情况下工作得很好,但有些编译器不能。编写具有可变数量参数的宏不像 C 的某些其他部分那样可移植。但上述形式在 Microsoft C 和 GCC 和 Clang 中有效,因此您可能对它没问题。

对于第二部分,C 并没有给你太多帮助。我可以为这个问题提出两种可能的解决方案。一是要有严格的命名约定,方便用户判断调用什么函数;对于任何数据类型FOO,总会有一个函数StringFromFoo(),用户必须学会调用正确的函数。第二个是实现自己的面向对象类型,并确保每个对象都有一个指向方法函数ToString()或其他调用的指针。像这样的东西:

typedef struct
{
    char const *(*ToString)(void const *this);
} FOO_CLASS;  // class struct
static FOO_CLASS _foo_class;

char const *FooToString(void const *this)
{
    return NULL; // yeah I'm not really implementing this here
}
_foo_class.ToString = FooToString;

typedef struct
{
    // member variables go here
    int x;
    float f;
    // need a reference back to the class; just one
    FOO_CLASS *pclass;
} FOO;  // instance struct

int InitFoo(int x, float f, FOO **ppfooNew)
{
    FOO *pfoo = malloc(sizeof(FOO));
    if (!pfoo)
    {
        *ppfooNew = NULL;
        return ENOMEM;
    }
    pfoo->x = x;
    pfoo->f = f;
    pfoo->pclass = &_foo_class;

    *ppfooNew = pfoo;
    return 0; // success!
}
// in your code:
FOO *pfoo;
char const *str;

if (InitFoo(x, f, &pfoo))
    goto crash_and_burn;

str = pfoo->pclass->ToString(pfoo);

哇,这比面向对象的语言更烦人!那是面向对象的代码,但 C 对你一点帮助都没有;你必须自己做所有的工作。

注意我们如何使用pfoo查找类,然后在类中找到方法函数,然后我们仍然必须pfoo作为方法函数的参数(this指针)传入。C++ 为您完成了这一切。

至少您可以为调用方法函数的常见工作制作一个宏:

#define STR(instance) \
    (instance)->pclass->ToString(instance)

str = STR(pfoo);

对于这个简单的示例,我们在类中只有一个函数。对于一个真实的例子,你可能会有更多的功能!对于单个函数,您可以将其放入实例中,它的工作量会减少,但这不会扩展,如果您完全这样做,您应该始终如一地这样做。

所以,我认为大多数人会采用命名约定解决方案,并称其为足够好。我以部分面向对象的风格编写了一个大型项目,我认为它有助于使代码更易于管理。即使在面向对象的风格中,我仍然有很多命名约定。

PS你可能已经注意到上面的代码实际上并没有实现ToString(),实现它实际上会有点烦人。字符串会去哪里?如果您的需求非常简单,您可以在ToString()函数内部使用静态缓冲区,但这不是一个好的解决方案……否则您需要更改ToString()以传入缓冲区,或者调用它malloc()以获取新的字符串缓冲区;然后处理可能的失败malloc();然后在处理完字符串后以某种方式调用free()(或者只是可耻地泄漏内存!)。这是 C++ 将为您做的更多事情……您可以创建 a std::string,将其传递,当它超出范围时,将调用析构函数。

于 2013-02-01T02:41:43.357 回答