欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++入门——预处理的二三事

程序员文章站 2022-05-04 19:44:39
...

预处理

概念

预处理也称为预编译,它为编译做预备工作用于处理#开头的指令。

常见的预处理指令及其功能

  1. #:空指令,无任何效果

  2. #include:包含一个源代码文件,把源文件中的#include扩展为文件正文,即把包含的.h文件找到并扩展到#include所在处

  3. #define:定义宏

  4. #undef:取消已定义的宏

  5. #if:条件编译指令,如果给定条件为真,则编译下面代码

  6. #ifdef:条件编译指令,如果宏已经定义,则编译下面代码

  7. #ifndef:条件编译指令,如果宏没有定义,则编译下面代码

  8. #elif:条件编译指令,如果前面的#if给定条件不为真,当前条件为真,则编译下面代码

  9. #endif:结束一个#if…#else条件

  10. #error:停止编译并显示错误信息

经过预处理器处理的源程序与之前的源程序会有所不同,在预处理阶段进行的工作只是纯粹地替换与展开,没有任何计算功能

C/C++头文件中的ifndef/define/endif的作用

主要用于防止头文件的重复包含和编译

如果一个项目中存在两个C文件,而这两个C文件都include了同一个头文件,当编译时,这两个C文件要一同编译成一个可运行文件,可能会产生大量的声明冲突。而解决的办法是把头文件的内容都放在#ifndef#endif中,格式如下:

#inndef<标识>
    #define<标识>
    //引用头文件
    ...
#endif

上述代码的作用是当标识没有由#define定义过时,则定义标识。“标识”在理论上来说可以是*命名的,但每个头文件的这个“标识”都应该是唯一的。“标识”的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如stdio.h

#ifndef _STDIO_H_
#define _STDIO_H_
...
#endif

假设一个工程中有如下4个文件:main.cpp,a.h,b.h,c.h
其中main.cpp的头部为:

#include "a.h"
#include "b.h"

而a.h和b.h的头文件中都引用了c.h,此时如果在c.h中定义了一个变量如下:

int count;

在这种情况下,编译器会编译2次c.h,导致2次count的定义,编译器会报重定义错误。
如果在c.h中假设ifndef/define/endif,就可以防止这种重定义错误。实现方式如下:

#ifndef _C_H_
#define _C_H_
    int count;
#endif

在这种情况下,在编译完a.h,c.h后_C_H_就被定义过了,因此当编译b.h,c.h的时候,由于_C_H_被定义过了,此时就不会再重新编译int count

include<filename.h>和#include "filename.h"的区别

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

头文件的作用

  1. 通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便向用户公布,只用向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码

  2. 头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担

#define有哪些缺陷

  1. 它无法进行类型检查。宏定义是在预处理阶段进行字符的替换,因此无法检查类型

  2. 由于优先级的不同,在使用宏定义时,可能会存在副作用。例如,对于表达式的运算就可能存在潜在的问题

  3. 无法单步调试

  4. 会导致代码膨胀。由于宏定义是文本替换,需要对代码进行展开,相比较函数调用的方式,会存在较多的冗余的代码

  5. 在C++语言中,使用宏无法操作类的私有数据成员

含参数的宏与函数

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

  1. 函数调用时,首先求出实参表达式的值,然后带入形参,而使用带参的宏只是进行简单的字符替换

  2. 函数调用是在程序运行时处理的,它需要分配临时的内存单元,在函数调用的时候需要参数传递、压栈与弹栈操作;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值的传递处理,也没有“返回值”的概念。也就是说,函数调用占用的是运行时间,而宏替换占用的是编译时间。因此,使用宏会有更好的执行效率

  3. 对函数中的实参和形参都要定义类型,两者的类型要求一致,如果不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据

  4. 使用宏次数多时,宏展开后源程序会变得很长,因为每展开一次都使程序内容增长,而函数调用不会使源程序变长

  5. 参数每次用于宏定义时,它们都将重新求值,由于多次求值,具有副作用的参数可能会产生不可预料的结果。而参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多次求值过程,参数的副作用并不会造成任何特殊的问题

  6. 在C++语言中,宏不能访问对象的私有成员,而成员函数可以访问

  7. 可以在函数体内设置单步调试,但是不能在宏定义中设置断点进行单步调试。一般来说,用宏代表简短的表达式比较合适

不使用大于、小于、if语句,定义一个宏比较两个整数a、b的大小

如果需要返回较大的值,在宏定义可以写为如下形式:

#define MAX(a,b) (abs((a)-(b)) == ((a)-(b))?(a):(b))

另外一种方法:

#define MAX (a,b)(((a)-(b))&0x80000000)?(b):(a)

如何判断一个变量是有符号数还是无符号数

  1. 采用取反操作。数据在计算机中都是以二进制的0或1存储的,如果是由符号数,正数以0开头,负数以1开头,取反操作会把所有的1改为0,所有的0改为1;如果是无符号数则不会受此影响
#define ISUNSIGNED(a) (a>=0 && ~a>=0)
  1. 通过改变符号位判断。把变量a进行一个位运算,将最高位置为1,判断是否大于0
#define ISUNSIGNED(a) ((a)|(1<<31))>0

#define TRACE(S) (printf("%s\n", #S), S)的意思

“#”进行宏字符串链接,在宏中把参数解释为字符串,不可以在语句中直接使用。在宏定义中,printf("%s\n", #S)被解释为printf("%s\n", "S")

#define TRACE(S) (printf("%s\n", #S), S)还用到了逗号表达式。逗号表达式从左往右计算,但返回右边的数据值作为表达式值。

语句int b = TRACE(a);,会被替换为int b = (prntf("%s\n", "a"), a);,显然,这个printf的输出结果为“a”,同时代码执行后b=a=5

不使用sizeof,求int占用的字节数

可以使用宏定义实现:

#define MySizeof(Value) (char*)(&Value + 1) - (char*)&Value

也可以使用模板函数:

template <class Any>
int lengthofArray(Any *p)
{
    return int(p+1) - int(p);
}

使用宏求结构体的内存偏移地址

#define OffSet(type, field) ((size_t)&((type *)0->field))

其中,size_t是表示长度(尺寸)的类型,这个类型是由typedef unsigned int size_t;定义的,一般用于保存一些长度信息,比如数组的长度、字符串的长度等。

在C语言中,ANSI C标准允许值为0的常量被强制转换成任何一种类型的指针,而且转换结果是一个控制住,即NULL指针。如果利用这个NULL指针访问type的成员,当然是非法的,但是因为&(((type *)0->field)的意图只不过是计算filed字段的地址,因此是合法的

C语言编译器根本就不生成访问type的代码,而仅仅是根据type的内容布局和结构体实例首址在编译期计算这个(常量)地址,这样就完全避免了通过NULL指针访问内存可能出现的问题。同时又因为地址为0,所以这个地址的值就是字段相对于结构体基址的偏移

用sizeof判断数组中元素个数

只需要用整个数组的sizeof去除以一个元素的sizeof即可求出数组中元素的个数。

#define Count(array) (sizeof(array)/sizeof(array[0]))

枚举与define的区别

  1. 枚举常量是实体中的一种,但宏定义不是实体
  2. 枚举常量属于常量,但宏定义不是常量
  3. 枚举常量具有类型,但宏没有类型,枚举变量具有与普通变量相同的性质,如作用域、值等,但是宏没有
  4. #define宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值
  5. 一般在编译期里,可以调试枚举变量,但是不能调试宏常量
  6. 枚举可以一次性定义大量相关的常量,而#define宏一次只能定义一个

typedef与define的区别

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

  1. 原理不同。#define是C语言定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检查。typedef是关键字,它在编译时处理,所以typedef具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里使用标识符typedef

用typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如:

typedef int a[10];

表示a是整型数组类型,数组长度为10.然后就可以用a说明变量,例如语句a s1, s2;完全等效于语句int s1[10], s2[10];。同理,typedef void (*p)(void);表示p是一种指向void型的指针类型

  1. 功能不同。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,在double都不支持的机器上,该typedef看起来会是:typedef float REAL

#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等

  1. 作用域不同。#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域

  2. 对指针的操作不同。两者修饰指针类型时,作用不同。

#define INTPTR1 int*
type int* INTPTR2;
INTPTR1 p1, p2;
INTPTR2 p3, p4;

INTPTR1 p1, p2进行字符串替换后变成int* p1, p2,要表达的意义是声明一个指针变量p1和一个整型变量p2。而INTPTR2 p3, p4,由于INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于int *p1, *p2

程序示例如下:

#define INTPTR1 int*
typedef int* INTPTR2;
int a = 1;
int b = 2;
int c = 3;
const INTPTR1 p1 = &a;
const INTPTR2 p2 = &b;
INTPTR2 const p3 = &c;

上述代码中,const INTPTR1 p1表示p1是一个常量指针,即指针指向可以改,指针指向的内容不能改。而对于const INTPTR2 p2,由于INTPTR2表示的是一个指针类型,因此用const去限定,表示*了这个指针类型,因此p2是一个指针常量,指针指向不可以改,指针指向的内容可以改。INTPTR2 const p3同样声明的是一个指针常量

宏定义与内联函数的区别

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

二者的主要区别是:

  1. 宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码

  2. 宏定义没有类型检查,而内联函数有类型检查

内联函数与普通函数的区别

内联函数的参数传递机制与普通函数相同,但是编译器会在每个调用内联函数的地方将内联函数的内容展开,这样既避免了函数调用的开销,又没有宏机制的缺陷

内联函数和普通函数的最大区别在于其内部的实现,普通函数在被调用时,系统首先要跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个复制;而内联函数则不需要进行一个寻址的过程,当执行到内联函数时,此函数展开,如果在N处调用了此内联函数,则此函数就会有N个代码段的复制。

内联函数也并非万能的,在使用的过程中也存在一定的局限性,如果函数体过大,编译器也会放弃内联方式,而采用普通的方式调用函数。此时,内联函数就和普通函数执行效率一样了

inline函数是否可以是递归函数

inline函数可以被定义为递归函数,但是,一旦被定义为递归函数,这个函数将会失去inline的功能,因为在编译阶段,编译器无法知道递归的深度

#define与const

define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异如下:

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

  2. const常量有数据类型,而define常量没有数据类型。编译器可以对const常量进行类型安全检查,如类型、语句结构等,而define不行

  3. 很多IDE支持调试const定义的常量,而不支持define定义的常量

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