介绍C/C++,java,和go语言对函数变参的使用。
1. C/C++语言的变参
1.1 变参函数声明
变参函数的声明
type func_name(const char * format, ...)
type func_name(int count, ...)
说明
- 变参是通过三个点号(...)来表示。
- 变参不能是函数第一个参数,否则编译器会报错,例如
t.c:4:12: error: ISO C requires a named argument before ‘...’
- 变参必须是函数的最后一个参数
- 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++的声明是一样的;不同点是
- Java变参除了三个点(...)之外,还有名字和类型。
- 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
明白了把。