当前位置:首页 > > 充电吧
[导读]一、什么是可变参数我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: int printf( const char* format, ...); 它除了有一个

一、什么是可变参数

我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: 

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

它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式: printf("%d",i); 

printf("%s",s); 

printf("the number is %d ,string is:%s", i, s);    

以上这些东西已为大家所熟悉。但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助.

 

二、写一个简单的可变参数的C函数 

先看例子程序。该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定. 在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值。这也是老师布置的作业

先看源代码:


#include#include "stdarg.h"


int count(int start, ...){
 va_list valist;
 int count = 0;
 int va_val = start;
 va_start(valist, start);

 do{
	count++;
	printf("%d ", va_val);
	va_val = va_arg(valist, int);
 }while(va_val != -1);

 printf("n");
return count;
}


从这个函数的实现可以看到,我们使用可变参数应该有以下步骤: 

⑴由于在程序中将用到以下这些宏: 

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中,所以用到可变参数的程序应该包含这个头文件.

⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变 

量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。

⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数. 

⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。

⑸设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完这几个宏的内部实现机制后,自然就会明白。


(二)可变参数在编译器中的处理 

我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装目录下的vc98includestdarg.h)

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 )

下面我们解释这些代码的含义:

1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的

2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。

3、va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。 

这里要知道两个事情:

    ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。

    (2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。

|——————————————————————————|

|  最后一个可变参数             |   ->高内存地址处

|——————————————————————————|

   ...................

|——————————————————————————|

|  第N个可变参数              |     ->va_arg(arg_ptr,int)后arg_ptr所指的地方,

|                               |     即第N个可变参数的地址。

|——————————————— |     

   ………………………….

|——————————————————————————|

|  第一个可变参数               |     ->va_start(arg_ptr,start)后arg_ptr所指的地方

|                               |     即第一个可变参数的地址

|——————————————— |     

|———————————————————————— ——|

|                               |

|  最后一个固定参数             |    -> start的起始地址

|—————————————— —|       .................

|—————————————————————————— |

|                               |  

|——————————————— |  -> 低内存地址处


(4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。

因此,现在再来看va_arg()的实现就应该心中有数了:

#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

这个宏做了两个事情,

       ①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值

   ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。

(5)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 的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 例如,在C的经典教材《the c programming language》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。

(四)小结: 

1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。

2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如

⑴在固定参数中设标志—— printf函数就是用这个办法。后面也有例子。

⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.

无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。

3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:

①函数栈的生长方向

②参数的入栈顺序

③CPU的对齐方式

④内存地址的表达方式

结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。

4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。下面就是一个例子

(五)扩展——自己实现简单的可变参数的函数。

下面是一个简单的printf函数的实现,参考了

#include "stdio.h"

#include "stdlib.h"

void myprintf(char* fmt, ...)        //一个简单的类似于printf的实现,//参数必须都是int 类型

{ 

    char* pArg=NULL;               //等价于原来的va_list 

    char c;

    

    pArg = (char*) &fmt;          //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值

    pArg += sizeof(fmt);         //等价于原来的va_start          

 

    do

    {

        c =*fmt;

        if (c != '%')

        {

            putchar(c);            //照原样输出字符

        }

        else

{

//按格式字符输出数据

            switch(*++fmt) 

{

            case 'd':

                printf("%d",*((int*)pArg));           

                break;

            case 'x':

                printf("%#x",*((int*)pArg));

                break;

            default:

                break;

            } 

            pArg += sizeof(int);               //等价于原来的va_arg

        }

        ++fmt;

    }while (*fmt != ''); 

    pArg = NULL;                               //等价于va_end

    return; 

}

int main(int argc, char* argv[])

{

    int i = 1234;

    int j = 5678;

    

    myprintf("the first test:i=%dn",i,j); 

    myprintf("the secend test:i=%d; %x;j=%d;n",i,0xabcd,j); 

    system("pause");

    return 0;

}



在intel+win2k+vc6的机器执行结果如下:

the first test:i=1234

the secend test:i=1234; 0xabcd;j=5678;





最后那个 printf 函数的改写自己还没完成,等全部做好再贴上代码



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

本文中,小编将对PLC予以介绍,如果你想对它的详细情况有所认识,或者想要增进对它的了解程度,不妨请看以下内容哦。

关键字: PLC 编程

在下述的内容中,小编将会对PLC的相关消息予以报道,如果PLC是您想要了解的焦点之一,不妨和小编共同阅读这篇文章哦。

关键字: PLC 编程 模块化

-Cognizant正在筹办全球最大规模的氛围编程活动,以提升数千名员工的AI素养 为抓住人工智能经济将创造的巨大机遇,Cognizant与Lovable、Windsurf、Cursor、Gemini Code Ass...

关键字: 编程 NI AN PI

链表作为一种基础的数据结构,在程序设计中扮演着重要角色。掌握链表的高效操作技巧,特别是逆序、合并和循环检测,对于提升算法性能和解决复杂问题至关重要。本文将详细介绍这些操作的C语言实现,并分析其时间复杂度。

关键字: 链表 C语言

在C/C++多文件编程中,静态变量(static)与全局变量的作用域规则看似简单,实则暗藏诸多陷阱。开发者若未能准确理解其链接属性与生命周期,极易引发难以调试的内存错误、竞态条件以及维护灾难。本文将深入剖析这两类变量的作...

关键字: 静态变量 全局变量 C语言

在嵌入式系统和服务器开发中,日志系统是故障排查和运行监控的核心组件。本文基于Linux环境实现一个轻量级C语言日志库,支持DEBUG/INFO/WARN/ERROR四级日志分级,并实现按大小滚动的文件轮转机制。该设计在某...

关键字: C语言 嵌入式系统

在嵌入式系统和底层驱动开发中,C语言因其高效性和可控性成为主流选择,但缺乏原生单元测试支持成为开发痛点。本文提出一种基于宏定义和测试用例管理的轻量级单元测试框架方案,通过自定义断言宏和测试注册机制,实现无需外部依赖的嵌入...

关键字: C语言 嵌入式系统 驱动开发

在嵌入式系统开发中,实时操作系统(RTOS)的任务调度算法直接影响系统的响应速度和资源利用率。时间片轮转(Round-Robin, RR)作为一种经典的公平调度算法,通过为每个任务分配固定时间片实现多任务并发执行。本文将...

关键字: 实时操作系统 RTOS C语言

在Linux设备驱动开发中,等待队列(Wait Queue)是实现进程睡眠与唤醒的核心机制,它允许进程在资源不可用时主动放弃CPU,进入可中断睡眠状态,待资源就绪后再被唤醒。本文通过C语言模型解析等待队列的实现原理,结合...

关键字: 驱动开发 C语言 Linux

在嵌入式系统开发中,C语言与汇编的混合编程是优化性能、访问特殊指令或硬件寄存器的关键技术。然而,内联汇编的语法差异和寄存器使用规则常导致难以调试的问题。本文以ARM Cortex-M和x86架构为例,系统梳理内联汇编的核...

关键字: C语言 汇编混合编程
关闭