c语言可变参数函数宽字节实现,面试题目---C语言可变参数函数的实现

这道题的关键是理解函数参数的压栈顺序:

1、函数的参数是从右到左压栈;

2、栈的生长方向是从上到下(即从高地址往低地址)

3、printf(char *fmt,...) fmt 是放在栈顶(最低地址),第二个参数是高一个地址,

4、第二个参数的获取方法是 (char *)&fmt +

sizeof(fmt) ,这个就是第二个参数的地址,第二个参数是什么类型就用什么类型取。

void myprintf(int fmt,...)

{

int

a,b,c,d;

char *p =

NULL;

a =

fmt;

p = (char

*)&fmt + sizeof(int);

b = *(int

*)p;

printf("addr1:[%u] addr2:[%u].\n",&fmt,p);

printf("a:[%u] b:[%u].\n",a,b);

return

;

}

int main()

{

myprintf(1,2,3,4);

return 0;

}

一、什么是可变参数

我们在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函数

先看例子程序。该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定.

在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值.

函数代码如下:

//示例代码1:可变参数函数的使用#i nclude "stdio.h"

#i nclude

"stdarg.h"

void simple_va_fun(int start, ...)

{

va_list

arg_ptr;

int

nArgValue =start;

int

nArgCout=0; //可变参数的数目 va_start(arg_ptr,start);

//以固定参数的地址为起点确定变参的内存起始地址。 do

{

++nArgCout;

printf("the

%d th arg: % d\n",nArgCout,nArgValue); //输出各参数的值 nArgValue

= va_arg(arg_ptr,int); //得到下一个可变参数的值 }

while(nArgValue

!= -1); return;

}

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

{

simple_va_fun(100,-1);

simple_va_fun(100,200,-1);

return

0;

}

下面解释一下这些代码

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

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

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安装目录下的\vc98\

include\stdarg.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指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。

这里要知道两个事情:

[1]在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函数的实现,参考了

Language>中的156页的例子

#i nclude

"stdio.h"

#i nclude

"stdlib.h"

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

类型{

char*

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

c;

pArg

= (char*) &fmt;//注意不要写成p = fmt

!!因为这里要对//参数取址,而不是取值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: GBK和UTF-8是两种不同的字符编码方式,GBK是中国的编码方式,而UTF-8是一种国际通用的编码方式。在实际开发过程中,由于不同平台和程序使用的编码方式不同,会出现需要进行GBK和UTF-8之间的互转的情况。 在C语言中,实现GBK和UTF-8的互转,可以使用iconv函数。iconv函数是一个系统函数,用于进行字符编码的转换。在进行转换的时候,需要提供源编码和标编码,同时也需要传入需要进行转换的字符串和字符串的长度。 具体的实现过程如下: 1. 获取需要转换的字符串和字符串的长度。 2. 定义iconv_t类型的变量,用于存储转换方式。 3. 调用iconv_open函数,打开转换方式,获取iconv_t类型的变量。 4. 定义一个用于存储转换后字符串的char数组和数组长度。 5. 调用iconv函数,将源编码的字符串转换为标编码的字符串。 6. 关闭iconv_t类型的变量。 7. 返回转换后的字符串。 下面是一个简单的示例代码: ``` #include <iconv.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char gbk_str[100] = "这是一个GBK编码的字符串"; char utf8_str[100] = {0}; char *inbuf = gbk_str; char *outbuf = utf8_str; size_t inlen = strlen(gbk_str); size_t outlen = strlen(utf8_str); iconv_t ic = iconv_open("utf-8","gbk"); if(ic == (iconv_t)-1) { perror("iconv_open"); exit(1); } int ret = iconv(ic, &inbuf, &inlen, &outbuf, &outlen); if(ret == -1) { perror("iconv"); exit(1); } printf("gbk_str: %s\nutf8_str: %s\n", gbk_str, utf8_str); iconv_close(ic); return 0; } ``` 这段代码将一个GBK编码的字符串转换为UTF-8编码的字符串,最终输出转换后的字符串。可以看到,使用iconv函数可以非常方便地进行编码的转换。 ### 回答2: GBK和UTF-8是两种常见的字符编码方式,它们的字节长度和编码规则不同。如何进行互转呢?我们可以通过C语言实现。 首先,需要了解GBK和UTF-8编码的规则。GBK是双字节编码,每个字符占用2个字节,而UTF-8是变长编码,每个字符的字节长度不同,最长可达4个字节。 其次,需要用C语言写出转换函数。以将GBK转为UTF-8为例,可按如下步骤进行: 1. 定义两个指针,一个指向源字符串GBK,一个指向标字符串UTF-8。 2. 使用循环遍历GBK字符串中的每个字符。 3. 如果字符的高位为0,表示该字符为ASCII字符,将该字符直接复制到UTF-8字符串中。 4. 如果字符的高位为1,表示该字符为汉字或其他非ASCII字符,需要将该字符转为UTF-8编码。 5. 将该字符的二进制形式转为Unicode编码,再根据以下规则将Unicode编码转为UTF-8编码: - 对于1字节的UTF-8编码,Unicode编码的范围为U+0000~U+007F; - 对于2字节的UTF-8编码,Unicode编码的范围为U+0080~U+07FF; - 对于3字节的UTF-8编码,Unicode编码的范围为U+0800~U+FFFF; - 对于4字节的UTF-8编码,Unicode编码的范围为U+10000~U+10FFFF。 6. 将得到的UTF-8编码复制到标字符串UTF-8中,并移动标字符串指针。 7. 循环结束后,在标字符串UTF-8的末尾添加一个\0字符,表示字符串的结束。 最后,我们需要注意转换时可能会出现一些错误,如GBK字符串中包含非法字符等。因此,在实现时需要对这些情况进行判断和处理。 总的来说,通过以上步骤,我们就能够将GBK字符串转为UTF-8编码的字符串了。同样的,我们也可以实现将UTF-8字符串转为GBK编码的函数。 ### 回答3: GBK与UTF-8是两种编码格式,GBK适用于汉字编码,UTF-8适用于多语言编码。想要在C语言中进行互转需要先了解一些基本操作。 UTF-8编码格式使用变长字节,一个字符可以由1-4个字节组成,其中第一个字节有特殊标记来表示后面有几个字节是该字符的一部分。而GBK编码格式每个汉字占两个字节。 在C语言中可以使用stdlib.h库函数中的mbstowcs()和wcstombs()来进行编码转换。mbstowcs()函数可以将一个字符串转换为字符数组,wcstombs()函数可以将字符数组转换为字符串。 将GBK编码格式的字符串转换为UTF-8格式需要进行如下操作: 1. 使用mbstowcs()函数将GBK字符串转换为字符数组。 2. 遍历字符数组,使用wcstombs()函数将每个字符(即一个汉字或一个英文字母)转换为UTF-8格式的字节。 3. 将所有转换后的字节组合成一个字符串即为UTF-8格式的字符串。 将UTF-8编码格式的字符串转换为GBK格式也需要进行类似的操作: 1. 使用mbstowcs()函数将UTF-8字符串转换为字符数组。 2. 遍历字符数组,使用wcstombs()函数将每个字符(即一个汉字或一个英文字母)转换为GBK格式的字节。 3. 将所有转换后的字节组合成一个字符串即为GBK格式的字符串。 需要注意的是,在进行编码转换时可能会出现一些字符无法转换的问题,处理方式可以选择忽略该字符或将该字符替换为一个特定字符。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值