如何判断在C语言函数中是否传递了可选参数

3

编辑3:若要查看完整代码,请参阅第一个答案或本帖的末尾。

正如标题所述,我正在尝试找到一种方法来判断函数是否传递了可选参数。我想做的是类似于几乎所有动态语言处理它们的子字符串函数的方式。以下是目前的代码,但它不起作用,因为我不知道何时使用/未使用该参数。

char *substring(char *string,unsigned int start, ...){
    va_list args;
    int unsigned i=0;
    long end=-1;
    long long length=strlen(string);
    va_start(args,start);
    end=va_arg(args,int);
    va_end(args);
    if(end==-1){
        end=length;
    }
    char *to_string=malloc(end);
    strncpy(to_string,string+start,end);
    return to_string;
}

基本上,我希望仍然可以不包括我想要返回的字符串的长度,只需让它到达字符串的末尾。但我似乎找不到一种方法来实现这个功能。由于在 C 中没有办法知道传递的参数数量,所以这让我失去了第一个想法。
编辑: 这里是当前代码的新方法。
#define substring(...) P99_CALL_DEFARG(substring, 3, __VA_ARGS__)
#define substring_defarg_2 (0)
char *substring(char *string,unsigned int start, int end){
    int unsigned i=0;
    int num=0;
    long long length=strlen(string);
    if(end==0){
        end=length;
    }
    char *to_string=malloc(length);
    strncpy(to_string,string+start,end);
    return to_string;
}

然后我在一个名为test.c的文件中调用它,以查看是否有效。

#include "functions.c"
int main(void){
    printf("str:%s",substring("hello world",3,2));
    printf("\nstr2:%s\n",substring("hello world",3));
return 0;
}

functions.c中包含了functions.h的头文件,该头文件包含了所有需要的内容。以下是clang输出(因为clang通常会提供更详细的信息)。

In file included from ./p99/p99.h:1307:
./p99/p99_generic.h:68:16: warning: '__error__' attribute ignored
__attribute__((__error__("Invalid choice in type generic expression")))
               ^
test.c:4:26: error: called object type 'int' is not a function or function
      pointer
    printf("\nstr2:%s\n",substring("hello world",3));
                         ^~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from test.c:1:
In file included from ./functions.c:34:
In file included from ./functions.h:50:
./string.c:77:24: note: instantiated from:
#define substring(...) P99_CALL_DEFARG(substring, 3, __VA_ARGS__)

GCC只是说这个对象不是一个函数。

编辑2:请注意,将其设置为-1也不会改变它,它仍然会抛出相同的错误。我使用的编译选项如下。

gcc -std=c99 -c test.c -o test -lm -Wall

Clang也是同样的情况(它是否与之一起工作是另一个问题)。

回答在此处。

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include "p99/p99.h"
#define substring(...) P99_CALL_DEFARG(substring, 3, __VA_ARGS__)
#define substring_defarg_2() (-1)
char *substring(char *string, size_t start, size_t len) {
  size_t length = strlen(string);
  if(len == SIZE_MAX){
    len = length - start;
  }
  char *to_string = malloc(len + 1);
  memcpy(to_string, string+start, len);
  to_string[len] = '\0';
  return to_string;
}

你需要从这里获取p99。它是由选定的答案提供的。只需将其放入您的源目录中,您就可以使用了。此外,总结他在许可证方面的回答。您可以自由使用它,但基本上不能派生它。因此,为此目的,无论是专有项目还是开源项目,您都可以自由使用它和字符串函数。

我唯一要求的是,您至少要提供一个链接返回到这个线程,以便其他人能够了解stackoverflow,因为这是我对在这里获得帮助的事情进行评论的方式。


6
抱歉,但你不能这样做。你必须向函数传递一些东西来告诉它要查找多少个参数(例如,printf使用格式字符串)。 - Jerry Coffin
@JerryCoffin,抱歉但是如果你能以某种方式做到的话,请看我的回答。预处理器比大多数人意识到的要强大得多,例如,您可以让它根据接收到两个或三个参数来选择两个不同的函数调用。 - Jens Gustedt
@JensGustedt:我看过了——尽管它可能是一个合理的选择,但这并不是他真正要求的,也不会与我在评论中所说的相矛盾。 - Jerry Coffin
@JensGustedt:这不是你展示的内容。你展示的是将可选参数传递给宏,它可以始终将参数传递给函数,或者根据参数数量调用不同的函数。但是,两种方法都不能让函数确定是否传递了可选参数。在第一种情况下,用户可以显式地传递所选择的值以指示未传递任何值,在第二种情况下,任何一个函数都可以直接调用。正如我所说,这可能是一个合理的替代方案,但都不是他实际要求的。 - Jerry Coffin
@Jerry,这就是我说“在某种程度上”的原因。而且在我看来,宏似乎是现代标准预见到这些东西的方式。类型通用的宏,例如,走相同的路线。我认为“函数宏”就像是一个函数接口。 - Jens Gustedt
显示剩余2条评论
4个回答

6
在C语言中,没有可选参数这一说法。在这种情况下,常见的做法是要么使用两个函数:substr(char *, size_t start, size_t end)substr_f(char *, size_t start);要么使用单个函数,其中end如果给定了一个特殊值,将具有特殊含义(例如,在这种情况下,可能小于start的任何数字,或者简单地为0)。
在使用可变参数时,需要在参数列表末尾使用哨兵值(如NULL),或者将argc(参数计数)作为较早的参数传入。
C语言的运行时内省非常低,这是一个特性而不是缺陷。
编辑:另外需要注意的一点是,在C语言中用于字符串长度和偏移量的正确类型是size_t。它是唯一保证足够大以寻址任何字符串中任何字符,并保证大小足够小而不会浪费存储空间的整数类型。
还需要注意的是它是无符号的。

计算函数参数并不真正是运行时内省,因为在调用端它在编译时已知。 - Jens Gustedt
@JensGustedt:忽略那个评论,我误读了你的评论。它通常与运行时内省的各个方面联系在一起,尽管它本身可能并不是内省。无论如何,将这种元数据附加到函数调用中将使其变成消息分派,这是更适合具有运行时而不是简单编译代码的语言的特性。 - Williham Totland
我没有看到你的第一条评论,所以很容易忽略它 :) 请看看我的回答,我为什么认为C语言的这个特性(函数调用在调用方已知参数数量)是一个重要的特性,以及如何利用它来实现OP想要的功能。 - Jens Gustedt
啊...好的,我会把它改成size_t类型,我之前匆忙写这个代码只是为了让它能够工作。说实话,我现在都不记得当时为什么要写这个了...但我知道我想把它放在我的“C库”文件夹里。 - 133794m3r
对于子字符串而言,使用负值可能会很有用,表示相对于字符串末尾的偏移量,因此也许可以使用 ssize_t。当然,这会减少可用的错误检查数量。 - Jonathan Leffler

4

除了普遍的信仰之外,带有可选参数的常见函数可以在C中实现,但是va_arg函数不是这样做的正确工具。它可以通过va_arg宏实现,因为有一些方法可以捕获函数接收的参数数量。整个过程有点繁琐,需要解释和实现,但您可以使用P99进行即时使用。

你需要将你的函数签名更改为:

char *substring(char *string, unsigned int start, int end);

并且为 end 发明一个特殊的代码,如果在调用端省略,则使用 -1。然后使用 P99 可以这样做:

#include "p99.h"

#define substring(...) P99_CALL_DEFARG(substring, 3, __VA_ARGS__)
#define substring_defarg_2() (-1)

当你声明一个宏来“重载”函数时(是的,这是可能的,常见的C库实现经常使用此功能),并提供替换方案以了解函数接收的参数数量(在本例中为3)。对于每个需要具有默认值的参数,您将使用带有_defarg_N后缀的第二种类型的宏,其中N0开始。

这些宏的声明不太美观,但至少告诉了我们正在发生什么,就像va_arg函数的接口一样。收益在调用者(“用户”)方面。在那里,您现在可以做如下操作:

substring("Hello", 2); 
substring("Holla", 2, 2);

按照您的喜好进行设置。

(您需要一个实现C99的编译器来完成所有这些。)


编辑:如果您不想为end实现这个约定而想要拥有两个不同的函数,则可以进一步扩展。您可以实现两个函数:

char *substring2(char *string, unsigned int start);
char *substring3(char *string, unsigned int start, unsigned int end);

然后将宏定义为:
#define substring(...)                \
 P99_IF_LT(P99_NARG(__VA_ARGS__, 3))  \
 (substring2(__VA_ARGS__))            \
 (substring3(__VA_ARGS__))

这将确保预处理器通过查看接收到的参数数量来选择适当的函数调用。


编辑2:这里是一个更适合的substring函数版本:

  • 使用在长度和其他方面语义上正确的类型
  • 第三个参数对您来说似乎是长度,而不是字符串的结尾,请相应地命名它
  • strncpy 函数几乎从不是正确的选择,因为有些情况下它不会写入终止字符'\0'。当您知道字符串的大小时,请使用memcpy

char *substring(char *string, size_t start, size_t len) {
  size_t length = strlen(string);
  if(len == SIZE_MAX){
    len = length - start;
  }
  char *to_string = malloc(len + 1);
  memcpy(to_string, string+start, len);
  to_string[len] = '\0';
  return to_string;
}

嗯,那真是丑陋的一种。 ;) 这也是宏使用的层次,你不再真正写C了。 不过,这是个很棒的技巧。 - Williham Totland
让我们在聊天中继续这个讨论 - Williham Totland
由于它使用了QPL,我不知道我有多少信任这个东西。根据我在许可证中所读到的内容,我不能在QPL之外发布任何使用它的程序。我还必须向所有使用p99的事物发布完整的源代码。我通常会为我发布的项目尝试LGPLv3,但这里说这是不可能的,更别提其他任何许可证了。除非我错读了这个东西。我希望我是因为它尚未获得DFSQ认证,但它读起来像是copyleft,并强制执行他们的许可证。我可能只是在第一次遭遇时没有正确理解许可证。 - 133794m3r
QPL并不是我的个人选择,而是与我的雇主妥协的结果。但它并没有你想象的那么糟糕 :) QPL的理念是没有人可以分叉它,也没有人可以将其集成到他们的源代码中。在你自己的代码上使用任何许可证都没有任何问题。LGPL也是我最喜欢的许可证之一,但P99有一个微妙之处,在LPGL中没有预见到:P99不是传统意义上的库,因为它只是预处理器文件。 - Jens Gustedt
啊好的,很好。谢谢你澄清这个问题。另外无关紧要的一点,我曾经在那里坐了大约5分钟,想知道为什么你的头像是带牛仔帽的Master Chief。当我点击它时,结果却是不同的。而我的问题关于标题,则是因为我以为你只是一个给建议的随机人。我不知道你就是写它的那个人。无论如何,感谢您的回答,下次……我可能会在检查谷歌之前先看文档,因为我认为文档会有更多的技巧。 - 133794m3r
显示剩余11条评论

2
很遗憾,你不能像那样使用va_arg
此外,请注意va_arg无法确定检索到的参数是否是传递给函数的最后一个参数(甚至是否是该列表末尾之后的元素)。 函数应设计为可以通过已读取的命名参数或其他附加参数的值以某种方式推断出参数的数量。
常见的“解决方法”是为另一个“重载”提供一个好记的助记符名称,例如right_substr。 它看起来可能不会那么花哨,但肯定会运行得更快。
如果复制实现是您关心的问题,则可以将left_substr,substring和right_substr实现为对将start和length作为有符号整数的隐藏函数的包装器,并将负数解释为缺少参数。 在公共接口中使用这种“约定”可能不是一个好主意,但在私有实现中可能完全可行。

在字符串处理中应始终使用 size_t,由于它是无符号的,因此 -1 不适合作为特殊值。 - Williham Totland
@WillihamTotland 就公共 API 而言,您是完全正确的:size_t 是正确的选择。我所说的是实现三个 API 并采用“穷人版”可选参数的私有函数:在那里规则更加宽松,因为您可以更改它而不被任何人注意到。 - Sergey Kalinichenko
是的和不是的:使用size_t有一个原因,我在我的回答中概述了它;但长话短说:使用任何其他类型都有明显的风险,在非常微妙的方式下可能会以糟糕的结局告终。 - Williham Totland

1
在标准C中,当使用变参原型(...)时,没有直接的方法可以告诉有多少参数被传递。
在幕后,像printf()等函数会基于格式字符串来假设参数的数量。
其他需要可变数量指针的函数,例如,期望列表以NULL结尾。
考虑使用这些技术之一。

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