今天我们来探究一下c语言之可变长参数的秘密。我相信看了这篇文章后,c语言的可变长参数就不再有什么秘密。
一、函数参数的入栈
1.1 函数的参数如何传递
在c语言的函数调用中,有些函数需要参数的传入。编译器是如何实现的呢?首先在函数的调用时,会为即将调用的函数准备一个栈,然后将输入参数按照实参的顺序从右到左依次压入栈中了。
例如:
void func(arg0, arg1, arg2, arg3, arg3, arg4)
程序会依次将arg4, arg3, arg2, arg1, arg0压入栈中,程序运行栈如下:
---------栈底------
[arg4]
[arg3]
[arg2]
[arg1]
[arg0]
---------栈顶------
1.2 特殊符号 ...
编译器在c源码的编译中遇到了 ... 这种特殊符号时,编译器会检查在... 符号前面是否至少 有一个常规参数(普通形参)。如果没有,源码在编译中就会报错。除此之外,编译器也会生成进行输入参数压栈常规操作。
正确实例:
int func1(int a, ...)
int func2(const char* f, ...)
int func3(int a, int* b, const char* f, ...)
错误实例:
int func5(...)
int func5(int a, const char* f, ..., int b)
1.3 定位参数
用来定位可变长参数列表开始的地址。例如func1 中,在变量a中偏移4个字节就是可变长参数列表开始的地址。 func3中b就是可变长度定位参数。
二、如何取出函数参数列表
2.1 宏解读
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 宏的作用就是为了4(siezof(int))字节对齐。假定sizeof(n)等于7,(7+(4-1))&3 = 8,原本7个字节被对齐成 8个字节。
va_start 有2个输入参数,一个临时变量ap 用来存储指针,一个v 是起始定位实参变量。va_start作用就是根据定位参数,找到变长度参数列表的开始地址,并赋值给ap。
va_arg 有2个输入参数,ap是当前参数列表指针,t是当前参数的类型。作用是先根据类型取出当前参数的值,并把指针到下一个参数的地址,并赋值改ap。
代码实现上是,ap+=_INTSIZEOF(t)先将ap指针偏移到下一个参数开始的地址,然后 -_INTSIZEOF(t), 进行地址回退回来进行去取值。但回退后的地址并没有取改变ap。
va_end 作用是将临时变量ap 指针复位为0,防止非法访问。
三、如何使用可变长度参数
3.1 c源码使用
问题1:实现printf函数
/*(转载)
* A simple printf function. Only support the following format:
* Code Format
* %c character
* %d signed integers
* %i signed integers
* %s a string of characters
* %o octal
* %x unsigned hexadecimal
*/
int my_printf( const char* format, ...)
{
va_list arg;
int done = 0;
va_start (arg, format);
while( *format != '\0')
{
if( *format == '%')
{
if( *(format+1) == 'c' )
{
char c = (char)va_arg(arg, int);
putc(c, stdout);
} else if( *(format+1) == 'd' || *(format+1) == 'i')
{
char store[20];
int i = va_arg(arg, int);
char* str = store;
itoa(i, store, 10);
while( *str != '\0') putc(*str++, stdout);
} else if( *(format+1) == 'o')
{
char store[20];
int i = va_arg(arg, int);
char* str = store;
itoa(i, store, 8);
while( *str != '\0') putc(*str++, stdout);
} else if( *(format+1) == 'x')
{
char store[20];
int i = va_arg(arg, int);
char* str = store;
itoa(i, store, 16);
while( *str != '\0') putc(*str++, stdout);
} else if( *(format+1) == 's' )
{
char* str = va_arg(arg, char*);
while( *str != '\0') putc(*str++, stdout);
}
// Skip this two characters.
format += 2;
} else {
putc(*format++, stdout);
}
}
va_end (arg);
return done;
}
3.2 可变长参数宏 __VA_ARGS__
当我们掌握了可变参数的低层原理后,我们再来看一下可变长参数宏。特殊符号...在宏中使用时的展开规则,我们再下面的实例中探索。
实例1:
#include<stdio.h>
#define PRINT(...) printf(...)
int main(int argc, char** argv)
{
PRINT("%d, %d, %s", 1, 2, "hello world");
return 0;
}
gcc -E hello.c -o h.e 预编译后的代码
int main(int argc, char** argv)
{
printf(...);
return 0;
}
gcc hello.c编译报错。
hello.c: In function ‘main’:
hello.c:4:27: error: expected expression before ‘...’ token
#define PRINT(...) printf(...)
^
hello.c:8:2: note: in expansion of macro ‘PRINT’
PRINT("%d, %d, %s", 1, 2, "hello world");
实例2:
#include<stdio.h>
#define PRINT(...) printf(__VA_ARGS__)
int main(int argc, char** argv)
{
PRINT("%d, %d, %s", 1, 2, "hello world");
return 0;
}
gcc -E hello.c -o h.e 预编译后的代码
int main(int argc, char** argv)
{
printf("%d, %d, %s", 1, 2, "hello world");
return 0;
}
编译正常,运行符合预期。实例3:
#include<stdio.h>
#define PRINT(f, ...) printf(f, __VA_ARGS__)
int main(int argc, char** argv)
{
PRINT("%d, %d, %s", 1, 2, "hello world");
return 0;
}
编译正常,运行符合预期。
总结:...在宏中使用时,与__VA_ARGS__配套使用展开,当 不与__VA_ARGS__配套使用时,不展开。在宏中不对...进行语法检查,
展开后进行的。
3.3 调试使用宏
#include < stdarg.h>
#define PRINT(f, ...) printf("[%s:%d] "f"\n", __FUNCTION__, __LINE__, ##__VA_ARGS__)
本文揭示了C语言中可变长参数的秘密,包括函数参数的入栈过程、...符号的使用规则、定位参数的概念,以及如何通过宏来取出和使用函数参数列表。通过对va_start、va_arg和va_end等宏的解读,详细阐述了实现printf等功能的底层原理。

15万+

被折叠的 条评论
为什么被折叠?



