C++入门——预处理的二三事
预处理
概念
预处理也称为预编译,它为编译做预备工作用于处理#开头的指令。
常见的预处理指令及其功能
-
#
:空指令,无任何效果 -
#include
:包含一个源代码文件,把源文件中的#include扩展为文件正文,即把包含的.h文件找到并扩展到#include所在处 -
#define
:定义宏 -
#undef
:取消已定义的宏 -
#if
:条件编译指令,如果给定条件为真,则编译下面代码 -
#ifdef
:条件编译指令,如果宏已经定义,则编译下面代码 -
#ifndef
:条件编译指令,如果宏没有定义,则编译下面代码 -
#elif
:条件编译指令,如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 -
#endif
:结束一个#if…#else条件 -
#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
,然后去寻找系统路径,使得自定义文件较快
头文件的作用
-
通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便向用户公布,只用向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码
-
头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担
#define有哪些缺陷
-
它无法进行类型检查。宏定义是在预处理阶段进行字符的替换,因此无法检查类型
-
由于优先级的不同,在使用宏定义时,可能会存在副作用。例如,对于表达式的运算就可能存在潜在的问题
-
无法单步调试
-
会导致代码膨胀。由于宏定义是文本替换,需要对代码进行展开,相比较函数调用的方式,会存在较多的冗余的代码
-
在C++语言中,使用宏无法操作类的私有数据成员
含参数的宏与函数
含参数的宏有时完成的是函数实现的功能,但是并非所有的函数都可以被含参的宏替代。二者的特点如下:
-
函数调用时,首先求出实参表达式的值,然后带入形参,而使用带参的宏只是进行简单的字符替换
-
函数调用是在程序运行时处理的,它需要分配临时的内存单元,在函数调用的时候需要参数传递、压栈与弹栈操作;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值的传递处理,也没有“返回值”的概念。也就是说,函数调用占用的是运行时间,而宏替换占用的是编译时间。因此,使用宏会有更好的执行效率
-
对函数中的实参和形参都要定义类型,两者的类型要求一致,如果不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据
-
使用宏次数多时,宏展开后源程序会变得很长,因为每展开一次都使程序内容增长,而函数调用不会使源程序变长
-
参数每次用于宏定义时,它们都将重新求值,由于多次求值,具有副作用的参数可能会产生不可预料的结果。而参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多次求值过程,参数的副作用并不会造成任何特殊的问题
-
在C++语言中,宏不能访问对象的私有成员,而成员函数可以访问
-
可以在函数体内设置单步调试,但是不能在宏定义中设置断点进行单步调试。一般来说,用宏代表简短的表达式比较合适
不使用大于、小于、if语句,定义一个宏比较两个整数a、b的大小
如果需要返回较大的值,在宏定义可以写为如下形式:
#define MAX(a,b) (abs((a)-(b)) == ((a)-(b))?(a):(b))
另外一种方法:
#define MAX (a,b)(((a)-(b))&0x80000000)?(b):(a)
如何判断一个变量是有符号数还是无符号数
- 采用取反操作。数据在计算机中都是以二进制的0或1存储的,如果是由符号数,正数以0开头,负数以1开头,取反操作会把所有的1改为0,所有的0改为1;如果是无符号数则不会受此影响
#define ISUNSIGNED(a) (a>=0 && ~a>=0)
- 通过改变符号位判断。把变量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的区别
- 枚举常量是实体中的一种,但宏定义不是实体
- 枚举常量属于常量,但宏定义不是常量
- 枚举常量具有类型,但宏没有类型,枚举变量具有与普通变量相同的性质,如作用域、值等,但是宏没有
-
#define
宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值 - 一般在编译期里,可以调试枚举变量,但是不能调试宏常量
- 枚举可以一次性定义大量相关的常量,而#define宏一次只能定义一个
typedef与define的区别
typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在以下不同:
- 原理不同。
#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型的指针类型
- 功能不同。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
不只是可以为类型取别名,还可以定义常量、变量、编译开关等
-
作用域不同。
#define
没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域 -
对指针的操作不同。两者修饰指针类型时,作用不同。
#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),并且内联函数本身不能直接调用递归函数
二者的主要区别是:
-
宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码
-
宏定义没有类型检查,而内联函数有类型检查
内联函数与普通函数的区别
内联函数的参数传递机制与普通函数相同,但是编译器会在每个调用内联函数的地方将内联函数的内容展开,这样既避免了函数调用的开销,又没有宏机制的缺陷
内联函数和普通函数的最大区别在于其内部的实现,普通函数在被调用时,系统首先要跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个复制;而内联函数则不需要进行一个寻址的过程,当执行到内联函数时,此函数展开,如果在N处调用了此内联函数,则此函数就会有N个代码段的复制。
内联函数也并非万能的,在使用的过程中也存在一定的局限性,如果函数体过大,编译器也会放弃内联方式,而采用普通的方式调用函数。此时,内联函数就和普通函数执行效率一样了
inline函数是否可以是递归函数
inline函数可以被定义为递归函数,但是,一旦被定义为递归函数,这个函数将会失去inline的功能,因为在编译阶段,编译器无法知道递归的深度
#define与const
define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异如下:
-
define只是用来进行单纯的文本替换,define常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中,它只是一个常数;而const常量存在于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实实存在,并且可以被调用、传递
-
const常量有数据类型,而define常量没有数据类型。编译器可以对const常量进行类型安全检查,如类型、语句结构等,而define不行
-
很多IDE支持调试const定义的常量,而不支持define定义的常量
由于const修饰的变量可以排除程序之间的不安全性因素,保护程序中的常量不被修改,而且对数据类型也会进行相应的检查,极大地提高了程序的健壮性,所以一般倾向于用const来定义常量类型
上一篇: js的&&与||的那些事