预处理详解以及宏定义详解

       当编译器进行编译的时候,编译器会对源文件进行预处理,编译,汇编,链接等几种操作,而其中预处理会对我们的代码进行相关的操作,比如#define的符号替换,头文件的包含的,接下来就让我们仔细看看预处理是怎么一回事吧;

预定义符号

在C语言中还有些内置的符号:

__FILE__ 进行编译的源文件
__LINE__ 文件当前的行号
__DATE__ 文件编译的日期
__TIME__ 文件被编译的时间
__STDC__ 查看编译器是否遵循ANSI C,遵守为1

这些符号可以直接用printf进行打印比如:

printf("file:%s line:%d\n", __FILE__, __LINE__);

这样就能知道源文件在哪里了;

#define

#define这个定义标识符相信大家都用过,它能够将某些符号定义成特定值,也可以将某些名字很长的关键字换成一些简短的符号;

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现

就像这样,但是这样使用#define有一点需要注意的是,那就是尾端不应该有 ; 。 

这是为什么呢?这就要讲到#define的特性了;

#define实际上是将函数内有对应符号给直接替换成所定义的字符串或者名字,就像上面,

 #define MAX 1000

若是使用MAX,那么实际上编译器会将所有MAX替换成1000

若是#define MAX 1000;

那么使用的时候会将MAX替换成1000;

这样的话,我们在实际运用中就会出现语法错误,所以需要注意;

#define定义宏

当我们明白#define定义标识符后,接下来介绍一个#define定义的一个新功能——宏定义

当我们想了解宏定义,首先需要了解宏定义的声明方式

宏定义的声明方式 

#define name(parament-list)   stuff

name 定义的宏的名字
parament-list 一个符号表,会在stuff中出现
stuff 宏的内容

下面我来定义一个经典的宏:

#define SQUARE(x) x*x

当我们在main函数使用该宏的时候,可以直接得到答案:

预处理详解以及宏定义详解_第1张图片

 这里我们可以看到成功的算出了答案;

                                          

但是,如果我们这样写就会出错:

预处理详解以及宏定义详解_第2张图片

这里我们看到,本来应该是36的答案,却变成了11,这是为什么呢? 

                                                       

 之前提到过,#define是将对应符号替换使用的,因此这里实际上是这样计算的:

                                                       5+1*5+1 = 11

因此如果我们使用#define定义宏的时候一定要注意优先级,我们这里应该在x旁边加个括号

                                             #define SQUARE(x)  (x)*(x)  

这样我们才能正确的使用宏定义;

而要想真正的用好宏定义,我们就需要了解宏定义的规则:

宏定义的规则:

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define 定义的符号。如果是,就
重复上述处理过程。
注意:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。

 

#和##的使用

宏定义里面有两个预定义符号,这两个符号有什么用呢?接下来一一解答;

#的使用 

将一个宏参数转换成对应的字符串

 #的使用规则就是可以将一个宏参数转换成对应的字符串;

比如我想在屏幕上打印这样一串字符串:

"The value of  a  is  10"

 如果这种语句使用次数少的话,我们可以单纯的使用printf语句打印,但是当使用次数多的时候,使用printf语句就捉襟见肘了,并且这个功能使用函数不能实现,那么就轮到宏出场了;

#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is "FORMAT "\n", VALUE)

int main()
{

    int a = 10;
	PRINT("%d", a);
}

预处理详解以及宏定义详解_第3张图片

上述代码打印出来就是这个样子;

我们可以看到,#VALUE被替换成了a,FORMAT则依旧是原来的宏形参——"%d";

这就是宏定义的作用,并且我们可以用这个方式打印不同类型的数据,只需要将传过去的"%d"改成变量对应的类型就可以了;

##的使用 

##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。

 在这里给出一串代码更好理解:

#define ADD_TO_SUM(num, value) sum##num += value
int main()
{

	int sumb = 5;
	printf("%d", ADD_TO_SUM(b,10));
	return 0;
}

根据宏定义规则,此处代码在预处理后会变成这样:

#define ADD_TO_SUM(num, value) sum##num += value
int main()
{

	int sumb = 5;
	printf("%d", sum##num += value);
	return 0;
}

再根据##的使用规则

这里的意思就变成了 sumb+= 10;

因此sumb = 15了;预处理详解以及宏定义详解_第4张图片

 

当看到这里后我们就会发现宏定义好像和函数差不多,但是花样比函数多,比如之前的#符号的作用,而且还能做到与函数一样的事,那么是不是说宏定义就能取代函数了呢?

实际上并不能;

在c语言的语法中,宏定义虽然比函数快,但也有许多缺点,这里给出函数与宏定义的对比表格
 

#define定义宏
函数

长度

每次使用时,宏代码都会被插入到程序中。除了非常
小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
更快 存在调用返回等开销,慢些
宏定义需要根据使用文段的上下代码才能求值,需要加上括号,否则会产生不可预计的错误 直接在函数内部求值,容易预计结果
宏可能会替换在代码多处位置,会产生难以预料的结果 结果容易控制
宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型。
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
不同的。
宏是不方便调试的
函数是可以逐语句调试的
宏是不能递归的
函数是可以递归的

看到上述对比,大家可以根据自己需要来选择使用宏还是函数;

#undef命令

用来移除一个宏定义

 

#define M 100

int main()
{
	printf("%d\n", M);
#undef M
	printf("%d\n", M);
}

我们这样写一串代码,第一个printf可以正常打印,而第二个则不行,这就是#undef的运用;

命令行定义

给出如下代码:

#include 
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编译器上面输入

gcc - D ARRAY_SIZE = 10 programe . c
这样就不会报错了;
而上面代码中的-D实际上就是命令行定义,它定义了ARRY_SIZE为10;

条件编译

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 OPTION1
                unix_version_option1 ();
        #endif
        #ifdef OPTION2
                unix_version_option2 ();
        #endif
#elif defined(OS_MSDOS)
        #ifdef OPTION2
                msdos_version_option2 ();
        #endif
#endif

上面给出了条件编译的一些常用指令,这些实际上和  if  和 else  等语句使用方式相同,若#if后面的条件为真就会编译;

比如

#define __DEBUG__ 1

int main()
{
#if __DEBUG__ 
	printf("1");
#endif
	return 0;
}

若__DEBUG__为0,则不会打印1,而为1才会打印出来,其他的条件编译语句和这个使用方式一样,就不赘述了;

文件包含

        自己写过头文件的同学,都知道,若是包含库里的头文件,就应该这样写

#include

 而若是自己创建的头文件一般是这样包含

#include"FILENAME.h"

这两种方式有什么区别呢?

实际上,若是<>包含的头文件,那么编译器就会去库里直接查找,而若是""包含的头文件,则会优先在源文件所在的位置查找是否有对应的头文件,没有才回去库中寻找;

当然,库中的头文件也可以用""包含,只是这样就会导致效率降低,所以不推荐;

嵌套文件包含

当我们开始一个大项目,由多个成员一起分工合作,每个成员负责一个板块时,每个成员包含头文件都是自己包含自己所需的,那么就会导致头文件重复定义问题,那么如何解决这个问题呢?

这就需要用到之前说的条件编译指令了;

在每个头文件开头这样写

#ifndef __TEST_H__

#define __TEST_H__

//对应的头文件内容

#endif        

如果时第一次包含这个头文件,那么__TEST_H__这个符号还没有被定义,因此#ifndef就条件为真,而第二次包含这个头文件,__TEST_H__这个符号就被定义了,#ifndef条件为假,就不会包含了;

#ifndef 的意思和#ifdef是相反的,若是#ifndef后面的字符未被定义,则条件为真,否则为假

以上就是本篇文章的全部内容了。

你可能感兴趣的:(笔记,c语言)