字符串类型、结构体、共用体、枚举、container宏、内存来源
一、C語言的字符串類型
1、C語言沒有原生字符串類型
- 很多高級語言像java、C#等就有字符串類型,有個String來表示字符串,用法和int這些很像,可以String s1 = "linux";來定義字符串類型的變量。
- C語言沒有String類型,C語言中的字符串是通過字符指針來間接實現的。
2、C語言使用指針來管理字符串
- C語言中定義字符串方法:char *p = "linux";
- 此時p就叫做字符串,但是實際上p只是一個字符指針(本質上就是一個指針變量,只是p指向了一個字符串的起始地址而已)。
3、C語言中字符串的本質
(1)指針指向頭、固定尾部的地址相連的一段內存;
(2)字符串就是一串字符。
- 字符反映在現實中就是文字、符號、數字等人用來表達的字符,反映在編程中字符就是字符類型的變量。
- C語言中使用ASCII編碼對字符進行編程,編碼后可以用char型變量來表示一個字符。字符串就是多個字符打包在一起共同組成的。
- 字符串在內存中其實就是多個字節連續分布構成的(類似于數組,字符串和字符數組非常像)。
- C語言中字符串有3個核心要點:第一是用一個指針指向字符串頭;第二是固定尾部(字符串總是以'\0'來結尾);第三是組成字符串的各字符彼此地址相連。
- 字符'\0'是一個ASCII字符,其實就是ASCII編碼為0的那個字符(數字0有它自己的ASCII編碼48)。要注意區分'\0'和'0'和0。('\0'等于0,'0'等于48)
- '\0'作為一個特殊的數字被字符串定義為(幸運的選為)結尾標志。產生的副作用就是:字符串中無法包含'\0'這個字符。(C語言中不可能存在一個包含'\0'字符的字符串),這種思路就叫“魔數”(魔數就是選出來的一個特殊的數字,這個數字表示一個特殊的含義,你的正式內容中不能包含這個魔數作為內容)。
4、指向字符串的指針和字符串本身
- char *p = "linux";
- p本質上是一個字符指針,占4字節;
- "linux"分配在代碼段,占6個字節;
5、存儲多個字符的2種方式
- 字符串和字符數組
6、字符數組初始化與sizeof、strlen
(1)sizeof是C語言的一個關鍵字,也是C語言的一個運算符(sizeof使用時是sizeof(類型或變量名)
- sizeof運算符用來返回一個類型或者是變量所占用的內存字節數。
- 為什么需要sizeof?主要原因一是int、double等原生類型占幾個字節和平臺有關;二是C語言中除了ADT之外還有UDT,這些用戶自定義類型占幾個字節無法一眼看出,所以用sizeof運算符來讓編譯器幫忙計算。
- sizeof(數組名)得到的永遠是數組的元素個數(也就是數組的大小),和數組中有無初始化,初始化多、少等是沒有關系的;
(2)strlen是一個C語言庫函數
- 這個庫函數的原型是:size_t strlen(const char *s);這個函數接收一個字符串的指針,返回這個字符串的長度(以字節為單位)。
- strlen返回的字符串長度是不包含字符串結尾的'\0'的。
- 因為從字符串的定義(指針指向頭、固定結尾、中間依次相連)可以看出無法直接得到字符串的長度,需要用strlen函數來計算得到字符串的長度。
- strlen是用來計算字符串的長度的,只有傳遞合法的字符串進去才有意義,如果隨便傳遞一個字符指針,但是這個字符指針并不是字符串是沒有意義的。
7、字符串初始化與sizeof、strlen
8、字符數組與字符串的本質差異(內存分配角度)
(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。
(3)字符數組和字符串有本質差別
- 字符數組本身是數組,數組自身自帶內存空間,可以用來存東西(所以數組類似于容器);而字符串本身是指針,本身永遠只占4字節,而且這4個字節還不能用來存有效數據,所以只能把有效數據存到別的地方,然后把地址存在p中。也就是說字符數組自己存那些字符;字符串一定需要額外的內存來存那些字符,字符串本身只存真正的那些字符所在的內存空間的首地址。
二、C語言之結構體概述
1、結構體類型是一種自定義類型
- C語言中的2種類型:原生類型和自定義類型。
2、從數組到結構體的進步之處
- 數組有2個明顯的缺陷:第一個是定義時必須明確給出大小,且這個大小在以后不能再更改;第二個是數組要求所有的元素的類型必須一致。更復雜的數據結構中就致力于解決數組的這兩個缺陷。
- 結構體是用來解決數組的第二個缺陷的,可以將結構體理解為一個其中元素類型可以不相同的數組。結構體完全可以取代數組,只是在數組可用的范圍內數組比結構體更簡單。
3、如何訪問結構體變量中的元素?
(1)數組中元素的訪問方式:表面上有2種方式(數組下標方式和指針方式);實質上都是指針方式訪問。
(2)C語言規定用結構體變量來訪問元素用. ;用結構體變量的指針來訪問元素用->。
(3)其實本質上還是用指針來訪問的。
4、什么是結構體對齊訪問?
- 結構體中元素的訪問其實本質上還是用指針方式,結合這個元素在整個結構體中的偏移量和這個元素的類型來進行訪問的。
- 但是因為結構體要考慮元素的對齊訪問,所以每個元素實際占的字節數和自己本身的類型所占的字節數不一定完全一樣。
- 一般來說,我們用.的方式來訪問結構體元素時,我們是不用考慮結構體的元素對齊的。因為編譯器會幫我們處理這個細節。
- 但是因為C語言本身是很底層的語言,而且做嵌入式開發經常需要從內存角度,以指針方式來處理結構體及其中的元素,因此還是需要掌握結構體對齊規則。
5、結構體為何要對齊訪問
- 結構體中元素對齊訪問主要原因是為了配合硬件,也就是說硬件本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。
- 內存本身是一個物理器件(DDR內存芯片,SoC上的DDR控制器),本身有一定的局限性:如果內存每次訪問時按照4字節對齊訪問,那么效率是最高的。
- Cache的一些緩存特性,還有其他硬件(譬如MMU、LCD顯示器)的一些內存依賴特性,要求內存對齊訪問。
- 對齊訪問犧牲了內存空間,換取了速度性能;而非對齊訪問犧牲了訪問速度性能,換取了內存空間的完全利用。
6、結構體對齊的規則和運算
(1)編譯器可以設置內存對齊的規則
- 32位編譯器,一般編譯器默認對齊方式是4字節對齊。
(2)gcc支持但不推薦的對齊指令:#pragma pack() ? #pragma pack(n) (n=1/2/4/8)
- #pragma用來設置編譯器的對齊方式。
- #pragma pack()設置編譯器1字節對齊;#pragma pack(n),括號中的數字表示多少字節對齊。
- 需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間內的對齊參數就是n。
- #prgma pack的方式在很多C環境下都是支持的,但是gcc不建議使用。
(3)gcc推薦的對齊指令__attribute__((packed)) ?__attribute__((aligned(n)))
- __attribute__((packed))放在要進行內存對齊的類型定義的后面,起作用的范圍只有加了此修飾的這一個類型。packed的作用就是取消對齊訪問。
- __attribute__((aligned(n)))放在要進行內存對齊的類型定義的后面,起作用的范圍只有加了此修飾的這一個類型。它的作用是讓整個結構體變量整體進行n字節對齊(注意是結構體變量整體n字節對齊,而不是結構體內各元素也要n字節對齊)
(4)規則
- 見博客http://blog.csdn.net/oqqhutu12345678/article/details/65437355
三、offsetof宏與container_of宏
1、offsetof宏
(1)offsetof宏的作用:用宏來計算結構體中某個元素和結構體首地址的偏移量(其實質是通過編譯器來幫我們計算)。
(2)offsetof宏的原理:虛擬一個type類型結構體變量,然后用type.member的方式來訪問那個member元素,繼而得到member相對于整個變量首地址的偏移量。
(3)學習思路:第一步先學會用offsetof宏,第二步再去理解這個宏的實現原理。
- (TYPE *)0 :這是一個強制類型轉換,把0地址強制類型轉換成一個指針,這個指針指向一個TYPE類型的結構體變量。(實際上這個結構體變量可能不存在,但是只要我不去解引用這個指針就不會出錯)。
- ((TYPE *)0)->MEMBER (TYPE *)0是一個TYPE類型結構體變量的指針,通過指針指針來訪問這個結構體變量的member元素。
- &((TYPE *)0)->MEMBER ?等效于&(((TYPE *)0)->MEMBER),意義就是得到member元素的地址。但是因為整個結構體變量的首地址是0,
2、container_of宏
- 見博客http://blog.csdn.net/oqqhutu12345678/article/details/72851192
四、共用體union
1、共用體類型的定義、變量定義和使用
(1)共用體union和結構體struct在類型定義、變量定義、使用方法上很相似。
(2)共用體和結構體的不同
- 結構體類似于一個包裹,結構體中的成員彼此是獨立存在的,分布在內存的不同單元中,只是被打包成一個整體叫做結構體而已;
- 共用體中的各個成員其實是一體的,彼此不獨立,他們使用同一個內存單元??梢岳斫鉃?#xff1a;有時候是這個元素,有時候是那個元素。更準確的說法是同一個內存空間有多種解釋方式。
- 共用體union就是對同一塊內存中存儲的二進制的不同的理解方式。
- 在有些書中把union翻譯成聯合(聯合體),這個名字不好。現在翻譯成共用體比較合適。
(3)union的sizeof測到的大小實際是union中各個元素里面占用內存最大的那個元素的大小。
(4)union中的元素不存在內存對齊的問題
- 因為union中實際只有1個內存空間,都是從同一個地址開始的(開始地址就是整個union占有的內存空間的首地址),所以不涉及內存對齊。
2、共用體和結構體的相同和不同
- 相同點就是操作語法幾乎相同。
- 不同點是本質上的不同。struct是多個獨立元素(內存空間)打包在一起;union是一個元素(內存空間)的多種不同解析方式。
3、共用體的主要用途
- 共用體就用在那種對同一個內存單元進行多種不同規則解析的這種情況下。
- C語言中其實是可以沒有共用體的,用指針和強制類型轉換可以替代共用體完成同樣的功能,但是共用體的方式更簡單、更便捷、更好理解。
五、枚舉
1、枚舉的作用
- 枚舉在C語言中其實是一些符號常量集。直白點說:枚舉定義了一些符號,這些符號的本質就是int類型的常量,每個符號和一個常量綁定。這個符號就表示一個自定義的一個識別碼,編譯器對枚舉的認知就是符號常量所綁定的那個int類型的數字。
- 枚舉中的枚舉值都是常量,怎么驗證?
- 枚舉符號常量和其對應的常量數字相對來說,數字不重要,符號才重要。符號對應的數字只要彼此不相同即可,沒有別的要求。所以一般情況下我們都不明確指定這個符號所對應的數字,而讓編譯器自動分配。(編譯器自動分配的原則是:從0開始依次增加。如果用戶自己定義了一個值,則從那個值開始往后依次增加)
2、C語言為何需要枚舉
- C語言沒有枚舉是可以的。使用枚舉其實就是對1、0這些數字進行符號化編碼,這樣的好處就是編程時可以不用看數字而直接看符號。符號的意義是顯然的,一眼可以看出。而數字所代表的含義除非看文檔或者注釋。
- 而宏定義的目的是不用數字而用符號。從這里可以看出:宏定義和枚舉有內在聯系。宏定義和枚舉經常用來解決類似的問題,他們倆基本相當可以互換,但是有一些細微差別。
3、宏定義和枚舉的區別
- 枚舉是將多個有關聯的符號封裝在一個枚舉中,而宏定義是完全散的。也就是說枚舉其實是多選一。
- 什么情況下用枚舉?當我們要定義的常量是一個有限集合時(譬如一星期有7天,譬如一個月有31天,譬如一年有12個月····),最適合用枚舉。(其實宏定義也行,但是枚舉更好)
- 不能用枚舉的情況下(定義的常量符號之間無關聯,或者無限的)用宏定義。
- 宏定義先出現,用來解決符號常量的問題;后來發現定義的符號常量彼此之間有關聯(多選一的關系),用宏定義來做雖然可以但是不貼切,于是發明枚舉來解決這種情況。
4、枚舉的定義和使用
六、大小端模式
1、什么是大小端模式
(1)大端模式(big endian)和小端模式(little endian)。
(2)在串口等串行通信中
- 一次只能發送1個字節。這時候要發送一個int類型的數就遇到一個問題。int類型有4個字節,我是按照byte0 byte1 byte2 byte3這樣的順序發送,還是按照byte3 byte2 byte1 byte0這樣的順序發送。規則就是發送方和接收方必須按照同樣的字節順序來通信,否則就會出現錯誤。這就叫通信系統中的大小端模式。這是大小端這個詞和計算機掛鉤的最早問題。
(3)更多是指計算機存儲系統的大小端。
- 在計算機內存/硬盤/Nnad中。因為存儲系統是32位的,但是數據仍然是按照字節為單位的。于是乎一個32位的二進制在內存中存儲時有2種分布方式:高字節對應高地址(大端模式)、高字節對應低地址(小端模式)
(4)大端模式和小端模式本身沒有優劣
- 理論上按照大端或小端都可以,但是要求必須存儲時和讀取時按照同樣的大小端模式來進行,否則會出錯。
(5)有些CPU公司用大端(譬如C51單片機);有些CPU用小端(譬如ARM)
- 當不知道當前環境是用大端模式還是小端模式時就需要用代碼來檢測當前系統的大小端。
- 經典筆試題:用C語言寫一個函數來測試當前機器的大小端模式。
2、用union來測試機器的大小端模式
3、指針方式來測試機器的大小端
4、看似可行實則不行的測試大小端方式:位與、移位、強制類型轉化
- 位與運算是編譯器提供的運算,這個運算是高于內存層次的(或者說&運算在二進制層次具有可移植性,也就是說&的時候一定是高字節&高字節,低字節&低字節,和二進制存儲無關)。
- 因為C語言對運算符的級別是高于二進制層次的。右移運算永遠是將低字節移除,而和二進制存儲時這個低字節在高位還是低位無關的。
5、通信系統中的大小端(數組的大小端)
(1)譬如要通過串口發送一個0x12345678給接收方,但是因為串口本身限制,只能以字節為單位來發送,所以需要發4次;接收方分4次接收,內容分別是:0x12、0x34、0x56、0x78.接收方接收到這4個字節之后需要去重組得到0x12345678(而不是得到0x78563412)。
(2)所以在通信雙方需要有一個默契,就是:先發/先接的是高位還是低位?這就是通信中的大小端問題。
(3)在通信協議中,大小端是非常重要的,大家使用別人定義的通信協議還是自己要去定義通信協議,一定都要注意標明通信協議中大小端的問題。
七、內存來源
1、棧(stack)、堆(heap)、數據區(.data)
- 在一個C語言程序中,能夠獲取的內存就是三種情況:棧(stack)、堆(heap)、數據區(.data)
2、棧的詳解
- 運行時自動分配&自動回收:棧是自動管理的,程序員不需要手工干預。方便簡單。
- 反復使用:棧內存在程序中其實就是那一塊空間,程序反復使用這一塊空間。
- 臟內存:棧內存由于反復使用,每次使用后程序不會去清理,因此分配到時保留原來的值。
- 臨時性:(函數不能返回棧變量的指針,因為這個空間是臨時的)。
- 棧會溢出:因為操作系統事先給定了棧的大小,如果在函數中無窮盡的分配棧內存總能用完。
3、堆內存詳解
- 操作系統堆管理器管理:堆管理器是操作系統的一個模塊,堆管理內存分配靈活,按需分配。
- 大塊內存:堆內存管理者總量很大的操作系統內存塊,各進程可以按需申請使用,使用完釋放。
- 程序手動申請&釋放:手工意思是需要寫代碼去申請malloc和釋放free。
- 臟內存:堆內存也是反復使用的,而且使用者用完釋放前不會清除,因此也是臟的。
- 臨時性:堆內存只在malloc和free之間屬于我這個進程,而可以訪問。在malloc之前和free之后都不能再訪問,否則會有不可預料的后果。
(1)堆內存使用范例
- void *是個指針類型,malloc返回的是一個void *類型的指針,實質上malloc返回的是堆管理器分配給我本次申請的那段內存空間的首地址(malloc返回的值其實是一個數字,這個數字表示一個內存地址)。為什么要使用void *作為類型?主要原因是malloc幫我們分配內存時只是分配了內存空間,至于這段空間將來用來存儲什么類型的元素malloc是不關心的,由我們程序自己來決定。
- 什么是void類型。早期被翻譯成空型,這個翻譯非常不好,會誤導人。void類型不表示沒有類型,而表示萬能類型。void的意思就是說這個數據的類型當前是不確定的,在需要的時候可以再去指定它的具體類型。void *類型是一個指針類型,這個指針本身占4個字節,但是指針指向的類型是不確定的,換句話說這個指針在需要的時候可以被強制轉化成其他任何一種確定類型的指針,也就是說這個指針可以指向任何類型的元素。
- malloc的返回值:成功申請空間后返回這個內存空間的指針,申請失敗時返回NULL。所以malloc獲取的內存指針使用前一定要先檢驗是否為NULL。
- malloc申請的內存時用完后要free釋放。free(p);會告訴堆管理器這段內存我用完了你可以回收了。堆管理器回收了這段內存后這段內存當前進程就不應該再使用了。因為釋放后堆管理器就可能把這段內存再次分配給別的進程,所以你就不能再使用了。
- 再調用free歸還這段內存之前,指向這段內存的指針p一定不能丟(也就是不能給p另外賦值)。因為p一旦丟失這段malloc來的內存就永遠的丟失了(內存泄漏),直到當前程序結束時操作系統才會回收這段內存。
(2)malloc的一些細節表現
- malloc(0):malloc申請0字節內存本身就是一件無厘頭事情,一般不會碰到這個需要。如果真的malloc(0)返回的是NULL還是一個有效指針?答案是:實際分配了16Byte的一段內存并且返回了這段內存的地址。這個答案不是確定的,因為C語言并沒有明確規定malloc(0)時的表現,由各malloc函數庫的實現者來定義。
- malloc(4):gcc中的malloc默認最小是以16B為分配單位的。如果malloc小于16B的大小時都會返回一個16字節的大小的內存。malloc實現時沒有實現任意自己的分配而是允許一些大小的塊內存的分配。
- malloc(20)去訪問第25、第250、第2500····會怎么樣。實戰中:120字節處正確,1200字節處正確····終于繼續往后訪問總有一個數字處開始段錯誤了。
4、代碼段、數據段、bss段
(1)編譯器在編譯程序的時候,將程序中的所有的元素分成了一些組成部分,各部分構成一個段,所以說段是可執行程序的組成部分。
(2)代碼段
- 代碼段就是程序中的可執行部分,直觀理解代碼段就是函數堆疊組成的。
(3)數據段(也被稱為數據區、靜態數據區、靜態區)
- 數據段就是程序中的數據,直觀理解就是C語言程序中的全局變量。(注意:全局變量才算是程序的數據,局部變量不算程序的數據,只能算是函數的數據)。
(4)bss段(又叫ZI(zero initial)段)
- bss段的特點就是被初始化為0,bss段本質上也是屬于數據段,bss段就是被初始化為0的數據段。
- 數據段(.data)和bss段的區別和聯系:二者本來沒有本質區別,都是用來存放C程序中的全局變量的。區別在于把顯示初始化為非零的全局變量存在.data段中,而把顯式初始化為0或者并未顯式初始化(C語言規定未顯式初始化的全局變量值默認為0)的全局變量存在bss段。
(5)有些特殊數據會被放到代碼段
- C語言中使用char *p = "linux";定義字符串時,字符串"linux"實際被分配在代碼段,也就是說這個"linux"字符串實際上是一個常量字符串而不是變量字符串。
- const型常量:C語言中const關鍵字用來定義常量,常量就是不能被改變的量。const的實現方法至少有2種:第一種就是編譯將const修飾的變量放在代碼段去以實現不能修改(普遍見于各種單片機的編譯器);第二種就是由編譯器來檢查以確保const型的常量不會被修改,實際上const型的常量還是和普通變量一樣放在數據段的(gcc中就是這樣實現的)。
(5)顯式初始化為非零的全局變量和靜態局部變量放在數據段
- 放在.data段的變量有2種:第一種是顯式初始化為非零的全局變量。第二種是靜態局部變量,也就是static修飾的局部變量。(普通局部變量分配在棧上,靜態局部變量分配在.data段)
(6)未初始化或顯式初始化為0的全局變量放在bss段
- bss段和.data段并沒有本質區別,幾乎可以不用明確去區分這兩種。
5、總結
(1)相同點
- 三種獲取內存的方法,都可以給程序提供可用內存,都可以用來定義變量給程序用。
(2)不同點
- 棧內存對應C中的普通局部變量(別的變量還用不了棧,而且棧是自動的,由編譯器和運行時環境共同來提供服務的,程序員無法手工控制);
- 堆內存完全是獨立于我們的程序存在和管理的,程序需要內存時可以去手工申請malloc,使用完成后必須盡快free釋放。(堆內存對程序就好象公共圖書館對于人);
- 數據段對于程序來說對應C程序中的全局變量和靜態局部變量。
(3)不同的存儲方式有不同的特點,簡單總結如下:
- ?函數內部臨時使用,出了函數不會用到,就定義局部變量
- ?堆內存和數據段幾乎擁有完全相同的屬性,大部分時候是可以完全替換的。但是生命周期不一堆內存的生命周期是從malloc開始到free結束,而全局變量是從整個程序一開始執行就開始,直到整個程序結束才會消滅,伴隨程序運行的一生。啟示:如果你這個變量只是在程序的一個階段有用,用完就不用了,就適合用堆內存;如果這個變量本身和程序是一生相伴的,那就適合用全局變量。
總結
以上是生活随笔為你收集整理的字符串类型、结构体、共用体、枚举、container宏、内存来源的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 宏定义与预处理、函数和函数库
- 下一篇: Java的接口及实例