Item2 Prefer consts, enums, and inlines to #defines.
使用预编译的缺点(#define
语句的缺点)
缺点1
当你使用如下语句#define PI (3.14)
,预编译器会把全文出现PI的地方都替换成3.14。对编译器而言,他看不到PI,他看到的只是3.14。如果编译器在编译3.14所在语句发生了错误,编译器的提示语句也只会显示3.1,这对于定位问题很麻烦。上述是作者的意思。但实际情况已经不是这样了,用最新的g++5.4.0
尝试下,会发现现在的编译器在发出编译告警中,明确的告诉你发生错误时的语句发生过宏替换,并且会告诉你宏定义在什么地方。先暂时认为作者用的可能是太旧的编译环境,才会有这个提示不全面的问题。
#define PI ("abc")
int main()
{
int a = PI; return 0;
}
使用g++ main.cpp -std=c++11
编译报错,报错信息很详细,并没有作者说的找不着北的情况。
缺点2:
类似缺点1,因为编译器不知道宏定义的符号,所以在调试的时候,调试器不认识宏定义。这个是确实存在的,宏定义是没有办法调试。
#define SWAP(x,y) \
x= x+y;\
y=x-y;\
x=x-y;
int main()
{
int a = 1,b =2;
SWAP(a,b); return 0;
}
针对上述的代码,你是没有办法单步进入宏定义内部进行进一步调试的。
缺点3:
有些宏替换会潜藏着风险,尤其是带表达式如++,--
之类的宏会让人不省心,经典的例子如下:
#define MAX(a,b) a>b?a:b
int main()
{
int a= 1, b =2;
int maxone = MAX(a++,b);
return 0;
}
这个例子中预编译器在替换的时候,替换出的结果是a++>b?a++:b
,这个应该不是编程人员的本意。
使用const取代宏定义常量
使用const常量取代宏定义的好处
- 使用const常量就省去了预处理器进行替换的工作。就不会存在编译器不认识常量符号的问题。
- const定义的常量一般被编译器放到text区,特殊情况下也会放到data区,但不管放在哪里,只会放一份。反观宏定义,经过预处理器替换之后,在源代码中会出现多份该常量,编译器会在text区存放多份该常量。编译器可没有足够聪明能把这些重复的常量提炼成一个。至于什么是data区,text,bss区这个在引申章节中说明。
- const常量可以定义在类里面让该常量只在这个类里面生效,这个是宏定义不具有的封装性。
使用const常量需要注意的地方
归纳起来主要有两点
- 定义指向常量的指针要用两个const
- const变量的初始化时机
定义指向常量的指针为什么需要两个const
正确定义指向指针的常量方式如下const char * const authorName = "Scott Meyers";
这种写法有两个const限定,一方面限定authorName
是常量,编译器会禁止对他的赋值操作。另一方面限定了authorName
指向的内容是常量,编译器会禁止诸如authorName[0] = 'x'
通过指针篡改字符串的操作。 少任何一个const通常都是你不希望的限定缺失。
const 变量的初始化时机
这块作者直接上来介绍static const的初始化。对于一般人是有些难度的。这里这样子总结或许会更好理解:
- 对于non static const变量,初始化需要通过初始化列表进行。(一般网上查的资料都这么说,但是如果用c++11,实际上是支持在类声明中直接进行对const成员进行初始化的。而且对于non static const成员不管是什么类型都是可以直接在类中声明时赋值的。并不需要初始化列表的方式。举例如下
class A
{
public:
const string str = "123";
}
int main()
{
A a;
cout<<a.str<<endl;
}
如果用g++ main.cpp
的方式编译,编译器会编译失败,并提示不支持在这种初始化方式。 如果用g++ main.cpp -std=c++11
编译是正常通过的。
- 对于static const变量如果是整形,支持在类声明时直接初始化。除此之外必须在类外面定义并初始化。通用的编写方法是这样的
static const string A::CONSTSTR = "hello123";
class A
{
public:
static const string CONSTSTR;
}
int main()
{
cout<<A::CONSTSTR<<endl;
return 0;
}
使用enum取代宏定义
enum的使用类似于static const的使用。唯一不同的是编译器保证碰到enum的时候不会分配内存,而是一定只放在text区。
作者还讲到enum是模板元编程的基础技术。这个在Item48中有讲述。
使用inline取代宏定义
inline函数在编译的时候会展开,这样在运行期间也能减少函数的调用开销。相对于宏来说,还更容易调试。
item2的引申
可执行文件的结构
对于任何可执行的文件exe也好,.o文件也好,.out文件也好,在linux下你可以用size a.out
的类似命令查看可执行文件a.out的结构组成。
一个可执行文件至少包括3个区段。text区,data区和bss区。
- text区存放的是编译后组成程序功能的一系列的可执行命令。
- data区存放的是初始化后的全局变量,静态变量。这些有初始化的数据,执行文件需要记录下来。
- bss区存放的是没有初始化的全局数据或静态数据。
bss区因为没有初始值,为了减小可执行文件的大小,可执行文件可以只记录总共需要的bss区域大小就可以。当操作系统加载可执行文件的时候,只要根据这个大小,分配出空间,然后会自动初始化成0一次。
可执行文件的加载过程
在PC机上,可执行文件存放在硬盘,要执行的时候,必须先加载到内存中才能执行。(注意这和单片机系统不一样,单片机的可执行文件存放在ROM中,程序执行的时候可以从ROM中直接开始执行,不需要把所有的可执行代码放到RAM中。)这里只针对PC系统描述。
加载大致过程:
- 根据可执行文件上的bss标记(一般在header里面)为bss分配指定的大小。
- 根据data区分配大小,并把所有初始化值复制到分配的空间上。
- 把text区拷贝到内存
- 系统为可执行文件分配栈
- 系统开始跳转到内存中拷贝到的text区并执行第一条指令。
系统为每个进程自动分配栈,默认栈大小取决于不同的操作系统。
网友评论