<center id="cog8c"></center>
  • <sup id="cog8c"></sup>
    <nav id="cog8c"></nav>
  • 嵌入式 C語言 代碼

    如何在嵌入式C語言編程的針尖上舞蹈?看15年大神7個實例

    2019-04-30
    823次瀏覽

    整合自網絡信息,作者:小馬兒,編輯:付斌


    不記得從何時開始,自己就經常承擔企業招聘的工作,側重于工業嵌入式產品研發人員的招聘。


    出于對企業和新人的負責態度,我傾向于尋覓那種基礎知識扎實,且對嵌入式編程有興趣的人員,這樣對企業發展、對個人成長都有好處,可惜很多次都如同大海撈針,希望而來失望而歸。


    這是我很糾結的一個問題,不由得想起了曾經聽過的一個故事:一個外國人來國內某公司考察,首先看到了職員的工作態度,狂吐槽,這樣的員工企業應該全部裁掉;然后中午吃飯的時候,品嘗了企業的午餐,又開始罵企業了。


    回頭看一看目前的大學教育模式,很多學校還是沿襲類似于初高中的填鴨式教育方式,大多數學生也是在吃喝玩樂混日子,即使大一新生還有些激情,很快就被大環境給污掉了。然后就是學生、學校和企業的互罵,搞得一片狼藉,戾氣沖天。


    一個要好的朋友在國內某985大學教計算機,一次和他聊天埋怨到,現在的學生入職后好長時間都沒法上手,培訓周期很長,然而朋友的觀點是,這應該是理所當然的啊,大學能學點啥,都需要到企業才能鍛煉的。無語……


    真應該是這樣嗎?我很茫然,但看到每個學生背后為學費辛勤勞作的父母,看到他們對子女期望的眼神,我認為,我們可以做的更好,也應該做的更好。


    為了讓大家體會我內心糾結的心情,先簡單的聊一聊招聘試題的事情。
    以前我參加過中興的招聘,招聘老師給我出了一道題目,int ?n = ?3,4;我說我不知道,這是變態的用法,我們不應該這樣用,您也不應該出這樣的題目。可能當時年輕氣盛,將招聘老師氣了個半死,我當然也無緣中興了。吸取以前的教訓,后來我在組織招聘時,有一個基本原則,只有在工作中經常會用到的知識,才會作為考察的范圍。


    為了挖掘一些好的苗子,我們也一直在思考怎樣的試題可以更好的考察出求職者的能力和興趣。最開始的方式是先問一些基礎課和專業基礎課的基本概念,濾除基礎知識不扎實的人;然后期望求職者能講解一個自己曾經憑興趣寫過的程序,然后借著求職者的描述,然后不斷的深入,以判斷他的能力具體到了什么程度。


    可惜,99%的人根本說不出自己憑興趣寫的程序,碰上幾個胡謅的人,幾句話就露餡了。后來只好雪藏該問題,然后降低難度,出一些單層循環,且每層循環適度判斷的編程例子(如判斷一個整數中0的個數等)。如果每次招聘,能碰上一兩個將這類程序快速寫出來的同學,大家都可以興奮半天了。


    這就是我們能招聘到的學生的普遍能力了,在我看來,要達到這個能力,只要隨意的學習幾天編程就可以了,還需要大學四年干嘛,讓人郁悶啊。
    但一個更加現實的問題是,這樣的人招聘進來企業如何使用。我們不敢直接拿著產品讓菜鳥人員練手,大家都知道,產品是要賣給用戶的,初期的小問題到了用戶那兒就會放大成大問題的。


    企業培養不同于學校教育,不會有人給你填鴨式,更別指望有人手把手的教你,很多時候都是散養,頂多指點幾句然后自己看書去。這種情況也導致了很多人初期很難上手,一些人甚至被迫轉行,荒廢了自己多年的專業方向。


    如何快速過渡,結合自己多年的工作和新人培養經驗,我總結了一套比較有效的方式,將產品研發需要的初級C語言知識巧妙的融入到三個例子中,讓新人通過這三個例子去學習,去碰壁,去思考,持續的提高自己的C語言能力,盡快具備可參與產品研發的能力。


    如何學習C語言?


    很多剛入職的新人,都喜歡問一個類似的問題:“如何學習……?”,然后一些朋友就會給拷貝一大堆書籍資料,更熱心的還會指導先看哪本在看哪本。

    但不幸的事,很多時候就截至于此了,一大堆資料依然靜靜的躺在電腦硬盤里,只是在偶然的情況下才會打開翻看兩眼目錄。

    走過一些企業,培訓體系一般是這樣的:


    1、新人入職后,師傅會給一堆資料讓看,然后新人硬著頭皮看一些;
    2、哪天師傅不忙了,惦記起這個新人,然后交給其一個產品,讓其折騰;
    3、可惜具體產品一般都涉及多個學科,面對一大堆疑問,新人會感覺騰云駕霧般難以前行;4.一段時間后部分人邁過了入職時的絕望懸崖,有了自己的積累,開始慢慢的深入接觸產品,但因各種文檔資料奇缺,只能一邊學習一邊調整;
    5、數年后,新人成為了老手,同時新的產品體系也誕生了;
    6、然后重復以上死循環。


    長此以往,公司的產品體系變得非常的雜亂,技術難以復用,無法進行有效的積累,那種如臂使指的團隊構建更是空談。

    如何才能跳出上述死循環呢?我在自己的職業生涯中進行了大量的探索嘗試,有了一點感悟:不僅需要將新人的培訓工作盡可能前移,盡可能體系化,而且要盡早的融入我們的設計及團隊理念。因此在后續的三個C語言例子中大家會體會到很多東西不僅僅是C語法層次的內容。

    前面我談到,在公司,不可能在重復學校的填鴨式的教育模式,也不會有人手把手的教你。那么在這種情況下,如何組織培訓工作,很多時候反而成了一件頗具藝術感的選擇難題。


    隨著移動互聯網的盛行,在線教育也流行了起來,網絡上出現了大量的視頻教程。緊隨時代的脈絡,我嘗試過將入職需要學習的內容做成書籍和視頻,但效果一般。后來細細研究這個領域,發現大多數視頻僅僅是將大學課堂的內容移到了網絡上,很多人也僅僅是耐著性子看個幾集,然后就不了了之了,因此導致的效果不佳。關于在線教育我也有一些自己的想法和思考,后面會專門撰文描述,這兒就不深入介紹了,總之,初期的嘗試是不太成功的。


    C語言第一個例子:需求不明確


    第一個例子:“編寫一個控制臺程序,已知內層和外層菱形的高度,輸出一個空心菱形”。

    是否看上去很簡單啊,寫到這兒,很是期望正在閱讀的你能先停下來,思考思考,然后付諸行動,編寫幾行代碼小試一番,然后在繼續讀下去,收獲會更大。

    這些年,我帶過很多人,大部分人看到這個題目后,然后立即就開始寫程序去了,自己內心多少有點小小的失望,為何?


    一般通過公司層層把關招聘進來的軟件人員,這個例子總是可以弄出來的,不過可惜的是,近一半以上的人不能準確的實現出來,僅一個菱形高度的理解,就是五花八門,亂七八糟的,執行如這樣的:


    或者這樣的:


    或者干脆是這樣的:


    為何會出現這種情況,這恰恰是該例子的第一個大坑:需求不明確。一開始就碰到挫折,對很多新人來說,這無異于當頭一棒,不過記憶也最為深刻。

    一個團隊協作時,會存在大量的交流,而交流過程中,總是會產生或多或少的歧義,而恰恰是這些歧義會導致需求的不明確,會導致大量的返工,甚至會導致項目的失敗。我給新人的第一份建議:將需求用自己的語言表達出來,和對方確認后再實施。這一點在以后的團隊協作中非常的重要,因此我早早的將這一點嵌入到了入職培訓中。

    經過這么一折騰,第一個例子重新描述如下:“寫一個控制臺程序,用戶輸入內層和外層菱形的高度,輸出一個空心菱形,菱形的高度定義為菱形的上三角形的高度,如輸入5和3,輸出如下:
    ??
    ????? *

    ??? ***
    ? **? **
    ** ? ?? **
    ** ? ?? **
    ?** ? **
    ? ** **
    ? ?***
    ? ? *


    這兒稍微補充兩點:


    1、菱形高度定義好似有違常理,但在編程的世界中,簡潔就是常理,如果定義為整個菱形高度,那么不僅需要進行正整數判斷,而且還需要奇數判斷,增加了程序的復雜度。

    2.、用一個例子描述,比一大堆文字管用多了,是所謂一圖頂千言。

    明確知道要做什么了,然后很多人又回去,然而很多情況下執行結果如下所示:


    這兒引出了第二個很重要的概念:邊界判斷。給出兩個數,如何簡潔且完備的判斷其是合理的輸入,這是編程的基本功,這些內容在大學教學過程中一般不太重視,但想做出合格的嵌入式產品,這一點必須引起足夠的重視。如何做出優雅的邊界判斷,這個例子比較簡單,留給正在閱讀的你吧,順便回頭想一想我前面菱形高度的定義吧。

    第二次被打道回府后,很多人會有急躁情緒,菱形還沒輸出呢,就給灌輸了一堆規則。碰到這種情況,我會給他們講解嵌入式程序的特點(這一點留待后文慢慢描述),以及我以前的經歷。我剛開始工作時,因CPU速度受限,程序還主要是匯編語言,領導讓寫一小段匯編程序,但每次提交后,都被劈頭蓋臉的罵一通,讓回去整改,直到后來一行匯編語句都省不下去了,才算通過了。回頭看,為了一個小小的例子,竟然將大多數匯編指令熟悉了,方才明白領導的良苦用心。現在的新人臉皮薄了一些,不敢亂罵了,只好慢慢講道理了。

    經過第二次的折騰,大部分新人態度都能稍微端正起來,而且這次寫出來的例子,和需求基本吻合了,但當興致沖沖的跑過來交差時,我反而不看程序,開始問起在實現這個程序過程中用到的調試技巧。

    這個例子對新人來說有一點點的難度,不可能一次就寫好的,肯定會經歷一些痛苦的調試過程。但是在國內的大學教育體系下,卻又不太重視基本調試技能的鍛煉,期望新人一開始意識到調試是一項基本功夫,需要引起足夠的重視,需要去持續的加強。

    雄關漫道真如鐵,而今邁步從頭越,一個小小的例子還未起步,對很多人已是一場痛苦的經歷。要想成為一個合格的嵌入式工程師,需要方法,需要才智,更需要背后的汗水和堅持,本系列文章會帶領大家體味我的成長路,但無法代替你自己的付出和汗水。


    C語言第二個例子:邊界判斷


    上文中,我提到由該例子引出了三個重要概念,其中第二個概念是:邊界判斷。給出兩個數,如何完備且簡潔的判斷其是合理的輸入,這是編程的基本功。很可惜的是,這幾個例子都沒有做到。結合自己多年帶人的經驗,這兒補充闡述一下。

    輸入兩個數,一個是外菱形的高度(m表示),一個是內菱形的高度(n表示),有如下幾個判據:


    1、兩個高度都應該是正整數(內菱形高度可以為0);
    2、受限于屏幕的大小,菱形高度應該受限;
    3、外菱形高度應該大于內菱形的高度。


    以前帶人的時候,拿到例程后,我喜歡先輸入(1000,800)這樣的值,因為這是容易被忽略的地方,很多人郁悶的鎩羽而歸。

    大部分人都是缺判據,也有一些人性格比較謹慎,習慣寫一大堆的判斷條件,如下面這種的:


    if ?(m > 0 && n > 0 && m > n && m < 30 ?&& n < 30) { ? ……}

    總之,都沒有抓住判斷條件應該完備且簡潔的基本準則,實際上該例子很簡單,只要簡單的分析,判斷條件也就三個,如下:


    1、內菱形高度大于等于0;
    2、外菱形高度小于約定之(假設30);
    3、外菱形高度大于內菱形高度;


    寫成程序示意如下:


    if (n >= 0 && m > n && m < 30) { ? ……}


    還記得第(1)節中我們約定菱形的高度是上三角形高度嗎,帶來的好處就是判斷的簡潔化,概念是為目標而服務的,不然該處的判斷還需要額外的增加兩條奇數判據了,簡潔性也會打折扣了,呵呵,可以再回味一番。

    在嵌入式產品中,最終產品的魯棒性,很多時候就是表現在這樣一點一滴的簡單判據上,該處的不厭其煩,也是期望新人慢慢的融入研發團隊時,能夠充分意識到這一點。


    終于要開始輸出空心菱形了,但對于剛畢業的大學生,這個例子剛上手還是有一些繞的,其思維邏輯是如何的呢。


    一般人都會發現,輸出空心菱形要稍微復雜一些,那么我們修改為輸出菱形呢,是否簡單很多,如果還嫌復雜,修改為輸出三角形呢。呵呵,說白了,就是將復雜的問題去其枝葉,先簡后繁的慢慢處理。


    一開始的問題就簡單多了,已知三角形高度m,輸出三角形,如m=5,輸出如下:


    ? ? *

    ? ?***

    ? *****

    ?*******

    *********


    為了直觀,將空格也表示出來,示例如下:


    ----*

    ---***

    --*****

    -*******

    *********


    此時,結論已經很形象了,每行輸出的空格從m-1遞減,每行輸出的*從1開始遞增,循環子為m,程序示例如下:


    for (i = 1; i <= m; i++)
    {
    ?
    ?j = m - i;

    ?while (j--)
    ? ?printf(" ");


    ?j = i * 2 - 1;
    ?
    ?while (j--)
    ? ?printf("*");


    ?printf("\n");
    }



    問題復雜化,考慮空心三角形,相當于里面又多了一個三角形,我們輸出的時候,將其簡單扣去即可,假設n=3,如下圖示例:


    ----*

    ---***

    --**-**

    -**---**

    **-----**


    程序示例如下:


    for (i = 1; i <= m; i++)
    {
    ?j = m - i;
    ? while (j--)
    ? ? ? printf(" ");
    ?j = i * 2 - 1;
    ? for (k = 0; k < j; k++)
    ?{ ? ?
    ? if (k < m-n || k >= j-m+n)
    ? ? ?printf("*");
    ? ? else
    ? ? ?printf(" ");
    ? }
    ? printf("\n");
    }


    進一步復雜化,輸出完整的空心菱形,僅僅需要將上面的程序重復一下,僅僅是將其顛倒一下,且高度調整一下(減1處理,表達式更加復雜了)而已,我就不展示示例程序了,大家有興趣的可以自己嘗試一下。


    有些人在實現該程序的時候,一開始就是對空心菱形進行分析,最直觀的分析策略就是將菱形分層了三段,上三角形,下三角形,中間的空心部分,如下圖示意:


    ? ? *

    ? ?***

    ? ** **

    ?** ? **

    ** ? ? **


    ?** ? **

    ? ** **

    ? ?***

    ? ? *


    按照這種思路,程序示意如下:


    for(i=1;i<2*m;i++)
    { ? ?
    ? ?if(i<=m-n)
    ? ?{
    ? ? ? ?
    ? ? ? ?star = 2*i-1;

    ? ? ? ?empty= m-i;

    ? ? ? ?while(empty--)

    ? ? ? ? ? ?printf(" ");
    ? ? ? ?while(star--)
    ? ? ? ? ? ?printf("*");
    ? ?}
    ? ?else if(m-n<i && ?i< m+n && j< 2*n)
    ? ?{
    ? ? ? ?
    ? ? ? ?if(j <= n && i <= m)
    ? ? ? ?{
    ? ? ? ? ? ?
    ? ? ? ? ? ?num_empty = 2*j-1;
    ? ? ? ? ? ?empty = m -i;
    ? ? ? ?}
    ? ? ? ?
    ? ? ? ?else{
    ? ? ? ? ? ?num_empty = 2*(2*n-1-(j-1))-1;
    ? ? ? ? ? ?empty = i-m;
    ? ? ? ?} ? ? ? ?
    ? ? ? ?num_star = star = m-n;

    ? ? ? ?while(empty--)
    ? ? ? ? ? ?printf(" ");
    ? ? ? ?while(star--)
    ? ? ? ? ? ?printf("*");

    ? ? ? ?while(num_empty--)
    ? ? ? ? ? ?printf(" ");
    ? ? ? ?while(num_star--)

    ? ? ? ? ? ?printf("*");
    ? ? ? ?j++;
    ? ?}
    ? ? else
    ?{
    ? ? ? star = 2*(2*m-1-(i-1))-1;
    ? ? ? empty = (2*m-1-star)/2;
    ? ? ? while(empty--)
    ? ? ? ? ? ?printf(" ");
    ? ? ? ?while(star--)

    ? ? ? ? ? ?printf("*");
    ? ?}
    ? ?
    printf("\n");
    }


    不管如何,至此,這個例程就算完成了,但大家有沒有發現上面這些程序都談不上優雅啊,其中各種m和n的表達式,一段時間以后看,基本同亂麻差不多了,試想,如果這是產品的程序,讓后來人如何閱讀并維護。


    C語言第三個例子:簡單粗暴的數字之美


    優美,總是讓人心醉,一提到優美,最容易想到的是悅目的圖畫,動聽的樂章、精妙的詩文……。然而,數學,自然科學的皇后,卻蘊含著比詩畫更多的優美。

    優雅的程序,或許其背后都蘊藏著數學的優美。

    在上一節中描述的一些例子中,我們總是在努力的拼湊各種m和n的表達式,與其這樣苦苦尋找,為何不直接將這個空心菱形放入坐標軸中呢。

    在電腦屏幕上,人們習慣將靠右稱之為x軸,靠下稱之為y軸,將空心菱形畫在屏幕上,示意如下:

    然后通過解析幾何知識勾勒空心菱形,程序示意如下:


    for (x = 0; x < m * 2 - 1; x++)
    {
    ? ?for (y = 0; y < m * 2 - 1; y++) ? ?
    ? ?{ ? ? ? ?
    ? ? ?if (abs(m - 1 - y) <= m - 1 - abs(m - 1 - x) && ? ? ? ? ? ? ?
    ? ? ? abs(m - 1 - y) > n - 1 - abs(m - 1 - x)) ? ? ? ? ? ?
    ? ? ? printf("*");
    ? ? ?else
    ? ? ? printf(" ");
    ? ? ? }
    ? ? ? printf("\n");
    }


    我們將所有的判斷都集中在了一起,閱讀程序,很容易明白這個大大的判斷語句是干嘛的了,是否比以前的實現都優雅了很多呢。


    不過這個判據好像還是挺復雜的,有沒有更好的辦法呢,估計很多朋友在看到我上面的那幅圖時已經想到了,那就是將坐標軸移到菱形的中間去,示意如下:

    外菱形的四條邊我們用表達式描述出來,如下:


    (+x) + (+y) < m
    (-x) + (+y) < m
    (+x) + (-y) < m
    (-x) + (-y) < m


    合并后的表達式為:abs(x)+abs(y)<m,此時的程序示意如下:


    for (x = -m; x <= m; x++)
    { ? ?
    for (y = -m; y <= m; y++) ? ?
    { ? ? ? ?
    ? t = abs(x) + abs(y); ? ? ? ?
    ? if (t >= n && t <= m) ? ? ? ? ? ?
    ? ? printf("*"); ? ? ? ?
    ? else ? ? ? ? ? ?
    ? ? printf(" "); ? ?
    ?} ? ?
    ?printf("\n");
    }


    不知大家看到這段代碼是怎樣的感覺,我僅記得當初自己發現這個實現后,第一次被這種簡單的數學美給震撼了。如果大家也有相同的感覺,我堅信,你可以在編程的這條荊棘路上走很高很遠…


    C語言第五個例子:嵌入式C語言和桌面C語言差異巨大


    在大學階段,我學的是計算機專業,對編程的愛好聚焦在各種巧妙的算法和實現上。工作后,開始從事嵌入式產品開發,碰了N多的壁,吃了N多的苦,才明白了嵌入式C語言和桌面C語言是有差別的。

    為了讓新人盡快邁入嵌入式門檻,因此,今天,注定是一場批斗大會,經歷波折,方能成長。

    我們首先拿最優秀的實現開刀,在上一節最后,分享了一個很優美的實現,示例如下:


    for (x = -m; x <= m; x++)
    { ? ?
    ?for (y = -m; y <= m; y++) ? ?
    ?{ ? ? ? ?
    ? ?if (abs(x) + abs(y) >= n && abs(x) + abs(y) <= m)
    ? ? printf("*"); ? ? ? ?
    ? ?else
    ? ? printf(" ");
    ? } ? ?
    ? printf("\n");
    }


    該實現將最復雜的條件判斷置于兩層循環之中,因此程序效率很低,而在嵌入式編程中,尤其是一些強實時模塊,性能經常是緊繃著的弦。

    工業嵌入式產品研發,追求團隊作戰,追求產品可維護性,追求性能和資源的均衡。可讀性、可維護性、cpu計算能力、內存、可靠性等等東東都成為了資源,需要我們的庖丁解牛,經常,我們喜歡將工業嵌入式產品編程戲稱為針尖上的舞蹈。

    除了這一點,前面的程序還有很多的不足,舉例如下:


    1、整個程序經常混雜一談,增加了他人閱讀程序的復雜性;
    2、m,n類似的名字,典型的學校風格;
    3、各種復雜的m和n的表達式,一段時候后,估計自己看起來都頭大了;
    ……


    因此,我們需要繼續上路,為了后續程序實現的方便,將整個程序結構約定如下:


    int main()

    { ? ?
    /* 輸入內外菱形高度,并進行合法判斷 */

    ? ?/* 循環輸出菱形 */ ? ?
    for
    (……)
    ? ?
    ? ?{
    ? ?
    ? ? ? ?/* 輸出前導空格 */
    ? ? ? ?/* 輸出左邊星號 */
    ? ? ? ?/* 輸出中間空格 */
    ? ? ? ?/* 輸出右邊星號 */
    ? ? ? ?/* 輸出換行 */
    ? ? ? ?printf("\n");

    ? ?}

    ? ?return 0;
    }


    看到這個程序框架,容易明白一點,我們要求以單層循環的方式輸出菱形。

    細細觀察空心菱形的結構,每一行從左到右可以抽象為前導空格,左邊星號,中間空格和右邊星號四部分,只需要找出這四個值和行號的函數關系,整個程序也就迎刃而解了,如下圖示意:


    ----*
    ---***
    --**-**
    -**---**
    **-----**
    -**---**
    --**-**
    ---***
    ----*

    我的職業導師經常會和我探討計算機編程思維的概念,時至今日,我們也沒有辦法給其下一個準確的定義,但在反復的迭代鍛煉中,慢慢的體會到了哪些實現可以稱之為計算機編程思維。

    舉一個例子,在計算機世界中,我們經常僅鼠標、鍵盤、磁盤、磁盤上的數據等等都稱之為文件,可以進行簡單的遍歷。為何風牛馬不相及的東西,我們非要將其抽象成統一的概念呢,這非一句話能描述清楚,后續會帶著大家慢慢體悟,但這種設計思想在產品研發過程中,卻比比皆是。

    插入這兩段廢話后,回頭再來看我們的菱形輸出例子,明明每一行的結構不盡相同(回憶一下第二個實現版本,就是分類型分別輸出的),但我們非要求同存異,或許,我期望從一開始,就在新人的心中播下架構設計的種子,期待著后續的萌芽。

    雖然這兒的話題是以新人培訓切入的,但未嘗不是自己的成長之路,所不同的是我是在一次次的跌打滾爬中成長起來的。將自己曾經摔過的跟頭融入到入職C語言訓練的例子中,甚至將自己的整個成長史寫出來,但愿別人能以我為鏡,成長的更踏實更快速一些。


    C語言第六個例子:嵌入式C語言和桌面C語言差異巨大


    在上一節中,我們已約定了程序基本框架,并且也簡單的介紹了編程思維的理念,好似,新人很快就可以寫出合乎要求的程序了。

    可惜,每個人的成長之路都不是一帆風順的, 記得當初自己學習計算機編程時,哪個技能不是從大量的模仿中才能體悟一點點的,進而融入到自己的知識體系中的,一點一滴的成長起來的。

    依據上一節的整體框架要求,我曾經帶過的很多新人,甚至包含一些粉絲給我的發來的郵件,寫出來的程序基本上都是新瓶裝舊酒,僅是以前實現例子的翻版而已。

    我給大家示意一下,大家體會體會,自己是否也有這樣實現的沖動。


    /* 循環輸出菱形 */

    for (……)
    {
    ? ?/* 輸出前導空格 */ ? ?if (上半菱形) ? ? ? 以三角型方式輸出; ? ?else ? ? ? 以倒三角形方式輸出;
    ? ?/* 輸出左邊星號 */ ? ?if (上三角形) ? ? ? ... ? ?else if (中心空心) ? ? ? ... ? ?else ? ? ? ...
    ? ?/* 輸出中間空格 */ ? ?if (上半空心) ? ? ? 以三角型方式輸出; ? ?else if (下半空心) ? ? ? 以倒三角形方式輸出;
    ? ?/* 輸出右邊星號 */ ? ?if (上三角形) ? ? ? ... ? ?else if (中心空心) ? ? ? ... ? ?else ? ? ? ...
    ? ?/* 輸出換行 */ ? ?printf("\n"); }


    注:該處的示例是結合第二個實現版本的,將一個菱形分為上三角形,下三角形,中間的空心三部分分別輸出,詳情請查看《入職C語言例子一(2)》。

    應該如何實現呢?在上一節中,我們提到了重點在于尋找四個函數關系。文字的表現力經常是蒼白的,給大家展現一幅我指導新人時的隨意手繪圖,或許,大家立即就明白了。


    理解了上圖,額外強調幾點:


    1、空心菱形的每一行都必須等同看待,忘記上三角、下三角、中間空心等這樣的分割;
    2、四個函數關系內部不允許出現if語句,但允許提煉公共函數,如abs類的;
    3、成功沒有捷徑,技能的學習也沒有捷徑,將浮躁的心放下來;

    走到這兒,很多人都寫了七八遍了,拜目前的大學教育模式所賜,很多人又出現了浮躁情緒,即時的安撫還是很有必要的,經常打趣的一句話就是瞧瞧你的師兄xxx,別看現在負責好幾款產品得心應手,當初還寫了十多遍呢,哈哈。

    實際上很多人也意識到了我這樣折騰的目的。大學的學習都是淺嘗輒止的,很多東西都急于求成,但將這種心態帶入企業,帶入產品研發中卻是致命的,我僅是想通過這樣的形式,讓新人少走彎路。用心良苦,卻常引來誤解無數,內向者口是心非肚子里罵幾句,張狂者會直接詰問我這樣折騰他們有何意義,呵呵。

    隨意牢騷了幾句人生坎坷路啊,經這樣一指點,大部分人都可以按照要求順利的完成該例子,雖然每一部分都是一個復雜的表達式。

    此時,我會給大家講解一個很關鍵的計算概念:迭代。

    我們所從事的嵌入式產品是強實時工業產品,不僅經常涉及傅里葉等各種復雜計算,而且還要求在指定時間內完成,因此對計算效率等要求會比較高,最經常采取的策略就是通過迭代,保留有價值的中間計算結果,減少整體計算量。

    而通過迭代,不僅各部分的表達式不至于那么的復雜,而且會減少計算量。該例子比較簡單,沒什么技術含量,大部分人很快的就完成了迭代的調整,但前后對比的效果卻印象深刻,算是額外的收獲吧。

    至此,空心菱形程序的所有技術點都描述完畢了,按著這樣的要求,大多數人可以寫出滿足要求的程序了。還是非常的建議正在閱讀的你能親自寫一寫,調一調,下一節我會貼出標準答案,大家可以在比對一下,而差異部分正好是我們后續內容的起點。


    C語言第七個例子:不斷完善心中的答案


    前面幾節中,列舉了一個很優雅的實現,一些朋友提醒我該程序輸出不正常,自己測試了一下,確實如此。

    當時寫程序時,是直接以文檔的方式寫的,一些例程也是小伙寫的程序中拷貝出來的,重在意圖表現,所有的程序代碼都沒有測試過,缺乏了嚴謹性,優化如下:


    for (x = -m+1; x < m; x++)
    { ? ?
    ?for (y = -m+1; y < m; y++) ? ?
    ?{ ? ? ? ?
    ? ?t = abs(x) + abs(y); ? ? ? ?
    ? ?if (t >= n && t < m) ? ? ? ? ? ?
    ? ? ?printf("*"); ? ? ? ?
    ? ? else ? ? ? ? ? ?
    ? ? ?printf(" "); ? ?
    ?} ? ?
    ?printf("\n");
    }


    咱們書接上節,言歸正傳,在上一節中,新人磕磕碰碰的,終于寫出了符合技術要求的程序,皆大歡喜,以為要完工了,可惜,路依舊漫漫。

    此時,我會給大家分享該題目的標準答案,讓大家同自己寫的程序進行比對,以前的程序都是以片段方式提供的,標準答案以完整的格式提供,示意如下:


    /********************************************************************* * ? Copyright (C), 1999-2004, xxxxxx. Co., Ltd.* * ? 文件名稱:diamond.c* ? 軟件模塊:空心菱形輸出* ? 版 本 號:1.0* ? 生成日期:2003/3/23* ? 作 ? ?者:xiaomaer* ? 功 ? ?能:空心菱形輸出,該程序占用內存小,但計算稍大,可修改為"內存換資源"算法* *********************************************************************/
    #include <stdio.h>
    /* 菱形最大高度 */#define MAX_DIAMOND_HEIGHT 16
    /* 提前申明 */
    int
    myGreater(int n)
    ;

    /* 主程序 */

    int main()

    { ? ?
    ? ?int n, nRow;
    ? ?int nIn, nOut;
    ? ?int nCount1, nCount2, nCount3;

    ? ?/* 輸入內外菱形高度,并進行合法判斷 */
    ? ?for (;;)

    ? ?{
    ? ? ? ?printf("輸入內外菱形高度(最大%d行):外菱形高度,內菱形高度:\n", MAX_DIAMOND_HEIGHT - 1);
    ? ? ? ?scanf("%d,%d", &nOut, &nIn);
    ? ? ? ?if (nIn < nOut && nOut < MAX_DIAMOND_HEIGHT && nIn >= 0)
    ? ? ? ? ? ?break;
    ? ? ? ?printf("輸入不合法,請重新輸入:\n\n");
    ? ?}
    ? ?/* 循環輸出菱形 */
    ? ?nIn = nOut - nIn;
    ? ? ? ? ? ?/* 調整為菱形內外差值 */
    ? ?for (nRow = -nOut+1; nRow < nOut; nRow++)
    ? ?{
    ? ? ? ?//行號
    ? ? ? ?printf("%-010d", nRow); ? ? ? ? ? ? ? ? ? ?/* 輸出前導空格 */
    ? ? ? ?nCount1 = nRow >= 0 ? nRow : -nRow; ? ? ? ?/* 取絕對值 */ ? ? ? ?
    ? ? ? ?for (n = 0; n < nCount1; n++)
    ? ? ? ? ? ?
    ? ? ? ? ? ?printf(" ");
    ? ? ? ?/* 輸出左邊星號 */
    ? ? ? ?nCount1 = nOut - nCount1; ? ? ? ? ? ?/* 外三角部分,后續迭代使用 */

    ? ? ? ?nCount2 = myGreater(nCount1 - nIn); ?/* 內三角部分,后續迭代使用 */

    ? ? ? ?nCount3 = nCount1 - nCount2; ? ? ? ? /* 內外之差為實際需要輸出 */

    ? ? ? ?for (n = 0; n < nCount3; n++)
    ? ? ? ? ? ?printf("*"); ? ? ? ?/* 輸出中間空格 */
    ? ? ? ?nCount3 = 2 * nCount2 - 1; ? ? ? ? ? /* 由三角形拓展為菱形 */
    ? ? ? ?for (n = 0; n < nCount3; n++)

    ? ? ? ? ? ?printf(" ");
    ? ? ? /* 輸出右邊星號 */
    ? ? ? ?nCount1--; ? ? ? ? ? ? ? ? ? ? ? ? ? /* 外三角部分 */

    ? ? ? ?nCount2 = myGreater(nCount1 - nIn); ?/* 內三角部分 */

    ? ? ? ?nCount3 = nCount1 - nCount2; ? ? ? ? /* 內外之差為實際需要輸出 */

    ? ? ? ?for (n = 0; n < nCount3; n++)
    ? ? ? ? ? ?printf("*"); ? ? ?/* 輸出換行 */
    ? ? ? ?printf("\n");
    ? ?}
    ? ?
    return 0;
    }
    /* 取大于0的數 */
    int
    myGreater(int n)
    {
    ? ?if (n < 0)
    ? ? ? ?return 0;

    ? ?return n;

    }


    不知正在閱讀的你看到這個標準答案的感覺,能否尋找出和自己程序的差異的地方,下一節我們以此程序為起點,給大家介紹一些真實產品中的代碼特點。


    撇開大道理講嵌入式代碼


    在上一節中,我們給出了標準答案,是以真實產品代碼風格寫的,期望小伙伴們能同自己的實現比較一下。

    現在的90后是有個性的一代,很多人都非常反感直接的大道理灌輸,鑒于此,我期望我們的小伙伴們能自己需尋找答案,然后大家在交流碰撞中成長,效果或許會更好一些。

    這篇文章就讓我們一起來找出標準答案中有價值的地方吧。

    1. 有意義的變量命名

    大學老師教編程的時候,重點精力都放在了語法方面,側重于將所有的語法給大家展現一下(這種學習方法我相當不贊同,后續會展現自己的方法,項目組內俗稱大樹法則的方法),因此經常使用短小的程序展示語法,但因為程序短小,因此變量命名也就隨意了一些,因此i,j,k,m,n就成了常客,然后不小心帶入了產品中,然后……。

    但在產品研發的時候,即使比較簡單的設備,代碼量也會比較大,為了代碼閱讀維護方便,有意義的名字就變得非常的重要了。在標準例子中,使用了nIn和nOut就是想用簡單的英語單詞表示內外菱形的高度。

    一些朋友可能讀過《可讀代碼的藝術》或《代碼整潔之道》等書籍,作者強調使用準確的英文單詞來表達特定含義。

    但我們是中國人,能想起一些簡單的單詞詞匯已經頗為不易,想準確表達更是天方夜譚,因此,項目組內經過了無數次的迭代和探索后,形成了一種變量定義習慣:盡可能使用簡單的相近英文詞匯,全局變量必須加準確含義的中文注釋,函數內的一些自動變量,因其作用范圍很小,有時候注釋可省略。

    關于變量命名的故事還有好多,這個剛剛是給大家起個頭,我們后面會有專門的系列文章介紹,記住在真實產品的代碼中,需要有意義的變量命名,忘記m和n吧。

    2. 代碼分節

    是什么是節(section),第一次知道這個概念的時候,正式全球跨千年的時候,我還在大四,我在北京一家企業打零工,當時公司承接了一個日本銀行的項目,日方對代碼質量要求很嚴格,專門派了一個專家過來給我們講解各種要求,以及其背后的道理。

    當時還很年輕,狂傲不羈,因此大部分的苦口婆心都被當做了耳旁風,但唯獨對節的概念記憶比較深刻(可能是一開始就講的是這個了,呵呵,后續的就沒耐心聽了,這個系列文章閱讀比例逐次下降,估計是同樣的道理)。

    我們在讀代碼的時候,人的思維一段時間內僅停留在一個較窄范圍的點上,如果面對的是看不到尾的代碼,會潛移默化的將其看做滅絕師太的裹腳布——又臭又長,逆反情緒悠然而生。

    因此,我們需要將代碼按邏輯分成一塊一塊的,以空格作為區分,然后每塊代碼前增加適當的注釋,解釋這一塊代碼的功能,是所謂節的概念。

    經過這樣的改造,讀代碼的時候,感覺會完全不一樣。關于節的價值,遠不值這些,后續會在編程規范系列文章中和大家慢慢道來,此時,我們僅要求小伙伴知道壘又臭又長的代碼是不對的。

    實際上在第5節中,我約定程序整體結構時,已經有這樣的意圖了,大家不放回憶并體味一下。


    /* 循環輸出菱形 */
    for
    (……)

    {

    ? ?/* 輸出前導空格 */

    ? ?/* 輸出左邊星號 */
    ? ?/* 輸出中間空格 */
    ? ?/* 輸出右邊星號 */
    ? ?/* 輸出換行 */
    }



    3. 細節化標注

    某些代碼存在著一定的深度,一段時間就會忘記,通過右側簡單的注釋加以標注,不僅便于后續代碼的閱讀理解,而且標注點一般是關鍵代碼段,給后續的代碼審查也帶來的方便。

    在示例代碼中,會看到迭代表達式右側(微信公眾號排版問題,經常到了下面一行)有簡單的標注,主要就是起這樣的作用的。

    但萬事過猶不及,很多剛入職的小伙伴喜歡在右側加好多的注釋,僅挑出有價值的進行標注,需要長期的鍛煉和慢慢的體悟,或許,那一天回頭審視自己的代碼,會將許多無用的注釋刪除的時候,就修煉到家了。

    4. 資源

    嵌入式系統中,資源是一個需要時時刻刻關注的問題。

    何為資源,在我們的概念中,不僅內存和flash空間大小是資源,cpu計算能力也是資源,甚至代碼可讀性(可維護性),代碼實現復雜度(耗去的人力成本),復用率等等諸多方面,都被我們稱之為資源。

    好鋼要用在刀刃上,但首先要明白刀刃在哪兒。缺乏了明確邊界,空談提高資源利用率是無意義的。如可讀性第一位,內存和cpu資源比較寬裕,我們那個最優雅實現版本最佳了。如果cpu計算能力緊張,上一節的標準實現更好一些。如果內存稍微寬裕,為了增加代碼可讀性,我們還有更好的方法。

    前面已經有人給我留言提到過這種方法了,不知大家有沒有感受到,鍛煉到現在這個時候,單純的菱形輸出是多么easy的事情啊,非要搞個空心菱形,將程序搞的混亂不堪。

    但加入我們用一個數組來表示整個菱形輸出,第一次以*輸出一個菱形,第二次以空格在輸出一個菱形,然后將整個數組輸出出來,是否會非常的簡單呢,代碼可讀性瞬間爆棚,執行效率也高,僅僅多占了一些內存而已。

    總結


    經過馬拉松的歷程,終于到了最后的篇章,我們來歸納匯總一下第一個空心菱形輸出例程中提到的知識點:


    1、需求清晰理解,最使用的策略是:將需求用自己的語言表達出來,和對方確認后再實施。
    2、邊界判斷,要讓小伙伴意識到:在嵌入式產品中,最終產品的魯棒性,很多時候就是表現在這樣一點一滴的簡單判據上。
    3、基本調試手段的鍛煉,工欲善其事必先利其器,無須多言。
    4、編程思維的引入,需要慢慢的體會抽象的價值,這是一個難點,但想走得遠必須扛過去。
    5、數值計算過程中,很重要的一個概念:巧用迭代。
    6、基礎編程規范的引入,體會節的概念,要意識到產品代碼可讀可維護的重要性。
    7、在嵌入式編程領域,資源是受限的,而我們要學會針尖上的舞蹈。
    8、撰寫工作筆記,善于總結,習慣去體悟成長的腳步。


    記得剛開始從事嵌入式編程的時候,我的職業導師給我欣賞了他的記事本,密密麻麻的各種調試記錄,感悟想法,技術資料,知識歸納,我終于明白了他為何獲得了全公司上上下下的認可。

    因此,我們的團隊形成了一條不成文的規矩,必須做工作筆記,不管方式,不管格式,只要開始記錄就好。


    來自嵌入式ARM.png

    我要點評

    最新評論

    • 11967272992019-05-08 11:04回復

      nb

    新人擼一發 排列五胆拖