超详细最全C语言个人笔记【第五章节-宏定义与条件编译】

预处理

在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:

  1. 头文件:#include
  2. 定义宏:#define
  3. 取消宏:#undef
  4. 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
  5. 显示错误:#error
  6. 修改当前文件名和行号:#line
  7. 向编译器传送特定指令:#progma
  • 基本语法
    • 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠连接成一个逻辑行
    • 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
    • 可以通过如下编译选项来指定来限定编译器只进行预处理操作:
gcc example.c -o example.i -E
编译过程
编译过程分为以下四个阶段:

1)预处理:处理预处理语句(#开头的语句),删除注释、头文件展开、宏替换...
			gcc  hello.c  -o  hello.i  -E
2)编译:将C语言程序转化为汇编语言
			gcc  hello.c  -o  hello.s  -S
3)汇编:将程序代码转化为二进制代码
			gcc  hello.c  -o  hello.o   -c
4)链接:将所有二进制代码合并起来,根据应用规则生成一个专门针对某个平台执行的应用程序镜像
			gcc  hello.c  -o  hello

宏的概念

宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:

#define PI 3.14

此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:

int main()
{
    printf("圆周率: %f\n", PI); 
    // 此语句将被替换为:printf("圆周率: %f\n", 3.14);
}
  • 宏的作用:

    • 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
    • 使得程序修改更容易:修改宏定义,即修改了所有该宏替换的表达式。
    • 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。
    #include <stdio.h>
    
    // 定义宏,注意没有分号
    // 编译之前可以确认PI的数据内容以及类型,所以编译的时候不会报错
    // 方便管理数据
    #define PI 3.14
    
    // typedef 的作用是将数据类型取别名
    // 注意typedef是c语法,所以需要用;结尾
    // 编译的时候才能调用typdef,所以typdef修饰常量的时候会报错
    typedef int (*ptr)(float,int);
    
    int func(float a, int b)
    {
        printf("%.2f\n",a+b);
    }
    
    int main(int argc, char const *argv[])
    {
        int (*p)(float,int);
       
        p = func;
        p(PI,10);
    
        ptr fun = func;
        fun(PI,30);
    
        return 0;
    }
    

无参宏

无参宏意味着使用宏的时候,无需指定任何参数,比如:

#define PI          3.14
#define SCREEN_SIZE 800*480*4 
int main()
{
    // 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式:
    printf("圆周率: %f\n", PI); 
    mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...);
}

注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏:

// 自定义宏:
#define PI          3.14
#define SCREEN_SIZE 800*480*4 

// 系统预定义宏
#define NULL ((void *)0)
#define PROT_READ	0x1	/* Page can be read.  */
#define PROT_WRITE	0x2	/* Page can be written.  */
#define MAP_SHARED	0x01	/* Share changes.  */

宏的最基本特征是进行直接文本替换,以上代码被替换之后的结果是:

int main()
{
    printf("圆周率: %f\n", 3.14); 
    mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, ...);
}

带参宏

带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像,例如:

#define MAX(a, b)   a>b ? a : b
#define MIN(a, b)   a<b ? a : b
// 多个变量要用{}
#define A(a,b,c) ({a=1;b+=1;c=a+b;})

以上的MAX(a,b) 和 MIN(a,b) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉:

int main()
{
    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y));
    printf("最小值:%d\n", MIN(x, y));
    // 以上代码等价于:
    // printf("最大值:%d\n", x>y ? x : y);
    // printf("最小值:%d\n", x<y ? x : y);
}
  • 带参宏的特点:

    1. 直接文本替换,不做任何语法判断,更不做任何中间运算。
    2. 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
    3. 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面有节约了切换时间。
  • typedef与宏区别

    #include <stdio.h>
    
    // 宏是预编译的时候进行替换,无消耗编译时间
    // 宏是可以修改任意数据的别名
    // 一般数据该别名用宏
    #define int_t int
    #define PI 3.14
    
    // typedef : 编译的时候替换,消耗编译时间
    // typedef 只能用于关键字改别名
    // 一般数据类型改别名用typedef
    typedef int int32_t;
    //typedef 3.14 pi;
    
    int main(int argc, char const *argv[])
    {
        int32_t a = 10;
        int_t b = 20;
    
        return 0;
    }
    
    

带参宏的副作用

由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:

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

int main()
{
    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y==200?888:999));
}

直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于:

printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999);

可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是:

  • 将宏定义中所有能用括号括起来的部分,都括起来,比如:
#define MAX(a, b) ((a)>(b) ? (a) : (b))
#include <stdio.h>

#define PI 3.14
#define HELLO "hello world"

#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define A(a,b,c) ({a=1;b+=1;c=a+b;})


int main(int argc, char const *argv[])
{
    printf("%f\n",PI);
    printf("%s\n",HELLO);
    int a = 10,b = 20,c;
    A(a,b,c);
    printf("%d,%d,%d\n",a,b,c);
    printf("max:%d\n",MAX(a,b));

    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y==200?888:999));
    int z = x > y==200?888:999;
    printf("%d\n",z);
    return 0;
}

宏定义中的符号粘贴

有些时候,宏参数中的符号并非用来传递数据,而是用来形成多种不同的字串,例如在某些系统函数中,系统本身规范了函数接口的部分标准,形如:

void __zinitcall_service_1(void)
{
    ...
    wifi
}

void __zinitcall_service_2(void)
{
    ...
    有线网
}

void __zinitcall_feature_1(void)
{
    ...
    zigbee(无线网)
}

void __zinitcall_feature_2(void)
{
    ...
    4G
}

此时,若需要向用户提供一个方便整合字串的宏定义,可以这么写:

#define LAYER_INITCALL(num, layer)  __zinitcall_##layer##_##num

用户的调用如下:

LAYER_INITCALL(service, 1);
LAYER_INITCALL(service, 2);
LAYER_INITCALL(feature, 1);
LAYER_INITCALL(feature, 2);
// demo
#include <stdio.h>

#define INTERNET_CALL(layer,num) __zinitcall_##layer##_##num

void __zinitcall_service_1(void)
{
    printf("切换到wifi模式\n");
}

void __zinitcall_service_2(void)
{
    printf("切换到飞行模式\n");
}

void __zinitcall_feature_1(void)
{
   printf("切换到5g模式\n");
}

void __zinitcall_feature_2(void)
{
    printf("切换到4g模式\n");
}

int main()
{
    // 编译出错的时候查找问题,需要观察预编译的结果
    // gcc 4_粘贴宏.c -o a.i -E
    INTERNET_CALL(service,1)();
    INTERNET_CALL(service,2)();
    INTERNET_CALL(feature,1)();
    INTERNET_CALL(feature,2)();
}
#include <stdio.h>

// 定义粘贴宏
#define CAL_FUNC(layer) CAL_##layer##_func

int CAL_add_func(int a,int b)
{
    return a+b;
}

int CAL_sub_func(int a, int b)
{
    return a-b;
}

int CAL_mul_func(int a, int b)
{
    return a * b;
}

int CAL_div_func(int a, int b)
{
    return a / b;
}

int main(int argc, char const *argv[])
{
    int ret = CAL_FUNC(sub)(20,5);
    printf("%d\n",ret);
    return 0;
}

注意:
在书写非字符串的字串时(如上述例子),使用两边双井号来粘贴字串,并且要注意如果字串出现在最末尾,则最后的双井号必须去除,例如上述代码不可写成:

#define LAYER_INITCALL(num, layer)  __zinitcall_##layer##_##num##

但如果粘贴的字串并非出现在最末尾,则前后都必须加上双井号:

#define LAYER_INITCALL(num, layer)  __zinitcall_##layer##_##num##end

注意:
另外,如果字串本身拼接为字符串,那么只需要使用一个井号即可,比如:

#define domainName(a, b) "www." #a "." #b ".com"

int main()
{
    printf("%s\n", domainName(vstc, lab));
}

执行打印如下:

gec@ubuntu:~$ ./a.out
www.vstc.lab.com
gec@ubuntu:~$
#include <stdio.h>
// 字符串粘贴宏
#define COMPANAMECOM(a) "www."#a".com"

int main()
{
    printf("%s\n",COMPANAMECOM(yueqian));
}

在这里插入图片描述

无值宏定义

定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如:

#define BIG_ENDIAN
#define __cplusplus

条件编译

  • 概念:有条件的编译,通过控制某些宏的值,来决定编译哪段代码。
  • 形式:
    • 形式1:判断表达式 MACRO 是否为真,据此决定其所包含的代码段是否要编译
    • 注意:#if形式条件编译需要有值宏
#define A 0
#define B 1
#define C 2

#if A
    ... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif
// 二路分支
#if A
    ... 
#elif B
    ...
#endif
// 多路分支
#if A
    ... 
#elif B
    ...
#elif C
    ...
#endif
  • 形式:
    • 形式2:判断宏 MACRO 是否已被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifdef MACRO
    ...
#endif

// 二路分支
#ifdef MACRO
    ...
#else
   ...
#endif
  • 形式:
    • 形式3:判断宏MACRO是否未被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifndef MACRO
    ...
#endif

// 二路分支
#ifndef MACRO
    ...
#else
   ...
#endif
  • 总结:
    • #ifdef 此种形式,判定的是宏是否已被定义,这不要求宏有值。
    • #if 、#elif 这些形式,判定的是宏的值是否为真,这要求宏必须有值。

条件编译的使用场景

控制调试语句:在程序中,用条件编译将调试语句包裹起来,通过gcc编译选项随意控制调试代码的启停状态。例如:

gcc example.c -o example -DMACRO

以上语句中,-D意味着 Define,MACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。

选择代码片段:在一些大型项目中(例如 Linux 内核),某个相同功能的模块往往有不同的实现,需要用户根据具体的情况来“配置”,这个所谓的配置的过程,就是对代码中不同的宏的选择的过程。

例如:

#define A 0  // 网卡1
#define B 1  // 网卡2  √
#define C 0  // 网卡3

// 多路分支
#if A
    ... 
#elif B
    ...
#elif C
    ...
#endif       

取消宏

#ifdef 宏名
#undef 宏名 // 取消该宏
	...
#endif
#ifdef B
#undef B
    printf("BBBB\n");
#endif

#include <stdio.h>

#define A

int main(int argc, char const *argv[])
{
#ifdef A // 如果定义宏A
#undef A // 取消宏A

printf("A\n");

#ifdef A // 如果定义宏A

printf("aaa\n");

#endif
    return 0;
}

定义宏

#ifndef 宏名
	...
#else
    ...
endif
#ifndef B
#define B
    printf("BBBBBB\n");
#endif

标准预定义宏

1.预定义日期和时间
__DATE__      __TIME__
2.函数名和当前行号
__FUNCTION__   ___LINE__
3.文件名
__FILE__
printf("日期:%s 时间:%s 当前文件:%s 当前行数:%d 函数名:%s\n",__DATE__,__TIME__,__FILE__,__LINE__,__FUNCTION__);
#include <stdio.h>

int main(int argc, char const *argv[])
{
    // 打印行号
    printf("__%d__\n",__LINE__);
    
    // 打印日期
    printf("日期:%s",__DATE__);

    // 打印时间
    printf("时间 : %s\n",__TIME__);
    
    // 打印函数名
    printf("函数名 : %s\n",__FUNCTION__);

    // 打印文件名
    printf("文件名 : %s\n",__FILE__);
    
    return 0;
}

宏有这么多用途 同志们都学费了吗?宏无处不在,如果有兴趣的同志可以去看看c语言底层代码,加油!

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值