建议8:拒绝晦涩难懂的函数指针
在C/C++程序中,数据指针是最直接也是最常用的,理解起来也相对简单容易,但是函数指针理解起来却并不轻松。函数指针在运行时的动态调用中应用广泛,是一种常见而有效的手段。但是,如果不注重一定的使用技巧,函数指针也会变得晦涩难懂。
告诉我下面定义的含义是什么?
void (*p[10]) (void (*)()); |
如此繁琐的语法定义几乎难以辨认,这与我们提倡的可读性背道而驰了。这样的函数指针之所以让程序员发愁,最主要的原因是它的括号太多了,往往会让程序员陷在括号堆中理不清头绪。下面一层一层地来分析吧。第一个括号中的*p[10]是一个指针数组,数组中的指针指向的是一些函数,这些函数参数为void (*)(),返回值为空;参数部分的void (*)()是一个无参数、返回值为空的函数指针。
分析这样的代码简直是一种折磨。如何有效地提高函数指针定义的可读性呢?那就是使用typedef。typedef 方法可以有效地减少括号的数量,可以通过typedef来合理地简化这些声明,理清层次,所以它的使用倍受推荐。
以上面的定义为例。首先,声明一个无参数、返回空的函数指针的typedef,如下所示:
typedef void (*pfv)(); |
接下来,声明另一个typedef,一个指向参数为pfv且返回为空的函数指针:
typedef void (*pFun_taking_pfv) (pfv); |
现在,再去声明一个含有10个这样指针的数组就变得轻而易举了,而且可读性有了很大的提升:
typedef void (*pFun_taking_pfv) (pfv); |
现在,再去声明一个含有10个这样指针的数组就变得轻而易举了,而且可读性有了很大的提升:
pFun_taking_pfv p[10]; /*等同于void (*p[10]) (void (*)());*/ |
请记住:
函数指针在运行时的动态调用(例如函数回调)中应用广泛。但是直接定义复杂的函数指针会由于有太多的括号而使代码的可读性下降。使用typedef可以让函数指针更直观和易维护。拒绝晦涩难懂的函数指针定义,拒绝函数定义中成堆的括号。
建议9:防止重复包含头文件
假设,我们的工程中有如下三个文件:a.h、b.h和c.cpp,其中b文件中包含了a.h,c文件中又分别包含了a.h和b.h两个文件,如图1-1所示。
图1-1 工程文件示例
在编译整个工程时,编译器会出现“multiple definition of”错误。原因在于a.h文件被包含了两次。为了避免同一个文件被包含多次,C/C++中有两种处理方式,一种是#ifndef方式,另一种是#pragma once方式。
方式1:
|
方式2:
|
C/C++语言标准支持第一种方式。这种方式不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被同时包含。当然,其缺点就是如果一不小心在不同头文件中定义了相同的宏名,造成了宏名“撞车”,那就可能会导致明明看到头文件存在,编译器却硬说找不到声明,这确实会令人非常恼火。为了避免宏名“撞车”,保证宏的唯一性,建议按照Google公司建议的那样,头文件基于其所在项目源代码树的全路径而命名。命名格式为:
<PROJECT>_<PATH>_<FILE>_H_ |
由于编译器在每次编译时都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使编译时间相对较长。
#pragma once方式一般由编译器提供,它保证同一个文件不会被包含多次。这里所说的“同一个文件”指的是物理上的一个文件,而不是指内容相同的两个文件。#pragma once声明只针对文件,而不能针对某一文件中的一段代码。这种方式避免了因想方设法定义一个独一无二的宏而产生的烦恼;另外,针对大型项目的编译速度也有了提升。但是这种方式因为不受C/C++语言标准支持,所以受到了编译器的限制,它在兼容性方面表现得不是很好。因此很多程序员为了代码的兼容性,宁肯降低一些编译性能,而选择遵循C/C++标准,采用第一种方式。
注意 针对#pragma once,GCC已经取消了对其的支持,而微软的VC++却仍在坚持。
请记住:
为了避免重复包含头文件,建议在声明每个头文件时采用“头文件卫士”加以保护,比如采用如下的形式:
|
建议10:优化结构体中元素的布局
下面的代码片段定义了结构体A和B:
|
在32位机器上,char、short、int三种类型的大小分别是1、2、4。那么上面两个结构体的大小如何呢?
结构体A中包含了一个4字节的int,一个1字节的char和一个2字节的short,B也一样,所以A、B的大小应该都是4+2+1 = 7字节。但是,实验给出的却是另外的结果:
sizeof(strcut A) = 8, sizeof(struct B) = 12 |
其原因还要从字节对齐说起。
现代计算机中内存空间都是按照字节来划分的,从理论上来讲,对变量的访问可以从任何地址开始;但在实际情况中,为了提升存取效率,各类型数据需要按照一定的规则在空间上排列,这使得对某些特定类型的数据只能从某些特定地址开始存取,以空间换取时间,这就是字节对齐。
结构体默认的字节对齐一般满足三个准则:
(1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
(2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员自身大小的整数倍,如有需要,编译器会在成员之间加上填充字节(Internal Adding)。
(3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(Trailing Padding)。
按照这三条规则再去分析结构体A和B,就不会对于上述的结果一脸诧异了。这两个结构体在内存空间中的排列如图1-2所示(灰色网格表示的字节为填充字节)。
图1-2 结构体A和B的内存分布
在编程应用中,如果空间紧张,需要考虑节约空间,那么就需要将结构体中的各个变量按照上面的原则进行排列。基本的原则是:把结构体中的变量按照类型大小从小到大依次声明,尽量减少中间的填充字节。
也可以采用保留字节的形式显式地进行字节填充实现对齐,以提高存取效率。其实这就是时间与空间的博弈。如下面的代码片段所示,其中的reserved成员对程序没有什么意义,它只是填补空间以达到字节对齐的目的:
|
在某些时候,还可以通过编译器的pack指令调整结构体的对齐方式。#pragma pack的基本用法为:
|
将结构体A的对齐方式设为1字节对齐,那么A就不再有填充字节了,sizeof(A)的结果即为各元素所占字节之和7。
请记住:
了解结构体中元素的对齐规则,合理地为结构体元素进行布局。这样不仅可以有效地节约空间,还可以提高元素的存取效率。