標籤:

C語言如何封裝printf函數?

我現在想自己寫一個函數來簡單封裝一下printf函數,因為printf是可變參數的,因此我自己定義的函數MyPrintf函數也是可變參數的。但是怎樣將MyPrintf函數的各個參數順序傳入printf函數,使外界看來調用MyPrintf函數與庫函數printf效果一致呢?


有個接受 va_list 的版本 vprintf

你看下 stdarg.h 的用法吧


int your_printf(const char* format, ...)
{
va_list vp;
va_start(vp, format);
int result = vprintf(format, vp);
va_end(vp);
return resu< }

這基本上是唯一標準保證正確的方法


C語言參數從右到左進棧,棧的增長方式是從高地址往低地址,所以在棧空間裡面,參數排布非常整齊而有規律,從第一個參數開始,按參數大小緊密排列;當讀取可變參數的時候,一般來說一定至少有一個不可變的參數,取最後一個不可變參數的地址,加上這個參數的大小,就是下一個可變參數的起始地址,之後按參數的類型添加相應的位移,依次取出這些參數。這就是可變參數的原理。va_list等宏實際上也是這麼做的。

所以如果想要用自定義的可變參數來調用vprintf,可以自己分配一段空間,然後依次將想要傳入的可變參數填充到這個空間中,最後把這個空間的地址強制轉換為va_list然後傳給vprintf。在大多數架構上,va_list是個簡單的指針類型。

#include &
#include &

int call_printf(){
const char *mystr = "mystring";
char *my_params = malloc(64);
char *current_pos = my_params;
*(int*)current_pos = 1;
current_pos += sizeof(int);
*(const char**)current_pos = mystr;
current_pos += sizeof(const char*);
return vprintf("%d %s", (va_list)my_params);
}

用va_xxx的宏應該也是可以的

#include &
#include &

int call_printf(){
const char *mystr = "mystring";
char* my_params = malloc(64);
va_list current_pos;
int retv;
va_start(current_pos, *my_params);
va_arg(current_pos, int) = 1;
va_arg(current_pos, const char*) = mystr;
va_end(current_pos);
va_start(current_pos, *my_params);
retv = vprintf("%d %s", current_pos);
va_end(current_pos);
return retv;
}

不保證在其他奇奇怪怪的架構上也都能用

不過還是那句話,何必呢,正常情況下都不如對每個參數調用一次printf……


宏定義方式封裝:

#define TRACE(fmt, ...) printf(fmt, __VA_ARGS__)

函數方式封裝:

void printf_wrapperV(const char* format, va_list args_list)
{
vprintf(format, args_list);
}

void printf_wrapper(const char* format, ...)
{
va_list marker;
va_start(marker, format);
printf_wrapperV(format, marker);
va_end(marker);
}

學習關鍵字:va_list, _cdecl,為什麼printf只能用_cdecl調用約定


自己實現一個printf也不難,遵循_cdecl約定即可,printf就根據%的個數來確定參數個數.

簡單解釋下將用到的3個宏

va_list其實只是char*指針而已

va_start(ap,arg);一個參數是va_list變數,arg是第一個參數,指向第一個可變參數.

其實現簡易理解為

ap=format+sizeof(arg)

va_arg(ap,type),第一個參數是va_list變數,第二個參數是可變參數類型

其功能:返回當前ap指向的類型變數,並且指向下一個變數

*((type*)(ap+=sizeof(type)-sizeof(type));

#define _CRT_SECURE_NO_WARNINGS
#include&
#include&
using namespace std;
void myprint(const char *format, ...)
{
char buf[1024];
va_list my_format;
va_start(my_format, format);
char c;
char * pstr;
while ((c=*format++)!=NULL)
{
switch (c)
{
case "%":
c = *format++;
switch (c)
{
case "d":
_itoa(va_arg(my_format, int), buf, 10);
fputs(buf, stdout);
break;
case "x":
_itoa(va_arg(my_format, int), buf, 16);
fputs(buf, stdout);
break;
case "s":
pstr = va_arg(my_format, char*);
fputs(pstr, stdout);
break;
case "c":
putchar(va_arg(my_format, char));
break;
}
break;
default:
putchar(c);
}
}
}
int main()
{
myprint("這是整數%d,這是字元串%s,這是16進位%x,這是字元%c",100,"hello world",100,"a");
system("pause");
return 0;
}


那麼printf()如何確定變參的個數呢。

我猜測通過統計第一個參數(即parmN)(意即format參數)中的%d,%f等格式字元串的個數來確定參數列表的長度(其實是終止地址),如果格式化字元串中的個數少於變參個數,多的會讀不到。這樣的話可以自己實現一個printf函數了。建議看一看va_list,va_start()等宏的原型就清楚了。

parmN最主要的作用就是確定初始地址。

大多數情況va_list是char*類型,就是用來存地址的,所以應該可以直接對parmN copy一份地址,va_start然後重新訪問整個參數列表(好像多餘了)。


我假設你封裝是為了擴展功能,這裡有一個簡單的列印結構體的版本,可能對你有用。

struct test
{
int a;
int b;
};

void printsss(const char *fmt, ...)
{
int *arg_ptr = (int *)fmt + 1;

for(; *fmt; ++fmt) {
switch (*fmt) {
case "%": continue;
case "d":
printf("%d
", *arg_ptr);
arg_ptr++;
break;
case "f":
printf("%f
", *(double*)arg_ptr);
arg_ptr += 2;
break;
case "j":
printf("a: %d b: %d
", *arg_ptr, *(arg_ptr + 1));
arg_ptr += 2;
break;
default: break;
}
}
}

int main(int argc, char *argv[])
{
struct test t = { 22, 33 };
printsss("%d, %j", 1, t);
return 0;
}

? gcc -o printf printf.c -m32 -std=c99
? ./printf
1
a: 22 b: 33


推薦閱讀:

C語言結構體內部的函數指針有什麼意義?
一條C語言語句不一定是原子操作,但是一個彙編指令是原子操作嗎?
c語言函數是如何獲取傳入的數組(指針)的指針所指向內容的長度的,有辦法嗎?
C語言零基礎想要自學,有什麼書可以推薦一下嗎?
寫操作系統只能用彙編和 C 語言嗎?

TAG:C編程語言 |