天天看點

【C語言進階剖析】24、#pragma 使用分析

1 #pragma 簡介

2 pragma message

3 #pragma once

4 #pragma pack

5 sizeof(struct) 32位系統與64位系統差別

6 小結

今天學習一個非常重要的預處理訓示字 #pragma,在實際工程開發中這個預處理訓示字用的非常多,我們以前卻接觸的非常少,為什麼呢,因為 #pragma 是 c 語言留給編譯器生産廠商對 c 語言進行擴充了一個特殊的預處理訓示字。這也就導緻了一個問題,

#pragma 在不同的編譯器之間可能是無法移植的,這裡我們學習幾個常用的功能。

#pragma 用于訓示編譯器完成一些特定的動作

#pragma 所定義的很多訓示字是編譯器特有的(後面的參數決定)

#pragma 在不同的編譯器間是不可移植的

預處理器将忽略它不認識的 #pragma 指令

不同編譯器可能以不同的方式解釋同一條 #pragma 指令

就是因為不同的編譯器生産廠商對 #pragma 的實作可能不同,某一指令,可能一個編譯器有,另一個沒有,怎麼處理這種情況呢,那就把不認識的 #pragma 指令直接删除就行了。 也可能出現兩個編譯器都有這個指令,但是功能不同。

下面看一下用法:

【C語言進階剖析】24、#pragma 使用分析

下面看一些具體的指令吧

message 參數在大多數的編譯器中都有相似的實作

message 參數在編譯時輸出消息到編譯輸出視窗中

message 用于條件編譯中可提示代碼版本資訊

下面看一個例子:

【C語言進階剖析】24、#pragma 使用分析
上面的代碼功能是:如果定義了宏 android20,就在編譯時就将消息 compile android sdk 2.0…輸出到視窗。這裡并不表示編譯出錯,僅僅是輸出一條消息而已

我們來用編譯器編譯一下試試,代碼如下:

上面的代碼表示如果定義了對應的宏,就列印對應的資訊,并且将宏 version 定義為對應的版本資訊 #error 表示如果沒有定義宏,就編譯出錯,并生成編譯錯誤資訊。

先不定義宏,編譯一下:

【C語言進階剖析】24、#pragma 使用分析
可以看到 #error 生成一條編譯錯誤資訊

在指令行中定義宏,再次編譯,可以看到列印了一條編譯資訊(這裡僅僅是列印了一條編譯消息),并生成了可執行檔案,結果如下:

【C語言進階剖析】24、#pragma 使用分析

上面使用的是 gcc 編譯器,下面我們再用 vs 編譯器嘗試編譯一下:

在此之前,先說一下怎麼在 cmd 中用 vs 編譯器

vs的cmd使用: win+r 打開運作視窗 輸入“cmd”,回車打開 dos 視窗 找到 vs 軟體所在路徑 \vc\bin 在檔案夾中找到 vcvars32.bat 将這個 bat 檔案拖拽到 dos 視窗中,回車 切換磁盤(如 e:),回車 通過 cd 更改操作路徑(如 cd practice),回車 輸入 cl -dandroid23 24-1.c -o 24-1(android23 是指令行定義的宏,24-1.c 是本執行的cpp檔案名,請根據實際進行替換),回車 輸入24-1.exe,回車 大功告成

下面就來用 vs 編譯器編譯運作一下,結果如下:

【C語言進階剖析】24、#pragma 使用分析

可以看到 gcc 編譯器和 vs 編譯器對#pragma message 的處理略有差別:

gcc 編譯器:#pragma message(“compile android sdk 2.3…”)

vs 編譯器:compile android sdk 2.3…

這說明這兩個編譯器對 #pragma message 都有實作,但是實作略有差異。

#pragma once 用于保證頭檔案隻被編譯一次

#pragma once 是編譯器相關的,不一定被支援

前面我們學習了【c語言進階剖析】22、條件編譯使用分析,其中說到使用條件編譯可以保證頭檔案隻被編譯一次。

【C語言進階剖析】24、#pragma 使用分析

二者有什麼差別呢,#ifndef 這種方式是被 c 語言所支援的,實際上并不是隻包含一次頭檔案,而是包含多次,但是我們使用宏保證隻被嵌入一次到源代碼中,雖然隻嵌入一次,但是還是包含了多次,編譯器還是要多次處理。

#pragma once 告訴預處理器目前頭檔案隻被編譯一次,隻要 #include 一次,後面的 #include 相同的頭檔案都不起作用,不會被處理,是以 #pragma once 效率更高。更詳細的差別請看#pragma once 和 #ifndef 的差別

但是實際工程中 #ifndef 使用的更多,這是因為 #ifndef 是被 c 語言所支援的,所有的編譯器都可以編譯,但是對于 #pragma once,有些編譯器不支援。

下面通過一個例子說明:

上面的代碼包含兩次檔案 global.h,由于使用了 #pragma once,檔案 global.h 隻會被處理一次

下面我們用不同的編譯器來編譯,首先是 gcc 編譯器:

沒有任何問題,再用 vs 編譯器試試,結果如下:

【C語言進階剖析】24、#pragma 使用分析

也沒有問題,再試試 bcc 編譯器,結果如下:

【C語言進階剖析】24、#pragma 使用分析

bcc 編譯器報錯了,提示變量 g_value 被初始化了不止一次,也就是說 g_value 被多次定義。

從上面使用 gcc,vs,bcc 編譯器來看。gcc 和 vs 編譯器支援 #pragma once,bcc 編譯器不支援 #pragma once,提示 g_value 重複定義了。由于不認識 #pragma once 這個訓示字,怎麼處理呢,就直接把檔案 global.h 中的 #pragma once 删除即可。最終導緻預處理後的源代碼中 global.h 被包含兩次,g_value 被重複定義。

由于有些編譯器支援 #pragma once,有些不支援,怎麼做既能保證高效又能保證多個編譯器之間可以通用呢,那就是混合使用 #ifndef 和 #pragma once

将 global.h 的代碼更改如下:

如果編譯器支援 #pragma once,遇見該訓示字後,後面的 #include 将不再處理,這樣提高了效率,如果編譯器不支援 #pragma once,#pragma once 将直接被删除,使用 #ifndef 來保證頭檔案中的代碼隻被嵌入到預編譯後的源代碼中一次

在說這個訓示字之前,我們先說說記憶體對齊。

什麼是記憶體對齊:

不同類型的資料再記憶體中按照一定的規則排序,而不一定是順序的一個接一個的排序

下面看一個記憶體對齊的例子,下面兩個結構體的大小相同嗎。

【C語言進階剖析】24、#pragma 使用分析

我們用編譯器編譯運作一下,代碼如下:

編譯運作結果如下:

結構體大小有如下規則:

結構體變量中成員的偏移量必須是成員大小的整數倍(0被認為是任何數的整數倍)

結構體大小必須是所有成員大小的整數倍,也即所有成員大小的公倍數。

求嵌套的結構體大小規則:

嵌套的結構體,需要将其展開。對結構體求sizeof時,上述兩種原則變為:

展開後的結構體的第一個成員的偏移量應當是被展開的結構體中最大的成員的整數倍。

結構體大小必須是所有成員大小的整數倍,這裡所有成員計算的是展開後的成員,而不是将嵌套的結構體當做一個整體。

【C語言進階剖析】24、#pragma 使用分析

兩個結構體在記憶體中的分布如圖所示:test1 中放置了c1後,開始 s 的大小為 2,偏移量必須是成員大小的整數倍,是以從偏移量為 2 處開始存放,

為什麼需要記憶體對齊呢?原因如下:

cpu 對記憶體的讀取是不連續的,而是分塊讀取的,塊的大小隻能是1、2、4、8、16……位元組

當讀取操作的資料未對齊,則需要兩次總線周期來通路記憶體,是以性能會大打折扣

某些硬體平台隻能從規定的相對位址處讀取特定類型的資料,否則産生硬體異常

#pragma pack 就是用于指定記憶體對齊方式

先來感受一下 #pragma pack 是如何改變記憶體對齊方式的

将上面24-3.c 的代碼更改如下,重新編譯運作。

可以看到記憶體對齊的方式已經改變了

上面的結果我們已經初步知道了 #pragma pack 可以改變記憶體對齊的方式,具體是如何影響的呢,下面具體說明:

struct 占用記憶體大小計算:

第一個成員起始于 0 偏移處

每個成員按其類型大小和 pack 參數中較小的進行對齊

偏移位址必須能被對齊參數整除

結構體成員的大小取其内部長度最大的資料成員作為去大小

結構體總長度必須為所有對齊參數的整數倍

注意:編譯器在預設情況下按照 4 位元組對齊,也就是說如果 #pragma pack() 不寫,則和 #pragma pack(4) 效果是相同的

!!!注意:這裡是針對 32 位系統,對于64 位系統而言,預設情況下按照 8 位元組對齊

下面我們手動計算一下結構體的大小,首先是編譯器預設的對齊方式

是以 test1 的大小為 12 位元組。

下面我們再看一個例子,這是一個微軟的面試題:

我們先來手動分析一下:

經過上面的分析,struct s1 的大小為 8 個位元組,struct s2 的大小為 24個位元組,真的是這樣嗎。我們來嘗試一下。

先用 vs 編譯器編譯一下,結果如下,和我們手動計算的一樣。

【C語言進階剖析】24、#pragma 使用分析

再用 gcc 編譯器編譯一下,結果如下:

gcc 編譯器的結果和我們分析的不一樣呀,什麼原因呢,gcc 編譯器暫時不支援 8 位元組對齊,碰見不支援的 #pragma pack(8),怎麼處理呢,直接删除 #pragma pack(8) 和 #pragma pack(),這樣就變成了預設四個位元組對齊,是以 struct s2 的大小計算方法如下:

最後計算變量 e 的對齊參數時,對齊方式變成 4 位元組對齊,偏移量為 18,所有結構體大小為 20。

這再次說明了#pragma 是編譯器相關的。

32 位的編譯器預設情況按照 4 位元組對齊

對于64 位系統而言,預設情況下按照 8 位元組對齊

64位系統結果如下:

【C語言進階剖析】24、#pragma 使用分析
【C語言進階剖析】24、#pragma 使用分析

32位系統結果如下:

【C語言進階剖析】24、#pragma 使用分析
【C語言進階剖析】24、#pragma 使用分析

32 位隻有 4 個位元組,最長對齊模數隻能按 4 個位元組來對齊,double 是分成了 2 個 4 位元組。

指針大小:

32位系統:4位元組

64位系統:8位元組

1、#pragma 用于訓示編譯器完成一些特定的動作

2、#pragma 所定義的很多訓示字是編譯器特有的

#pragma message 用于自定義編譯消息

#pragma pack 用于指定記憶體對齊方式