读书笔记_代码大全_第10章_使用变量的一般事项

时间:2022-01-13 16:07:09

使用变量的一般事项

注:希望我的读书笔记能带你翻过20页的书 http://www.cnblogs.com/jerry19880126/

本章主要讨论变量的一些使用事项,看似非常基础,但你是否真有“好的使用习惯”?不妨来看看。

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 A
2 {
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 = 6
12 }

 

注意这里使用全局的a都是OK的,但若局部再定义相同的a,比如把fun()改成:

1 void fun()
2 {
3          int a = 5;
4          int b = a + 3;// 有效,b = 8
5 }

 

可以看到,局部变量会屏蔽掉全局变量。另外,全局数据尽量不要使用,因为这不是线程安全的(无法承受多个线程对之读写),也会破坏程序的模块性,不利于封装和重构。

本书说到一个很重要的知识点,就是作用域提升的问题,存在这样一种现象,那就是将小作用域提升至大作用域时,需要改动的地方会很少(比如将块作用域提升至函数作用域,只要把变量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>