LV01-14-C语言-预处理

本文主要是C语言基础——预处理相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
点击查看相关文件下载
--- ---

一、预处理的概念

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

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

二、预定义

1.宏

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

2.预定义符号串

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

符号常量类型含义
__FILE__字符串正在预编译编译的文件名(字符串字面值)
__LINE__整数文件的当前行号(十进制常量)
__DATE__字符串文件被编译的日期
__TIME__字符串文件被编译的时间
__STDC__整数如果编译器遵循ANSI C,其值为1,否则未定义
__FUNCTION__字符串表示调用此预定义的函数名称
__func__字符串表示调用此预定义的函数名称
点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

在终端执行以下命令:

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

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

1
2
3
4
5
6
7
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无参宏定义

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

1
#define 标识符 字符串
点击查看各部分说明
# 表示这是一条预处理命令
define 宏定义命令
标识符 所定义的宏名(习惯上用全大写表示,但是也允许小写,看个人喜好喽),后边在程序中使用的宏都是直接使用宏名
字符串 可以是常数,表达式,格式串等
【说明】 字符串 与 (字符串) 似乎没有区别。
点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
2
3
4
The pi is : 3.141593
The str is : Hello,world!
The y is : 3
The sum is : 15

【说明】

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

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

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

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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;
}

在终端执行以下命令:

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

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

1
2
3
4
5
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

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

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

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

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
2
5
3.141593

3.2含参宏定义

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

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

调用的一般形式是:

1
标识符(实参表);

【注意】

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

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

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
MAX(a, b) is : 5

【说明】

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

点击查看实例
1
#define MAX (a, b) (a > b)?a:b

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

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

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

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
MAX(a, b) is : 6

3.3宏与函数

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

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

3.4 # 与 ##

3.4.1 # 的用途

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

例如,下边的测试程序:

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

#define STR(s) #s

int main(int arc, char *argv[])
{
char a[10] = STR(mine);
return 0;
}

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

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

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

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

我们会发现, mine 被替换为 “mine” 了。

3.4.2 ## 的用途

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

1
2
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family

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

1
2
3
4
5
6
7
8
9
10
11
#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;
}

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

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

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

1
2
3
4
5
6
7
8
# 前边的省略 ... ...
# 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 语言预处理的另一种方式,文件包含的一般形式是:

1
2
3
#include <filename>
/* 或者 */
#include "filename"

有木有觉得很熟悉呢,这经常用于引入对应的头文件( .h 文件)。

文件包含的处理过程很简单,就是将头文件的内容插入到该语句所在行的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

点击查看两种写法的区别

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

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

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

【注意】

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

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

1
#include "./include/test.h"

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

四、条件编译

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

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

1. #if

1.1使用格式

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

1
2
3
4
/* 1.只有 #if */
#if 常量表达式
语句块;
#endif

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

1
2
3
4
5
6
/* 2. #if ... #else... */
#if 常量表达式
语句块1;
#else
语句块2;
#endif

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

1
2
3
4
5
6
7
8
/* 3. #if ... #elif... */
#if 常量表达式1
语句块1;
#elif 常量表达式2
语句块2;
#else 常量表达式3
语句块3;
#endif

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

1.2使用实例

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
#elif N==1

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

1
error: floating constant in preprocessor expression

2. #ifdef

2.1使用格式

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

1
2
3
4
5
/* 1.只有 #ifdef */
#define macro
#ifdef macro
语句块;
#endif

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

1
2
3
4
5
6
7
/* 2. #ifdef ... #else... */
#define macro
#ifdef macro
语句块1;
#else
语句块2;
#endif

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

2.2使用实例

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
2
DEBUG1 is define
DEBUG2 is define

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

3. #ifndef

3.1使用格式

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

1
2
3
4
/* 1.只有 #ifndef */
#ifndef macro
语句块;
#endif

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

1
2
3
4
5
6
/* 2. #ifndef ... #else... */
#ifndef macro
语句块1;
#else
语句块2;
#endif

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

3.2使用实例

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

在终端执行以下命令:

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

会看到有如下信息输出:

1
2
EBUG1 is define
DEBUG2 is not define

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

五、 #pragma

4.1简介

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

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

4.2 message

4.2.1语法格式

1
#pragma message("string")

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

4.2.2使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
#include <stdio.h>

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

在终端执行以下命令:

1
gcc test.c -Wall

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

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

4.3 once

4.3.1语法格式

1
#pragma once

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

1
2
3
4
5
6
#ifndef __HEAD_FILENAME_H__
#define __HEAD_FILENAME_H__

/* code */

#endif

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

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

1
2
3
4
5
6
#ifndef __HEAD_FILENAME_H__
#define __HEAD_FILENAME_H__
#pragma once
/* code */

#endif

4.3.2使用实例

点击查看实例
1
2
3
4
5
6
7
8
#include <stdio.h>
#include "global.h"
#include "global.h"
int main(int argc, char *argv[])
{
printf("a=%d\n", a);
return 0;
}
1
2
#pragma once
int a = 10;

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

1
gcc test.c -Wall

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

1
2
3
4
5
6
7
8
9
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 会有以下信息显示:

1
a=10

4.4 pack

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

4.4.1内存对齐

什么是内存对齐?

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

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
#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;
}

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

1
2
gcc test.c -Wall
./a.out

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

1
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语法格式

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

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

4.4.2使用实例

点击查看实例
test.c
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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;
}

在终端执行以下命令:

1
2
gcc test.c -Wall
./a.out

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 的时候将停止编译,并输出自定义的消息,一般使用格式如下:

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

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

【注意】

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

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

2.使用实例

1
2
3
4
5
6
7
8
9
10
11
#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;
}

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

1
2
3
4
5
6
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
#define M(x, y, z) x*y+z

在主程序有以下语句:

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

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

【解答】

错误解答: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

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