摘要:結構體類型的特殊聲明在初階結構體中,我們已經將了結構體類型是如何進行聲明的,那么在這里,我們將講一些特殊的結構體聲明不完全的聲明。所以我們應該這樣寫通過指針來找到下一個同類型結構體的寫法,我們就稱之為結構體的自引用。
char
short
int
long
float
double
那么除了這些內置的類型,C語言還為我們提供了一些自定義類型(構造類型),讓我們可以自己創造自己需要的類型。
其中包含我們結構體、枚舉、聯合體。
那么今天我們就來詳細地學習一下這三種類型。
在看到這類代碼,別再說你不認識了!手把手帶你認識初階結構體(結構體類型的聲明、初始化、成員訪問與傳參,全在這篇文章里)一文中,我們已經對結構體有了一個初步的認識,學習了結構體類型的聲明、定義和初始化,(忘記了的同學趕緊點擊文章鏈接去復習一下!!)那么今天,我們將在這個基礎上,對結構體的使用進行更多的講解。
在初階結構體中,我們已經將了結構體類型是如何進行聲明的,那么在這里,我們將講一些特殊的結構體聲明——不完全的聲明。
比如:當我們進行結構體聲明的時候,沒有聲明結構體的名字。
下面我們寫了兩個內容相同的匿名結構體,在第一個結構體中創建了結構體變量,在第二個結構體中創建了指針變量,那么第一個結構體的結構體變量的地址能否放到第二個結構體創建的指針變量呢?
其實這個問題的本質就是,這兩個匿名的結構體雖然內容是一樣的,但是編譯器會認為它們是相同的類型嗎?
從上圖的結果中我們可以看到,雖然程序跑起來了,但是編譯會報警告,這是因為編譯器把這兩個內容完全相同的結構體類型當成是完全不同的兩個類型。
所以我們在把&a1賦值給p1時,實際上是非法的。
在初階結構體中,我們提到了結構體中可以包含結構體,那么今天我們再講一下結構體包含結構體的進階——結構體的自引用。即結構體中不僅可以包含別的結構體,還可以包含自己的結構體。
那么它有什么意義呢?
相信大家應該聽過數據結構的概念。
數據結構其實描述的是數據在內存中存儲的結構,它在內存中可能是按順序存放的,稱為順序表。
也可能是在內存中散亂地存放著,但是我們可以把這些數據依次鏈接起來,通過一個數據來找到另一個數據,這種結構我們稱為鏈表。
所以鏈表其實就是通過鏈條把數據串聯起來。
所以,在鏈表中,我們可以把1看做一個節點,通過節點就能找到2。
那么在結構體中,我們是否就可以通過自引用來從一個節點出發,找到下一節點,依次把所有數據都找到呢?
答案好像是肯定的,那么落到代碼中,又應該如何寫呢?
我們看上面這段代碼,我們在結構體Node中放入了一個數據,表示這個節點中的數據,然后又放入了該結構體的變量n,這樣我們就能通過一個結構體找到另一個結構體了。
但是大家思考一下,上面這段代碼有什么問題嗎?
是不是如果一個結構體包含另一個結構體,另一個結構體又包含另外一個結構體,如此嵌套下去,那么這個結構體就會變得非常大。所以這種寫法是不正確的。
那么正確的應該是怎樣的呢?
我們要訪問下一個節點,其實并不一定要把整個結構體的信息都存起來,我們只需要把這個節點的地址存起來,通過地址就能訪問它的信息了,這樣就可以大大減少空間的浪費了。
所以我們應該這樣寫:
通過指針來找到下一個同類型結構體的寫法,我們就稱之為結構體的自引用。而這實際上就是我們實現鏈表的思路。
我們再看看下面這種寫法:
雖然我們寫了一個匿名的結構體,但是我們給它類型重定義為了Node,即給它重新進行了命名,但是要注意,上面這種寫法是不可行的!!
因為我們對結構體類型重定義時,實際上是在后面重定義的,而成員變量是定義在了類型重定義之前的,即這里成員變量Node*是找不到Node類型的。
所以,正確的寫法還是要求我們老老實實把結構體的名字寫在前面,并把結構體指針的類型名寫全,不要再悄悄匿名啦!!
下面我們討論一個很重要的問題:結構體的大小是如何計算的?
這實際上是結構體中一個非常重要的問題,因為計算結構體大小的時候涉及到內存對齊的問題,所以它也是一個特別熱門的考點。
首先,我們通過sizeof()操作符計算一下一個結構體的大小是多少。
在運行代碼之前,我們先猜測一下,s的大小應該是多少呢?會是6嗎?
從屏幕上打印的結果中,我們可以看到,結構體s的大小是12個字節,比我們猜測的6打了整整一倍,這是為什么呢?
帶著疑問,我們把結構體類型中的成員變量做一個微調,再計算一下它的大小。
再次運行程序,我們得到了如下結果:
屏幕上打印出來的結構體s的大小變成了8。
那么為什么成員一樣,在僅改變順序的情況下結構體的大小就發生了變化呢?下面我們就帶著疑惑來找答案。
實際上,出現上述情況是因為在結構體中,存在著內存對齊的情況。
由于內存對齊在結構體這一塊中體現得特別明顯,所以我們也經常稱其為結構體內存對齊。
那么什么是結構體內存對齊呢?
首先我們通過第一個結構體來了解一下結構體內存對齊的規則:
- 結構體的第一個成員永遠放在結構體起始位置偏移量為0的位置。
- 結構體成員從第二個開始,總是放在偏移量為一個對齊數的整數倍處。
其中:對齊數=編譯器默認的對齊數和變量自身大小的較小值
ps:在Linux環境下,沒有默認對齊數;在VS環境下,默認對齊數為8
那么在這個結構體中,第二個成員是a,類型為int,大小是4個字節,比VS環境下的默認對齊數8小,所以第二個成員的對齊數是4。
那么按照第二條規則(第二個及以后的成員變量放在偏移量為對齊數整數倍處),我們畫出第二個成員變量a在內存中的存放。
同理,第三個成員c2的對齊數是1,而a的后面是偏移量為8的位置,8是1的整數倍數,所以c2就放在偏移量為8的位置。
- 結構體的總大小必須是各個成員的對齊數中最大的那個對齊數的整數倍。
那么我們看在結構體s中,按個成員變量的對齊數分別是1、4、1,所以最大的對齊數是4。所以結構體的總大小就應該是4的整數倍。
而剛才我們畫完三個成員在內存中的存放之后,內存總大小是9,不是4的整數倍數,所以結構體的大小應該是一個大于9并且是4的整數倍的數,即12。
我們可以看到,結構體s的成員變量只用了6個字節的空間,而其余的6個字節的空間實際上是被浪費掉了 。
那么同理,我們再來看看調換了順序之后的結構體s的大小。
(這里大家快動手自己算一下這個結構體s的總大小吧~)
由于內存對齊,調換位置之后的結構體s的總大小變為了8,浪費的空間為2個字節。
那么當結構體中包含結構體時,我們應該如何計算呢?
這時候,我們就要引入結構體內存對齊的第四條規則:
- 如果存在嵌套,則嵌套結構體對齊到自己內部的成員變量的最大對齊數的整數倍數,結構體的整體大小就是所有成員的最大對齊數(包含嵌套結構體的對齊數)的整數倍數。
所以我們可以寫出上面結構體的對齊數。
那么根據以上4條結構體內存對齊規則,我們可以畫處并計算出結構體s4的總大小。
運行下面程序,可以驗證結構體s4的大小確實是32。
ps:當變量的大小超過編譯器默認的對齊數時,對齊數為編譯器默認的對齊數。(因為對齊數取的是這二者中的較小值)
例如:在VS環境下(默認對齊數為8),如果成員變量的大小超過了8,則該成員變量的對齊數仍然是8。
以上就是結構體內存對齊的規則,你弄懂了嗎??
從上面的計算中我們知道,結構體內存對齊實際上浪費了很多內存的空間,那么為什么我即使浪費了內存空間,也要內存對齊呢?
雖然這個對于結構體內存對齊的意義并沒有一個官方的解釋,但是我們我們可以從兩個角度來思考內存對齊的原因。
- 平臺原因(移植原因)
不是所有平臺都能訪問任意地址上的任意數據的,某些硬件平臺只能在某些地址處訪問某些特定類型的數據,否則就會拋出硬件異常。
即一些硬件可能只能在一些特定的位置上讀取數據(比如4的整數倍數),那么如果我們把數據放在了偏移量為2或者3的位置的時候,它就讀不到我們的數據了,所以為了保證硬件能夠讀到我們的數據,我們最好把數據對齊放在硬件能讀取的位置上。
2.性能原因
數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。
如果內存未對齊,則處理器訪問內存時可能需要訪問兩次,而對齊的內存則僅需要一次訪問。
ps:我們通常認為的自然邊界通常是4、8、16等等4的倍數。
舉例說明:
struct s{ char c;//1 int a;//4};
如上圖,我們可以很直觀地看到內存對齊對于讀取數據的好處,它實際上提高了程序運行的效率。
所以總體來說:我們可以認為結構體內存對齊相當于是拿空間換取時間的做法。
但是,雖然有時候我們會為了換取更高的效率而犧牲掉一些空間,但是從前面兩個內容相同而大小不同的結構體中,我們也可以發現,通過設計,我們是可以在盡量節省空間的情況下做到內容對齊的。
也就是說,我們在設計結構體的時候,應該既要滿足內存對齊,又要節省空間。
那么我們應該如何做呢?
答案就是:讓占用空間小的成員盡量集中在一起。
這樣我們就能盡量把可能會被浪費掉的空間利用上。
我們知道,VS的默認對齊數是8,那我們能不能對默認對齊數進行修改呢?
其實是可以的。
下面我們就來看看修改的方法。
那么如果我們把默認對齊數設置為1,那么結構體中每個成員的對齊數都將變為1,相當每個成員都是依次存在內存中,沒有內存對齊的情況。而結構體的大小就是每個成員的大小之和。
所以,如果在對齊方式不合適的時候,我們可以根據自己的需要來修改默認對齊數。
當然,內存對齊是為了提升效率,所以盡管我們可以設置默認對齊數,但是我們也要合理地對它進行應用,一般我們設置的默認對齊數為2n。
最后我們再講一個跟結構體相關的內容:結構體實現位段的能力。
那么究竟什么是位段呢?我們先來看看它的“真容”。
位段的聲明和結構體類似,但是有兩點不同:
- 位段的成員必須是int、unsigned int或signed int。
- 位段的成員名后邊有一個冒號和一個數字。
如上圖中的A就是一個位段類型。
雖然我們說位段的成員必須是int、unsigned int、或signed int。但是寫在代碼的過程中,位段的成員也可以是char,所以實際上位段的成員可以是整形家族中的成員。
那么位段到底是什么意思呢?
首先我們先計算一下struct A的空間大小。
我們發現A的大小是8個字節,而A中包含4個int類型,按我們平常的計算A的大小應該是16個字節,而屏幕輸出的卻是8。
這說明了位段是可以節省空間。
那么它到底是怎么節省空間的呢?我們再來看。
其實位段中的位指的是二進制位,位段成員后面數量表示分給該成員的二進制位數。
那么為什么我們這樣分配呢?
在生活生我們并不是所有的數據都需要占用那么大的內存空間,如果我們根據需要給數據分配合理適用的內存,那么就可以節省很多空間。
舉個栗子:假如我們現在想表示性別,而性別只有男、女、其他三種狀態,那我們其實可以用兩個bit位來表示:
男 - 00
女 - 01
其他 - 11
我們會發現,我們只需要兩個bit位就足以表示性別,而并不需要一塊很大的空間,那么這時候如果我們用位段來實現,就能給內存節省很大的一塊空間。
那么我們再回過頭來看前面位段A,我們一共給A中的成員分配了47個bit位,大約占6個字節的空間,那struct A的大小應該是6個字節才對啊?可是屏幕上輸出的大小是8,這又是為什么呢?
在這里我們要注意,位段只是在一定程度上節省了空間,但并不意味著它一點也不浪費。
下面我們就來看看位段在內存中到底是怎么分配的。
- 位段的成員可以是int、unsigned int、signed int或者是char(屬于整型家族)類型
- 位段的空間上是按照需要以4字節(int)或者1個字節(char)的方式來開辟的。
- 位段涉及很多不確定因素,位段是不跨平臺的,注意可移植的程序應該避免使用位段。
那么我們就以前面的struct A作為例子來講。
首先我們看到A中的成員都是int,所以它在空間上是以4字節(int)的方式來開辟的。即一次開辟一個int的大小,當空間不夠的時候,再開辟一個int大小的空間。
所以這里我們可以看到,A的大小是8個字節。
那么這里有一個問題:_d的30個bit位是怎么存放在內存中的呢?
是其中15個bit位存放在第一個int中剩下的位置,另外的15個bit位存放在新開辟的int空間上嗎?
還是_d的30個bit位都存放在新開辟的int空間上呢?
對于這個問題,標準并沒有給出說明,但是我們不妨對它進行一個猜測。
首先我們拿下面這段代碼來作為例子進行猜測。
我們用位段S創建了一個變量s,那么內存是怎么為s分配空間的呢?
首先,內存開辟了一個char的大小的空間,然后我們先分配其中的3個bit給a。
但是問題又來了,在分配的時候,是從高地址端開始分配,還是從低地址端開始分配呢?
這個問題標準仍然沒有說明,那我們就先假設是從右邊(低地址)開始分配的內存,那么分配了3個bit給a之后,剩下了5個bit,其中又分了4個bit給b。
現在還剩下1個bit,而c需要5個bit位,所以我們再開辟一個char的空間,
那么問題又又又來了,第一塊開辟的空間中剩余的1個bit是浪費掉了呢?還是存了c中的一個bit呢?
這個問題標準仍然是沒有告訴我們,所以我們還是繼續猜測:這塊空間被浪費掉了,c中的5個bit全部放在新開辟的空間上。
這時候我們第二塊開辟的空間還剩下3個bit,不足以放d,所以緊接著我們又開辟了一塊空間來存放d。
那么現在我們把a、b、c、d的值放進去。
那么內存中到底是不是我們猜測的樣子呢?
現在我們把它轉換成16進制,得到62 03 04。
然后再回到程序中,一邊調試一邊查看內存。
程序進一步調試,我們看看s的內存空間上的值是不是我們剛剛得到的62 03 04。
我們可以看到,這和我們得到的數是完全吻合的,說明我們之前的猜測都是正確的。
位段在內存中是先開辟了一塊空間,然后把數據從低位向高位存放,如果高位中的空間不夠用了,那我們再繼續開辟下一塊空間,然后再在新的空間中存放數據,并且每次剩余的不夠存放一個數據的空間是被浪費掉了。
但是注意,我們的猜測僅僅是在VS環境下得到了驗證,這并不意味著在其他環境下我們也可以得到這樣的結論,因為標準并沒有給出一個統一的說明。
所以我們要注意位段在內存分配的第三條,即位段中存在許多模糊地帶,我們在使用位段的時候應該十分謹慎,避免在可移植程序上使用位段,以防程序移植到另一個平臺上時出現錯誤。
從前面我們已經知道,位段中有許多標準未定義的地方,所以它是不能跨平臺,具體我們總結了以下幾點。
- int位段是有符號數還是無符號數是模糊的。
- 位段中的最大數目是模糊的。不同機器上允許存放的位段的最大bit數是不同,這有可能導致位段從一個平臺移植到另一個平臺上后出現錯誤。(16位機器上最大數是16,32位機器上最大數是32)
- 位段中開辟的空間是從左向右使用愛是從右向左使用是模糊的。
- 當一個結構包含兩個或以上的位段成員時,第二個位段成員比較大,無法被第一塊開辟的空間容納時,第一塊空間中剩余的空間是被舍棄還是繼續利用,這也是模糊的。
所以,對比結構,位段可以和結構達到相同的效果。
理論上講,結構可以出現的地方,位段也可以出現。相比結構,位段有它的優點——節省空間,也有它的缺點——存在跨平臺問題。
那么我們認識了位段之后,它到底是怎么用的呢?它通常在什么時候用呢?
我們可以看看下面這張圖。
我們在網絡上使用數據的時候,需要對數據進行封裝,這時候就需要存放一些與數據相關的信息,我們暫且把它成為一個包。
假如我們不使用位段,那么這個包相對來說就會比較大, 而如果網絡上所有的數據都背著一個大包,那么網絡上那么龐大的數據量,就會造成許多空間的浪費,整個網絡也會顯得很擁擠。
而如果我們使用位段,包中的每個信息我們只需要給它分配足夠的空間,那么這個包的就會小很多,這個數據背著小包在網絡上通行的時候也會走的更加輕快。
所以實際上,利用位段可以很好地搭建網絡底層的一些架構。
在我們的生活中,有一些值是有限的,我們可以一一列舉出來,比如性別、月份等等,我們把這些值一一列出來,就是枚舉。
枚舉中我們要使用到一個關鍵字enum,我們以一周的星期來舉例。
上面這個day就是一個枚舉類型,而{ }內的內容就是我們之前提到的枚舉常量。
我們把上面的值用整型的形式打印出來。
我們可以看到,這些枚舉常量是有值的,它默認從0開始,依次遞增。
那么如果我們不想讓它的值從0開始,我們可以在定義枚舉類型的時候給它進行賦值。
我們要注意,day是一個枚舉類型,其中放的是枚舉常量,而我們給它賦的值是未來我們創建了一個枚舉常量之后可能的取值。我們只能在定義枚舉類型的時候給它們賦初值,但是不能再后面創建變量的時候修改它的值。
注意:當我們創建了一個枚舉常量之后,我們應該給它賦相應的枚舉類型的可能取值,而不是直接賦上一個數值。
上面的代碼雖然在C中可能通過,但是在.cpp文件中是非法的。
那么我們為什么要使用枚舉呢?
這是因為,利用枚舉相當于給這些值賦上了一些相應的意義,當我們比如上面的1賦給了Mon,那么我們就能知道1表示的就是星期一。這實際上增加了代碼的可讀性和可維護性。
那么我們之前也學過#define定義的標識符常量,枚舉和它相比又有上面區別呢?
實際上,#define定義的是一個標識符,即用一個符號來代表某個有特定含義的數,這樣未來在這個數發生變化時,可以通過#define來進行快速修改。
而枚舉不僅可以提供這樣的功能,枚舉的定義讓這些常量具有了類型——枚舉類型,相比只下枚舉會更加嚴謹。
不僅如此,#define定義的常量是直接暴露在全局范圍內的,而我們定義的枚舉常量是被封裝在一個類型里面的,它能防止命名的污染。
并且我們可以通過枚舉類型創建變量,于是我們可以在程序中通過調試來觀察,而#define定義的標識符常量實際上只是一種等價替換,我們是無法通過調試來觀察它的。
枚舉可以一次定義多個常量,而#define一次僅能定義一個值,所以從使用的角度來講,枚舉更便于使用。
拿我們之前寫過的一個簡易計算機來舉例:
如果我們這樣寫,數值和選項要通過菜單一一對應,比較別扭。
但是如果我們定義了一個枚舉類型,我們就可以直接這樣寫:
我們知道枚舉類型是一種自定義類型,我們根據這個類型創建了一個變量才在內存上分配了空間,而枚舉常量的值都是一些整數,所以我們可以得到,枚舉類型的大小就是一個整型的大小。
接下來我們再講一種特殊的自定義類型——聯合體。
聯合體也叫共用體,它的變量包含一系列的成員,而這些成員是共用同一塊空間的。(具體如何共用,別急,后面會講到~)
那么聯合類型是如何定義的呢?
首先聯合的關鍵字是union。
前面我們已經說到,聯合的特點就是成員共用一塊空間。
下面我們先計算一下聯合變量的大小。
我們可以看到,聯合體Un中包含一個char c,一個int i,它們加起來應該是5,而屏幕上輸出的聯合體的大小是4,這里就體現了聯合體的特點,因為c和i是共用一塊空間的,所以聯合體Un的大小是聯合體中最大成員的大小。
接下來,我們再來看一下聯合體和其中的成員的地址。
我們可以看到,聯合體和它的成員的地址是一樣的,進一步體現了聯合體中的成員是共用一塊空間的。
所以這里我們也要注意,我們改變i的時候,可能會改變c,改變c的時候,也會改變了 i 的值。
同時,一個聯合變量的大小,至少是最大成員的大小,因為聯合體得有能力保存最大的那個成員。
那么利用聯合體,我們也可以判斷當前機器的大小端。(關于機器大小端字節序的內容,忘記的同學可以翻看這篇噢C語言進階第一問:數據在內存中是如何存儲的?(手把手帶你深度剖析數據在內存中的存儲,超全解析,碼住不虧))
那么我們什么時候適合用聯合體呢?
當聯合體中的所有成員在同一時間中只使用一個,并且它們可以共用同一塊的空間時候,我們就可以使用聯合體。
比如學校要開發一個教務管理系統,那么教務管理系統中的人的身份就分為學生和老師,而一個人不可能既是學生,又是老師,而我們可以用同一塊空間來表示學生和老師,這時候我們就可以用結構體來描述教務系統中的人的身份。
那么聯合大小應該如何計算呢?
- 聯合體的大小至少是最大成員的大小
- 聯合體中也存在內存對齊。當最大成員的大小不是最大對齊數的大小時,聯合體的大小就要對齊到最大對齊數的整數倍。
我們通過例子來看看。
以上就是C語言中的自定義類型,有了這些自定義類型,我們就可以根據自己的需要寫出自己想要的類型啦!
好啦,今天的文章就到這里啦!如果你喜歡博主的文章,記得點贊評論收藏一波哦!
你的鼓勵將是我繼續努力碼字的巨大動力!!!!
關注我,一起精進C語言吧!~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/121673.html
摘要:在符號位中,表示正,表示負。我們知道對于整型來說,內存中存放的是該數的補碼。在計算機系統中,數值一律用補碼來表示和存儲。表示有效數字,。規定對于位的浮點數,最高的位是 ...
摘要:簡介比更強大的開源語言,簡稱,親爸是微軟。大彬哥就愛吃剁椒魚頭。接口,范型,命名空間,以及模塊化管理,并講在框架和工作流中的應用等更多精彩內容歡迎大家觀看我的講座極速完全進階指南 +TypeScript簡介 ? 1.比javascript更強大的開源語言,簡稱TS,親爸是微軟。 ? 2.官網 ? 英文官網:https://www...
摘要:插件鍵位映射技巧性的配置等等都是錦上添花,它們有助于你進一步提高效率以及個性化你的工作環境,但是對于哲學的理解幫助甚少。為你開啟語法高亮。你可以自定義各種語言的語法高亮,無非就是根據這些規 如果沒有挑戰,人生將多么無趣! 兩種副本 在我的硬盤上總是保留著(至少)兩份 Vim 的配置文件。其中一份是所謂完全正式版,它的文件名是 .vimrc,到本系列結束的時候,我們將了解其中...
閱讀 2723·2023-04-25 22:15
閱讀 1804·2021-11-19 09:40
閱讀 2149·2021-09-30 09:48
閱讀 3212·2021-09-03 10:36
閱讀 2025·2021-08-30 09:48
閱讀 1853·2021-08-24 10:00
閱讀 2725·2019-08-30 15:54
閱讀 698·2019-08-30 15:54