天天看點

通過序列槽實作printf和scanf函數

在做裸闆開發時,常常需要通過輸出或者通過序列槽輸入一些資訊。

在有作業系統機器上,我們很少關心輸入和輸出的問題。因為有很多現成的庫函數供我們調用。在做裸闆開發時,可沒有現成庫函數供我們調用,一切都需要我們自己實作。

下面我們通過序列槽在裸闆上實作一個printf和scanf函數。

printf主要用來進行格式化輸出,scanf函數主要用來進行格式化輸入的。這裡個函數都是不定參數函數,這裡簡單介紹一下不定參函數實作方法。

一、不定參數的造型

function(type  arg,...);

一般第一個參數arg指定參數的個數,

int function(3,arg1,arg2,arg3);

這是一種顯示的告訴編譯器有幾個參數,第一個參數3,即代表後面有三個參數。

但也不是唯一的,比如printf的第一個參數是一個字元串,它是通過這個參數來确定有幾個參數的.

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

例如我們調用的時候printf("%d,%s,%d",i,str,j);

第一個參數是"%d,%s,%d",通過分析它我們可以知道有幾個

二、函數的參數壓棧

1. 固定的參數函數調用過程

通過序列槽實作printf和scanf函數

一般函數參數入棧是按照從右向左的順序入棧。這樣第一個參數arg1就放在了棧頂的位置。

2.變參函數調用過程

通過序列槽實作printf和scanf函數

通過上面的參數入棧方式我們可以得到如下結論:

如果想将棧中的參數讀出來,我們隻需要知道,棧頂元素的位址即第一個參數的位址即可。通過前面變參函數的分析,通過變參函數第一個參數可以知道傳遞的參數個數。根據參數入棧的順序,我們可以知道第一個參數是放在棧頂位置的。

總結一下,現在我們已經獲得兩個條件了:

1.棧中參數的個數

2.棧頂元素的位址

有了這兩個條件,我們就可以從棧中讀取我們想要的參數了。

當然,每個參數都有自己的類型,還有的就是位元組對齊了。在讀取參數的時候,這些問題都必須考慮到。

幸運的是這些問題在已經被大牛們解決了,已經封裝成相應的宏,我們在操作的時候隻需要知道這些宏的含義即可。

三、變參函數常用宏

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)

這些宏在不同的作業系統,有不同的實作,想使用的話,隻需要包含頭檔案stdarg.h就可以了。

(1)va_start宏的作用 : 

v是第一個參數,通過前面我們知道,第一個參數就是用來表明有幾個參數,它不是我們實際需要的參數。我們通過它來計算出,第一個實際參數的位址,主意哦是實際參數,可不是第一個表明參數個數的參數位址,讓ap指針變量儲存。

(1)va_arg宏的作用:

通過va_start,我們的ap的指針已經指向了第一個實際參數。

可以看到的是ap指針先更新了,然後又減了一個值,最終把這個值傳回。這裡面的t代表即将獲得參數的類型。

可以看出,通過va_arg宏我們獲得每個實際參數的值。

(2)va_end宏的作用

将ap指針指派為NULL,即0

下面我們自己寫一個測試程式來看一下,這些宏怎麼使用。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <stdarg.h>
  4. int my_printf(char *fmt,...)
  5. {
  6.     char *p;
  7.     va_list ap;
  8.     //獲得第一個實際參數的起始位址
  9.     va_start(ap,fmt);
  10.     //分析fmt指向的字元串
  11.     for(p = fmt; *p;p ++)
  12.     {
  13.         if(*p == '%')
  14.         {
  15.             p ++;
  16.             switch(*p)
  17.             {
  18.             //整形十進制數
  19.             case 'd':
  20.                 printf("%d",va_arg(ap,int));
  21.                 break;
  22.             //字元
  23.             case 'c':
  24.                 //變參傳遞char類型變量時,編譯器在
  25.                 //編譯的時候将其提升為int類型
  26.                 printf("%c",va_arg(ap,int));
  27.                 break;
  28.             //字元串
  29.             case 's':
  30.                 //位址占用4個位元組
  31.                 printf("%s",(char *)va_arg(ap,int));
  32.                 break;
  33.             //浮點數
  34.             case 'f':
  35.                 //變參傳遞float類型變量時,編譯器在
  36.                 //編譯的時候将其提升為double類型
  37.                 printf("%f",va_arg(ap,double));
  38.                 break;
  39.             // %
  40.             case '%':
  41.                 putchar('%');
  42.                 break;
  43.             }
  44.         }else{
  45.             putchar(*p);
  46.         }
  47.     }
  48.     //将ap指派為NULL
  49.     va_end(ap);
  50.     return 0;
  51. }
  52. int main(int argc, const char *argv[])
  53. {
  54.     int a = 123;
  55.     char b = 'c';
  56.     float c = 12.38;
  57.     char buf[] = "hello my_printf";
  58.     my_printf("a = %d b = %c buf = %s c = %f.n",a,b,buf,c);
  59.     return 0;
  60. }

實際上,格式化的轉換有現成的函數可以調用,例如:vsprintf()和vsscanf()這些函數的源代碼可以從bootloader和核心源碼上獲得。

四、常用格式轉換函數

 int vsprintf(char *str, const char *format, va_list ap);

這個函數的功能,就是把輸入的格式字元串進行解釋,把解釋好的字元串放在str。這個函數的源碼可以直接在核心中獲得。

 int vsscanf(const char *str, const char *format, va_list ap);

str中是我們從鍵盤上輸入的一些字元串,format是我們調用scanf的時候輸入的格式串。通過這些資訊,vsscanf函數解釋出每個變量應該賦為什麼值。這個函數的源碼可以直接在核心中獲得。

有了這兩個函數後,我們就可以通過序列槽封裝自己的printf和scanf了。

(1)通過序列槽實作printf函數

  1. int printf(const char *fmt,...)
  2. {
  3.     int i = 0;
  4.     va_list args;
  5.     unsigned int n;
  6.     char buffer[1024];
  7.     va_start(args,fmt);
  8.     n = vsprintf(buffer,fmt,args);
  9.     va_end(args);
  10.     //初始化序列槽
  11.     void uart_0_init();
  12.     for(i = 0;i < n;i ++)
  13.     {
  14.                 //通過序列槽發送字元
  15.         send_char(buffer[i]);
  16.     }
  17.     return n;
  18. }

(2)通過序列槽實作scanf函數

  1. int scanf(const char *fmt,...)
  2. {
  3.     int i = 0;
  4.     unsigned char c;
  5.     va_list args;
  6.     char buffer[1024];
  7.     //初始化序列槽
  8.     void uart_0_init();
  9.     while(1)
  10.     {
  11.                 //從序列槽接收字元
  12.         c = recv_char();
  13.         send_char(c);
  14.         if((c == 0x0d) || (c == 0x0a))
  15.         {
  16.             buffer[i] = '';
  17.             break;
  18.         }else{
  19.             buffer[i++] = c;
  20.         }
  21.     }
  22.     va_start(args,fmt);
  23.     i = vsscanf(buffer,fmt,args);
  24.     va_end(args);
  25.     send_char('r');
  26.     send_char('n');
  27.     return i;
  28. }

原博文位址:http://blog.chinaunix.net/uid-26833883-id-3757915.html

繼續閱讀