C语言之预处理

简介: C语言之预处理

程序的翻译环境和执行环境:

在ANSI的任何一种实现中,存在两个不同的环境。

第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第二种是执行环境,它用于实际执行代码。


而程序的编译过程如下:


组成一个程序的每个源文件通过编译过程分别转换成目标代码。

每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。

链接器同时会引入标准C函数库中任何该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。


翻译环境:

感兴趣的同学可以去看这篇文章,说的挺详细的哈

http://t.csdn.cn/E0I8x


运行环境:

程序执行的过程:

1:程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。


2:程序的执行就已经开始了,接着是调用main函数。


3:开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序执行过程一直保留他们的值。


4:终止程序,正常终止main函数,也有可能是意外终止。


预处理:

预定义符号:

FILE //进行编译的源文件

#include<stdio.h>
int main()
{
  printf("%s\n", __FILE__);
  return 0;
}


输出如下:

LINE //文件当前的行号

输出如下:

5

DATE//文件被编译的日期

#include<stdio.h>
int main()
{
  printf("%s\n",__DATE__);
  return 0;
}

输出如下:


TIME//文件被编译的时间

#include<stdio.h>
int main()
{
  printf("%s\n",__TIME__);
  return 0;
}

输出如下:

STDC//如果编译遵循ANSI C,其值为1,否则未定义

vs编译器下是不支持C语言标准的,因此__STDC__会直接显示未定义。

但在gcc编译器下是 支持的。



那么这些预定义符号到底有什么作用呢?

事实上,我们可以通过这些符号,将文件的信息写入日志记录下来。

举例:

#include<stdio.h>
int main()
{
  int i = 0;
  int arr[10] = { 0 };
  FILE* pf = fopen("log.txt", "w");
  for (i = 0; i < 10; i++)
  {
    arr[i] = i;
    fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\n",__FILE__,__LINE__,__DATE__,__TIME__,i);//写入文件信息
  }
  fclose(pf);
  pf = NULL;
  for (i = 0; i < 10; i++)
  {
    printf("%d ", arr[i]);
  }
  return 0;
}

当我们打开log.txt文件后,会发现关于程序的一些信息都被写入了log.txt文件,方便以后查阅。

0 1 2 3 4 5 6 7 8 9

预处理指令:

#define

#include

#pragma pack(4)

#pragma

#if

#endif

#ifdef

#line

举例:

#define:定义标识符

语法:#define name stuff

举例:

#include<stdio.h>
#define str1 "你好"//定义字符串
#define number 9//定义数字
int main()
{
  printf("%s ",str1);
  printf("%d\n", number);
  return 0;
}

输出如下:

你好 9

#define不仅可以用来定义数字和字符串,还可以用来定义关键字的别名等等。

那么#define后面是否要加分号吗?

举例:


此时编译不会通过,会出现语法错误,但在预处理阶段,我们很难发现代码的问题,只有在进行编译时,将定义的标识符的值进行替换后,编译器才会报错,因此,在以后写代码的过程中,#define后面不要加分号。


#define:定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)


下面是宏的申明方式:


#define name(parament—list)stuff 其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。


注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。


举例:

#include<stdio.h>
#define number(x) x+x ;//x用10替换,实现10+10
int main()
{
  int ret = number(10);
  printf("%d\n", ret);
  return 0;
}

输出如下:

20

举例:

#include<stdio.h>
#define number(x) x*x ;
int main()
{
  int ret = number(5+1);
  printf("%d\n", ret);
  return 0;
}

按照上面的方式将x替换为5+1,那么xx的值为36,但事实输出结果并不是36,而是11,原因是宏不是进行传参的,而是进行替换的,因此实际计算为5+15+1,输出结果为11

那么应该怎么杜绝这种情况呢?

宏修改如下:

#define number(x) (x)*(x) ;


传递过去,计算为(5+1)*(5+1),此时输出结果为36.

因此对于数值表达式进行求值的宏定义都应该使用注意不要出现由于忘记加括号而出现问题这种情况。

#define替换的规则:

在程序扩展#define定义符号和宏时,需要涉及几个步骤:


1:在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,他们首先被替换。


2:替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值替换。


3:再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程。


注意:

1:宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归。

2:当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

举例:

#define number 10
#define numbers(x) (x)*(x) ;
int main()
{
  int ret=10*numbers(number)
  printf("number=%d\n", number);//字符串常量内容和定义的符号相同。
  printf("%d\n", ret);
  return 0;
}

字符串有自动连接的特点。

int main()
{
  printf("he" "llo" " world\n");
}

输出

hello world

如何把参数插入字符串中?

只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

举例:

#include<stdio.h>
#define PRINT(format,value) printf("the value is "format"\n", value);
int main()
{
  PRINT("%d", 10);
}

使用#,把一个宏参数变成对应的字符串。

#include<stdio.h>
#define PRINT(format,value) printf("the value of "#value" is "format"\n",value);
int main()
{
  int i = 10;
  PRINT("%d", i+3);
}

输出:

the value of i+3 is 13

##的作用:

举例:

#include<stdio.h>
#define STR(X,Y) X##Y
int main()
{
  int str1 = 90;
  printf("%d\n", STR(str, 1));
  //##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。
  //printf("%d\n",STR(str##1);
  //printf("%d\n",str1);
  return 0;
}

输出

90

注:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。

带副作用的宏参数:

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,副作用即为表达式求值的时候出现的永久性效果。

举例:

#include<stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
  int a = 10;
  int b = 11;
  int max = MAX(a++, b++);
  //将a++和b++直接传递到宏,而不是将其计算结果替换进去:MAX((a++)>(b++)?(a++);(b++)) 
  //MAX((10)>(11)?(a++);(b++))注:后面的b++第一次加一为前面11的b++,而第二次b++是为后面产生比较结果的b++
  printf("%d\n", max);//12
  printf("%d\n", a);//11
  printf("%d\n", b);//13
  return 0;
}

输出:

产生这种结果的原因即为x++带有副作用,虽然没有直接的体现在参数上,但这种副作用往往会体现在表达式求值中。

12
11
13

宏和函数的对比:

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个。

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

那么为什么不使用函数来执行这个任务?


原因有两个:


1:用于调用函数和从函数返回的代码可能比实际执行这个小型计算机工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。


2:更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之,这个宏既可以适用于整形,长整型,浮点型等可以用于>来比较的类型,宏是无关类型的。


当然,宏相比于函数也有劣势的地方:


1:每次使用宏的时候,一份宏定义的代码将插入到程序中,除非 宏比较短,否则可能大幅度增加程序的程度。


2:宏是无法进行调试的。


3:宏由于类型无关,也就不够严谨。


4:宏可能会带来运算符优先级的问题,导致容易出现错误。


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


命名约定:

一般来讲,函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的一个习惯是:

把宏名全部大写,函数名不要全部大写

#undef:用于移除一个宏定义。

如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。


命令行定义:

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


编译指令:


gcc -D ARRAY_SIZE programe.c


条件编译:

在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或者放弃是很方便的,可以通过条件编译指令。

比如:调试性的代码,删除可惜,保留又碍事,那么我们可以对其进行选择性的编译。

举例:

#include<stdio.h>
int main()
{
  int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    arr[i] = 0;
#ifdef DEBUG//如果DEBUG在此之前被定义过,那么ifdef和endif之间的语句会被执行,否则不会执行。
    printf("%d ", arr[i]);
#endif
  }
  return 0;
}

上述代码将无任何输出。

那么如果想输出printf的内容,我们可以定义DEBUG。

代码修改如下所示:

#define DEBUG
#include<stdio.h>
int main()
{
  int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    arr[i] = 0;
#ifdef DEBUG//如果DEBUG在此之前被定义过,那么ifdef和endif之间的语句会被执行,否则不会执行。
    printf("%d ", arr[i]);
#endif
  }
  return 0;
}

输出:

0 0 0 0 0 0 0 0 0

常见的条件编译指令:

1:

#if 常量表达式

//…

#endif

//常量表达式由预处理器求值

举例:

#include<stdio.h>
int main()
{
  int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    arr[i] = 0;
#if 1//if 后面的常量表达式为真,下面的printf将会被执行,否则不执行
    printf("%d ", arr[i]);
#endif
  }
  return 0;
}

输出:

0 0 0 0 0 0 0 0 0

2:多个分支的条件编译

#if 常量表达式

//…

#elif 常量表达式

//…

#else

//…

#endif

举例:

#include<stdio.h>
int main()
{
#if 1==1//当if后面的条件为真,则if后面的语句将会被执行
  printf("haha\n");
  //当if后面的语句为假时,则对elif后面的语句进行判断
#elif 7==1
  printf("heihei\n");
#else
  print("wiwi\n");
#endif
  return 0;
}

输出:

haha

3:判断是否被定义

#if defined(symbol)
#ifdef symbol
上述逻辑反操作如下:
#if !defined(symbol)
#ifndef symbol

举例:

#include<stdio.h>
int main()
{
#if defined(DEBUG)
//当defined()括号中的标识符被定义过,那么执行if后面的语句,反之不执行
  printf("hehe\n");
#endif
  return 0;
}

此时没有输出。

要想输出printf语句,只需要在程序开头定义DEBUG即可。

#define DEBUG 0

注:即使这里的DEBUG被定义为0,但是printf语句依然会输出hehe,因为这里只关心是否定义,而不关心定义的内容。

4:嵌套指令:

和if/elif/else的条件编译用法基本相同,这里就是增加了嵌套用法而已。


文件包含:

通过前面的学习,我们已经知道,#include指令可以使另一个文件被编译,就像它实际出现于#include指令的地方一样,这种替换方式很简单:预处理器先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含10次,那就被编译10次。


头文件被包含的方式:

本地文件包含:

#include“filename”

add.h:

int ADD(int x, int y)
{
  return x + y;
}

test.h:

#include<stdio.h>
#include"add.h"//注意引add函数的文件方式不是<>,而是双引号
int main()
{
  int ret = ADD(2, 3);
  printf("ret=%d\n",ret);
  return 0;
}

输出:

ret=5

那么什么时候使用<>呢?什么时候使用“”呢?


如果是本地文件,也就是自己编写的头文件,就使用“”包含。


查找策略:先在源文件所在目录下查找,如果该头文件为找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果在找不到,就提示编译错误。


Linux环境的标准头文件路径:


/usr/include


VS环境的标准头文件的路径:


C:\program Files(x86)\Microsoft STudio 9.0\VC\include


注意按照自己的安装路径去找。

库文件包含:


#include<filename.h>


查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。


那么,我们可以得出一个结论,无论是我们自己编写的头文件还是库文件,都可以使用<>和双引号的形式,只要注意路径即可。


但如果库文件也使用双引号包含的形式,这样一来在查找文件的时候效率就会低很多,并且本地文件和库文件也不容易被区分了。


嵌套文件包含:

comm.h和comm.c是公共模块,test.h和test1.c使用了公共模块。test2.h和test2.c使用了公共模块,test.c和test.h使用了test1模块和test2模块,这样最终程序中就会出现两份comm.h的内容,这样就造成了文件内容的重复。


对于这种现象,我们可以通过条件编译来解决。


每个头文件的开头写:


#ifndef _TEST_H

#define _TEST_H

//头文件的内容

#endif //_TEST_H


或者#pragma once


这样就可以避免头文件的重复使用。

相关文章
|
2月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
2月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
2月前
|
自然语言处理 编译器 Linux
【C语言篇】编译和链接以及预处理介绍(上篇)1
【C语言篇】编译和链接以及预处理介绍(上篇)
43 1
|
1月前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
27 2
|
2月前
|
编译器 Linux C语言
【C语言篇】编译和链接以及预处理介绍(下篇)
【C语言篇】编译和链接以及预处理介绍(下篇)
37 1
【C语言篇】编译和链接以及预处理介绍(下篇)
|
2月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
2月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
2月前
|
存储 C语言
【C语言篇】编译和链接以及预处理介绍(上篇)2
【C语言篇】编译和链接以及预处理介绍(上篇)
41 0
|
4月前
|
存储 自然语言处理 程序员
【C语言】文件的编译链接和预处理
【C语言】文件的编译链接和预处理
|
4月前
|
程序员 编译器 C语言
C语言中的预处理指令及其实际应用
C语言中的预处理指令及其实际应用
94 0