Skip to content

LV005-预处理简介

一、预处理的概念

预处理就是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所做的工作,它由预处理程序负责完成。当编译一个程序时,系统将自动调用预处理程序对程序中的 # 开头的预处理部分进行处理,处理完毕之后才会进入程序的编译阶段。

C 语言为我们提供了多种预处理功能,如宏定义,文件包含和条件编译等。

二、预定义

1. 宏

在 C 语言源程序中,允许使用一个标识符来表示一串符号,称之为 ,被定义为宏的标识符称为 宏名。在编译预处理的时候,对程序中出现的所有宏名,都会用宏定义中的符号串去代替,这被称为 宏替换 或者叫 宏展开

2. 预定义符号串

2.1 有哪些?

在 C 语言中,有一些预定义的符号串,它们的值是字符串常量或者是十进制数字常量,通常是用于在调试程序时输出源程序的各项信息。

符号 常量类型 含义
__FILE__ 字符串 正在预编译编译的文件名(字符串字面值)
__LINE__ 整数 文件的当前行号(十进制常量)
__DATE__ 字符串 文件被编译的日期
__TIME__ 字符串 文件被编译的时间
__STDC__ 整数 如果编译器遵循 ANSI C,其值为 1,否则未定义
__FUNCTION__ 字符串 表示调用此预定义的函数名称
__func__ 字符串 表示调用此预定义的函数名称

2.2 使用实例

c
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("The __FILE__ is : %s\n", __FILE__);
    printf("The __LINE__ is : %d\n", __LINE__);
    printf("The __DATE__ is : %s\n", __DATE__);
    printf("The __TIME__ is : %s\n", __TIME__);
    printf("The __STDC__ is : %d\n", __STDC__);
    printf("The __FUNCTION__ is : %s\n", __FUNCTION__);
    printf("The __func__ is : %s\n", __func__);
	
    return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

然后我们会看到有如下信息输出:

shell
The __FILE__ is : test.c
The __LINE__ is : 6
The __DATE__ is : Mar 23 2022
The __TIME__ is : 09:31:59
The __STDC__ is : 1
The __FUNCTION__ is : main
The __func__ is : main

3. 宏定义

除了预定义的符号外,我们也可以自己定义宏,宏定义就是 用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就 全部替换 成指定的字符串。

注意

(1)这里的字符串字可以是常数、表达式、格式串等。不仅仅是代表“字符串”。

(2)如果说,“字符串”是一个含参的表达式,一定要注意,先替换,再运算。

3.1 无参宏定义

3.1.1 一般格式

无参宏定义的宏名,也就是标识符,后边不带参数,定义的一般形式是:

c
#define 标识符 字符串

各部分说明如下:

# 表示这是一条预处理命令
define 宏定义命令
标识符 所定义的宏名(习惯上用全大写表示,但是也允许小写,看个人喜好喽),后边在程序中使用的宏都是直接使用宏名
字符串 可以是常数,表达式,格式串等
【说明】 字符串 与 (字符串) 似乎没有区别。

(1)宏定义用宏名来表示一串符号,在宏展开的时候又以该符号串取代宏名,这只是一种简单的替换,符号串中可以包含任何字符,可以是常数,也可以是表达式,预处理程序不对它做任何检查。如果有错误的话,只能在编译已被宏展开后的源程序时被发现。

(2)宏定义不是声明或者语句,行尾不必加分号( ; ),如果加上分号( ; )了,就会连分号( ; )一起替换。不过我在测试的时候是直接报错了,这里注意一下就好啦。

(3)宏定义的作用域包括从宏定义命名起到源程序结束,如果要终止其作用域,我们可以使用 #undef 来取消宏作用域。

(4)宏名引用时不要写在 " " 中,否则预处理程序不会对其进行替换。

(5)宏定义允许嵌套,在宏定义的符号串中就可以使用已经定义过的宏名,在宏展开时由预处理程序进行层层替换。

(6)可以对输出做一个宏定义,以减少编写麻烦。但是这样格式就不能自己想怎样就怎样了,不过还是要看自己需求啦。

3.1.2 使用实例 1
c
#include <stdio.h>

#define pi 3.1415926
#define str "Hello,world!"
#define y (x + 3)
int main(int argc, char *argv[])
{
    int sum = 0;
    int x = 0;

    sum = 5 * y;
    printf("The pi is : %f\n", pi);
    printf("The str is : %s\n", str);
    printf("The y is : %d\n", y);
    printf("The sum is : %d\n", sum);

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
The pi is : 3.141593
The str is : Hello,world!
The y is : 3
The sum is : 15
3.1.3 使用实例 2
c
#include <stdio.h>

#define pi 3.1415926

void fun1()
{
    printf("The pi is : %f\n", pi);
}
#undef pi
void fun2()
{
    printf("The pi is : %f\n", pi);
}

int main(int argc, char *argv[])
{

    fun1();
    fun2();
    
	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

其中 pi 只会在 fun1() 中生效,会直接在 fun2() 定义时报错,会看到有如下信息输出:

shell
test.c: In function ‘fun2’:
test.c:12:32: error: ‘pi’ undeclared (first use in this function)
     printf("The pi is : %f\n", pi);
                                ^~
test.c:12:32: note: each undeclared identifier is reported only once for each function it appears in
3.1.4 使用实例 3
c
#include <stdio.h>

#define P printf
#define D "%d\n"
#define F "%f\n"

int main(int argc, char *argv[])
{
    int a = 5;
    float b = 3.1415926;

    P(D F, a, b);
    
	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
5
3.141593

3.2 含参宏定义

3.2.1 一般格式

含参宏定义的宏名,也就是标识符,后边带参数,这个参数称为形参,调用的时候不仅要进行宏展开,还要传入实参。定义的一般形式是:

c
#define 标识符(形参表) 字符串

各部分说明如下:

# 表示这是一条预处理命令
define 宏定义命令
标识符 所定义的宏名(习惯上用全大写表示,但是也允许小写,看个人喜好喽),后边在程序中使用的宏都是直接使用宏名
(形参表) 形参的列表,可以有多个形参,用逗号 "," 分隔,形参最好用 () 括起来,以免出错
字符串 可以是常数,表达式,格式串等
【说明】字符串 与 (字符串) 似乎没有区别,但是在含参的宏中最好用 () 括起来,减小出错的概率。

调用的一般形式是:

c
标识符(实参表);

注意

(1)定义的时候不要带分号( ; ),调用的时候就是语句了,这就需要带上分号( ; )。

(2)注意 先替换,再运算

(3)含参宏定义中,宏名和形参表之间不可以有空格。

c
#define MAX (a, b) (a > b)?a:b

这种的在处理的时候直接报错了,其实按理来说,这句相当于是一个无参宏定义,宏名为 MAX ,它代表 (a, b) (a > b)?a: b 。

(4)在含参的宏定义中,形参是不会被分配内存的,所以不必做类型的定义,这是与函数不同的。在含参宏定义中,这只是 符号的替换,不存在值的传递

(5)宏定义的形参相当于一个标识符,宏调用的实参可以是一个表达式。

3.2.2 使用实例 1
c
#include <stdio.h>

#define MAX(a, b) (a > b)?a:b

int main(int argc, char *argv[])
{
    int a = 3;
    int b = 5;

    printf("MAX(a, b) is : %d\n", MAX(a, b));

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
MAX(a, b) is : 5
3.2.3 使用实例 2
c
#include <stdio.h>

#define MAX(a, b) ((a > b)?a:b)

int main(int argc, char *argv[])
{
    int a = 3;
    int b = 5;

    printf("MAX(a, b) is : %d\n", MAX(a + 3, b - 1));

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
MAX(a, b) is : 6

3.3 宏与函数

含参的宏与函数有着很类似的形式,但是他们却有着很大的不同之处:

属性 函数
处理阶段 预处理阶段,只是符号串的简单的替换 编译阶段
代码长度 每次使用宏时,宏代码都被插入到程序中。因此,除了非常小的宏之外,程序的长度都将被大幅增长 除了 inline 函数之外,函数代码只出现在一个地方,每次使用这个函数,都只调用那个地方的同一份代码
执行速度 更快 存在函数调用/返回的额外开销(inline 函数除外)
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境中,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果 函数参数只在函数调用时求值一次,它的结果值传递给函数,因此,表达式的求值结果更容易预测
参数求值 参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果 参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多种求值问题,参数的副作用不会造成任何特殊的问题
参数类型 宏与类型无关,只要对参数的操作是合法的,它可以使用任何参数类型 函数的参数与类型有关,如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的

3.4 ###

3.4.1 # 的用途

# 的功能是将其后面的宏参数进行字符串化操作( Stringfication ),简单说就是在对它所引用的宏变量,在预编译完成替换后同时在其左右各加上一个双引号。

例如,下边的测试程序:

c
#include <stdio.h>

#define STR(s) #s

int main(int arc, char *argv[])
{
	char a[10] = STR(mine);
    int b[10] =  STR(5); // 这里暂不管语法对不对只考虑到预处理
	return 0;
}

然后我们在终端中输入以下命令进行预编译:

shell
gcc -E test.c -o test.i # 预编译
vim test.i

然后在文件的结束,我们会看到如下几行:

c
# 前边的省略 ... ...
# 5 "test.c"
int main(int arc, char *argv[])
{
 char a[10] = "mine";
 int b[10] = "5";
 return 0;
}

我们会发现, mine 被替换为 "mine" 了,下面的 5 也变成了 "5"。

3.4.2 ## 的用途

后边学习过程中,遇到了一个定义:

c
#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

当时看的我一脸懵逼,查阅资料后,了解到,在宏定义中, ## 也就是两个 # 连用,称为连接符,主要是用于连接两个参数, ## 符会把传递过来的参数当成字符串进行替代。例如,下边的测试程序:

c
#include <stdio.h>

#define mine(prefix) int prefix##family
#define CONS(a,b) int(a##e##b)

int main(int arc, char *argv[])
{
	mine(x_);
	CONS(A, B);
	return 0;
}

然后我们在终端中输入以下命令进行预编译:

shell
gcc -E test.c -o test.i # 预编译
vim test.i

然后在文件的结束,我们会看到如下几行:

c
# 前边的省略 ... ...
# 6 "test.c"
int main(int arc, char *argv[])
{
 int x_family;
 int(AeB);
 return 0;
}

可以看到,我们传入的参数 x_在预编译后,与 fanily 连接起来了,变成了 x_family ,然后 a##e##b 变成了 AeB 。

三、文件包含

文件包含是 C 语言预处理的另一种方式,文件包含的一般形式是:

c
#include <filename>
/* 或者 */
#include "filename"

有木有觉得很熟悉呢,这经常用于引入对应的头文件( .h 文件)。文件包含的处理过程很简单,就是 将头文件的内容插入到该语句所在行的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

Tips:使用尖括号 < > 和双引号 " " 的区别在于 头文件的搜索路径 不同:

  • 使用尖括号 < > ,编译器会到 系统路径 下查找头文件;
  • 使用双引号 " " ,编译器首先在 当前目录 下查找头文件,如果没有找到,再到 系统路径 下查找。

像 stdio.h 和 stdlib.h 这些都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。

注意

(1)一个 include 命令只能指定一个被包含的文件,若有多个文件要包含,则需要用多个 include 命令。而且文件的包含允许嵌套,在一个被包含的文件中还可以包含别的文件。

(2)在使用我们自己编写的头文件时,也可以在 include 中直接指明路径,例如,

c
#include "./include/test.h"

(3)同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有 防止重复引入 的机制。

四、条件编译

当我们写的源代码是跨平台的,而代码每次只会在一个平台上运行,我们就全部编译所有代码吗?那样多少有些浪费。

其实 C 语言为我们提供了条件编译功能,以便于我们只编译需要的部分。所谓 条件编译,就是能够根据不同情况编译不同代码、产生不同目标文件的机制。条件编译的关键字为 #if 、 #ifdef 和 #ifndef 。

1. #if

1.1 使用格式

使用的时候,可以有三种格式:

1.1.1 #if
c
/* 1.只有 #if */
#if 常量表达式
	语句块;
#endif

【说明】如果常量表达式为真( 1 ),则编译语句块,若常量表达式为假( 0 ),则不做处理。

1.1.2 #if...#else
c
/* 2. #if ... #else... */
#if 常量表达式
	语句块1;
#else
	语句块2;
#endif

【说明】如果常量表达式为真( 1 ),则编译语句块 1 ,若常量表达式为假( 0 ),则编译语句块 2 。

1.1.3 #if...#elif
c
/* 3. #if ... #elif... */
#if 常量表达式1
	语句块1;
#elif 常量表达式2
	语句块2;
#else 常量表达式3
	语句块3;
#endif

【说明】如果 常量表达式 1 的值为真(非 0 ,也就是 1 ),就对 语句块 1 进行编译;否则计算 常量表达式表达式 2 ,结果为真就对 语句块 2 进行编译;若为假就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else 。

1.2 使用实例

c
#include <stdio.h>

#define N 1

int main(int argc, char *argv[])
{
    #if N == 0
        printf("#if N==0\n");
    #elif N == 1
        printf("#elif N==1\n");
    #else
        printf("#else\n");
    #endif

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
#elif N == 1

【注意】 #if 命令要求判断条件为整型常量表达式,也就是说,表达式中不能包含变量,而且结果必须是整数。这是与 if 不同的地方。例如,若表达式结果为 1.3 ,将会报以下错误:

shell
error: floating constant in preprocessor expression

2. #ifdef

2.1 使用格式

使用的时候,常见的有两种格式,如下所示:

2.1.1 #ifdef
c
/* 1.只有 #ifdef */
#define macro
#ifdef macro
	语句块;
#endif

【说明】如果宏 macro 定义了,则编译语句块,若宏 macro 未定义,则不做处理。

2.1.2 #ifdef...#else
c
/* 2. #ifdef ... #else... */
#define macro
#ifdef macro
	语句块1;
#else
	语句块2;
#endif

【说明】如果宏 macro 定义了,则编译语句块 1 ,若宏 macro 未定义,则编译语句块 2 。

2.2 使用实例

c
#include <stdio.h>

#define DEBUG1
#define DEBUG2 0
int main(int argc, char *argv[])
{
    #ifdef DEBUG1
        printf("DEBUG1 is define\n");
    #else
        printf("DEBUG1 is not define\n");
    #endif
    
    #ifdef DEBUG2
        printf("DEBUG2 is define\n");
    #else
        printf("DEBUG2 is not define\n");
    #endif

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
DEBUG1 is define
DEBUG2 is define

【注意】这种情况定义宏的话,可以有字符串替换宏名,也可以没有。

3. #ifndef

3.1 使用格式

使用的时候,一般格式如下:

3.1.1 #ifndef
c
/* 1.只有 #ifndef */
#ifndef macro
	语句块;
#endif

【说明】如果宏 macro 没有被定义,则编译语句块,若宏 macro 被定义,则不做处理。

3.1.2 #ifndef...#else
c
/* 2. #ifndef ... #else... */
#ifndef macro
	语句块1;
#else
	语句块2;
#endif

【说明】如果宏 macro 没有被定义,则编译语句块 1 ,若宏 macro 被定义,则编译语句块 2 。

3.2 使用实例

c
#include <stdio.h>

#define DEBUG1
int main(int argc, char *argv[])
{
    #ifndef DEBUG1
        printf("DEBUG1 is not define\n");
    #else
        printf("DEBUG1 is define\n");
    #endif
    
    #ifndef DEBUG2
        printf("DEBUG2 is not define\n");
    #else
        printf("DEBUG2 is define\n");
    #endif

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 编译程序 
./a.out          # 执行可执行文件

会看到有如下信息输出:

shell
EBUG1 is define
DEBUG2 is not define

【注意】这种情况定义宏的话,可以有字符串替换宏名,也可以没有。

五、 #pragma

4.1 简介

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

# pragma 指令应该是预处理指令中最复杂的,其用法很多。

4.2 message

4.2.1 语法格式

c
#pragma message("string")

该参数可以在编译信息输出窗口中输出相应的信息。

4.2.2 使用实例

c
#include <stdio.h>

#pragma message("This is pragma message!")
int main(int argc, char *argv[])
{
	printf("hello World!\n");
	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall

然后,终端会有以下信息显示:

shell
test.c:3:9: note: ‘#pragma message: This is pragma message!’
    3 | #pragma message("This is pragma message!")
      |         ^~~~~~~

4.3 once

4.3.1 语法格式

c
#pragma once

该参数用于保证头文件只被编译一次,它 与编译器相关,不一定被编译器所支持。还记得之前我们定义头文件的时候使用的是以下形式

c
#ifndef __HEAD_FILENAME_H__
#define __HEAD_FILENAME_H__

 /* code */

#endif

它与使用 #pragma once 的区别在于前者是 C 语言所支持的,并不是只包含一次头文件,而是会包含多次,然后通过宏控制是否嵌入到源代码中,也就是说通过宏的方式,可以保证头文件里面的内容只被嵌入一次,但是由于包含了多次,预处理器还是处理了多次,所以效率上来说比较低;后者是告诉预处理器当前文件只编译一次,所以说效率较高。

如果说既想要保证移植性,又想要保证效率,我们可以两种方式同时使用:

c
#ifndef __HEAD_FILENAME_H__
#define __HEAD_FILENAME_H__
#pragma once
 /* code */

#endif

4.3.2 使用实例

  • test.c
c
#include <stdio.h>
#include "global.h"
#include "global.h"
int main(int argc, char *argv[])
{
	printf("a=%d\n", a);
	return 0;
}
  • global.h
c
#pragma once
int a = 10;
  • 编译测试

在终端执行以下命令编译程序:

shell
gcc test.c -Wall

然后若是我们没有在 global.h 中添加 #pragma once 的话,会有以下信息产生:

shell
In file included from test.c:3:
global.h:1:5: error: redefinition of ‘a’
    1 | int a = 10;
      |     ^
In file included from test.c:2:
global.h:1:5: note: previous definition of ‘a’ was here
    1 | int a = 10;
      |     ^

我们发现报错了, a 出现了重复定义,我们在 global.h 中加上 #pragma once 之后,便不会再有报错。我们在终端执行 ./a, out 会有以下信息显示:

shell
a=10

4.4 pack

后边在自定义数据类型的地方还会用到这个参数。

4.4.1 内存对齐

什么是内存对齐

我使用的 64 位 Ubuntu 中, int 类型占 4 字节, char 类型占 1 字节,当他们出现在一个结构体(后边会学习到)中应该是 5 字节,但是实际上却会是 8 字节,这就是内存对齐导致的。

c
#include <stdio.h>
struct
{
	char a;
	int b;
} S1;
int main(int argc, char *argv[])
{
	printf("sizeof(S1)=%ld, sizeof(S1.a)=%ld, sizeof(S1.b)=%ld\n", sizeof(S1), sizeof(S1.a), sizeof(S1.b));
	return 0;
}

在终端执行以下命令编译程序:

shell
gcc test.c -Wall
./a.out

然后,终端会有以下信息显示:

shell
sizeof(S1)=8, sizeof(S1.a)=1, sizeof(S1.b)=4

为什么要内存对齐

内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的。它一般会以 2 、 4 、 6 、 8 甚至 32 字节为单位来存取内存.现在以每次存取 4 字节的处理器为例分析,取 int 类型变量( 64 位系统),该处理器只能从地址为 4 的倍数的内存开始读取数据。

假如没有内存对齐机制,数据可以任意存放,现在一个 int 变量存放在从地址 1 开始的连续 4 个字节字节地址中,该处理器去取数据时,要先从 0 地址开始读取第一个 4 字节块,并剔除不想要的字节( 0 地址), 然后从地址 4 开始读取下一个 4 字节块,同样需要删除不要的数据(也就是 5 , 6 , 7 地址处的数据), 最后留下的 2 块数据便是我们存放的 int 类型数据,这就意味着处理器要进行大量的处理,存取数据效率将会很低。

现在有了内存对齐, int 类型数据只能存放在按照对齐规则的内存中,比如说 0 地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。内存对齐在结构体和联合体的大小计算中会得到很好的体现。

对齐规则

编译器都有自己的默认“对齐系数”(也叫对齐模数)。 gcc 中默认 #pragma pack(4) ,可以通过预编译命令 #pragma pack(n) , n = 1,2,4,8,16 来改变这一系数。

给定值 #pragma pack(n) 和结构体中最长数据类型长度中较小的那个被称之为有效对齐值,也叫 对齐单位

4.4.2 语法格式

c
#pragma pack(n)      /* 设置编辑器按照 n 个字节对齐,n 可以取值 1,2,4,8,16 */
#pragma pack()       /* 取消自定义字节对齐方式。 */

该参数可以改变编译器默认的字节对齐方式。

4.4.2 使用实例

c
#include <stdio.h>
#pragma pack(1) /* 字节对齐改成了1个字节 */

/* 定义结构体数据类型 */
struct Student
{
	char *name;      /* 姓名 */
	char gender;     /* 性别 */
	int age;         /* 年龄 */
	char id[3];     /* 学号 */
	float score;     /* 成绩 */
};                   /* ; 不可缺少 */
#pragma pack() /* 取消自定义字节对齐 */
struct Test
{
	char *name;      /* 姓名 */
	char gender;     /* 性别 */
	int age;         /* 年龄 */
	char id[3];     /* 学号 */
	float score;     /* 成绩 */
};                   /* ; 不可缺少 */


int main(int argc, char *argv[])
{
	struct Student stu1 = {"qidaink", 'm', 18, "01", 95.8}; /* 定义结构体变量 */

	printf("sizeof(stu1) = %ld\n", sizeof(stu1));
	printf("sizeof( struct Student) = %ld\n", sizeof(struct Student));
	printf("sizeof(stu1.name) = %ld\n", sizeof(stu1.name));
	printf("sizeof(stu1.gender) = %ld\n", sizeof(stu1.gender));
	printf("sizeof(stu1.age) = %ld\n", sizeof(stu1.age));
	printf("sizeof(stu1.id) = %ld\n", sizeof(stu1.id));
	printf("sizeof(stu1.score) = %.ld\n\n", sizeof(stu1.score));


	struct Test stu2 = {"qidaink", 'm', 18, "01", 95.8}; /* 定义结构体变量 */
	printf("sizeof(stu2) = %ld\n", sizeof(stu2));
	printf("sizeof( struct Student) = %ld\n", sizeof(struct Student));
	printf("sizeof(stu2.name) = %ld\n", sizeof(stu2.name));
	printf("sizeof(stu2.gender) = %ld\n", sizeof(stu2.gender));
	printf("sizeof(stu2.age) = %ld\n", sizeof(stu2.age));
	printf("sizeof(stu2.id) = %ld\n", sizeof(stu2.id));
	printf("sizeof(stu2.score) = %.ld\n\n", sizeof(stu2.score));

	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall
./a.out

然后,终端会有以下信息显示:

shell
sizeof(stu1) = 20
sizeof( struct Student) = 20
sizeof(stu1.name) = 8
sizeof(stu1.gender) = 1
sizeof(stu1.age) = 4
sizeof(stu1.id) = 3
sizeof(stu1.score) = 4

sizeof(stu2) = 24
sizeof( struct Student) = 20
sizeof(stu2.name) = 8
sizeof(stu2.gender) = 1
sizeof(stu2.age) = 4
sizeof(stu2.id) = 3
sizeof(stu2.score) = 4

六、 #error

1. 使用格式

#error 也是一个预处理命令,当编译器遇到 #error 的时候将停止编译,并输出自定义的消息,一般使用格式如下:

c
#error [自定义的错误消息]

其中 [] 中的内容是可选的,也可以不输出提示信息。我们可以使用该预处理指令来停止编译,保证程序是按照我们所设想的那样进行编译的,以免产生不可预料的后果。

注意

(1)自定义的错误消息不需要加引号 " " ,如果加上的话,引号会被一起输出。

(2)当程序比较大时,往往有些宏定义是在外部指定的(如 makefile ),或是在系统头文件中指定的。

2. 使用实例

c
#include <stdio.h>

#define MACRO 1
#if MACRO == 1
#error MACRO=1
#endif
int main(int argc, char *argv[])
{
	printf("hello world!\n");
	return 0;
}

当我们编译的时候,会有如下提示:

shell
gcc main.c -Wall -o main
main.c:5:2: error: #error MACRO = 1
 #error MACRO = 1
  ^
Makefile:2: recipe for target 'all' failed
make: *** [all] Error 1

七、易错点

1.案例 1

1.1 题目

有以下宏定义:

c
#define M(x, y, z) x*y+z

在主程序有以下语句:

c
sum = M(a + b, b + c, c + a);

若 a = 1, b = 2, c = 3,则 sum 为多少?

1.2 解答

错误解答:sum = (a + b) * (b + c) + (c + a) = 3 * 5 + 4 = 19

正确解答:sum = a + b * b + c + c + a) = 1 + 2 * 2 + 3 + 3 + 1 = 12

【注意】 一定是先替换,后运算,不可直接运算。

参考资料:

C 语言 | 认识认识#pragma、#error 指令_51CTO 博客_trcv_c 指令

C 语言 详细讲解#pragma 的使用方法_C 语言_脚本之家

#pragma 的用法 - Boblim - 博客园

#paragma 详解-CSDN 博客

(7 封私信) C/C++内存对齐详解 - 知乎