Linux c函数open是如何实现变参的?

在学习Linux c编程中遇到了open函数 int open(const char *pathname, int flag, ... )
open函数根据是否传入第三个参数进行不同的操作,
现在我想自己编写一个函数 int va_fun(const char *str, int n, ... ),如果只有两个参数传入(va_fun(str,n)),则返回1,如果有第三个参数传入(va_fun(str,n,int_the_third)),则返回2,但我却不知道如何实现,希望会的好心人能给点提示。

我曾经尝试了如下代码:

#include<stdio.h>
#include<stdarg.h>

char *va_fun(const char *s,int n,...)
{
    va_list ap;
    int a;
    va_start(ap,n);
    if(a=va_arg(ap,int))
    {
        return "do it";
    }
    else
    {
        return "==";
    }
    va_end(ap);
}
void IntegerVarArgFunc(int i, ...){
    va_list pArgs;
    va_start(pArgs, i);
    int j = va_arg(pArgs, int);
    va_end(pArgs);
    printf("i=%d, j=%d\n", i, j);
}
int main()
{
    puts(va_fun("a",1));
    puts(va_fun("a",1,2));
    IntegerVarArgFunc(1);
    IntegerVarArgFunc(1,2);
    IntegerVarArgFunc(1,2,3);
    return 0;
}

但是这样做却不管用,请问正确的做法如何?

(以上代码的输出是:
do it
do it
i=1, j=-157228448 //j是随机值
i=1, j=2
i=1, j=2)

阅读 3.9k
1 个回答

这个就是C语言函数可变参数的实现。
man va_arg 可得详细的帮助信息。

看起来你还没有搞清楚c语言的可变参数的机制,那我就多说几句吧。
要搞清楚这一点,需要了解一些预备知识点:

C语言函数调用机制

程序进程运行时的内存分为5种:

  • 代码段:存储代码的机器指令
  • 数据段:存储程序里手工初始化的全局变量、静态变量(编译时就已确定了长度和内容)
  • BSS段:存储程序里未手工初始化的全局变量、静态变量
  • 堆:存储malloc/free、new/delete操作的内存
  • 栈:存储函数返回地址、函数参数、局部变量等

函数调用时,先把调用方的返回地址(下一条指令)压栈,接着把被调用函数的实参压栈,最后再把被调用函数的局部变量压栈。
然后就把控制权交给了被调用函数(跳转到被调用函数的入口处指令开始执行)。
待函数执行完毕以后,先把函数的返回值放入寄存器,然后把局部变量弹栈、把函数的实参弹栈、把函数返回地址弹栈并跳转回该地址继续执行。也就返回了调用函数。

常见的调用机制有几种:

  • __cdecl:调用时,函数实参从右至左压栈,返回时由调用方负责弹栈。这种是linux所采用的默认方式,因为由调用方负责压栈和弹栈,所以可以实现可变参数列表的传递。
  • __stdcall:调用时,函数实参从右至左压栈;返回时,由被调用方负责弹栈。可以看出来,这种调用方式是不可能实现可变参数传递的。因为函数返回时要想把栈清干净,就必须要明确参数的类型和个数。而这种调用方式下,被调用方无法知道调用方实际传了几个参数,也就不能保证栈能清干净
  • __pascal:调用时,函数的实参从左至右压栈;返回时,由被调用方负责弹栈。
  • __fastcall:调用时,前两个实参不通过栈传递,而是通过寄存器传递(所以fast)
  • __thiscall:是为了解决面向对象传递this指针,其他与__stdcall类似。

va_arg可变参数机制

可变参数函数的定义,一般具有如下形式:

void foo(int fixed1, int fixed2, int last_fixed, ...) {
   va_list ap;
   <可变参数的类型> *s;

   va_start(ap, last_fixed);
   while (<可变参数的个数>)  {
       s = va_arg(ap, <可变参数的类型>);
   }
   va_end(ap);
}

在这里,va_list、va_start、va_end、va_arg(还有一个va_copy),其实都是一些宏定义。它们的作用如下:

  • va_list:这其实是定义一个无类型的指针变量(void*)。在处理过程中,这个指针依次指向每一个可变参数。
  • va_start:对指针ap(va_list)进行初始化,也就是将ap指向第一个可变参数的地址。根据上面的预备知识可以了解到,所有参数都是依次入栈的。所以在栈中,最后一个固定参数后面接着就是可变参数。所以这个宏的第二个参数,必须是最后一个固定参数(last_fixed)。因为是固定参数,它的类型也是明确的,所占用的内存大小也是明确的。只要跳过sizeof(last_fixed),自然就定位到了第一个可变参数。注意:最后一个固定参数不可以声明为寄存器变量(register),因为寄存器变量不在栈中,与可变参数的地址没有关系,也就无法定位了。
  • va_end:没有什么特别的含义,只是为了与va_start配对。因为va_start的宏里有“{”存在,所以后面必须有配对的“}”。否则语法错误。
  • va_arg:有两个作用:一是取出当前的可变参数,二是把ap指针跳到下一个可变参数。要实现这一点,必须要知道当前可变参数的类型。所以这个宏的第二个参数就必须指定其类型。

这里有两个问题必须在调用方和被调用方之间实现协调好:

问题一:总共有几个可变参数?

要解决这个问题,一般有两种方法:

一种方法是像printf那样,利用固定参数来表明后续有几个可变参数,例如:

char name[] = "ahua";
int id = 777;
printf("hello %s, your id is %d\n", name, id);

可以看到,在可是字符串里,通过占位符(%?)与可变参数一一对应,这样个数就明确了。

另一种方法是通过一个事先约定好的特殊值,表示可变参数的结束,例如:

sum('total', 1, 43, 22, 55, 31, -1);

这里通过一个特殊值(-1),表示后面没有可变参数了。

问题二:每个可变参数的类型分别是什么?

要解决这个问题,同样有两种方法:
一种方式仍然是像printf那样,通过固定变量明确每个可变参数的类型(比如:%s为字符串、%d为int、%ld为long int、%lld为long long int等)。注意:%c对应的可变参数仍然是int,而不是char,因为编译器默认会对代码做优化,内存地址对齐。

另一种方式,就是约定所有可变参数都是同样的类型。如上例的sum。

例程

把上面两个例子简单写一下:

#include <stdio.h>
#include <stdarg.h>

int myprint(const char *fmt, ...) {
    va_list ap;
    char *place_holder = (char*)fmt;
    int a;
    char *b;
    double c;
    va_start(ap, fmt);
    while (*place_holder != 0x00) {
        int i = place_holder - fmt;
        switch (*place_holder) {
            case 'i':
                a = va_arg(ap, int);
                printf("arg%d = %d\n", i, a);
                break;
            case 's':
                b = va_arg(ap, char*);
                printf("arg%d = %s\n", i, b);
                break;
            case 'f':
                c = va_arg(ap, double);
                printf("arg%d = %f\n", i, c);
                break;
        }
        place_holder ++;
    }
    va_end(ap);
    return 0;
}

int mysum(const char* label, ...){
    va_list ap;
    int sum = 0;
    int val = 0;
    va_start(ap, label);
    while (1) {
        val = va_arg(ap, int);
        if (val < 0) {
            break;
        }
        sum += val;
    }
    printf("sum of '%s' is %d.\n", label, sum);
    return 0;
}

int main() {
    int x = 666;
    char y[] = "hello";
    double z = 3.14159;
    myprint("i", x);
    myprint("isf", x, y, z);

    int a = 1, b = 2, c = 3, d = 4;
    mysum("a,b,c,d", a, b, c, d, -1);
    return 0;
}

运行结果如下:

arg0 = 666
arg0 = 666
arg1 = hello
arg2 = 3.141590
sum of 'a,b,c,d' is 10.
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题