程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言 內核printf源代碼分析

C語言 內核printf源代碼分析

編輯:關於C語言
 

打開Source Insight來閱讀EduOS的源代碼,我們在stdio.c裡找到了printf的實現代碼.首先看看對printf的定義:
  int printf (const char *cntrl_string , ... )
  第一個參數cntrl_string是控制字符串,也就是平常我們寫入%d,%f的地方.緊接著後面是一個變長參數.
  看看函數頭部的定義:
   int pos = 0 , cnt_printed_chars = 0 , i ;
   unsigned char * chptr ;
   va_list ap ;
  馬上暈!除了ap我們可以馬上判斷出來是用來讀取變長參數的,i用於循環變量.其他變量都不知道是怎麼回事.不要著急,我們邊看代碼邊分析.代碼的第一行必然是
  va_start (ap , cntrl_string );
  用來初始化變長參數.
  接下來是一個while循環
  while (cntrl_string [pos ])
  {...
   ...
  }
  結束條件是cntrl_string[pos]為NULL,顯然這個循環是用來遍歷整個控制字符串的.自然pos就是當前遍歷到的位置了.進入循環首先闖入視線的是
   if (cntrl_string [pos ] == '%')
  {...
   pos ++;
   ...
  }
  開門見山,上來就當前字符是否辦斷是否%.一猜就知道如果成立pos++馬上取出下一個字符在d,f,l等等之間進行判斷.往下一看,果真不出所料:
  switch (cntrl_string [pos ])
  {...
   case 'c':
   ... case 's':
   ... case 'i':
   ... case 'd':
   ... case 'u':
   ...
  用上switch-case了. 快速浏覽一下下面的代碼.
  首先看看case 'c'的部分
  case 'c':
   putchar (va_arg (ap , unsigned char ));
   cnt_printed_chars ++;
   break ;
  %c表示僅僅輸出一個字符.因此先通過va_arg進行參數的類型轉換,之後用putchar[1]輸出到屏幕上去.之後是
  cnt_printed_chars++,通過這句我們就可以判斷出cnt_printed_chars使用來表示,已經被printf輸出的字符個數的.
  再來看看 case 's':
   case 's':
   chptr = va_arg (ap , unsigned char *);
   i = 0 ;
   while (chptr [i ])
   {...
   cnt_printed_chars ++;
   putchar (chptr [i ++]);
  
  } break ;
  和case 'c',同出一轍.cnt_printed_chars++放在了循環內,也證明了剛才提到的他的作用.另外我們也看到了cnptr是用來在處理字符串時的位置指針.到此為止,我們清楚的所有變量的用途,前途變得更加光明了.
  接下來:
  // PartI
   case 'i':
   case 'd':
   cnt_printed_chars += printInt (va_arg (ap , int ));
   break ;
   case 'u':
   cnt_printed_chars += printUnsignedInt (va_arg (ap , unsigned int ));
   break ;
   case 'x':
   cnt_printed_chars += printHexa (va_arg (ap , unsigned int ), 'x');
   break ;
   case 'X':
   cnt_printed_chars += printHexa (va_arg (ap , unsigned int ), 'X');
   break ;
   case 'o':
   cnt_printed_chars += printOctal (va_arg (ap , unsigned int ));
   break ;
   // Part II
   case 'p':
   putchar ('0');
   putchar ('x');
   cnt_printed_chars += 2 ;
   /* of '0x' */
   cnt_printed_chars += printHexa (va_arg (ap , unsigned int ), 'x');
   break ;
   case '#':
   pos ++;
   switch (cntrl_string [pos ])
   {...
   case 'x':
   putchar ('0');
   putchar ('x');
   cnt_printed_chars += 2 ;
   /* of '0x' */
   cnt_printed_chars += printHexa (va_arg (ap , unsigned int ), 'x');
   break ;
   case 'X':
   putchar ('0');
   putchar ('X');
   cnt_printed_chars += 2 ;
   /* of '0X' */
   cnt_printed_chars += printHexa (va_arg (ap , unsigned int ), 'X');
   break ;
   case 'o':
   putchar ('0');
   cnt_printed_chars ++;
   cnt_printed_chars += printOctal (va_arg (ap , unsigned int ));
   break ;
  注意觀察一下,PartII的代碼其實就是比PartI的代碼多一個樣式.在16進制數或八進制前加入0x或是o,等等.因此這裡就只分析一下PartI咯.
  其實仔細看看PartI的個條case,也就是把參數分發到了更具體的函數用於顯示,然後以返回值的形式返回輸出個數.對於這些函數就不具體分析了.我們先來看看一些善後處理:
  先看case的default處理.
  default :
   putchar ((unsigned char ) cntrl_string [pos ]);
   cnt_printed_chars ++;
  就是直接輸出cntrl_string裡%號後面的未知字符.應該是一種容錯設計處理.
  再看看if (cntrl_string[pos] == '%')的else部分
  
  else
  {...
   putchar ((unsigned char ) cntrl_string [pos ]);
   cnt_printed_chars ++;
   pos ++;
  
  }
  如果不是%開頭的,那麼直接輸出這個字符.
  最後函數返回前
   va_end (ap );
   return cnt_printed_chars ;
  va_end處理變長參數的善後工作.並返回輸出的字符個數.
  在最後我們有必要談談putChar函數以及基本輸出的基礎函數printChar,先來看看putChar
  int putchar (int c )
  {...
   switch ((unsigned char ) c )
   {...
   case 'n' :
   newLine ();
   break ;
   case 'r' :
   carriageReturn ();
   break ;
   case 'f' :
   clearScreen ();
   break ;
   case 't' :
   printChar (32 );
   printChar (32 );
   /* 32 = space */
   printChar (32 );
   printChar (32 );
   printChar (32 );
   printChar (32 );
   printChar (32 );
   printChar (32 );
   break ;
   case 'b':
   backspace ();
   break ;
   case 'a':
   beep ();
   break ;
   default :
   printChar ((unsigned char ) c );
  
   } return c ;
  }
  通覽一下,也是switch-case為主體的.主要是用來應對一些特殊字符,如\n,\r,....這裡需要提一下,關於\t的理解.有些人認為\t就是8個space,有些人則認為,屏幕分為10大列(每個大列8個小列總共80列).一個\t就跳到下一個大列輸出.也就是說不管你現在實在屏幕的第1,2,3,4,5,6,7位置輸出字符,只要一個\t都在第8個位置開始輸出. VS.NET中就是用的這種理解.因此如果按照這個理解的話,\t的實現可以這樣
  int currentX = ((currentX % 10 ) + 1 ) * 8 ;
  然後在currentX位置輸出.
  接下來看printChar也就是輸出部分最低層的操作咯
  void printChar (const byte ch )
  {...
   *(word *)(VIDEO + y * 160 + x * 2 ) = ch | (fill_color << 8 );
   x ++;
   if (x > ;
   = WIDTH ) newLine ();
   setVideoCursor (y , x );
  }
  這裡VIDEO表示顯存地址也就是0xB8000.通過 y * 160 + x 屏幕(x,y)坐標在顯存中的位置.這裡需要知道,一個字符顯示需要兩個字節,一個是ASCII碼,第二個是字符屬性代碼也就是顏色代碼.因此才必須 y * 80 * 2 + x = y * 160 + x.那麼ch | (fill_color << 8)也自然就是寫入字符及屬性代碼用的了.每寫一個字符光標位置加1,如果大於屏幕寬度WIDTH就換行.最後通過setVideoCursor設置新的光標位置.完成了整個printChar過程.
  到此,把printf從上到下說了一遍.不知道各位大家感覺如何,如果說得不清楚還大家多提意見.有說得不對的地方請大家多多指教.

 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved