日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

参数控制c语言代码走向,C语言可变参数完全解读

發(fā)布時(shí)間:2024/9/19 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 参数控制c语言代码走向,C语言可变参数完全解读 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文轉(zhuǎn)自:http://www.cnblogs.com/wangyonghui/archive/2010/07/12/1776068.html

一、是什么

我們學(xué)習(xí)C語言時(shí)最經(jīng)常使用printf()函數(shù),但我們很少了解其原型。其實(shí)printf()的參數(shù)就是可變參數(shù),想想看,我們可以利用它打印出各種類型的數(shù)據(jù)。下面我們來看看它的原型:

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

它的第一個(gè)參數(shù)是format,屬于固定參數(shù),后面跟的參數(shù)的個(gè)數(shù)和類型都是可變的(用三個(gè)點(diǎn)“…”做參數(shù)占位符),實(shí)際調(diào)用時(shí)可以有以下的形式:

printf("%d",i);

printf("%s",s);

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

那么它的原型是怎樣實(shí)現(xiàn)的呢?我今天在看內(nèi)核代碼時(shí)碰到了vsprintf,花了大半天時(shí)間,終于把它搞的有點(diǎn)明白了。

二、先看兩個(gè)例子

不必弄懂,先大致了解其用法,繼續(xù)往下看。

①一個(gè)簡單的可變參數(shù)的C函數(shù)

在函數(shù)simple_va_fun參數(shù)列表中至少有一個(gè)整數(shù)參數(shù),其后是占位符…表示后面參數(shù)的個(gè)數(shù)不定.。在這個(gè)例子里,所有輸入?yún)?shù)必須都是整數(shù),函數(shù)的功能只是打印所有參數(shù)的值。

#include

#include

void simple_va_fun(int start, ...)

{

va_list arg_ptr;

int nArgValue =start;

int nArgCout=0;???? //可變參數(shù)的數(shù)目

va_start(arg_ptr,start); //以固定參數(shù)的地址為起點(diǎn)確定變參的內(nèi)存起始地址。

do

{

++nArgCout;

printf("the %d th arg: %d\n",nArgCout,nArgValue);???? //輸出各參數(shù)的值

nArgValue = va_arg(arg_ptr,int);????????????????????? //得到下一個(gè)可變參數(shù)的值

} while(nArgValue != -1);

return;

}

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

{

simple_va_fun(100,-1);

simple_va_fun(100,200,-1);

return 0;

}

②格式化到一個(gè)文件流,可用于日志文件

FILE *logfile;

int WriteLog(const char * format, ...)

{

va_list arg_ptr;

va_start(arg_ptr, format);

int nWrittenBytes = vfprintf(logfile, format, arg_ptr);

va_end(arg_ptr);

return nWrittenBytes;

}

稍作解釋上面兩個(gè)例子。

從這個(gè)函數(shù)的實(shí)現(xiàn)可以看到,我們使用可變參數(shù)應(yīng)該有以下步驟:

⑴在程序中用到了以下這些宏:

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(可變參數(shù))的意思.

這些宏定義在stdarg.h中,所以用到可變參數(shù)的程序應(yīng)該包含這個(gè)頭文件.

⑵函數(shù)里首先定義一個(gè)va_list型的變量,這里是arg_ptr,這個(gè)變量是存儲參數(shù)地址的指針.因?yàn)榈玫絽?shù)的地址之后,再結(jié)合參數(shù)的類型,才能得到參數(shù)的值。

⑶然后用va_start宏初始化⑵中定義的變量arg_ptr,這個(gè)宏的第二個(gè)參數(shù)是可變參數(shù)列表的前一個(gè)參數(shù),即最后一個(gè)固定參數(shù).

⑷然后依次用va_arg宏使arg_ptr返回可變參數(shù)的地址,得到這個(gè)地址之后,結(jié)合參數(shù)的類型,就可以得到參數(shù)的值。

⑸設(shè)定結(jié)束條件,①是判斷參數(shù)值是否為-1。注意被調(diào)的函數(shù)在調(diào)用時(shí)是不知道可變參數(shù)的正確數(shù)目的,程序員必須自己在代碼中指明結(jié)束條件。②是調(diào)用宏va_end。

三、剖析可變參數(shù)真相

1. va_* 宏定義

我們已經(jīng)知道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 )

再來看看linux中的定義

typedef char *va_list;

#define __va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

#define va_start(AP, LASTARG) (AP=((char*)&(LASTARG) + __va_rounded_size (LASTARG))

void va_end (va_list);

#define va_end(AP) (AP= (char *)0)

#define va_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),\

*((TYPE *)(AP - __va_rounded_size (TYPE))))

要理解上面這些宏定義的意思,需要首先了解:

①棧的方向②參數(shù)的入棧順序③CPU的對齊方式④內(nèi)存地址的表達(dá)方式。

2.棧——以Intel 32位的CPU為分析基礎(chǔ)

在Intel CPU中,棧的生長方向是向下的,即棧底在高地址,而棧頂在低地址;從棧底向棧頂看過去,地址是從高地址走向低地址的,因?yàn)榉Q它為向下生長,如圖。

從上面壓棧前后的兩個(gè)圖可明顯看到棧的生長方向,在Intel 32位的CPU中,windown或linux都使用了它的保護(hù)模式,ss指定棧所有在的段,ebp指向棧基址,esp指向棧頂。顯然執(zhí)行push指令后,esp的值會減4,而pop后,esp值增加4。 棧中每個(gè)元素存放空間的大小決定push或pop指令后esp值增減和幅度。Intel 32位CPU中的棧元素大小為16位或32位,由定義堆棧段時(shí)定義。在Window和Linux系統(tǒng)中,內(nèi)核代碼已定義好棧元素的大小為32位,即一個(gè)字長(sizeof(int))。因此用戶空間程棧元素的大小肯定為32位,這樣每個(gè)棧元素的地址向4字節(jié)對齊。

C語言的函數(shù)調(diào)用約定對編寫可變參數(shù)函數(shù)是非常重要的,只有清楚了,才更欲心所欲地控制程序。在高級程序設(shè)計(jì)語言中,函數(shù)調(diào)用約定有如下幾種,stdcall,cdecl,fastcall ,thiscal,naked call。cdel是C語言中的標(biāo)準(zhǔn)調(diào)用約定,如果在定義函數(shù)中不指明調(diào)用約定(在函數(shù)名前加上約定名稱即可),那編譯器認(rèn)為是cdel約定,從上面的幾種約定來看,只有cdel約定才可以定義可變參數(shù)函數(shù)。下面是cdel約定的重要特征:如果函數(shù)A調(diào)用函數(shù)B,那么稱函數(shù)A為調(diào)用者(caller),函數(shù)B稱為被調(diào)用者(callee)。caller把向callee傳遞的參數(shù)存放在棧中,并且壓棧順序按參數(shù)列表中從右向左的順序;callee不負(fù)責(zé)清理?xiàng)?#xff0c;而是由caller清理。 我們用一個(gè)簡單的例子來說明問題,并采用Nasm的匯編格式寫相應(yīng)的匯編代碼,程序段如下:

void callee(int a, int b)

{

int c = 0;

c = a +b;

}

void caller()

{

callee(1,2);

}

來分析一下在調(diào)用過程發(fā)生了什么事情。程序執(zhí)行點(diǎn)來到caller時(shí),那將要執(zhí)行調(diào)用callee函數(shù),在跳到callee函數(shù)前,它先要把傳遞的參數(shù)壓到棧上,并按右到左的順序,即翻譯成匯編指令就是push 2; push 1;

圖2

函數(shù)棧如圖中(a)所示。接著跳到callee函數(shù),即指令call calle。CPU在執(zhí)行call時(shí),先把當(dāng)前的EIP寄存器的值壓到棧中,然后把EIP值設(shè)為callee(地址),這樣,棧的圖變?yōu)槿鐖D2(b)。程序執(zhí)行點(diǎn)跳到了callee函數(shù)的第一條指令。C語言在函數(shù)調(diào)用時(shí),每個(gè)函數(shù)占用的棧段稱為stack frame。用ebp來記住函數(shù)stack frame的起始地址。故在執(zhí)行callee時(shí),最前的兩條指令為:

push ebp

mov esp, ebp

經(jīng)過這兩條語句后,callee函數(shù)的stack frame就建好了,棧的最新情況如圖2(c)所示。 函數(shù)callee定義了一個(gè)局部變量int c,該變量的儲存空間分配在callee函數(shù)占用的棧中,大小為4字節(jié)(insizeof int)。那么callee會在如下指令:

sub esp, 4

mov [ebp-4], 0

這樣棧的情況又發(fā)生了變化,最新情況如圖2(d)所示。注意esp總是指向棧頂,而ebp作為函數(shù)的stack frame基址起到很大的作用。ebp地址向下的空間用于存放局部變量,而它向上的空間存放的是caller傳遞過來的參數(shù),當(dāng)然編譯器會記住變量c相對ebp的地址偏移量,在這里為-4。跟著執(zhí)行c = a + b語句,那么指令代碼應(yīng)該類似于:

mov eax , [ebp +? 8] ;這里用eax存放第一個(gè)傳遞進(jìn)來的參數(shù),記住第一個(gè)參數(shù)與ebp的偏移量肯定為8

add eax,? [ebp + 12] ;第二個(gè)參數(shù)與ebp的偏移量為12,故計(jì)算eax = a+b

mov [ebp -4], eax? ;執(zhí)行 c = eax, 即c = a+b

棧又有了新了變化,如圖2(e)。至此,函數(shù)callee的計(jì)算指令執(zhí)行完畢,但還要做一些事情:釋放局部變量占用的棧空間,銷除函數(shù)的stack-frame過程會生成如下指令:

mov esp, ebp;把局部變量占用的空間全部略過,即不再使用,ebp以下的空間全部用于局部變量

pop ebp;彈出caller函數(shù)的stack-frame 基址

在Intel CPU里上面兩條指令可以用指令leave來代替,功能是一樣。這樣棧的內(nèi)容如圖2(f)所示。最后,要返回到caller函數(shù),因此callee的最后一條指令是

ret

ret指令用于把棧上的保存的斷點(diǎn)彈出到EIP寄存器,新的棧內(nèi)容如圖2(g)所示。函數(shù)callee的調(diào)用與返回全部結(jié)束,跟著下來是執(zhí)行call callee的下一條語句。

從caller函數(shù)調(diào)用callee前,把傳遞的參數(shù)壓到棧中,并且按從右到左的順序;函數(shù)返回時(shí),callee并不清理?xiàng)?#xff0c;而是由caller清楚傳遞參數(shù)所占用的棧(如上圖,函數(shù)返回時(shí),1和2還放在棧中,讓caller清理)。棧元素的大小為4個(gè)字節(jié),每個(gè)參數(shù)占用棧空間大小為4字節(jié)的倍數(shù),并且任何兩個(gè)參數(shù)都不能共用同一個(gè)棧元素。

從C語言的函數(shù)調(diào)用約定可知,參數(shù)列表從右向左依次壓棧,故可變參數(shù)壓在棧的地址比最后一個(gè)命名參數(shù)還大,如下圖3所示:

由圖3可知,最后一個(gè)命名參數(shù)a上面都放著可變參數(shù),每個(gè)參數(shù)占用棧的大小必為4的倍數(shù)。因此:可變參數(shù)1的地址 = 參數(shù)a的地址 + a占用棧的大小,可變參數(shù)2的地址 = 可變參數(shù)1的地址 + 可變參數(shù)1占用棧的大小,可變參數(shù)3的地址 = 可變參數(shù)2的地址 + 可變參數(shù)2占用棧的大小,依此類推。如何計(jì)算每個(gè)參數(shù)占用棧的大小呢?

3.數(shù)據(jù)對齊問題

對于兩個(gè)正整數(shù) x, n 總存在整數(shù) q, r 使得

x = nq + r, 其中? 0<= r

q, r 是唯一確定的。q = [x/n], r = x - n[x/n]. 這個(gè)是帶余除法的一個(gè)簡單形式。在 c 語言中, q, r 容易計(jì)算出來: q = x/n, r = x % n.

所謂把 x 按 n 對齊指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 這也相當(dāng)于把 x 表示為:

x = nq + r', 其中 -n < r' <=0??????????????? //最大非正剩余

nq 是我們所求。關(guān)鍵是如何用 c 語言計(jì)算它。由于我們能處理標(biāo)準(zhǔn)的帶余除法,所以可以把這個(gè)式子轉(zhuǎn)換成一個(gè)標(biāo)準(zhǔn)的帶余除法,然后加以處理:

x+n = qn + (n+r'),其中 0

x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1

所以 qn = [(x+n-1)/n]n. 用 c 語言計(jì)算就是:

((x+n-1)/n)*n

若 n 是 2 的方冪, 比如 2^m,則除為右移 m 位,乘為左移 m 位。所以把 x+n-1 的最低 m 個(gè)二進(jìn)制位清 0就可以了。得到:

(x+n-1) & (~(n-1))

根據(jù)這些推導(dǎo),相信已經(jīng)了解#define __va_rounded_size(TYPE)? (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))的涵義。

4.再看va_* 宏定義

va_start(va_list ap, last)

last為最后一個(gè)命名參數(shù),va_start宏使ap記錄下第一個(gè)可變參數(shù)的地址,原理與“可變參數(shù)1的地址 = 參數(shù)a的地址 + a占用棧的大小”相同。從ap記錄的內(nèi)存地址開始,認(rèn)為參數(shù)的數(shù)據(jù)類型為type并把它的值讀出來;把a(bǔ)p記錄的地址指向下一個(gè)參數(shù),即ap記錄的地址 += occupy_stack(type)

va_arg(va_lit ap, type)

這里是獲得可變參數(shù)的值,具體工作是:從ap所指向的棧內(nèi)存中讀取類型為type的參數(shù),并讓ap根據(jù)type的大小記錄它的下一個(gè)可變參數(shù)地址,便于再次使用va_arg宏。從ap記錄的內(nèi)存地址開始,認(rèn)為存的數(shù)據(jù)類型為type并把它的值讀出來;把a(bǔ)p記錄的地址指向下一個(gè)參數(shù),即ap記錄的地址 += occupy_stack(type)

va_end(va_list ap)

用于“釋放”ap變量,它與va_start對稱使用。在同一個(gè)函數(shù)內(nèi)有va_start必須有va_end。

5.可變參數(shù)函數(shù)問題

考慮了參數(shù)大小和數(shù)據(jù)對齊問題,使得可變參數(shù)的類型不但可以是基本類型,同樣適用于用戶定義類型。值的注意的是,如果是用戶定義類型,最好用typedef定義的名字作為類型名,這樣就會減少在va_arg進(jìn)行宏展開時(shí)出錯(cuò)的機(jī)率。

在可變參數(shù)函數(shù)中,由va_list變量來記錄(或獲得)可變參數(shù)部分,但是va_list中并沒有記錄下它們的名字,事實(shí)上也是不可能的。要想把可變參數(shù)部分傳遞給下一個(gè)函數(shù),唯有通過va_list變量去傳遞,而原來定義的函數(shù)用"..."來表示可變參數(shù)部分,而不是用va_list來表示。為了方便程序的標(biāo)準(zhǔn)化,ANSIC在標(biāo)準(zhǔn)庫代碼中就作出了很好的榜樣:在任何形如: type fun( type arg1, type arg2, ...)的函數(shù),都同時(shí)定義一個(gè)與它功能完全一樣的函數(shù),但用va_list類型來替換"...",即type fun(type arg1, type arg2, va_list ap)。以printf函數(shù)為例:

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

int vprintf(const char *format, va_list ap);

第一個(gè)函數(shù)用"..."表示可變參數(shù),第二個(gè)用va_list類型表示可變參數(shù),目的是用于被其它可變參數(shù)調(diào)用,兩者在功能功能上是完全上一樣。只是在函數(shù)名字相差一個(gè)'"v"字母。

四、可變參數(shù)函數(shù)的應(yīng)用

一個(gè)中的例子:一個(gè)簡單的實(shí)現(xiàn)printf函數(shù)的例子:

#include

#include

#include

/* minprintf: minimal printf with variable argument list */

void minprintf(char *fmt, ...)

{

va_list ap; /* points to each unnamed arg in turn */

char *p, *sval;

int ival;

double dval;

va_start(ap, fmt); /* make ap point to 1st unnamed arg */

for (p = fmt; *p; p++) {

if (*p != '%') {

putchar(*p);

continue;

}

switch (*++p) {

case 'd':

ival = va_arg(ap, int);

printf("%d", ival);

break;

case 'x':

ival=va_arg(ap,int);

printf("%#x",ival);

break;

case 'f':

dval = va_arg(ap, double);

printf("%f", dval);

break;

case 's':

for (sval = va_arg(ap, char *); *sval; sval++)

putchar(*sval);

break;

default:

putchar(*p);

break;

}

}

va_end(ap); /* clean up when done */

}

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

{

int i = 1234;

int j = 5678;

char *s="nihao";

double f=0.11f;

minprintf("the first test:i=%d\n",i,j);

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

minprintf("the 3rd test:s=%s\n",s);

minprintf("the 4th test:f=%f\n",f);

minprintf("the 5th test:s=%s,f=%f\n",s,f);

system("pause");

return 0;

}

不使用va_*宏定義的實(shí)現(xiàn):

void minprintf(char* fmt, ...) //一個(gè)簡單的類似于printf的實(shí)現(xiàn)不過參數(shù)必須都是int 類型

{

char* pArg=NULL;?????????????? //等價(jià)于原來的va_list

char c;

pArg = (char*) &fmt; //注意不要寫成p = fmt !因?yàn)檫@里要對//參數(shù)取址,而不是取值

pArg += sizeof(fmt);???????? //等價(jià)于原來的va_start

do

{

c =*fmt;

if (c != '%')

{

putchar(c);??????????? //照原樣輸出字符

}

else

{

//按格式字符輸出數(shù)據(jù)

switch(*++fmt)

{

case 'd':

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

break;

case 'x':

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

break;

default:

break;

}

pArg += sizeof(int);?????????????? //等價(jià)于原來的va_arg

}

++fmt;

}while (*fmt != '\0');

pArg = NULL;?????????????????????????????? //等價(jià)于va_end

return;

}

五、參考引用:

感謝以上?!

總結(jié)

以上是生活随笔為你收集整理的参数控制c语言代码走向,C语言可变参数完全解读的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。