C语言预处理流程全面分析

概述

  在前面各章中,已多次使用过以“#”号开头的预处理命令。如包含命令# include,宏定义命令# define等。在源程序中这些命令都放在函数之外, 而且一般都放在源文件的前面,它们称为预处理部分。

  所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能, 它由预处理程序负责完成。当对一个源文件进行编译时, 系统将自动引用预处理程序对源程序中的预处理部分作处理, 处理完毕自动进入对源程序的编译。

  C语言提供了多种预处理功能,如宏定义、文件包含、 条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、 移植和调试,也有利于模块化程序设计。本章介绍常用的几种预处理功能。

执行命令:

gcc -E xxx.c  或 cpp xxx.c

宏定义 #define

在C语言源程序中允许用一个标识符来表示一个字符串, 称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换, 这称为“宏代换”或“宏展开”。

宏定义是由源程序中的宏定义命令完成的。 宏代换是由预处理程序自动完成的。在C语言中,“宏”分为有参数和无参数两种。 下面分别讨论这两种“宏”的定义和调用。

无参宏定义


  无参宏的宏名后不带参数。其定义的一般形式为: #define 标识符 字符串 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。 “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。在前面介绍过的符号常量的定义就是一种无参宏定义。 此外,常对程序中反复使用的表达式进行宏定义。

例如: 
# define M (y*y+3*y) 定义M表达式(y*y+3*y)。在编写源程序时,所有的(y*y+3*y)都可由M代替,而对源程
序作编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去置换所有的宏名M,然后再进行编译。


#define M (y*y+3*y)
main(){
    int s,y;
    printf("input a number: ");
    scanf("%d",&y);
    s=3*M+4*M+5*M;
    printf("s=%d\n",s);
}

上例程序中首先进行宏定义,定义M表达式(y*y+3*y),在s= 3*M+4*M+5* M中作了宏调用。在预处理时经宏展开后
该语句变为:s=3*(y*y+3*y)+4(y*y+3*y)+5(y*y+3*y);但要注意的是,在宏定义中表达式(y*y+3*y)两边的括
号不能少。否则会发生错误。
当作以下定义后: #difine M y*y+3*y在宏展开时将得到下述语句: s=3*y*y+3*y+4*y*y+3*y+5*y*y+3*y
这相当于 3y2+3y+4y2+3y+5y2+3y;显然与原题意要求不符。计算结果当然是错误的。 因此在作宏定义时
必须十分注意。应保证在宏代换之后不发生错误。

对于宏定义还要说明以下几点:

1. 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

2. 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起置换。

3. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结 束。如要终止其作用域可使用# undef命令,
 

例如: 

# define PI 3.14159
main()
{
……
}
# undef PIPI的作用域
f1()

....表示PI只在main函数中有效,在f1中无效。

4. 宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。

#define OK 100
main()
{
    printf("OK");
    printf("\n");
}
上例中定义宏名OK表示100,但在printf语句中OK被引号括起来,因此不作宏代换。程序的运行结果为:OK这表示把“OK”当字符串处理。

5. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层代换。

例如: 

#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/

对语句: printf("%f",s);在宏代换后变为: printf("%f",3.1415926*y*y);

6. 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

7. 可用宏定义表示数据类型,使书写方便。

例如:

#define STU struct stu在程序中可用STU作变量说明: STU body[5],*p;

#define INTEGER int 在程序中即可用INTEGER作整型变量说明: INTEGER a,b;

应注意用宏定义表示数据类型和用typedef定义数据说明符的区别宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换, 而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。请看下面的例子:

#define PIN1 int* typedef (int*) PIN2;从形式上看这两者相似, 但在实际使用中却不相同。下面用PIN1,PIN2说明变量时就可以看出它们的区别: PIN1 a,b;在宏代换后变成 int *a,b;表示a是指向整型的指针变量,而b是整型变量。

然而:PIN2 a,b;表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟是作字符代换。在使用时要分外小心,以避出错。

8. 对“输出格式”作宏定义,可以减少书写麻烦。

#define P printf
#define D "%d\n"
#define F "%f\n"
main(){
    int a=5, c=8, e=11;
    float b=3.8, d=9.7, f=21.08;
    P(D F,a,b);
    P(D F,c,d);
    P(D F,e,f);
}

带参宏定义

  C语言允许宏带有参数。在宏定义中的参数称为形式参数, 在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开, 而且要用实参去代换形参。带参宏定义的一般形式为:

#define 宏名(形参表) 字符串 在字符串中含有各个形参。带参宏调用的一般形式为: 宏名(实参表);
例如:
#define M(y) y*y+3*y /*宏定义*/
:
k=M(5); /*宏调用*/
: 在宏调用时,用实参5去代替形参y, 经预处理宏展开后的语句
为: k=5*5+3*5
#define MAX(a,b) (a>b)?a:b
main(){
    int x,y,max;
    printf("input two numbers: ");
    scanf("%d%d",&x,&y);
    max=MAX(x,y);
    printf("max=%d\n",max);
}


  上例程序的第一行进行带参宏定义,用宏名MAX表示条件表达式(a>b)?a:b,形参a,b均出现在条件表达式中。程序第七行max=MAX(x,y)为宏调用,实参x,y,将代换形参a,b。宏展开后该语句为: max=(x>y)?x:y;用于计算x,y中的大数。对于带参的宏定义有以下问题需要说明:

1. 带参宏定义中,宏名和形参表之间不能有空格出现。

例如把: 
#define MAX(a,b) (a>b)?a:b
写为: 
#define MAX (a,b) (a>b)?a:b 
将被认为是无参宏定义,宏名MAX代表字符串 (a,b)(a>b)?a:b。
宏展开时,宏调用语句: max=MAX(x,y);将变为: max=(a,b)(a>b)?a:b(x,y);这显然是错误的。

2. 在带参宏定义中,形式参数不分配内存单元,因此不必作类型定义

而宏调用中的实参有具体的值。要用它们去代换形参,因此必须作类型说明。这是与函数中的情况不同的。在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中,只是符号代换,不存在值传递的问题。

3. 在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。
 

#define SQ(y) (y)*(y)
main(){
    int a,sq;
    printf("input a number: ");
    scanf("%d",&a);
    sq=SQ(a+1);
    printf("sq=%d\n",sq);
}

上例中第一行为宏定义,形参为y。程序第七行宏调用中实参为a+1,是一个表达式,
在宏展开时,用a+1代换y,再用(y)*(y) 代换SQ,得到如下语句: sq=(a+1)*(a+1); 
这与函数的调用是不同的, 函数调用时要把实参表达式的值求出来再赋予形参。 
而宏代换中对实参表达式不作计算直接地照原样代换。


4. 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。

在上例中的宏定义中(y)*(y)表达式的y都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:

#define SQ(y) y*y
main(){
    int a,sq;
    printf("input a number: ");
    scanf("%d",&a);
    sq=SQ(a+1);
    printf("sq=%d\n",sq);
}
运行结果为:input a number:3

sq=7 同样输入3,但结果却是不一样的。问题在哪里呢? 
这是由于代换只作符号代换而不作其它处理而造成的。 
宏代换后将得到以下语句: sq=a+1*a+1; 由于a为3故sq的值为7。
这显然与题意相违,因此参数两边的括号是不能少的。
即使在参数两边加括号还是不够的,
请看下面程序:

#define SQ(y) (y)*(y)
main(){
    int a,sq;
    printf("input a number: ");
    scanf("%d",&a);
    sq=160/SQ(a+1);
    printf("sq=%d\n",sq);
}

本程序与前例相比,只把宏调用语句改为: sq=160/SQ(a+1); 
运行本程序如输入值仍为3时,希望结果为10。
但实际运行的结果如下:input a number:3 
sq=160为什么会得这样的结果呢?分析宏调用语句,
在宏代换之后变为: sq=160/(a+1)*(a+1);a为3时,
由于“/”和“*”运算符优先级和结合性相同, 
则先作160/(3+1)得40,再作40*(3+1)最后得160。
为了得到正确答案应在宏定义中的整个字符串外加括号, 
程序修改如下

#define SQ(y) ((y)*(y))
main(){
    int a,sq;
    printf("input a number: ");
    scanf("%d",&a);
    sq=160/SQ(a+1);
    printf("sq=%d\n",sq);
}
以上讨论说明,对于宏定义不仅应在参数两侧加括号, 也应在整个字符串外加括号。

5. 带参的宏和带参函数很相似,但有本质上的不同,除上面已谈到的各点外,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的。

/*9.6*/
main(){
    int i=1;
    while(i<=5)
        printf("%d\n",SQ(i++));
}
SQ(int y)
{
    return((y)*(y));
}

/*9.7*/
#define SQ(y) ((y)*(y))
main(){
    int i=1;
    while(i<=5)
        printf("%d\n",SQ(i++));
}

在例9.6函数名为SQ,形参为y,函数体表达式为((y)*(y))。
在例9.7中宏名为SQ,形参也为y,字符串表达式为(y)*(y))。 
两例是相同的。
例9.6的函数调用为SQ(i++),
例9.7的宏调用为SQ(i++),
实参也是相同的。
从输出结果来看,却大不相同。分析如下:
在例9.6中,函数调用是把实参i值传给形参y后自增1。 然后输出函数值。因而要循环5次。输出1~5的平方值。
而在例9.7中宏调用时,只作代换。SQ(i++)被代换为((i++)*(i++))。在第一次循环时,由于i等于1,其计算
过程为:表达式中前一个i初值为1,然后i自增1变为2,因此表达式中第2个i初值为2,两相乘的结果也为2,
然后i值再自增1,得3。在第二次循环时,i值已有初值为3,因此表达式中前一个i为3,后一个i为4, 乘积为
12,然后i再自增1变为5。进入第三次循环,由于i 值已为5,所以这将是最后一次循环。计算表达式的值为5*6
等于30。i值再自增1变为6,不再满足循环条件,停止循环。
从以上分析可以看出函数调用和宏调用二者在形式上相似, 在本质上是完全不同的。

6. 宏定义也可用来定义多个语句,在宏调用时,把这些语句又代换到源程序内。看下面的例子。

#define SSSV(s1,s2,s3,v) s1=l*w;s2=l*h;s3=w*h;v=w*l*h;
main(){
    int l=3,w=4,h=5,sa,sb,sc,vv;
    SSSV(sa,sb,sc,vv);
    printf("sa=%d\nsb=%d\nsc=%d\nvv=%d\n",sa,sb,sc,vv);
}

程序第一行为宏定义,用宏名SSSV表示4个赋值语句,4 个形参分别为4个赋值符左部的变量。
在宏调用时,把4 个语句展开并用实参代替形参。使计算结果送入实参之中。

 

文件包含 #include

文件包含是C预处理程序的另一个重要功能。文件包含命令行的一般形式为: #include"文件名" 在前面我们已多次用此命令包含过库函数的头文件。例如:
#include"stdio.h"
#include"math.h"
文件包含命令的功能是把指定的文件插入该命令行位置取代该命令行, 从而把指定的文件和当前的源程序文件连成一个源文件。在程序设计中,文件包含是很有用的。 一个大的程序可以分为多个模块,由多个程序员分别编程。 有些公用的符号常量或宏定义等可单独组成一个文件, 在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量, 从而节省时间,并减少出错。

对文件包含命令还要说明以下几点:
1. 包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来。

例如以下写法都是允许的: #include"stdio.h" #include<math.h>

但是这两种形式是有区别的:

使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的), 而不在源文件目录去查找;

使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。 用户编程时可根据自己文件所在的目录来选择某一种命令形式。

2. 一个include命令只能指定一个被包含文件, 若有多个文件要包含,则需用多个include命令。

3. 文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

4. 可以通过 -I 指定目录去添加查找目录,通过 -I 指定的是最先查找

 

条件编译 #if #ifdef #ifndef #else #elif #endif 

预处理程序提供了条件编译的功能。 可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。 这对于程序的移植和调试是很有用的。 条件编译有三种形式,下面分别介绍:

1. 第一种形式:
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的功能是,如果标识符已被 #define命令定义过则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式中的#else可以没有, 即可以写为:
#ifdef 标识符
程序段 #endif

#define NUM ok
main(){
struct stu
{
    int num;
    char *name;
    char sex;
    float score;
} *ps;

ps=(struct stu*)malloc(sizeof(struct stu));
ps->num=102;
ps->name="Zhang ping";
ps->sex='M';
ps->score=62.5;
#ifdef NUM
printf("Number=%d\nScore=%f\n",ps->num,ps->score);
#else
printf("Name=%s\nSex=%c\n",ps->name,ps->sex);
#endif
free(ps);
}

由于在程序的第16行插入了条件编译预处理命令, 因此要根据NUM是否被定义过来决定编译那一个printf语句。
而在程序的第一行已对NUM作过宏定义,因此应对第一个printf语句作编译故运行结果是输出了学号和成绩。在程
序的第一行宏定义中,定义NUM表示字符串OK,其实也可以为任何字符串,甚至不给出任何字符串,
写为: #define NUM 也具有同样的意义。只有取消程序的第一行才会去编译第二个printf语句。读者可上机试作。

2. 第二种形式:
#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译, 否则对程序段2进行编译。这与第一种形式的功能正相反。

3. 第三种形式:
#if 常量表达式
程序段1
#else
程序段2
#endif
它的功能是,如常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下,完成不同的功能

#define R 1
main(){
    float c,r,s;
    printf ("input a number: ");
    scanf("%f",&c);
    #if R
    r=3.14159*c*c;
    printf("area of round is: %f\n",r);
    #else
    s=c*c;
    printf("area of square is: %f\n",s);
    #endif
}

本例中采用了第三种形式的条件编译。在程序第一行宏定义中,定义R为1,因此在条件编译时,常量表达式的值为
真, 故计算并输出圆面积。上面介绍的条件编译当然也可以用条件语句来实现。 但是用条件语句将会对整个源程
序进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段1或程序段2, 
生成的目标程序较短。如果条件选择的程序段很长, 采用条件编译的方法是十分必要的。

本章小结

1. 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。程序员在程序中用预处理命令来调用这些功能。

2. 宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。

3. 宏定义可以带有参数,宏调用时是以实参代换形参。而不是“值传送”。

4. 为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。

5. 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。

6. 条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。

7. 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

扩展知识

预处理命令:

#define

#elif

#else

#if

#ifdef

#ifndef

#undef

#include

#include_next

#line

#pragma

#error

#warning

##

1. 一个宏定义如果长度过长为了清晰表达可以用 \ 符号作为一个行的继续符。

#define ran(low,high) \
           ((int)random() % (high-low+1)) \
           + low


2. 宏定义的顺序

#define A 100
sum = A + B;
#define B 200
sum = A + B;

结果为:
sum = 100 + B;
sum = 100 + 200;

3. 替换是递归的,因此它们可以嵌套在另一个内部。 一旦进行了替换,预处理器将处理相同的文本再次进行进一步的替换。

#define TANKARD TSIZE
#define TSIZE 100
int tank1 = TANKARD;
#define TSIZE 200
int tank2 = TANKARD;

结果为:
tank1 = 100
tank2 = 200

4. 对于要定义为具有参数的宏,必须没有空格在宏的名称和括号之间。

#define showint(a) printf("%d\n",a)
#define incrint (a) a++
showint(300);
incrint(bbls);

结果为:
printf("%d\n",300);
(a) a++(bbls)

5. 宏名字不能出现在字符串中。

#define BLOCK 8192
printf("The BLOCK number.\n");

结果为:
The BLOCK number.

6. 参数传入字符串中需要在参数前面加上 #符号进行字符串化。

#define MONCK(ARGTERM) \
    printf("The term " #ARGTERM " is a string\n")

MONCK(A to B);

结果为:
The term A to B is a string

7. 可以定义没有值的宏。 虽然宏没有价值与之相关联,它仍然被定义并且可以用作测试的标志

#define BBB 1  
   printf("BBB = %d\n",BBB);

这样是编译不过的,因为在预处理阶段会替换为

printf("BBB = %d\n",);

这不同于gcc 参数传入例如:

//macro.c
printf("CCC=%d\n",CCC);

gcc -DCCC macro.c 

结果为 1

因为预处理阶段替换为
printf("CCC=%d\n",1);

8. 可变参数宏是具有可变数量参数的宏。 由省略号(三个点)表示,全部存储为单个逗号分隔名为__VA_ARGS__的变量中的字符串,将在其中展开宏。 

#define err(...)fprintf(stderr,__ VA_ARGS__)

err("%s %d\n","The error code: ",48);
结果为:
fprintf(stderr,"%s %d\n","The error code ",48);

9. 可变参数宏可以包含命令参数,只要是可变参数在最后。

#define errout(a,b,...) \
     fprintf(stderr,"File %s Line %d\n",a,b); \
     fprintf(stderr,__VA_ARGS__)


errout(__FILE__,__LINE__,"Unexpected termination\n");
结果为:
File macro.c Line 41
Unexpected termination

但是这里有个强调的地方就是 如果可变参数为空的时候,那边编译是会错误的
因为fprintf(stderr,__VA_ARGS__) 这个stderr 后面有个逗号,为了解决这个问题
使用了 ## 去消除掉

我们先看下上面的如果可变参数为空的时候 预处理是什么样子
,"File %s Line %d\n","macro.c",47); fprintf(
# 47 "macro.c" 3 4
stderr
# 47 "macro.c"
,);  // 可以看到这里多了一个逗号

如果使用了 ## 去消除

# 46 "macro.c"
,"File %s Line %d\n","macro.c",46); fprintf(
# 46 "macro.c" 3 4
stderr
# 46 "macro.c"
);  // 这里就没有了逗号

## 在这里是一种特殊的功能 而不是连接字符作用。



 

#error

#error指令将导致预处理器报告致命错误并停止。 这个可用于捕获尝试在某些程序中编译程序的条件已知不起作用的方式。#error用于生成一个编译错误消息,并停止编译 
用法: #error message 
注:message 不一定要用双引号包围

例如以下内容只能成功编译如果定义了宏__unix__:

#ifndef __unix__
#error "This section will only work on UNIX systems"
#endif

也可以

#ifndef __unix__
#error This section will only work on UNIX systems
#endif

#warning

#warning用于生成编译警告,但不会停止编译

int main(void)
{
   #warning yexiang warning

   return 0;
}

预处理或者编译的时候
macro.c:50:2: warning: #warning yexiang warning [-Wcpp]
 #warning yexiang warning

#inclue_next

#include_next是GNU的一个扩展,并不是标准C中的指令。

#include_next看起来有些复杂,但在这里我将详详细细的说明一下,希望可以把它讲的清楚,让读者了解 :)

 

首先,我将会说明一下这条指令的功能,然后说明一下为什么要引人这条指令,希望能说个明白。

#include_next和#include指令一样,也是包含一个头文件,它们的不同地方是包含的路径不一样。

#include_next的意思就是“包含指定的这个文件所在的路径的后面路径的那个文件”,听起来是不是很坳口,我自己也觉得是这样,但下面举个例子说明就清楚了。

例如有个搜索路径链,在#include中,它们的搜索顺序依次是A,B,C,D和E。在B目录中有个头文件叫a.h,在D目录中也有个头文件叫a.h,如果在我们的源代码中这样写#include <a.h>,那么我们就会包含的是B目录中的a.h头文件,如果我们这样写#include_next <a.h>那么我们就会包含的是D目录中的a.h头文件。#include_next <a.h>的意思按我们上面的引号包含中的解释来说就是“在B目录中的a.h头文件后面的目录路径(即C,D和E)中搜索a.h头文件并包含进来)。#include_next <a.h>的操作会是这样的,它将在A,B,C,D和E目录中依次搜索a.h头文件,那么首先它会在B目录中搜索到a.h头文件,那它就会以B目录作为分割点,搜索B目录后面的目录(C,D和E),然后在这后面的目录中搜索a.h头文件,并把在这之后搜索到的a.h头文件包含进来。这样说的话大家应该清楚了吧。

 

还有一点是#include_next是不区分<>和""的包含形式的。

 

现在来说说为什么要引人这条指令!

假如,你要创建一个新的头文件,而这个新的头文件和现在已有的头文件有相同的名字,而且你想用你的这个新的头文件,那么你要做的就是把这个新的头文件放在#include指令的搜索路径的前面,即是在旧的头文件的前面新的头文件首先被搜索到,这样你就可以使用你这个新的头文件。但是你在另一个源代码文件中想使用旧的头文件了,那怎么办!有个办法就是使用绝对路径来搜索,那么就不存在这样的问题了。问题出在,如果我们把头文件的位置移动了,移到了其它的目录里了,那我们就得在相应的源码文件中修改这个包含的绝对路径,如果一个源码文件还好,但如果是大型工程的话,修改的地方多了就容易出问题。

又进一步说,如果你这个新的头文件引用了旧的头文件,而这个新的头文件如果没有使用只编译一次的预处理语句包含(即#ifndef,#endif等),那么就会陷入一个无限的递归包含中,这个新的头文件就会无限的包含自己,就会出现一个致命的错误。如果我们使用#include_next就会避免这样的问题。

在标准的C中,这没有一个办法来解决上面的问题的,因此GNU就引人了这个指令#include_next。

 

下面再举一个#include_next的例子。

假设你用-I选项指定了一个编译包含的路径 '-I /usr/local/include',这个路径下面有个signal.h的头文件,在系统的'/usr/include'下也有个signal.h头文件,我们知道-I选项的路径首先搜索。如果我们这样 #include <signal.h> 包含,就会包含进/usr/local/include下的signal.h头文件;如果是 #include_next <signal.h>,就会包含'/usr/include'下的signal.h头文件。

GNU建议一般没有其它可取代的办法的情况下才使用#include_next的。

 

又一个例子,如在系统头文件stdio.h中,里面有个函数(应该说是一个宏)getc,它从标准输入中读取一个字符。你想重新定义一个getc,并放到自己新建的stdio.h文件中,那么你可以这样使用你自定义的getc。

#include_next "stdio.h"
#undef getc
#define getc(fp) ((int)'x')

#line

#line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号 
 用法: #line number filename 
注:filename可省略,filename为字符串

#line编译指示字的本质是重定义 _ _ LINE _ _ 和_ _ FILE _ _ 
从#line number filename语句的下一行开始,行号改为number的值。

#include<stdio.h>

int main(void)
{

   printf(" __LINE__ = %d , __FILE__ = %s\n",__LINE__,__FILE__);
#line 20
   printf(" __LINE__ = %d , __FILE__ = %s\n",__LINE__,__FILE__);
#line 40 "new.c"  
   printf(" __LINE__ = %d , __FILE__ = %s\n",__LINE__,__FILE__);

   return 0 ;
}

结果为:

 __LINE__ = 7 , __FILE__ = line.c
 __LINE__ = 20 , __FILE__ = line.c
 __LINE__ = 40 , __FILE__ = new.c

#pragma

在所有的预处理指令中,#pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma 指令对每个编译器给出了一个方法,在保持与C 和C ++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。

其格式一般为:
   #pragma para
其中para 为参数,下面来看一些常用的参数。

1.  #pragma GCC dependency file +【other text】

依赖项pragma根据指定文件时间戳测试当前文件的时间戳。如果另一个文件较新,则会发出警告消息。

#pragma GCC dependency "lexgen.tbl"

如果指定文件较新

warning: current file is older than "lexgen.tbl"

如果有 other text 可以作为警告消息一部分

#pragma GCC dependency "lexgen.tbl" Header lex.h needs to be updated

show.c:26: warning: current file is older than "lexgen.tbl"
show.c:26: warning: Header lex.h needs to be updated

2. #pragma GCC poison name

指定name出现时候报错

#pragma GCC poison printf

printf("aaaaa \n");

预处理时候

new.c:45:1: error: attempt to use poisoned "printf"

3. __pragma() 关键字

编译器还支持 __pragma 关键字,该关键字具有与 #pragma 指令相同的功能,但可用于宏定义中的内联。 #pragma 指令不能用于宏定义中,因为编译器会将指令中的数字符号(“#”)解释为字符串化运算符 (#)

下面的代码示例说明如何在宏中使用 __pragma 关键字。 此代码摘自“编译器 COM 支持示例”中的 ACDUAL 示例中的 mfcdual.h 头:

#define CATCH_ALL_DUAL \  
CATCH(COleException, e) \  
{ \  
_hr = e->m_sc; \  
} \  
AND_CATCH_ALL(e) \  
{ \  
__pragma(warning(push)) \  
__pragma(warning(disable:6246)) /*disable _ctlState prefast warning*/ \  
AFX_MANAGE_STATE(pThis->m_pModuleState); \  
__pragma(warning(pop)) \  
_hr = DualHandleException(_riidSource, e); \  
} \  
END_CATCH_ALL \  
return _hr; \  

4. #pragma message

message 参数:Message 参数是我最喜欢的一个参数,它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
   #pragma message(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来

#pragma message("hello message")

编译时候:

new.c:46:9: note: #pragma message: hello message

5. #pragma code_seg

另一个使用得比较多的pragma 参数是code_seg。格式如:
   #pragma code_seg( ["section-name"[,"section-class"] ] )
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。

6. #pragma once
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在Visual C++6.0 中就已经有了,但是考虑到兼容性并没有太多的使用它。

7. #pragma hdrstop

#pragma hdrstop 表示预编译头文件到此为止,后面的头文件不进行预编译。BCB 可以预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。有时单元之间有依赖关系,比如单元A 依赖单元B,所以单元B 要先于单元A 编译。你可以用#pragma startup 指定编译优先级,如果使用了#pragma package(smart_init) ,BCB就会根据优先级的大小先后编译。

8. #pragma resource

#pragma resource "*.dfm"表示把*.dfm 文件中的资源加入工程。*.dfm 中包括窗体外观的定义。

9.  #pragma warning

   #pragma warning( disable : 4507 34; once : 4385; error : 164 )
等价于:
   #pragma warning(disable:4507 34) // 不显示4507 和34 号警告信息
   #pragma warning(once:4385) // 4385 号警告信息仅报告一次
   #pragma warning(error:164) // 把164 号警告信息作为一个错误。
同时这个pragma warning 也支持如下格式:
   #pragma warning( push [ ,n ] )
   #pragma warning( pop )  //这里n 代表一个警告等级(1---4)。
   #pragma warning( push )保存所有警告信息的现有的警告状态。
   #pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告等级设定为n。
   #pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作的一切改动取消。例如:
   #pragma warning( push )
   #pragma warning( disable : 4705 )
   #pragma warning( disable : 4706 )
   #pragma warning( disable : 4707 )
   //.......
   #pragma warning( pop )
在这段代码的最后,重新保存所有的警告信息(包括4705,4706 和4707)。

10.  #pragma comment

#pragma comment(...)
该指令将一个注释记录放入一个对象文件或可执行文件中。

常用的lib 关键字,可以帮我们连入一个库文件。比如:
 #pragma comment(lib, "user32.lib")
该指令用来将user32.lib 库文件加入到本工程中。

linker:将一个链接选项放入目标文件中,你可以使用这个指令来代替由命令行传入的或者在开发环境中设置的链接选项,你可以指定/include 选项来强制包含某个对象,例如:
#pragma comment(linker, "/include:__mySymbol")

11.  #pragma pack()

这里重点讨论内存对齐的问题和#pragma pack()的使用方法。
使用指令#pragma pack (n),编译器将按照n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。

在#pragma pack (n)和#pragma pack ()之间的代码按n 个字节对齐。但是,成员对齐有一个重要的条件,即每个成员按自己的方式对齐.也就是说虽然指定了按n 字节对齐,但并不是所有的成员都是以n 字节对齐。其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是n 字节)中较小的一个对齐,即:min( n, sizeof( item )) 。并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。看如下例子:

 

#pragma pack(8)
struct TestStruct4
{
   char a;
   int b;
}t4;
struct TestStruct5
{
   char c;
   TestStruct4 d;
   long long e;
}t5;
#pragma pack()

t4.a 按1 字节对齐, t4.b 是4 个字节,默认是按4 字节对齐, 但是 默认< pack(8),这时就选取最小 按4 字节对齐,所以sizeof(TestStruct4)应该为8;

内存图为:

     a x x x b b b b

t5,c 和 t4.a 一样,按1 字节对齐, 而d 是个结构,它是8 个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个, t4最大是4.所以t5.d 就是按4 字节对齐, t5.e 是8 个字节,它是默认按8字节对齐,和指定的一样,所以它对到8 字节的边界上,这时,已经使用了12 个字节了,所以又添加了4 个字节的空,从第16 个字节开始放置成员e.这时,长度为24,已经可以被8(成员e 按8字节对齐)整除.这样,一共使用了24 个字节.

内存图为 :

    c x x x a x x x

    b b b b x x x x

    e e e e e e e e
 

#pragma pack(4)
struct TestStruct4
{
   char a;
   int b;
}t4;
struct TestStruct5
{
   char c;
   TestStruct4 d;
   long long e;
}t5;
#pragma pack()

t4.a 按1 字节对齐, t4.b 是4 个字节,默认是按4 字节对齐, 但是 默认== pack(4),这时就选取最小 按4 字节对齐,所以sizeof(TestStruct4)应该为8;

内存图为:

     a x x x

     b b b b

t5,c 和 t4.a 一样,按1 字节对齐, 而d 是个结构,它是8 个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个, t4最大是4.所以t5.d 就是按4 字节对齐, t5.e 是8 个字节,它是默认按8字节对齐 但是这个时候 默认 > pack(4), 所以选取 4 它对到4字节的边界上,这时, 一共使用了20 个字节.

内存图为 :

    c x x x

   a x x x

   b b b b

   e e e e

   e e e e

 


这里有三点很重要:
首先,每个成员分别按自己的方式对齐,并能最小化长度。
其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。

补充一下,对于数组,比如:char a[3];它的对齐方式和分别写3 个char 是一样的.也就是说它还是按1 个字节对齐.如果写: typedef char Array3[3];Array3 这种类型的对齐方式还是按1个字节对齐,而不是按它的长度。

但是不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。

另外,注意别的#pragma pack 的其他用法:
#pragma pack(push) //保存当前对其方式到packing stack
#pragma pack(push,n) 等效于
#pragma pack(push)
#pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按n 字节对齐

#pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐方


3.结构体对齐规则

结构体中各个成员按照它们被声明的顺序在内存中顺序存储。
1)将结构体内所有数据成员的长度值相加,记为sum_a; 
2)将各数据成员内存对齐,按各自对齐模数而填充的字节数累加到和sum_a上,记为sum_b。对齐模数是【该数据成员所占内存】与【#pragma pack指定的数值】中的较小者。
3)将和sum_b向结构体模数对齐,该模数是【#pragma pack指定的数值】、【未指定#pragma pack时,系统默认的对齐模数8字节】和【结构体内部最大的基本数据类型成员】长度中数值较小者。结构体的长度应该是该模数的整数倍。
3.1 基本数据类型所占内存大小


以下例子均按32bit编译器处理。
3.2 Test1

#pragma pack(4)
struct Test1
{
    char c;
    short sh;
    int a;
    float f;
    int *p;
    char *s;
    double d;
};

总共占28Bytes。 c的偏移量为0,占1个Byte。sh占2个Byte,它的对齐模数是2(2<4,取小者),存放起始地址应该是2的整数倍,因此c后填充1个空字符,sh的起始地址是2。a占4个Byte,对齐模数是4,因此接在sh后存放即可,偏移量为4。f占4个字节,对齐模数是4,存放地址是4的整数倍,起始地址是8。p,s的起始地址分别是12,16。d占8个字节,对齐模数是4(4<8),d从偏移地址为20处存放。存放后结构体占28个字节,是4的整数倍不用补空字符。

 

struct Test2
{
    char c;
    double d;
    int a;
    short sh;
    float f;
    int *p;
    char *s;        
};

将Test1个变量的顺序换一下位置,结构体Test2占用内存32Byte,可见写结构体时,将各个变量按所占内存从小到大排列所占结构体所占内存较小。


3.3关于静态变量static

静态变量的存放位置与结构体实例的存储地址无关,是单独存放在静态数据区的,因此用siezof计算其大小时没有将静态成员所占的空间计算进来。

#pragma pack(4)
struct Test3
{
    char c;
    short sh;
    int a;
    float f;
    int *p;
    char *s;
    double d;
    static double sb;
    static int sn;
};
sizeof(Test3)=28

3.4关于类

空类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。

(一)类内部的成员变量:

普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。
(二)类内部的成员函数:

普通函数:不占用内存。
虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的

 

#pragma pack(4)
class cBase{};
sizeof(cBase)=1


3.4.1 不包含虚函数的类

#pragma pack(4)
class CBase1
{
private:
    char c;
    short sh;
    int a;
public:
    void fOut(){ cout << "hello" << endl; }
};
不包含虚函数时,对于类中的成员变量按结构体对齐方式处理,普通函数函数不占内存。sizeof(CBase1)=8


3.4.2 包含虚函数的类

#pragma pack(4)
class CBase2
{
private:
    char c;
    short sh;
    int a;
public:
    virtual void fOut(){ cout << "hello" << endl; }
};
包含虚函数时,类中需要保存虚函数表的入口地址指针,即需要多保存一个指针。这个值跟虚函数的个数多少没有
关系。sizeof(CBase2)=12


3.4.3 子类子类所占内存大小是父类+自身成员变量的值。特别注意的是,子类与父类共享同一个虚函数指针,因此当子类新声明一个虚函数时,不必在对其保存虚函数表指针入口。

#pragma pack(4)
class CBase2
{
private:
    char c;
    short sh;
    int a;
public:
    virtual void fOut(){ cout << "virtual 1" << endl; }
};
class cDerive :public CBase
{
private :
    int n;
public:
    virtual void fPut(){ cout << "virtual 2"; }
};

sizeof(cDerive)= sizeof(cBase)+sizeof(int n) = 16

 

预处理宏

__DATE__,字符串常量类型,表示当前所在源文件的编译日期,输出格式为Mmm dd yyyy(如May 27 2006)。
__TIME__,字符串常量类型,表示当前所在源文件的编译日期,输出格式为hh:mm:ss(如09:11:10)。
__FILE__,字符串常量类型,表示当前所在源文件名,且包含文件路径。
__LINE__,整数常量类型,表示当前所在源文件中的行号。
__FUNCTION__,字符串常量类型,表示当前所在函数名。

__func__ 和 __FUNCTION__ 一样

__GNUC__  是编译gcc 主版本号

__GNUC_MINOR__ 是编译gcc 次版本号

__STDC__ 如果符合ISO 标准,那么为 1

特殊的关键字:

Operator Name         Equivalent Punctuation Form
and                                              &&
and_eq                                        &=
bitand                                           &
bitor                                              |
compl                                           ~
not                                                !
not_eq                                         !=
or                                                  ||
or_eq                                            |=
xor                                                ^
xor_eq                                         ^=

定义在iso64.h

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HarkerYX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值