C语言提供了一些预定义符号,这些符号在预处理期间被处理。
__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__STDC__ // 如果编译器遵循ANSI-C,其值为 1,否则未定义printf("file:%s line:%d\n", __FILE__, __LINE__);这是我们平时最常见到的#define的用法
#define name stuff命名规范: 宏定义的标识符通常全部大写。
#define MAX 1000 // 宏定义一个值为1000的常量MAX
#define REG register // 将关键词register重命名为REG
#define DO_FOREVER for(;;) // 用更形象的符号来替换一种实现
#define CASE break; case // 写case语句时,自动补充break(不推荐应用,就是个示例)
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续航符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__, __LINE__ , \
__DATE__, __TIME__ )在#define定义标识符时,是否需要在最后加上;?
#define MAX 1000;
#define MAX 1000建议:不要在
#define定义末尾添加;,这样容易导致问题。
if(condition)
max = MAX; // 替换后:max = 1000;;
else // 语法错误:if和else之间有多条语句
max = 0; #define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
#define name(parament-list) stuff其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:参数列表的左括号必须与
name紧邻,任何空白都会导致参数列表被解释为stuff的一部分
#define SQUARE(x) x * x
// 问题代码
printf("%d\n", SQUARE(5 + 1)); // 输出11而非36
// 替换后:5 + 1 * 5 + 1 = 5 + 5 + 1 = 11
// 解决方案
#define SQUARE(x) (x) * (x) // 为参数添加括号建议:宏定义一般用于实现简单算法,而复杂一点的算法最好用函数解决
#define DOUBLE(x) (x) + (x)
// 问题代码
printf("%d\n", 10 * DOUBLE(5)); // 输出55而非100
// 替换后:10 * (5) + (5) = 50 + 5 = 55
// 解决方案
#define DOUBLE(x) ((x) + (x)) // 为整个表达式添加括号最佳实践: 对数值表达式进行求值的宏定义,应该为每个参数和整个表达式都加上括号。
当宏参数在宏定义的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。
x+1; // 不带副作用
x++; //带有副作用副作用定义: 表达式求值时出现的永久性效果。
MAX宏可以证明具有副作用的参数所引起的问题
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 8;
int z = MAX(x++, y++);
// 替换结果:
// z = ((x++) > (y++) ? (x++) : (y++));
// 输出:x=6 y=10 z=9#define定义的符号,如果是则首先被替换
#define定义的符号,如果是,就重复上述处理过程。
#define定义中可以出现其他#define定义的符号,但不能递归
宏通常被应用于执行简单的运算。比如:在两个数中找出较大的一个时,写成下面的宏,更有优势一些
#define MAX(a, b) ((a) > (b) ? (a) : (b))>来比较的类型。宏的参数是类型无关的
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
#define MALLOC(num, type)\
(type)malloc(num sizeof(type))
...
// 使用
MALLOC(10, int); // 类型作为参数
// 预处理器替换之后
(int *)malloc(10, sizeof(int));
补充:C++中有个内联函数inline,同时具备了函数和宏的特点
这两个运算符在实际工程中很少用到,所以大家对其有所了解就行
将宏参数转换为字符串字面量。
#define PRINT(n, format) printf("the value of " #n " is " format "\n", n)
int a = 10;
PRINT(a, "%d");
// 替换后:printf("the value of " "a" " is %d\n", a);
// 输出:the value of a is 10注意:
#仅允许出现在带参数的宏的替换列表中
##可以把位于它两边的符号合成一个标识符,它允许宏定义从未分离的文本片段创建标识符。
注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
这里我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数
int int_max(int x, int y)
{
return (x > y ? x : y);
}
double double_max(double x, double 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 ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) // 替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{
//调⽤函数
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}
// 输出:
// 3
// 4.500000在实际开发过程中##很少用到,很难找到非常贴切的例子
补充:对于以上情景,C++中的解决方案有运算符重载
一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者。
用于移除一个宏定义
#undef NAME使用场景: 需要重新定义已存在的宏时,需要先移除旧定义。
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小单独数组,但是另外一个机器内存大些,我们需要一个数组能够大些)
#include <stdio.h>
int main()
{
int array [SIZE]; // SIZE未定义
for(int i = 0; i< SIZE; i ++)
{
array[i] = i;
}
for(int i = 0; i< SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0
}编译指令:
gcc -D SIZE=10 test.c在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令
// 1. 单分支
#if 常量表达式
// ...
#endif
// 2. 多分支
#if 常量表达式
// ...
#elif 常量表达式
// ...
#else
// ...
#endif
// 3. 判断是否定义
#if defined(symbol) // 或 #ifdef symbol
// ...
#endif
#if !defined(symbol) // 或 #ifndef symbol
// ...
#endif调试性的代码,删除可惜,保留又碍事,此时我们就可以使用条件编译
#include <stdio.h>
#define __DEBUG__
int main()
{
int arr[10] = {0);
for(int i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]); // 为了观察数组是否赋值成功
#endif // __DEBUG__
}
return 0;
}#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方式 | 语法 | 查找策略 |
|---|---|---|
本地文件包含 | #include "filename.h" | 1. 源文件所在目录 |
#include <filename.h>|1. 直接搜索标准系统目录注意: 虽然库文件也可以用
""包含,但不推荐,因为会降低查找效率,也不利于区分库文件和本地文件。
我们已经了解,#include指令会使被包含文件的内容在预处理阶段被完整地插入到指令位置。这种简单的文本替换机制会导致一个严重问题:头文件被多次包含时,其内容会被多次拷贝到源文件中。
void test();
struct Stu
{
int id;
char name[20];
};#include "test.c"
#include "test.c"
int main()
{
return 0;
}如果直接这样写,test.c文件中将test.h包含2次,那么test.h文件的内容将会在test.c中被拷贝2次
// 第一次包含 test.h 的内容
void test();
struct Stu
{
int id;
char name[50];
};
// 第二次包含 test.h 的内容(重复!)
void test();
struct Stu
{
int id;
char name[50];
};
int main()
{
return 0;
}如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,如何解决头文件被重复引入的问题?答案:条件编译
每个头文件开头写:
#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容(声明)
#endif工作原理:
__TEST_H__未定义,执行#define __TEST_H__并包含内容
__TEST_H__已定义,跳过整个头文件内容
#pragma once特性 | #ifndef/#define/#endif | #pragma once |
|---|---|---|
标准性 | 符合所有C标准 | 编译器扩展,非标准但广泛支持 |
可靠性 | 绝对可靠 | 在绝大多数情况下可靠 |
编译性能 | 每次都需要打开文件检查 | 编译器可能优化,性能更好 |
使用便利性 | 需要唯一宏名称 | 简单直接 |
宏名称冲突 | 可能发生 | 不会发生 |
#error
#pragma
#line
...
这里不做过多介绍,感兴趣的可以自己去了解。特定应用:
#pragma pack()在结构体内存对齐中使用。
ifndef/define/endif 是干什么用的?#include <filename.h> 和 #include "filename.h" 有什么区别?#include <filename.h>:直接搜索标准系统目录
#include "filename.h":先搜索当前目录,再搜索标准系统目录