例如:
void Console(char string[], int32_t value)
{
// write string here
// write value here, if it exists
}
当使用if(value != NULL)
语句时,我的Console()函数会发送4096。
我该如何检查并根据参数是否存在采取行动?
void Console(char string[], int32_t value)
{
// write string here
// write value here, if it exists
}
当使用if(value != NULL)
语句时,我的Console()函数会发送4096。
我该如何检查并根据参数是否存在采取行动?
在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(例如format
、sentinel
等)来辅助这一点。您甚至可以使用过时的MELT或2019年的GCC插件自定义gcc
,以添加执行自己类型检查的自定义属性。
如何检查并根据参数存在情况采取行动?
使用当前常用的调用约定(例如研究x86-64 ABI),通常情况下您无法这样做(除非使用可变参数函数)。
在不创建全新的编程语言的情况下,"正确"的方法是提供两个参数,当第二个参数不应被使用时,如果它是指针,则应将其传递为NULL
,如果它是数字类型,则应传递一些排除数字,例如-1
。
另一种方法是创建两个具有信息提示名称的单独函数,例如:console
和console_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。
#define MFUNC(x, args...) wrapped_func(x, args)
。在我的Cygwin GCC上可以工作,但据我所知,在不同的编译器和版本上可能会出现问题。args
将以可变参数的方式扩展为传递的参数。我已经提到应该优先选择和使用符合标准的方法,因此我不明白为什么还需要重复这一点。 - Edenia如果要区分只接受一个或两个参数的函数调用,可以使用宏。
虽然您可以实现所需的行为,但需要注意以下几点:
宏的实现会隐藏重载,这会让无法看到Console
是宏的代码读者感到困惑。C更多地关注细节,因此,如果有两个不同的函数,则它们应该具有不同的名称,例如cons_str
和cons_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;
}