【經典】把脈printf中的C進階技巧

2021-03-02 最後一個bug


    今天跟大家分享一首坤坤同學的新歌<情人>,個人覺得旋律有一種花花公子的味道,還是比較帶感的,大家感興趣可以欣賞下!     好了,今天為大家帶來一篇printf剖析的文章,該函數應該是大家在初學階段用得非常頻繁的一個函數,進入嵌入式的小夥伴也是經常將其重定位進行系統相關調試信息的列印,同樣也有很多大佬為了增加程序運行效率直接將其進行了改寫來滿足需求。    所以今天作者就帶大家看看printf裡面究竟是怎麼處理的?為什麼能夠輸入可變參數?到底支持哪些輸出輸出?等等。

    在嵌入式中一般談到C Library大家都會想到glibc,glibc是GUN旗下的一個C標準庫。那麼libc又是什麼呢?對於這個名詞的定義有點歧義,有些人把所有的C標準庫都統稱為libc,而有部分人認為libc是最開始linux下的標準庫。

    所以說C標準庫也是多種多樣的,不同平臺都有所不同,比如嵌入式中非常小型的uClibc等等。

    大家如果有對C庫感興趣的可以去簡單看一些源碼,裡面的寶藏也是特別的豐富。下面作者提供glic和uClibc的網站,大家可以到網站下載對應的源碼進行研究。

glibc官方網站 : http://www.gnu.org/software/libc/

uClibc官方網站 : https://uclibc-ng.org/

    由於uClibc相對glibc來說小得多,所以在嵌入式中也是經常使用到,作者也是特意下載了源碼並解壓出來了,證明訪問路徑是OK的:

    並用sourceInsight打開找到了printf的具體實現:

    由於glibc和uClibc中的printf實現相對嵌套比較多,不便於直接分析,後面作者會選擇相對比較有條理的printf函數實現進行分析講解,設計實現上都是大同小異,如果大家感興趣也可以下載源碼進行閱讀。

    C語言函數參數一定會入棧嗎?入棧一定是從右向左嗎?

    其實這是一個與平臺和處理器相關的問題,所以需要具體情況具體分析,首先大家要明確函數參數和定義的局部變量大部分都是存在堆棧中(不過也可以通過藉助寄存器來傳遞,比如說之前剖析register關鍵字文章中把局部變量放到寄存器中提高效率),使用完畢以後通過堆棧指針的移動進行自動的釋放。

    對於X86-32bit平臺一般都是從右向左參數入棧,而對於X86-64bit為了提高程序運行效率,會把前面的部分參數通過對應的寄存器進行傳遞,如果有更多的參數就通過壓棧進行處理,所以需要根據具體的平臺和編譯器進行分析。那麼為什麼作者這裡首先提到入棧順序呢?因為printf需要實現可變參數,那麼肯定是需要有約定的傳參數的規則,該約定的規則就決定了函數內部如何獲得對應參數。

    對於大部分小夥伴在平時的開發中基本上都是使用固定的參數類型,不過對於類似於printf這種用戶接口使用型函數,實現可變參數就顯得更加具有靈活性。學習過C++的小夥伴應該有種感覺,可變參數有點類似於函數重載,不過C的可變參數必須包含一個參數。下面作者簡單的實現一個可變參數函數使用demo供大家參考。

1#include<stdio.h>
2#include<stdarg.h>
3
4/***********************************
5 * Fuction: sCalSum
6 * Author :(公眾號:最後一個bug)
7 **********************************/
8int sCalSum(int Num,...)
9{
10    //定義獲取參數列表結構體
11    va_list ap;
12    int sum = 0;
13    int i = 0 ;
14    //定位起始變量
15    va_start(ap, Num);
16
17    for(i = 0 ;i < Num;i++)
18    {
19        //根據參數類型進行索引
20        sum+= va_arg(ap,int);
21    }
22    //結束變量獲取,並釋放資源
23    va_end(ap);
24    return sum;
25}
26
27
28int main(void)
29{
30    printf("%d + %d = %d\n",1,2,sCalSum(2,1,2));
31    printf("%d + %d + %d = %d\n",1,2,4,sCalSum(3,1,2,4));
32    printf("%d + %d + %d + %d = %d\n",1,2,4,8,sCalSum(4,1,2,4,8));
33    return 0;
34 }

    前面的兩個知識點都是為下面printf源碼分析鋪路的,浮點在處理器中運算是比較耗時間的,同時佔用的資源也是非常多的,所以很多集成開發環境或者編譯連結工具都會為標準庫提供精簡版本供大家選擇。

    特別是對於使用單片機的小夥伴調用庫相關的函數,如果精簡版本能夠滿足需求,就儘量使用精簡版本,如果覺得精簡版本還是太佔用資源,那就自己手動編寫修改吧,所以printf中的浮點處理成為了精簡的一部分,如果在使用過程中發現使用printf列印不了浮點可以查看一下是不是libc中不支持浮點列印等相關功能。(下圖是IAR編譯工具中的相關配置選項)

    為了方便大家學習和理解,所以這裡並沒有選用非常複雜的函數實現,而是選用IAR中的精簡版printf跟大家講講思路:(下面的代碼截圖均來自IAR安裝目錄,IAR安裝目錄下還有很多其他寶藏,大家可以參考學習)

在調用printf函數都會使用到頭文件#include<stdio>,所以大家搜索該文件即可,然後順著包含關係可以找到其他函數設計實現,所以推薦大家使用SI編輯器閱碼。

下面作者截取了printf函數實現,大家仔細觀察會發現printf竟然還有返回值,估計80%的小夥伴都沒有使用過吧。

從printf函數形式來看來和我們前面實現的可變參數實現並沒有太大的區別,只是說第一個參數變成了指針,這個指針就是平時所指定的參數格式,如"ADCSample:%d",函數內部就是通過解析該字符串獲得後面傳入參數的具體類型等信息,從而進行相應的轉化處理。

vprintf函數會最終調用vfprintf, vfprintf調用vsprintf,如下圖所示

 對於vfprintf函數中vsprintf僅僅只是通過ap參數和pFormat格式進行轉化為pstr,通過pstr把最終的輸出信息通過fputs進行輸出,所以你只需要改寫對應 fputs就可以把最終輸出到對應的終端上(比如串口,LCD屏幕等等),所以玩stm32使用重新位串口輸出也就是同樣的道理了。

下面我們來具體看看vsprintf裡面的實現思路,vsprintf會調用vsnprintf,同時通過宏定義限制了最終通過printf的長度。

    printf函數的基本實現就跟大家講解到這裡,其實很多libc並沒有想像中那麼神秘,大家如果在平時使用libc的過程中發現了相關問題完全可以通過閱讀相關源碼進行分析處理,也可以對其源碼進行改寫來滿足自身需求,自所謂"見源碼如見真理"

    好了,這裡是公眾號:「最後一個bug」,一個為大家打造的技術知識提升基地。同時非常感謝各位小夥伴的支持,我們下期精彩見!

推薦好文  點擊藍色字體即可跳轉

☞【重磅】【完全解讀】RTOS中的任務是線程?進程?還是協程?

☞【漲知識】OS下的內存使用原來這麼複雜

☞【原理分析】來看看慣性輪自平衡自行車實現原理

☞【重磅】剖析MCU的IAP升級軟體設計(設計思路篇)

☞ 【典藏】別怪"浮點數"太坑(C語言版本)

☞GUI必備知識之「告別」亂碼(淺顯易懂)

☞【典藏】大佬們都在用的結構體進階小技巧

☞聽說因為代碼沒"對齊"程序就奔了?(深度剖析)

☞【典藏】自製小型GUI界面框架(設計思想篇)

相關焦點

  • 學習c語言筆記——C庫函數printf()
    c語言中的printf是什麼來的?」。我答:「它是一個函數,主要用來輸出運算結果。」 ,下面就給大家介紹C庫函數printf()使用方法。下面我們通過一個調用c庫函數的c語言案例來說明printf()函數的使用方法,如c語言1。
  • C語言 printf詳解
    對長整型可以用"%lx"格式輸出。同樣也可以指定欄位寬度用"%mx"格式輸出。④u格式:以無符號十進位形式輸出整數。對長整型可以用"%lu"格式輸出。同樣也可以指定欄位寬度用「%mu」格式輸出。⑤c格式:輸出一個字符。⑥s格式:用來輸出一個串。有幾中用法%s:例如:printf("%s", "CHINA")輸出"CHINA"字符串(不包括雙引號)。
  • c語言面試題----printf()的參數
    intmain(void) {     int a = 10, b = 20, c = 30;     printf("\n %d..%d..%d \n", a+b+c,(b = b*2), (c = c*2));      return 0; }本題解析答:輸出結果是:110..40..60這道題目來說的話,許多同學感覺無從下手,所以沒法回答。
  • 【編程基礎】c printf知多少
    printf()函數是格式輸出函數,請求printf()列印變量的指令取決與變量的類型.例如,在列印整數是使用%d符號,在列印字符是用%c 符號
  • C語言中的scanf與printf
    為實現這樣的操作,C語言提供了scanf與printf兩個函數,使用它們之前,一般需要包含stdio.h頭文件。語法是: #include 1. 使用scanf函數的注意事項。本文引用地址:http://www.eepw.com.cn/article/201612/324575.htm2. printf()函數基本語法格式 printf(格式佔位符列表,變量列表);在printf()函數中,格式佔位符決定了輸出的樣子,只是在佔位符列表中,用佔位符%d等先把位置佔住,然後將後面的變量值依次填入前面的佔位符處。
  • 關於C語言中printf與scanf的具體詳解
    結果圖 c語言中,浮點數的輸入輸出:scanf是float(REAL4),printf是double(REAL8)。
  • STM32中如何使用printf()函數
    STM32串口通信中使用printf發送數據配置方法(開發環境 Keil RVMDK在STM32串口通信程序中使用printf發送數據,非常的方便。可在剛開始使用的時候總是遇到問題,常見的是硬體訪真時無法進入main主函數,其實只要簡單的配置一下就可以了。
  • glibc中的printf如何輸出到串口
    首先看glibc中printf函數的定義(glibc-2.3.6/stdio-common/printf.c):[plain]view plaincopyprint?*/strong_alias(printf,_IO_printf);strong_alias,即取別名。網上有人提及這個strong alias好像是為了防止c庫符號被其他庫符號覆蓋掉而使用的,如果printf被覆蓋了,還有_IO_printf可以用。
  • C進階 | 容易忽略的整形提升問題
    = 10;  6    char d = (a * b) / c;  7    printf ("%d ", d);   8    system("pause"); 9    return 0; 10}輸出結果:120直接看代碼,表達式(a * b)/ c似乎引起算術溢出,因為帶符號的字符只能具有-128至127的值(在大多數
  • 【C/C+】10個經典的C語言小程序,小白必看!
    c環境中運行,看一看,Very Beautiful! 程序原始碼: #include "stdio.h" main() { char a=176,b=219; printf("%c%c%c%c%c\n",b,a,a,a,b);
  • 10個經典的C語言面試基礎算法及代碼
    算法是一個程序和軟體的靈魂,作為一名優秀的程式設計師,只有對一些基礎的算法有著全面的掌握,才會在設計程序和編寫代碼的過程中顯得得心應手。
  • c語言經典小程序匯總大全
    經典C語言小程序10例,今天給大家分享10個比較基礎的C語言的小程序,附上幾個常用的10個小例,希望給C語言初學者帶來一定幫助,熟練運用,舉一反三。 \n」); printf(「 ****\n」); printf(「 *\n」); printf(「 * \n」); printf(「 ****\n」); } 【程序5】 題目:輸出特殊圖案,請在c環境中運行,看一看,Very Beautiful!
  • C語言經典經典必背程序100例
    ,請在c環境中運行,看一看,Very Beautiful!2.程序原始碼:#include "stdio.h"main(){char a=176,b=219;printf("%c%c%c%c%c\n",b,a,a,a,b);printf("%c%c%c%c%c\n",a,b,a,b,a);printf("%c%c%c%c%c\n",a,a,b,a
  • scanf和printf格式化輸入輸出中非常實用的小技巧
    C提供的輸入輸出函數除了具有必須的輸入輸出功能外,還有一些其他實用的小技巧,了解這些小技巧將會為程序帶來更友好的用戶體驗。一、printf欄位寬度、精度修飾符當我們要輸出類似表格形式的樣式時,我們會用到欄位寬度修飾符。它能夠讓printf函數的輸出更加規整。
  • 如何在單片機上使用printf函數
    當我們在調試代碼時,通常需要將程序中的某個變量列印至PC機上,來判斷我們的程序是否按預期的運行,printf函數很好的做到了這一點,它能直接以字符的方式輸出變量名和變量的值,這樣使輸出的信息很直觀;但printf函數在使用時,不僅僅要初始化串口
  • STM32 keil printf的使用
    請在MDK(keil)工程屬性的「Target「-》」Code Generation「中勾選」Use MicroLIB本文引用地址:http://www.eepw.com.cn/article/201611/315976.htm前提是你有一個完整keil的工程 比如ADC的調試的時候很多時候用到串口
  • C語言中scanf函數的3種常見問題與應對技巧
    #include<stdio.h>main(){ int a; printf("input the data "); scanf("%d ",&a);//這裡多了一個回車符 printf("%d",a); return 0;}結果要輸入兩個數程序才結束,而不是預期的一個。why?
  • 推薦零基礎C語言教程【一】
    贊助群目前資料火熱更新中,最近也是找人搞了點進階安全課程給大家,
  • C語言基礎知識學習經典入門
    }while (a<100);    return 0;} 一般情況,for循環用的最多,while用在死循環中。    return 0;} c語言入門經典必背8個程序1 、 /* 輸出 9*9 口訣。
  • C經典88案例 | 將M行N列的二維數組中的字符數據,按列的順序依次放到一個字符串中
    後臺回復「C88」 獲取 C經典88案例代碼及文檔後臺回復「百本電子書」 獲取整理好的幾百本打包電子書編寫:fun()功能:將M行N列的二維數組中的字符數據,按列的順序依次放到一個字符串中例如:二維數組中的數據為:W W W W