预处理与编译

Posted noticeable

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了预处理与编译相关的知识,希望对你有一定的参考价值。

1、预处理

  预处理也称为预编译,它为编译做准备工作,主要进行代码文本的替换工作,用于处理#开头的指令,其中预处理产生编译器的输出。下表是一些常见的预处理指令及其功能。 

技术分享图片

  经过预处理器处理的源程序与之前的源程序会有所不同,在预处理阶段所进行的工作只是纯粹的替换与展开,没有任何功能,所以在学习#define命令时只有阵阵理解这一点,才不会对此命令引起误解并误用。

 

1、#include<filename.h>和#include“filename.h"有什么区别

  对于#include<filename.h>,编译器先从标准库路径开始搜索filename.h。

  而对于#include"filename.h",编译器先从用户的工作路径开始搜索filename,h,然后取寻找系统路径,使得自定义文件较快。

 

2、#define的缺陷

  由于宏定义在预处理阶段进行,主要做的是字符替换工作,所以它存在一些固有的缺陷:

  1. 它无法进行类型检查。宏定义是在编译前进行字符的替换,因为还没有编译,不能编译前就检查好类型是否匹配,而只能在编译时才知道,所以不具备类型检查功能。
  2. 由于优先级的不同,使用宏定义时,可能会存在副作用。
  3. 无法单步调试
  4. 会导致代码膨胀。
  5. 在C++中,使用宏无法操作类的私有数据成员。

3、含参数的宏与函数有什么区别

  含参数的宏有时完成的是函数实现的功能,但是并非所有的函数都可以被含参数的宏所替代。具体而言,含参数的宏与函数的特点如下:

  1. 函数调用时,首先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
  2. 函数调用是在程序运行时处理的,它需要分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值的传递处理,也没有“返回值”的概念。
  3. 对函数中的实参和形参都要定义类型,两者的类型要求一致,如果不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号表示,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
  4. 调用函数只可得到一个返回值,而用宏可以设法得到几个结果。
  5. 使用宏次数多时,宏展开后源程序会变的很长。
  6. 宏替换不占用运行时间,而函数调用占用运行时间
  7. 参数每次用于宏定义时,他们都将重新求值,具有副作用的参数可能会产生不可预料的结果。而参数在函数被调用前只求值一次,在函数中多次调用参数并不会导致多种求值过程,参数的副作用并不会造成任何特殊的问题。

4、typedef与define有什么区别

  typedef与define都是替一个对象取一个别名,一次来增强程序的可读性,但是它们在使用和作用上也存在者以下几个方面的不同:

  1. 原理不同。#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检查,不管含义是否正确照样带入,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef是关键字,他在编译是处理,所以typedef有类型检查的功能。它在自己作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符typedef。
  2. 功能不同。typedef用来定义类型的别名,这写类型不只包含内部类型(int、char、等),还宝库奥自定义类型(如struct),可以起到使类型易于记忆的功能。例如
    typedef int (*PF)(const char*,const char *);
    

      定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为const char *。typedef还由另外一个重要的用途,那就是定义机器无关的类型。例如可以定义一个叫做REAL的浮点类型,在目标机器上它可以获得最高的精度:typedef long double REAL,在不支持long double的机器上,该typedef 是这样的typedef double REAL。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

  3. 作用域不同。#define没有作用域,只要在之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。程序示例如下:
    void fun()
    {
        #define A int
    }
    void gun()
    {
        //在这里可以定义A,因为宏替换没有作用域,
        //如果上面用detypedef,这里就不能调用A
    }
    

      

  4. 对指针的操作不同。两者修饰指针类型时,作用不同。
    #define INTPTR1 int*
    typedef int* INTPTR2;
    INTPTR1 p1,p2;
    INTPTR2 p3,p4;
    

      这里INTPTR1 p1,p2进行字符串替换后变成int* p1,p2,要表达的意义是声明一个指针变量和要给整型变量p2.而 INTPTR p3,p4由于INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于int* p1,*p2.从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用typedef为一种数据类型起别名是带有一定含义的。程序示例如下:

    #define INTPTR1 int*
    typedef int* INTPTR2;
    int a=1;
    int b=2;
    int c=3;
    const INTPTR1 p1=&a;//表示p1是一个常量指针,既不可以通过p1去修改p1指向的内容,但p1可以指向其他内容。
    const INTPTR2 p2=&b;//由于INTPTR2表示是一个指针类型,因此用const去限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他内容,但可通过p2修改当前指向的内容。
    INTPTR2 const p3=&c;//一个指针常量
    

       

5、C++中宏定义于内联函数有什么区别

  宏代码本身不是函数,但使用起来却像函数,预处理器用赋值宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。内联函数是代码被插入到调用者代码处的函数。对于C++而言,内联函数也不是万能的,它的使用有限制,它只适合函数体内diamagnetic简单的函数使用,不能包含复杂的结构控制语句(如while、switch),并且内联函数本身不能直接调用递归函数。

  两者的区别主要表现在以下几个方面:第一,宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;第二,宏定义没有类型检查,而内联函数有类型检查。

 

6、定义常量#define和const谁更好

  #define只是用来进行单纯的文本替换,define变量的声明周期止于编译期,不分配内存空间,它存在于代码段,在实际程序中他只是一个常数,一个命令中的参数并没有实际的存在;而const常量存在于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实是的存在,并且可以被调用、传递。

  const常量有数据类型,而define常量没有数据类型。编译器可以对const常量进行类型安全检查,而define不行。

  由于const修饰的变量可以排除程序之间的不安全因素,保护程序中的常量不被修改,而且对数据类型也会进行相应的检查,极大的提高了程序的健壮性,所以一般更倾向于用const来定义常量类型。

 

 

2、编译

  编译是程序执行执行过程中的一个重要步骤,了解程序的编译过程以及编译方式都对程序的开发起着至关重要的作用。

1、编译和链接的区别是什么

  在多到程序环境中,要想将用户源代码变成一个可以在内存中执行的程序,通常分为3个步骤:编译、链接、载入。

  1. 编译:将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化后编译成若干个目标模块。可以理解为将高级语言翻译成计算机可以理解的二进制码,即机器语言。
  2. 链接:由链接程序将编译后形成的一组目标模块以及它们所需要的库函数链接在一起,形成一个完整的载入模型。链接主要解决模块间相互引用的问题,分为地址和空间分配,符号解析和重定位几个步骤。在编译阶段生成目标文件时,会暂时搁置那些外部引用,而这些外部引用就是在链接时进行确定的,链接器在链接时,会根据符号名称去相应模块中寻找对应符号。待符号确定之后,链接器会重写之前哪些未确定的符号的地址,这个过程就是重定位。链接一般分为静态链接、载入时动态链接以及运行时动态链接3种。
  3. 载入:由载入程序将载入模块载入内存。

  编译和链接为将用户程序从硬盘上调入内存并将其转换成可执行程序服务的。用编译器时的compile就是在进行编译,link就是链接,运行程序可以看到。

2、编译型语言于解释型语言的区别是什么

  编译型语言:编译是指在应用源程序执行之前,就将程序源代码“翻译”成目标代码(机器语言),因此其目标程序可以脱离其语言环境独立执行,使用方便、效率较高。但应用程序一旦需要修改,必须先修改源代码,在重新编译生成新的目标文件(*.obj)才能执行,只有目标文件而没有源代码,修改很不方便。现在大多数的编译语言都是编译型的。编译程序将源程序翻译成目标程序后保存在另一个文件中,该目标程序可脱离编译程序直接在计算机上多次运行。CC++FortranVisual Foxpro等都是编译实现的。

  解释性语言:解释型语言的实现中,翻译器并不产生机器代码,而是产生易于执行的中间代码。这种中间代码与机器代码不同,中间代码的解释是由软件支持的,不能直接使用硬件,软件解释器通常会导致执行效率较低。用解释性语言编写的程序是由另一个可以理解中间代码的解释程序执行的。与编译程序不同的是,解释程序的任务是逐一将源程序的语句解释成可执行的机器指令,不需要将源程序翻译成目标代码后再执行。解释程序的优点是当语句出现语法错误时,可以立即发现。而程序员在程序开发期间就能进行校正。对于解释型Basic语言,需要一个专门的解释器解释执行Basic程序,每条语句只有在执行时才被翻译。这种解释性语言每执行一次就翻译一次,效率低下。例如:Tcl、Perl、VBScript、javascriptdeng .

 需要注意的是,Java是一类特殊的编程语言,Java程序也需要编译,但是却没有直接编译为机器语言,而是编译为字节码,然而Java虚拟机上以解释方式执行字节码。

 

3、如何判断一段程序是由C编译程序还是由C++编译程序编译的

  如果编译器在编译cpp问价,那么cplusplus就会被定义,如果是一个C文件在被编译,那么__STDC__就会被定义。__STDC__是预定义宏,当它被定义后,编译器将按照ANSIC标准来编译C语言程序。

  所以,可以采用如下程序示例判断。

#include "stdafx.h"
#include<stdio.h>

#ifdef _cplusplus
#define USING_C 0
#else
#define USING_C 1
#endif
#include <stdio.h>

int main()
{
	if (USING_C)
		printf("C
");
	else
		printf("C++
");
	return 0;
}

  在C编译环境下,程序运行结果如下:

技术分享图片

  编写C与C++兼容的代码所需的宏如下:

#ifdef _cplusplus
extern "C" {
#endif
	//具体的代码
#ifdef _cplusplus
}
#endif

  在上例中,_cplusplus是cpp中的自定义宏,当定义了这个宏时,其表示这是一段cpp的代码。也就是说,上面代码的含义为如果这是一段cpp的代码,那么加入extern "C"{和}处理其中的代码。

  考虑到C语言没有重载的概念,所以C编译器编译的程序例,所有函数只有函数名对应的入口,而由于C++支持重载,如果只有函数名对应的入口,则会出现混淆,所以C++编译器编译的程序,应该是函数名+参数类型列表对应到入口。

  因为main函数是整个函数的入口,所以main是不能有重载的。如果一个程序只有main()函数,是无法确认是C还是C++编译器编译的,此时可以通过nm来查看函数名入口。例如,函数inf foo(int i,float j),C编译的程序通过nm查看如下:foo 0x567xxxxxx(地址).C++编译程序,通过nm查看为foo(int ,float) 0x567xxxxxx.

 


4、在C++程序中调用C编译器编译后的函数,为什么要加extern "C"

  C++是一种面向对象编程的语言,支持函数重载,而C语言是面向过程的编程语言,不支持函数重载,所以函数被C++编译后在库中的名字于C语言的不同。如果声明一个C语言函数float f(int a,char b),C++的编译器就会将这个名字变成像_f_int_char之类的东西以支持函数重载。然而C语言编辑器的库一般不执行改转换,所以它的内部名为_f,这样链接器将无法解释C++对函数f()的调用。

  C++提供了C语言替代连接说明符号extern "C"来解决名字匹配问题,extern是C/C++语言中表明函数和全局变量作用范围的关键字,该关键字告诉编译器,其声明的函数和变量可以在模块或其他模块中使用。extern后跟一个字符串来指定想声明的函数的连接类型,后面是函数声明。

  extern "C" float f(int a,char b);

  该句目的是告诉编译器f()是C连接的,这样C++就不会转换函数名。标准的连接类型指定符有"C""C++"两种,但可以选择同样的方法支持其他语言。如果有一组替代连接的声明,可以把它们放在花括号里:

extern "C"
{
    float f(int a,char b);
..//其他函数
}
或者写成
extern "C"
{
    #include  "Myheader.h"
    ..//其他C头文件
}

  这就告诉C++编译器,函数f是采用C语言方式链接的,应用到库中找名字_f而不是找_f_int_char。C++编译器已经对C标准库的头文件做了extern "C"处理,所以可以用#include直接引用这些头文件。

 

以上是关于预处理与编译的主要内容,如果未能解决你的问题,请参考以下文章

你如何在 python 中处理 graphql 查询和片段?

Notepad++编辑器——Verilog代码片段直接编译

导致资产预编译在heroku部署上失败的代码片段

如何有条件地将 C 代码片段编译到我的 Perl 模块?

片段处理屏幕方向与操作栏中的选项卡

GCC:编译成程序集并明确与代码的对应关系?