[深入浅出C语言]浅析编译与预处理(篇三)

280 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

前言

        本文就来分享一波作者对编译和预处理的学习心得与见解。本篇属于第三篇,主要介绍预处理的一些内容。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

类函数宏与函数的选择问题

        没有硬性规定,实际上是时间与空间的权衡。

宏的优势&函数的不足

        (1)宏生成内联代码,即直接在程序中生成语句,而函数调用需要跳入函数内,执行完后又得跳回主调函数,相比起来,实现同样的功能语句时宏耗时更短,运行速度更快

        (2)宏可以不用担心变量类型,因为宏处理的是字符串而不是实际的值,不管是什么类型,直接替换。

//两数比大小

//使用宏
#define MAX (x, y) ((x) > (y) ? (x) : (y))   //int,float,char...都行,只要能比较值的大小即可

//使用函数
int MAX(int x, int y)//只能传int
{
    return x > y ? x : y;
}

        (3)宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type)\
    (type *)malloc(num * sizeof(type))
...
//使用
    MALLOC(10, int);//类型作为参数
//预处理器替换之后:
    (int *)malloc(10 * sizeof(int));

宏的不足&函数的优势

        (1)每次使用宏替换都会向程序中插入一段代码,插得越多占用空间越多,明显增加占用空间,而函数只有一份副本,每次调用时进入函数中执行语句,可能会传入一些参数值,但是函数调用完后栈帧释放,等到下次调用时再创立,不会明显增加占用的空间,更节省空间

        (2)宏无法调试,而函数可以逐句调试。

        (3)由于宏与变量类型无关,也就不够严谨,可能会出现难以察觉的问题。

        (4)宏可能会带来运算符优先级的问题,写代码时不注意括号容易出错。

        (5)宏参数可能被替换到替换体中的多个位置,带有副作用的宏参数求值可能会产生不可预料的后果,而函数参数只在传参的时候求值一次,结果更加可控。

        (6)宏不可递归,而函数可以递归。

如何选择

        对于简单的逻辑(如两数比大小之类的),通常考虑用宏,写起来方便,并且耗时短。

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

        对于复杂的逻辑,考虑用函数,占用空间更少,在耗时上与宏差别不大。

命名约定:

        一般宏名全部大写,而函数名不要全部大写。

宏和函数对比汇总表

属 性#define定义宏函数
代 码 长 度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。函数代码只出现于一个地方,每 次使用这个函数时,都调用那个 地方的同一份代码。
执 行 速 度更快存在函数的调用和返回的额外开销,所以相对慢一些
操 作 符 优 先 级宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。
带 有 副 作 用 的 参 数参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一 次,结果更容易控制。
参 数 类 型宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。
调 试宏是不方便调试的函数是可以逐语句调试的
递 归宏是不能递归的函数是可以递归的

#undef

        #undef用于“取消”已定义的#define。

关于预处理器中的已定义

        预处理器发现了未用#undef关闭的宏,则其为已定义标识符,否则就是未定义的。不过若是宏从头文件引入,#define在文件中的位置取决于#include的位置。

#define M 100

int main()
{
    int a = M;//这里M可以替换为100
#undef M
    printf("%d",M);//M就变成了未定义标识符
}

命令行定义

        许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

例子(Linux环境下)

#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;
}

编译指令:

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__ //如果__DEBUG__宏已定义的话就编译下面代码,不然不编译
    printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //条件编译结束标志
    }
    return 0;
}

#if

#if  常量表达式

//...

#endif

        常量表达式由预处理器求值,如果常量表达式值为真则编译代码,否则不编译。
如:

#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

多个分支的条件编译

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

        逻辑上类同于if..else if...else语句,只是用在了条件编译上。

判断是否被定义

        判断已定义,下面两种表达等价。 

#if defined(symbol)
#ifdef symbol

例子: 

#ifdef ARY //如果ARY已定义,那么就编译下面语句
    printf("%d",ARY);
#else //否则编译下面语句
    m = 10;
    printf("%d",m);
#endif //结束标记

        判断未定义,下面两种表达等价。

#if !defined(symbol)
#ifndef symbol

例子: 

#ifndef M    //如果M未被定义,则M定义为100
#define M 100
#else 
printf("M no defined");
#endif
#define C
#define CPP

int main()
{
#if (defined(C) && defined(CPP))//都被定义才保留
    printf("hello c&&cpp\n");
#else
    printf("hello other\n");
#endif
    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

条件编译的意义

       本质认识:条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里“实际情况”,取决于运行平台,代码本身的业务逻辑等。

可以认为有两个好处:

  1. 可以只保留当前最需要的代码逻辑,其他去掉,可以减少生成的代码大小。

  2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现,可以根据平台不同而选择保留适合的代码,降低移植成本,提高跨平台性。

条件编译都在哪些地方用

       举一个例子吧

       我们经常听说,某某版代码是完全版/精简版,某某版代码是商用版/校园版,某某软件是基础版/扩展版等。

       其实这些软件在公司内部都是一个项目,而项目本质是有多个源文件构成的。所以,所谓的不同版本,本质其实就是功能的有无,在技术层面上,公司为了好维护,可以维护多种版本,当然,也可以使用条件编译,你想用哪个版本,就使用哪种条件进行裁剪就行。

       著名的Linux内核,功能上,其实也是使用条件编译进行功能裁剪的,来满足不同平台的软件。

文件包含 #include

        预处理器发现#include时,会查看文件名并把文件内容全部包含到当前文件中,替换掉#include。

        但要是重复包含的话,头文件的内容也会重复拷贝到源文件中,造成代码冗余。

        文件包含有两种形式:

        #include" " 预处理器先到当前源文件目录下查找,找不到再到标准系统目录下查找,如果找不到就提示编译错误。  

        #include<> 预处理器到标准系统目录中查找文件(C语言库提供的头文件),如果找不到就提示编译错误。

        这样是不是可以说,对于库文件也可以使用 “” 的形式包含?

        答案是肯定的,可以。

        但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

       自定义头文件的话,一般把#define指令,结构体声明,typedef和函数声明放在头文件,函数定义放在对应源文件,通常还要用#ifndef和#define和#endif防止多重包含头文件。

        嵌套文件包含

image.png

         这里comm.h被test.c包含了两次,就产生了重复包含现象。

        解决办法:

       (1)使用条件编译

        在comm.h头文件中加上几段代码,使得comm.h在第一次被包含后不再被包含。

#ifndef _TEST_
#define _TEST_

int Add(int x, int y)  //假设这个就是被重复包含的头文件内容

#endif

        为什么这样就可以呢?首先,在第一次包含该头文件时,_TEST_未定义,满足条件就编译后面代码,然后定义了_TEST_,等到后面再次要包含该头文件时,发现_TEST_已定义,不满足条件就不编译后面的代码,头文件也就不会被重复包含。

        (2)使用#pragma once

        只要在头文件的最开始加入这条指令就能够保证头文件被编译一次。


以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~

u=779924663,1900594976&fm=253&fmt=auto&app=120&f=JPEG.webp