使用变量的一般事项
注:希望我的读书笔记能带你翻过20页的书
本章主要讨论变量的一些使用事项,看似非常基础,但你是否真有“好的使用习惯”?不妨来看看。
1. 在声明变量的时候就应该初始化
这告诉我们应该把int count换成int count = 0,把short *pointer换成short *pointer = 0。有些语言,比如VB不支持声明的时候就初始化,那就在变量声明的下一句就给赶紧给它赋个值吧!当变量是对象时,则要确保这个对象被合理地初始化了,在类中要定义构造函数(最好有个默认的构造函数)。
2. 能使用const的地方,就尽量用const
(1) 定名常量用const
一些常量,比如一年中月份的个数,可以这样定义:const int MONTH_AMOUNT = 12,这就是定义定名常量的方法,它比#define MONTH_AMOUNT 12 要好,因为用定名常量的方法可以让编译器再把把关,比如const int MONTH_AMOUNT = 12.0时编译器会给出警告,而宏定义则失去了编译器的帮助。
(2) 函数中包含引用的参数,也尽量使用const
比如两数相加的函数int add(int& firstNumber, int& secondNumber),如果调用函数是add(3, 5)就会报编译错,而int add(const int& firstNumber, const int& secondNumber)则不会这样,因为非const引用参数必须要附着到与之类型完全匹配而且也要是非const的变量上,而const引用参数的约束则变弱,既可以附着同类型的非const变量,也要以附着同类型的const变量,甚至包括常量。
(3) 指针const
指针的const分成两种,即指向常量的指针和常量指针,这可是出笔试题喜欢问到的地方。指向常量的指针要求指针指向的值不改变,比如const int* p = &a,这之后再有*p = 5就会报错,因为不允许改变指针所指向的值。常量指针要指针本身指向不能改变,比如int* const p = &a,这之后再有p = &b,就会报错,因为指针的指向不能变化了。还有这两种情况的混合体,比如const int* const p = &a,这时就要求指针指向以及指向的值同时都不能改变,称为指向常量的常量指针,比较绕口,不过理解意思就行了。这里还有一个比较tricky的地方,好像是今年中兴的考师,int const *p是上面的哪种情况?哈哈,是不是绕晕了,这其实是指向常量的指针。方法是看const与谁离的近,当表示指向常量的指针时,const后面出现的是*p(表示值为常量),而当表示常量指针时,const后面出现的是p(表示指针本身是常量),也就是看*与const的相对位置关系。
(4) 当类的成员函数不改变类成员变量的,也尽量在其后放置const
比如
1 class A2 {3 private:4 int a;5 public:6 void print() const;7 };
当print只是去打印而不改变类的成员变量时,就可以在其后放置const,防止误操作修改了成员变量。
总而言之,const防止变量(包括指针变量)被修改,同时也会降低对使用的约束要求。
3. 尽量使用小的变量作用域
变量的作用域是指变量有效的范围,由小到大依次分为块作用域,函数作用域,类作用域和全局作用域。
块作用域比如下面的i,它只存在于for循环内,比如:
1 for(int i = 0; i < 10; ++i)2 {…}3 i = 3; // 这句话编译器报错,因为这时候编译器已经不认识i了。
函作用域是指变量只在函数范围内有效,比如:
1 void fun(int a, int b) 2 { 3 int c = 3; 4 … 5 } 6 int main() 7 { 8 … 9 fun(5, 6);10 a = 3; // 编译器报错11 c = 5; // 编译器报错,因为这时候编译器已经不认识a和c了。12 }
类作用域是指变量在整个类范围内都是有效的,比如:
1 class A 2 { 3 int a; 4 public: 5 void setA(int t) 6 { 7 a = t; // 有效,类作用域 8 } 9 int getA()10 {11 return a; // 有效,类作用域12 }13 }
但在类外a就不能直接引用了。
全局作用域是最广的,它在自它声明位置起,到本文件结束,都可以使用它。比如:
1 int a = 3; 2 3 int main() 4 { 5 cout << a << endl; // 有效 6 } 7 8 9 void fun()10 {11 int b = a + 3; // 有效,b = 612 }
注意这里使用全局的a都是OK的,但若局部再定义相同的a,比如把fun()改成:
1 void fun()2 {3 int a = 5;4 int b = a + 3;// 有效,b = 85 }
可以看到,局部变量会屏蔽掉全局变量。另外,全局数据尽量不要使用,因为这不是线程安全的(无法承受多个线程对之读写),也会破坏程序的模块性,不利于封装和重构。
本书说到一个很重要的知识点,就是作用域提升的问题,存在这样一种现象,那就是将小作用域提升至大作用域时,需要改动的地方会很少(比如将块作用域提升至函数作用域,只要把变量i拿到for循环的外面就行了),但反过来,将大作用域减少至小作用域时,需要改动的地方就会很多,还是用这个例子,假定i在for循环外面定义了:
1 int i = 0;2 3 for(i = 0; i < 10; ++i)4 {…}5 6 i = 3;
这时候看似再把i放到for循环里面就OK了,但这样做编译器会报错,因为for的下面还有对i的使用,要把下面用到i的地方统统修改,可见会很麻烦!
大型的程序常常需要修改,变量作用域时有更新,因此在初次编写的时候,尽量用小的作用域!这样扩展至大作用域时会轻松很多。
4. 尽量使变量的生存时间减小
变量的生存时间是指其有效期,比如对于for循环块:
1 for(int i =0; i < 3; ++i){…}
变量i只在for循环内生存,一旦出了for循环i就不能使用了(现在VS都是这样处理的,但老版本的VC6.0却认为i的生存时间自此开始,直到程序尾)。
又如函数内
1 void fun(){ int a = 3;…}
变量a只在函数内生存,一旦出了函数,a的“生命”就结束了,也就不能使用了。
相信读者读到这里,心里肯定有疑惑,变量生存时间不是与作用域差不多嘛!确实,很多情况下(比如前面两个例子),变量的生存时间与变量的作用域刚好重合,但两者其实不是等价的,两者是这样一种关系:变量的生存时间≥变量的作用域。我再举个例子,比如有这样一个函数:
1 void f()2 {3 static int s = 3;4 …5 }
现在问你,s的作用域是什么,生存时间又是什么?作用域你一般不会答错,就是这个函数内,在函数外使用s会使编译器报错;但s的生存时间不是你想像的,s生存时间很长,从程序开始时就在了,因为在静态变量与一般的变量是分开存储的,静态变量和全局变量存在于内存空间的全局数据区,这里面的数据是不会随着f()的结束而消亡的——它们一直都在,直到这个程序终止。又如:
1 void fp()2 {3 int *p = new int(3);4 }
p在函数结束后的生命就结束了,作用域也限定在fp()函数内,但p所指向的一个字节的内存空间(这一个字节存的是整型的3)却一直存在,直到程序运行结束后被操作系统回收。为什么会这样呢?因为new是在堆上分配的空间,除非对其delete,堆上的内存是不会随着子函数的消亡而回收的。那为什么p会消亡?因为p只是对这一块内存的引用,它生存在栈上。事实上,除静态变量以外的局部变量的都位于内存空间中的栈上,函数调用的开始和结束会伴随着一系列的栈变量弹出与栈变量压入,局部变量的这一系列的操作中会诞生和消亡。
管理代码时最头疼的问题是一下子要关注很多“还活着”的变量,所以说“尽量使变量的生存时间减小”,这样我们都会用更集中的精力来对付更重要的变量。因此,个人觉得C++在这个方面做的比C要好,C要求在函数的开头就要给出变量的声明,在后面才能使用,但往往自其声明到使用,会相隔很长很长,它出生的太早了!
上面说了那么多,我其实只想表达“变量的作用域尽量短,生存时间也尽量短”,本书提供了以下四种方法:
(1) 在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量;
(2) 把相关语句放在一起;
(3) 把相关语句提取成单独的子程序;
(4) 开始时采用最短的作用域与生存时间,然后根据需要扩展。
5. 选择适合的绑定时间
学过多态的同学知道“绑定”这个词,有“早绑定”与“迟绑定”之分,早绑定发生在编译的时候,而迟绑定则发生在运行的时候。
像int a = 12或者int a = MONTH_AMOUNT(其中MONTH_AMOUNT是之前定义的具名常量),就是在编译期绑定好的,即将12这个数值与a绑定在一起。而int a = getMonthAmount()则发生在运行时,只有到程序执行到这一句时,才知道返回值是什么,所以a只有在这个时候才与函数的返回值发生绑定。早绑定简单但机械,而迟绑定复杂却灵活,比如多态就可以根据不同的对象采取作出不同的行为。到底使用什么样的绑定方式,则根据你的需要。但有一点,int a = MONTH_AMOUNT显然要优于int a = 12,12在这里是magic number(莫名其妙冒出来的值),但用MONTH_AMOUNT去代替12无疑增加了可读性,也便于修改。
6. 为变量指定单一用途
一句话,一个变量就做一件事,比如pageCount表示已经打印的纸的数量,但有的程序员会了节省变量,用pageCount = -1表示打印时出错,这个从原理上来说行的通,因为pageCount的只可能取大于等于0的正整数,-1在这里相当于哨兵了,表征打印出错。但这样做其实不好,完全可以用printStatus来单独作哨兵。最好还是让pageCount做它本来该做的事——计算打印纸数量。
最后总结一下这几个要点:
(1) 变量声明的时候就初始化,若是对象,则提供一个至少含有默认构造函数的类;
(2) 能用const的地方尽量用const;
(3) 应使变量作用域尽量窄,生存时间尽量短;
(4) 选择合适的变量与值的绑定方式;
(5) 把每个变量用于唯一的用途。
<end>