1. 程式人生 > >4.8一些雜散但是值得討論的問題

4.8一些雜散但是值得討論的問題

的人 用法 編譯運行 cas [1] 分配 實現 命令 結構

4.8.1操作系統究竟是什麽玩意?

4.8.1.1像人類社會一樣的計算機軟件系統(有些人只埋頭苦幹,有些人只做管理)

  • (1)人類社會最開始時人人都幹活,這時候沒有專業分工,所有人都直接做產生價值的工作。當時時合適的,因為當時生產力低下,人口稀少。就像裸機程序一樣(邏輯程序的特點:代碼量小,功能簡單、所有代碼都和直接目的有關,沒有服務性代碼)
  • (2)後來人口增加生產力提高,有一部分人脫離了直接產生價值的體力勞動專職指揮(誕生了階級)。本質上來說是合理的,因為資源得到了優化配置,提升了整體的效率。當計算機技術發展,計算機性能和資源大量增加,譬如CPU的性能越來月增加,內存越來越大。這時候寫代碼也要產生階級,進行分工。不然如果所有代碼都具有參加直接性的工作,則整體效率不高。(因為代碼很難進行資源的優化配置)
  • (3)解決方案就是操作系統。操作系統就是分出來管理階級,操作系統的代碼本身並不直接產生價值,它的主要任務是管理所有資源,它主要為直接產生價值的,直接勞動的那些程序(各種應用程序)提供服務。操作系統既是管理者也是服務者。
  • (4)邏輯程序就好像小公司,操作系統下的程序就好像大型跨國公司;邏輯程序就像小國家,操作系統下程序就好像大國家;如果我們要做一個產品,那麽軟件系統是裸機還是基於操作系統呢?本質上是取決於產品本身的復雜度。只有極簡單的功能,使用極簡單的CPU(譬如單片機)的產品才會選擇用裸機開發;一般的復雜性產品都會選擇基於操作系統來開發

4.8.1.2、操作系統的調用通道:API函數

(1)操作系統負責管理和資源調配,應用程序負責具體的直接勞動,他們之間的接口就是API函數,當應用程序需要自己使用系統資源時(譬如內存、CPU、硬件)就通過API向操作系統發出申請,然後操作系統就相應申請幫助應用程序實現功能。

4.8.1.3、C庫函數和API的關系

  • (1)單純的API只是提供了極簡單的沒有任何封裝的服務函數,這些函數是可用的,但是不太好用。應用程序為了好用,就對這個API進行了二次封裝,把他變得好用一些,於是就成了C庫函數
  • (2)有時候完成一個功能,有相應的庫函數可以完成,也可以有相應的API完成,用那個都行。譬如讀寫文件,API接口是open write read close;庫函數的接口是fopen fwrite fread fclose 。fopen本質上是open實現的,只是進行了封裝。封裝肯定有目的(添加緩沖機制)。

4.8.1.4、不同平臺(windows、linux、裸機)下庫函數的差異

  • (1)不同的操作系統API是不同的,但是都能完成所有任務,只是完成一個任務所調用的API是不同的。
  • (2)庫函數在不同的操作系統下也不同,但是相似性要更高一些。這是因為,人下意識想要屏蔽不同操作系統的差異,因此在封裝出來的庫函數挺像的,但是還是存在差異,所以我們在一個操作系統上寫的應用程序不可能直接在另一個操作系統下編譯運行。於是乎就有個可移植性出來。
  • (3)跨操作系統可移植平臺(譬如QT、譬如JAVA)

4.8.1.5操作系統的重大意義:軟件體系分工

(1)有了操作系統後,我們做鏟平可以分為兩個部分,一部分人負責做操作系統(開發驅動);一部分負責操作系統實現具體功能(開發應用)。實際上上層應用層的功能進一步復雜化後又分了很多層。

4.8.2、main函數的返回值返回給了誰?

4.8.2.1、函數為什麽需要有返回值

  • (1)函數設計的時候設計了參數和返回值,參數是函數輸入,返回值是函數輸出
  • (2)因為函數需要對外輸出數據(實際上是函數運行的一些結果)因此需要返回值
  • (3)形式上來說,函數被另一個函數所調用,返回值就作為函數的值返回給調用這個函數的地方

總結:函數的返回值就是給調用他的人返回一個值

4.8.2.2、main函數被誰調用

  • (1)main函數是特殊的,首先這個名字是特殊的。因為C語言規定了整個程序的入口。其他的函數只有直接或者間接被main函數調用才能被執行,如果沒有被main函數直接/間接調用則這個函數在整個程序中無用。
  • (2)main函數從某種角度來講代表了當期那這個程序,或者說代表了整個程序。main函數的開始一意味著整個程序開始執行,main 函數的結束意味著整個程序的結束
  • (3)誰執行了這個程序,誰就調用了main
  • (4)誰執行了程序?或者說程序有哪幾種被調用執行的方法?

4.8.2.3、linux下一個新程序執行的本質

  • (1)表面上來看,linux中在命令行中./xx執行一個可執行程序。
  • (2)我們還可以通過shell腳本來調用執行一個程序
  • (3)我們還可以在程序中調用執行一個程序(fork exec)

總結:我麽有多種方法執行一個程序,但是本質上是相同的。linux中一個新程序的執行本質上是一個進程的創建、加載、運行、消亡。linux中執行一個程序其實就是創建一個新進程,然後把這個程序丟進去執行知道結束。新進程被誰開啟。linux中進程都是被它的父進程fork出來的。

分析:命令行本身就是一個進程,在命令行低下./xx執行一個進程,其實就是新進程作為命令行進程的一個子進程去執行的。

總之一句話:一個程序被它的父進程所調用

結論:main函數返回給調用這個函數的父進程。父進程要這個返回值幹什麽?父進程調用子進程來完成一個任務,然後子進程執行完後通過main函數的返回值來給父進程一個答復。這個答復一般表示子進程的任務執行結果時成功了還是錯誤了。(0表示執行成功了,負數表示執行失敗了)

4.8.2.4實踐驗證獲取main的返回值

  • (1)用shell腳本執行程序,可以獲取程序的返回值,並且打印出來
  • (2)linux的shell中用 $? 這個符號來存儲和表示上一個程序執行的結果

4.8.2.5、啟示

  • (1)任何人任何稅務都是有媽生的,不會無緣無故的出現或者消亡
  • (2)看起來沒用,改掉或者去掉沒錯的,也不見得真的就沒錯,要大膽總結更要小心求證。

4.8.3、argc、argv與main函數的傳參

4.8.3.1、誰給main函數傳參

  • (1)調用main函數所在的程序的它的父進程給main函數傳參,並且接收它的返回值。

4.8.3.2、為什麽需要給main函數傳參

  • (1)首先,main函數不傳參是可以的,也就是說父進程調用子進程並且給子進程傳參不是必須的。int mian(void)這種形式就表示我們認為不必要給 main函數傳參
  • (2)有時候我們希望程序有一種靈活性,所以選擇在執行程序時通過出參來控制程序中的運行,達到不需要重新編譯程序就可以改變程序運行結果的效果。

4.8.3.3、表面上:給main函數進行傳參是怎樣實現的?

  • (1)給main函數傳參通過argc和argv這兩個c原因預定的參數來實現
  • (2)argc是int類型,表示我們運行程序的時候,我們給main函數傳了幾個參數。argv是一個字符數組,用來存儲多個字符串,每個字符串就是我們給main函數傳的一個參數。argv[0]就是我們給main函數的第一個傳參,argv[1]就是我們給main函數傳的第二個參數。

4.8.3.4、本質上:給main函數傳參是怎樣實現的?

  • (1)上節課講過,程序調用有各種方法,但是本質上都是父進程fork一個子進程,然後子進程可一個程序綁定以來一起去運行(exec函數族),我們子啊exec的時候可以給他同時傳參。
  • (2)程序調用是可以被傳參(也就是main的傳參)是操作系統層面的支持完成的

4.8.3.5、給main傳參要註意什麽?

  • (1)main函數傳參都是通過字符串傳進去的。
  • (2)程序被調用時傳參,各個參數之間通過空格來間隔的
  • (3)程序在使用argv之前要檢驗argc

4.8.4、void類型的本質

4.8.4.1、C語言屬於強類型語言

  • (1)編程語言分為強類型語言和弱類型語言。強類型語言所有的變量都有自己固定的類型,這個類型有固定的內存占用,有固定的解析方法,弱類型語言沒有類型這個概念(一般都是字符串的),程序在用的時候在根據自己的需要來處理變量。
  • (2)C語言是一種典型的強類型語言,C語言中所有的變量都必須要有類型,因為C語言中每一各變量都要對應內存中的一段內存,編譯器需要這個變量類型來確定這個變量占用的字節數和這一段內存的解析方法。

4.8.4.2、數據類型的本質含義

  • (1)數據類型的本質就是決定變量的內存占用數,和內存解析方法。
  • (2)所以得出結論:C語言中變量必須有確定的數據類型,如果一個額變量你沒有確定的類型,就是所謂的無類型。會導致編譯器無法給這個變量分配內存,也無法解析這個變量對應的內存。因此得出結論,不可能有沒有類型的變量。
  • (3)但是C語言中可以有沒有類型的內存。在內存還沒有和具體變量相綁定之前內存就可以沒有類型。實際上純粹的內存就是沒有類型的。內存只是因為和具體的變量相關聯後才有了確定的類型(其實內存自己本身是不知道的,而編譯器知道,我們程序在使用這個類型時會按照類型的含義去進行內存的讀和寫)。

4.8.4.3、voi類型的本質含義

  • (1)void類型的正確含義是:不知道類型,不確定類型,還沒確定類型
  • (2)void a; 定義了void類型的變量,含義就是說a就是一個變量,而且a肯定有確定的類型,知識目前我們還不知道a的類型,還不確定,所以標記為void

4.8.4.4、為什麽需要void類型

  • (1)什麽情況下需要void類型?其實就是在描述一段沒有具體使用的內存時需要使用void類型。
  • (2)void的一個典型應用案例就是malloc的返回值。我們知道malloc函數向系統堆管理器申請一段內存給當前程序使用,malloc返回的是一個指針,這個指針指向申請的那段內存。malloc剛申請的這段內存尚未用來存儲數據。我們也無法預知將來這段內存會用來存什麽類型的數據,所以不能返回一個具體類型的指針。所以解決方案就是返回一個void *類型,告訴編譯器我們返回的是一段幹凈的內存空間,尚未確定類型,所以當我們在malloc之後可以給這段內存讀寫任意類型的數據。
  • (3)void * 類型的指針指向的內存是尚未確定類型的,因此我們後續可以使用強制類型轉換成各種類類型,這就是void 類型的歸宿,就是強制類型轉換成一個具體類型。

4.8.5、C語言中的NULL

4.8.5.1、NULL在C/C++中的標準定義

  • (1)NULL不是C語言的關鍵字,本質上是一個宏定義
  • (2)NULL的標準定義:

#ifdef _cplusplus //條件編譯

#define NULL 0

#else

#define NULL (void *)0    //這裏對應的是C語言的情形

#endif

解釋:C++的編譯環境中,編譯器預先定義了一個宏_cplusplus,程序中可以用條件編譯來判斷當前的編譯環境是c++還是C的。

NULL的本質解析:NULL的本質是0,但是這個0不是當一個數字解析,而是當一個內存地址來解析的,這個0其實是0x00000000,代表的是內存的0地址。

(void *)0這個整體表達式表示一個指針,這個指針變量本身占4個字節,地址在哪裏取決於指針變量本身,但是這個指針變量的值是0,也就是說這個指針變量指向0地址(實際上是0地址開始的一段內存)。

4.8.5.2、從指針角度理解NULL的本質

  • (1)int *p;    //p是一個函數內的局部變量,則p的值是隨機的,也就是說p是一個野指針
  • (2)int* p = NULL; //p是一個局部變量;分配在棧上的地址是由編譯器決定的,我們並不關心,但是p的值是(void*)0實際就是0,意思是指針p指向內存的0地址處,這時候p就不是野指針了。
  • (3)為什麽要讓一個野指針指向內存地址0處,主要是因為在大部分的CPU當中,內存的0地址處都不是可以隨便訪問的(一般都是操作系統嚴密管控的,所以應用程序不能隨便訪問)。所以野指針指向了這個區域可以保證野指針不會造成誤傷。如果程序員無意識的解引用指向0地址的指針則會觸發段錯誤,這樣可以提示到你幫助你找到程序中的錯誤。

4.6.5.3、為什麽需要NULL

  • (1)第一個作用就是讓野指針指向0地址比較安全
  • (2)第二個作用就是一個特殊標記,按照標準的使用步驟

int *p =NULL;

p = xx;

if(NULL != p)

{

  *p = xxxx;    //再確認p不等於NULL的情況下再去解引用

}

P = NULL;    //用完後p再次等於NULL

註意:一般我們比較一個指針和NULL是否相等不寫成if(p != NULL),而寫成if(NULL != p)   原因是第一種寫法如果把==寫成了=編譯器不會報錯,但是意思已經完全改變。而第二種會報錯,提示你找到錯誤並改正。

4.8.5.4、註意不要混用NULL與‘\0’

  • (1)‘\0’ 和 ‘0‘ 和 0 和 NULL幾個區分開
  • (2)‘\0’是一個轉義字符,他對應的ASCII編碼值是0,本質就是0
  • (3)‘0’是一個字符,他對應的ASCII編碼值是48,本質是48
  • (4)0是一個數字,他本質就是0
  • (5)NULL是一個表達式,是強制類型轉換為void *類型的0,本質是0

總結:‘\0’用法是C語言字符串結尾標誌,一般用來比較字符串中的字符以判斷字符串有沒有到頭

‘0’是字符0,對應0這個字符的ASCII編碼;一般用來獲取0 的ASCII的碼值

0是數字,一般用來比較一個int類型的數字是否等於0

NULL是一個表達式,一般用來比較一個指針是否是一個野指針

4.8.6、運算中的臨時匿名變量

4.8.6.1、C語言和匯編的區別(匯編完全對應機器操作,C對應邏輯操作)

  • (1)C語言叫做高級語言,匯編語言叫低級語言。
  • (2)低級語言的意思是匯編語言和機器操作相對應,匯編語言只是CPU的機器碼的助記符,用匯編語言寫程序必須擁有機器的思維,因為冉的CPU設計時指令的差異比較大,所以用匯編編程差異很大。
  • (3)高級語言(c語言)它對低級語言進行了封裝(C語言的編譯來完成的),給程序員提供了一個靠近人類思維的一些語法特征,這樣人類可以不用過於考慮機器的原理。而可以按照自己的邏輯原理來編程。譬如說數組、結構體、指針............
  • (4)更高級的語言如java、C#等知識進一步強化了C語言提供的人性化的操作界面語法,在易用性上、安全性上進行了提升。

4.8.6.2、C語言的一些“小動作”

  • (1)高級語言中有一些元素是機器中沒有的
  • (2)高級語言在運算中允許我們大跨度的運算。意思就是低級語言中需要好幾部才能完成的運算,在高級語言中只要一步即可完成。譬如在C語言中有個i++,在C語言中只需要i++即可,看起來只有一句代碼。但實際上翻譯到匯編需要3步才能完成;第1步從內存中讀取i到寄存器,第二步對寄存器中的i進行加1,第三步,將加1後的i寫入內存i中;

4.8.6.3、使用臨時變量來理解強制類型轉換

4.8.6.4、使用臨時變量來理解不同數據類型之間的運算

4.8.7、順序結構

4.8.7.1、最前線的順序結構:三種結構之一

  • (1)代碼執行的時候如果沒有遇到判斷跳轉或者循環,默認是順序執行的。執行完再執行下一句
  • (2)順序結構說明CPU的工作狀態,就是以時間軸來順序執行所有的代碼語句直到停機。

4.8.7.2、選擇和循環結構內部的順序結構

  • (1)譬如if(){ }內部是if的代碼段,在代碼段的內部還是按照順序結構來執行的。
  • (2)switch case 內部也是一樣的,也是按照順序結構來運行的
  • (3)while for 內部也是按照順序結構來執行的

4.8.7.3、編譯過程中的順序結構

  • (1)一個C程序有多個.c文件組成,編譯的時候多個.c文件是獨立分開編譯的。每個c文件編譯的時候,編譯器是按照從前到後的順序逐行進行編譯的
  • (2)編譯時候的順序編譯會導致函數或者變量必須事先聲明才能被調用,這也是函數/變量聲明的來源
  • (3)鏈接過程中呢?應該說連接過程連接器實際上是在鏈接腳本的指導下完成的。所以說鏈接時的順序是由鏈接腳本決定的。如果鏈接腳本明確指定了順序就會按照鏈接腳本中的順序來鏈接,如果沒有鏈接腳本那麽連接器就會自動的進行鏈接

4.8.7.4、思考:為什麽本質都是順序結構?

  • (1)順序結構本質上符號CPU的設計原理,CPU是人設計的,所以CPU的設計符合人的思考原理

4.8一些雜散但是值得討論的問題