1. 程式人生 > >Python筆記_第一篇_童子功_0.內存詳解(含位運算)

Python筆記_第一篇_童子功_0.內存詳解(含位運算)

臨時 解決 har 內存鏈 計算器 它的 影響 con 封裝

  Python的很多教材中並沒有講內存方面的知識,但是內存的知識非常重要,對於計算機工作原理和方便理解編程語言是非常重要的,尤其是小白,因此需要把這一方面加上,能夠更加深入的理解編程語言。這裏引用了C語言關於內容的詳細講解,其實很多知識都是相同的。

第一部分:程序(計算機運行)為什麽需要內存?

  對於內存的理解是對編程語言直接相關的,如果沒有對內存有很深的認識的話,對於編程語言也就是沒有根本的認識,編程語言跟內存有千絲萬縷的聯系。

1.1 計算機程序運行的目的

  計算機為什麽需要編程?編程已經編了那麽多年了,已經寫了很多程序了,為什麽還需要另外寫程序?計算機有這個新的程序到底是為了什麽?這些問題想

過沒有?程序的目的是為了去運行,程序運行是為了得到一定的結果和目的。計算機為什麽叫計算機?計算機就是用來計算的,與你家裏買的計算器本質上都是一樣的。比如我們今天用計算機打遊戲,跟計算關系非常大,有些遊戲畫面非常唯美,就是用算法計算出來的,計算機每時每刻都在計算,所有的計算機程序其實都是在做計算,計算就是計算數據。所以計算機程序中很重要的部分就是數據。因此我們要有下面的公式:

  計算機程序 = 代碼 + 數據

  (計算機程序運行完得到的一個結果,就是說代碼 + 數據經過運行後) = 結果

  從宏觀上來理解,代碼就是動作,就是加工數據的動作;數據就是數字(對象),上就是被代碼所加工的東西。那麽可以得出結論:程序運行的目的不外乎2個:結果和過程。用函數類比:函數的形參就是待加工數據(函數內還需要一些臨時數據,就是局部變量),函數本體就是代碼,函數返回值就是結果,函數體的執行過程就是過程。

  比如我們用C++寫幾個簡單的函數:

int add(int a, int b)
{
  return a + b;     
}  //這個函數的執行就是為了得到結果

void add(int a, int b)
{
  int c;
  c = a + b;
  printf("c = %d.\n", c);  
}  //這個函數的執行重在過程(重在過程中的printf),返回值不重要

int add(int a, int b)
{
  int c;
  c = a + b;
  printf("c = %d.\n", c);  
  return a + b;
} //這個函數是又重結果又重過程 

從這三個函數來看,就可以知道在程序運行的過程中,結果和過程的相互概念。

1.2 計算機程序運行的過程分析

計算機程序運行過程,其實就是程序中很多個函數相繼運行的過程。計算機程序是個名詞,但是計算機的運行是個動詞。程序時由很多個函數組成的,程序的本質就是函數,函數的本質就是加工動作。

1.3 馮.諾依曼結構和哈佛結構

  馮.諾依曼結構:數據和代碼放在一起。

  哈佛結構:數據和代碼分開存放。

  什麽是代碼:函數

  什麽是數據:全局變量、局部變量等

  在S5PV210中運行Linux系統,運行應用程序時,這個時候所有的應用程序的代碼和數據都在DRAM。所以這種結構就是馮.諾依曼結構;在單片機中,我們把程序代碼燒寫到Flash(norFlash)中,然後程序在Flash中原地運行,程序中所涉及到的數據(全局變量、局部變量)不能放在Flash中,必須放在RAM中(SRAM)中。

1.4 動態內存DRAM和靜態內存SRAM

  DRAM是動態內存,SRAM是靜態內存。

1.5 總結:為什麽需要內存?

  內存是用來存儲可變數據的,數據在程序中表現為全局變量、局部變量等(在gcc中,其實常量也是存儲在內存中的)(大部分單片機中,常量是存儲在Flash中的,也就是在代碼段),對我們寫程序來說非常重要,對程序運行更是本質相關。所以內存對程序來說幾乎是本質需求,越簡單的程序需要越少的內存,而越龐大復雜的乘除需要更多的內存,內存管理是我們寫程序時很重要的話題。我們以前學習的了解過的很多編程的關鍵其實是為了內存,譬如說數據結構(數據結構是研究數據如何組織的,數據是存放在內存中的)和算法(算法是為了用更優秀更有效的方法來加工數據,既然跟數據有關就離不開內存,衡量一個算法優秀的標準是,同樣一個算法所使用的內存越少算法越優秀)。

1.6 深入思考:如何管理內存(無OS時,有OS時,有無操作系統)

  對於計算機來說,內存容量越大則可能越大,所以大家都希望自己的電腦內存更大。我們寫程序時如何管理內存就成了很大的問題,如果管理不善可能會造成程序運行消耗過多的內存,這樣遲早內存都被你這個程序吃光了,當沒有內存可用時程序就會崩潰。所以內存對程序來說是一種資源。所以管理內存對程序來說是一個重要的技術和話題。

  我們先從操作系統角度來講:操作系統掌管所有的硬件內存,因此內存很大,所以操作系統把內存分成每個頁面(其實就是塊,一般是4KB大小),然後以頁面為單位進行管理。頁面內用更細小的方式來以字節為單位管理。操作系統內存管理的原理非常麻煩、非常復雜、非常不人性化。那麽對於我們使用這些操作系統的人來說,其實不需要了解這些細節。操作系統給我們提供了內存管理的一些接口,我們只需要用API即可管理內存。譬如在C語言中使用malloc、free這些結構來管理內存。如果沒有操作系統時:沒有操作系統(其實就是邏輯程序)中,程序需要直接操作內存,編程者需要自己計算內存使用和安排,如果編程者不小心把內存用錯了,錯誤結果需自己承擔。

  再從語言角度來講:不同的語言提供了不同的操作內存接口。譬如匯編:根本沒有任何內存管理,內存管理全靠程序員自己,匯編操作內存是直接使用內存地址(譬如0xd0020010),非常麻煩;

  譬如C語言:C語言中編譯器幫我們管理直接內存地址,我們都是通過編譯器提供的變量名來訪問內存的,如果需要大塊的內存,可以通過API(malloc、free等)來訪問系統內存(有OS時)。如果在邏輯程序中需要大塊的內存需要來自己定義數組等來解決。

  譬如C++語言:對內存的使用進一步封裝。我們可以用new來創建對象(其實就是為對象分配內存),然後用完了用delete來刪除對象(其實就是釋放內存),所以C++語言對內存的管理比C要高級一些,容易一些,但是C++zhong內存的管理還是靠程序員自己來做,如果程序員new了一個對象,但是用完了忘記了delete就會造成這個對象占用的內存不能釋放,這就是內存泄漏(溢出)

  譬如java/C#等:這些語言不直接操作內存,而是通過虛擬機來操作內存,這樣虛擬機作為我們程序員的代理,來幫我們處理內存的釋放工作。如果我的程序申請了內存,使用完後忘記釋放,則虛擬機會幫我釋放掉這些內存。聽起來似乎這些高級語言比C/C++有優勢,但是其實他這個虛擬機回收內存是需要付出一定代價的,所以語言沒有好壞,只有適應不適應。當我們程序對性能非常在乎的時候(譬如操作系統內核)就會用C/C++語言;當我們對開發程序的速度非常在乎的時候,就會用這些高級語言。

第二部分:位、字節、半字、字的概念和內存位寬

  從邏輯上闡述內存的編程模型和邏輯認識,並且解釋了內存單元的幾個單位:位、字節、半字、字。從邏輯上對內存有一個認知,先建立起來大框架性概念。

2.1 什麽是內存?(硬件和邏輯兩個角度)

  硬件角度:內存實際上是我們電腦的一個配件(一般叫內存條),如下圖:黑色裏面放著內存顆粒,綠色的板子就是把這些內存鏈接起來。

技術分享圖片

  根據不同的硬件實現原理還可以把內存分成SRAM和DRAM(DRAM又有好多帶,譬如最早的SDRAM,後來的DDR1,DDR2,DDR3,DDR4...、LPDDR等)這些百度相關知識都有,這裏不是從硬件角度來講,主要從邏輯上面來說。

  邏輯角度:內存他是一個隨機訪問(隨機訪問的意思是只要給一個地址,就可以訪問這個內存地址),並且可以讀寫(當然了邏輯上也可以限制其為只讀或者只寫);內存在編程中天熱是用來存放變量的。也就是因為有了內存,所以C語言才能定義變量,C語言中的一個變量實際就是對應內存中的一個單元。

2.2 內存的邏輯抽象圖(內存的編程模型)

這是一個32位的內存:

技術分享圖片

  從邏輯角度來講,內存實際上是由無限多個內存單元格組成的,每個單元格有一個固定的地址叫內存地址,這個內存地址和這個內存單元格唯一對應且永久綁定。以大樓來類比內存最合適,邏輯上的內存就好像是一棟無限大的大樓,內存的單元格就好像大樓中一個個小房間,每個內存單元格的地址就好像每個小房間的房間號。內存中存儲的內容好像住在房間中的人一樣。

  邏輯上來說,內存可以有無限大(因為數學上編號永遠可以增加,無盡頭),但是現實中實際的內存大小是有限制的。

  譬如32位的系統:內存限制就為4G。(32位系統指的是32位數據線,但是一般地址也是32位,這個地址線32位決定了內存地址只能32位的二進制,所以邏輯上的大小為2^32次方),實際上32位的系統中可用的內存就是小於等於4G的(32位CPU中裝32位Windows,但實際電腦只有512M內存)。

  2^32次方 = 4294967296bit / 1024 = 4194304KB / 1024 = 4096M / 1024 = 4G

2.3 位和字節

  內存單元的大小單位有4個:位、字節、半字、字

  位:大小為1個bit

  字節:大小為8個bit

  在所有的計算機、所有的機器中(不管是32位系統還是16位系統還是64位系統),為用於是1個bit,字節永遠是8bit。位和字節是計算機最小的單位,也就是一層樓的4間房子,每間房子8個平方(8個bit或者1個字節)。

技術分享圖片

2.4 字和字節

  半字:一般是16bit

  字:一般是32bit

  歷史上出現過很多對於字的混亂,建議對字、半字、雙字這些概念不要詳細區分,只要知道這些單位具體有多少位是依賴於平臺的,實際工作中在每種平臺上先去搞清楚這個平臺的定義(字是多少位,半字永遠是字的一半,雙字永遠是字的兩倍大小)

  編程時一般根本用不到字這個概念,那我們區分這個概念主要是因為有些文檔中會用到這些概念,如果不加區別可能會造成你對程序的誤解。

2.5 內存位寬(硬件和邏輯兩個角度)

技術分享圖片

(我們知道一個int類型的變量是32位,不同位寬讀取的方式不同)

  從硬件角度來講:硬件內存的實現本身是有寬度的,也就是說有些內存條就是8位的,而有些就是16位的。那麽需要強調的是內存芯片之間是可以並聯的。通過並聯後即使8位的內存芯片也可以做出來16位或32位的硬件內存。

  從邏輯角度來講:內存位寬是任意的,甚至邏輯上存在內存的位寬是24位的內存(但是實際上這種硬件是買不到的,也沒有實際意義)。從邏輯角度來講不管內存位寬是多少,我就直接操作即可,對我的操作不構成影響,因為你的操作不是純邏輯而是需要硬件去執行的,所以不能為所欲為,所以我們實際的很多操作都是受限於硬件的特性的。譬如24位的內存邏輯上和32位的內存沒有任何區別,但實際硬件是32位的,都按照32位硬件的特性和限制來幹活。

  這裏特別說明:內存的位寬是衡量一個內存的最重要指標,如果CPU也是32位寬,內存也是相匹配的32位寬,在實際計算數據過程中,每次即可處理一層樓的數據。如果CPU是64位寬,內存是32位寬,每次給的一層樓的數據只有32位,這樣會造成資源浪費,因此這是一個硬件匹配的重要指標。

  可以理解為:每次處理數據的吞吐量 = 內存的位寬

第三部分:內存編址和尋址、內存對齊

3.1 內存編址方法

  內存在邏輯上就是一個一個的格子(8個平方或者說8bit或者說1KB),這些個子可以用來裝東西(裏面裝的東西就是內存中存儲的數),每個格子有一個編號,這個編號就是內存地址,這個內存地址(一個數字)和這個格子的動機(實質是一個空間)是一一對應且永久綁定的,這就是內存的編址方法。

  在程序運行時計算機中CPU實際只認識內存地址,而不關心這個地址所代表的空間在哪裏,怎麽分布這些實體問題。因為硬件設計保證了按照這個地址就一定能找到這個格子,所以說內存單元的2個概念:地址空間是內存單元的兩個方面。

3.2 關鍵:內存編址是以字節為單位的

  我隨便給一個數字(譬如說是7),然後這個數字是一個內存地址,然後我問你這個內存地址對應的空間是多大?

  這個地址的大小空間是固定的,就是一個字節(1KB = 8bit)。

  如果把內存比喻為一棟大樓,那麽這個樓裏面的一個一個房間就是一個一個內存格子,這個格子的大小是固定的8bit,就好像這個大樓裏面所有房間戶型都是一樣的,都是工工整整的。

3.3 內存和數據類型的關系

  C語言中的最基本數據類型有:char(8bit) short(半個int) int(32bit) long float double

  int 整型(整數類型,這個整體現在它和CPU本身的數據位寬是一樣的)。譬如32位的CPU,整型就是32位,所以int 就是32位和CPU位寬是綁定的。

  數據類型和內存的關系就在於

  數據類型是用來定義變量的,而這些變量需要存儲、運算在內存中。所以說數據類型必須和內存相匹配才能獲得最好的性能,否則可能不工作或者效率低下。

  在32位系統中定義變量最好用int,因為這樣效率高。原因就在於32位系統本身配合內存等也是32位,這樣的硬件配置天生適合定義3位的int類型變量,效率高。也能定義8位的char類型變量或者16位short類型變量,但實際上訪問效率不高。

  有一個特點:剛好匹配是最好的!

  特別說明:在很多32位環境下,我們實際定義bool類型變量(實際只需要1個bit就夠了)都是用int來實現bool的,也就是說我們定義一個bool b1;時,編譯器實際幫我們分配了32位的內存來存儲這個bool變量b1。編譯器這樣做實際上是浪費了31位的內存,但好處是效率高。比如你去商店買火柴,我1分錢買8根火柴,老板一般都不賣給你,一般都是按一盒來買。

  問題:實際編程時是以省內存為大還是以運行效率為重?答案是不定的,看具體情況。很多年前內存很貴,機器上內存都很少,那時候寫代碼以省內存為主。現在隨著半導體技術的發展內存變得很便宜了,現在的機器都是高配,不在乎省一點兒內存,而效率和用戶體驗變成了關鍵。所以現在寫程序大部分情況下都是以效率為重。

3.4 內存對齊

  內存的對齊方式有很多種,譬如下面:

技術分享圖片

  我們定義一個 int a;類型變量,在內存中必須分配4個字節(32bit = 4 * 1KB)來存儲這個a變量。那麽就可以任意提出2中不同的內存分配思路和策略:

  第一種:0、1、2、3        對齊訪問

  第二種:1、2、3、4  或者:2、3、4、5  或者:3、4、5、6      非對齊訪問

  內存的對齊訪問不是邏輯的問題,是硬件的問題,從硬件角度來說,32位的內存它0 1 2 3四個單元本身邏輯上就有相關性,這4個單元組合起來當做一個int硬件上就是合適的,效率就高

  比喻:4個人出差,辦了4個房間號,發現3 4 5是在一起連著的,但是其他2號房間隔著好遠。這是邏輯和現實方面的差異。

  對齊訪問配合硬件所以效率很高,而非對齊訪問因為和硬件本身不搭配,所以效率不高。(因為兼容性的問題,一般硬件也都非對齊訪問,但是效率要低很多。)

3.5 從內存編址看數組的意義

  int 類型有4個地址,32個地址碼,數組的出現是和內存編址天然對應的。  

第四部分:C語言如何操作內存

4.1 C語言對內存地址的封裝(用變量名來訪問內存、數據類型的含義、函數名的含義)

  用變量名來訪問內容

  比如在匯編裏面 #define GPJ0CON 0x0200240 中直接把內存地址進行宏定義GPJ0CON丟給編譯器去執行內存。

  譬如在C語言中:int a; a = 5; a += 4; //a = 9;

  結合內存來解析C語言語句的本質:

  int a; //編譯器幫我們申請了1個int類型的內存格子(長度是4個字節,地址只有編譯器知道,把這個細節隱藏了),並且把符號a和這個格子綁定

  a = 5; //編譯器發現我們要給a賦值,就會把這個值5丟到符號a綁定的那個內存格子中。

  a += 4; //編譯器發現我們要給a加值,編譯器就會把a原來的值讀出來,然後給這個值加4,再把加之後的和寫入a裏面去。

  這就是C語言和匯編的一種對應關系。

  數據類型的含義

  C語言中數據類型的本質含義是:表示一個內存格子的長度和解析方法。

  譬如把0進行類型的強制變換:

  (int *)0; //0地址裏面存在的指針指向的是int類型,給定這個類型是多長。

  (float *)0;

  (short)0; //0地址執行一個short類型的變量

  (char)0:

  之前講過一個很重要的概念;內存單元格子的編址單位是字節。數據類型決定了長度的含義,我們一個內存地址,本來這個地址只代表1個字節的長度,但實際上我們可以通過給他一個類型(int),讓它有了長度(4),這樣這個代表內存的數字就能表示從這個數字開頭的連續的8個字節的內存格子了。

  因此:內存地址數字(0x30000000)開始連續4個字節的內存格子(0x30000000, 0x30000001, 0x30000002, 0x30000003)

  數據類型決定解析方法的含義,譬如我有一個內存地址(0x30000000),我們可以通過給這個內存地址不同類型來指定這個內存單元中二進制數的解析方法,譬如:我int 0x30000000,含義就是(0x30000000, 0x30000001, 0x30000002, 0x30000003)這4個字節鏈起來共同存儲的是一個int類型。

  強制類型轉換的含義:就是開始的類型的解析進行強制更改。

  函數名的含義:

  C語言中,函數就是一段代碼的封裝!函數名的實質就是這一段代碼的首地址。所以說函數名的本質也是一個內存地址。

4.2 用指針來間接訪問內存

  關於類型(不管是普通的變量類型int float等,還是指針類型int * float*等),只要記住:

  第一:類型只是對後面數字或者符號(代表的是內存地址)所表征的內存的一種長度規定和解析方法規定而已。

  int a; //int a;時編譯器會自動給a分配一個內存地址,譬如說0x12345678

  (int *)a;

  (float *)a;

  第二:譬如int a 和int *p 其實沒有任何區別,a和p都代表一個內存地址(譬如0x20000000),但是這個內存地址(0x20000000)的長度和解析方法不同,a是int類型所以a的長度是4字節,解析方法是按照int的規定來的;p是int*類型,所以長度是4字節,解析方法是int *的規定來的(0x20000000)開頭的連續4個字節中存儲1個地址,這個地址所代表的內存單元中存放的是一個int 類型的數。

4.3 指針類型的含義

  C語言中的指針,全名叫指針變量,指針變量其實跟普通變量沒有任何區別。

4.4 用數組來管理內存

  數組管理內存和變量其實沒有本質區別,知識符號的解析方法不同。(普通變量、數組、指針變量其實都沒有本質差別,都是對內存地址的解析,知識解析方法不一樣)。

  int a; //int類型a //編譯器分配4字節長度給a,並且把首地址和符號a綁定起來。

  int b[10]; //數組變量b //編譯器分配40個字節長度給b,並且把首元素首地址和符號b綁定起來。

  數組中第一個元素(a[0])就稱為首元素;每一個元素類型都是int,所以長度都是4,其中第一個字節就成為首地址;那麽首元素a[0]的首地址就稱為首元素首地址

4.5 用變量來訪問內存和用指針來訪問內存的區別

  這裏用一個最簡單的表述就是用變量來訪問內存其實就是把一個變量名a規定了長度和空間大小,用指針來訪問是畫出以某個首地址作為開始多長截止就是一個規定類型。這句話有點兒繞口,形象點兒比喻買包裝好的一個一個大小相等的盒子裏面裝的蛋糕,這就是用變量來訪問;用指針訪問就是知道這個蛋糕大小,自己去拿著一個標準去稱,稱出來的也就是整個一樣的蛋糕。  

  一個是買成品,大小不能變了;一個是按照成品的標準,去丈量一個新的成品。

第五部分 內存管理之結構體

  內存管理的高級話題就是研究這些數據結構。

5.1 數據結構這門學問的意義

  數據結構就是研究數據如何組織(在內存中排布),如何加工的學問題。

5.2 最簡單的數據結構:數組

  為什麽要有數組?

  因為程序中有好多個類型相同、意義相關的變量需要管理,這時候如果用單獨的變量來做,程序看起來比較亂,用數組來管理會更高管理。

  譬如:

  int ages[20]; //20個同學的年齡放在一個數字裏面,20個同學的年齡都是int類型的(類型相關),而且20個同學的年齡都是相關的(意義相關)。

5.3 數組的優勢和缺陷

  優勢:數組比較簡單,訪問用下標,可以隨機訪問。

  缺陷:1.數組中所有元素類型必須相同;2.數組大小必須定義時給出,而且一旦確定不能再改(數組不具有伸縮性)。

  (Python當中類似的有一個變量叫列表,但是不必元素類型都相同。)

5.4 結構體隆重登場

  結構體發明出來就是為了解決數組的第一個缺陷(數組中所有元素類型必須相同)

  譬如一個結構體(我們要管理3個學生的年齡,int類型),怎麽辦?

  第一種解法:用數組  

  int ages[3];

  第二種解法:用結構體

  struct ages

  {

    int age1;

    int age2;

    int age3;

  };

  struct ages age;

  分析總結:在這個示例中,數組要比結構體好。三十不能得出結論說數組就比結構體好,在包中元素類型不同時就只能用結構體而不能用數組了。

  再比如定義這個結構體

  struct people

  {

    int age;  //人的年齡

    char name[20];  //人的姓名

    int height;  //人的身高

  }

  分析總結:因為people的各個元素類型不完全相同,所以必須用結構體,沒法用數組。

  另外:數組為什麽用下標來訪問,因為每一個變量之間的間隔是4個字節,a[0]的地址+8就是a[2]是多少。結構體裏面每個元素的長短是不一樣的,所以要用“.”來訪問,只有這樣編譯器才能找到每一個元素的地址。所以說數組和結構體的差異在內存的尋址的方式不同。

5.5 題外話:結構體內嵌指針實現面向對象

  面向過程與面向對象:

  總的來說:C語言是面向過程的,但是C語言寫出來的Linux系統是面向對象的。非面向對象的語言,不一定不能實現面向對象的代碼。知識說用面向對象的語言來實現面向對象要更加簡單一些、直觀一些、無腦一些。用C++、Java等面向對象的語言來實現面向對象簡單一些,因為語言本身幫我們做了很多事情;但是用C來實現面向對象很麻煩,看起來也不容易理解,這就是為什麽大多數人學過C語言卻看不懂Linux內核代碼的原因。

  怎麽實現呢?就用結構體內嵌指針實現,譬如下面一段代碼:

  struct a

  {

    int age;  //普通變量

    void (*pFunc)(void);  //函數指針,指向void func(void)這類的函數

  }

  使用這樣的結構體就可以實現面向對象。這樣包含了函數指針的結構體,就類似與面向對象裏面的class(類),結構體中的變量類似於class中的成員變量,結構體中的函數指針類似於class中的成員方法

第六部分 內存管理之棧(stack)

  棧是棧,堆是堆,沒有堆棧這麽一說

6.1 什麽是棧

  棧是一種數據結構,c語言中使用棧來保存局部變量。棧是被發明出來管理內存的。底指針不動,頂指針隨著數據的刪減(也叫彈棧)進行上下進行移動。

技術分享圖片

6.2 棧管理內存的特點(小內存、自動化)

  先進後出 FILO first in last out ==> 棧

   先進先出 FIFO first in first out ==> 隊列

  如圖所示:

  技術分享圖片

  棧的特點是入口即出口,只有一個口,另一個口是堵死的,所以陷進去的必須後出來。這很像是青島的棧橋。

  隊列的特點是入口和出口都有,必須從入口進去,從出口出來,所以先進去的必須先出來,否則就堵住後面的,這個比喻很像銀行的排隊。

6.3 棧的應用舉例:局部變量

  C語言中的局部變量是用棧來實現的。

  我們在C中定義一個局部變量時:

  int a;

  編譯器會在棧中分配一個段空間(4個字節)給這個局部變量用(分配棧頂指針會移動給出空間,給局部變量a用的意思就是將這4個字節的棧內存的內存地址和我們定義的局部變量名a給關聯起來),對應棧的操作是入棧。

  註意:這裏的棧指針的移動和內存分配是自動的(棧自己完成,不用我們寫代碼去操作)。

  然後等我們函數退出的時候,局部變量要“滅亡”,對應棧的操作是彈棧(出棧)。出站時也就是棧頂指針移動將棧空間中與a管理的那4個字節空間釋放。這個動作也是自動的,也不用人寫代碼幹預。

  棧的優點:棧管理內存,好處是方便,分配和最後回收都不用程序員操心,C語言會自動完成。

  分析一個細節:C語言中,定義局部變量時如果未初始化,則值是隨機的,為什麽?

  定義局部變量,其實就是在棧中通過移動棧指針來給程序提供一個內存空間和這個局部變量名綁定,因為這段內存空間在棧上,而棧內存是反復使用的(彈棧知識棧指針移動了,棧內存是“臟的”,上次用完沒有清零的),所以說使用棧來實現棧的局部變量時如果不顯示初始化,值就是臟的,如果你顯示初始化。

  C語言是通過一個手段來實現局部變量的初始化的。譬如下面這段代碼:

  int a =15;  //局部變量定義時初始化

  C語言編譯器會自動把這行轉成

  int a;  //局部變量定義

  a = 15;  //普通的賦值語句

6.4 棧的不足之處(預定棧大小不太靈活,怕溢出)

  首先,棧是由大小的。所以棧內存大小不好設置。如果太小怕溢出,太大怕浪費內存。(這個缺點有點兒像數組)

  其次,棧的溢出危害很大,一定要避免。所以我們在C語言中定義局部變量時不能定義太多或者太大(譬如不能定義局部變量時這樣:int a[10000])(再譬如使用遞歸來解決問題時一定要註意遞歸收斂

第七部分 內存管理之堆(heap)

  棧是棧,堆是堆,沒有堆棧這麽一說

7.1 什麽是堆

  堆是一種內存管理方式。內存管理對操作系統來說是一件非常復雜的事情,因為首先內存容量很大,其次內存需求在時間和大小塊上沒有規律(操作系統上運行著十幾、幾十、上百、上千的進程隨時都會申請或者釋放內存,申請或釋放的內存塊大小隨意)。

7.2 堆管理內存的特點(大塊內存、手工分配、使用、釋放)

  如圖所示,系統開始加載內存的時候,堆內存暫時沒有使用,供誰需求誰去拿取使用。

技術分享圖片

  堆這種內存管理方式特點就是自由(隨時申請、釋放、大小塊隨意),堆內存是操作系統劃歸給堆管理器(操作系統中的一段代碼,屬於操作系統的內存管理單元(操作系統最主要的兩部分就是內存管理和用戶進行))來管理的。然後項使用者(用戶進程)提供API(malloc 和 free)來使用堆內存。

  我們什麽時候使用堆內存?

  需要內容容量比較大,需要反復使用及釋放時,很多數據結構(譬如鏈表)的實現都要使用堆內存。

  總結:

  特點一:容量不限(常規使用的需求容量都能滿足)。

  特點二:申請及其釋放都需要手工進行,手工進行的含義就是需要程序員寫代碼明確進行申請malloc及其釋放free,如果程序員申請內存並使用後未釋放,這段內存就丟失了(在堆管理器的記錄中,這段內存仍然屬於你的這個進程,但進程自己又以為這段內存已經不能用了,再用的時候又會去申請新的內存塊,這就叫“吃內存”),成為內存泄漏。在C/C++語言中,內存泄漏是最嚴重的程序bug,這也是別人認為Java/C# 等語言比C/C++優秀的地方。

7.3 C語言操作堆內存接口(malloc、free)

  堆內存釋放時最簡單,直接調用free釋放即可。原型==>為:void free(void *ptr);

  其他常用的接口原型如下:

  malloc 原型==>為:void *malloc(size_t size);  //size 的單位是字節

  calloc 原型==>為:void *calloc(size_t nmemb, size_t size);  //nmemb是單位,需要nmemb個單元,每個單元size個字節

  realloc 原型==>為:void *realloc(void *ptr, size_t size);  //改變原來申請的空間大小的

  譬如要申請10個int元素的內存:

  malloc(40);  

  malloc(10*sizeof(int));  //如果遷移到32位平臺上int自動變化成32了,比前面那個直接定義好

  calloc(10,4);

  calloc(10,sizeof(int));  //同理比上面那個好

  int a[10];  

  realloc(a,20)  //數組定義時必須同時給出數組元素個數(數組大小),而且一旦定義在無法更改,堆內存申請時必須給定大小,然後一旦申請完成大小不變,如果要變只能通過realloc接口。

  另外,在Java等高級語言中,有一些語法技巧可以更改數組大小,但其實這只是一種障眼法。它的工作原理是:先重新創建一個新的數組大小為要更改後的數組,然後將原數組的所有元素復制進新的數組,然後釋放掉原數組,最後返回新的數組給用戶。realloc的實現原理類似於上面說的Java中的可變大小的數組的方法。

7.4 堆的優勢和劣勢(管理大塊內存、靈活、容易內存泄漏)

  優勢:靈活。

  劣勢:需要程序員去處理各種細節,說以容易出錯,嚴重依賴於程序員的水平。

第八部分:復雜數據結構(簡單介紹)

8.1 鏈表、哈希表、二叉樹、圖等

  鏈表:

  鏈表最重要的,鏈表在Liunx內核中使用非常多,驅動、應用編寫很多時候都需要使用鏈表,所以對鏈表必須掌握。所以對鏈表必須掌握,掌握到:會自己定義結構體來實現鏈表、會寫鏈表的節點插入(前插、後插)、節點刪除、節點查找、節點遍歷等。(至於像逆序這些很少用,掌握前面那幾個這個也不難)。

  如圖所示:

技術分享圖片

  哈希表:

  哈希表不是很常用,一般不需要自己寫實現,而直接使用別人實現的哈希表比較多,對我們來說最重要的是要明白哈希表的原理、從而知道哈希表的特點,從而知道什麽時候該用哈希表,當看到別人用了哈希表的時候明白別人為什麽要用哈希表、合適不合適?有沒有更好的選擇?

  如圖所示:

技術分享圖片

  哈希表的映射(map)其實和字典的意義是一樣的,通過key去更改鍵值,在Python的幾種數據中有一個叫Dict(字典)數據類型,就是和哈希表的映射是一樣的。

  二叉樹、圖等:

  對於這些復雜數據結構,不要太當回事兒。這些復雜數據結構用到的概率很小(在嵌入式開發中),其實這些數據結構被發明出來就是為了解決特定問題的,你不處理特定問題根本用不到這些,沒必要去研究。

8.2 為什麽需要更復雜的數據結構

  因為現實中的實際問題是多種多樣的。問題的復雜度不同,所以需要解決問題的算法和數據結構也不同。所以當處理什麽復雜度的問題,就取研究針對性解決的數據結構和算法;當你沒有遇到此類問題(或者你工作的領域根本跟這個就沒關系)時就不要去管了

8.3 數據結構和算法的關系

  數據結構的發明都是為了配合一定的算法;算法是為了處理具體問題,算法的實現依賴於相應的數據結構。當前我們說的算法和純數學是不同的(算法是基於數學的,大學計算機系的研究生、博士生本科都是數學相關專業的),因為計算機算法要求以數學算法為指導,並且結合計算機本身的特點來改進和加工,最終實現一個在計算機上可以運行的算法(意思是用代碼可以表示的算法)。

8.4 應該怎麽學習(數據結構)這部分?

  從上面表述應該明白以下事實:

  第一:數據結構和算法是相輔相成的,要一起研究。

  第二:數據結構和算法對嵌入式來說不全是重點,不要盲目跑去研究這個。

  第三:一般在實際應用中,實現數據結構和算法的人和使用數據結構和算法的人是分開的。實際中有一部分人的工作就是研究數據結構和算法,並試圖用代碼來實現這些算法(表現為 庫(import));其他做真正工作的人要做的就是理解、明白這些算法和數據結構的意義、優劣、特征,然後在合適的時候選擇合適的數據結構和算法來解決自己碰到的實現問題。

  

  

123

123

(持續..............................................)

Python筆記_第一篇_童子功_0.內存詳解(含位運算)