CC++变参函数

1.C实现变参函数

C语言中,有时需要变参函数来完成特殊的功能,比如C标准库函数printf()和scanf()。C中提供了省略符“…”能够帮主programmer完成变参函数的书写。变参函数原型申明如下:

type functionname(type param1,...);

变参函数至少要有一个固定参数,省略号“…”不可省略,比如printf()的原型如下:

int printf(const char *format,...);

在头文件stdarg.h中定义了三个宏函数用于获取指定类型的实参:

void    va_start(va_list arg,prev_param);    
type    va_arg(va_list arg,type);
void    va_end(va_list arg);

va在这里是variable argument(可变参数)的意思,那么变参函数的实现就变得相对简单很多。一般的变参函数处理过程: ①定义一个va_list变量设为va; ②调用va_start()使得va存放变参函数的变参前的一个固定参数的地址; ③不断调用va_arg()使得va指向下一个实参; ④最后调用va_end()表示变参处理完成,将va置空。 原理就是:函数的参数在内存中从低地址向高地址依次存放。

看一个例子:模仿pritnf()的实现[1]^{[1]}:

#include<iostream>  
#include<stdarg.h>  
#include<string.h>  
using namespace std;  

void func(char *c,...){  

    int i=0;  
    double result=0;  
    va_list arg;    //va_list变量  
    va_start(arg,c);    //arg指向固定参数c  
    while(c[i]!='\0'){  

        if(c[i]=='%'&&c[i+1]=='d'){  
            printf("%d",va_arg(arg,int));  
            i++;  
        }  
        else if(c[i]=='%'&&c[i+1]=='f'){  
            printf("%f",va_arg(arg,double));  
            i++;  
        }     
        else
            putchar(c[i]);  
        i++;  
    }  
    va_end(arg);  
}  


int main(){  
    int i=100;  
    double j=100.0;  
    printf("%d be equal %f\n",i,j);  
    func("%d be equal %f\n",i,j);
    system("pause");
}

程序输出:

100 be equal 100.000000
100 be equal 100.000000
请按任意键继续. . .

C变参函数缺点[2]^{[2]}: (1)缺乏类型检查,容易出现不合理的强制类型转换。在获取实参时,是通过给定的类型进行获取,如果给定的类型与实际参数类型不符,则会出现类型安全性问题,容易导致获取实参失败。 (2)不支持自定义类型。自定义类型在程序中经常用到,比如我们要使用printf()来打印一个Student类型的对象的内容,该用什么格式字符串去指定实参类型,通过C提供的va_list,我们无法提取实参内容。

鉴于以上两点,李健老师在其著作《编写高质量代码改善C++程序的150个建议》建议尽量不要使用C风格的变参函数。

2.C++实现变参函数

为了编写能够处理不同数量实参的函数,C++11提供了两种主要方法: (1)如果所有实参类型相同,可以传递initializer_list的标准库类型; (2)如果实参类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。

2.1initializer_list形参[3]^{[3]}

initializer_list是C++11引入的一种标准库类模板,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中,它提供的操作有:

initializer_list<T> lst;    //默认初始化T类型的空列表。
initializer_list<T> lst{a,b,c,...}; //lst的元素是对应初始值的副本,且列表中的元素是const。
lst2(lst);  //拷贝构造一个initializer_list对象,不拷贝列表中的元素,与原始列表共享元素
lst2=lst;   //赋值,与原始列表共享元素。

lst.size(); //列表中的元素数量。
lst.begin();    //返回指向lst中首元素的指针。
lst.end();  //返回lst中尾元素下一位置的指针。

和vector与list一样,initializer_list也是一种模板类型,定义initializer_list对象时必须指明列表中所含元素的类型。与vector和list不同之处在于initializer_list中的元素不可修改,拷贝构造和赋值时元素不会并不会被拷贝。如此设计,让initializer_list更加符合参数通过指针传递的,而非值传递,提高性能。所以C++11采用了initializer_list作为变参函数的形参,下面给出一个打印错误的变参函数:

void error_msg(initializer\_list<string> il){
    for(auto beg=il.begin();beg!=il.end())
        cout<<*beg<<" ";
    cout<<endl;
}

2.2可变参数模板

简介: 目前大部分主流编译器的最新版本均支持了C++11标准(官方名为ISO/IEC14882:2011)大部分的语法特性,其中比较难理解的新语法特性可能要属可变参数模板(variadic template)了,GCC 4.6和Visual studio 2013都已经支持变参模板。可变参数模板就是一个接受可变数目参数的函数模板或类模板。可变数目的参数被称为参数包(parameter packet),这个也是新引入 C++ 中的概念,可以细分为两种参数包: (1)模板参数包(template parameter packet) 表示零个或多个模板参数。 (2)函数参数包(function parameter packet) 表示零个或多个函数参数。

可变参数模板示例: 使用省略号…来指明一个模板的参数包,在模板参数列表中,class...typename...指出接下来的参数表示零个或多个类型参数;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数。声明一个带有可变参数个数的模板的语法如下所示:

//1.申明可变参数的类模板
template<typename... Types> class tuple;
tuple<int, string> a;  // use it like this

//2.申明可变参数的函数模板
template<typename T,typename... Types> void foo(const T& t,const Types&... rest);
foo<int,float,double,string>(1,2.0,3.0,"lvlv");//use like this

//3.申明可变非类型参数的函数模板(可变非类型参数也可用于类模板)
template<typename T,unsigned... args> void foo(const T& t);
foo<string,1,2>("lvlv");//use like this

其中第一条示例中Types就是模板参数包,第二条示例中rest就是函数参数包,第三条示例中args就是非类型模板参数包。

参数包扩展: 现在我们知道parameter packet了,怎么在程序中真正具体地去处理打包进来的“任意个数”的参数呢?也就是说可变参数模板,我们如何进行参数包的扩展,获取传入的参数包中的每一个实参呢?

对于一个参数包,除了可以通过运算符sizeof…来获取参数包中的参数个数,比如:

template<typename... Types> void g(Types... args){
    cout<<sizeof...(Types)<<endl;  //类型参数数目
    cout<<sizeof...(args)<<endl;   //函数参数
}

我们能够对参数包唯一能做的事情就是对其进行扩展,扩展一个包就是将它分解为构成的元素,通过在参数包的右边放置一个省略号…来触发扩展操作,例如:

template<typename T,typename... Types> ostream& print(ostream& os,const T& t,const Types&... rest){
    os<<t<<",";
    return print(os,rest...);
}

上面的示例代码中,存在两种包扩展操作: (1)const Types&... rest表示模板参数包的扩展,为print函数生成形参列表; (2)对print的调用中rest...表示函数参数包的扩展,为print调用生成实参列表。

可变参数函数实例: 可变参数函数通常以递归的方式来获取参数包的每一个参数。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。最后,定义一个非可变参数的同名函数模板来终止递归。我们以自定义的print函数为例,实现如下:

#include <iostream>  
using namespace std;

template<typename T> ostream& print(ostream& os,const T& t){
    os<<t<<endl;  //包中最后一个元素之后打印换行符
}

template<typename T,typename... Types> ostream& print(ostream& os,const T& t,const Types&... rest){
    os<<t<<",";  //打印第一个实参
    print(os,rest...);  //递归调用,打印其他实参
}

int main(){
    print(cout,10,123.0,"lvlv",1); //例1
    print(cout,1,"lvlv0","lvlv1");
}

程序输出:

10,123,lvlv,1
1,lvlv0,lvlv1

上面递归调用print,以例1为例,执行的过程如下:

调用

t

rest…

print(cout,10,123.0,”lvlv”,1)

10

123.0,”lvlv”,1

print(cout,123.0,”lvlv”,1)

123.0

“lvlv”,1

print(cout,”lvlv”,1)

“lvlv”

1

print(cout,1),调用非变参版本的print

1

前三个调用只能与可变参数版本的print匹配,非变参版本是不可行的,因为这三个调用要传递两个以上实参,非可变参数的print只接受两个实参。对于最后一次递归调用print(cout,1),两个版本的print都可以,因为这个调用传递两个实参,第一个实参的类型为ostream&,另一个是const T&参数。但是由于非可变参数模板比可变参数模板更加特例化,因此编译器选择非可变参数版本。


[1]编写高质量代码改善C++程序的150个建议.李健.2012:34-35 [2]c /c++变参函数 [3]Stanley B. Lippman著,王刚 杨巨峰译.C++ Primer中文版第五版.2013:197-199

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏blackheart的专栏

[C#6] 3-null 条件运算符

0. 目录 C#6 新增特性目录 1. 老版本的代码 1 namespace csharp6 2 { 3 internal class Perso...

22210
来自专栏程序生活

Python itertools的使用简介无限迭代器chain方法

用*放在一个可迭代对象前面可以将对象拆分成多个单元素,比如first=[['1','2','3','4','4'],['2','4','2']],如果把*放在f...

761
来自专栏Java学习网

Java面试中最常见的10个问题,Java底层知识,花点时间学习一下

1.什么是 Java 虚拟机?为什么 Java 被称作是“平台无关的编程语言”? Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文...

2745
来自专栏从流域到海域

《笨办法学Python》 第32课手记

《笨办法学Python》 第32课手记 本节课讲for循环和list,list里类似于c中的数组,但有区别很大。C语言中的数组是数据类型相同的值的集合,list...

2099
来自专栏racaljk

关于C++函数返回局部对象的详细分析

以前一直挺好奇的,C++是怎么在函数内返回一个局部对象的。因为按照我之前的想法,函数返回一个基本类型的值是通过存放到ecx实现的(关于浮点不了解),但是局部对象...

4521
来自专栏转载gongluck的CSDN博客

C++拷贝构造函数(深拷贝,浅拷贝)

对于普通类型的对象来说,它们之间的复制是很简单的,例如:   int a=88;   int b=a;   而类对象与普通对象不同,类对象...

3007
来自专栏信安之路

php 弱类型问题

php 是一门简单而强大的语言,提供了很多 Web 适用的语言特性,其中就包括了变量弱类型,在弱类型机制下,你能够给一个变量赋任意类型的值。

1590
来自专栏nummy

python operator模块学习

operator模块是python中内置的操作符函数接口,它定义了一些算术和比较内置操作的函数。operator模块是用c实现的,所以执行速度比python代码...

822
来自专栏超然的博客

ECMAScript 6 笔记(五)

Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环

1292
来自专栏九彩拼盘的叨叨叨

JavaScript 字符串练习题

如果对字符串的 API 不是很熟悉,可查阅 W3School JavaScript String API。

891

扫码关注云+社区

领取腾讯云代金券