C语言中可变参数的用法

2016-02-19 20:51 3 1 收藏

下面是个简单易学的C语言中可变参数的用法教程,图老师小编详细图解介绍包你轻松学会,喜欢的朋友赶紧get起来吧!

【 tulaoshi.com - 编程语言 】

我们在C语言编程中会碰到一些参数个数可变的函数,例如printf()这个函数,它的定义是这样的:
  int printf( const char* format, ...); !-- frame contents -- !-- /frame contents --
  它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的,例如我们可以有以下不同的调用方法:
  printf("%d",i);
  printf("%s",s);
  printf("the number is %d ,string is:%s", i, s);
  究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现的呢?本文就这个问题进行一些探讨,希望能对大家有些帮助.会C++的网友知道这些问题在C++里不存在,因为C++具有多态性.但C++是C的一个超集,以下的技术也可以用于C++的程序中.限于本人的水平,文中假如有不当之处,请大家指正. (一)写一个简单的可变参数的C函数
  
  下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的C函数要在程序中用到以下这些宏:
  void va_start( va_list arg_ptr, prev_param );
  
  type va_arg( va_list arg_ptr, type );
  
  void va_end( va_list arg_ptr );
  va在这里是variable-argument(可变参数)的意思.这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
  void simple_va_fun(int i, ...)
  {
  va_list arg_ptr;
  int j=0;
  
  va_start(arg_ptr, i);
  j=va_arg(arg_ptr, int);
  va_end(arg_ptr);
  printf("%d %d", i, j);
  return;
  }
  我们可以在我们的头文件中这样声明我们的函数:
  extern void simple_va_fun(int i, ...);
  我们在程序中可以这样调用:
  simple_va_fun(100);
  simple_va_fun(100,200);
  从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
  1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
  2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
  3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.
  4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.假如函数有多个可变参数的,依次调用va_arg获取各个参数.   假如我们用下面三种方法调用的话,都是合法的,但结果却不一样:
  1)  simple_va_fun(100);
  结果是:100 -123456789(会变的值)
  2)  simple_va_fun(100,200);
  结果是:100 200
  3)  simple_va_fun(100,200,300);
  结果是:100 200
  我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果的原因和可变参数在编译器中是如何处理的.
  
更多内容请看C/C++进阶技术文档专题,或
  (二)可变参数在编译器中的处理
  
  我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面以VC++中stdarg.h里x86平台的宏定义摘录如下(’’号表示折行):
   !-- frame contents -- !-- /frame contents --   typedef char * va_list;
  
  
  #define _INTSIZEOF(n)
  ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
  
  #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
  
  #define va_arg(ap,t)
  ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
  
  #define va_end(ap) ( ap = (va_list)0 )
  定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址,如图:
  
  高地址-----------------------------
  函数返回地址
  -----------------------------
  .......
  -----------------------------
  第n个参数(第一个可变参数)
  -------------------------------va_start后ap指向
  第n-1个参数(最后一个固定参数)
  低地址------------------------------- &v
  图(1)
  
  然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:
  j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
  首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址(图2).然后用*取得这个地址的内容(参数值)赋给j.
  
  高地址-----------------------------
  函数返回地址
  -----------------------------
  .......
  -------------------------------va_arg后ap指向
  第n个参数(第一个可变参数)
  -------------------------------va_start后ap指向
  第n-1个参数(最后一个固定参数)
  低地址------------------------------- &v
  图(2)
  
  最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在Linux的x86平台就是这样定义的.在这里大家要注重一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.关于va_start, va_arg, va_end的描述就是这些了,我们要注重的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
  
  (三)可变参数在编程中要注重的问题
  
  因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型.有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数printf是从固定参数format字符串来分析出参数的类型,再调用va_arg的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判定来实现的.另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.假如simple_va_fun()改为:
  void simple_va_fun(int i, ...)
  {
  va_list arg_ptr;
  char *s=NULL;
  
  va_start(arg_ptr, i);
  s=va_arg(arg_ptr, char*);
  va_end(arg_ptr);
  printf("%d %s", i, s);
  return;
  }
  可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出错,但错误却是难以发现,不利于我们写出高质量的程序.
  以下提一下va系列宏的兼容性.System V Unix把va_start定义为只有一个参数的宏:
  va_start(va_list arg_ptr);
  而ANSI C则定义为:
  va_start(va_list arg_ptr, prev_param);
  假如我们要用system V的定义,应该用vararg.h头文件中所定义的宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以用ANSI C的定义就够了,也便于程序的移植.
  
  
  小结:
  可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.假如在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现.
  
  写这篇文章时,适逢感冒,多得有她的关怀,
   谨把这篇文章献给她...
更多内容请看C/C++进阶技术文档专题,或

来源:https://www.tulaoshi.com/n/20160219/1624671.html

延伸阅读
#pragma#pragma 预处理指令详解 在所有的预处理指令中,#Pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma指令对每个编译器给出了一个方法,在保持与C和 C++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。 ...
基本解释 const是一个C语言的关键字,它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的健壮性,另外,在观看别人代码的时候,清晰理解const所起的作用,对理解对方的程序也有一些帮助。 虽然这听起来很简单,但实际上,const的使用也是c语言中一个比较微妙的地方,微妙在何处呢?请看下面几个问题。 问题: const变量 & ...
在C语言中,rand()函数可以用来产生随机数,但是这不是真真意义上的随机数,是一个伪随机数,是根据一个数,我们可以称它为种子,为基准以某个递推公式推算出来的一系数,当这系列数很大的时候,就符合正态公布,从而相当于产生了随机数,但这不是真正的随机数,当计算机正常开机后,这个种子的值是定了的,除非你破坏了系统,为了改变这个种子...
1. exit 用于在程序运行的过程中随时结束程序,exit 的参数是返回给OS的。main函数结束时也会隐式地调用exit函数。exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。exit是结束一个进程,它将删除进程使用的内存空间,同...
当我在linux下写c语言的时候经常会遇到段错误. 所以就来细究一下.   段错误或段违规(segmentation violation) 查看Expert C Programming(Peter Van Der Linden) Pg.156 解释到段错误是由于内存管理单元(MMU)的异常所致, 而该异常则通常是由于解除引用一个未初始化或非法的指针引起. 就是指针正在引用一个并不位于你的地址空间中的地址....

经验教程

506

收藏

20
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部