嵌入式 C 语言的可变参数表函数的设计

简介: 首先在介绍可变参数表函数的设计之前,我们先来介绍一下最经典的可变参数表printf函数的实现原理。一、printf函数的实现原理在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。
首先在介绍可变参数表函数的设计之前,我们先来介绍一下最经典的可变参数表printf函数的实现原理。
一、printf函数的实现原理
在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中a、b都是int型的)的汇编代码
[cpp]  view plain copy
 
  1. .section  
  2. .data  
  3. string out = "%d,%d"  
  4. push b  
  5. push a  
  6. push $out  
  7. call printf  
你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。
二、可变参数表函数的设计
      标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个“其他参数”。这种函数被称作“具有变长度参数表的函数”,或简称为“变参数函数”。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件<stdarg.h>,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。
      C中变长实参头文件stdarg.h提供了一个数据类型va-list和三个宏(va-start、va-arg和va-end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。va-list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va-list的变量,该变量用来指向va-arg和va-end所需信息的位置。下面给出va_list在C中的源码:
[cpp]  view plain copy
 
  1. typedef char *  va_list;  
     void va-start(va-list ap,lastfix)是一个宏,它使va-list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va-arg和va-end之前,必须首先调用该宏。va-start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va-start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。下面给出va_start在C中的源码:
[cpp]  view plain copy
 
  1. #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  
  2. #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址  
      type va-arg(va-list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va-arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va-arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,下面给出va_arg在C中的源码:
[cpp]  view plain copy
 
  1. #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数  
     在使用va-arg时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为char型、char * 型、int型和float型时,在va-arg中它们的类型则应分别为int、char *、int和double.
     void va-end(va-list ap)也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针ap赋值为0,使它不指向内存的变量。下面给出va_end在C中的源码:
[cpp]  view plain copy
 
  1. #define va_end(ap)      ( ap = (va_list)0 )  
     va-end必须在va-arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va-end,以保证正确地恢复栈。
    一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。
假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):
int sum(int n, ...)
我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。
下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用宏a_start对这个变量进行初始化。宏va_start的类型特征可以大致描述为:
va_start(va_list vap, 最后一个普通参数)
在函数sum里对vap初始化的语句应当写为:

va_start(vap, n); 相当于  char *vap= (char *)&n + sizeof(int);
此时vap正好指向n后面的可变参数表中的第一个参数。

在完成这个初始化之后,我们就可以通过另一个宏va_arg访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:
类型 va_arg(va_list vap, 类型名)
在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(“当前”实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:
v = va_arg(vap, int);
这里假定v是一个有定义的int类型变量。
在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:
void va_end(va_list vap);
三、栈中参数分布以及宏使用后的指针变化说明如下:

下面是函数sum的完整定义,从中可以看到各有关部分的写法:
[cpp]  view plain copy
 
  1. #include<iostream>  
  2. using namespace std;  
  3. #include<stdarg.h>  
  4.   
  5. int sum(int n,...)  
  6. {  
  7.     int i , sum = 0;  
  8.     va_list vap;  
  9.     va_start(vap , n);     //指向可变参数表中的第一个参数  
  10.     for(i = 0 ; i < n ; ++i)  
  11.         sum += va_arg(vap , int);     //取出可变参数表中的参数,并修改参数指针vap使其增加以指向表中下一个参数  
  12.     va_end(vap);    //把指针vap赋值为0  
  13.     return sum;  
  14. }  
  15. int main(void)  
  16. {  
  17.     int m = sum(3 , 45 , 89 , 72);  
  18.     cout<<m<<endl;  
  19.     return 0;  
  20. }  
这里首先定义了va_list变量vap,而后对它初始化。循环中通过va_arg取得顺序的各个实参的值,并将它们加入总和。最后调用va_end结束。
下面是调用这个函数的几个例子:
k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);
函数sum中首先定义了可变参数表指针vap,而后通过va_start ( vap, n )取得了参数表首地址(赋值给了vap),其后的for循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到的遍历方式是类似的。
  函数sum看起来简洁明了,但是实际上printf的实现却远比这复杂。sum函数之所以看起来简单,是因为:
  1、sum函数可变参数表的长度是已知的,通过num参数传入;
  2、sum函数可变参数表中参数的类型是已知的,都为int型。
  而printf函数则没有这么幸运。首先,printf函数可变参数的个数不能轻易的得到,而可变参数的类型也不是固定的,需由格式字符串进行识别(由%f、%d、%s等确定),因此则涉及到可变参数表的更复杂应用。
在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在for循环中。譬如,在识别为%d后,做的是va_arg ( vap, int ),而获知为%l和%lf后则进行的是va_arg ( vap, long )、va_arg ( vap, double )。格式字符串识别完成后,可变参数也就处理完了。
在编写和使用具有可变数目参数的函数时,有几个问题值得注意。
第一:调用va_arg将更新被操作的va_list变量(如在上例的vap),使下次调用可以得到下一个参数。在执行这个操作时,va_arg并不知道实际有几个参数,也不知道参数的实际类型,它只是按给定的类型完成工作。因此,写程序的人应在变参数函数的定义里注意控制对实际参数的处理过程。上例通过参数n提供了参数个数的信息,就是为了控制循环。标准库函数printf根据格式串中的转换描述的数目确定实际参数的个数。如果这方面信息有误,函数执行中就可能出现严重问题。编译程序无法检查这里的数据一致性问题,需要写程序的人自己负责。在前面章节里,我们一直强调对printf等函数调用时,要注意格式串与其他参数个数之间一致性,其原因就在这里。
第二:编译系统无法对变参数函数中由三个圆点代表的那些实际参数做类型检查,因为函数的头部没有给出这些参数的类型信息。因此编译处理中既不会生成必要的类型转换,也不会提供类型错误信息。考虑标准库函数printf,在调用这个函数时,不但实际参数个数可能变化,各参数的类型也可能不同,因此不可能有统一方式来描述它们的类型。对于这种参数,C语言的处理方式就是不做类型检查,要求写程序的人保证函数调用的正确性。
假设我们写出下面的函数调用:
k = sum(6, 2.4, 4, 5.72, 6, 2);

编译程序不会发现这里参数类型不对,需要做类型转换,所有实参都将直接传给函数。函数里也会按照内部定义的方式把参数都当作整数使用。编译程序也不会发现参数个数与6不符。这一调用的结果完全由编译程序和执行环境决定,得到的结果肯定不会是正确的。

四、简单的练习

 

问题1:可变长参数的获取
  有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:
  va_arg (argp, float);
  这样做可以吗?
  答案与分析:
  不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char、 short类型被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。
  问题2:定义可变长参数的一个限制
  为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?
[cpp]  view plain copy
 
  1. int f(...)  
  2. {  
  3.     ......  
  4.     ......  
  5.     ......  
  6. }  
答案与分析:
  不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。
      问题3:如何判别可变参数函数的参数类型?
函数形式如下:
[cpp]  view plain copy
 
  1. void fun(char *str ,...)  
  2. {  
  3.     ......  
  4.     ......  
  5.     ......  
  6. }  
若传的参数个数大于1,如何判别第2个以后传参的参数类型???
答案与分析:
这个是没有办法判断的,例如printf( "%d%c%s ",   ....)是通过格式串中的%d、 %c、 %s来确定后面参数的类型,其实你也可以参考这种方法来判断不定参数的类型。
最后,奉献上自己写的一个printf函数
[cpp]  view plain copy
 
  1. #include<stdio.h>  
  2. #include<stdarg.h>  
  3.   
  4. void myitoa(int n, char str[], int radix)  
  5. {  
  6.     int i , j , remain;  
  7.     char tmp;  
  8.     i = 0;  
  9.     do  
  10.     {  
  11.         remain = n % radix;  
  12.         if(remain > 9)  
  13.             str[i] = remain  - 10 + 'A';  
  14.         else  
  15.             str[i] = remain + '0';  
  16.         i++;  
  17.     }while(n /= radix);  
  18.     str[i] = '\0';  
  19.   
  20.     for(i-- , j = 0 ; j <= i ; j++ , i--)  
  21.     {  
  22.         tmp = str[j];  
  23.         str[j] = str[i];  
  24.         str[i] = tmp;  
  25.     }  
  26.   
  27. }  
  28.   
  29. void myprintf(const char *format, ...)  
  30. {  
  31.     char c, ch, str[30];  
  32.     va_list ap;  
  33.   
  34.     va_start(ap, format);  
  35.     while((c = *format))  
  36.     {  
  37.         switch(c)  
  38.         {  
  39.         case '%':  
  40.             ch = *++format;  
  41.             switch(ch)  
  42.             {  
  43.             case 'd':  
  44.                 {  
  45.                     int n = va_arg(ap, int);  
  46.                     myitoa(n, str, 10);  
  47.                     fputs(str, stdout);  
  48.                     break;  
  49.                 }  
  50.             case 'x':  
  51.                 {  
  52.                     int n = va_arg(ap, int);  
  53.                     myitoa(n, str, 16);  
  54.                     fputs(str, stdout);  
  55.                     break;  
  56.                 }  
  57.             case 'f':  
  58.                 {  
  59.                     double f = va_arg(ap, double);  
  60.                     int n;  
  61.                     n = f;  
  62.                     myitoa(n, str, 10);  
  63.                     fputs(str, stdout);  
  64.                     putchar('.');  
  65.                     n = (f - n) * 1000000;  
  66.                     myitoa(n, str, 10);  
  67.                     fputs(str, stdout);  
  68.                     break;  
  69.                 }  
  70.             case 'c':  
  71.                 {  
  72.                     putchar(va_arg(ap, int));  
  73.                     break;  
  74.                 }  
  75.             case 's':  
  76.                 {  
  77.                     char *p = va_arg(ap, char *);  
  78.                     fputs(p, stdout);  
  79.                     break;  
  80.                 }  
  81.             case '%':  
  82.                 {  
  83.                     putchar('%');  
  84.                     break;  
  85.                 }  
  86.             default:  
  87.                 {  
  88.                     fputs("format invalid!", stdout);  
  89.                     break;  
  90.                 }  
  91.             }  
  92.             break;  
  93.         default:  
  94.             putchar(c);  
  95.             break;  
  96.         }  
  97.         format++;  
  98.     }  
  99.     va_end(ap);  
  100. }  
  101.   
  102. int main(void)  
  103. {  
  104.     myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B', "hello");  
  105.     return 0;  
  106. }  
 
目录
相关文章
|
2月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
234 15
|
8月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
367 23
|
7月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
175 1
一文彻底搞清楚C语言的函数
|
8月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
303 15
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
|
8月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
145 24
|
8月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
360 16
|
8月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
196 3
|
8月前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
167 2
|
8月前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
141 1
|
9月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
340 10