當前位置:首頁 > C語言
  • C語言之父和Linux之父誰更偉大?

    前言 在計算機軟件領域,做出過重大貢獻的神人很多,在這閃耀的明星中,最為閃耀的莫過於「Linus  Torvalds和Dennis Ritchie」。 那麼這兩位誰的貢獻更大呢? 這是一個很難回答的問題,就如同關公戰秦瓊,仁者見仁,智者見智。 還是先對兩位大神做個介紹吧。 一、Dennis Ritchie C語言之父,UNIX之父。 1) Dennis Ritchie(1941年- 2011年10月12日) Dennis Ritchie Dennis Ritchie曾擔任朗訊科技公司貝爾實驗室下屬的計算機科學研究中心繫統軟件研究部的主任一職。1978年與布萊恩·科爾尼幹(Brian W. Kernighan)一起出版了名著《C程序設計語言(The C Programming Language)》。此書已翻譯成多種語言,被譽為c語言的聖經。 2011年10月12日,共事20年的同事Rob Pike從加州到新澤西去拜訪他,才發現他已經去世了。由於是獨居,無法知道準確的死亡時間。享年70歲。 丹尼斯·裏奇生平 丹尼斯·裏奇因為一直都是單身(大神的思想境界真的) 2) C語言 丹尼斯·裏奇創建了C編程語言和Unix 操作系統。無論是這其中哪一個項目,都可以讓他在計算機界傲視羣雄。而丹尼裏奇開發了兩大項目,可以説是計算機史上獨一無二的。 事實上,C語言在各種軟件程序,嵌入式系統開發,操作系統中,使用是最廣泛的。同時,C語言也影響了大多數現代主流的編程語言。 1960s年代後期,貝爾實驗室對計算機系統的研究進入繁盛時期。MIT、General Electric、Bell實驗室合作的Mutlics項目以失敗而告終(1969年左右)。 就是在這個時期,Ken Tompson開始寫Mutlics的替代品,他希望按照自己的設計構造一個令人舒服的計算系統(也就是Unix)。 後來在寫出第一個版本的Unix時,覺得Unix上需要一個新的系統編程語言,他創造了一個B語言。B語言是沒有類型的C,準確説B語言是Tompson把BCPL擠進8K內存,被其個人大腦過濾後的產生的語言。 由於B語言存在的一些問題,導致其只是被用來寫一些命令工具使用。恰好在這個時期,Ritchie在B語言的基礎上,進行了重新的設計改良,從而誕生了C語言。 1973年,C語言基本上已經完備,從語言和編譯器層面已經足夠讓Tompson和Ritchie使用C語言重寫Unix內核。後來,Unix在一些研究機構、大學、政府機關開始慢慢流行起來,進而帶動了C語言的發展。 1978年,K&R編寫的《The C Programming Language》出版,進一步推動了C語言的普及。 3)  unix Unix的誕生與C語言被廣泛的傳播、使用,有着密切的聯繫。 上圖時間線只顯示前幾個與C語言在相同時間段內誕生的Unix版本。 後來學術和政府組織中都在使用Unix,也正是由於Unix的風靡與興盛,帶動了C語言被廣泛的傳播、使用。 在1980年代,C語言的使用廣泛傳播,並且幾乎所有機器體系結構和操作系統都可以使用編譯器。尤其是,它已成為個人計算機的編程工具,無論是用於這些機器的商業軟件製造商,還是對編程感興趣的最終用户,都非常受歡迎。Unix分支,實在太震撼了。 來看看其中最著名的幾個分支:BSD、minix、Linux、Mac OS X... 足可見unix對現在操作系統的影響,其地位就像《易經》,為羣經之首。 易經 4)  第一個C語言編譯器是怎樣編寫的? 不知道你有沒有想過,大家都用C語言或基於C語言的語言來寫編譯器,那麼世界上第一個C語言編譯器又是怎麼編寫的呢?這不是一個“雞和蛋”的問題…… 回顧一下C語言歷史:Tomphson在BCPL的基礎上開發了B語言,Ritchie又在B語言的基礎上成功開發出了現在的C語言。在C語言被用作系統編程語言之前,Tomphson也用過B語言編寫過操作系統。可見在C語言實現以前,B語言已經可以投入使用了。因此第一個C語言編譯器的原型完全可能是用B語言或者混合B語言與PDP彙編語言編寫的。 我們現在都知道,B語言的執行效率比較低,但是如果全部用匯編語言來編寫,不僅開發週期長、維護難度大,更可怕的是失去了高級程序設計語言必需的移植性。 所以早期的C語言編譯器就採取了一個取巧的辦法:先用匯編語言編寫一個C語言的一個子集的編譯器,再通過這個子集去遞推,進而完成完整的C語言編譯器。 所以創建第一個C編譯器的難度不亞於創造C語言的難度。 如果還不理解,舉個例子,我們要建一個大廈,圖紙什麼的都已經設計好了,要開工建設。 那麼用於建造大樓的各種設備和工具:塔吊、腳手架、鉗子、螺絲刀、水平儀、捲尺等等,這些所有用到的所有工具,都是無數的公司的公司經過多年不斷研發積累才達到今天的標準。 而編譯c語言的編譯器,就相當於建設大樓所需要的各種工具,丹尼斯不光親自設計了C語言,還親自從頭到尾設計這一整套的工具。 這個工作是創造性的,可參考內容並不是很多,其難度可想而知。 二、 Linus  Torvalds(1969年12月28日- ) Linux之父、Git之父。 1)  Linus  Torvalds(1969年12月28日- ) Linus  Torvalds 芬蘭赫爾辛基人,著名的電腦程序員,Linux內核的發明人及該計劃的合作者 ,畢業於赫爾辛基大學計算機系,1997年至2003年在美國加州硅谷任職於全美達公司(Transmeta Corporation),現受聘於開放源代碼開發實驗室(OSDL:Open Source Development Labs, Inc),全力開發Linux內核。與**妻子託芙(Tove,芬蘭前女子空手道冠軍)**育有三個女孩。 Linus 劃重點:「妻子託芙(Tove,芬蘭前女子空手道冠軍)」。 跟隨着我爺爺的學院教學生涯,我也成了赫爾辛基大學的一名助教,被分配在這年秋季學期裏開始用瑞典語教授《計算機科學入門》課程。就這樣,我遇上了塔芙。 她對我一生的影響甚至比 《操作系統:設計與執行》 一書對我的影響還要大。不過,我不會用這種影響的細節來讓你煩惱的。當時,塔芙是我的班上十五個學生中的一個。她已經有了一個學齡前教育學的學位(不像在美國,芬蘭要求學齡前兒童的教師要有大學學歷),她還想學習計算機,卻不能取得像班上其他同學那樣的進步。當然,最後她還是?上去了。我們交往的過程是如此簡單。那是在 1993 年秋天,互聯網還沒有流行開來。 因此,有一天,我在這個班佈置的家庭作業就是給我發一個電子郵件(這要放在今天簡直要笑死人),我對學生説:“今天的家庭作業:發給我一個電子郵件。”其他人的郵件不是一些供記錄的短語,就是一些沒什麼意思的筆記。只有塔芙,她邀請我和她出去約會。我娶了第一個通過電子方式走近我的女人。塔芙是一個曾六次獲得過芬蘭空手道冠軍的幼兒園教師。 她的家庭很獨特,儘管我認為還不如我們家那麼離奇。 她有許多朋友。從我們在一起的第一刻起,她就像是最適合我的女人。 經過了幾個月的約會,我和我的貓蘭迪就搬到她的公寓房間去了。在搬進去後的最初兩週,我甚至都沒有動過一下我的計算機。不算上我服兵役的時間,這兩週是我自從我十歲那一年坐在外祖父膝蓋上擺弄計算機以來 ,離開計算機最長的一段時間了。 不必詳細描述,但這確實是除去服兵役之外我離開計算機最長的時間的記錄了。 ---出自linus自傳《just for fun》。 just for fun 祖師爺和祖師奶奶的相遇居然這麼浪漫,他們的第一個孩子應該就是那兩個星期造出來的吧。 2) Git Git是一個開源的分佈式版本控制系統,可以有效、高速地處理從很小到非常大的項目版本管理,它是目前世界上最先進的分佈式版本控制系統。Git 是用於 Linux內核開發的版本控制工具。 與常用的版本控制工具 CVS, Subversion 等不同,它採用了分佈式版本庫的方式,不必服務器端軟件支持,使源代碼的發佈和交流極其方便。Git 的速度很快,這對於諸如 Linux kernel 這樣的大項目來説自然很重要。Git 最為出色的是它的合併跟蹤(merge tracing)能力。 Git是一種非常流行的分佈式版本控制系統,它和其他版本控制系統的主要差別在於Git只關心文件數據的整體是否發生變化,而大多數版本其他系統只關心文件內容的具體差異,這類系統(CVS,Subversion,Perforce,Bazaar 等等)每次記錄有哪些文件作了更新,以及都更新了哪些行的什麼內容。 Git另一個比較好的地方在於絕大多數操作都可以在本地執行,而每個本地都可以從服務器獲取一份完整的倉庫代碼,而且在沒網的時候仍然可以修改和使用大部分命令,在方便的時候再跟服務器進行同步,這樣可以更好的實現多人聯合編程。 Git 2002年,Linux系統已經發展了十年了,代碼庫之大讓Linus很難繼續通過手工方式管理了,社區的弟兄們也對這種方式表達了強烈不滿,於是Linus選擇了一個商業的版本控制系統BitKeeper,BitKeeper的東家BitMover公司出於人道主義精神,授權Linux社區免費使用這個版本控制系統。 安定團結的大好局面在2005年就被打破了,原因是Linux社區牛人聚集,不免沾染了一些梁山好漢的江湖習氣。 「開發Samba的Andrew試圖破解BitKeeper的協議(這麼幹的其實也不只他一個),被BitMover公司發現了」(監控工作做得不錯!),於是BitMover公司怒了,要「收回Linux社區的免費使用權」。 Linus本可以向BitMover公司道個歉,保證以後嚴格管教弟兄們,嗯,但是Linus不是一般人,「道歉是不可能的,這輩子都不可能的」。 於是Linus花了「兩週時間」自己用C寫了一個分佈式版本控制系統,這就是Git!一個月之內,Linux系統的源碼已經由Git管理了! 「牛」是怎麼定義的呢?大家可以體會一下。 Git Git迅速成為最流行的分佈式版本控制系統,尤其是2008年,GitHub網站上線了,它為開源項目免費提供Git存儲,無數開源項目開始遷移至GitHub,包括jQuery,PHP,Ruby等等。 GitHub,全世界開發者的安全空間,在這裏,你可以分享你的代碼為大家所用,也可以和全世界的開發者一起共建完善你的代碼。現在有越來越多的公司都把代碼放在了Github服務器上。 一口君從第一次用過之後就愛不釋手,被其中的設計哲學深深折服,因為Git管理軟件版本實在太過方便了。 通過commit來研究和學習一個軟件產品如何從最初code base慢慢迭代成一個成熟的產品,這是提升自己技術水平最快捷之路。 3) Linux Linux時間線 Linux時間線比較龐大,詳情請複製下面鏈接到瀏覽器://upload.wikimedia.org/wikipedia/commons/1/1b/Linux_Distribution_Timeline.svg 1991 8月25號 : 21歲的芬蘭學生Linus Benedict Torvalds 在comp.os.minix 新聞組上宣佈了它正在編寫一個免費的操作系統。 9月1號 : Linux 0.01在網上發佈。 1992 1月5號 : Linux v0.12 release 版本的內核重新以GUN GPL的協議發佈。原來的許可證是禁止任何商業用途的。通過這次協議變更,發佈和出售修改或未修改版的Linux成為了可能,只要你將這些複製版本以相同的GPL許可證發佈,並且有相對應完整的源代碼。在後來的一次採訪中,Linus對這次許可證的更改説了這樣一句話(讓Linux遵守GPL絕對是我幹過的最正確的事): " Making Linux GPL'd was definitely the best thing I ever did." 1月29號 : Andrew S. Tanenbaum向comp.os.minix郵件列表發送了一封名為LINUX is obsolete的郵件。總的來説,這次被一些人升級為“戰火”的爭論是關於Linux和內核架構的。Tanenbaum爭辯説微內核比宏內核更加高級,所以Linux是過時的。 4月5號 : 第一個Linux新聞組,comp.os.linux由Ari Lemmke提議和開通。 5月21號 : Peter MacDonald 發佈第一個獨立的Linux安裝包SLS。可以通過軟盤安裝,包括比較前沿的TCP-IP網絡支持和X Window系統。建議至少預留10M的磁盤空間來安裝。 1993 6月17號 : Slackware Linux由Patrick Volkerding發佈。Slackware被認為是第一個取得廣泛成功的Linux發行版,而且它現在還在使用。 8月16號 : Ian Murdock(Debian中的'ian')發佈了第一個Debian Linux的發行版。Debian是最有影響力的Linux發行版之一,是MEPIS,Mint,Ubuntu和很多其它發行版的鼻祖。 8月19號 : Matt Welsh寫的《Linux Installation and Getting Started》第1版出版,這是第一本關於Linux的書籍。 1994 3月14號 : Linux內核V1.0發佈。它支持基於i386單處理器的計算機系統。這3年來,內核代碼庫已經增長到了176,250行。 3月26號 : 第一期《Linux Journal》雜誌發行。這一期雜誌的特點是發表了一篇對Linus Torvalds的採訪和一些Phil Hughes, Robert “Bob” Young, Michael K. Johnson, Arnold Robbins, Matt Welsh, Ian A寫的文章。 8月15號 : Willian R. Della Croce, Jr. 申請了“Linux”商標,9月進行了註冊。Della Croce在不知道Linux社區財政窘迫的前提下,向煊赫的“Linux公司”寫了一封信,要求他們為“Linux”商標使用支付費用。直到1997年,這次風波以將商標轉讓給代表所有請願者和Linux使用者的Linus Torvalds而告終。 11月3號 : Red Hat的共同創始人Marc Ewing宣佈可以以49.95美元的零售價格獲得Red Hat Software Linux的CD-ROM和30天的安裝支持。2012年Red Hat成為第一家市值達10億美元的開源公司。 1995 4月4號 : 開展了第一個專門針對Linux的貿易展和會議系列,名字叫Linux Expo。這成為接下來幾年中,最流行和備受關注的年度Linux盛會。貿易展和會議的入場券價格是4美元。3年後,Red Hat接管了組織工作,同時也是主要的贊助商。 1996 5月9號 : 最初由Alan Cox提議,之後又經Linus Torvalds改良,Larry Ewing在1996年創造了現在看到的這隻叫做Tux的吉祥物。選定企鵝作為Linux吉祥物的主意來自Linus Torvalds,他説自己被一隻企鵝輕輕地咬了一口之後就具有了企鵝的特徵。(這是要變成企鵝俠嗎?估計是原作者調皮了?--譯註) 6月9號 : Linux內核V2.0發佈。相比更早的版本這是一次意義重大的提升,這是第一個在單系統中支持多處理器的穩定內核版本,也支持更多的處理器類型。Linux從此以後成了很多公司一個鄭重選擇的對象。你可以閲讀1996年8月在Linux Journal上發佈的回顧Linux V2.0來了解更多相關的提升。 10月14號 : 1996年Mattias Ettrich發起了KDE項目,因為他深受Unix桌面系統下應用程序的不一致之苦。(在此之前Unix和Linux都沒有一個統一的桌面系統,編寫桌面軟件非常複雜--譯註) 1997 1月9號 : 第一個“Linux病毒”Bliss被發現了。Bliss不危害系統的安全,它依賴於人們用特權幹蠢事來感染系統,然後提醒用户只安裝從可靠站點下載的可以驗證數字簽名的軟件,並且安裝之前一定要先驗證簽名(很多地方把這個算作第2個Linux病毒,因為之前還有一個更加“綠色”的病毒--譯註)。“事實上,在Linux上寫一個病毒可能會更加的簡單,因為Linux是開源的,所有的源代碼都是可以獲取的。所以,隨着Linux變得更加通用和流行的時候,我們將看到更多的Linux病毒。”--來自McAfee的暢想。 1998 5月1號 : Google搜索引擎面世。它不僅僅是世界上最好的搜索引擎之一,更是基於Linux的,它的特徵是有一個Linux的搜索頁面。 12月4號 : 一份來自IDC的報告稱1998年Linux的出貨量至少上升了200%,市場佔有率上升至少150%。Linux的市場佔有率為17%,並且以其它任何操作系統無法企及的速度增長着。 1999 2月9號 : Linux和BSD使用者們發起了“Windows退款日”。他們聯合起來造訪了微軟公司,希望退還他們在買電腦時綁定購買Windows許可證的錢,這些許可證他們從來沒有用過。 3月3號 : 另一個頗具影響力的桌面系統進入了Linux的世界,就是GNOME桌面系統。在很多主要的Linux發行版比如Debian,Fedora,RedHad Enterprise Linux和SUSE Linux Enterprise Desktop中,GNOME是默認的桌面環境。 2000 2月4號 : 最新的IDC報告表明Linux現在排在“最受歡迎的服務器操作系統的第2位”,1999年服務器系統銷售量佔總量的25%。Windows NT以38%位列第1,NetWare以19%排在第3位。 3月11號 : 摩托羅拉公司宣佈發行HA Linux。這個發行版專注於通信應用領域,對系統不關機連續運行時間要求非常高。它還包括了熱交換能力和支持i386和PowerPC架構。 3月23號 : 愛立信公佈了“Screen Phone HS210”,這是一款基於Linux的觸屏手機,具備郵件和網頁瀏覽等功能。愛立信和Opera Software公司同時宣佈這款手機將會安裝Opera的網頁瀏覽器。 10月30號 : 第一個Linux live發行版由Linux諮詢顧問Klaus Knopper發佈,名字叫做Knoppix。 2001 1月3號 : 美國NAS(美國國家安全局)以GPL許可證發佈了SELinux。SELinux提供了標準Unix權限管理系統以外的另一層安全檢查。 2003 3月6號 : SCO Group公司宣佈他們正在發起對IBM高達10億美元的訴訟,他們聲稱IBM把SCO的商業機密整合到了Linux中。之後SCO公司發起了一系列的法律訴訟案,這威脅到了很多計算機行業的巨頭包括惠普,微軟,Novell,Silicon Graphics,Sun Microsystems和RedHat。這次案件在Novell公司的支持下於2010年3月30號判決完畢。(著名的SCO-Linux爭議,SCO聲稱擁有System V的部分源碼所有權,IBM將這部分源碼整合到Linux中侵犯了SCO著作權。最後聯邦法院裁定Novell才是Unix商標的合法擁有者。--譯註) 2004 10月20號 : Ubuntu以一個不同尋常的版本號4.10和怪異的版本代號“Warty Warthog”(長滿疙瘩的非洲疣豬)進入大家的生活。用這個版本號是因為發佈日期是2004年10月。Ubuntu的開發由Cannonical Ltd公司主導,公司的創始人是Mark Shuttleworth(就是那個不到30歲的億萬富翁,錢多的不知道怎麼花,只能燒錢上太空的遊一圈的人--譯註)。Ubuntu雖然不是內核的主要貢獻者,然而對於Linux的台式機和筆記本電腦的普及,Ubuntu扮演着一個重要的角色。 2007 6月6號 : 華碩在2007的台北電腦展上展出了兩款“易PC”(Eee PC):701和1001。第1批易PC預裝的是Xandros Linux,這是一個基於Debian,輕量級的為適應小屏幕進行過優化的Linux發行版。 8月8號 : 2007年Linux基金會由開源發展實驗室(OSDL)和自由標準組織(FSG)聯合成立。這個基金會目的是贊助Linux創始人Linus的工作。基金會得到了主要的Linux和開源公司,包括富士通,HP,IBM,Intel,NEC,Oracle,Qualcomm,三星和來自世界各地的開發者的支持。 「11月5號 : 與之前大家推測的發佈Gphone不同,Google宣佈組建開放手機聯盟(Open Handset Alliance)和發佈Android,它被稱為“第一個真正開放的綜合移動設備平台”。」 2009 1月29號 : 2009年1月紐約時報稱“現在預計有超過10億人在運行Ubuntu系統”。 2011 5月11號 : 2011年Google I/O大會發布了Chrombook。這是一款運行着所謂雲操作系統Chrome OS的筆記本。Chome OS是基於Linux內核的。 6月21號 : Linus Torvalds 發佈了Linux3.0版本。 2013 12月13號 : Valve公司發佈基於Linux的SteamOS操作系統,這是一個視頻遊戲控制枱系統。 4)《大教堂與集市》 Linus發明Linux過程有點像將簡陋的集市構建成一個宏偉壯麗的大教堂的一個過程。 《大教堂與集市》這本書分析了這種設計哲學。 大教堂與集市 世界上的建築可以分兩種:「一種是集市」,天天開放在那裏,從無到有,從小到大;還有一種是「大教堂」,幾代人嘔心瀝血,幾十年才能建成,投入使用。 「當你新建一座建築時,你可以採用集市的模式,也可以採用大教堂的模式。」 一般來説,集市的特點是開放式建設、成本低、週期短、品質平庸;大教堂的特點是封閉式建設、成本高、週期長、品質優異。 Eric Raymond總結了集市要變成大教堂,有幾個前提條件: 1)你不能從零開始建設集市,你必須先有一個原始項目。(It's fairly clear that one cannot code from the ground up in bazaar style.)2)你的原始項目可以有缺陷,但是它必須能運行。(It can be crude, buggy, incomplete, and poorly documented. What it must not fail to do is run.)3)你必須向用户展示一個可行的前景,且讓潛在的合作者相信在可預見的將來它會變成一個真正漂亮的東西。(When you start community-building, what you need to be able to present is a plausible promise, and convince potential co-developers that it can be evolved into something really neat in the foreseeable future.)4)項目的主持者本身不一定是天才,但他一定要能夠慧眼識別出他人的優秀想法。(it is not critical that the coordinator be able to originate designs of exceptional brilliance, but it is absolutely critical that the coordinator be able to recognize good design ideas from others.)5)項目的主持者必須要有良好的人際關係、交流技能和人格魅力。這樣才能吸引他人,使別人對你所做的事感興趣,願意幫助你。(A bazaar project coordinator or leader must have good people and communications skills.) Eric Raymond同時也總結了一些成功的充分條件。 1)項目首先必須是你自己感興趣的,但是最終能對其他人有用。2)將用户當作合作者。3)儘快地和經常地做出改進,多聽取用户的意見。4)健壯的結構遠比精巧的設計來得重要。換句話説,結構是第一位的,功能是第二位的。5)保持項目的簡單性。設計達到完美的時候,不是無法再增加東西了,而是無法再減少東西了。 一個開放式的項目,如果加以良好的管理和運作,能取得比同等的封閉式項目大得多的成功。 三、比較 已經介紹了兩位大神的生平的功績,那下面我們來做個點評吧。 其實究竟誰更強,誰的貢獻更大,作為一個晚輩程序員,對他們只有膜拜的分,豈敢隨便下結論説他們誰更強,只能從他們的工作內容和主要成就來説説個人看法,不足之處,還請指正。 1. 成果 首先説説C語言之父丹尼斯•裏奇(Dennis M. Ritchie),他對C語言的貢獻大家有目共睹,不必多説。 除了是C語言的主要發明者之外,他還因為對Unix操作系統的重大貢獻而被稱為Unix之父。然而他的貢獻遠非如此,在Unix取得了巨大成功之後,他們在20世紀80年代開始研究一個名為Plan 9的操作系統,其目的就是解決Unix中的一些問題。 在之後的幾十年中,該項目演變出了一個叫Inferno的項目以及一個名為Limbo的新語言,而該語言被公認為是目前火熱的編程語言Go的前身。 從上面可以看出,丹尼斯•裏奇的貢獻是多方面的,除了編程語言還有操作系統,也正是因為如此,他獲得了無數的獎項,其中最著名的就是「圖靈獎」(1983)和「計算機先驅獎」(1994)。 再説説Linux之父,Linus Torvalds,他的貢獻主要是開發了Linux操作系統的內核,然後將之開源公佈於世,最後形成了對計算機行業影響巨大的一套操作系統,並且因此獲得了計算機先驅獎(2014)。 2005年用兩週的時間就研發出了Git,而這個軟件版本管理軟件影響了全球無數的軟件開發者。 成果上來説,二位實在不相上下。 2. 難度 從難度上講,編譯器要比操作系統難度高很多,而且linux並不是從零起步的,它是繼承unix系統內核的,使之適應PC兼容機,而我們知道,C語言之父Dennis M Ritchie同時也是unix的創建者之一,在發明C語言之後,他自己又重新用C寫了一遍unix,linus對他就是小弟弟了,寫個操作系統對他而言並不費事。 現代的windows、linux給人感覺很龐大,因為它們是個平台,上面有海量應用和中間件,編譯器給人感覺很小,但對內行來説,「編譯器的難度要遠高於操作系統」。 操作系統是需要耗費很多人工開發上面的應用,是軟件界的勞動密集產品,而編譯器更像是大師的藝術品,編譯器是生產其它軟件的工廠,對它的要求非常高。 我們國家其實早就能做操作系統了,985大學裏專業的本科學生就能交一份非商用的操作系統作業,但自己的編譯器還是零,並不是學了編譯原理就寫的來編譯器的。 3. 編程水平 從他們的工作成就來看,我認為丹尼斯•裏奇更厲害一些,他的成就是多方面的,而且最關鍵的一點,Linux的系統原型(Unix的變體minix)和編程語言(C語言)都來自於丹尼斯•裏奇的貢獻的。但是如果僅僅從編程能力來説,到他們那種級別對編程都已經是爐火純青,很難分出高下! C語言之父相當於自己動手造了一台挖掘機。人們可用他的挖掘機做能做的事。 linux之父相當於自己帶頭挖了幾個基坑,然後有一大票自帶磚瓦、鋼筋、混凝土的小夥伴共同建起一座大廈。又因為是linus是帶頭人,所以大廈名字由他起且看門大爺是他當。 況且,蓋大廈(寫操作系統)這種事,裏奇同志早就幹過了,而且是用自家造的機器和兩個小夥伴就幹成了! 應該説在Dennis M Ritchie面前Linus就是弟弟。 4. 對社會直接貢獻 兩位都為社會進步做了很大貢獻。如果硬要比較的話,個人覺得linux和git的直接貢獻更大一些。 沒什麼好説的,去各大軟件公司走一圈就知道了,幾乎沒有不用Linux的。 Linux應用的領域非常之廣: Linux在服務器領域的發展 隨着開源軟件在世界範圍內影響力日益增強,Linux服務器操作系統在整個服務器操作系統市場格局中佔據了越來越多的市場份額,已經形成了大規模市場應用的局面。並且保持着快速的增長率。尤其在政府、金融、農業、交通、電信等國家關鍵領域。此外,考慮到Linux的快速成長性以及國家相關政策的扶持力度,Linux服務器產品一定能夠衝擊更大的服務器市場。 據權威部門統計,目前Linux在服務器領域已經佔據75%的市場份額,同時,Linux在服務器市場的迅速崛起,已經引起全球IT產業的高度關注,並以強勁的勢頭成為服務器操作系統領域中的中堅力量。 Linux在桌面領域的發展 近年來,特別在國內市場,Linux桌面操作系統的發展趨勢非常迅猛。國內如中標麒麟Linux、紅旗Linux、深度Linux等系統軟件四方a集運倉電話都推出的Linux桌面操作系統,目前已經在政府、企業、OEM等領域得到了廣泛應用。另外SUSE、Ubuntu也相繼推出了基於Linux的桌面系統,特別是Ubuntu Linux,已經積累了大量社區用户。但是,從系統的整體功能、性能來看,Linux桌面系統與Windows系列相比還有一定的差距,主要表現在系統易用性、系統管理、軟硬件兼容性、軟件的豐富程度等方面。 Linux在移動嵌入式領域的發展 Linux的低成本、強大的定製功能以及良好的移植性能,使得Linux在嵌入式系統方面也得到廣泛應用,目前Linux以廣泛應用於手機、平板電腦、路由器、電視和電子遊戲機等領域。在移動設備上廣泛使用的Android操作系統就是創建在Linux內核之上的。目前,Android已經成為全球最流行的智能手機操作系統,據2015年權威部門最新統計,Android操作系統的全球市場份額已達84.6% 此外,思科在網絡防火牆和路由器也使用了定製的Linux,阿里雲也開發了一套基於Linux的操作系統“YunOS”,可用於智能手機、平板電腦和網絡電視;常見的數字視頻錄像機、舞台燈光控制系統等都在逐漸採用定製版本的Linux來實現,而這一切均歸功於Linux與開源的力量 Linux在雲計算/大數據領域的發展 互聯網產業的迅猛發展,促使雲計算、大數據產業的形成並快速發展,雲計算、大數據作為一個基於開源軟件的平台,Linux佔據了核心優勢;據Linux基金會的研究,86%的企業已經使用Linux操作系統進行雲計算、大數據平台的構建,目前,Linux已開始取代Unix成為最受青睞的雲計算、大數據平台操作系統 國內 Linux 操作系統發展現狀 國內目前涉足Linux操作系統研發除學校、研發機構外,主要 Linux 發行版包括紅旗、中標、共創、新華、拓林思等,均有桌面和服務器兩個版本;國內各發行版均基於國際社區版本發展而來,基於國際社區成果,在界面定製上做了一些工作,並沒有掌握核心技術,且與國際Linux 操作系統發行版之間存在一定的技術差距,缺少技術積累,面臨Linux發展後勁不足等問題 國外Linux操作系統發展現狀 國外 Linux 操作系統發展現狀 國外主要發行版包括redhat、ubuntu、Suse 等,均提供桌面和服務器兩個不同版本。服務器領域 Linux 操作系統發展比較成熟,桌面發展比較緩慢,嵌入式領域發展較快。 可以説我們的生活已經無法離開Linux,絕大部分軟件工程師都會用到Linux,但一定要記住這些都是建立在C語言的基礎之上。 5. 個人理解 其實2人都不是從0開始的, 一個是基於B語言和unix系統的需求出發創造了c語言, 一個是基於minix和unix思想還有當時的機遇開發了linux。都是站在前任的肩膀上成功的。 在彭老師心裏,Dennis Ritchie更加重要一些,因為他用C語言開闢了一個新的世界,從誕生開始到現在,一直穩居語言榜前三位,都快50年了,我們依然在用它。 而Linux在服務器和移動端的佔有率實在太高,直接貢獻更多一些,而linus最厲害的一點是他做到了協調世界各地數以百萬計的開發者開發維護linux,這難度更大些。 彭老師內心可能更崇拜linus多一些,尤其他那一句“「Talk is cheap!Show me your code!」”! Linus  Torvalds怒懟英偉達 一起欣賞下祖師爺那性感的中指!!【四方a集運倉電話】 要獲取電子書《大教堂與集市》,關注後台回覆 [ 大教堂與集市 ]。 部分圖片來自於網絡侵權刪。參考:知乎 騰訊技術工程 Linus自傳《just for fun》 END 來源:一口Linux,作者:土豆居士 版權歸原作者所有,如有侵權,請聯繫刪除。 ▍ 推薦閲讀 成功為華為“續命:中國芯片之父張汝京 一個工程師的“噩夢”:剛分清CPU和GPU,卻發現還有…… 這位“華為天才少年”,竟然要我用“充電寶”打《只狼》 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2021-01-12 關鍵詞: C語言 Linux

  • C 與 C++ 的真正區別在哪裏?

     

    時間:2021-01-11 關鍵詞: 區別 C C語言

  • 20個成熟軟件中常用的宏定義,趕快收藏!

      1#ifndef COMDEF_H 2#define COMDEF_H 3//頭文件內容 4#endif 2. 重新定義一些類型,防止由於各種平台和編譯器的不同,而產生的類型字節數差異,方便移植。 1#define MEM_B( x ) ( *( (byte *) (x) ) ) 2#define MEM_W( x ) ( *( (word *) (x) ) ) 4. 求最大值和最小值 1#define FPOS( type, field ) \ 2/*lint -e545 */ ( (dword) &(( type *) 0)-> field ) /*lint +e545 */ 6. 得到一個結構體中field所佔用的字節數 1#define FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] ) 8. 按照LSB格式把一個Word轉化為兩個字節 1#define B_PTR( var ) ( (byte *) (void *) &(var) ) 2#define W_PTR( var ) ( (word *) (void *) &(var) ) 10. 得到一個字的高位和低位字節 1#define RND8( x ) ((((x) + 7) / 8 ) * 8 ) 12. 將一個字母轉換為大寫 1#define DECCHK( c ) ((c) >= '0' && (c) <= '9') 14. 判斷字符是不是16進制的數字 1#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val)) 16. 返回數組元素的個數 1#define MOD_BY_POWER_OF_TWO( val, mod_by ) \ 2( (dword)(val) & (dword)((mod_by)-1) ) 18. 對於IO空間映射在存儲空間的結構,輸入輸出處理 A N S I標準説明了五個預定義的宏名。它們是: 1_ L I N E _ 2_ F I L E _ 3_ D A T E _ 4_ T I M E _ 5_ S T D C _ 如果編譯不是標準的,則可能僅支持以上宏名中的幾個,或根本不支持。記住編譯程序也許還提供其它預定義的宏名。 _ L I N E _及_ F I L E _宏指令在有關# l i n e的部分中已討論,這裏討論其餘的宏名。 _ D AT E _宏指令含有形式為月/日/年的串,表示源文件被翻譯到代碼時的日期。 源代碼翻譯到目標代碼的時間作為串包含在_ T I M E _中。串形式為時:分:秒。 如果實現是標準的,則宏_ S T D C _含有十進制常量1。如果它含有任何其它數,則實現是非標準的。 可以定義宏,例如: 當定義了_DEBUG,輸出數據信息和所在文件所在行 1#ifdef _DEBUG 2#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_) 3#else 4#define DEBUGMSG(msg,date) 5#endif 20. 宏定義防止使用時錯誤用小括號包含。

    時間:2021-01-11 關鍵詞: 宏定義 C語言

  • 單片機程序開發時,常見錯誤

    這裏利用一個實際發生的例子,針對初級工程師經常犯的一個小錯誤,或者經常要走的一個彎路,做了針對性的糾正。希望可以幫到大家,文筆不好文章中有敍述不清的地方大家多多指教。 這篇文章我不是想説編程的規範性的東西,如果你想讓自己的程序文件最起碼直觀的看起來美觀、可讀性強,推薦找華為的“C語言編程規範”。我只想説一説當我們的單片機遇到多個模塊的數據需要處理,類似於“多任務”時我們應該怎麼辦? 背景是這樣的,去年9月份開始安排一個工程師開始做電動汽車交流充電樁,機械設計部分由公司機械結構部門負責。充電樁的電子部分總體上分為X個部分(用到的資源),電阻觸摸屏(RS232),M1卡讀寫(RS232),電能計量表(RS485),語音提示(SPI),電力開關(繼電器IO),通訊接口(RS485、CAN)。 工程師做的過程非常勤奮,期間也是困難重重,改了很多個版本,總算今年6月把充電樁立起來了。 咱們來驗收一下吧,結果發現讀卡的時候不能處理觸摸屏,播放語音的時候不能處理讀卡,語音播放不能打斷或者跳躍,反正就是所有事件必須一個一個按部就班的來,一旦操作錯誤就需要多次執行、等待、甚至重新來過。 一個工作3年多的工程師怎麼會把產品做成這樣呢?看看程序吧! 一看不要緊,嚇一跳!整個的程序是沒有邏輯的,一條線就往下寫……   While(1)  {  //上電進入主程序 或 觸發觸摸屏  //播放提示語音  Delay();//等待播放完畢  //讀取M1卡信息  Delay();//等待讀卡數據返回  //播放提示語音  Delay();//等待播放完畢  //M1卡數據交互,判定下一步操作及提示  Delay();//等待數據處理完畢  ……  ……  } 這裏説這個工程師基本上對於自己設計的產品沒有任何的整體概念,或者説對自己開發的程序用到設計上會有怎樣的實際效果根本就不清楚。 他犯了幾個我們在程序開發過程中最忌諱的幾個問題: 1、 delay(死等)這類函數只在應該實驗室驗證某個功能過程中用到,在實際的產品開發時無論是主循環while中,還是其調用的函數中,亦或是中斷服務程序中絕對不可以用到。 2、 產品設計的各個子模塊之間的邏輯關係太強,例如:必須等待播音完畢才能讀卡進入下一步操作等。 我們講,產品設計中只有各個事件處理模塊間的邏輯關係弱化,才能更加靈活的進行處理。例如:兩個事件A和B,如果程序開發時將A做成B事件的必要條件,B事件的觸發就必須等待A事件的發生。反之如果A事件作為B事件處理的一個特殊情況,那麼程序開發起來就變得靈活很多。   3、 沒有考慮到單片機本身是一個單核單任務的架構,每一個事件都會獨佔CPU內核,當多個任務模塊同時存在時我們應該對各個事件進行區分,我們應當分情況、分事件實時性要求等區分對待。   那麼針對於這樣的問題,或者是遇到類似的項目我們應該如何處理呢?   我提幾條建議:   1、將硬件系統區分為獨立單元單獨做成底層驅動函數和應用函數,並且函數正常應該有參數和返回值,其中返回值是必要的。如何衡量這類函數呢?這類函數可移植性強,只要一個.h文件和一個.c文件就可以隨意放到任何工程中。例如:語音播放、M1讀卡、485處理等等。   2、將1中的所有函數進行時間評估,評估點有兩個。一個是函數的執行時間t,第二個是函數的週期性發生的時間T,一個最基本的條件是t < T,理想情況應該是t

    時間:2021-01-08 關鍵詞: 單片機 C語言

  • 養成良好的嵌入式C代碼編碼習慣要遵循哪些規則?

    嵌入式C代碼 Cortex-M這類微控制器編程通常採用C代碼,那麼編程人員如何編寫代碼才能讓C編譯器產生高質量底層代碼就成為一個很重要的話題。這裏所説的高質量底層代碼是指既達到編程人員意圖又方便編譯器優化的代碼。 本文將從編寫利於優化的源代碼,節省棧和內存空間,函數原型,整型和位取反,同時讀寫變量的保護,不進行初始化的變量這幾個方面來討論如何編寫良好的嵌入式C代碼。 一、編寫利於優化的源代碼 我們在編寫源代碼的時候如果能夠遵循以下幾點,可以讓編譯器更好的對代碼進行優化: 1)局部變量(自動變量和參數)比靜態或全局變量要更好。 為什麼這麼説呢,因為優化器會假定任何一個函數都可能修改靜態或全局變量。當局部變量的生命週期結束的時候,它所佔據的內存就可以被其它變量使用,而全局變量在整個程序的生命週期內都不會釋放它所佔據的內存空間。 2)避免用&運算符取局部變量的地址。 這裏有兩個原因會導致該操作的效率低下。首先,變量必須放在內存中,不能放在處理器的寄存器中,這將導致更長更慢的代碼效率。其次,優化器不再假設其它的函數,因此不會影響到該變量。 3)編譯器的內聯函數能力。 為了最大限度的影響編譯器的內聯轉換,我們最好把那些多個模塊都用到的小函數寫在頭文件中而不是實現文件中。 二、節省棧和內存空間 以下的編程技術可以讓我們節省內存和棧空間: 1)如果棧空間有限,那麼我們就要儘量避免長的調用鏈和遞歸函數。 2)避免使用大的聚合類型(比如結構體)作為參數或者返回類型。為了節省棧空間,我們應該更多的使用指針來代替這種聚合類型。 三、函數原型 有兩種函數的定義和聲明方式可以使用。一種是原型風格,一種是Kernighan & Ritchie C風格。兩種風格都是可以的,但強烈建議應用原型風格,也就是説對每一個公共函數都在相應的頭文件中提供一個原型聲明。 這是因為編譯器對應用Kernighan & Ritchie C風格的參數不進行類型檢查。應用原型風格在某些情況下將產生高效的代碼,因為它不需要進行參數類型提升。 為了保證所有的公共函數都在定義之前聲明過,可以打開編譯器選項 Project>Options>C/C++ Compiler>Language 1>Require prototypes 以下是兩種風格的示例 1)原型風格: 原型風格中,必須寫明每個參數的類型。 int Test(char, int); /* 聲明 */ int Test(char ch, int i) /* 定義 */ { return i - ch; } 2)Kernighan & Ritchie風格: Kernighan & Ritchie風格中,不需要進行函數原型聲明。取而代之的是一個空參數列表的函數聲明。函數的定義也有些不同。 int Test(); /* 聲明 */ int Test(ch, i) /* 定義 */ char ch; int i; { return i - ch; } 四、整型和位取反 在某些情況下,整數類型和它們的轉換提升規則會導致難以理解的行為。這經常出現在賦值或者條件表達式中,這裏涉及不同長度類型的數據和邏輯操作尤其是位取反操作。這裏的類型也包括常數類型。例如:1個8位的字符類型,1個32位的整數類型,按照二進制補碼操作。 void F1(unsigned char c1) { if (c1 == ~0x08); } 這裏,測試條件總是false。因為右邊的0x08 = 0x00000008,~0x00000008 = 0xFFFFFFF7。左邊的c1是1個8位無符號字符類型,因此它不可能比255大,也不可能是負數,這就意味着它的高24位不可能置1。所以這個測試條件總是false的。 五、同時讀寫變量的保護 在中斷程序或者單獨線程中用到的變量經常是異步讀寫的,它們必須進行適當地標記和適當的保護。 編譯器應用volatile關鍵字對這類變量進行標記。這個關鍵字通知編譯器該對象的值無任何持久性,不要對它進行任何優化。它迫使編譯器每次需要該對象數據內容的時候都必須讀該對象,而不是隻讀一次數據並將它放在處理器的寄存器中以便後續訪問之用。 六、不進行初始化的變量 通常,運行時環境在應用程序啓動的時候會初始化所有的靜態和全局變量。編譯器支持用__no_init關鍵字來聲明不進行初始化的變量。用__no_init關鍵字聲明的變量通常用在大的數據輸入緩衝這樣的地方。 本文介紹了編寫良好的嵌入式C代碼涉及的多個方面。編寫良好的嵌入式C代碼需要大量的專業知識,本文雖盡力描述編寫良好的嵌入式C代碼所需要的各種技能,但難免會有不足的地方,希望大家多多指正。 END 版權歸原作者所有,如有侵權,請聯繫刪除。 ▍ 推薦閲讀 成功為華為“續命:中國芯片之父張汝京 一個工程師的“噩夢”:剛分清CPU和GPU,卻發現還有…… 這位“華為天才少年”,竟然要我用“充電寶”打《只狼》 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2021-01-06 關鍵詞: 嵌入式 代碼 C語言

  • 深度好文!C語言代碼優化方案

    轉自公號:嵌入式雲IOT技術圈 1、選擇合適的算法和數據結構 選擇一種合適的數據結構很重要,如果在一堆隨機存放的數中使用了大量的插入和刪除指令,那使用鏈表要快得多。數組與指針語句具有十分密切的關係,一般來説,指針比較靈活簡潔,而數組則比較直觀,容易理解。對於大部分的編譯器,使用指針比使用數組生成的代碼更短,執行效率更高。 在許多種情況下,可以用指針運算代替數組索引,這樣做常常能產生又快又短的代碼。與數組索引相比,指針一般能使代碼速度更快,佔用空間更少。使用多維數組時差異更明顯。下面的代碼作用是相同的,但是效率不一樣。 數組索引 指針運算 For(;;){ p=array A=array[t++]; for(;;){ a=*(p++); 。。。。。。。。。。。。。。。 } } 指針方法的優點是,array的地址每次裝入地址p後,在每次循環中只需對p增量操作。在數組索引方法中,每次循環中都必須根據t值求數組下標的複雜運算。 2、使用盡量小的數據類型 能夠使用字符型(char)定義的變量,就不要使用整型(int)變量來定義;能夠使用整型變量定義的變量就不要用長整型(long int),能不使用浮點型(float)變量就不要使用浮點型變量。當然,在定義變量後不要超過變量的作用範圍,如果超過變量的範圍賦值,C編譯器並不報錯,但程序運行結果卻錯了,而且這樣的錯誤很難發現。 在ICCAVR中,可以在Options中設定使用printf參數,儘量使用基本型參數(%c、%d、%x、%X、%u和%s格式説明符),少用長整型參數(%ld、%lu、%lx和%lX格式説明符),至於浮點型的參數(%f)則儘量不要使用,其它C編譯器也一樣。在其它條件不變的情況下,使用%f參數,會使生成的代碼的數量增加很多,執行速度降低。 3、減少運算的強度 (1)、查表(遊戲程序員必修課) 一個聰明的遊戲大蝦,基本上不會在自己的主循環裏搞什麼運算工作,絕對是先計算好了,再到循環裏查表。看下面的例子: 舊代碼: long factorial(int i) { if (i == 0) return 1; else return i * factorial(i - 1); } 新代碼: static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720 /* etc */ }; long factorial(int i) { return factorial_table[i]; } 如果表很大,不好寫,就寫一個init函數,在循環外臨時生成表格。 (2)求餘運算 a=a%8; 可以改為: a=a&7; 説明:位操作只需一個指令週期即可完成,而大部分的C編譯器的“%”運算均是調用子程序來完成,代碼長、執行速度慢。通常,只要求是求2n方的餘數,均可使用位操作的方法來代替。 (3)平方運算 a=pow(a, 2.0); 可以改為: a=a*a; 説明:在有內置硬件乘法器的單片機中(如51系列),乘法運算比求平方運算快得多,因為浮點數的求平方是通過調用子程序來實現的,在自帶硬件乘法器的AVR單片機中,如ATMega163中,乘法運算只需2個時鐘週期就可以完成。既使是在沒有內置硬件乘法器的AVR單片機中,乘法運算的子程序比平方運算的子程序代碼短,執行速度快。 如果是求3次方,如: a=pow(a,3.0); 更改為: a=a*a*a; 則效率的改善更明顯。 (4)用移位實現乘除法運算 a=a*4; b=b/4; 可以改為: a=a2; 通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR中,如果乘以2n,都可以生成左移的代碼,而乘以其它的整數或除以任何數,均調用乘除法子程序。用移位的方法得到代碼比調用乘除法子程序生成的代碼效率高。實際上,只要是乘以或除以一個整數,均可以用移位的方法得到結果,如: a=a*9 可以改為: a=(a> 1;     }   }   *r = a - *q * *q; } 推薦的代碼: // 假設 q != r void isqrt(unsigned long a, unsigned long* q, unsigned long* r) {   unsigned long qq, rr;   qq = a;   if (a > 0)   {     while (qq > (rr = a / qq))     {       qq = (qq + rr) >> 1;     }   }   rr = a - qq * qq;   *q = qq;   *r = rr; } 5、循環優化 (1)充分分解小的循環 要充分利用CPU的指令緩存,就要充分分解小的循環。特別是當循環體本身很小的時候,分解循環可以提高性能。注意:很多編譯器並不能自動分解循環。不好的代碼: // 3D轉化:把矢量 V 和 4x4 矩陣 M 相乘 for (i = 0;i < 4;i ++) { r[i] = 0; for (j = 0;j < 4;j ++) { r[i] += M[j][i]*V[j]; } } 推薦的代碼: r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3]; r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3]; r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3]; r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3]; (2)提取公共部分 對於一些不需要循環變量參加運算的任務可以把它們放到循環外面,這裏的任務包括表達式、函數的調用、指針運算、數組訪問等,應該將沒有必要執行多次的操作全部集合在一起,放到一個init的初始化程序中進行。 (3)延時函數 通常使用的延時函數均採用自加的形式: void delay (void) { unsigned int i; for (i=0;i0;i--) ; } 兩個函數的延時效果相似,但幾乎所有的C編譯對後一種函數生成的代碼均比前一種代碼少1~3個字節,因為幾乎所有的MCU均有為0轉移的指令,採用後一種方式能夠生成這類指令。在使用while循環時也一樣,使用自減指令控制循環會比使用自加指令控制循環生成的代碼更少1~3個字母。但是在循環中有通過循環變量“i”讀寫數組的指令時,使用預減循環有可能使數組超界,要引起注意。 (4)while循環和do…while循環 用while循環時有以下兩種循環形式: unsigned int i; i=0; while (i0); 在這兩種循環中,使用do…while循環編譯後生成的代碼的長度短於while循環。 (5)循環展開 這是經典的速度優化,但許多編譯程序(如gcc -funroll-loops)能自動完成這個事,所以現在你自己來優化這個顯得效果不明顯。 舊代碼: for (i = 0; i < 100; i++) { do_stuff(i); } 新代碼: for (i = 0; i < 100; ) { do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; } 可以看出,新代碼裏比較指令由100次降低為10次,循環時間節約了90%。不過注意:對於中間變量或結果被更改的循環,編譯程序往往拒絕展開,(怕擔責任唄),這時候就需要你自己來做展開工作了。 還有一點請注意,在有內部指令cache的CPU上(如MMX芯片),因為循環展開的代碼很大,往往cache溢出,這時展開的代碼會頻繁地在CPU 的cache和內存之間調來調去,又因為cache速度很高,所以此時循環展開反而會變慢。還有就是循環展開會影響矢量運算優化。 (6)循環嵌套 把相關循環放到一個循環裏,也會加快速度。 舊代碼: for (i = 0; i < MAX; i++) /* initialize 2d array to 0's */ for (j = 0; j < MAX; j++) a[i][j] = 0.0; for (i = 0; i < MAX; i++) /* put 1's along the diagonal */ a[i][i] = 1.0; 新代碼: for (i = 0; i < MAX; i++) /* initialize 2d array to 0's */ { for (j = 0; j < MAX; j++) a[i][j] = 0.0; a[i][i] = 1.0; /* put 1's along the diagonal */ } (7)Switch語句中根據發生頻率來進行case排序 Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鏈/樹。當switch用比較鏈的方式轉化時,編譯器會產生if-else-if的嵌套代碼,並按照順序進行比較,匹配時就跳轉到滿足條件的語句執行。所以可以對case的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣可以提高性能。此外,在case中推薦使用小的連續的整數,因為在這種情況下,所有的編譯器都可以把switch 轉化成跳轉表。 不好的代碼: int days_in_month, short_months, normal_months, long_months; 。。。。。。 switch (days_in_month) {   case 28:   case 29:     short_months ++;     break;   case 30:     normal_months ++;     break;   case 31:     long_months ++;     break;   default:     cout b->c[4]->cheetah + a->b->c[4]->dog; 新代碼: struct animals * temp = a->b->c[4]; total = temp->aardvark + temp->baboon + temp->cheetah + temp->dog; 一些老的C語言編譯器不做聚合優化,而符合ANSI規範的新的編譯器可以自動完成這個優化,看例子: float a, b, c, d, f, g; 。。。 a = b / c * d; f = b * g / c; 這種寫法當然要得,但是沒有優化 float a, b, c, d, f, g; 。。。 a = b / c * d; f = b / c * g; 如果這麼寫的話,一個符合ANSI規範的新的編譯器可以只計算b/c一次,然後將結果代入第二個式子,節約了一次除法運算。 8、函數優化 (1)Inline函數 在C++中,關鍵字Inline可以被加入到任何函數的聲明中。這個關鍵字請求編譯器用函數內部的代碼替換所有對於指出的函數的調用。這樣做在兩個方面快於函數調用:第一,省去了調用指令需要的執行時間;第二,省去了傳遞變元和傳遞過程需要的時間。但是使用這種方法在優化程序速度的同時,程序長度變大了,因此需要更多的ROM。使用這種優化在Inline函數頻繁調用並且只包含幾行代碼的時候是最有效的。 (2)不定義不使用的返回值 函數定義並不知道函數返回值是否被使用,假如返回值從來不會被用到,應該使用void來明確聲明函數不返回任何值。 (3)減少函數調用參數 使用全局變量比函數傳遞參數更加有效率。這樣做去除了函數調用參數入棧和函數完成後參數出棧所需要的時間。然而決定使用全局變量會影響程序的模塊化和重入,故要慎重使用。 (4)所有函數都應該有原型定義 一般來説,所有函數都應該有原型定義。原型定義可以傳達給編譯器更多的可能用於優化的信息。 (5)儘可能使用常量(const) 儘可能使用常量(const)。C++ 標準規定,如果一個const聲明的對象的地址不被獲取,允許編譯器不對它分配儲存空間。這樣可以使代碼更有效率,而且可以生成更好的代碼。 (6)把本地函數聲明為靜態的(static) 如果一個函數只在實現它的文件中被使用,把它聲明為靜態的(static)以強制使用內部連接。否則,默認的情況下會把函數定義為外部連接。這樣可能會影響某些編譯器的優化——比如,自動內聯。 9、採用遞歸 與LISP之類的語言不同,C語言一開始就病態地喜歡用重複代碼循環,許多C程序員都是除非算法要求,堅決不用遞歸。事實上,C編譯器們對優化遞歸調用一點都不反感,相反,它們還很喜歡幹這件事。只有在遞歸函數需要傳遞大量參數,可能造成瓶頸的時候,才應該使用循環代碼,其他時候,還是用遞歸好些。 10、變量 (1)register變量 在聲明局部變量的時候可以使用register關鍵字。這就使得編譯器把變量放入一個多用途的寄存器中,而不是在堆棧中,合理使用這種方法可以提高執行速度。函數調用越是頻繁,越是可能提高代碼的速度。 在最內層循環避免使用全局變量和靜態變量,除非你能確定它在循環週期中不會動態變化,大多數編譯器優化變量都只有一個辦法,就是將他們置成寄存器變量,而對於動態變量,它們乾脆放棄對整個表達式的優化。儘量避免把一個變量地址傳遞給另一個函數,雖然這個還很常用。C語言的編譯器們總是先假定每一個函數的變量都是內部變量,這是由它的機制決定的,在這種情況下,它們的優化完成得最好。但是,一旦一個變量有可能被別的函數改變,這幫兄弟就再也不敢把變量放到寄存器裏了,嚴重影響速度。看例子: a = b(); c(&d); 因為d的地址被c函數使用,有可能被改變,編譯器不敢把它長時間的放在寄存器裏,一旦運行到c(&d),編譯器就把它放回內存,如果在循環裏,會造成N次頻繁的在內存和寄存器之間讀寫d的動作,眾所周知,CPU在系統總線上的讀寫速度慢得很。比如你的賽楊300,CPU主頻300,總線速度最多66M,為了一個總線讀,CPU可能要等4-5個週期,得。。得。。得。。想起來都打顫。 (2)同時聲明多個變量優於單獨聲明變量 (3)短變量名優於長變量名,應儘量使變量名短一點 (4)在循環開始前聲明變量 11、使用嵌套的if結構 在if結構中如果要判斷的並列條件較多,最好將它們拆分成多個if結構,然後嵌套在一起,這樣可以避免無謂的判斷。 説明: 上面的優化方案由王全明收集整理。很多資料來源於網上,出處不祥,在此對所有作者一併致謝! 該方案主要是考慮到在嵌入式開發中對程序執行速度的要求特別高,所以該方案主要是為了優化程序的執行速度。 注意:優化是有側重點的,優化是一門平衡的藝術,它往往要以犧牲程序的可讀性或者增加代碼長度為代價。 (任何情況下,空間優化和時間優化都是對立的--東樓)。 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2021-01-04 關鍵詞: 代碼 C語言

  • C語言文件操作

    C 語言把文件看作是一個字符(字節)的序列,即由一個一個字符(字節)的數據順序組成。根據數據的組織形式,可分為 ASCⅡ 文件和二進制文件。文件的操作包括:文件的打開、文件的關閉、文件的讀寫操作、文件狀態檢查以及文件的定位等。 1 文件的打開 1.1 函數原型 FILE *fopen(char *pname,char *mode) 1.2 功能説明 按照 mode 規定的方式,打開由 pname指定的文件。若找不到由 pname 指定的相應文件,就按以下方式之一處理: 此時如 mode 規定按寫方式打開文件,就按由pname 指定的名字建立一個新文件; 此時如 mode 規定按讀方式打開文件,就會產生一個錯誤; 打開文件的作用: 分配給打開文件一個FILE 類型的文件結構體變量,並將有關信息填入文件結構體變量; 開闢一個緩衝區; 調用操作系統提供的打開文件或建立新文件功能,打開或建立指定文件; FILE *:指出 fopen 是一個返回文件類型的指針函數; 1.3 參數説明 pname:是一個字符指針,它將指向要打開或建立的文件的文件名字符串。 mode:是一個指向文件處理方式字符串的字符指針。 1.4 返回值 正常返回:被打開文件的文件指針。 異常返回:NULL,表示打開操作不成功。 //定義一個名叫fp文件指針 FILE *fp; //判斷按讀方式打開一個名叫test的文件是否失敗 if((fp=fopen("test","r")) == NULL)//打開操作不成功 { printf("The file can not be opened.\n"); exit(1);//結束程序的執行 } 要説明的是:C 語言將計算機的輸入輸出設備都看作是文件。例如,鍵盤文件、屏幕文件等。ANSI C 標準規定,在執行程序時系統先自動打開鍵盤、屏幕、錯誤三個文件。這三個文件的文件指針分別是:標準輸入 stdin、標準輸出 stdout 和標準出錯 stderr。 2 文件的關閉 2.1 函數原型 int fclose(FILE *fp); 2.2 功能説明 關閉由 fp 指出的文件。此時調用操作系統提供的文件關閉功能,關閉由 fp->fd 指出的文件;釋放由 fp 指出的文件類型結構體變量;返回操作結果,即 0 或 EOF。 2.3 參數説明 fp:一個已打開文件的文件指針。 2.4 返回值 正常返回:0。 異常返回:EOF,表示文件在關閉時發生錯誤。 int n=fclose(fp); 3 文件的讀寫操作 3.1 從文件中讀取一個字符 3.1.1 函數原型 int fgetc(FILE *fp); 3.1.2 功能説明 從fp所指文件中讀取一個字符。 3.1.3 參數説明 fp:這是個文件指針,它指出要從中讀取字符的文件。 3.1.4返回值 正常返回: 返回讀取字符的代碼。 非正常返回:返回 EOF。例如,要從"寫打開"文件中讀取一個字符時,會發生錯誤而返回一個 EOF。 顯示指定文件的內容: //程序名為:display.c //執行時可用:display filename1 形式的命令行運行。顯示文件filename1中的內容。例如,執行命令行display display.c將在屏幕上顯示display的原代碼。 //File display program. #include  void main(int argc,char *argv[]) //命令行參數 {     int ch;//定義文件類型指針     FILE *fp;//判斷命令行是否正確 if(argc!=2)     { printf("Error format,Usage: display filename1\n"); return; //鍵入了錯誤的命令行,結束程序的執行     }     //按讀方式打開由argv[1]指出的文件 if((fp=fopen(argv[1],"r"))==NULL)     { printf("The file  can not be opened.\n",argv[1]);//打開操作不成功 return;//結束程序的執行     }     //成功打開了argv[1]所指文件     ch=fgetc(fp); //從fp所指文件的當前指針位置讀取一個字符 while(ch!=EOF) //判斷剛讀取的字符是否是文件結束符     {         putchar(ch); //若不是結束符,將它輸出到屏幕上顯示         ch=fgetc(fp); //繼續從fp所指文件中讀取下一個字符     } //完成將fp所指文件的內容輸出到屏幕上顯示     fclose(fp); //關閉fp所指文件 } 3.2 寫一個字符到文件中去 3.2.1 函數原型 int fputc(int ch,FILE *fp) 3.2.2 功能説明 把 ch中的字符寫入由 fp 指出的文件中去。 3.2.3 參數説明 ch:是一個整型變量,內存要寫到文件中的字符(C 語言中整型量和字符量可以通用)。 fp:這是個文件指針,指出要在其中寫入字符的文件。 3.2.4 返回值 正常返回: 要寫入字符的代碼。 非正常返回:返回 EOF。例如,要往"讀打開"文件中寫一個字符時,會發生錯誤而返回一個EOF。 將一個文件的內容複製到另一個文件中去: //程序名為:copyfile.c //執行時可用:copyfile filename1 filename2形式的命令行運行,將文件filename1中的內容複製到文件filename2中去。 //file copy program. #include  void main(int argc,char *argv[]) //命令行參數 {     int ch;     FILE *in,*out; //定義in和out兩個文件類型指針 if(argc!=3) //判斷命令行是否正確     { printf("Error in format,Usage: copyfile filename1 filename2\n"); return; //命令行錯,結束程序的執行     }     //按讀方式打開由argv[1]指出的文件 if((in=fopen(argv[1],"r"))==NULL)     { printf("The file  can not be opened.\n",argv[1]); return; //打開失敗,結束程序的執行     }     //成功打開了argv[1]所指文件,再     //按寫方式打開由argv[2]指出的文件 if((out=fopen(argv[2],"w"))==NULL)     { printf("The file %s can not be opened.\n",argv[2]); return; //打開失敗,結束程序的執行     }     //成功打開了argv[2]所指文件     ch=fgetc(in); //從in所指文件的當前指針位置讀取一個字符 while(ch!=EOF) //判斷剛讀取的字符是否是文件結束符     {         fputc(ch,out); //若不是結束符,將它寫入out所指文件         ch=fgetc(in); //繼續從in所指文件中讀取下一個字符     } //完成將in所指文件的內容寫入(複製)到out所指文件中     fclose(in); //關閉in所指文件     fclose(out); //關閉out所指文件 } 按十進制和字符顯示文件代碼,若遇不可示字符就用井號"#"字符代替之。 //程序名為:dumpf.c //執行時可用:dumpf filename1 形式的命令行運行。 // File dump program. #include  void main(int argc,char *argv[]) {     char str[9];     int ch,count,i;     FILE *fp; if(argc!=2)     { printf("Error format,Usage: dumpf filename\n"); return;     } if((fp=fopen(argv[1],"r"))==NULL)     { printf("The file %s can not be opened.\n",argv[1]); return;     }     count=0; do{         i=0;         //按八進制輸出第一列,作為一行八個字節的首地址 printf("%06o: ",count*8); do{             // 從打開的文件中讀取一個字符             ch=fgetc(fp);             // 按十進制方式輸出這個字符的ASCII碼 printf("%4d",ch);             // 如果是不可示字符就用"#"字符代替 if(ch'~') str[i]='#';             // 如果是可示字符,就將它存入數組str以便形成字符串 else str[i]=ch;             // 保證每一行輸出八個字符 if(++i==8) break;         }while(ch!=EOF); // 遇到文件尾標誌,結束讀文件操作         str[i]='\0'; // 在數組str加字符串結束標誌 for(;i

    時間:2020-12-31 關鍵詞: 嵌入式 C語言

  • 這個“整型提升”隱藏的太深了...

    Integer Promotions in C 簡介 整型提升是C程序設計語言中的一項規定:在表達式計算時,各種整形首先要提升為int類型,如果int類型不足以表示的話,就需要提升為unsigned int類型,然後再執行表達式的運算。 這一規則是由C語言的發明人丹尼斯·裏奇與肯·湯普遜創設的: "A character, a short integer, or an integer bit-field, all either signed or not, or an object of enumeration type, may be used in an expression wherever an integer maybe used. If an int can represent all the values of the original type, then the value is converted to int; otherwise the value is converted to unsigned int. This process is called integral promotion." 這段話的大意是:無論使用什麼整數,都可以在表達式中使用char,short int或 int字段(全部帶符號或沒有符號)或枚舉類型的對象。如果一個int可以代表原始類型的所有值,則該值將轉換為int;否則,該值將轉換為unsigned int,這個過程稱為整體提升。 舉例子來了解一下整形提升 一些數據類型(比如char,short int)比int佔用更少的字節數,對它們執行操作時,這些數據類型會自動提升為int或unsigned int,例如,在較小的類型(如char,short和enum)上不會進行算術計算,代碼如下: 1//在win10_64位+vs2017 2#include 3int main() 4{ 5 char a = 30, b = 40, 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編譯器中),而子表達式的值(a * b)=1200,大於128。 但是整數提升是在char類型進行算術運算時發生的,我們得到了適當的結果而沒有任何溢出。 整型提升的意義 雖然機器指令中可能有現兩個8比特字節這種字節相加指令,但是一般用途的CPU是難以直接實現這樣的字節相加運算的。 所以,表達式中各種長度可能小於int長度的整型值,都必須先轉換為int或unsigned int,然後才能送入CPU去執行運算。 CPU內整型運算器(ALU)的操作數的字節長度一般就是int的字節長度,同時也是CPU的通用寄存器的長度。而表達式的整型運算要在CPU的相應運算器件內執行。 因此,兩個char類型的樹進行相加運算時,是在CPU中執行,自然而然的需要先轉換為CPU內整型操作數的標準長度。 應用舉例 short int的長度 = int的長度的情況 C語言標準中僅規定了: char的長度 ≤ short int的長度 ≤ int的長度 這意味着short int與int的長度相等的可能,這種情形下,unsigned short就無法提升為int表示,只能提升為unsigned int,代碼如下: 1//在win10_64位+vs2017 2#include 3int main(){ 4 char a = 0xb6; 5 short b = 0xb600; 6 int c = 0xb6000000; 7 if ( a == 0xb6) printf("a"); 8 if ( b == 0xb600) printf("b"); 9 if ( c == 0xb6000000) printf("c"); 10 system("pause"); 11} 輸出結果:c C語言標準沒有規定char類型是有符號還是無符號,在這些環境下,編譯器把char定義為signed char。 表達式a==0xb6被整型提升,其中char類型的a提升為int類型並表示為一個負值,因此這個表達式的結果為false; 表達式b==0xb600被整型提升,其中short類型的b提升為int類型併為一個負值,因此這個表達式的結果為false; 表達式c == 0xb6000000沒有做整型提升,==運算符的兩段都是int類型的負值,其結果為true。 我們再考慮以下程序作為另一個示例。 1//在win10_64位+vs2017 2#include 3 4int main() 5{ 6 char a = 0xfb; 7 unsigned char b = 0xfb; 8 9 printf("a = %c", a); 10 printf("\nb = %c", b); 11 12 if (a == b) 13 printf("\nSame"); 14 else 15 printf("\nNot Same"); 16 17 system("pause"); 18 return 0; 19} 輸出結果: a= b= Not Same 當我們打印“a”和“b”時,將打印相同的字符,但是當我們比較它們時,輸出的結果卻不相同。 “a”和“b”與char具有相同的二進制表示形式,但是,當對“a”和 ”b”執行比較操作時,它們首先會轉換為int。 “a”是一個有符號的字符,當轉換為int時,其值變為-5(有符號的值0xfb)。 “b”是無符號字符,當將其轉換為int時,其值變為251。 值-5和251具有不同的int表示形式,因此我們得到的輸出為“Not Same”。 前綴+的情況 C語言的單操作數的+運算符(即“前綴+”),一個主要作用就是實現對操作數的整型提升。例如: 1//在win10_64位+vs2017 2#include 3int main() 4{ 5 char a = 1; 6 printf("%u", sizeof(a) ); 7 printf("\n"); 8 printf("%u", sizeof( +a ) ); 9 system("pause"); 10 return 0; 11} 輸出結果: 1 4 從結果中我們可以看到,前綴+把大小給提升了。 話説C語言的特點 這裏不得不提一下C語言的特點,C語言高效、靈活、功能豐富、表達力強,在誕生起初,為了避免各開發四方a集運倉電話用的C語言語法產生差異,C語言訂定了一套語法ANSI C,作為C語言的標準。 以上所有的程序以及結論都是在win10_64位+vs2017完成的,不同平台和不同編譯器之間可能的實驗結果都不一樣,但這並不影響我們深入理解C語言的特性,講原理,摳細節,究根源,樂趣在此。 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-31 關鍵詞: 整型提升 C語言

  • C語言狀態機編程思想

    有限狀態機概念 有限狀態機是一種概念思想,把複雜的控制邏輯分解成有限個穩定狀態,組成閉環系統,通過事件觸發,讓狀態機按設定的順序處理事務。單片機C語言的狀態機編程,是利用條件選擇語句( switch 、 case 或者 if 、 else )切換狀態,通過改變狀態機狀態,讓程序按設定的順序執行。 有限狀態機由有限的狀態和相互之間的轉移構成,在任何時候只能處於給定數目的狀態中的一個。當接收到一個輸入事件時,狀態機產生一個輸出,同時也可能伴隨着狀態的轉移。狀態機的原理如下:在當前狀態下,發生某個事件後轉移到下一個狀態,然後決定執行的功能動作。可參考如下示意圖: 應用舉例 要想使用狀態機思想進行編程,需要將任務分解成有限個穩定狀態。 這裏以常見的按鍵動作進行舉例説明: 上圖為按鍵典型的動作圖,可以分解為四個狀態,分別為: 狀態1 = 按鍵彈起、 狀態2 = 前沿抖動、 狀態3 = 按鍵按下、 狀態4 = 後沿抖動。 有限狀態機的C代碼實現如下: if (定時器 >= 10ms) //10ms是典型消抖時間{   switch (按鍵狀態)   {     case 按鍵彈起狀態:      if (IO讀取為低電平) 按鍵狀態=前沿抖動;      break;     case 前沿抖動狀態:      if (IO讀取為低電平) 按鍵狀態=按鍵按下;      break;     case 按鍵按下狀態:      if (IO讀取為高電平) 按鍵狀態=後沿抖動;      break;     case 後沿抖動狀態:      if (IO讀取為高電平) 按鍵狀態=按鍵彈起;      break;default:按鍵狀態=按鍵彈起;   }} 狀態機編程建議 巧妙的使用結構體和枚舉一方面可以便於擴展和維護狀態機的狀態和事件,另一方面可提高程序的可讀性。假設有3種狀態(狀態數可以隨意增加),狀態枚舉如下: typedef enum {  state_1=1,  state_2,  state_3}State; 假設有5個事件(也可以隨意增加),事件枚舉如下: typedef enum{  event_1=1,  event_2,  event_3,  event_4,  event_5}Event; 定義一個結構體描述如下: typedef struct {  State curState;      //當前狀態  Event eventId;      //事件  State nextState;   //下一個狀態  Action action;     //動作功能}StateEvent; 根據具體的應用場景調整 State 和 Event ,並賦予相應的動作功能,整體的基本流程如下: 當前狀態->有事件觸發->跳到下一個狀態->具體的動作功能 總結 狀態機應用很廣泛,也可以鍛鍊邏輯思維,LoRa消息推送也常採用狀態機的思想, 實際上狀態機涉及的知識點很多,本篇文章只是簡要的介紹了下單片機C語言的狀態機編程思想,在日後的開發設計中,需要不斷的總結經驗並靈活應用。 來源:頭條-嵌入式在左C語言在右 鏈接://www.toutiao.com/i6843028812112855564/ 版權歸原作者所有,如有侵權,請聯繫刪除。 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-30 關鍵詞: 編程 C語言

  • 嵌入式C語言的7個硬核知識

    ID:嵌入式情報局 作者:情報小哥 1 void 與 void* void表示的是無類型,不可以採用這個類型聲明變量或常量,但是可以把指針定義為void類型,如void* ptr。 void指針可以指向任意類型的數據,可用任意數據類型的指針對void指針賦值,比如int *ptrInt;void *ptrVoid = ptrInt ;指針的賦值可以認為是地址的傳遞,而一般的32位系統指針都是佔用4個字節,所以指針賦值僅僅只是這4個字節的賦值與類型沒什麼關係。 1 void * memcpy( void *dest, const void *src, size_t len ); 2 void * memset( void * buffer, int c, size_t num); 2 volatile關鍵字 volatile修飾表示變量是易變的,編譯器中的優化器在用到這個變量時必須每次都小心地從內存中重新讀取這個變量的值,而不是使用保存在寄存器裏的備份,有效的防止編譯器自動優化,從而與軟件設計相符合。 3 數據佔用大小 數據佔用大小是指不同的數據類型在平台中所佔用的字節個數,不同的平台不同類型佔用的字節個數稍有不同,不過在對應的平台進行開發過程中,必須要對每個數據類型的佔用大小了如指掌,否則各種數據溢出,數據越界等等接踵而來。下面是簡單羅列的一些數據佔用情況:(在一般32位PC中) char 8bit short 16bit i n t 32bit long 32bit float 32bit dou ble 64bit 4 const與指針 const是恆定不變的意思,與指針的結合主要的問題是其const在指針中的位置導致該變量屬性不同。主要的識別辦法是去掉數據類型,看const修飾的是哪部分。 const int *ptr --> const *ptr -->那麼const修飾的就是*ptr,而*ptr表示的是指針所指向內容,所以其總體也叫"常量指針"表示值無法改變。 int *const ptr --> *const ptr -->那麼const修飾的就是ptr,而ptr表示的是指針變量,指針變量的值就是地址,所以總體也叫"指針常量"表示地址無法改變。 5 結構體與共聯體 對於結構體和共聯體在嵌入式領域是使用得非常頻繁的,一些可編程芯片提供的寄存器庫都是採用結構體和共聯體結合的方式來提供給軟件人員進行開發,同時在平時的編碼過程中這兩個數據類型的靈活應用也能夠實現代碼更好的封裝與簡化。 如下面的簡單示例,就可以非常靈活的訪問Val中的bit位。 1typedef union 2{ 3 BYTE Val; 4 struct __packed 5 { 6 BYTE b0: 1; 7 BYTE b1: 1; 8 BYTE b2: 1; 9 BYTE b3: 1; 10 BYTE b4: 1; 11 BYTE b5: 1; 12 BYTE b6: 1; 13 BYTE b7: 1; 14 } bits; 15}BYTE_VAL, BYTE_BITS; 6 預定義標識符 一般編譯器都支持預定義標識符,這些標識符結合printf等打印信息幫助程序員調試程序是非常有用的,一般編譯器會自動根據用户指定完成替換和處理。 如下是常用的標識: __FILE__  :表示進行編譯的源文件字符串; __LINE__ :表示當前文件的行號; __DATE__:表示文件日期; __TIME__ :表示文件時間; 使用範例: 1printf( "file:%s\n line:%d \n data:%s \n time: %s \n",__FILE__,__LINE__,__DATE__,__TIME__); 7 #與## #:是一種運算符,用於帶參宏的文本替換,將跟在後面的參數轉成一個字符串常量。 ##:是一種運算符,是將兩個運算對象連接在一起,也只能出現在帶參宏定義的文本替換中。 1#define STR(s) #s 2#define COMB(str1,str2) str1##str2 3int main() 4{ 5 int UART1= 57600; 6 printf("%d\n", COMB(UART, 1)); 7 printf("%s\n", STR(3.1415)); 8 return 0; 9} 2最後 這裏小哥就總結了一些嵌入式C進階的一些要點, 希望能夠對你有幫助,Linux應用編程大全專輯還會繼續更新,中間會安插一些其他嵌入式知識作為緩衝,今天就分享到這裏,下期精彩見! 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-29 關鍵詞: 嵌入式 C語言

  • 來,看看這20個常用的宏定義!

    ID:技術讓夢想更偉大 作者:李肖遙 寫好C語言,漂亮的宏定義很重要,使用宏定義可以防止出錯,提高可移植性,可讀性,方便性等等。下面列舉一些成熟軟件中常用的宏定義。 1. 防止一個頭文件被重複包含 1#ifndef COMDEF_H 2#define COMDEF_H 3//頭文件內容 4#endif 2. 重新定義一些類型,防止由於各種平台和編譯器的不同,而產生的類型字節數差異,方便移植。 1typedef unsigned char boolean; /* Boolean value type. */ 2typedef unsigned long int uint32; /* Unsigned 32 bit value */ 3typedef unsigned short uint16; /* Unsigned 16 bit value */ 4typedef unsigned char uint8; /* Unsigned 8 bit value */ 5typedef signed long int int32; /* Signed 32 bit value */ 6typedef signed short int16; /* Signed 16 bit value */ 7typedef signed char int8; /* Signed 8 bit value */ 下面的不建議使用 1typedef unsigned char byte; /* Unsigned 8 bit value type. */ 2typedef unsigned short word; /* Unsinged 16 bit value type. */ 3typedef unsigned long dword; /* Unsigned 32 bit value type. */ 4typedef unsigned char uint1; /* Unsigned 8 bit value type. */ 5typedef unsigned short uint2; /* Unsigned 16 bit value type. */ 6typedef unsigned long uint4; /* Unsigned 32 bit value type. */ 7typedef signed char int1; /* Signed 8 bit value type. */ 8typedef signed short int2; /* Signed 16 bit value type. */ 9typedef long int int4; /* Signed 32 bit value type. */ 10typedef signed long sint31; /* Signed 32 bit value */ 11typedef signed short sint15; /* Signed 16 bit value */ 12typedef signed char sint7; /* Signed 8 bit value */ 3. 得到指定地址上的一個字節或字 1#define MEM_B( x ) ( *( (byte *) (x) ) ) 2#define MEM_W( x ) ( *( (word *) (x) ) ) 4. 求最大值和最小值 1#define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) ) 2#define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) ) 5. 得到一個field在結構體(struct)中的偏移量 1#define FPOS( type, field ) 2/*lint -e545 */ ( (dword) &(( type *) 0)-> field ) /*lint +e545 */ 6. 得到一個結構體中field所佔用的字節數 1#define FSIZ( type, field ) sizeof( ((type *) 0)->field ) 7. 按照LSB格式把兩個字節轉化為一個Word 1#define FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] ) 8. 按照LSB格式把一個Word轉化為兩個字節 1#define FLOPW( ray, val ) 2(ray)[0] = ((val) / 256); 3(ray)[1] = ((val) & 0xFF) 9. 得到一個變量的地址(word寬度) 1#define B_PTR( var ) ( (byte *) (void *) &(var) ) 2#define W_PTR( var ) ( (word *) (void *) &(var) ) 10. 得到一個字的高位和低位字節 1#define WORD_LO(xxx) ((byte) ((word)(xxx) & 255)) 2#define WORD_HI(xxx) ((byte) ((word)(xxx) >> 8)) 11. 返回一個比X大的最接近的8的倍數 1#define RND8( x ) ((((x) + 7) / 8 ) * 8 ) 12. 將一個字母轉換為大寫 1#define UPCASE( c ) ( ((c) >= 'a' && (c) = '0' && (c) = '0' && (c) = 'A' && (c) = 'a' && (c)  (val)) ? (val)+1 : (val)) 16. 返回數組元素的個數 1#define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) ) 17. 返回一個無符號數n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n) 1#define MOD_BY_POWER_OF_TWO( val, mod_by ) 2( (dword)(val) & (dword)((mod_by)-1) ) 18. 對於IO空間映射在存儲空間的結構,輸入輸出處理 1#define inp(port) (*((volatile byte *) (port))) 2#define inpw(port) (*((volatile word *) (port))) 3#define inpdw(port) (*((volatile dword *)(port))) 4#define outp(port, val) (*((volatile byte *) (port)) = ((byte) (val))) 5#define outpw(port, val) (*((volatile word *) (port)) = ((word) (val))) 6#define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val))) 19. 使用一些宏跟蹤調試 A N S I標準説明了五個預定義的宏名。它們是: 1_ L I N E _ 2_ F I L E _ 3_ D A T E _ 4_ T I M E _ 5_ S T D C _ 如果編譯不是標準的,則可能僅支持以上宏名中的幾個,或根本不支持。記住編譯程序也許還提供其它預定義的宏名。 _ L I N E _及_ F I L E _宏指令在有關# l i n e的部分中已討論,這裏討論其餘的宏名。 _ D AT E _宏指令含有形式為月/日/年的串,表示源文件被翻譯到代碼時的日期。 源代碼翻譯到目標代碼的時間作為串包含在_ T I M E _中。串形式為時:分:秒。 如果實現是標準的,則宏_ S T D C _含有十進制常量1。如果它含有任何其它數,則實現是非標準的。 可以定義宏,例如: 當定義了_DEBUG,輸出數據信息和所在文件所在行 1#ifdef _DEBUG 2#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_) 3#else 4#define DEBUGMSG(msg,date) 5#endif 20. 宏定義防止使用時錯誤用小括號包含。 例如: 1#define ADD(a,b) (a+b) 用do{}while(0)語句包含多語句防止錯誤 例如: 1#difne DO(a,b) a+b; 2a++; 應用時: 1if(….) 2DO(a,b); //產生錯誤 3else 解決方法: 1#difne DO(a,b) do{a+b; 2a++;}while(0) -END- 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-29 關鍵詞: 宏定義 C語言

  • C語言執行效率如何保證,看這一文就夠了!

    來自公眾號:嵌入式ARM 嵌入式開發基本都會選擇C語言 這是因為C語言有出色的可移植性 能在多種不同體系結構的軟/硬平台上運行 雖然代碼的複用性差 代碼的維護性差 擴展性很差 但,C語言簡潔緊湊 使用靈活的語法機制 並且,C語言具有很高的運行效率 那麼如何保證C語言的執行效率? 嵌入式ARM告訴你! 01 C代碼執行效率與哪些因素有關 C代碼執行效率與時間複雜度和空間複雜度有關: 1、空間複雜度是指算法在計算機內執行時所需存儲空間的度量 2、一般情況下,算法中基本操作重複執行的次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函數。 記作T(n)=O(f(n)),稱O(f(n))為算法的漸進時間複雜度,簡稱時間複雜度。在各種不同算法中,若算法中語句執行次數為一個常數,則時間複雜度為O(1),另外,在時間頻度不相同時,時間複雜度有可能相同,如T(n)=n2+3n+4與T(n)=4n2+2n+1它們的頻度不同,但時間複雜度相同,都為O(n2)。 按數量級遞增排列,常見的時間複雜度有:常數階O(1),對數階O(log2n),線性階O(n),線性對數階O(nlog2n),平方階O(n^2),立方階O(n^3),。。。,k次方階O(n^k),指數階O(2^n)。隨着問題規模n的不斷增大,上述時間複雜度不斷增大,算法的執行效率越低。 02 保障C代碼執行效率的原則 1、選擇合適的算法和數據結構 選擇一種合適的數據結構很重要,如果在一堆隨機存放的數中使用了大量的插入和刪除指令,那使用鏈表要快得多。數組與指針語句具有十分密切的關係,一般來説,指針比較靈活簡潔,而數組則比較直觀,容易理解。對於大部分的編譯器,使用指針比使用數組生成的代碼更短,執行效率更高。在許多種情況下,可以用指針運算代替數組索引,這樣做常常能產生又快又短的代碼。與數組索引相比,指針一般能使代碼速度更快,佔用空間更少。使用多維數組時差異更明顯。下面的代碼作用是相同的,但是效率不一樣。 數組索引 指針運算 For(;;){ p=array A=array[t++]; for(;;){ a=*(p++); 。。。。。。。。。。。。。。。    }                      } 指針方法的優點是,array的地址每次裝入地址p後,在每次循環中只需對p增量操作。在數組索引方法中,每次循環中都必須根據t值求數組下標的複雜運算。 時間複雜度更低、效率更高的算法可以提高執行效率。一個簡單的例子,計算1~100這些數的和,可以循環100次,也可以直接使用求和公式,在執行效率上,是顯而易見的。 2、代碼儘量簡潔,避免重複 在10天學會單片機那本書上看到寫的數碼管顯示那部分代碼,選中一個位,然後送數據,再選中一個位,再送數據,依次做完。代碼重複率太高了,不僅佔用過多的類存,而且執行效率差可讀性差,僅僅是實現了功能而已,實際的編程可以做一個循環,for循環或者while循環。這樣的代碼看起來更有水平。 3、合理使用宏定義 在程序中如果某個變量或寄存器經常用到,可以使用宏定義定義一個新的名代替它。這樣的好處是方便修改,比如液晶的數據端總線接的P1,現在想改到P0,那麼只需要修改宏定義這裏就可以了,編譯器編譯的時候,會自動的把定義的名替換成實際的名稱。 函數和宏的區別就在於,宏佔用了大量的空間,而函數佔用了時間。大家要知道的是,函數調用是要使用系統的棧來保存數據的,如果編譯器 裏有棧檢查選 項,一般在函數的頭會嵌入一些彙編語句對當前棧進行檢查;同時,CPU也要在函數調用時保存和恢復當前的現場,進行壓棧和彈棧操作,所以,函數調用需要一 些CPU時間。而宏不存在這個問題。宏僅僅作為預先寫好的代碼嵌入到當前程序,不會產生函數調用,所以僅僅是佔用了空間,在頻繁調用同一個宏的時候,該現象尤其突出。舉例如下:方法A: #define bwMCDR2_ADDRESS 4#define bsMCDR2_ADDRESS 17int BIT_MASK(int __bf){return ((1U "通常可以提高算法效率。因為乘除運算指令週期通常比移位運算大。C語言位運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用着的是位間的與(&)、或 (|)、非(~)操作,這跟嵌入式系統的編程特點有很大關係。我們通常要對硬件寄存器進行位設置,譬如,我們通過將AM186ER型80186處理器的中 斷屏蔽控制寄存器的第低6位設置為0(開中斷2),最通用的做法是: #define INT_I2_MASK 0x0040wTemp = inword(INT_MASK);outword(INT_MASK, wTemp &~INT_I2_MASK); 而將該位設置為1的做法是: #define INT_I2_MASK 0x0040wTemp = inword(INT_MASK);outword(INT_MASK, wTemp | INT_I2_MASK);    判斷該位是否為1的做法是: #define INT_I2_MASK 0x0040wTemp = inword(INT_MASK);if(wTemp & INT_I2_MASK){… /* 該位為1 */} 運用這招需要注意的是,因為CPU的不同而產生的問題。比如説,在PC上用這招編寫的程序,並在PC上調試通過,在移植到一個16位機平台上的時候,可能會產生代碼隱患。所以只有在一定技術進階的基礎下才可以使用這招。12、利用硬件特性首先要明白CPU對各種存儲器的訪問速度,基本上是:CPU內部RAM > 外部同步RAM > 外部異步RAM > FLASH/ROM對於程序代碼,已經被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取代碼執行,但通常這不是一個好辦法,我們最好在系統啓動後將FLASH或ROM中的目標代碼拷貝入RAM中後再執行以提高取指令速度;對於UART等設備,其內部有一定容量的接收BUFFER,我們應儘量在BUFFER被佔滿後再向CPU提出中斷。例如計算機終端在向目標機通過RS-232傳遞數據時,不宜設置UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間;如果對某設備能採取DMA方式讀取,就採用DMA讀取,DMA讀取方式在讀取目標中包含的存儲信息較大時效率較高,其數據傳輸的基本單位是塊,而所傳輸 的數據是從設備直接送入內存的(或者相反)。DMA方式較之中斷驅動方式,減少了CPU 對外設的干預,進一步提高了CPU與外設的並行操作程度。13、使用寄存器變量 當對一個變量頻繁被讀寫時,需要反覆訪問內存,從而花費大量的存取時間。為此,C語言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使 用時,不需要訪問內存,而直接從寄存器中讀寫,從而提高效率。寄存器變量的説明符是register。對於循環次數較多的循環控制變量及循環體內反覆使用 的變量均可定義為寄存器變量,而循環計數是應用寄存器變量的最好候選者。(1) 只有局部自動變量和形參才可以定義為寄存器變量。因為寄存器變量屬於動態存儲方式,凡需要採用靜態存儲方式的量都不能定義為寄存器變量,包括:模塊間全局變量、模塊內全局變量、局部static變量;(2) register是一個"建議"型關鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能因為條件不滿足並未成為寄存器變量,而是被放在了存儲器中,但編譯器中並不報錯(在C++語言中有另一個"建議"型關鍵字:inline)。下面是一個採用寄存器變量的例子: /* 求1+2+3+….+n的值 */WORD Addition(BYTE n){register i,s=0;for(i=1;i 1;    }  }  *r = a - *q * *q;}   推薦的代碼: // 假設 q != rvoid isqrt(unsigned long a, unsigned long* q, unsigned long* r){  unsigned long qq, rr;  qq = a;  if (a > 0)  {    while (qq > (rr = a / qq))    {      qq = (qq + rr) >> 1;    }  }  rr = a - qq * qq;  *q = qq;  *r = rr;} 9、循環優化 (1)、充分分解小的循環 要充分利用CPU的指令緩存,就要充分分解小的循環。特別是當循環體本身很小的時候,分解循環可以提高性能。注意:很多編譯器並不能自動分解循環。不好的代碼: // 3D轉化:把矢量 V 和 4x4 矩陣 M 相乘for (i = 0;i < 4;i ++){  r[i] = 0;  for (j = 0;j < 4;j ++)  {    r[i] += M[j][i]*V[j];  }}推薦的代碼: r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3]; (2)、提取公共部分 對於一些不需要循環變量參加運算的任務可以把它們放到循環外面,這裏的任務包括表達式、函數的調用、指針運算、數組訪問等,應該將沒有必要執行多次的操作全部集合在一起,放到一個init的初始化程序中進行。 (3)、延時函數 通常使用的延時函數均採用自加的形式: void delay (void){unsigned int i; for (i=0;i0;i--) ; } 兩個函數的延時效果相似,但幾乎所有的C編譯對後一種函數生成的代碼均比前一種代碼少1~3個字節,因為幾乎所有的MCU均有為0轉移的指令,採用後一種方式能夠生成這類指令。在使用while循環時也一樣,使用自減指令控制循環會比使用自加指令控制循環生成的代碼更少1~3個字母。但是在循環中有通過循環變量“i”讀寫數組的指令時,使用預減循環有可能使數組超界,要引起注意。 (4)、while循環和do…while循環 用while循環時有以下兩種循環形式: unsigned int i; i=0; while (i0); 在這兩種循環中,使用do…while循環編譯後生成的代碼的長度短於while循環。 (6)、循環展開 這是經典的速度優化,但許多編譯程序(如gcc -funroll-loops)能自動完成這個事,所以現在你自己來優化這個顯得效果不明顯。 舊代碼: for (i = 0; i < 100; i++){do_stuff(i);} 新代碼: for (i = 0; i < 100; ){do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;do_stuff(i); i++;} 可以看出,新代碼裏比較指令由100次降低為10次,循環時間節約了90%。不過注意:對於中間變量或結果被更改的循環,編譯程序往往拒絕展開,(怕擔責任唄),這時候就需要你自己來做展開工作了。 還有一點請注意,在有內部指令cache的CPU上(如MMX芯片),因為循環展開的代碼很大,往往cache溢出,這時展開的代碼會頻繁地在CPU 的cache和內存之間調來調去,又因為cache速度很高,所以此時循環展開反而會變慢。還有就是循環展開會影響矢量運算優化。 (6)、循環嵌套 把相關循環放到一個循環裏,也會加快速度。 舊代碼: for (i = 0; i < MAX; i++) /* initialize 2d array to 0's */ for (j = 0; j < MAX; j++) a[i][j] = 0.0; for (i = 0; i < MAX; i++) /* put 1's along the diagonal */ a[i][i] = 1.0; 新代碼: for (i = 0; i < MAX; i++) /* initialize 2d array to 0's */{ for (j = 0; j < MAX; j++) a[i][j] = 0.0; a[i][i] = 1.0; /* put 1's along the diagonal */} (7)、Switch語句中根據發生頻率來進行case排序 Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鏈/樹。當switch用比較鏈的方式轉化時,編譯器會產生if-else-if的嵌套代碼,並按照順序進行比較,匹配時就跳轉到滿足條件的語句執行。所以可以對case的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣可以提高性能。此外,在case中推薦使用小的連續的整數,因為在這種情況下,所有的編譯器都可以把switch 轉化成跳轉表。 不好的代碼: int days_in_month, short_months, normal_months, long_months;。。。。。。switch (days_in_month){  case 28:  case 29:    short_months ++;    break;  case 30:    normal_months ++;    break;  case 31:    long_months ++;    break;  default:    cout b->c[4]->cheetah + a->b->c[4]->dog; 新代碼: struct animals * temp = a->b->c[4]; total = temp->aardvark + temp->baboon + temp->cheetah + temp->dog; 一些老的C語言編譯器不做聚合優化,而符合ANSI規範的新的編譯器可以自動完成這個優化,看例子: float a, b, c, d, f, g; 。。。 a = b / c * d; f = b * g / c; 這種寫法當然要得,但是沒有優化 float a, b, c, d, f, g; 。。。 a = b / c * d; f = b / c * g;   如果這麼寫的話,一個符合ANSI規範的新的編譯器可以只計算b/c一次,然後將結果代入第二個式子,節約了一次除法運算。 11、函數優化   (1)Inline函數 在C++中,關鍵字Inline可以被加入到任何函數的聲明中。這個關鍵字請求編譯器用函數內部的代碼替換所有對於指出的函數的調用。這樣做在兩個方面快於函數調用:第一,省去了調用指令需要的執行時間;第二,省去了傳遞變元和傳遞過程需要的時間。但是使用這種方法在優化程序速度的同時,程序長度變大了,因此需要更多的ROM。使用這種優化在Inline函數頻繁調用並且只包含幾行代碼的時候是最有效的。 (2)不定義不使用的返回值 函數定義並不知道函數返回值是否被使用,假如返回值從來不會被用到,應該使用void來明確聲明函數不返回任何值。 (3)減少函數調用參數 使用全局變量比函數傳遞參數更加有效率。這樣做去除了函數調用參數入棧和函數完成後參數出棧所需要的時間。然而決定使用全局變量會影響程序的模塊化和重入,故要慎重使用。 (4)所有函數都應該有原型定義 一般來説,所有函數都應該有原型定義。原型定義可以傳達給編譯器更多的可能用於優化的信息。 (5)儘可能使用常量(const) 儘可能使用常量(const)。C++ 標準規定,如果一個const聲明的對象的地址不被獲取,允許編譯器不對它分配儲存空間。這樣可以使代碼更有效率,而且可以生成更好的代碼。 (6)把本地函數聲明為靜態的(static) 如果一個函數只在實現它的文件中被使用,把它聲明為靜態的(static)以強制使用內部連接。否則,默認的情況下會把函數定義為外部連接。這樣可能會影響某些編譯器的優化——比如,自動內聯。 12、採用遞歸 與LISP之類的語言不同,C語言一開始就病態地喜歡用重複代碼循環,許多C程序員都是除非算法要求,堅決不用遞歸。事實上,C編譯器們對優化遞歸調用一點都不反感,相反,它們還很喜歡幹這件事。只有在遞歸函數需要傳遞大量參數,可能造成瓶頸的時候,才應該使用循環代碼,其他時候,還是用遞歸好些。 13、變量 (1)register變量 在聲明局部變量的時候可以使用register關鍵字。這就使得編譯器把變量放入一個多用途的寄存器中,而不是在堆棧中,合理使用這種方法可以提高執行速度。函數調用越是頻繁,越是可能提高代碼的速度。 在最內層循環避免使用全局變量和靜態變量,除非你能確定它在循環週期中不會動態變化,大多數編譯器優化變量都只有一個辦法,就是將他們置成寄存器變量,而對於動態變量,它們乾脆放棄對整個表達式的優化。儘量避免把一個變量地址傳遞給另一個函數,雖然這個還很常用。C語言的編譯器們總是先假定每一個函數的變量都是內部變量,這是由它的機制決定的,在這種情況下,它們的優化完成得最好。但是,一旦一個變量有可能被別的函數改變,這幫兄弟就再也不敢把變量放到寄存器裏了,嚴重影響速度。看例子: a = b(); c(&d); 因為d的地址被c函數使用,有可能被改變,編譯器不敢把它長時間的放在寄存器裏,一旦運行到c(&d),編譯器就把它放回內存,如果在循環裏,會造成N次頻繁的在內存和寄存器之間讀寫d的動作,眾所周知,CPU在系統總線上的讀寫速度慢得很。比如你的賽楊300,CPU主頻300,總線速度最多66M,為了一個總線讀,CPU可能要等4-5個週期,得。。得。。得。。想起來都打顫。 (2)、同時聲明多個變量優於單獨聲明變量 (3)、短變量名優於長變量名,應儘量使變量名短一點 (4)、在循環開始前聲明變量 14、使用嵌套的if結構 在if結構中如果要判斷的並列條件較多,最好將它們拆分成多個if結構,然後嵌套在一起,這樣可以避免無謂的判斷。 該方案主要是考慮到在嵌入式開發中對程序執行速度的要求特別高,所以該方案主要是為了優化程序的執行速度。 注意:優化是有側重點的,優化是一門平衡的藝術,它往往要以犧牲程序的可讀性或者增加代碼長度為代價。 15、儘量不要定義成全局變量 先來看一下局部變量、全局變量、靜態局部變量、靜態全局變量的異同。 ▶局部變量: 在一個函數中或複合語句中定義的變量,在動態存儲區分配存儲單元,在調用時動態分配,在函數或複合語句結束時自動釋放。 ▶靜態局部變量: 在一個函數中定義局部變量時,若加上static聲明,則此變量為靜態局部變量,在靜態存儲區分配存儲單元,在程序運行期間都不釋放;靜態局部變量只能在該函數中使用;靜態局部變量在編譯時賦值(若在定義時未進行賦值處理,則默認賦值為0(對數值型變量)或空字符(對字符型變量));靜態局部變量在函數調用結束後不自動釋放,保留函數調用結束後的值。 ▶全局變量: 在函數外定義的變量稱為全局變量;全局變量在靜態存儲區分配存儲單元,在程序運行期間都不釋放,在文件中的函數均可調用該全局變量,其他文件內的函數調用全局變量,需加extern聲明。 ▶靜態全局變量: 在函數外定義變量時,若加上staTIc聲明,則此變量為靜態全局變量;靜態全局變量在靜態存儲區分配存儲單元,在程序運行期間都不釋放,靜態全局變量在編譯時賦值(若在定義時未進行賦值處理,則默認賦值為0(對數值型變量)或空字符(對字符型變量));只能在當前文件中使用。 ▶小結: 一般情況下就定義成局部變量,這樣不僅運行更高效,而且很方便移植。局部變量大多定位於MCU內部的寄存器中,在絕大多數MCU中,使用寄存器操作速度比數據存儲器快,指令也更多更靈活,有利於生成質量更高的代碼,而且局部變量所佔用的寄存器和數據存儲器在不同的模塊中可以重複利用。 當中斷裏需要用到的變量時,就需要定義成全局變量,並且加volaTIle修飾一下,防止編譯器優化。如果數據是隻讀的比如數碼管的斷碼、漢字取模的字庫需要放在ROM裏,這樣可以節省RAM,51單片機是加code,高級點的單片機都是加const修飾。 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-27 關鍵詞: 嵌入式 C語言

  • 雙非渣碩的秋招之路總結(已拿抖音研發崗SP)

    前言       最近應邀在牛客網寫 C++ 求職專欄,又把以前的秋招總結補充了很多東西,現在想想還是發出來,希望能夠幫助更多的新手小夥伴們。 個人情況簡介 樓主本碩均讀於雙非院校(普通二本學校)、本碩都是計算機相關專業,英語六級水平,本科時期輔修了一個水的不能再水的英語第二學位。 本科時期學過很多語言:VB、C、C++、Java、C# 都有所涉獵,研究生時期則主攻 Python 和 C++。研二上學期開始系統學習 C++,並且不斷系統看書和實踐,中間崩潰過、迷茫過、放縱過,但從未放棄,始終相信自己,堅持咬牙走下去。所幸天道酬勤,最終也是拿到了一些不錯的 offer 。 投遞經歷 筆者從 2020.6.15 號正式開始投遞簡歷,到 2020.8.23 號截止一共投遞過 94家公司,其中既有提前批(2020年 6 月- 7 月),也包括正式批(2020 年 7 月- 10月)。 小建議:如果説求職者對自身實力不自信,可以多投投一些公司,選擇面放寬一些,不要死盯着那幾個大廠投。 共計筆試 59 場(最多一天做了 5 場筆試,那天天差點去世),54 家公司給了面試機會,54 家企業中有些企業是免筆試的。 秋招結果        最終成功走到了 6 家公司的 offer 環節:字節跳動研發崗 SP、華為通用軟件開發、百度 C++ 研發崗、B 站後端研發崗、深信服 C++ 研發崗以及農業銀行研發崗,最後簽了字節跳動,也是自己心心念唸的大廠之一,十分滿意了~         接下來從 6 個方面對秋招進行復盤和總結,希望能夠幫到大家鴨,特別是大三大四的小學弟們。 1、算法         在秋招過程中,算法是極其重要的,再次重申一遍,真的很重要!筆試就不提了,算法不過關,筆試基本涼涼,面試過程中基本都要手撕代碼,很多面試中算法題是具有一票否決權的,如果你能夠順利解出來,面試不敢説一定會過,至少是有很大加分項,但如果算法題做不出來或者説 bug 太多調試不通的話,面試上基本上就跪了(個人以及身邊朋友經歷,不一定準確)。在牛客網上也看到過很多基礎很好的牛友就是因為面試過程中的算法題沒解出來而直接飲恨的,希望大家千萬重視算法這一塊,千萬千萬要重視算法。         我大概在力扣上刷了 300+,HOT100 都刷了,劍指 offer 刷了 3 遍,刷完這些基本夠用了,自己也有注意總結題型,常見題型就是那些,所以算法題基本沒怎麼拉過我後腿。一般來説,主要考的就是動態規劃、貪心、二叉樹、鏈表、數組、字符串之類的。 推薦資料: 力扣1-300題(前300道題非常經典,建議學有餘力的同學都刷一刷) 力扣HOT100(跟上面有不少是重複的,刷的時候要注意總結) 啊哈!算法、大話數據結構(這兩本書都是面向新手的圖書,圖畫很多,有基礎的可以直接跳過不看) 劍指offer(這本書不需要多做介紹,校招必備) 挑戰程序設計競賽(這本書屬於進階一點的算法書籍了,作者是ACM-ICPC全球總冠軍,可以説是世界頂級程序設計高手的經驗總結了,需要慢慢消化,經典題型太多) 程序員代碼面試指南(左程雲大神的書,阿秀並沒有看完,只是看了其中的海量數據處理部分的題目就已經十分受用了,在百度三面中就考查到了其中的海量數據集處理的問題) 2、操作系統         操作系統是比較重要的,面試三大要點之一(操作系統、計網、數據庫),我是在 B 站上看過一些操作系統視頻,同時自己慢慢看書、看博客學的。其中死鎖、虛擬內存、堆棧、進程線程、內存管理、磁盤調度等都是重點,也是面試過程中問的比較多的一些知識點。你如果能夠在面試過程中講出來一些具體的操作系統知識,而不是泛泛而談,肯定是很加分的,比如常見知識點進程線程區別,在提到線程切換比進程更快時,你如果能夠很清楚明白的説出來進程切換做了哪些、線程切換做了哪些以及線程為什麼比進程快,毫無疑問很加分的。  推薦資料: B站哈工大操作系統://www.bilibili.com/video/BV1d4411v7u7 B站清華大學操作系統://www.bilibili.com/video/BV1js411b7vg B站美國麻省理工MIT 6.828操作系統神級課程://www.bilibili.com/video/BV1px411E7ST 現代操作系統(也是講操作系統的一本好書,講的很細) 深入理解計算機系統(大名鼎鼎的CSAPP,被譽為“和金子一樣重要的計算機基礎書籍” , 很厚的一本黑皮書,需要慢慢看) 現代操作系統:原理與實現(上海交通大學陳海波教授的著作,書中主要介紹操作系統的理論與具體實現細節等,感覺不如CSAPP) 3、計算機網絡         計算機網絡也是重點之一,特別是 HTTP 以及 TCP/UDP 相關知識點,算是校招必備考點了,面試必問,但是難度是逐年上升的,原因可能就在於內卷程度越來越嚴重了吧。比如説以前對於三次握手四次揮手只問過程,現在直接讓面試者畫出客户端以及服務器端的各個狀態碼以及解釋各種意外情況,比如 SYN 請求丟失會怎麼樣?         建議計網的學習先從視頻入手,然後再看經典書籍,畢竟視頻中的知識都是別人總結好又給你講解的,只有自己親自揣摩、親自動手實踐得來的知識才是自己的,自己學來的才是真,經過實踐方知分曉的~ 推薦資料: B站韓立剛老師的計算機網絡(韓老師講課詼諧易懂,讓你在哈哈大笑中學到很多知識點://www.bilibili.com/video/BV17p411f7ZZ) 圖解HTTP、圖解TCP/IP(這兩本書比較簡單,日本人寫的,把複雜的知識點簡單化) 網絡是怎樣連接的(這本書緊緊圍繞一個問題:輸入一個URL,直到我們在網頁端看到請求的內容,這中間發生了什麼?抽絲剝繭將這個問題逐步細化,帶你走完整個網頁訪問的過程) 計算機網絡:自頂向下方法 (也是常見經典書籍之一,重點看第三章傳輸層TCP/UDP) 4、Linux        C++ 跟 Linux 基本是離不開的,特別是後端方向跟網絡通信關係很大。在實際工作裏,很多成熟的項目都是在 Linux 上進行開發的。所以有必要學一些 Linux 以及一些網絡通信編程,網絡通信涉及到的知識點很多,比如 IO 模型、線程池、多線程之類的。本人在秋招過程中被問過不少網絡通信的問題,最頻繁的就是 select、poll、epoll 的區別以及相關底層實現了。這裏也推薦一些資料,都是我個人看過的。 推薦資料: 鳥哥的Linux以及Linux就該這麼學這兩本書(個人感覺更適合作為一本工具書來使用,當然了,如果你有充足的時間也可以系統的看上一遍,對於Linux也會有更深的認識和了解了) TCP/IP網絡編程(韓國人寫的,書中例子很多,適合作為入門,另外github上有很多筆記,可以邊看別人的筆記邊看書,加深個人理解) Linux高性能服務器編程(遊雙老師的書,其中前四五章講的是計網的東西,後面講的很好,涉及內容很多,看完就大概明白服務端編程常見知識點和所需要掌握的技能了) Linux多線程服務端編程:使用muduo C++網絡庫  (北師大陳碩大神的書,需要很多基本,建議後期再看,我也只是看了一小半) 5、數據庫         數據庫主要問的都是 MySQL 以及 Redis 相關的一些知識,普通研發崗掌握這兩個基本也夠用了,數據庫常問知識點包括索引相關、性能優化、B+ 樹、Redis 底層模型、跳錶以及緩存擊穿、雪崩、穿透等常見問題。有時候也會讓你手寫一些簡單的 SQL 語句,比如給你一個學生表和課程表,讓你找出成績排名前十的學生姓名之類的。 推薦資料: MySQL必知必會(一本很薄的小冊子,不到一週就看完了,看完基本的SQL語句沒什麼問題了) 高性能MySQL(建議只看索引以及優化這兩章,後續的可以慢慢再看,這本書,真的真的很厚。) Redis設計與實現(算是Redis入門資料吧,認真看完的話就對Redis有大概瞭解了,話説Redis這麼火爆是有原因的,其中的一些精妙設計真的看完令人大呼過癮,不得不承認,人與人之間真是有差距的。。。) 極客時間- Redis核心技術與實戰 (中科院的研究員開設的Redis專欄,個人已經買了,非常不錯) 另外再推薦基本數據庫底層的書籍:數據庫系統實現(華東師範大學數據學院指定數據庫原理書籍)、MySQL技術內幕 -InnoDB存儲引擎(InnoDB的詳細剖析) 6、C++         C++ 的知識點比較多,也比較細,其實 C++ 並不容易學好,如果你只是簡單學習一下語法比如 for 循環、變量類型之類的,那麼一兩週你就可以上手,但是如果想要學好 C++ 還是需要持之以恆的 coding,由於個人是 C++ 技術棧,這裏也只是推薦 C++ 相關書籍和視頻,都是本人自己看過的經典書籍和資料。 推薦資料: B站黑馬C++視頻(黑馬機構出版的入門級C++教學視頻,很不錯://www.bilibili.com/video/BV1Tb411j7uM) STL源碼剖析視頻(C++大師侯捷老師的源碼視頻,搭配STL源碼剖析看效果更佳://www.bilibili.com/video/BV1db411q7B8) C++ Primer 第五版(我願稱之為C++聖經,800頁左右,我看了2遍,超級棒!強推!) Effective C++、More Effective C++(前者2遍,後者1遍,跟C++Primer中很多內容是有重複的) STL源碼剖析(源碼方面的好書,看了2遍,現在時不時還拿出來翻翻) 深入探索C++對象模型(重點是虛函數那一章,看完你就會對虛函數有新的認識了) 碎碎念         可能有些人會問,這些書你都看了嗎?這也太多了之類的?自己能不能看完?         説實話,看着是挺多,但是其中有很多知識點是一樣的,比如你詳細瞭解計網後,遊雙老師的那本 Linux 高性能服務端編程中的前四章你大概略過即可,就不再需要細看了,知識是有相關性和相通性的,有了前面的沉澱後期自然就好很多了。         還有就是學會善用目錄。有時候,看過目錄後就大概知道這章或者這小節講的是什麼了,建議在看一本書的時候先看一遍目錄,挑選出自己不懂得或者感興趣的章節來看,而將已看過的或者暫時不需要的放到後期再去看  。         慢慢學、慢慢看,慢慢的就會有收穫了。         如果你像我一樣學校不太好,不是什麼重點學校或者不是計算機專業的,那麼請你笨鳥先飛,贏在起跑線上。上面的書籍資料之類的,我並不是在 5 個月時間內看完的,自從學 C++ 以來就慢慢看、慢慢學的,我想其他語言,Java/Go 之類的也應該如此,本號後續也會分享 Java/Go/前端 的一些小知識,因為崗位要求是全棧,所以日後也會點亮個人前端技能點。         正所謂,天道酬勤,你付出汗水和努力,剩下的交給時間就好! 最近在看彙編語言,王爽老師的那本《彙編語言》講得真好,以前的那些寄存器、數據總線、地址總線概念忽然變得很清晰明瞭、活靈活現了,我自己也慢慢學會使用匯編寫一些程序,懂得一些指令級程序優化的思路和方法,算是沉迷其中不可自拔吧!哈哈~         一入IT就做好終生學習的準備吧,你既然想要拿別人拿不了的高薪,怎麼能不付出比別人多的汗水、時間和精力呢?天上掉餡餅是不可能的,如果你還在想着偷懶耍滑,想要不付出時間和汗水就想拿到好 offer ,説明你並不是很適合計算機這一行~ 結語         如果你沒有別人聰明,不如別人條件好,如果你下定決心學習計算機,請你多投入時間、多投入精力、多投入汗水! 小夥伴你好,我是阿秀,一枚從底層慢慢爬到互聯網大廠的程序員,公眾號上每一篇原創文章都是我精心創作、慢慢打磨出來的,如果你覺得本文對你有所幫助,麻煩點亮一下「贊」和「在看」,也可以「分享」給需要的小夥伴,阿秀真的很需要你的點亮,十分感謝! 本公眾號將會陸續分享編程學習相關的良心知識,敬請期待。 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-27 關鍵詞: 嵌入式 C語言

  • 一文詳解 C++ 日誌框架,是否應該自己造輪子?

    1 日誌框架  日誌框架 一個經過專門設計的實用程序,用於規範應用程序的日誌記錄過程。 日誌框架可以自己編寫(需要一定的能力哦),也可以由第三方(例如:log4cplus)提供。對於不同的日誌框架,各自在實現方式上也有所不同。 雖然可以簡單地“標準化”日誌(例如:調用文件系統 API,將信息寫入名為 log.txt 的文件),但是要成為一個嚴格意義上的框架,必須要超越標準化。也就是説,日誌框架必須通過處理日誌記錄來標準化解決方案,從而暴露一個標準的 API。 沒明白?那就再具體一些,設想一個日誌框架,封裝了三個主要部分: 當想要捕獲程序的運行時信息時,首先要發出要記錄的信息。然後格式化這些信息,最後決定將它輸出到哪裏。一般情況下,會輸出到文件中,但是也可以將其輸出到控制枱、數據庫,或者任何能夠接收數據的地方。 如果有一系列代碼,能夠解決這些問題,那麼就可以被看作是一個日誌框架。 2 為什麼不是 cout 使用日誌,只為成為更好的攻城獅。 也許有人會問:既然 C++ 中有 cout,為什麼還要使用日誌呢? 無法否認,在使用像 C++、Java、PHP 這樣的編程語言時,我們會經常將消息打印到控制枱,因為這是開發、測試和調試程序的一部分。但倘若我們正在處理一個服務端程序,卻無法看到其內部發生了什麼,這時該怎麼辦?唯一的可見性工具是日誌文件,如果沒有日誌,我們就不能進行任何調試,也無法知道程序內部在做什麼。 儘管 C++ 中有相當方便的 cout 輸出流,可以在控制枱上打印一些信息,或者可以通過其他方式將這些信息重定向到文件中,但這對於實際的應用程序來説根本不夠。尤其對於複雜的 C++ 程序來説,像 log4cplus 或任何其他日誌框架能夠提供了更多的靈活性,而這是 cout 不可能完成的。 在編寫代碼時,使用日誌框架是一種很好的實踐。即使像《代碼整潔之道》這樣的書籍,也建議學習像 Log4 這樣的框架進行日誌記錄。所以,應儘可能的在生產代碼中使用日誌,而不是用 cout 來打印東西(這是不可接受的)。 3 使用日誌的好處 日誌是一個優秀系統不可或缺的組成部分。 對於很多人來説,日誌的作用僅限於調試。其實不然,它在很多方面都非常有用。 日誌是最好的診斷工具 絕大多數人都曾面臨這樣的困境 - 一旦程序出現問題,很長時間都找不出原因! 缺少日誌,我們將不得不依賴於客户或技術支持,讓他們描述在什麼情況下發生了什麼(很可能會存在一些誤導)。隨後我們需要通過開發環境重現問題,並進行各種調試,直至錯誤修復為止,然而這一般會耗費很長時間。但若有日誌的幫助,我們便能迅速擺脱這種困境,可以很快地發現異常,並快速定位、解決問題! 日誌讓我們有機會監測模塊的瓶頸 隨着項目規模的增加,模塊會越來越多,調優也變成了一場持久戰。 通過記錄某些操作所花費的時間,我們可以及時地檢測模塊的瓶頸,並針對性地對一些耗時操作做出優化。 日誌有助於我們瞭解用户的行為 為了提高產品質量,提供個性化服務,就必須瞭解用户行為 - 他們做了什麼,想要什麼。 要搞清楚這些,當然要有數據,所以需要採集和分析用户的行為,而日誌無疑是最主要的數據來源。 4 要不要重新發明輪子 不要去重新發明輪子 - 《麥肯錫方法》 既然已經對日誌框架有了明確的瞭解,那麼應該使用現有的日誌框架,還是構建自己的日誌框架呢?其實,這是一個老生常談的問題了 - 要不要重新發明輪子?引用莎士比亞戲劇《哈姆雷特》中的一句名言: To be or not to be - that is the question. 現在我們正處於技術大爆發的時代,每一個技術領域中都有很多優秀的解決方案。因此,自己完全不需要、也不應該去寫日誌框架。就像不應該寫版本控制工具或 Bug 跟蹤管理工具一樣,其他人已經把這些東西搞出來了,而且搞的很好,Git、SVN,Bugzilla、Trac ...... 應有盡有,我們完全可以花很少的錢,甚至是免費使用它們。 所以,請避免重複勞動,不要去重新發明輪子。應該堅持在自己的領域解決問題,將時間花在刀刃上。話雖如此,但我們還是需要了解關於輪子的一些細節(誰造的?怎麼創造的?如何使用?),以從中接受靈感,並把自己的知識加進去。 因此,當需要一個日誌框架時,應該使用現有的框架,而不是去重新發明輪子。那麼,問題來了,日誌框架都有哪些呢? 5 日誌框架都有哪些 C++ 中的日誌框架有很多,其中比較著名的有: log4cxx:Java 社區著名的 Log4j 的 C++ 移植版,用於為 C++ 程序提供日誌功能,以便開發者對目標程序進行調試和審計。 log4cplus:一個簡單易用的 C++ 日誌記錄 API,它提供了對日誌管理和配置的線程安全、靈活和任意粒度控制(也基於 Log4j)。 Log4cpp:一個 C++ 類庫,可以靈活地記錄到文件、syslog、IDSA 和其他目的地(也基於 Log4j)。 google-glog:一個 C++ 語言的應用級日誌記錄框架,提供了 C++ 風格的流操作和各種輔助宏。 Pantheios:一個類型安全、高效、泛型和可擴展性的 C++ 日誌 API 庫(號稱 C++ 領域速度最快的日誌庫)。 POCO:還提供了一個 好的日誌支持文檔。 ACE:ACE 也有日誌支持。 Boost.Log:設計的非常模塊化,並且可擴展。 Easylogging++:輕量級高性能 C++ 日誌庫(只有一個頭文件)。 G3log:一個開源、支持跨平台的異步 C++ 日誌框架,支持自定義日誌格式。基於 g2log 構建,提升了性能,支持自定義格式。 Plog:可移植、簡單和可擴展的 C++ 日誌庫。 spdlog:一個快速的 C++ 日誌庫,只包含頭文件,兼容 C++11。 …… 這麼多框架,應該選擇哪一個呢? 由於每個人的需求和技術棧都不一樣,所以很難直接回答這個問題,但是有一些選擇標準可供參考。 6 日誌選擇標準 易用性 易於使用的框架,能讓你事半功倍。 易用性,是優秀框架的一個重要特性。所以無論使用什麼框架,都應優先考慮這一點。 對於日誌框架而言,一旦被選定,在後期開發過程中,項目組中的每個人都會頻繁使用。如果易用性不好的話,那絕對是一個噩夢!所以,在選擇日誌框架時,應儘量找那些由簡單、直觀的 API 實現的方案。衡量標準可參考:對於缺乏經驗的團隊成員,應該在查看文檔和簡單示例之後,就能夠快速上手。 性能(效率) 功能決定當下,性能決定未來。 另一個要考慮的重要因素是性能影響。正如上面提到的,我們會經常調用日誌,這可能會對程序的性能產生巨大的影響。 想象一下,一個日誌框架,需要花費很長的時間才能啓動,在每次調用時阻塞並執行文件 I/O,缺少緩衝機制……對於這樣的框架,你會用嗎? 因此,在評估日誌框架時,可以參考網上的一些文章來比較,也可以自己做一些比較性實驗(基準測試),例如:吞吐量 - 測量每秒可以完成多少次方法調用(越高越好);採樣 - 測量每次調用執行究竟花費了多少時間(越低越好)。 對現有代碼的影響 影響越小,越容易維護。 在以後的使用中,日誌將無處不在。就像會影響運行時性能一樣,它們也會影響源碼的可維護性。 從應用程序的角度來看,日誌所處的位置比較尷尬。之所以這麼説,是因為我們盡力隔離依賴性,提供良好的接口,並最小化耦合,在編程時考慮的是單一職責原則。然後日誌出現了,它到處都是,與周圍的代碼無關。與這些優秀的設計原則相比,日誌顯得有些背道而馳。 因此,應充分考慮所引入日誌的影響範圍,儘量讓我們的代碼改動最小化! 社區支持 每一個成功框架的背後,都有一個偉大的社區。 框架的生命力源於不斷地完善和發展,如果沒有強大的社區做支撐,這個框架便失去了源動力。 因此,在選擇框架時,這一點非常重要 - 所選的框架是否有專門的團隊做支撐?在像 Stack Overflow 這樣的問答網站上,它是否有很強的存在感?確保一點,如果在使用過程中遇到了問題,你能有辦法快速解決。倘若選擇了一個不知名的框架,當遇到了 Bug 時,那麼可能會浪費大量的時間來解決問題。 完整性 完整的框架,鑄就完美的生產力。 在最前面,我們將日誌的功能分為三個主要部分:日誌記錄、格式化和輸出地,所以要確保所選的日誌框架徹底解決了這些問題。 日誌記錄和輸出地比較基礎,幾乎所有日誌框架都有這些概念。話雖如此,但對於好的框架來説,應該巧妙地將日誌記錄與輸出地分開,並且還應該有多種可選的輸出地。在理想情況下,最好能夠自定義輸出地。 一般情況下,格式化日誌文件會整齊地排列,並具有很好的可讀性。但在 DevOps 的世界裏,這些遠遠不夠。具體來説,日誌文件需要被格式化為可解析的數據。通過將日誌輸出作為數據處理,可以很容易地聚合、搜索和可視化日誌,從而能夠在生產支持方面助你一臂之力。所以,要確保日誌框架擁有這種能力。 發展前景 只有前途光明,方能大行其道。 無法繞過這一點 - 在選擇日誌框架時,不僅要考慮它的現狀,還應該注重它的發展前景。 像上面提到的 C++ 日誌框架,每一個都非常優秀且特點鮮明。但有一些卻獲得了更多的關注度,例如 log4cplus、glog,為什麼如此呢?因為它們有很強大的“基因”和“後台”,一個是著名的 Log4j 的衍生品,另一個則是 Google 的“親兒子”。 一個框架的發展前景,取決於眾多因素 - 關注度、用户基數、社區活躍度 …… 要想大行其道,這些幾乎都不能少。 往期推薦 1、深度好文|面試官:進程和線程,我只問這19個問題 2、他來了,他來了,C++17新特性精華都在這了 3、一文讓你搞懂設計模式 4、C++11新特性,所有知識點都在這了! 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-27 關鍵詞: 嵌入式 C語言

  • 乾貨!結構體、聯合體嵌套使用的一些實用操作

    結構體、聯合體是C語言中的構造類型,結構體我們平時應該都用得很多。但是,對於聯合體,一些初學的朋友可能用得並不多,甚至感到陌生。我們先簡單看一下聯合體: 在C語言中定義聯合體的關鍵字是union。 定義一個聯合類型的一般形式為: union 聯合名{成員表}; 成員表中含有若干成員,成員的一般形式為:類型説明符 成員名。其佔用的字節數與成員中最大數據類型佔用的字節數。 下面我們一起看一下結構體、聯合體結合使用在C語言、嵌入式中的一些實用技巧。 1、應用於管理不同的數據 示例代碼: enum DATA_PKG_TYPE{    DATA_PKG1 = 1,    DATA_PKG2,    DATA_PKG3    };struct data_pkg1{    // ...};struct data_pkg2{    // ...};struct data_pkg3{    // ...};struct data_pkg{    enum DATA_PKG_TYPE data_pkg_type;    union     {       struct data_pkg1 data_pkg1_info;   struct data_pkg2 data_pkg2_info;   struct data_pkg3 data_pkg3_info;    }data_pkg_info;}; 這裏把struct data_pkg1、struct data_pkg2、struct data_pkg3三個結構體放到了struct data_pkg這個結構體裏進行管理,把data_pkg_type與union裏的三個結構體建立一一對應關係,我們需要用哪一結構體數據就通過data_pkg_type來進行選中。 在進行數據組包的時候,先給data_pkg_type進行賦值,確定數據包的類型,再給對應的union裏的結構體進行賦值;在進行數據解析的時候,通過data_pkg_type來選擇解析哪一組數據。 思考一下,如果在union裏面再嵌套一層union會怎麼樣?會變得更復雜?以前的話,我會覺得越嵌套會越複雜,我也很抵制這種不斷嵌套的做法。但後來看了我同事魚鷹(公眾號:魚鷹談單片機)的設計之後,我驚呆了!這可太秀了,他就是這麼嵌套使用把原本複雜的系統數據管理得明明白白的。我們看他怎麼設計的(看個大概的圖): 可以看到最左邊和最右邊這就建立起了一一對應關係,我們的模塊很多,數據很多,但是在這樣的設計中顯得很清晰、很容易維護。 2、寄存器、狀態變量封裝 我們看一看TI的寄存器封裝是怎麼做的: 所有的寄存器被封裝成聯合體類型的,聯合體裏邊的成員是一個32bit的整數及一個結構體,該結構體以位域的形式體現。這樣就可以達到直接操控寄存器的某些位了。比如,我們要設置PA0引腳的GPAQSEL1寄存器的[1:0]兩位都為1,則我們只操控兩個bit就可以很方便的這麼設置: GpioCtrlRegs.GPAQSEL1.bit.GPIO0 = 3 或者直接操控整個寄存器: GpioCtrlRegs.GPAQSEL1.all |=0x03  位域相關文章:【四方a集運倉電話】位域 如果不是工作於芯片原廠,寄存器的封裝應該離我們很遠。但我們可以學習使用這種方法,然後用於我們的實際應用開發中。 下面就看一種實際應用:管理一些狀態變量。 示例代碼: union sys_status{   uint32 all_status;   struct    {      bool status1:  1; // FALSE / TRUE      bool status2:  1; //       bool status3:  1; //       bool status4:  1; //       bool status5:  1; //       bool status6:  1; //       bool status7:  1; //       bool status8:  1; //       bool status9:  1; //       bool status10: 1; //    // ...  }bit;}; 之前記得羣裏有一位小夥伴問系統有幾十個狀態變量需要管理,怎麼做比較好。如上例子就是比較好的一種管理方法。 3、數據組合/拆分、大小端 (1)驗證大小端 #include typedef unsigned int  uint32_t;typedef unsigned char uint8_t;union bit32_data{    uint32_t data;    struct     {        uint8_t byte0;        uint8_t byte1;        uint8_t byte2;        uint8_t byte3;    }byte;};int main(void){    union bit32_data num;        num.data = 0x12345678;   if (0x78 == num.byte.byte0)  {   printf("Little endian\n");  }  else if (0x78 == num.byte.byte3)  {   printf("Big endian\n");  }else{}    return 0;} 運行結果: (2)數據組合、拆分 這其實也就是上一篇文章《面試題 | 獲取整數各個字節》介紹的。在數據組合與拆分之前首先需要確實當前平台的大小端。比如小編使用的平台是小端模式。 ① 把0x12345678拆分成0x78、0x56、0x34、0x12: #include typedef unsigned int  uint32_t;typedef unsigned char uint8_t;union bit32_data{    uint32_t data;    struct     {        uint8_t byte0;        uint8_t byte1;        uint8_t byte2;        uint8_t byte3;    }byte;};int main(void){    union bit32_data num;        num.data = 0x12345678;    printf("byte0 = 0x%x\n", num.byte.byte0);    printf("byte1 = 0x%x\n", num.byte.byte1);    printf("byte2 = 0x%x\n", num.byte.byte2);    printf("byte3 = 0x%x\n", num.byte.byte3);    return 0;} 運行結果: ② 把0x78、0x56、0x34、0x12組合成0x12345678: #include typedef unsigned int  uint32_t;typedef unsigned char uint8_t;union bit32_data{    uint32_t data;    struct     {        uint8_t byte0;        uint8_t byte1;        uint8_t byte2;        uint8_t byte3;    }byte;};int main(void){    union bit32_data num;        num.byte.byte0 = 0x78; num.byte.byte1 = 0x56; num.byte.byte2 = 0x34; num.byte.byte3 = 0x12;    printf("num.data = 0x%x\n", num.data);    return 0;} 運行結果: 但是數據組合與拆分有更好的方法:移位操作。篇幅有限不再貼出代碼,詳細代碼可參考:《面試題 | 獲取整數各個字節》、《C語言、嵌入式位操作精華技巧大彙總》兩篇文章。 4、結構體 & 緩衝區 #define BUF_SIZE 16union protocol_data{ uint8_t data_buffer[BUF_SIZE]; struct  {  uint8_t data1;  uint8_t data2;  uint8_t data3;  uint8_t data4;  // ... }data_info;}; 這種應用得很廣泛,用於自定義通信協議。struct裏面的內容可以設計得很簡單,比如全是有用的數據,或是設計得很複雜,包含一些協議頭尾、包長、有效數據、校驗等內容。 但無論如何,我們組包發送的過程是填充結構體->發送data_buffer;反之接收數據解析的過程就是接收數據存於data_buffer->使用結構體數據。我們之前分享的《乾貨 | protobuf-c之嵌入式平台使用》也是這個思路。 5、傳輸浮點數據 union f_data { float f; struct {  unsigned char byte[4]; };} 類似的,使用這樣子的方法可以用於傳輸浮點數,更具體地不再展開,網絡上有很多這一塊的資料。感興趣的朋友可以自己操作驗證驗證。 最後 以上就是本次的分享,如果覺得文章不錯,轉發、在看,也是我們繼續更新的動力。 猜你喜歡: 2020年精選原創筆記彙總 長文 | 一些Linux知識彙總 硬核 | 關於Linux內核的簡明知識 1024G 嵌入式資源大放送!包括但不限於C/C++、單片機、Linux等。在公眾號聊天界面回覆1024,即可免費獲取! 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-27 關鍵詞: 嵌入式 C語言

  • OOP面向對象編程:由C語言到C++

    鏈接://www.cnblogs.com/whale90830/p/10488595.html 由C到C++ OOP第一課 C語言的侷限 C++的特點 C++的程序特徵 C++程序的結構特性 C++程序的編輯、編譯和運行 ⭐C++對C的補充 C語言的侷限 類型檢查機制相對較弱,使得程序中的一些錯誤不能在編譯時由編譯器檢查出來。 C語言本身沒有支持代碼重用的語言結構 不適合開發大型程序,當程序的規模達到一定的程度時,程序員很難控制程序的複雜性。 C++的特點 C++繼承了C的優點,並有自己的特點,主要有: 1、全面兼容C,C的許多代碼不經修改就可以為Cpp所用,用C編寫的庫函數和實用軟件可以用於Cpp。 2、用C++編寫的程序可讀性更好,代碼結構更為合理,可直接在程序中映射問題空間結構。 3、生成代碼的質量高,運行效率高。 4、從開發時間、費用到形成軟件的可重用性、可擴充性、可維護性和可靠性等方面有了很大提高,使得大中型的程序開發項目變得容易得多。 5、支持面向對象的機制,可方便的構造出模擬現實問題的實體和操作。 C++的程序特徵 例1.1 輸出一行字符:“This is a C++ program.”。 程序如下: #include  //包含頭文件iostreamusing namespace std; //使用命名空間stdint main( ){    couta>>b; //輸入語句    sum=a+b; //賦值語句    coutb; //輸入變量a和b的值    m=max(a,b); //調用max函數,將得到的值賦給m    cout

    時間:2020-12-25 關鍵詞: 嵌入式 C語言

  • C++高端知識點之智能指針和enable_shared_from_this機制

    大家好,我是小牛,今天跟聊一下 BAT 面試 C++ 開發工程師必問的一個考點:智能指針。 小艾:你昨晚面 C++ 去了? 小牛:對啊,不是這個廠主要技術棧都是 C++ 嘛,我就面去了。 小艾:問了點啥啊? 小牛:BAT 這 C++ 問的都差不多,又問智能指針了。 小艾:那來講講唄。 小牛:來。 智能指針的引入 大家都知道,指針是 C++ 中非常重要的一部分,大家在初期學習 C++ 的時候一定學過類似這樣的指針方式。 int  *ptr; 這種指針也被稱為裸指針。但是使用裸指針會存在一些不足: 如果使用裸指針分配內存後,忘記手動釋放資源,會出現內存泄漏。 如果使用多個裸指針指向同一資源,其中一個指針對資源進行釋放,其它指針成為空懸指針,如果再次釋放會存在不可預測的錯誤。上圖中當 sp1 把資源釋放後,sp2 成了空懸指針。空懸指針指的是指針所指向的對象已經釋放的時候自身卻沒有被置為 nullptr。sp1 通過 free/delete 釋放資源的內存時,內存不會立刻被系統回收,而是狀態改變為可被其它地方申請的狀態。這時當再次操作 sp2,這塊內存可能被其它地方申請了,而具體被誰申請了是不確定的,因此可能導致的錯誤也是不可預測的。 如果程序異常退出時,裸指針的釋放資源的代碼未能執行,也會造成內存泄漏。 為了改善裸指針的不足,確保資源的分配和釋放是配對的,開發者提出了智能指針。智能指針主要是對裸指針進行了一次面向對象的封裝,在構造函數中初始化資源地址,在析構函數中釋放資源。 當資源應該被釋放時,指向它的智能指針可以確保自動地釋放它。 C++ 庫中,為智能指針提供了不帶引用計數和帶引用計數的兩種方案。 引用計數用於表示有多少智能指針引用同一資源。不帶引用計數的智能指針採用獨佔資源的方式,而帶引用計數的智能指針則可以同時多個指向同一資源。下面介紹一下它們的主要特點和區別。 智能指針的分類 不帶引用計數的智能指針 不帶引用計數的智能指針包括 auto_ptr、scoped_ptr 和 unique_ptr 三種指針。 不帶引用計數的智能指針 1. auto_ptr: 我們先來看個例子: #includeint main(){ auto_ptr  ptr(new int(6));//定義auto_ptr指針ptr auto_ptr  ptr1(ptr);  //拷貝構造ptr定義ptr1 *ptr=8;//對空指針ptr賦值會產生不可預料的錯誤 return 0;} 開始時 ptr 指向資源,一個整型數字6,當用 ptr1 拷貝構造 ptr 時,ptr1 指向資源,而 ptr 則指向 nullptr。下一行程序中如果對空指針 ptr 賦值 8,將會產生不可預料的錯誤。 下圖表示 auto_ptr 指針對資源的指向過程。 auto_ptr 使用拷貝構造時,如果只有最後一個 auto_ptr 持有資源,其餘 auto_ptr 持有的資源會被置為 nullptr。 因此需要注意,不能在容器中使用 auto_ptr,當容器發生拷貝時,原容器中 auto_ptr 持有的資源會置 nullptr。 下面我們再來看一下 auto_ptr 的部分源碼和部分解析: templateclass auto_ptr{ public: typedef _Ty element_type; explicit auto_ptr(_Ty * _Ptr=nullptr) noexcept     : _Myptr(_Ptr)//初始化列表     { //構造函數     } auto_ptr(auto_ptr& _Right) noexcept  : _Myptr(_Right.release())  { //拷貝構造函數,會調用release()函數  }   _Ty * release() noexcept  {            /*使用拷貝構造時,最後一個auto_ptr持有資源,   其餘被置為nullptr*/   _Ty * _Tmp = _Myptr;  _Myptr = nullptr;  return (_Tmp);  }private: _Ty * _Myptr;//指向資源 }; 當試圖調用 auto_ptr 的拷貝構造函數時,在初始化列表中調用了 release() 函數,release() 函數用一個 _Tmp 指針保存資源並返回用於初始化當前的 auto_ptr 的類成員 _Myptr,而 _Right 對應的 _Myptr 被置為 nullptr。 2. scoped_ptr scoped_ptr 和 auto_ptr 有些不同,它私有化了拷貝構造函數和賦值函數,資源的所有權無法進行轉移,也無法在容器中使用。 下面使用一段代碼表現 scoped_ptr 的特性,如果不規範使用會發生錯誤。 正確用法: scoped_ptr sp1(new int(6));//初始化sp1指針 錯誤用法: scoped_ptr sp2(sp1);//錯誤,無法拷貝構造 這種方法是錯誤的,因為scoped_ptr私有化了拷貝構造函數,無法顯式調用。  scoped_ptr sp3(new int(5))//初始化sp2指針 sp1=sp3;//錯誤,無法賦值 這種方法是錯誤的,因為scoped_ptr私有化了賦值構造函數,無法顯式調用。 有時候面試官會問到,scoped_ptr 是如何保證資源的所有權的? 這時候就可以按照上面的講解來回答: scoped_ptr 私有化了拷貝構造函數和賦值函數,資源的所有權無法進行轉移,所以保證了資源的所有權。 然後再來看一下 scoped_ptr 的部分源碼和部分解析: template class scoped_ptr{private:    T * px;     scoped_ptr(scoped_ptr const &);//拷貝構造函數    scoped_ptr & operator=(scoped_ptr const &);//賦值構造函數 public:    typedef T element_type;    explicit scoped_ptr( T * p = nullptr ): px( p )    {    }    ~scoped_ptr() //析構函數}; scoped_ptr 通過私有化拷貝構造函數和賦值構造函數來拒絕淺拷貝的發生。 值得注意的是,auto_ptr 是通過將除最後一個以外的其它 auto_ptr 置 nullptr 來避免淺拷貝的發生,它的資源所有權是可以轉移的。 而 scoped_ptr 是直接禁止了拷貝與賦值,資源所有權無法轉移。 3. unique_ptr unique_ptr 刪除了拷貝構造函數和賦值函數,因此不支持普通的拷貝或賦值操作。如下所示: unique_ptr p1(new int(6));//正確寫法unique_ptr p2(p1); //這麼寫是錯誤的:// unique_ptr不支持拷貝unique_ptr p3;p3=p2;//這麼寫是錯誤的:unique_ptr不支持賦值 再來看一下 unique_ptr 的部分源碼和部分解析: templateclass unique_ptr: public _Unique_ptr_base{ public: typedef _Unique_ptr_base _Mybase; typedef typename _Mybase::pointer pointer; typedef _Ty element_type; typedef _Dx deleter_type; unique_ptr(unique_ptr&& _Right) noexcept  : _Mybase(_Right.release(),   _STD forward(_Right.get_deleter()))  { // 右值引用的拷貝構造函數  } unique_ptr& operator=(unique_ptr&& _Right) noexcept  { //提供了右值引用的operator=賦值構造函數  if (this != _STD addressof(_Right))   {    reset(_Right.release());   this->get_deleter() = _STD forward   (_Right.get_deleter());   }  return (*this);  } /* 刪除了unique_ptr的拷貝構造和賦值函數,拒絕淺拷貝 */ unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete; }; unique_ptr和scoped_ptr一樣禁止了拷貝構造和賦值構造,引入了帶右值引用的拷貝構造和賦值。可以把 unique_ptr 作為函數的返回值。 不帶引用計數的智能指針總結: 相同點:最終只有一個智能指針持有資源。 不同點: auto_ptr 進行拷貝構造時,會對之前的auto_ptr的資源置nullptr操作; scoped_ptr 通過私有化了拷貝構造和賦值函數杜絕淺拷貝; unique_ptr 通過刪除了拷貝構造和賦值函數函數杜絕淺拷貝,但引入了帶右值引用的拷貝構造和賦值函數。 帶引用計數的智能指針 當需要多個智能指針指向同一個資源時,使用帶引用計數的智能指針。 每增加一個智能指針指向同一資源,資源引用計數加一,反之減一。當引用計數為零時,由最後一個指向資源的智能指針將資源進行釋放。 下圖表示帶引用計數智能指針的工作過程。sp1 對象和 sp2 對象通過指針指向同一資源,引用計數器記錄了引用資源的對象個數。 智能指針的工作過程 當 sp1 對象發生析構時,引用計數器的值減 1,由於引用計數不等於 0,資源並未釋放,如下圖所示: sp1 對象發生析構 當 sp2 對象也發生析構,引用計數減為 0,資源釋放,如下圖所示: sp2 對象也發生析構 即引用計數可以保證多個智能指針指向資源時資源在所有智能對其取消引用再釋放,避免過早釋放產生空懸指針。帶引用計數的智能指針包括 shared_ptr 和 weak_ptr。 資源釋放 1. shared_ptr shared_ptr 一般稱為強智能指針,一個 shared_ptr 對資源進行引用時,資源的引用計數會增加一,通常用於管理對象的生命週期。只要有一個指向對象的 shared_ptr 存在,該對象就不會析構。 上圖中引用計數的工作過程就使用了 shared_ptr。 2. weak_ptr weak_ptr 一般被稱為弱智能指針,其對資源的引用不會引起資源的引用計數的變化,通常作為觀察者,用於判斷資源是否存在,並根據不同情況做出相應的操作。 比如使用 weak_ptr 對資源進行弱引用,當調用 weak_ptr 的 lock() 方法時,若返回 nullptr,則説明資源已經不存在,放棄對資源繼續操作。否則,將返回一個 shared_ptr 對象,可以繼續操作資源。 另外,一旦最後一個指向對象的 shared_ptr 被銷燬,對象就會被釋放。即使有 weak_ptr 指向對象,對象也還是會被釋放。 小艾問:既然它這引用都不算數,那它有什麼用呢? 小牛答:別急,我們來慢慢講。 enable_shared_from_this 機制 小牛:考慮下面這樣一個場景: 在多線程環境中,假設有一個對象池類 ObjectPool 和一個對象類 Object。ObjectPool 類主要實現通過不同的 key 返回對應 Object 對象。 要求同一程序中由 Object 類實例出的不同對象只有一個,即當多處用到同一個對象,Object 對象應該被共享。同時當對象不再需要時應該被析構,並刪除對應的 key。 多線程應用場景 小艾説:這還不簡單,看我的。代碼刷的一下就寫完了。 //場景代碼#includeclass ObjectPool:boost::noncopyable{public: shared_ptr get(const string& key) { shared_ptr shObject;    MutexLockGuard lock(mutex); weak_ptr& wkObject=object[key]; shObject=wkObject.lock(); //對象存在,提升成功並返回 if(!shObject){  /*對象不存在,提升失敗,shOject重新  指向新創建的Object對象,  並綁定回調函數,讓對象Oject需要析構時  調用OjectPool對象的成員函數*/       shObject.reset(new Object(key),                    boost::bind(&        ObjectPool::deleteObject,this,        _1));       wkObject=shObject; } return shObject; } private: void deleteObject(Object* obj) {   /*回調函數,在對象需要析構時調用,從map中 刪除對象和對應的key*/  if(obj){   MutexLockGuard lock(mutex);   object.erase(obj->key());  }  delete obj; } mutable MutexLock mutex; std::map object; /*map中不能使用shared_ptr,這會導致Oject對象永遠不會被銷 毀*/}; 小牛説:你這有問題啊? 小艾答:有什麼問題?為了實現 Object 類析構時調用 ObjectPool 的回調函數,代碼中把 ObjectPool 的 this 指針保存在了 boost::function 處。 小牛説:那線程安全問題就來了。如果 ObjectPool 先於 Object 對象析構,就會發生 core dump。因為 ObjectPool 對象已經不存在了,也就沒有辦法調用其成員方法。 小艾問:那怎麼解決呢? 小牛説:簡單啊,只需將 this 指針替換成指向當前對象的 shared_ptr,從而保證在 Object 對象需要調用 ObjectPool::deleteObject 時 ObjectPool 還活着。你要不試試實現一下? 小艾説:那我寫一個吧。 shared_ptr getSharedPtr() {    return shared_ptr(this); } 小牛答:問題來了,在多線程環境中,在需要返回 this 對象時是無法得知對象的生存情況的。因此不能直接返回 this 對象。 給你普及個解決方法吧,你可以通過繼承 enable_shared_from_this 模板對象,然後調用從基類繼承而來的 shared_from_this 方法來安全返回指向同一資源對象的 shared_ptr。 小艾:為什麼繼承 enable_shared_from_this 模板對象就可以安全返回? 小牛:在回答你的問題前,我們先來講講 shared_ptr 的構造函數和拷貝構造函數對資源和引用計數影響的區別。 下面從 shared_ptr 的實現原理來看: shared_ptr 從 _Ptr_base 繼承了 element_type 和 _Ref_count_base 類型的兩個成員變量。 templateclass _Ptr_base{ private: element_type * _Ptr{nullptr}; // 指向資源的指針 _Ref_count_base * _Rep{nullptr}; // 指向資源引用計數的指針}; _Ref_count_base 中定義了原子類型的變量 _Uses 和 _Weaks,它們分別記錄資源的引用個數和資源觀察者的個數。 class __declspec(novtable) _Ref_count_base{ private: _Atomic_counter_t _Uses;//記錄資源引用個數 _Atomic_counter_t _Weaks;//記錄觀察者個數} 當要使用 shared_ptr 管理同一資源,調用 shared_ptr 的構造函數和拷貝構造函數是不一樣的,它們雖然使得不同 shared_ptr 指向同一資源,但管理引用計數資源的方式卻不一樣。 下面給出兩個 shared_ptr 管理同一資源(A對象)使用不同構造函數對引用計數對象的影響。 方式1:調用構造函數 class A{  public:  A(){}  ~A(){}};A *p = new A(); shared_ptr ptr1(p);//調用構造函數shared_ptr ptr2(p);//調用構造函數 方式1:調用構造函數 如上圖所示,方式1中 ptr1 和 ptr2 都調用了 shared_ptr 的構造函數,該構造方式使得 ptr1 和 ptr2 都開闢了自已的引用資源對象 _Ref_count_base,即 _Ref_count_base 有兩個,都記錄了 A 對象的引用計數為 1,析構時 ptr1 和 ptr2 的引用計數各自減為 1,導致 A 對象析構兩次,出現邏輯錯誤。 方式2:調用拷貝構造函數 class A{public:   A(){}   ~A(){}}A *p = new A(); shared_ptr ptr1(p);//調用構造函數shared_ptr ptr2(ptr1);//調用拷貝構造函數 方式2:調用拷貝構造函數 如上圖所示,方式2中由於 ptr2 拷貝構造 ptr1,它們引用的 _Ref_count_base 是同一個,因此引用計數為 2,析構的時候 A 對象只析構一次,正常運行。 在明白了 shared_ptr 構造函數和拷貝構造函數的做的事情不同後,就能理解當需要返回一個需要 shared_ptr 管理的對象為什麼不能寫成 return shared_ptr< A >(this) 了。 小艾:説的沒錯,因為這樣會調用 shared_ptr 的構造函數,對於 this 對象再創建一個新的引用計數對象,從而導致對象多次析構而出現邏輯錯誤。 小牛:再給你深入講講 enable_shared_from_this 的實現機制。 如下所示,enable_shared_from_this 類中包含一個作為觀察者的成員變量。 templateclass enable_shared_from_this{ public:   mutable weak_ptr _Wptr;//指向資源}; 當一個類繼承了 enable_shared_from_this 類,就繼承了 _Wptr 這個成員變量。 當使用 shared_ptr< A >(new A()) 第一次構造智能指針對象時,就會初始化一個作為觀察者的弱智能指針 _Wptr 指向A對象資源。 再通過 shared_from_this() 方法代替 shared_ptr 的普通構造函數來返回一個 shared_ptr 對象,從而避免產生額外的引用計數對象。 shared_ptr getSharedPtr() {    return shared_from_this(); } 在 shared_from_this 函數中,主要嘗試將弱智能指針提升為強智能指針來返回一個 shared_ptr 對象。 這樣還能在多線程環境中判斷對象是否存活,存活即提升成功,安全返回。如果對象已經析構,則放棄提升,即起到了保證線程安全的作用。 小牛:瞭解了enable_shared_from_this,要不再試試改代碼? 小艾:那我來改一下之前的代碼。 第一處修改: class ObjectPool:boost::noncopyable//為class ObjectPool:public boost::enable_shared_from_this,                boost::noncopyable{/*...*/}; 第二處修改: //改變shared_ptr get(const string& key){  /*...*/  shObject.reset(new Object(key),                     boost::bind(&ObjectPool::deleteObject,this,_1));  /*...*/   }//為shared_ptr get(const string& key){  /*...*/  shObject.reset(new Object(key),                     boost::bind(&ObjectPool::deleteObject,shared_from_this(),_1));  /*...*/   } 完整代碼: #includeclass ObjectPool:public boost::enable_shared_from_this,                boost::noncopyable{public:shared_ptr get(const string& key){   shared_ptr shObject;   MutexLockGuard lock(mutex);   weak_ptr& wkObject=object[key];   shObject=wkObject.lock();//對象存在,提升成功並返回   if(!shObject){    /*對象不存在,提升失敗,shOject重新指向新創建的    Object對象,並綁定回調函數,讓對象Oject需要析構時    調用OjectPool對象的成員函數*/      shObject.reset(new Object(key),                      boost::bind(&          ObjectPool::deleteObject,shared_from_this(),          _1));      wkObject=shObject;   }   return shObject;}private:void deleteObject(Object* obj){   /*回調函數,在對象需要析構時調用,從map中刪除對象和對應的key*/    if(obj){     MutexLockGuard lock(mutex);     object.erase(obj->key());    }    delete obj;}mutable MutexLock mutex;std::map object;/*map中不能使用shared_ptr,這會導致Oject對象永遠不會被銷燬*/}; 小牛:不錯不錯,這下懂了 shared_ptr 和 weak_ptr 結合的用法了吧。 帶引用計數智能指針總結: shared_ptr 會增加資源的引用計數,常用於管理對象的生命週期。 weak_ptr 不會增加資源的引用計數,常作為觀察者用來判斷對象是否存活。 使用 shared_ptr 的普通拷貝構造函數會產生額外的引用計數對象,可能導致對象多次析構。使用 shared_ptr 的拷貝構造函數則隻影響同一資源的同一引用計數的增減。 當需要返回指向當前對象的 shared_ptr 時,優先使用 enable_shared_from_this 機制。 總結 今天我們瞭解了面試中常常會問到的C++ 智能指針的相關知識點,結合源碼和示例理清各種智能指針的特點。 並且結合一個實際的多線程應用場景,講解了enable_shared_from_this 機制,希望能對大家的學習有所幫助。 總結 參考 //blog.csdn.net/qiangweiyuan/article/details/88562935 《Linux多線程服務端編程使用muduo C++ 網絡庫》 《C++ Primer》 《More Effective C++》 感謝各位少俠閲讀,我們將會為大家帶來更多精彩原創文章 往期推薦 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-25 關鍵詞: 嵌入式 C語言

  • Redis的8大數據類型,寫得非常好!

    來源://blog.itzhouq.cn/redis2 NoSQL 開發中或多或少都會用到,也是面試必問知識點。 最近這幾天的面試每一場都問到了,但是感覺回答的並不好,還有很多需要梳理的知識點,這裏通過幾篇 Redis 筆記整個梳理一遍。關注公眾號Java技術棧回覆面試也可以刷我整理的系列面試題。 Redis 的八大數據類型 官網可查看命令://www.redis.cn/commands.html Redis-key 127.0.0.1:6379> keys *(empty list or set)127.0.0.1:6379> set name xxxOK127.0.0.1:6379> keys *1) "name"127.0.0.1:6379> set age 1OK127.0.0.1:6379> keys *1) "age"2) "name"127.0.0.1:6379> exists name  # 判斷key 是否存在(integer) 1127.0.0.1:6379> exists name1(integer) 0127.0.0.1:6379> move name 1(integer) 1127.0.0.1:6379> keys *1) "age"127.0.0.1:6379> set name yyyOK127.0.0.1:6379> expire name 10  # 設置key的過期時間,單位是秒(integer) 1127.0.0.1:6379> ttl name  # 查看當前key的剩餘過期時間(integer) 7127.0.0.1:6379> ttl name(integer) -2127.0.0.1:6379> type age  # 查看當前key的類型string127.0.0.1:6379> Redis 有以下 8 種數據類型 1、String(字符串) 127.0.0.1:6379> set key1 v1   #設置值OK127.0.0.1:6379> get key1"v1"127.0.0.1:6379> append key1 "hello"  # 追加值,如果不存在,相當於 set key(integer) 7127.0.0.1:6379> get key1"v1hello"127.0.0.1:6379> strlen key1  # 獲取字符串長度(integer) 7127.0.0.1:6379> 自增、自減 127.0.0.1:6379> set views 0OK127.0.0.1:6379> get views"0"127.0.0.1:6379> incr views  # 自增 1(integer) 1127.0.0.1:6379> get views"1"127.0.0.1:6379> decr views       # 自減 1(integer) 0127.0.0.1:6379> decr views(integer) -1127.0.0.1:6379> get views"-1"127.0.0.1:6379> incrby views 10  # 設置步長、自增 10 (integer) 9127.0.0.1:6379> decrby views 5      # 設置步長、自減 5(integer) 4 字符串範圍 127.0.0.1:6379> set key1 "hello,world!"OK127.0.0.1:6379> get key1"hello,world!"127.0.0.1:6379> getrange key1 0 3  # 截取字符串\[0, 3\]"hell"127.0.0.1:6379> getrange key1 0 -1  # 獲取全部的字符串,和 get key一樣"hello,world!"127.0.0.1:6379> 替換: 127.0.0.1:6379> set key2 abcdefgOK127.0.0.1:6379> get key2"abcdefg"127.0.0.1:6379> setrange key2 1 xx(integer) 7127.0.0.1:6379> get key2"axxdefg"127.0.0.1:6379> setex(set with expire):設置過期時間 和setnx(set if not exist):不存在再設置(在分佈式鎖中會經常使用) 127.0.0.1:6379> setex key3 30 "hello"  # 設置 30 秒後過期OK127.0.0.1:6379> ttl key3     # 剩餘過期時間(integer) 25127.0.0.1:6379> setnx mykey "redis"   # mykey 不存在時設置成功(integer) 1127.0.0.1:6379> keys *1) "key2"2) "key1"3) "views"4) "mykey"127.0.0.1:6379> setnx mykey "mongoDB"  # mykey 存在時設置失敗(integer) 0127.0.0.1:6379> get mykey     # mykey 值不變"redis"127.0.0.1:6379> mset 和 mget 127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3  # 同時設置多個值OK127.0.0.1:6379> keys *1) "k1"2) "k3"3) "k2"127.0.0.1:6379> mget k1 k2 k3   # 同時獲取多個值1) "v1"2) "v2"3) "v3"127.0.0.1:6379> msetnx k1 v1 k4 v4       # msetnx 是一個原子性的操作,要麼一起成功,要麼都失敗(integer) 0127.0.0.1:6379> get k4(nil)127.0.0.1:6379> 對象 set user:1 {name:zhangsan, age:3}     # 設置一個 user:1 對象 值為 json  字符來保存一個對象127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2OK127.0.0.1:6379> mget user:1:name user:1:age1) "zhangsan"2) "2"127.0.0.1:6379> getset:先 get 再 set 127.0.0.1:6379> getset db redis  # 如果不存在值,則返回 nil(nil)127.0.0.1:6379> get db"redis"127.0.0.1:6379> getset db mongodb  # 如果存在值,獲取原來的值,並設置新的值"redis"127.0.0.1:6379> get db"mongodb"127.0.0.1:6379> String 的使用場景:value 除了是字符串以外還可以是數字 計數器 統計多單位的數量 粉絲數 對象緩存存儲 2、List(列表) 基本的數據類型,列表。 在 Redis 中可以把 list 用作棧、隊列、阻塞隊列。 list 命令多數以 l開頭。 127.0.0.1:6379> lpush list one   # 將一個值或者多個值,插入到列表的頭部(左)(integer) 1127.0.0.1:6379> lpush list two(integer) 2127.0.0.1:6379> lpush list three (integer) 3127.0.0.1:6379> lrange list 0 -1   # 查看全部元素1) "three"2) "two"3) "one"127.0.0.1:6379> lrange list 0 1    # 通過區間獲取值1) "three"2) "two"127.0.0.1:6379> rpush list right   # 將一個值或者多個值,插入到列表的尾部(右)(integer) 4127.0.0.1:6379> lrange list 0 -11) "three"2) "two"3) "one"4) "right"127.0.0.1:6379> 彈出 pop 127.0.0.1:6379> lrange list 0 -11) "!"2) "world"3) "world"4) "hello"127.0.0.1:6379> lpop list  # 移除list的第一個元素"!"127.0.0.1:6379> lrange list 0 -11) "world"2) "world"3) "hello"127.0.0.1:6379> rpop list   # 移除list的第一個元素"hello"127.0.0.1:6379> lrange list 0 -11) "world"2) "world"127.0.0.1:6379> 索引 Lindex 127.0.0.1:6379> lrange list 0 -11) "hjk"2) "world"3) "world"127.0.0.1:6379> lindex list 1  # 通過下標獲取list中的某一個值"world"127.0.0.1:6379> lindex list 0"hjk"127.0.0.1:6379> Llen 長度: 127.0.0.1:6379> llen list(integer) 3127.0.0.1:6379> 移除指定的值: 127.0.0.1:6379> lrange list 0 -11) "hjk"2) "world"3) "world"127.0.0.1:6379> lrem list 1 world  # 移除list集合中指定個數的value,精確匹配(integer) 1127.0.0.1:6379> lrange list 0 -11) "hjk"2) "world"127.0.0.1:6379> lpush list hjk(integer) 3127.0.0.1:6379> lrange list 0 -11) "hjk"2) "hjk"3) "world"127.0.0.1:6379> lrem list 2 hjk(integer) 2127.0.0.1:6379> lrange list 0 -11) "world"127.0.0.1:6379> trim 截斷 127.0.0.1:6379> lrange mylist 0 -11) "hello1"2) "hello2"3) "hello3"4) "hello4"127.0.0.1:6379> ltrim mylist 1 2 # 通過下標截取指定長度,這個list已經被破壞了,截斷之後只剩下截斷後的元素OK127.0.0.1:6379> lrange mylist 0 -11) "hello2"2) "hello3"127.0.0.1:6379> rpoplpush :移除列表的最後一個元素,將他移動到新的列表中。 127.0.0.1:6379> lrange mylist 0 -11) "hello1"2) "hello2"3) "hello3"127.0.0.1:6379> rpoplpush mylist myotherlist  # 移除列表的最後一個元素,將他移動到新的列表中。"hello3"127.0.0.1:6379> lrange mylist 0 -1  # 查看原來的列表1) "hello1"2) "hello2"127.0.0.1:6379> lrange myotherlist 0 -1  # 查看目標列表中,確實存在該值1) "hello3"127.0.0.1:6379> lset:將列表中指定下標的值替換為另一個值,更新操作 127.0.0.1:6379> exists list  # 判斷這個列表是否存在(integer) 0127.0.0.1:6379> lset list 0 item  # 如果不存在的話,更新會報錯(error) ERR no such key127.0.0.1:6379> lpush list value1(integer) 1127.0.0.1:6379> lrange list 0 0 1) "value1"127.0.0.1:6379> lset list 0 item  # 如果存在,更新當前下標的值OK127.0.0.1:6379> lset list 1 other  # 如果不存在的話,更新會報錯(error) ERR index out of range127.0.0.1:6379> linsert:將某個具體的value插入到列表中某個元素的前面或者後面 127.0.0.1:6379> lrange mylist 0 -11) "hello1"2) "hello2"127.0.0.1:6379> linsert mylist before "hello2" hello(integer) 3127.0.0.1:6379> lrange mylist 0 -11) "hello1"2) "hello"3) "hello2"127.0.0.1:6379> linsert mylist after "hello2" hello(integer) 4127.0.0.1:6379> lrange mylist 0 -11) "hello1"2) "hello"3) "hello2"4) "hello"127.0.0.1:6379> 小結: list 實際上是一個鏈表,前後都可以插入 如果key不存在,創建新的鏈表 如果移除了所有的值,空鏈表,也代表不存在 在兩邊插入或者改動值,效率最高。 3、Set (集合) 127.0.0.1:6379> sadd myset "hello"  # set 集合中添加元素(integer) 1127.0.0.1:6379> sadd myset "world"(integer) 1127.0.0.1:6379> smembers myset      # 查看指定Set的所有值1) "world"2) "hello"127.0.0.1:6379> sismember myset hello  # 判斷某一個值是不是在set中(integer) 1127.0.0.1:6379> sismember myset hello1(integer) 0127.0.0.1:6379> 127.0.0.1:6379> scard myset  # 獲取集合中的個數(integer) 2127.0.0.1:6379> sadd myset "hello2"(integer) 1127.0.0.1:6379> smembers myset   1) "world"2) "hello2"3) "hello"127.0.0.1:6379> srem myset hello   # 移除元素(integer) 1127.0.0.1:6379> smembers myset1) "world"2) "hello2"127.0.0.1:6379> 127.0.0.1:6379> smembers myset1) "kkk"2) "world"3) "hjk"4) "hello2"127.0.0.1:6379> srandmember myset   # 隨機抽取一個元素"hjk"127.0.0.1:6379> srandmember myset"hello2"127.0.0.1:6379> srandmember myset 2   # 隨機抽取指定個數的元素1) "world"2) "hello2"127.0.0.1:6379> srandmember myset 21) "hello2"2) "hjk"127.0.0.1:6379> 127.0.0.1:6379> smembers myset1) "kkk"2) "world"3) "hjk"4) "hello2"127.0.0.1:6379> spop myset  # 隨機刪除元素"hjk"127.0.0.1:6379> smembers myset1) "kkk"2) "world"3) "hello2"127.0.0.1:6379> spop myset"hello2"127.0.0.1:6379> smembers myset1) "kkk"2) "world"127.0.0.1:6379> 127.0.0.1:6379> smembers myset1) "kkk"2) "world"127.0.0.1:6379> sadd myset2 set2(integer) 1127.0.0.1:6379> smove myset myset2 "kkk"   # 將一個特定的值,移動到另一個set集合中(integer) 1127.0.0.1:6379> smembers myset1) "world"127.0.0.1:6379> smembers myset21) "kkk"2) "set2"127.0.0.1:6379> 127.0.0.1:6379> smembers key11) "b"2) "a"3) "c"127.0.0.1:6379> smembers key21) "e"2) "d"3) "c"127.0.0.1:6379> sdiff key1 key2   # 差集1) "b"2) "a"127.0.0.1:6379> sinter key1 key2         # 交集1) "c"127.0.0.1:6379> sunion key1 key2  # 並集1) "e"2) "a"3) "c"4) "d"5) "b" 4、Hash(哈希) 也是 key - value 形式的,但是value 是一個map。 127.0.0.1:6379> hset myhash field xxx  # set 一個 key-value(integer) 1127.0.0.1:6379> hget myhash field   # 獲取一個字段值"xxx"127.0.0.1:6379> hmset myhash field1 hello field2 world  # set 多個 key-valueOK127.0.0.1:6379> hmget myhash field field1 field2   # 獲取多個字段值1) "xxx"2) "hello"3) "world"127.0.0.1:6379> hgetall myhash    # 獲取全部的數據1) "field"2) "xxx"3) "field1"4) "hello"5) "field2"6) "world" 127.0.0.1:6379> hdel myhash field1  # 刪除指定的key,對應的value也就沒有了(integer) 1127.0.0.1:6379> hgetall myhash1) "field"2) "xxx"3) "field2"4) "world"127.0.0.1:6379> 127.0.0.1:6379> hlen myhash  # 獲取長度(integer) 2127.0.0.1:6379> hexists myhash field1   # 判斷指定key是否存在(integer) 0127.0.0.1:6379> hexists myhash field2(integer) 1127.0.0.1:6379> hkeys myhash  # 獲取所有的key1) "field"2) "field2"127.0.0.1:6379> hvals myhash  # 獲取所有的value1) "xxx"2) "world"127.0.0.1:6379> 127.0.0.1:6379> hset myhash field3 5(integer) 1127.0.0.1:6379> hincrby myhash field3 1  # 指定增量(integer) 6127.0.0.1:6379> hincrby myhash field3 -1(integer) 5127.0.0.1:6379> hsetnx myhash field4 hello  # 如果不存在則可以設置(integer) 1127.0.0.1:6379> hsetnx myhash field4 world  # 如果存在則不能設置(integer) 0127.0.0.1:6379> Hash 適合存儲經常變動的對象信息,String 更適合於存儲字符串。關注公眾號Java技術棧,回覆 Redis,可以獲取我整理的 Redis 系列教程。 5、zset (有序集合) 127.0.0.1:6379> zadd myset 1 one  # 添加一個值(integer) 1127.0.0.1:6379> zadd myset 2 two 3 three # 添加多個值(integer) 2127.0.0.1:6379> zrange myset 0 -11) "one"2) "two"3) "three"127.0.0.1:6379> 實現排序: 127.0.0.1:6379> zadd salary 2500 xiaohong(integer) 1127.0.0.1:6379> zadd salary 5000 xiaoming(integer) 1127.0.0.1:6379> zadd salary 500 xaiozhang(integer) 1127.0.0.1:6379> zrange salary 0 -11) "xaiozhang"2) "xiaohong"3) "xiaoming"127.0.0.1:6379> zrangebyscore salary -inf +inf  # 從小到大顯示全部的用户1) "xaiozhang"2) "xiaohong"3) "xiaoming"127.0.0.1:6379> zrevrange salary 0 -1  # 從大到小進行排序1) "xiaoming"2) "xiaohong"3) "xaiozhang"127.0.0.1:6379> zrangebyscore salary -inf +inf withscores   # 附帶成績的顯示所有用户1) "xaiozhang"2) "500"3) "xiaohong"4) "2500"5) "xiaoming"6) "5000"127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores   # 顯示工資小於 2500 的用户1) "xaiozhang"2) "500"3) "xiaohong"4) "2500" 127.0.0.1:6379> zrange salary 0 -11) "xaiozhang"2) "xiaohong"3) "xiaoming"127.0.0.1:6379> zrem salary xiaohong  # 移除特定元素(integer) 1127.0.0.1:6379> zrange salary 0 -11) "xaiozhang"2) "xiaoming"127.0.0.1:6379> zcard salary  # 獲取有序集合的個數(integer) 2127.0.0.1:6379> 127.0.0.1:6379> zadd myset 1 hello(integer) 1127.0.0.1:6379> zadd myset 2 world 3 !(integer) 2127.0.0.1:6379> zcount myset 1 3  # 獲取指定區間的人員數量(integer) 3127.0.0.1:6379> zcount myset 1 2(integer) 2 6、geospatial Redis 在 3.2 推出 Geo 類型,該功能可以推算出地理位置信息,兩地之間的距離。 文檔://www.redis.net.cn/order/3687.html 藉助網站模擬一些數據://www.jsons.cn/lngcode/ geoadd 添加地理位置 規則:兩極無法直接添加,一般會下載城市數據,直接通過 Java 程序一次性導入。 有效的經度從 -180 度到 180 度。有效的緯度從 -85.05112878 度到 85.05112878 度。當座標位置超出指定範圍時,該命令將會返回一個錯誤。 (error) ERR invalid longitude latitude pair xxx yyy 添加一些模擬數據: 127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing(integer) 1127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai(integer) 1127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shengzhen(integer) 2127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian(integer) 2127.0.0.1:6379> geopos 獲得當前定位座標值 127.0.0.1:6379> geopos china:city beijing  # 獲得指定城市的經緯度1) 1) "116.39999896287918091"   2) "39.90000009167092543"127.0.0.1:6379> geopos china:city shanghai1) 1) "121.47000163793563843"   2) "31.22999903975783553"127.0.0.1:6379> geodist 獲取兩個位置之間的距離 單位: m 表示單位為米。 km 表示單位為千米。 mi 表示單位為英里。 ft 表示單位為英尺。 如果用户沒有顯式地指定單位參數, 那麼 GEODIST 默認使用米作為單位。 127.0.0.1:6379> geodist china:city beijing shanghai km # 查看北京和上海直接的直線距離"1067.3788"127.0.0.1:6379> geodist china:city beijing chongqing km"1464.0708"127.0.0.1:6379> georedius 以給定的經緯度為中心,找出某一半徑內的元素 127.0.0.1:6379> georadius china:city 110 30 1000 km # 以110, 30 這個點為中心,尋找方圓 1000km 的城市1) "chongqing"2) "xian"3) "shengzhen"4) "hangzhou"127.0.0.1:6379> georadius china:city 110 30 500 km 1) "chongqing"2) "xian"127.0.0.1:6379> georadius china:city 110 30 500 km withcoord #  顯示他人的定位信息1) 1) "chongqing"   2) 1) "106.49999767541885376"      2) "29.52999957900659211"2) 1) "xian"   2) 1) "108.96000176668167114"      2) "34.25999964418929977"127.0.0.1:6379> 127.0.0.1:6379> georadius china:city 110 30 500 km withdist #  顯示到中心點的距離1) 1) "chongqing"   2) "341.9374"2) 1) "xian"   2) "483.8340"127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 1  # 指定數量1) 1) "chongqing"   2) "341.9374"   3) 1) "106.49999767541885376"      2) "29.52999957900659211"127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 21) 1) "chongqing"   2) "341.9374"   3) 1) "106.49999767541885376"      2) "29.52999957900659211"2) 1) "xian"   2) "483.8340"   3) 1) "108.96000176668167114"      2) "34.25999964418929977"127.0.0.1:6379> GEORADIUSBYMEMBER 找出位於指定元素周圍的其他元素 127.0.0.1:6379> georadiusbymember china:city shanghai 1000 km1) "hangzhou"2) "shanghai"127.0.0.1:6379> geo 底層實現原理其實就是 zset ,可以使用 zset 命令操作 geo 127.0.0.1:6379> zrange china:city 0 -11) "chongqing"2) "xian"3) "shengzhen"4) "hangzhou"5) "shanghai"6) "beijing"127.0.0.1:6379> zrem china:city beijing  # 刪除一個元素(integer) 1127.0.0.1:6379> zrange china:city 0 -11) "chongqing"2) "xian"3) "shengzhen"4) "hangzhou"5) "shanghai"127.0.0.1:6379> 7、hyperloglog 基數:數學上集合的元素個數,是不能重複的。 UV(Unique visitor):是指通過互聯網訪問、瀏覽這個網頁的自然人。訪問的一個電腦客户端為一個訪客,一天內同一個訪客僅被計算一次。 Redis 2.8.9 版本更新了 hyperloglog 數據結構,是基於基數統計的算法。 hyperloglog 的優點是佔用內存小,並且是固定的。存儲 2^64 個不同元素的基數,只需要 12 KB 的空間。但是也可能有 0.81% 的錯誤率。 這個數據結構常用於統計網站的 UV。傳統的方式是使用 set 保存用户的ID,然後統計 set 中元素的數量作為判斷標準。 但是這種方式保存了大量的用户 ID,ID 一般比較長,佔空間,還很麻煩。我們的目的是計數,不是保存數據,所以這樣做有弊端。但是如果使用 hyperloglog 就比較合適了。 127.0.0.1:6379> pfadd mykey a b c d e f g h i j # 創建第一組元素(integer) 1127.0.0.1:6379> PFCOUNT mykey     # 統計 mykey 基數(integer) 10127.0.0.1:6379> PFADD mykey2 i j z x c v b n m  # 創建第二組元素(integer) 1127.0.0.1:6379> PFCOUNT mykey2     # 統計 mykey2 基數(integer) 9127.0.0.1:6379> PFMERGE mykey3 mykey mykey2  # 合併兩組 mykey mykey2 => mykey3OK127.0.0.1:6379> PFCOUNT mykey3(integer) 15127.0.0.1:6379> 8、bitmap 位圖 bitmap就是通過最小的單位bit來進行0或者1的設置,表示某個元素對應的值或者狀態。一個bit的值,或者是0,或者是1;也就是説一個bit能存儲的最多信息是2。 bitmap 常用於統計用户信息比如活躍粉絲和不活躍粉絲、登錄和未登錄、是否打卡等。 這裏使用一週打卡的案例説明其用法: 127.0.0.1:6379> setbit sign 0 1  # 週一打卡了(integer) 0127.0.0.1:6379> setbit sign 1 0  # 週二未打卡(integer) 0127.0.0.1:6379> setbit sign 2 0  # 週三未打卡(integer) 0127.0.0.1:6379> setbit sign 3 1(integer) 0127.0.0.1:6379> setbit sign 4 1(integer) 0127.0.0.1:6379> setbit sign 5 1(integer) 0127.0.0.1:6379> setbit sign 6 0(integer) 0127.0.0.1:6379> 查看某一天是否打卡: 127.0.0.1:6379> GETBIT sign 3(integer) 1127.0.0.1:6379> GETBIT sign 6(integer) 0127.0.0.1:6379> 統計:統計打卡的天數 127.0.0.1:6379> BITCOUNT sign(integer) 4127.0.0.1:6379> 特別推薦一個分享架構+算法的優質內容,還沒關注的小夥伴,可以長按關注一下: 長按訂閲更多精彩▼如有收穫,點個在看,誠摯感謝 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-25 關鍵詞: 嵌入式 C語言

  • C語言字符串詳解

    來源://www.cnblogs.com/tongye/p/10688941.html 字符串是一種非常重要的數據類型,但是C語言不存在顯式的字符串類型,C語言中的字符串都以字符串常量的形式出現或存儲在字符數組中。同時,C 語言提供了一系列庫函數來對操作字符串,這些庫函數都包含在頭文件 string.h 中。 一、字符串常量和字符數組 1.1、什麼是字符串常量 C 語言雖然沒有字符串類型,但是 C語言提是存在字符串這個概念的,也就是字符串常量:以 NUL 字節結尾的 0 個或多個字符組成的序列。字符串常量是不可被修改的,一般用一對雙引號(" ")括起的一串字符來表示字符串常量,如: "Hello!"、"\aWarning!\a"、"123abc\n"、"" 字符串常量可以為空,如""就是一個空的字符串常量,但是即使為空,還是存在一個終止符 NUL 的。(在 C 語言中,常用轉義字符 \0 來表示 NUL)   1.2、字符串常量與指針 字符串常量與指針關係密切,因為字符串常量的值,實際上表示的是存儲這些字符的內存空間的地址,更準確地説是字符串常量中第 1 個字符的地址,而不是這些字符本身。因此,在 C 語言中是不能直接進行字符串賦值的(因為沒有字符串類型嘛)。在 C 語言中,常通過聲明一個指向 char 類型的指針並將其初始化為一個字符串常量的方式來訪問一個字符串: char *message = "Hello World!";// 上面的語句也可以拆分成下面兩句char *message;message = "Hello World!";    // 這句話看起來像是字符串複製,其實不是,只是涉及到指針操作 上述語句聲明瞭一個指向 char 類型的指針,並用字符串常量中第 1 個字符的地址對該指針進行初始化。可以通過字符指針 message 來訪問字符串常量: #include int main(){  char *message = "Hello World!";  printf("%s\n",message);  while(*message != '\0'){    printf("%c ",*message++);  }  printf("\n");  return 0;}/* output: * Hello World! * H e l l o   W o r l d ! */ 這段代碼,使用字符指針遍歷了字符串常量中的每一個字符。   1.3、字符數組 用於存放字符的數組稱為字符數組。在 C 語言中,除了字符串常量外,其他所有字符串都必須存儲於字符數組或動態分配的內存中。定義一個字符數組和定義一個普通數組一樣,不同的是字符數組中存放的是字符數據而已: char charArray[] = {'H','e','l','l','o'};    // 聲明並初始化一個字符數組   這句話定義並初始化了一個字符數組 charArray。這個數組的長度實際上為 6 ,因為會自動添加一個字符串結束符 '\0'。 C 語言提供了一種更簡潔的方法來對字符數組進行初始化: char charArray[] = "Hello World!";    // 聲明並初始化一個字符數組 上述兩種聲明方式等價。  可以對一個字符數組做出修改: #include #include int main(){        char str[] = "hello world!";        int len = strlen(str);        int i;        for(i = 0; i  len,則只有 len 個字符被複制到 dst 中去,此時 dst 將不會以 NUL 字節結尾(也就是説,strncpy 調用的結果可能不是一個字符串); 2)如果 strlen(src) < len,則 src 中的字符全被複制到 dst 中去,dst 中剩餘的部分用 NUL 填充。 四、連接字符串 C 語言中使用庫函數 strcat 來連接兩個字符串: char *strcat(char *dst,char const *src); 函數 strcat 將參數 src 字符串連接到參數 dst 字符串的後面。與 strcpy 函數一個同樣的問題是,必須保證 dst 的剩餘空間足夠存放下 src 整個字符串。C 語言中提供了 strncat 函數來解決這個問題:  char *strncat(char *dst , char const *src , size_t len);   strncat 函數從 src 中最多複製 len 個字符到目標數組 dst 後面,並且,strncat 總是在結果字符串後面添加一個 NUL 字節,而且不會像 strncpy 函數那樣對 dst 剩餘的空間用 NUL 進行填充。 五、字符串比較 C 語言中使用庫函數 strcmp 來進行字符串比較。strcmp 函數會對被比較的兩個字符串進行逐字符地比較,直到發現不匹配為止:最先不匹配的字符中較小的那個字符所對應的字符串即被認為小於另一個字符串;如果兩者所有字符都匹配,則認為這兩個字符串相等; int strcmp(char const *s1 , char sonst *s2); 該函數的返回值如下: 1)s1 小於 s2,返回一個負值; 2)s1 等於 s2,返回 0; 3)s1 大於 s2,返回一個正值。 char *strncmp(char const *s1 , char const *s2 , size_t len); 可以使用 strncmp 函數限定比較的字符的個數,返回值與 strcmp 一樣,但是隻針對前 len 個字符進行比較。 六、字符串的查找 6.1 查找一個字符 可以使用 strchr 函數或 strrchr 函數來在一個字符串中查找一個特定的字符: char *strchr(char const *str,int ch);    // int ch 是字符的ASCII碼值char *strrchr(char const *str,int ch); 函數 strchr 在字符串 str 中查找字符 ch 第一次出現的位置,並返回一個指向該位置的指針;如果沒有找到相應的字符,則返回一個 NULL 指針。函數 strrchr 在字符串中查找字符 ch 最後一次出現的位置,並返回指向該位置的指針。   6.2 查找任意幾個字符 可以使用 strpbrk 函數來查找任何一組字符第一次在字符串中出現的位置: char *strpbrk(char const *str , char const *group); 這個函數返回一個指向字符串 str 中第一個匹配 group 中任何一個字符的字符位置,如果沒有匹配到,則返回一個 NULL 指針。    6.3 查找一個子串 可以使用 strstr 函數來在一個字符串中查找一個子串: char *strstr(char const *str1 , char const *str2); 這個函數在 str1 中查找整個字符串 str2 第一次出現的起始位置,並返回一個指向該位置的指針;如果 str2 並沒有完整的出現在 str1 中,則函數將返回一個 NULL 指針;如果 str2 是一個空字符串,則返回str1. 參考資料 《C和指針》 《C程序設計語言 第二版》 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-25 關鍵詞: 嵌入式 C語言

  • 小宇宙爆發!Spring Boot新特性:節省95%內存佔用

    作者 | 冷冷 來源 | //mp.weixin.qq.com/s/0m6ofmMlQTDUQwC7oRRIrQ GraalVM[1] 是一種高性能的虛擬機,它可以顯著的提高程序的性能和運行效率,非常適合微服務。最近比較火的 Java 框架 Quarkus[2] 默認支持 GraalVM 下圖為 Quarkus 和傳統框架(SpringBoot) 等對比圖,更快的啓動數據、更小的內存消耗、更短的服務響應。 Spring Boot 2.4 開始逐步提供對 GraalVM 的支持,旨在提升上文所述的 啓動、內存、響應的使用體驗。 安裝 GraalVM 目前官方社區版本最新為 20.3.0 ,是基於 OpenJDK 8u272 and 11.0.9 定製的,可以理解為 OpenJDK 的衍生版本 。 官方推薦的是  SDKMAN[3] 用於快速安裝和切換不同版本 JDK 的工具 ,類似於 nodejs 的  nvm[4]。 使用類似命令即可完成指定版本安裝和指定默認版本 sdk install java 11.0.9.hs-adptsdk default java 11.0.9.hs-adpt 不過安裝過程中需要從國外下載相關資源 ,筆者在嘗試後使用體驗並不是很好,所有建議大家下載指定版本 GraalVM 安裝即可(和 JDK 安裝方式一樣)。 安裝成功查看版本 ⋊> ~ java -version                                                      11:30:34openjdk version "11.0.9" 2020-10-20OpenJDK Runtime Environment GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06)OpenJDK 64-Bit Server VM GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06, mixed mode, sharing) 安裝 native-image native-image 是由 Oracle Labs 開發的一種 AOT 編譯器,應用所需的 class 依賴項及 runtime 庫打包編譯生成一個單獨可執行文件。具有高效的 startup 及較小的運行時內存開銷的優勢。 但 GraalVM 並未內置只是提供 gu 安裝工具,需要我們單獨安裝。 - 切換到 jdk 的安裝目錄⋊> ~ cd $JAVA_HOME/bin/- 使用gu命令安裝⋊>  ./gu install native-image 初始化 Spring Boot 2.4 項目 Spring Initializr 創建 demo 項目 curl //start.spring.io/starter.zip -d dependencies=web \           -d bootVersion=2.4.1 -o graal-demo.zip 先看一下啓動基準數據 , 單純運行空項目 需要 1135 ms 秒 java -jar demo-0.0.1-SNAPSHOT.jarengine: [Apache Tomcat/9.0.41]2020-12-18 11:48:36.856  INFO 91457 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext2020-12-18 11:48:36.856  INFO 91457 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1135 ms 內存佔用情況 ps aux | grep demo-0.0.1-SNAPSHOT.jar | grep -v grep | awk '{print $11 "\t" $6/1024"MB" }'/usr/bin/java 480.965MB 支持 GraalVM 增加相關依賴, 涉及插件較多完整已上傳  Gitee Gist[5]     org.springframework.experimental    spring-graalvm-native    0.8.3    org.springframework    spring-context-indexer        spring-milestones      Spring Milestones      //repo.spring.io/milestone   Main 方法修改,proxyBeanMethods = false @SpringBootApplication(proxyBeanMethods = false) 使用 native-image 構建可執行文件  mvn -Pnative package#構建過程比較慢,日誌如下spring.factories files...[com.example.demo.demoapplication:93430]    classlist:   4,633.58 ms,  1.18 GB   _____                     _                             _   __           __     _  / ___/    ____    _____   (_)   ____    ____ _          / | / /  ____ _  / /_   (_) _   __  ___  \__ \    / __ \  / ___/  / /   / __ \  / __ `/         /  |/ /  / __ `/ / __/  / / | | / / / _ \ ___/ /   / /_/ / / /     / /   / / / / / /_/ /         / /|  /  / /_/ / / /_   / /  | |/ / /  __//____/   / .___/ /_/     /_/   /_/ /_/  \__, /         /_/ |_/   \__,_/  \__/  /_/   |___/  \___/        /_/                            /____/...[com.example.demo.demoapplication:93430]      [total]: 202,974.38 ms,  4.23 GB 編譯結果 在 targe 目錄生成 名稱為 com.example.demo.demoapplication 可執行文件 啓動應用  這裏執行的編譯後的可執行文件而不是 jar cd target./com.example.demo.demoapplication 啓動時間 0.215 seconds 2020-12-18 12:30:40.625  INFO 94578 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.215 seconds (JVM running for 0.267) 看一下內存佔用 24.8203MB ps aux | grep com.example.demo.demoapplication | grep -v grep | awk '{print $11 "\t" $6/1024"MB" }'./com.example.demo.demoapplication 24.8203MB 數據對比 是否引入 GraalVM 內存佔用 啓動時間 否 480.965MB 1135 ms 是 24.8203MB 215 ms 參考資料 [1]GraalVM: //www.graalvm.org [2]Quarkus: //quarkus.io [3]SDKMAN: //sdkman.io/install [4]nvm: //github.com/creationix/nvm [5]Gitee Gist: //gitee.com/gi2/codes/famcqz6n21iylpg3us7j036 喜歡本文的朋友,歡迎關注公眾號 程序員小灰,收看更多精彩內容 點個[在看],是對小灰最大的支持! 免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

    時間:2020-12-25 關鍵詞: 嵌入式 C語言

首頁  上一頁  1 2 3 4 5 6 7 8 9 10 下一頁 尾頁
發佈文章

技術子站

更多

項目外包