函数变参的使用

介绍C/C++,java,和go语言对函数变参的使用。

1. C/C++语言的变参

1.1 变参函数声明

变参函数的声明

type func_name(const char * format, ...)
type func_name(int count, ...)

说明

  1. 变参是通过三个点号(...)来表示。
  2. 变参不能是函数第一个参数,否则编译器会报错,例如
    t.c:4:12: error: ISO C requires a named argument before ‘...’
  3. 变参必须是函数的最后一个参数
  4. va_start的第二个参数要求是最后一个命名参数,否则:
    t.c:11:4: warning: second parameter of ‘va_start’ not last named argument [-Wvarargs]

1.2 如何调用

这个很简单啦

func_name("%s,%s,%d\n", "AA", "BB", 33);    // 变参个数为 3
func_name("Hello\n");                       // 变参个数为 0
func_name(1, 100)                           // 变参个数为 1
func_name(2, 100, 200)                      // 变参个数为 2

1.3 函数内如何分析变参

主要有三个相关的变量和宏,定义在 stdarg.h 头文件里。

// 定义va_list类型的变量
va_list valist;

// 初始化valist,第二个参数表示从这个参数之后的参数进入valist
// 这也正好符合前面的限制,变参不能是第一个参数。
va_start(valist, num);

// 依次访问所有的参数
// 其中第二个参数是类型,这依赖于应用程序自行保证类型的正确性。
for (i = 0; i < num; i++) {
  sum += va_arg(valist, int);
}

// 清除valist
va_end(valist);

举个例子来说:

#include 
#include 

void showParam(char * types, ...) {  
   int i;
   char c;
   char * s;

   va_list valist;  
   char * p = types;
   va_start(valist, types);  
   for(;*p != '\0'; p++) {  
      switch(*p) {
         case 'i':
            i = va_arg(valist, int);  
            printf( "%d\n", i);  
            break;  
         case 'c':
             c = va_arg(valist, int);  
             printf("%c\n", c);  
             break;  
         case 's':
             s = va_arg(valist, char *);  
             printf("%s\n", s);  
             break;  
         default:  
             break;  
      }
   }  
   va_end( valist );  
} 

int main(int argc, char * argv[]) {
    showParam("ics", 123,'A',"abc");
    return 0;
}

运行结果如下:

$ gcc main.c && ./a.out 
123
A
abc

1.4 一个变参函数如何调用另一变参函数

例如

void Log(char * level, const char * format, ...) {
  ...
}
void Debug(const char * format, ...) {
  ...
}

Debug函数如何调用Log呢?

void Debug(const char * format, ...) {
  Log("DEBUG", format, ...)
}

这样写行吗,好像语法不通啊。

实际上C/C++不支持一个变参直接调用另一个变参函数。
但是可以把被调用的函数调整一下即可,看例子:

#include 
#include 

void Debug(const char* format, ...)
{
    char output[1024];
    va_list valist;
    va_start(valist, format);
    vsprintf(output, format, valist);
    va_end(valist);
    printf("%s\n", output);
}

int main(int argc, char * argv[]) {
    Debug("This is test log:%d=%s", 123, "ABCD");
    return 0;
}

运行结果为:

$ gcc main.c && ./a.out 
This is test log:123=ABCD

我们可以看到有一个函数vsprintf,看它的声明:

int vsprintf(char *str, const char *format, va_list ap);

这个函数里面,我们使用了va_list作为变参的类型,至此我们得出结论,如果一个变参函数需要调用另一个变参函数,那么被调用的变参函数需要进行改造,使用va_list定参来模拟变参。
这样前面的Debug/Log函数需要改造成如下格式:

#include 
#include 

void Log(char * level, const char * format, va_list valist) {
    char output[1024];
    vsprintf(output, format, valist);
    printf("%s: %s\n", level, output);
}

void Debug(const char * format, ...) {
    va_list valist;
    va_start(valist, format);
    Log("DEBUG", format, valist);
    va_end(valist);

}

int main(int argc, char * argv[]) {
    Debug("This is test log:%d=%s", 123, "ABCD");
    return 0;
}

2. Java语言的变参

2.1 变参函数声明

type func_name(type ...params)

这个声明和C/C++的声明是一样的;不同点是

  1. Java变参除了三个点(...)之外,还有名字和类型。
  2. Java允许第一个参数就是变参。但同样不允许变参后面还有参数。

2.2 如何调用

调用方式和C/C++一样

    public static void run(String... strings) {
        for (String s: strings) {
            System.out.println("run: " + s);
        }
    }

    public static void main(String[] args) {
        run();                                // 变参个数为 0
        run("");                              // 变参个数为 1
        run("AA","BB");                      // 变参个数为 2
        run("AA","BB", "CC");                // 变参个数为 3
    }

运行结果

run: 
run: AA
run: BB
run: AA
run: BB
run: CC

2.3 函数内如何分析变参

前面例子我们也看到了,很简单遍历参数就行了,不展开讲了。

2.4 一个变参函数如何调用另一变参函数

    public static void run2(String ... strings) {
        for (String s: strings) {
            System.out.println("run2: " + s);
        }
    }
    public static void run1(String... strings) {
        run2(strings);
    }

    public static void main(String[] args) {
        run1("AA","BB", "CC");
    }

比起前面的C/C++,是不是感觉很幸福;太简单了,直接调用就行了。

到这里是不是可以完美的结束了呢,我问一个问题:
run1()和run2()都是变参函数,main传给run1()有三个参数("AA","BB","CC"),run1()传给run2()的是参数strings,我的问题是run2()收到的是一个参数(String...类型),还是三个参数(String)类型。

改造一下代码:

    public static void run2(String ... strings) {
        System.out.println("run2 param type=" + strings.getClass().getName());
        System.out.println("run2 param size=" + strings.length);
    }
    public static void run1(String... strings) {
        System.out.println("run1 param type=" + strings.getClass().getName());
        System.out.println("run1 param size=" + strings.length);
        run2(strings);
    }

    public static void main(String[] args) {
        run1("AA","BB", "CC");
    }

再运行

run1 param type=[Ljava.lang.String;
run1 param size=3
run2 param type=[Ljava.lang.String;
run2 param size=3

我们会发现,不管在run1()里面还是在run2()里面收到的参数strings实际上是一个string数组类型,是不是java使用数组来包装变参呢?
答案是肯定的。调用者可以使用数组来传递变参。

public static void main(String[] args) {
    String params[] = {"AA","BB", "CC"};
    run1(params);
    // run1("AA","BB", "CC");
}

上述两个调用方法是一样的效果。

再看一个例子:

    public static void run2(Object ... strings) {
        System.out.println("run2 param type=" + strings.getClass().getName());
        System.out.println("run2 param size=" + strings.length);
        for (Object s: strings) {
            System.out.println("run2 param=" + s);
        }
    }
    public static void run1(Object... strings) {
        System.out.println("run1 param type=" + strings.getClass().getName());
        System.out.println("run1 param size=" + strings.length);
        run2(strings);
    }

    public static void main(String[] args) {
        String params[] = {"AA","BB", "CC"};
        run1(params, "DD");
    }

运行结果:

run1 param type=[Ljava.lang.Object;
run1 param size=2
run2 param type=[Ljava.lang.Object;
run2 param size=2
run2 param=[Ljava.lang.String;@15db9742
run2 param=DD

理解一下为什么是这样。
因为run1()收到的变参实际上是一个数组,包含两个元素,第一个元素是一个字符串数组("AA","BB", "CC"),第二个元素是一个字符串("DD")。

3. go语言的变参

3.1 变参函数的声明

func func_name(args ...interface{}) {
  ...
}

这种声明方式和java是一致的。

3.2 如何调用

func_name()                              # 变参个数为 0
func_name("")                            # 变参个数为 1
func_name("AA", "BB", "CC")              # 变参个数为 3

3.3 参数分析

再看一个例子:

package main

import  "fmt"
import  "reflect"

func Log(args ...string) {
    fmt.Println(reflect.TypeOf(args))
    fmt.Printf("%d\n", len(args))
}

func main() {
    Log()
    Log("")
    Log("INFO", "AAA", "This is a message")
}

运行结果:

$ go build && ./main 
[]string
0
[]string
1
[]string
3

这下发现golang的变参和java的变参原理是一样的,都是以数组的方式(golang用slice)组织参数,那么就可以简单的把变参理解到slice就非常清楚了。

3.4 一个变参函数如何调用另一变参函数

package main

import  "fmt"
import  "reflect"

func Debug(args ...string) {
    Log("DEBUG", args...)
}

func Log(level string, args ...string) {
    fmt.Println(reflect.TypeOf(args))
    fmt.Printf("%d\n", len(args))
}

func main() {
    Debug()
    Debug("")
    Debug("AAA", "This is a message")
}

这个例子中main()=>Debug()=>Log(),Debug和Log都是变参函数;
注意在Debug里面调用Log的时候 需要显式的指定args是一个变参变量,这和java的使用方式有点差异。

正是这个差异,在golang里面变参和slice[]不是完全相等。虽然在java里面可以传一个数组给变参,但是在golang里面却不能传一个slice给变参。

package main

import  "fmt"
import  "reflect"


func Debug(args ...string) {
    ss := []string{"aa", "bb"}
    Log("DEBUG", ss)
}

func Log(level string, args ...string) {
    fmt.Println(reflect.TypeOf(args))
    fmt.Printf("%d\n", len(args))
}

func main() {
    Debug()
    Debug("")
    Debug("AAA", "This is a message")
}

编译

./main.go:9: cannot use ss (type []string) as type string in argument to Log

可见在golang里面变参就是变参,不是slice对象;两者不能互通使用。

但是还是可以把slice标记为变参,通过在变量名后跟三个点(varname...)的格式,看下面例子代码:

package main

import  "fmt"
import  "reflect"


func Log(args ...string) {
    fmt.Println(reflect.TypeOf(args))
    fmt.Printf("%d\n", len(args))
}

func main() {
    ss1 := []string{}
    ss2 := []string{"aa", "bb"}
    Log(ss1)
    Log(ss2)
}

编译

$ go build && ./main
./main.go:15: cannot use ss1 (type []string) as type string in argument to Log
./main.go:16: cannot use ss2 (type []string) as type string in argument to Log

我们把他改成:

package main

import  "fmt"
import  "reflect"


func Log(args ...string) {
    fmt.Println(reflect.TypeOf(args))
    fmt.Printf("%d\n", len(args))
}

func main() {
    ss1 := []string{}
    ss2 := []string{"aa", "bb"}
    //Log(ss1)
    //Log(ss2)
    Log(ss1...)
    Log(ss2...)
}

再运行

$ go build && ./main
[]string
0
[]string
2

这样就运行的很好了。因为如果直接使用ss1那就是这是一个slice类型的参数,但是Log()函数并不支持slice类型参数,而如果换成ss1...表示这是多个字符串类型参数。

最后一个例子:

package main

import  "fmt"
import  "reflect"


func Log(args ...interface{}) {
    fmt.Println(reflect.TypeOf(args))
    fmt.Printf("%d\n", len(args))
}

func main() {
    ss1 := []interface{}{}
    ss2 := []interface{}{"aa", "bb"}
    Log(ss1)
    Log(ss2)
}

编译运行

$ go build && ./main 
[]interface {}
1
[]interface {}
1

明白了把。

你可能感兴趣的:(函数变参的使用)