趣說數據類型

1、C語言沒有原生字符串類型

(1)很多高級語言像java、C#等就有字符串類型,有個String來表示字符串,用法和int這些很像,可以String s1 = "linux";來定義字符串類型的變量。

(2)C語言沒有String類型,C語言中的字符串是通過字符指針來間接實現的。


2、C語言使用指針來管理字符串

(1)C語言中定義字符串方法:char *p = "linux";此時p就叫做字符串,但是實際上p只是一個字符指針(本質上就是一個指針變量,只是p指向了一個字符串的起始地址而已)。

.5.3、C語言中字符串的本質:指針指向頭、固定尾部的地址相連的一段內存

(1)字符串就是一串字符。字符反映在現實中就是文字、符號、數字等人用來表達的字符,反映在編程中字符就是字符類型的變量。C語言中使用ASCII編碼對字符進行編程,編碼後可以用char型變量來表示一個字符。字符串就是多個字符打包在一起共同組成的。

(2)字符串在內存中其實就是多個字節連續分佈構成的(類似於數組,字符串和字符數組非常像)

(3)C語言中字符串有3個核心要點:第一是用一個指針指向字符串頭;第二是固定尾部(字符串總是以'\0'來結尾);第三是組成字符串的各字符彼此地址相連。

(4)'\0'是一個ASCII字符,其實就是編碼爲0的那個字符(真正的0,和數字0是不同的,數字0有它自己的ASCII編碼)。要注意區分'\0'和'0'和0.(0等於'\0','0'等於48)

(5)'\0'作爲一個特殊的數字被字符串定義爲(幸運的選爲)結尾標誌。產生的副作用就是:字符串中無法包含'\0'這個字符。(C語言中不可能存在一個包含'\0'字符的字符串),這種思路就叫“魔數”(魔數就是選出來的一個特殊的數字,這個數字表示一個特殊的含義,你的正式內容中不能包含這個魔數作爲內容)。


4、注意:指向字符串的指針和字符串本身是分開的兩個東西

(1)char *p = "linux";在這段代碼中,p本質上是一個字符指針,佔4字節;"linux"分配在代碼段,佔6個字節;實際上總共耗費了10個字節,這10個字節中:4字節的指針p叫做字符串指針(用來指向字符串的,理解爲字符串的引子,但是它本身不是字符串),5字節的用來存linux這5個字符的內存纔是真正的字符串,最後一個用來存'\0'的內存是字符串結尾標誌(本質上也不屬於字符串)。


5、存儲多個字符的2種方式:字符串和字符數組

(1)我們有多個連續字符(典型就是linux這個字符串)需要存儲,實際上有兩種方式:第一種就是字符串;第二種是字符數組。


.6.字符串和字符數組的細節

1、字符數組初始化與sizeof、strlen

(1)sizeof是C語言的一個關鍵字,也是C語言的一個運算符(sizeof使用時是sizeof(類型或變量名),所以很多人誤以爲sizeof是函數,其實不是),sizeof運算符用來返回一個類型或者是變量所佔用的內存字節數。爲什麼需要sizeof?主要原因一是int、double等原生類型佔幾個字節和平臺有關;二是C語言中除了ADT之外還有UDT,這些用戶自定義類型佔幾個字節無法一眼看出,所以用sizeof運算符來讓編譯器幫忙計算。

(2)strlen是一個C語言庫函數,這個庫函數的原型是:size_t strlen(const char *s);這個函數接收一個字符串的指針,返回這個字符串的長度(以字節爲單位)。注意一點是:strlen返回的字符串長度是不包含字符串結尾的'\0'的。我們爲什麼需要strlen庫函數?因爲從字符串的定義(指針指向頭、固定結尾、中間依次相連)可以看出無法直接得到字符串的長度,需要用strlen函數來計算得到字符串的長度。

(3)sizeof(數組名)得到的永遠是數組的元素個數(也就是數組的大小),和數組中有無初始化,初始化多、少等是沒有關係的;strlen是用來計算字符串的長度的,只能傳遞合法的字符串進去纔有意義,如果隨便傳遞一個字符指針,但是這個字符指針並不是字符串是沒有意義的。

(4)當我們定義數組時如果沒有明確給出數組大小,則必須同時給出初始化式,編譯器會根據初始化式去自動計算數組的大小(數組定義時必須給出大小,要麼直接給,要麼給初始化式)


2、字符串初始化與sizeof、strlen

(1)char *p = "linux"; sizeof(p)得到的永遠是4,因爲這時候sizeof測的是字符指針p本身的長度,和字符串的長度是無關的。

(2)strlen剛好用來計算字符串的長度。


3、字符數組與字符串的本質差異(內存分配角度)

(1)字符數組char a[] = "linux";來說,定義了一個數組a,數組a佔6字節,右值"linux"本身只存在於編譯器中,編譯器將它用來初始化字符數組a後丟棄掉(也就是說內存中是沒有"linux"這個字符串的);這句就相當於是:char a[] = {'l', 'i', 'n', 'u', 'x', '\0'};

(2)字符串char *p = "linux";定義了一個字符指針p,p佔4字節,分配在棧上;同時還定義了一個字符串"linux",分配在代碼段;然後把代碼段中的字符串(一共佔6字節)的首地址(也就是'l'的地址)賦值給p。

總結對比:字符數組和字符串有本質差別。字符數組本身是數組,數組自身自帶內存空間,可以用來存東西(所以數組類似於容器);而字符串本身是指針,本身永遠只佔4字節,而且這4個字節還不能用來存有效數據,所以只能把有效數據存到別的地方,然後把地址存在p中。

也就是說字符數組自己存那些字符;字符串一定需要額外的內存來存那些字符,字符串本身只存真正的那些字符所在的內存空間的首地址。


C語言之結構體概述

1、結構體類型是一種自定義類型

(1)C語言中的2種類型:原生類型和自定義類型2、結構體使用時先定義結構體類型再用類型定義變量

(1)結構體定義時需要先定義結構體類型,然後再用類型來定義變量。

(2)也可以在定義結構體類型的同時定義結構體變量。


3、從數組到結構體的進步之處

(1)結構體可以認爲是從數組發展而來的。其實數組和結構體都算是數據結構的範疇了,數組就是最簡單的數據結構、結構體比數組更復雜一些,鏈表、哈希表之類的比結構體又複雜一些;二叉樹、圖等又更復雜一些。

(2)數組有2個明顯的缺陷:第一個是定義時必須明確給出大小,且這個大小在以後不能再更改;第二個是數組要求所有的元素的類型必須一致。更復雜的數據結構中就致力於解決數組的這兩個缺陷。

(3)結構體是用來解決數組的第二個缺陷的,可以將結構體理解爲一個其中元素類型可以不相同的數組。結構體完全可以取代數組,只是在數組可用的範圍內數組比結構體更簡單。


4、結構體變量中的元素如何訪問?

(1)數組中元素的訪問方式:表面上有2種方式(數組下標方式和指針方式);實質上都是指針方式訪問。

(2)結構體變量中的元素訪問方式:只有一種,用.或者->的方式來訪問。(.和->訪問結構體元素其實質是一樣的,只是C語言規定用結構體變量來訪問元素用. 用結構體變量的指針來訪問元素用->。實際上在高級語言中已經不區分了,都用.)

(3)結構體的訪問方式有點類似於數組下標的方式

思考:結構體變量的點號或者->訪問元素的實質是什麼?其實本質上還是用指針來訪問的。



.結構體的對齊訪問1

.1、舉例說明什麼是結構體對齊訪問

(1)上節講過結構體中元素的訪問其實本質上還是用指針方式,結合這個元素在整個結構體中的偏移量和這個元素的類型來進行訪問的。

(2)但是實際上結構體的元素的偏移量比我們上節講的還要複雜,因爲結構體要考慮元素的對齊訪問,所以每個元素時間佔的字節數和自己本身的類型所佔的字節數不一定完全一樣。(譬如char c實際佔字節數可能是1,也可以是2,也可能是3,也可以能4····)

(3)一般來說,我們用.的方式來訪問結構體元素時,我們是不用考慮結構體的元素對齊的。因爲編譯器會幫我們處理這個細節。但是因爲C語言本身是很底層的語言,而且做嵌入式開發經常需要從內存角度,以指針方式來處理結構體及其中的元素,因此還是需要掌握結構體對齊規則。


.2、結構體爲何要對齊訪問

(1)結構體中元素對齊訪問主要原因是爲了配合硬件,也就是說硬件本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。

(2)內存本身是一個物理器件(DDR內存芯片,SoC上的DDR控制器),本身有一定的侷限性:如果內存每次訪問時按照4字節對齊訪問,那麼效率是最高的;如果你不對齊訪問效率要低很多。

(3)還有很多別的因素和原因,導致我們需要對齊訪問。譬如Cache的一些緩存特性,還有其他硬件(譬如MMU、LCD顯示器)的一些內存依賴特性,所以會要求內存對齊訪問。

(4)對比對齊訪問和不對齊訪問:對齊訪問犧牲了內存空間,換取了速度性能;而非對齊訪問犧牲了訪問速度性能,換取了內存空間的完全利用。


結構體對齊的規則和運算

(1)編譯器本身可以設置內存對齊的規則,有以下的規則需要記住:

第一個:32位編譯器,一般編譯器默認對齊方式是4字節對齊。


總結下:結構體對齊的分析要點和關鍵:

1、結構體對齊要考慮:結構體整體本身必須安置在4字節對齊處,結構體對齊後的大小必須4的倍數(編譯器設置爲4字節對齊時,如果編譯器設置爲8字節對齊,則這裏的4是8)

2、結構體中每個元素本身都必須對其存放,而每個元素本身都有自己的對齊規則。

3、編譯器考慮結構體存放時,以滿足以上2點要求的最少內存需要的排布來算。


.4、gcc支持但不推薦的對齊指令:#pragma pack()   #pragma pack(n) (n=1/2/4/8)

(1)#pragma是用來指揮編譯器,或者說設置編譯器的對齊方式的。編譯器的默認對齊方式是4,但是有時候我不希望對齊方式是4,而希望是別的(譬如希望1字節對齊,也可能希望是8,甚至可能希望128字節對齊)。

(2)常用的設置編譯器編譯器對齊命令有2種:第一種是#pragma pack(),這種就是設置編譯器1字節對齊(有些人喜歡講:設置編譯器不對齊訪問,還有些講:取消編譯器對齊訪問);第二種是#pragma pack(4),這個括號中的數字就表示我們希望多少字節對齊。

(3)我們需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間內的對齊參數就是n。

(4)#prgma pack的方式在很多C環境下都是支持的,但是gcc雖然也可以不過不建議使用。


.5、gcc推薦的對齊指令__attribute__((packed))  __attribute__((aligned(n)))

(1)__attribute__((packed))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。packed的作用就是取消對齊訪問。

(2)__attribute__((aligned(n)))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。它的作用是讓整個結構體變量整體進行n字節對齊(注意是結構體變量整體n字節對齊,而不是結構體內各元素也要n字節對齊)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章