目录
前言
#和##
1.#运算符
2.##运算符
命名约定
#undef
命令行定义
条件编译
1.单分支条件编译
2.多分支条件编译
3.判断是否被定义
4.嵌套指令
头文件的包含
1.头文件被包含的方式
(1)本地文件包含
(2)库文件包含
2.嵌套文件包含
其他预处理指令
1.#error
2.#pragma
3.#line
4.#pragma pack()
结束语
前言
在上一篇文章——C语言——预处理详解(上),我们学习了预处理中有关宏的部分知识,接下来我们接着学习预处理的知识。
#和##
1.#运算符
#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为”字符串化“。
我们先来看一下这段代码:
int main()
{
printf("hello" "world\n");
printf("helloworld\n");
return 0;
}
输出结果为:
我们可以看到,这两种写法输出结果是一样的,中间的空格并不会影响输出的结果。
再来看看下面的代码:
int main()
{
int a = 10;
printf("a=%d\n", a);
float b = 3.14f;
printf("%f\n", b);
return 0;
}
我们可以看出,这两句代码的逻辑是十分相似的。既然如此,我们可不可以使用什么方法,实现这段代码的功能呢?
我们可以使用宏来实现:
#define Print(n, format) printf("n = " format "\n", n)
#include<stdio.h>
int main()
{
int a = 10;
Print(a, "%d");
float b = 3.14f;
Print(b, "%f");
return 0;
}
输出结果如下:
我们看到:n 没有改变,那应该怎么办呢?
这个时候就轮到 # 派上用场了。
# 将宏的一个参数转换成字符串字面量,将 n 变成 "n"。
把宏修改成这样:
#define Print(n, format) printf(""#n" = " format "\n", n)
输出结果:
这样子就符合需求了。
2.##运算符
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称
为记号粘合。这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。
当我们试图写一个求两个数中的最大数,不同数据类型往往需要写不同的函数:
int int_max(int x, int y)
{
return x > y ? x : y;
}
float float_max(float x, float y)
{
return x > y ? x : y;
}
显然,这样子实在有些繁琐。有没有什么方法可以简化呢?
我们可以尝试使用宏来解决。
如下所示:
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return x > y ? x : y; \
}
使用宏,定义不同的函数:
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return x > y ? x : y; \
}
GENERIC_MAX(int); //相当于定义了一个函数int_max
GENERIC_MAX(float); //相当于定义了一个函数float_max
int main()
{
int a = int_max(3, 5);
printf("%d\n", a);
float b = float_max(3.14f, 1.41f);
printf("%f\n", b);
return 0;
}
运行结果:
我们在gcc环境观察一下:
注意:使用宏生成的函数是十分不方便进行调试的。
只用加了##,编译器才会认为它们是符号。
命名约定
一般来讲,函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者。
为了方便我们写代码,我们平时可以保持一个习惯:
1.把宏名全部大写
2.函数名不要全部大写
#undef
#undef 指令用于移除一个宏定义。
来看如下代码:
#include<stdio.h>
#define MAX 10
int main()
{
printf("%d\n", MAX);
#undef MAX
printf("%d\n", MAX);
return 0;
}
我们可以看到,在第6行MAX可以使用,在第7行使用#undef只会,后面MAX就变成未定义的标识符了。
命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性就有用处。(假定某个程序中声明了一个某长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要的数组能够大些)
比如这段代码:
#include<stdio.h>
int main()
{
int array[ARRAY_SIZE];
int i = 0;
for (i = 0; i < ARRAY_SIZE; i++)
{
array[i] = i;
}
for (i = 0; i < ARRAY_SIZE; i++)
{
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
在这段代码中,ARRAY_SIZE是未定义的,但是我们可以通过命令行对该符号进行定义。
编译指令:
gcc -D ARRAY_SIZE=10 programe.c
使用编译指令对其进行定义,如图所示:
条件编译
在编译一个程序的时候我们如果要将一条语句(⼀组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
条件编译指令允许代码在编译时根据一定的条件选择性地包含或排除某些代码段。
我们可以给他设定一个条件,条件为真,这段代码就参与编译,条件为假,这段代码就不参与编译。
例如:
一些调试性的代码,删除可惜,保留又碍事,所以我们可以选择性编译
#include <stdio.h>
//存在,代码执行
//注释掉,代码不执行
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
常见的条件编译指令:
1.单分支条件编译
#if 常量表达式 //常量表达式为真则执行,为假则不执行
//···
#endif
int main()
{
#if 10 //10为真,执行
printf("hello world\n");
#endif
return 0;
}
int main()
{
#if 0 //0为假,不执行
printf("hello world\n");
#endif
return 0;
}
2.多分支条件编译
#if 常量表达式
//···
#elif 常量表达式
//···
#else
//···
#endif
哪条为真则执行哪条语句
#define M 1
int main()
{
#if M == 0
printf("hello\n");
#elif M == 1
printf("world\n");
#endif
return 0;
}
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
//与上面相反
if !defined(symbol)
#ifndef symbol
比如这样使用:
#define M 1
int main()
{
#if defined(M)
printf("hello\n");
#endif
#if defined(N)
printf("world\n");
#endif
return 0;
}
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
头文件的包含
1.头文件被包含的方式
(1)本地文件包含
# include "filename"
查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。
如果找不到就提示编译错误。
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
注意:应当按照自己的安装路径去找。
(2)库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 " " 的形式包含?
答案是肯定的,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
因此,最好还是区分库文件包含和和本地文件包含,这样也有利于我们维护代码。
2.嵌套文件包含
我们已经知道,预处理阶段, #include 指令会导致指定头文件的内容被直接插入到源文件中的相应位置。
如果一个头文件被包含了 10 次,那就实际被编译了 10 次,如果重复包含,对编译的压力就比较大。
例如:
test.c:
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
test.h:
void test();
struct Stu
{
int id;
char name[20];
};
如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。
如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。
如何解决头文件被重复引入的问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
// 这里可以包含函数声明、宏定义、类型定义等
#endif
这样子写比较麻烦,下面有个更简单的方式:
#pragma once
就可以避免头文件的重复引入。
其他预处理指令
在C和C++中,#error 、#pragma 、#line 以及 #pragma pack() 是预处理指令,它们在编译之前由预处理器处理,用于控制编译过程或提供编译时的信息。
#error
#pragma
#line
//···
#pragma pack()
1.#error
#error指令用于在编译时生成一个编译错误。
#include <stdio.h>
// 假设我们有一个宏定义,用于控制是否包含某个特性
#define FEATURE_ENABLED 0
// 使用 #error 来检查 FEATURE_ENABLED 是否被设置为 1
#if !FEATURE_ENABLED
#error "FEATURE_ENABLED 必须被设置为 1 以启用此功能!"
#endif
// 接下来是代码的其他部分,但由于上面的 #error 指令,如果 FEATURE_ENABLED 不是 1,
// 编译器将在这里停止编译,并显示错误消息。
int main()
{
// 如果 FEATURE_ENABLED 是 1,则这段代码会被编译和执行。
// 但如果 FEATURE_ENABLED 不是 1,由于 #error 指令,编译器将不会到达这里。
printf("此功能已启用。\n");
return 0;
}
2.#pragma
#pragma 是一个通用的预处理指令,用于向编译器提供特定的指令。
例如:
当我们使用#pragma:
3.#line
#line 是一个预处理指令,它允许你重新设置当前文件的行号和文件名。
#line 100 "newfile.c"
// 接下来的代码将报告为来自newfile.c的第100行
4.#pragma pack()
#pragma pack() 是 #pragma 指令的一个特定用法,用于控制结构体或联合体的字节对齐。
在 C语言——结构体 中我们已经接触过了。
#pragma pack(1) //设置默认对齐数为1
struct example5
{
char c1;
int i;
char c2;
}s5;
int main()
{
printf("%zu\n", sizeof(s5));
return 0;
}
输出结果为:6
由于我们设置默认对齐数为1,因此结构体成员在内存中是连续储存的,这时结构体大小等于每个结构体成员大小之和。
结束语
花了一段时间把C语言——预处理部分的内容大致的讲了讲。
感谢看到这篇文章的朋友们!!!十分感谢大家的支持!!!
希望能得到朋友们的支持!!!