【C進階】24、#pragma分析

bryson發表於2021-12-28

Summary

1)#pragma 用於 指示編譯器 完成一些特定的動作;#pragma 所定義的很多指示字是編譯器特有的,所以在不同的編譯器間是不可移植

  • 前處理器忽略它不認識的#pragma指令
  • 不同的編譯器可能以不同的方式解釋同一條#pragma指令
  • 一般用法:#pragma parameter ,不同的parameter引數語法和意義各不相同。

2)#pragma message在編譯時輸出資訊到編譯輸出視窗;和#error、#warning不同,#pragma message僅僅表示一條提示資訊,不代表錯誤。(vc、bcc和gcc三款編譯器行為不同)

3)#pragma once用於保證標頭檔案只被編譯一次#pragma once編譯器相關的,不一定被支援。(vc和gcc支援,bcc不支援)

4)#ifndef...#define...#endif也可以用來防止標頭檔案被重複包含,和#pragma once有何不同?

  • #ifndef方式是C語言支援的,用在各個編譯器都可以;如果一個.h被include了n次預編譯器就會判斷n次這個標頭檔案是否已經包含了。
  • #pragma once方式是編譯器相關的,不一定所有編譯器都支援;預編譯器只會處理一次,後面不再判斷。

5)什麼是記憶體對齊?

  • 不同型別的資料在記憶體中按照一定的規則排列不一定是順序的一個接一個的排列

6)為什麼需要記憶體對齊?

  • CPU對記憶體的讀取不是連續的,而是分成塊讀取的,塊的大小隻能是2的冪,1、2、4、8...位元組
  • 當讀取操作的資料未對齊,則需要兩次匯流排週期來訪問記憶體,因此效能會大打折扣
  • 某些硬體平臺只能從規定的相對地址處讀取特定型別的資料,否則會產生硬體異常

7)編譯器預設的對齊方式為4位元組對齊, #pragma pack(n) 可以調整編譯器的預設對齊方式(vc和bcc編譯器支援8位元組對齊,但是gcc不支援)

8)記憶體對齊的規則

·#pragma分析

#pragma 用於 指示編譯器 完成一些特定的動作;
#pragma 所定義的很多指示字是編譯器特有的,所以在不同的編譯器間是不可移植

  • 前處理器忽略它不認識的#pragma指令
  • 不同的編譯器可能以不同的方式解釋同一條#pragma指令
  • 一般用法:#pragma parameter ,不同的parameter引數語法和意義各不相同。

1、#pragma message

  • message引數在大多數的編譯器中都有相似的實現
  • message引數在編譯時輸出訊息到編譯輸出視窗中
  • message用於條件編譯中可提示程式碼的版本資訊

    #include <stdio.h>
    
    #if defined(ANDROID20)
      #pragma message("Compile Android SDK 2.0...")
      #define VERSION "ANDROID 2.0"
    
    #elif defined(ANDROID30)
      #pragma message("Compile Android SDK 3.0...")
      #define VERSION "Android 3.0"
    
    #elif defined(ANDROID40)
      #pragma message("Compile Android SDK 4.0...")
      #define VERSION "Amdroid 4.0"
    
    #else 
      #error Compile version is not provided!
     
    #endif
    
    int main()
    {
      printf("%s\n", VERSION);
    
      return 0;
    }
    不同編譯器的編譯結果:
      bcc編譯器:bcc32 -DANDROID30 test.c
      編譯輸出:Compile Android SDK 3.0...
    
      vc編譯器:cl -DANDROID30 test.c
      編譯輸出:Compile Android SDK 3.0...
    
      gcc編譯器:gcc -DANDROID30 test.c
      編譯輸出:note: #pragma message: Compile Android SDK 4.0...
    
    三款編譯器的輸出說明:#pragma message會輸出提示資訊,但是行為相似,可能具體實現不同
    單步編譯的中間檔案: gcc -DANDROID30 -E test.c -o test.i
    # 12 "test.c"
    #pragma message("Compile Android SDK 3.0...")
    # 12 "test.c"
    # 20 "test.c"
    int main()
    {
      printf("%s\n", "Android 3.0");
    
      return 0;
    }

    注意:和#error、#warning不同,#pragma message僅僅代表一條編譯資訊,不代表程式錯誤。

2、#pragmae once

  • #pragma once用於保證標頭檔案只被編譯一次
  • #pragma once編譯器相關的,不一定被支援

#ifndef...#define...#endif也可以用來防止標頭檔案被重複包含,和#pragma once有何不同?

  • #ifndef方式是C語言支援的,用在各個編譯器都可以;如果一個.h被include了n次預編譯器就會判斷n次這個標頭檔案是否已經包含了。
  • #pragma once方式是編譯器相關的,不一定所有編譯器都支援;預編譯器只會處理一次,後面不再判斷。

    // test.h
    #pragma once
    int aa = 1;
    
    // test.c
    #include <stdio.h>
    #include "test.h"
    #include "test.h"
    
    int main()
    {
      return 0;
    }
    不同編譯器的編譯結果:
      bcc編譯器:bcc32 test.c
      編譯輸出:Variable 'aa' is initialized more than once
    
      vc編譯器:cl test.c
      編譯輸出:成功
    
      gcc編譯器:gcc test.c
      編譯輸出:成功
    
    分析:#pragma once預處理指示字,vc和gcc都可以識別,但是bcc就無法識別,直接忽略了,然後aa就會被定義2次,造成編譯錯誤

由於#ifndef會被判斷n次,但是#pragma once又不是所有編譯器都支援的,為了提高效率,可以這兩種方式同時使用:

#ifndef _TEST_H_
#define _TEST_H_

#pragma once

// code

#endif

3、#pragma pack和記憶體對齊

3.1 什麼是記憶體對齊?

  • 不同型別的資料在記憶體中按照一定的規則排列不一定是順序的一個接一個的排列

3.2 為什麼需要記憶體對齊?

  • CPU對記憶體的讀取不是連續的,而是分成塊讀取的,塊的大小隻能是2的冪,1、2、4、8...位元組
  • 當讀取操作的資料未對齊,則需要兩次匯流排週期來訪問記憶體,因此效能會大打折扣
  • 某些硬體平臺只能從規定的相對地址處讀取特定型別的資料,否則會產生硬體異常

3.3 #pragma pack

編譯器預設的對齊方式為4位元組對齊
#pragama pack可以調整編譯器的預設對齊方式

  • #include <stdio.h>
    
    #pragma pack(1)
    struct Test1
    {
      char c1;
      short s;
      char c2;
      int i;
    };
    #pragma pack()
    
    #pragma pack(1)
    struct Test2
    {
      char c1;
      char c2;
      short s;
      int i;
    };
    #pragma pack()
    
    int main()
    {
      printf("sizeof(Test1) = %d\n", sizeof(struct Test1));
      
      printf("sizeof(Test2) = %d\n", sizeof(struct Test2));   
      
      return 0;
    }

    相同的兩個struct,在調整了記憶體對齊方式後,Test1佔用的記憶體大小發生了變化。

3.3 struct記憶體對齊的規則:

  • 第一個成員起始於0偏移處
  • 每個成員按照對齊引數進行對齊(型別大小pack引數中較小的一個)

    • 偏移地址必須能被對齊引數 整除(上一個成員的offset+size位置,偏移地址 / 對齊引數 == 0)
    • 結構體型別成員的型別大小取其內部長度最大的資料成員作為其大小
  • struct的總長度必須為所有對齊引數的整數倍
#include <stdio.h>

#pragma pack(8)
struct Test1
{
    short a;
    long b;
};

struct Test2
{
    char c;
    struct Test1 st;
    double d;
};
#pragma pack()

int main()
{
    printf("sizeof(Test1) = %d\n", sizeof(struct Test1));
    
    printf("sizeof(Test2) = %d\n", sizeof(struct Test2));   
    
    return 0;
}
不同編譯器下的輸出:
vc:8 和 24
gcc:8 和 20
bcc:8 和 24

分析:vc和bcc的輸出結果符合我們的演算法,為什麼gcc不一樣的結果呢?
答案:#pragma定義的很多編譯器指示字是特有的,不同編譯器間不可移植gcc編譯器不支援8位元組對齊,所以gcc編譯器看到'#pragma pack(8)'就直接刪掉、忽略了,仍然按照4位元組對齊,得到結果為20:

struct Test2
{                            pack    offset     memberSize
    char c;                    1        0        1
    struct Test1 st;           4        4        8
    double d;                  4        12       8
}                                                    總大小:20

本文總結自“狄泰軟體學院”唐佐林老師《C語言進階課程》。
如有錯漏之處,懇請指正。

相關文章