1

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语言进阶课程》。
如有错漏之处,恳请指正。


bryson
169 声望12 粉丝