C語(yǔ)言如何實(shí)現(xiàn)可變參數(shù)詳解
可變參數(shù)
可變參數(shù)是指函數(shù)的參數(shù)的數(shù)據(jù)類型和數(shù)量都是不固定的。
printf函數(shù)的參數(shù)就是可變的。這個(gè)函數(shù)的原型是:int printf(const char *format, ...)。
用一段代碼演示printf的用法。
// code-A #include <stdio.h> int main(int argc, char **argv) { printf("a is %d, str is %s, c is %c\n", 23, "Hello, World;", 'A'); printf("T is %d\n", 78); return 0; }
在code-A中,第一條printf語(yǔ)句有4個(gè)參數(shù),第二條printf語(yǔ)句有2個(gè)參數(shù)。顯然,printf的參數(shù)是可變的。
實(shí)現(xiàn)
代碼
code-A
先看兩段代碼,分別是code-A和code-B。
// file stack-demo.c #include <stdio.h> // int f(char *fmt, int a, char *str); int f(char *fmt, ...); int f2(char *fmt, void *next_arg); int main(int argc, char *argv) { char fmt[20] = "hello, world!"; int a = 10; char str[10] = "hi"; f(fmt, a, str); return 0; } // int f(char *fmt, int a, char *str) int f(char *fmt, ...) { char c = *fmt; void *next_arg = (void *)((char *)&fmt + 4); f2(fmt, next_arg); return 0; } int f2(char *fmt, void *next_arg) { printf(fmt); printf("a is %d\n", *((int *)next_arg)); printf("str is %s\n", *((char **)(next_arg + 4))); return 0; }
編譯執(zhí)行,結(jié)果如下:
# 編譯
[root@localhost c]# gcc -o stack-demo stack-demo.c -g -m32
# 反匯編并把匯編代碼寫入dis-stack.asm中
[root@localhost c]# objdump -d stack-demo>dis-stack.asm
[root@localhost c]# ./stack-demo
hello, world!a is 10
str is hi
code-B
// file stack-demo.c #include <stdio.h> // int f(char *fmt, int a, char *str); int f(char *fmt, ...); int f2(char *fmt, void *next_arg); int main(int argc, char *argv) { char fmt[20] = "hello, world!"; int a = 10; char str[10] = "hi"; char str2[10] = "hello"; f(fmt, a, str, str2); return 0; } // int f(char *fmt, int a, char *str) int f(char *fmt, ...) { char c = *fmt; void *next_arg = (void *)((char *)&fmt + 4); f2(fmt, next_arg); return 0; } int f2(char *fmt, void *next_arg) { printf(fmt); printf("a is %d\n", *((int *)next_arg)); printf("str is %s\n", *((char **)(next_arg + 4))); printf("str2 is %s\n", *((char **)(next_arg + 8))); return 0; }
編譯執(zhí)行,結(jié)果如下:
# 編譯
[root@localhost c]# gcc -o stack-demo stack-demo.c -g -m32
# 反匯編并把匯編代碼寫入dis-stack.asm中
[root@localhost c]# objdump -d stack-demo>dis-stack.asm
[root@localhost c]# ./stack-demo
hello, world!a is 10
str is hi
str2 is hello
分析
在code-A中,調(diào)用f的語(yǔ)句是f(fmt, a, str);;在code-B中,調(diào)用f的語(yǔ)句是f(fmt, a, str, str2);。
很容易看出,int f(char *fmt, ...);就是參數(shù)可變的函數(shù)。
關(guān)鍵語(yǔ)句
實(shí)現(xiàn)可變參數(shù)的關(guān)鍵語(yǔ)句是:
char c = *fmt; void *next_arg = (void *)((char *)&fmt + 4); printf("a is %d\n", *((int *)next_arg)); printf("str is %s\n", *((char **)(next_arg + 4))); printf("str2 is %s\n", *((char **)(next_arg + 8)));
- &fmt是第一個(gè)參數(shù)的內(nèi)存地址。
- next_arg是第二個(gè)參數(shù)的內(nèi)存地址。
- next_arg+4、next_arg+8分別是第三個(gè)、第四個(gè)參數(shù)的內(nèi)存地址。
為什么
內(nèi)存地址的計(jì)算方法
先看一段偽代碼。這段偽代碼是f函數(shù)的對(duì)應(yīng)的匯編代碼。假設(shè)f有三個(gè)參數(shù)。當(dāng)然f也可以有四個(gè)參數(shù)或2個(gè)參數(shù)。我們用三個(gè)參數(shù)的情況來(lái)觀察一下f。
f:
; 入棧ebp
; 把ebp設(shè)置為esp
; ebp + 0 存儲(chǔ)的是 eip,由call f入棧
; ebp + 4 存儲(chǔ)的是 舊ebp
; 第一個(gè)參數(shù)是 ebp + 8
; 第二個(gè)參數(shù)是 ebp + 12
; 第三個(gè)參數(shù)是 ebp + 16
; 函數(shù)f的邏輯
; 出棧ebp。ebp恢復(fù)成了剛進(jìn)入函數(shù)之前的舊ebp
; ret
調(diào)用f的偽代碼是:
; 入棧第三個(gè)參數(shù)
; 入棧第二個(gè)參數(shù)
; 入棧第一個(gè)參數(shù)
; 調(diào)用f,把eip入棧
在匯編代碼中,第一個(gè)參數(shù)的內(nèi)存地址很容易確定,第二個(gè)、第三個(gè)還有第N個(gè)參數(shù)的內(nèi)存地址也非常容易確定。無(wú)法是在ebp的基礎(chǔ)上增加特定長(zhǎng)度而已。
可是,我們只能確定,必定存在第一個(gè)參數(shù),不能確定是否存在的二個(gè)、第三個(gè)還有第N個(gè)參數(shù)。沒(méi)有理由使用一個(gè)可能不存在的參數(shù)作為參照物、并且還要用它卻計(jì)算其他參數(shù)的地址。
第一個(gè)參數(shù)必定存在,所以,我們用它作為確定其他參數(shù)的內(nèi)存地址的參照物。
內(nèi)存地址
在f函數(shù)的C代碼中,&fmt是第一個(gè)參數(shù)占用的f的棧的元素的內(nèi)存地址,換句話說(shuō),是一個(gè)局部變量的內(nèi)存地址。
局部變量的內(nèi)存地址不能作為函數(shù)的返回值,卻能夠在本函數(shù)執(zhí)行結(jié)束前使用,包括在本函數(shù)調(diào)用的其他函數(shù)中使用。這就是在f2中仍然能夠使用fmt計(jì)算出來(lái)的內(nèi)存地址的原因。
難點(diǎn)
當(dāng)參數(shù)是int類型時(shí),獲取參數(shù)的值使用*(int *)(next_arg)。
當(dāng)參數(shù)是char str[20]時(shí),獲取參數(shù)的值使用*(char **)(next_arg + 4)。
為什么不直接使用next_arg、(next_arg + 4)呢?
分析*(int *)(next_arg)。
在32位操作系統(tǒng)中,任何內(nèi)存地址的值看起來(lái)都是一個(gè)32位的正整數(shù)??墒沁@個(gè)正整數(shù)的值的類型并不是unsigned int,而是int *。
關(guān)于這點(diǎn),我們可以在gdb中使用ptype確認(rèn)一下。例如,有一小段代碼int *a;*a = 5;,執(zhí)行ptype a,結(jié)果會(huì)是int *。
next_arg只是一個(gè)正整數(shù),損失了它的數(shù)據(jù)類型,我們需要把數(shù)據(jù)類型補(bǔ)充進(jìn)來(lái)。我們能夠把這個(gè)操作理解成”強(qiáng)制類型轉(zhuǎn)換“。
至于*(int *)(next_arg)前面的*,很容易理解,獲取一個(gè)指針指向的內(nèi)存中的值。
用通用的方式分析*(char **)(next_arg+4)。
- 因?yàn)槭堑谌齻€(gè)參數(shù),因此next_arg+4。
- 因?yàn)榈谌齻€(gè)參數(shù)的數(shù)據(jù)類型是char str[20]。根據(jù)經(jīng)驗(yàn),char str[20]對(duì)應(yīng)的指針是char *。
- 因?yàn)閚ext_arg+4只是函數(shù)的棧的元素的內(nèi)存地址,在目標(biāo)元素中存儲(chǔ)的是一個(gè)指針。也就是說(shuō),next_arg+4是一個(gè)雙指針類型的指針。它最終又指向字符串,根據(jù)經(jīng)驗(yàn),next_arg+4的數(shù)據(jù)類型是char **。沒(méi)必要太糾結(jié)這一點(diǎn)。自己寫一個(gè)簡(jiǎn)單的指向字符串的雙指針,使用gdb的ptype查看這種類型的數(shù)據(jù)類型就能驗(yàn)證這一點(diǎn)。
- 最前面的*,獲取指針指向的數(shù)據(jù)。
給出一段驗(yàn)證第3點(diǎn)的代碼。
char str[20] = "hello"; char *ptr = str; // 使用gdb的ptype 打印 ptype &ptr
打印結(jié)果如下:
Breakpoint 1, main (argc=1, argv=0xffffd3f4) at point.c:13
13 char str7[20] = "hello";
(gdb) s
14 char *ptr = str7;
(gdb) s
19 int b = 7;
(gdb) p &str
$1 = (char **) 0xffffd2fc
優(yōu)化
在code-A和code-B中,我們?nèi)斯じ鶕?jù)參數(shù)的類型來(lái)獲取參數(shù),使用*(int *)(next_arg)或*(char **)(next_arg + 4)。
庫(kù)函數(shù)printf顯然不是人工識(shí)別參數(shù)的類型。
這個(gè)函數(shù)的第一個(gè)參數(shù)中包含%d、%x、%s等占位符。遍歷第一個(gè)參數(shù),識(shí)別出%d,就用*(int *)next_arg替換%d。識(shí)別出
%s,就用*(char **)next_arg。
實(shí)現(xiàn)了識(shí)別占位符并且根據(jù)占位符選擇指針類型的功能,就能實(shí)現(xiàn)一個(gè)完成度很高的可變參數(shù)了。
總結(jié)
到此這篇關(guān)于C語(yǔ)言如何實(shí)現(xiàn)可變參數(shù)的文章就介紹到這了,更多相關(guān)C語(yǔ)言可變參數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- C語(yǔ)言可變參數(shù)與函數(shù)參數(shù)的內(nèi)存對(duì)齊詳解
- C語(yǔ)言進(jìn)階可變參數(shù)列表
- C語(yǔ)言可變參數(shù)列表的用法與深度剖析
- C語(yǔ)言可變參數(shù)函數(shù)詳解
- C語(yǔ)言的可變參數(shù)函數(shù)實(shí)現(xiàn)詳解
- 詳解C語(yǔ)言中的動(dòng)態(tài)內(nèi)存管理
- 一文帶你搞懂C語(yǔ)言動(dòng)態(tài)內(nèi)存管理
- 深入了解C語(yǔ)言的動(dòng)態(tài)內(nèi)存管理
- C語(yǔ)言可變參數(shù)與內(nèi)存管理超詳細(xì)講解
相關(guān)文章
Qt自定義實(shí)現(xiàn)一個(gè)等待提示Ui控件
等待樣式控件是我們?cè)谧鯱I時(shí)出場(chǎng)率還挺高的控件之一,所以這篇文章主要為大家介紹了Qt如何自定義一個(gè)好看的等待提示Ui控件,感興趣的可以了解下2024-01-01使用pthread庫(kù)實(shí)現(xiàn)openssl多線程ssl服務(wù)端和客戶端
使用pthread庫(kù)實(shí)現(xiàn)openssl多線程ssl服務(wù)端和客戶端,大家參考使用吧2014-01-01關(guān)于VS2022不能使用<bits/stdc++.h>的解決方案(萬(wàn)能頭文件)
#include<bits/stdc++.h>包含了目前 C++ 所包含的所有頭文件,又稱萬(wàn)能頭文件,那么如何在VS2022中使用萬(wàn)能頭呢?下面小編給大家代理了關(guān)于VS2022不能使用<bits/stdc++.h>的解決方案(萬(wàn)能頭文件),感興趣的朋友一起看看吧2022-03-03C語(yǔ)言手寫多級(jí)時(shí)間輪定時(shí)器
這篇文章主要為大家詳細(xì)介紹了如何利用C語(yǔ)言實(shí)現(xiàn)手寫多級(jí)時(shí)間輪定時(shí)器,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下2022-09-09C++中用substr()函數(shù)消除前后空格的解決方法詳解
本篇文章是對(duì)C++中用substr()函數(shù)消除前后空格的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05C語(yǔ)言實(shí)現(xiàn)輸入ascii碼,輸出對(duì)應(yīng)的字符方式
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)輸入ascii碼,輸出對(duì)應(yīng)的字符方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01