C语言学习之预处理指令
目录
预定义符号
#define的应用
#define定义常量
#define定义宏
带有副作用的宏参数
宏替换的规则
函数和宏定义的区别
#和##
#运算符
##运算符
命名约定
#undef
编辑
命令行定义
条件编译
头文件包含
头文件被包含的方式
1.本地头文件包含
2.库文件包含
3.嵌套文件包含
1.头文件中ifndef/define/endif分别是干什么用的?
2.#include 和#include"filename.h"有什么区别?
2. #include "filename.h"
错误用法
总结
其它预处理指令
#error
#pragma
#line
#pragma pack() (在结构体部分介绍)
总结
预定义符号
C语言设置了一些预处理符号,可以直接使用,预定义符号也是在预处理期间处理的
__FILE__ //进行编译的文件
__LINE__ //文件当前行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则为定义(VS不严格遵循ASNI C)
示例:
#define的应用
#define 是预处理指令
#include也是预处理指令
#define定义常量
基本语法:
#define name stuff
上图所示的name是常量的名字,stuff是常量的内容。
如图所示就是它的应用。
#define 定义的数据通常储存在寄存器中
以下是一些#define定义常量的应用
但是如果我们定义的stuff过多怎么办呢?我们可以分行,除了最后一行外每一行都用”\“结束
但是注意:#define 定义标识符的是后面不要加 “;”
显然,我们发现else语句报错了,这是为什么呢?
在如下所示的代码中if语句后面没有大括号,if语句只能有一个语句,出现一个空语句的时候就会报错
#define A 100;//这样写是不好的,错误示范
int main()
{int a = 10;if (a < 0)a = 100;;//等效于a=Aelsea = -100;;//等效于a=-Areturn 0;
}
#define定义宏
#define机制包括一个规定,允许把参数替换到文本中,通常称之为宏(macro)或者定义宏(define macro)
下面的是宏的声明方式
其中parament—list是逗号隔开的符号表,它们可能出现在stuff中。
#define name( parament——list) stuff
注意:()左侧必须紧挨着name,否则存在的任何空白会把(parament—list)中的内容认为是stuff中的一部分
举例:
但是如果表达式是这样的呢?
结果为:
预期结果为:121,但是实际结果为21
可能与你的预期结果不符,但是原理很简单:乘法运算优先级大于宏定义的加法运算
实际上r的值等于a+1*a+1,因此实际上r=10+10+1=21.
所以我们可以将宏改成这样
#define square(x) (x)*(x)
但是这样可能会产生新的问题:
预期结果200,实际结果如上
原理也很简单:printf打印的表达式结果本质上为:10*10+10
所以针对这个代码,我们要这么写
#define add(x) ((x)+(x))
因此我们可以知道:定义宏的时候涉及对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免使用宏因为参数之间操作符或者临近操作符的相互作用导致不可预知的后果。
带有副作用的宏参数
当宏参数在宏定义中出现吵过一次后,如果参数带有副作用,使用宏就会出现危险,导致不可预知的后果。副作用就是表达式求指导时候出现的永久性效果。
举例说明:
在上图中,x和y分别为5和8,进入表达式后执行结果为x为6,y为9,执行b语句,返回给z的值为9,但是在这之后y还会++一次,因此y结果为10
宏替换的规则
在程序中扩展#define定义符号和宏的时候需要以下几个步骤
1.在调用宏的时候,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2.替换文本中随后被插入到程序中原来文本中的位置。对于宏,参数名被它们的值所替换
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述过程
注意:
1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能递归
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容不进行搜索
因为预处理的时候会对宏进行预处理操作,预处理后代码和我们编写的代码会有所差异
函数和宏定义的区别
宏通常被应用于执行简单的运算。
如图所示
为什么会导致这两种截然不同的结果呢?
因为函数是先将参数的表达式计算好后带入函数的形参部分,宏定义是直接替换。
而在上述代码中,相较于函数,为什么宏更有优势呢?
原因有二:
1.用于调用函数和从函数返回的代码可能比实际执行这个计算的工作需要的时间小很多。所以宏比函数在程序的规模和速度方面更胜一筹
2.函数的参数必须要声明特定的类型。因此函数只适合在类型合适的表达式上使用。繁殖这个宏可以适用于整型、浮点型、长整型等可以用>进行比较的类型。宏的参数无关乎类型
和函数相比宏的劣势
1.每次使用宏一份宏定义插入程序中。当功能比较复杂的适合,宏定义内容很长,会大幅度增加程序的长度
2.宏没办法调试
3.宏与类型无关,不够严谨
4.宏可能会带来运算优先级的问题,可能导致程序出错。
以下是一个汇总的对比表格
#和##
#运算符
#运算符将红的一个参数转换为字符串字面量。仅允许出现在带宏参数的宏的替换列表中。
#运算符所执行的操作可以理解为“字符串化”
比如我们对于
int a=10;
的时候,我们想打印出:a的数值为10。我们就可以这样写
#define PRINTF(n) printf("“#n”的数值为%d",n)
我们按这种方式调用的时候
PRINTF(a);//我们将a替换到宏内,就出现了#a,而#a就是转换为“a”时的一个字符串。
//所以代码就会被处理:
printf(""a"的数值为%d",a);
运行代码就是
a的数值为10
##运算符
##可以把位于它两边的符号合成为一个符号,允许宏定义从分离的文本片段创建标识符。##被称之为记号粘合
但是请注意:这样的粘合必须产生一个合法的标识符。否则结果就是未定义
举例说明:
我们要写一段代码进行两个数字比较大小的时候,不同类型就要写不同的函数
实在是过于繁琐,我们可以将其改进一下:
当我们调用的时候
但是实际应用的时候##应用的并不算多。
命名约定
一般而言函数和宏的写法很相似,我们靠语言很难去区分它们。因此我们有个约定:
宏名全部大写
函数名不要全部大写
特例:
offsetof——宏
#undef
这条指令用去移除一个宏
#undef NAME
//当我们需要定义一个新的宏变量的时候,旧名需要去除
举例说明:
命令行定义
许多C的编辑器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
当我们根据同一个源文件编译出一个程序的不同版本的时候,这个特性有些用处(假定一个程序中声明了一个某一长度的数组,如果机器内存有限,我们要一个很小的数组,如果另一个机器内存很大我们需要一个大数组)
#include<stdio.h>
int main()
{int arr[arr_size];int i=o;int j=0;for(i=0;i<arr_size;i++){arr[i]=i;}for(i=0;i<arr_size;i++){printf("%d",arr[i]);}printf("\n");return 0;
}
(该代码需要在Linux系统环境下运行,VS无法进行验证。因为作者本人并没有相应的环境,因此作者只能借用别人的结果验证)
编译代码:
//Linux环境演示
gcc-test.c -D arr_size=10 -o test//这里D与arr_size之间是否留有空格均可。
结果为:
条件编译
因为我们有条件编译指令,在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者方式是很方便的。
#include<stdio.h>
#define __DEBUG__
//当我们不用的时候将这段代码注释
int main()
{//code}
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值2.多分枝条件编译#if
//... 常量表达式
#elif
//... 常量表达式
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)#ifdef option1unix_version_option1();#endif#elif option2unix-version_option2();#endif
#elif defined(OS_MSDOS) #ifdef option2msdos_version_option2();#endif
#endif
标准库应用较多
头文件包含
头文件被包含的方式
1.本地头文件包含
#include"file.name"
查找策略:
先在源文件找,如果该头文件没找到,编译器就像查找库函数头文件一样在标准位置查找头文件/
如果找不到就提示编译错误。
2.库文件包含
#include<stdio.h>
直接去标准路径查找,如果找不到就提示编译错误
其实也可以用“”包含,但是这样查找效率更低,也不容易区分库文件和本地文件
3.嵌套文件包含
实际应用中,我们也可以这样
test.h
#include<stdio.h>
void test()
{
//code
}
struct person
{int id;char name[30];
}
test.c
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
如果这样写,test.c文件将test.h包含5次,即test.h的内容会被拷贝5份在test.c中
如果test.h文件比较大,这样预处理后代码量暴增。如果工程大头公共头文件,被大家使用又不做任何处理后果影响很严重。
如果解决这个问题呢?答案:条件编译
每个头文件开头写:
1.#ifndef __TEST_H__
2.#define __TEST_H__
3.头文件内容
4#endif//__TEST_H
或者也可以这样
1.#pragma once
这样就可以避免头文件重复引用。
注:
推荐《高质量C/C++编程指南》中的考试试卷(很重要)
1.头文件中ifndef/define/endif分别是干什么用的?
在C语言中,#ifndef
、#define
和 #endif
是预处理指令,用于防止头文件被重复包含,避免因多次声明同一内容导致的编译错误(如重复定义函数、结构体等)。以下是它们的详细作用和使用方法:
1. 作用说明
-
#ifndef
(if not defined)
检查是否未定义某个宏。如果未定义,则继续处理后续代码;如果已定义,则跳过直到#endif
之间的所有代码。 -
#define
定义一个宏,标记该头文件已被包含。通常以头文件名的大写形式命名(如MY_HEADER_H
),确保唯一性。 -
#endif
结束#ifndef
的条件编译块。
2. 使用场景
当多个源文件(.c
)包含同一个头文件(.h
),或头文件之间互相嵌套包含时,可能导致同一段代码被重复包含。例如:
// 文件 "myheader.h" struct Data {int value; };
如果 myheader.h
被多次包含,编译器会报错:
error: redefinition of 'struct Data'
。
3. 解决方法:添加头文件保护
// 文件 "myheader.h" #ifndef MY_HEADER_H // 如果未定义 MY_HEADER_H #define MY_HEADER_H // 定义 MY_HEADER_Hstruct Data {int value; };#endif // 结束条件编译
效果:
-
第一次包含
myheader.h
时,MY_HEADER_H
未定义,代码被处理,同时定义宏MY_HEADER_H
。 -
后续再次包含
myheader.h
时,#ifndef
检测到宏已存在,直接跳过整个内容,避免重复声明。
4. 注意事项
-
宏名称必须唯一
建议以头文件名的大写形式命名(如MY_HEADER_H
),避免与其他头文件冲突。 -
与
#pragma once
的区别-
#ifndef
是C/C++标准方式,兼容所有编译器。 -
#pragma once
是编译器扩展(如GCC、MSVC支持),更简洁高效,但非标准。
-
-
作用范围
头文件保护仅对当前文件有效,不同头文件仍需独立保护。
5. 示例对比
未使用头文件保护(报错):
#include "myheader.h" #include "myheader.h" // 重复包含int main() {return 0; } // 编译报错:struct Data 重复定义
使用头文件保护(正常):
// myheader.h 添加了 #ifndef/#define/#endif #include "myheader.h" #include "myheader.h" // 第二次包含被跳过int main() {return 0; } // 编译通过!
总结
指令 | 作用 | 必要性 |
---|---|---|
#ifndef | 检查宏是否未定义 | 必须 |
#define | 定义宏标记头文件已包含 | 必须 |
#endif | 结束条件编译块 | 必须 |
使用头文件保护是编写C语言头文件的最佳实践,能有效避免重复包含问题,确保代码可移植性和健壮性。
2.#include<filename.h>和#include"filename.h"有什么区别?
在C语言中,#include <filename.h>
和 #include "filename.h"
用于包含头文件,但它们的搜索路径和适用场景有本质区别:
核心区别
指令形式 | 搜索路径顺序 | 典型用途 |
---|---|---|
#include <filename.h> | 1. 系统/编译器标准头文件目录 (如 /usr/include 、编译器内置路径等) | 包含标准库头文件或系统级头文件 |
#include "filename.h" | 1. 当前源文件所在目录 2. 若未找到,再按 <filename.h> 的路径搜索 | 包含自定义头文件或项目本地头文件 |
详细说明
1. #include <filename.h>
-
适用场景:
用于包含标准库头文件(如stdio.h
、math.h
)或第三方库头文件(如通过系统包管理器安装的库)。 -
搜索机制:
编译器直接在预定义的系统头文件路径中查找文件,不会检查当前目录。 -
示例:
-
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库函数(如malloc)
2. #include "filename.h"
-
适用场景:
用于包含项目本地头文件(如自定义的myheader.h
)。 -
搜索机制:
-
先在当前源文件所在目录查找文件;
-
若未找到,再按
<filename.h>
的路径搜索(即系统目录)。
-
-
示例:
-
#include "utils.h" // 项目内的工具函数头文件
#include "config.h" // 本地配置文件
实际应用中的注意事项
-
区分使用场景
-
标准库必须用
< >
,例如#include <string.h>
。 -
自定义头文件应优先用
" "
,例如#include "my_lib.h"
。
-
-
路径配置
-
如果自定义头文件不在当前目录,可通过编译器的
-I
选项指定搜索路径:
-
gcc -I/path/to/headers main.c -o main
-
-
此时,
#include "myheader.h"
会先在/path/to/headers
中查找。
-
-
兼容性与可读性
-
虽然
#include "stdio.h"
可能也能编译(因为最终会回退到系统路径),但为了代码规范性和可读性,建议严格区分< >
和" "
。
-
-
重复包含问题
-
无论使用哪种方式,头文件中都应添加
#ifndef
/#define
/#endif
或#pragma once
防止重复包含(详见头文件保护机制)。
-
示例对比
场景:项目中包含自定义头文件
// 项目结构
project/
├── src/
│ └── main.c
└── include/
└── mylib.h
// main.c 中正确包含头文件
#include "../include/mylib.h" // 可行,但路径硬编码
// 或通过编译选项 -Iinclude/,直接写:
#include "mylib.h"
错误用法
#include <mylib.h> // 错误!编译器不会在项目目录中搜索此文件
总结
形式 | 目的 | 搜索路径 | 最佳实践 |
---|---|---|---|
<filename.h> | 系统/标准库头文件 | 系统目录 → 编译器内置路径 | 标准库、第三方库 |
"filename.h" | 项目本地头文件 | 当前目录 → 系统目录(可扩展) | 自定义头文件、项目内文件 |
正确使用两种包含方式,可以避免路径错误、提升代码可维护性,并明确头文件来源。
其它预处理指令
#error
作用
强制触发编译错误,并显示自定义错误信息。通常用于条件编译中检测非法配置或代码依赖。
使用场景
-
检查编译器是否支持特定功能。
-
防止代码在不兼容的环境中编译。
示例
#ifndef REQUIRED_VERSION
#error "REQUIRED_VERSION must be defined!" // 触发错误并终止编译
#endif
编译输出:
error: #error "REQUIRED_VERSION must be defined!"
#pragma
作用
向编译器发送特定指令,用于启用或禁用编译器功能(如优化、警告控制等)。
注意:#pragma
的行为是编译器相关的,不同编译器可能支持不同的指令。
常见用法
-
禁用警告(如 GCC/Clang/MSVC):
-
#pragma warning(disable : 4996) // 在 MSVC 中禁用特定警告
-
优化控制:
-
#pragma GCC optimize("O0") // 在 GCC 中关闭优化
-
标记代码段:
-
#pragma region DebugCode // 在 IDE 中折叠代码块(如 MSVC)
// 调试代码...
#pragma endregion -
跨平台兼容性
不同编译器的
#pragma
指令可能不兼容,需结合条件编译使用: -
#ifdef _MSC_VER
#pragma comment(lib, "mylib.lib") // 仅在 MSVC 中链接库
#endif
#line
作用
修改编译器报告的行号和文件名,主要用于代码生成工具(如 Lex/Yacc、预处理器生成的代码)中,使错误信息指向原始文件而非生成后的中间文件。
语法
#line <行号> ["文件名"]
示例
#line 100 "my_source.c" // 后续代码的行号从 100 开始,文件名标记为 my_source.c
int a = 10; // 若此代码出错,编译器会报告 my_source.c 的第 100 行
实际应用
在预处理后的代码中调整行号,方便调试:
#include <stdio.h>
#line 1 "original.c" // 强制将下一行视为 original.c 的第 1 行
int main() {
printf("Hello"); // 若此行出错,编译器显示 original.c:2
}
#pragma pack() (在结构体部分介绍)
作用
控制结构体(struct)或联合体(union)的内存对齐方式,通过指定对齐字节数优化内存布局或兼容特定硬件/协议要求。
语法
#pragma pack(n) // 设置对齐字节数为 n(1/2/4/8...)
#pragma pack() // 恢复默认对齐方式
#pragma pack(push, n) // 保存当前对齐方式并设置新对齐
#pragma pack(pop) // 恢复上一次保存的对齐方式
示例
#pragma pack(1) // 按 1 字节对齐(无填充)
struct Data {
char a; // 1 字节
int b; // 4 字节
}; // 结构体总大小 = 5 字节(默认对齐下可能为 8 字节)
#pragma pack() // 恢复默认对齐
使用场景
-
网络协议包解析(确保结构体与协议定义的字节对齐一致)。
-
硬件寄存器映射(精确控制内存布局)。
-
减少内存占用(但可能降低访问速度)。
注意事项
-
过度使用
#pragma pack(1)
可能导致性能下降(未对齐内存访问在某些平台上较慢)。 -
跨平台代码需谨慎处理对齐问题(不同编译器/平台的默认对齐可能不同)。
-
总结
指令 用途 关键点 #error
强制触发编译错误并显示信息 用于条件编译中的错误检查 #pragma
向编译器发送特定指令(编译器相关) 控制优化、警告、链接等 #line
修改编译器报告的行号和文件名 代码生成工具中调试友好 #pragma pack()
控制结构体的内存对齐方式 优化内存布局或兼容硬件/协议要求 使用建议:
-
#error
和#pragma
常用于平台适配或代码健壮性检查。 -
#line
主要在自动生成的代码中使用。 -
#pragma pack()
需谨慎使用,避免破坏内存对齐的默认优化。
感谢看到这里的读者大大,作者在这里求一个赞,谢谢各位