C默认参数

363

在 C 语言中,有没有一种方法可以指定函数的默认参数?


14
只想要一个稍微好一点的 C,不是 C++。认为 C+ 是从 C++ 中提取了各种小改进,但没有大混乱。而且,请不要使用不同的链接加载器。应该只是另一个类似预处理器的步骤。标准化。无处不在... - ivo Welch
在侧边栏中我没有看到的相关问题 - Shelby Moore III
我想说的是,别再像野蛮人一样了,好好学习使用C++(11,...)- 开个玩笑! /我熄灭了火焰...但是...你会爱上它的...哈哈哈,我忍不住了,抱歉。 - moodboom
23个回答

366

哇,大家在这里都是如此悲观。答案是肯定的。

这并不是一件小事:最后我们将拥有核心函数、支持结构、包装函数和一个围绕着包装函数的宏。在我的工作中,我有一组宏来自动完成所有这些工作;一旦你理解了流程,你也可以轻松地做到同样的事情。

我已经在别处写过这个内容,所以这里提供一个详细的外部链接来补充这里的总结:http://modelingwithdata.org/arch/00000022.htm

我们希望转换成:

double f(int i, double x)

将其放入一个接受默认值 (i=8, x=3.14) 的函数中。定义一个伴随结构体:

typedef struct {
    int i;
    double x;
} f_args;

将您的函数重命名为f_base,定义一个包装函数来设置默认值并调用基本函数:

double var_f(f_args in){
    int i_out = in.i ? in.i : 8;
    double x_out = in.x ? in.x : 3.14;
    return f_base(i_out, x_out);
}

现在使用C的变长宏添加一个宏。这样用户不需要知道他们实际上是在填充f_args结构体,而是认为他们正在进行通常的操作:

#define f(...) var_f((f_args){__VA_ARGS__});

好的,现在以下所有内容都可以正常工作:

f(3, 8);      //i=3, x=8
f(.i=1, 2.3); //i=1, x=2.3
f(2);         //i=2, x=3.14
f(.x=9.2);    //i=8, x=9.2

查看复合初始化器如何设置默认值的规则。

有一件事是行不通的:f(0),因为我们无法区分缺少值和零。根据我的经验,这是需要注意的一点,但可以随需求而调整处理——一半的时间你的默认值确实是零。

我费了很大的力气写这个东西,因为我认为命名参数和默认值确实让在C中编程更容易、更有趣。C非常简单,仍然有足够的功能使所有这些成为可能。


29
+1 创意!它有其限制,但也为命名参数带来了便利。请注意,{}(空初始化器)是C99中的错误。 - u0b34a0f6ae
43
然而,这里有一个很棒的东西:标准允许多次指定命名成员,后面的会覆盖前面的。因此,仅针对命名参数,您可以解决默认问题并允许进行空调用。 #define vrange(...) CALL(range,(param){.from=1, .to=100, .step=1, __VA_ARGS__}) - u0b34a0f6ae
4
我希望编译器错误信息易读,但这是一种很棒的技巧!看起来几乎像Python中的关键字参数。 - totowtwo
6
@RunHolt 虽然使用命名参数更简单,但并不意味着它就是更好的选择;命名参数有其优点,如调用时易读(但源代码可读性较差)。其中一种适合源代码开发人员,另一种适合函数的用户。仅仅认为“这个更好!”可能有些草率。 - Alice
3
C11 6.7.9(19) 关于初始化聚合体的规定:未明确初始化的所有子对象都应该隐式地与具有静态存储期的对象一样进行初始化。正如您所知,具有静态存储期的元素会被初始化为零|NULL|\0。【这在c99中也是如此。】 - bk.
显示剩余9条评论

186

可以。 :-) 但不是你所期望的方式。

int f1(int arg1, double arg2, char* name, char *opt);

int f2(int arg1, double arg2, char* name)
{
  return f1(arg1, arg2, name, "Some option");
}

很遗憾,C语言不支持方法重载,所以你最终会得到两个不同的函数。但是通过调用f2,实际上你会使用默认值调用f1。这是一种“不要重复自己”的解决方案,帮助你避免复制/粘贴现有的代码。


35
就我个人而言,我更喜欢用函数末尾的数字来表示它所需的参数数量。这样比使用任意数字要容易得多。 :) - Macke
8
这是迄今为止最好的答案,因为它展示了一种简单的方法来实现同样的目标。我有一个函数,它是固定 API 的一部分,我不想改变它,但我需要它接受一个新的参数。当然,这是如此显而易见,以至于我忽略了它(被默认参数卡住了思路!) - RunHolt
11
f2也可以是一个预处理宏。 - osvein

169

27
我讨厌在使用可变参数时缺乏检查。 - dmckee --- ex-moderator kitten
21
好的,我会尽力进行翻译。“你也应该这样做;实际上我不建议这样做;我只是想表达这是可能的。” - Eli Courtwright
4
但是,你如何检查调用者是否传递了参数?我认为要使这个方法生效,难道不需要让调用者告诉你他没有传递吗?我认为这使得整个方法变得不太可用——调用者也可以使用另一个名称调用函数。 - Johannes Schaub - litb
3
open(2)系统调用使用此参数作为可选项,具体取决于所需的参数。printf(3)读取指定有多少参数的格式字符串。两者都安全有效地使用可变参数列表,并且尽管你肯定可以出错,但printf()似乎特别受欢迎。 - Chris Lutz
4
并非所有的C编译器都是gcc,gcc能通过一些高级编译技巧,在printf()函数的参数与格式字符串不匹配时发出警告。但我认为对于自定义的可变参数函数(除非它们使用相同风格的格式字符串),无法获得类似的警告。 - tomlogic
显示剩余5条评论

48

我们可以创建只使用命名参数作为默认值的函数。这是bk.答案的延续。

#include <stdio.h>                                                               

struct range { int from; int to; int step; };
#define range(...) range((struct range){.from=1,.to=10,.step=1, __VA_ARGS__})   

/* use parentheses to avoid macro subst */             
void (range)(struct range r) {                                                     
    for (int i = r.from; i <= r.to; i += r.step)                                 
        printf("%d ", i);                                                        
    puts("");                                                                    
}                                                                                

int main() {                                                                     
    range();                                                                    
    range(.from=2, .to=4);                                                      
    range(.step=2);                                                             
}    

C99标准定义后面的初始化名称将覆盖先前的项。我们还可以使用一些标准的位置参数,只需相应地更改宏和函数签名即可。默认值参数只能在命名参数样式中使用。

程序输出:

1 2 3 4 5 6 7 8 9 10 
2 3 4 
1 3 5 7 9

3
这个实现看起来比Wim ten Brink或BK的解决方案更简单直接。这种实现是否有任何其他解决方案没有的缺点? - stephenmm

24

OpenCV 使用类似以下的东西:

/* in the header file */

#ifdef __cplusplus
    /* in case the compiler is a C++ compiler */
    #define DEFAULT_VALUE(value) = value
#else
    /* otherwise, C compiler, do nothing */
    #define DEFAULT_VALUE(value)
#endif

void window_set_size(unsigned int width  DEFAULT_VALUE(640),
                     unsigned int height DEFAULT_VALUE(400));

如果用户不知道该写什么,这个小技巧可能会有所帮助:

使用示例


7
这是一个关于C语言的问题,解决方案没有提供C语言的默认值。将你的C代码编译成C++并不是非常有趣,那么你可能想要使用C++提供的默认值。另外,C语言代码并不总是有效的C++代码,这意味着需要进行移植工作。 - Allan Wind

20

不行。

即使是最新的C99标准也不支持这个。


一个简单的“不”会更好 ;) - KevinDTimm
25
这是不可能的,因为 Stack Overflow 规定回答必须达到最低长度要求。我尝试过了。 :) - unwind
3
请参考我的回答。 :) - chaos

18

不,这是 C++ 语言的一个特性。


16

另一个使用宏的技巧:

#include <stdio.h>

#define func(...) FUNC(__VA_ARGS__, 15, 0)
#define FUNC(a, b, ...) func(a, b)

int (func)(int a, int b)
{
    return a + b;
}

int main(void)
{
    printf("%d\n", func(1));
    printf("%d\n", func(1, 2));
    return 0;
}

如果只传递一个参数,则b将接收默认值(在本例中为15)


在 FUNC(VA_ARGS, 15, 0) 中,最后一个参数“0”是否是必需的?我尝试过不带它,似乎也能正常工作。 - ungalcrys
1
@ungalcrys 如果你不使用gcc编译,那么这是强制性的,gcc允许包含 ... 并且只传递2个参数(在这种情况下)作为扩展,使用 -pedantic 编译并查看会发生什么。 - David Ranieri

16

简短答案:不行。

稍微长一点的回答:有一个古老的、非常古老的解决方法,你可以传递一个字符串,然后对其进行解析以获取可选参数:

int f(int arg1, double arg2, char* name, char *opt);

其中opt可能包括“name=value”对或其它内容,你可以这样调用:

n = f(2,3.0,"foo","plot=yes save=no");

显然,这仅在某些情况下有用。通常当您想要针对一系列功能使用单个接口时。


您仍会在由专业C++程序编写的粒子物理代码中找到这种方法(例如ROOT)。其主要优点是可以无限扩展而同时保持向后兼容性。


2
结合可变参数,你可以玩出各种花样! - David Thornley
5
我会使用自定义的 struct,让调用者创建一个结构体并填充不同选项的字段,然后通过地址传递它,或者传递 NULL 以获取默认选项。 - Chris Lutz
从ROOT复制代码模式是一个可怕的想法! - innisfree

16

也许最好的方法(根据您的情况,可能有可能不可行)是转向C++,并将其用作“更好的C语言”。 您可以在不使用类、模板、运算符重载或其他高级功能的情况下使用C++。

这将为您提供一个具有函数重载和默认参数(以及您选择使用的任何其他功能)的C语言变体。 如果您真的严格使用C++的受限子集,您只需要稍微有些自律。

很多人会说这样使用C++是个糟糕的主意,他们可能有一定的道理。但这仅仅是一种看法;我认为可以使用您熟悉的C++功能而无需完全掌握它的全部。 我认为C++之所以如此成功,其中一个重要原因就是在早期它被许多程序员用于这种方式。


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