预处理指令

注意:#开头的指示字,都是用于预处理器的

#error

#error 用于生成一个编译错误消息

用法

1
#error message

message 不需要用双引号包围

#error 编译指示字用于自定义程序员特有的编译错误消息

类似的,#warning 用于生成编译警告

#error 是一种预编译器的指示字

#error 可用于提示编译条件是否满足

1
2
3
#ifndef __cplusplus
#error This file should be processed with C++ compiler.
#endif

编译过程中的任意错误信息意味着无法生成最终的可执行程序;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

#ifndef __cplusplus
#error This file should be processed with C++ compiler.
#endif

class CppClass {
private:
int m_value;
public:
CppClass() {

}
~CppClass() {

}
};

int main() {
return 0;
}

在 C 文件中编译 C++ 的代码肯定是会报错的此时如果使用 C++ 编译器进行编译是没有问题的

g++ test.c // ok

但是如果我们此时使用 C 编译器进行编译则会出现问题:

test/demo.c:4:10: error: #error This file should be processed with C++ compiler.
4 | #error This file should be processed with C++ compiler.
| ^~~~~
test/demo.c:7:1: error: unknown type name ‘class’
7 | class CppClass {
| ^~~~~
test/demo.c:7:16: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
7 | class CppClass {
|

上述可以看出他输出了我们自定义的宏错误,程序会判断 __cplusplus 这个 C++ 关键字是否存在如果不存在则会执行里面的代码并中止程序的执行;

#line

#line 用于强制指定新的行号和编译文件名,并对源程序的代码重新编号

用法

1
2
3
4
5
#line number fileName
// fileName 可省略
// #line 编译指示字的本质就是重定义 __LINE__ 和 __FILE__
// __LINE__:当前编译的行号
// __FILE__:当前编译的文件名

示例

1
2
3
4
5
6
7
8
9
// demo.c
#include <stdio.h>

#line 1 "kay.c"

int main() {
printf("Hello world\n")
return 0;
}

上述代码运行后会产生错误信息:

kay.c: In function ‘main’:
kay.c:3:28: error: expected ‘;’ before ‘return’

此时我们看到,他的提示信息改变了,文件名变成了我们输出的文件名,提示的行号也从第 6 行变成了第 3 行;

他会从 #line 1 “kay.c” 下开始计算为第一行

#pragma

#pragma指示使每个编译程序在保留C和C++语言的整体兼容性时提供不同机器和操作系统特定的功能。一般是由编译器厂家在使用的;

  • #pragma 用于指示编译器完成一些特定的动作
  • #pragma 所定义的很多指示字是编译器特有的
  • #pragma 在不同的编译器间是不可移植的
    • 预处理器将忽略它不认识的 #pragma 指令
    • 不同的编译器可能以不同的方式解释同一条 #pragma 指令

#pragma 是 C 和 C++ 中的一种预处理指令,用于向编译器提供特定的指示。这些指示通常与编译器的实现相关,可以控制编译器的行为、优化选项或警告设置。他是编译器所制定的,而类似 #messge 是类似 C 语言的语法;

用法

1
2
// 一般用法
#pragma parameter

注:不同的 parameter 参数语法和意义各不相同

示例

  • message 参数在大多数的编译器中都有相似的实现
  • message 参数在编译时输出消息到编译输出窗口中
  • message 用于条件编译中可提示代码的版本信息
1
2
3
4
#if defined(ADNROID20)
#pragma message("Compiler Android SDK 2.0...")
#define VERSION "Android 2.0"
#endif

与 #error 和 #waring 不同,#pragma meaasge 仅仅代表一条编译消息,不代表程序错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

#if defined(ANDROID20)
#pragma message("Compiler Android SDK 2.0...")
#define VERSION "Android 2.0"
#elif defined(ANDROID30)
#pragma message("Compiler Android SDK 3.0...")
#define VERSION "Android 3.0"
#elif defined(ANDROID40)
#pragma message("Compiler Android SDK 4.0...")
#define VERSION "Android 4.0"
#else
#error Complier Version is not provided!
#endif

int main() {
printf("%s\n", VERSION);
return 0;
}

编译: gcc -DANDROID40 demo.c,他会输出提示信息:

demo.c:10:17: note: ‘#pragma message: Compiler Android SDK 4.0…’
10 | #pragma message(“Compiler Android SDK 4.0…”)
|

运行: ./a.out

Android 4.0

#pragma 后面跟着的参数也许在每个编译器都有实现,但是可能实现的方式是不同的

#pragma once

  • #pragma once 用于保证头文件只被编译一次
  • #pragma once 是编译器相关的,不一定被支持

Ctouwenjianduibi

#ifndef 宏保护:编译器需要检查每个包含的宏定义,因此在某些情况下可能效率稍低。他要去判断检查这个宏是否被定义,没有就展开,被定义了就不执行中间的内容

#pragma once:编译器可以更高效地处理它,因为它只需跟踪文件的路径,而不需要处理宏定义的条件。他是记住头文件的路径或者文件名,下次遇到就自动跳过,减少了判断时的时间消耗。

二者可以结合使用

#pragma pack

什么是内存对齐

  • 不同的数据类型在内存中按照一定的规则排列
  • 不一定是顺序的一个接着一个排列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

struct Test1 {
char c1; // 1B
short s; // 2B
char c2; // 1B
int i; // 4B
};

struct Test2 {
char c1; // 1B
char c2; // 1B
short s; // 2B
int i; // 4B
};
// Tset1 和 Test2 所占的内存是否相同?

int main() {

printf("sizeof(struct Test1 = %ld\n", sizeof(struct Test1));
printf("sizeof(struct Test2 = %ld\n", sizeof(struct Test2));
return 0;
}

// sizeof(struct Test1 = 12
// sizeof(struct Test2 = 8

为什么需要内存对齐?

  • CPU 对内存的读取不是连续的,而是分块读取的,块的大小只能是1,2,4,8,16,…个字节
  • 当读取操作的数据未对齐,则需要两次总线周期来访问内存,因为性能会大打折扣
  • 某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则产生硬件异常

#pragma pack 用于指定内存对齐的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

pragma pack(1)
struct Test1 {
char c1; // 1B
short s; // 2B
char c2; // 1B
int i; // 4B
};
pragma pack()

pragma pack(1)
struct Test2 {
char c1; // 1B
char c2; // 1B
short s; // 2B
int i; // 4B
};
pragma pack()
// Tset1 和 Test2 所占的内存是否相同?

int main() {

printf("sizeof(struct Test1 = %ld\n", sizeof(struct Test1));
printf("sizeof(struct Test2 = %ld\n", sizeof(struct Test2));
return 0;
}
// sizeof(struct Test1 = 8
// sizeof(struct Test2 = 8

#pragma pack 指定内存对齐的方式为:

struct 占用的内存大小

  • 第一个成员起始于0 偏移处
  • 每个成员按其大小和 pack 参数中较小的一个进行对齐
    • 偏移地址必须能被对齐参数(其大小和 pack 中较小的一个)整除
    • 结构体成员的大小取其内部长度最大的数据成员作为其对齐大小
  • 结构体总长度必须为所有对其参数的整数倍

编译器在 32 位系统下默认情况下按照 4 字节大小对齐(也就是默认情况下是#pragma pack(4))

编译器在 64 位系统下默认情况下按照 8 字节大小对齐(也就是默认情况下是#pragma pack(8))

上面的 12B 和 8B 分别是如何计算的?

Tset1:

c1 占一个字节 pack 是四个字节,按照 1 个字节去对齐,存入 0 下标位置

s 占两个字节,pack 是四个字节,按照 2 个字节去对其,存入下标为 2 的整数倍的最小地址处,也就是下标 2 处

c2 占 1 个字节,pack是四个字节,按照 1 个字节去对其,存入下标为 1 的整数倍的最小地址处的地址,也就是下标为 4 处

i 占4 个字节,pack也是四个字节,按照 4 个字节对齐,存入下标为 4 的整数倍的最小地址处地址处,也就是下标为 8处,

综上占了 12 个字节正好是 1 2 4 的整数倍,所以一共占了 12 个字节。

Cneicunduiqi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

#pragma pack(8)
struct Test1 {
char c1; // 1B
short s; // 2B
char c2; // 1B
int i; // 4B
};
#pragma pack()

#pragma pack(8)
struct Test2 {
char c; // 1B
struct Test1 s; // 2B 12
double e;
};
#pragma pack()
// Tset1 和 Test2 所占的内存是否相同?

int main() {

printf("sizeof(struct Test1 = %ld\n", sizeof(struct Test1)); // 12
printf("sizeof(struct Test2 = %ld\n", sizeof(struct Test2)); // 24
return 0;
}
// c 占下标 0处,大小为 0
// s 结构体成员的大小取其内部长度最大的数据成员作为其对齐大小 也就是取 4 作为对齐大小,大小为 12 个字节 4~15
// e 占 8 个字节取下标为 16 处 那么一共占 24 个字节

总结

  • #error 用于自定义一条编译错误信息
  • #warning 用于自定义一条编译警告信息
  • #rror 和 #warning 常用于条件编译的情形
  • #line 用于强制指定新的行号编译文件名
  • #pragma 用于指示编译器完成一些特定动作
  • #pragma 所定义的很多指示字是编译器特有的
    • #pragma message 用于自定义编译信息
    • #pragma once 用于保证头文件只被编译一次
    • #pragma pack 用于指定内存对齐方式