C函数中的可选参数

18
在一个C函数中,我想检查输入参数(在我的情况下是'value')是否存在。
例如:
void Console(char string[], int32_t value)
{
    // write string here
    // write value here, if it exists
}

当使用if(value != NULL)语句时,我的Console()函数会发送4096。

我该如何检查并根据参数是否存在采取行动?


你想要检查 1) 参数是否存在或者 2) 参数的值吗? - Jeegar Patel
想要检查参数是否存在。 - Eray CANLI
请参考 http://www.eskimo.com/~scs/cclass/int/sx11b.html 实现类似 printf 的函数。 - Paul R
3个回答

24

在C语言中,通常不允许使用可选参数(但在C++Ocaml等语言中存在)。唯一的例外是可变参数函数(例如printf)。

历史上,POSIX的open(2)函数在某些情况下接受可选的第三个参数(当时定义该函数时-20世纪70年代和80年代- 调用约定 实际上是将参数推入调用堆栈,因此忽略该参数很容易实现)。如果您今天查看Linux上自由软件libc实现中的该open函数的最新实现,例如musl-libc,则可以在其src/fcntl/open.c中看到它使用<stdarg.h>变参机制(通常作为编译器内置实现)。

顺便提一下,您可以定义一些宏来填充“缺失”的参数,这样如果您有

  void console(const char*, int32_t);

你可能也会

  #define console_plain(Msg) console((Msg),0)

这可以在某个头文件中使用内联函数来代替,例如

  static void inline console_plain (const char*msg) 
  { console(msg, 0); }

然后在其他地方使用console_plain("hello here")

然后,您的可变参数函数应该定义允许哪些参数以及如何允许这些参数(在非空序列的固定参数之后)。并使用stdarg(3)获取这些可变参数(实际参数)。

实际参数大多在编译时已知,而不是运行时。因此,您需要一种约定,通常定义从所需的固定参数中允许哪些可变参数。特别是,您无法测试是否存在某个参数(该信息在运行时丢失)。

顺便提一下,使用可变参数函数通常会失去大多数C编译器提供的类型检查(至少在启用所有警告时,例如gcc -Wall -Wextra)。如果使用GCC,您可能会在原型中使用一些function __attribute__-s(例如formatsentinel等)来辅助这一点。您甚至可以使用过时的MELT或2019年的GCC插件自定义gcc,以添加执行自己类型检查的自定义属性。

如何检查并根据参数存在情况采取行动?

使用当前常用的调用约定(例如研究x86-64 ABI),通常情况下您无法这样做(除非使用可变参数函数)。


15
这听起来好像你正在尝试使用技巧,潜在地方便自己或者打开你的源代码"写模式"的人。 我说"写模式"(不是读/写),因为这样的代码很难阅读,因为有所有隐藏的代码,因为你需要神奇的宏。 C语言并不聪明。编译器可能很聪明,但语言语义相当狭隘。你应该严格遵守规则,否则你就冒着制作糟糕的软件甚至更糟的风险——根本无法运行。

在不创建全新的编程语言的情况下,"正确"的方法是提供两个参数,当第二个参数不应被使用时,如果它是指针,则应将其传递为NULL,如果它是数字类型,则应传递一些排除数字,例如-1

另一种方法是创建两个具有信息提示名称的单独函数,例如:consoleconsole_full。分别带有一个和两个参数。

但是,如果您仍然不满足于前面提到的方法,您可以包含stdarg.h来为您完成。

void Console (char *string, ...) /* Note the ... */
{
    va_list param;
    int32_t optValue = (-1); /* -1 would indicate optValue is to be ignored */

    // write string here

    va_start(param, string);

    optValue = va_arg(param, int32_t);

    if(optValue != (-1))
    {
        /* Work with `optValue` */
    }

    va_end(param);
}

哪种方式都不好,因为你不知道附加参数的类型,也不知道有多少个。要了解这些信息,您应该像printf一样的函数那样解析字符串中指示存在参数以及参数类型的特定标记,或者至少只使用一个const变量计数器来记录参数。 您还可以进一步宏定义自动计算参数数量。


更新:

实际上,您不需要使用stdarg来在C语言中使用可变参数功能,因为这是一种内置功能。C预处理器也可以展开可变参数(尽管可能不是所有版本都支持)。库stdarg提供了这些有用的宏,但您也可以自行实现它们。 请注意,几乎总是不使用标准库是错误的。 只需获取第一个(参考)变量的地址,并将其增加指针的大小,就可以得到下一个参数的地址(大概是这样):

#include <stdio.h>

#define INIT_VARARG(x,ref)          void* x = &ref
#define GET_NEXT_VARARG(ptr,type)   (*((type* )(ptr+=sizeof(ptr))))

void func (int ref, ...) // ref must be the number of int args passed.
{
    INIT_VARARG(ptr,ref);
    int i;

    for(i = 0; i < ref; i++)
    {
        printf("[%i]\n", GET_NEXT_VARARG(ptr, int));
    }
}

int main (void)
{
    func(3, 10, 15, 20);

    return 0;
}

您仅可将此用于实验目的。一个良好、安全和设计良好的C代码不应该使用此类宏,而应该使用stdarg。


非常好的回答,而且你对“写”而不是“读/写”的评论让我感到开心,因为将来我可能会借用它。 - Neil
@AndrewHenle 我更喜欢使用 #define MFUNC(x, args...) wrapped_func(x, args)。在我的Cygwin GCC上可以工作,但据我所知,在不同的编译器和版本上可能会出现问题。args将以可变参数的方式扩展为传递的参数。我已经提到应该优先选择和使用符合标准的方法,因此我不明白为什么还需要重复这一点。 - Edenia
但是为了论证,虽然 C 没有明确说明关于函数参数存储在哪里(它们可以存储在堆栈上或者在寄存器中 - 如果它们不比这个要求更宽), 它说一个 C 函数有一个 activation record,根据新版本 gcc 标准,如果你请求参数相邻存储,你可以非常确定地知道它们将会被放在相邻的内存中。但通常这样的事情都是由平台规范辅助实现的。 - Edenia
你在6.3.2.3 N748 - N759中所指的是什么?如果结果指针未正确对齐指向的类型,则行为是未定义的?因为任何与对齐有关的事情都没有“确定”的东西。对齐属性可以被轻微地使用。 - Edenia
我测试了所有的情况,都能正常工作。不,它并不是“有效”的。你混淆了“未观察到故障”和“有效”。你只是还没有观察到它失败而已。你不能像那样安全地操作指针。 - Andrew Henle
显示剩余3条评论

7

如果要区分只接受一个或两个参数的函数调用,可以使用宏。

虽然您可以实现所需的行为,但需要注意以下几点:

  • 宏的实现会隐藏重载,这会让无法看到Console是宏的代码读者感到困惑。C更多地关注细节,因此,如果有两个不同的函数,则它们应该具有不同的名称,例如cons_strcons_str_int

  • 如果你传递的参数超过两个或类型与所需的C字符串和整数不兼容,该宏将引发编译器错误。这是一件好事。

  • 真正的可变参数函数(如使用来自<stdarg.h>的界面的printf)必须能够推导出可变参数的类型和数量。 在printf中,这是通过%格式说明符来完成的。 该宏可以仅根据参数数量切换不同的实现。

无论如何,这是一种实现方式。请谨慎操作。

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

#define NARGS(...) NARGS_(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define NARGS_(_5, _4, _3, _2, _1, N, ...) N

#define CONC(A, B) CONC_(A, B)
#define CONC_(A, B) A##B

#define Console(...) CONC(Console, NARGS(__VA_ARGS__))(__VA_ARGS__)



void Console1(const char string[])
{
    printf("%s\n", string);
}

void Console2(const char string[], int32_t value)
{
    printf("%s: %d\n", string, value);
}

int main()
{
    Console("Hello");
    Console("Today's number is", 712);

    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接