不常见的数据类型
前注:希望我的读书笔记能带你迅速走过25页的书籍,有不妥之处,欢迎指正。http://www.cnblogs.com/jerry19880126/
本章主要介绍三种“不常见”的数据类型,分别是结构体,指针和全局数据,其实我觉得这三种数据类型还是很常见的,不太认同本书将之分类成“不常见”。
第一部分:结构体
结构体好比是一个团体,它将一些相关的数据放在一起,比如对于student而言,属性可能包括name,age,sex,height和weight等,因此可以这样声明一个学生的结构体:
struct Student
{
string name;
int age;
char sex;
int height;
int weight;
};
定义一个学生的实体张三Student zhangSan,就可以这样引用张三的姓名zhangSan.name,年龄zhangSan.age。
结构体的优点有四个:
(1) 明确数据关系
如上面这个例子,若不组团,而分开写,比如给张三同学赋初值:
name = inputName;
age = inputAge;
…
这样貌似还是不很乱,但万一这时候还有李四同学怎么办?千万不要用name1,name2来区别,前面有读书笔记已经谈到,这种变量命名的方式是很糟糕的。那用nameZhangSan,ageZhangSan,nameLiSi,ageLiSi?这种命名方式会很麻烦,万一学生数很多,这样不是要有成千上万个变量了?那用数组name[100],age[100]来解决这个问题?数组的确是OK的,但有没有想过,这样的写法缺乏关联性?name[0]与age[0]是对同一个学生的描述(下标0表示这是第一个学生),但这两个变量名却没能反映它们都是学生的属性。
若采用上面的结构体声明,再定义Student student[100],然后用student[0].name来表示0号同学的名字,student[0].age来表示0号同学的年龄,这样能立刻感觉出name和age都是描述一个student的属性。因此:
student[0].name = inputName;
student[0].age = inputAge;
能更清楚地反映属性的归属。
(2) 可以简化对数据块的操作
假设要复制学生张三,若不采用结构体,则需要这样操作:
newName = nameZhangSan;
newAge = ageZhangSan;
…
这样要写很多句话,有多少属性就有多少个赋值语句,但如果用结构体:
Student newStudent;
newStudent = zhangSan;
只要两句话就搞定了。
(3) 可以简化参数列表
你觉得是调用函数时用fun(nameZhangSan, ageZhangSan, sexZhangSan, heightZhangSan, weightZhangSan)比较好,还是用fun(zhangSan)来得简单呢?
(4) 可以减少维护
假定要对Student删除sex属性,你觉得是删掉每个对sex数组的引用比较好,还是直接删掉结构体中的sex项比较方便呢?
第二部分:指针
指针本质是数据的地址,画个示意图就知道了:
以win32环境为例,方框表示内存块,内存块是以“字节”为单位的,方框里的数值表示的是内容,方框左侧的16进制表示的是方框所在的地址,由上到下地址逐渐增加。左侧表示的指针,指针在win32平台下都是4个字节(不管是int*,short*,double*还是char*,都是4个字节),这样指针的数值(存放的内容)是0x00001234(小端表示),而指针数值表示的是地址,所以它其实是指向首地址为0x00001234的内存块,也就是右侧。我们所熟悉的int*,short*等等,其实是解释应该怎样解读右侧的内存块。假定指针名为pointer,若指针类型是char*,则认为所指向的地址表示的是char数据,所以*pointer的值为0x0a,表示ASCII码为10的字符,也就是换行符;若指针类型为short*,则表示2字节的短整数0x610a,*pointer值为24842;若指针类型为float*,则表示4字节的浮点数0x6362610a,数值大约为4.176*10^21。综上,指针的数值(内存块的内容)表示的是目标内存块的首地址,指针总是4个字节,但怎样解释目标内存块是由指针类型决定的。从上面还可以看到指针其实也是有地址的,最左侧的数字就是指针的地址,用&pointer就可以获得0x003839了。
《代码大全》上列举了指针的使用技巧,我只列出自己认为重要的:
(1) 同时声明和定义指针
int* pointer并不一个好习惯,因为这时候pointer的值到底是什么,我们并不知道,万一pointer指向了一段重要的数据区,一旦有*pointer = 1234,数据区的数据就会被污染(现代操作系统已经有这类问题的预防了,只要使用了*pointer,就会弹出带红叉的对话框阻止程序继续往下走,但仍需要小心)。解决这个问题的方法是int* pointer = &var,让指针从诞生起就有明确的指向。
(2) 删除指针后将之设为空值
当执行delete pointer后,pointer的值是多少?这时候是不确定的,有的时候它指向上次的目标内存块,有时候它指向系统的随机地址,此时的指针被称为dangling pointer(迷途指针或悬空指针)。这时候引用*pointer是非常危险的,比如*pointer = 1234,你的重要数据又不保了。
(3) 粉碎垃圾数据
当执行delete pointer后,其实你不是想解放指针,你真正想要做的是释放动态分配的一段内存。比如说:
int *pointer = new int[2];
pointer[0] = 3;
pointer[1] = 9;
delete [] pointer其实是想释放掉这2个int型的内存块,但事实上,在执行delete操作后,原来的那两个int型内存块的值可能还是3和9。delete不可能真的从操作系统中删掉内存(那些内存块一个也不会少)!比如你电脑D盘的总容量是30G,你删除一个文件后的总容量还会是30G,不会变少的。delete所能做只是删掉这些内存的引用,至于这些内存的内容,可能是上次操作遗留的值,也可能是操作系统随机出的垃圾值。这些遗留下来的值会干扰你调试程序,因为你无法确定指针指向的内存何时已经变成非法的了!解决方法是粉碎这些垃圾数据,比如memset(pointer, 0xcc, MemoryBlockSize(pointer)),然后再delete pointer。这样垃圾数据都被置0xcc了。《代码大全》上推荐在每一个待使用的内存块后多申请一个标记字段,称为dog tag(狗牌),这时只要检测狗牌是有效值还是0xcc,就知道还能否继续使用这段内存块了。
(4) 在与指针分配相同的作用域中删除指针
一句话,若在一个函数中new了内存块,则也在这个函数中delete掉它!不要把delete操作放在这个函数之外的地方。(因为new是在堆在分配内存,所以这个函数外的其他地方还是能够使用这块内存的,但这样做不安全,不要指望你的记性有多好,你不会总记得在最后释放掉它的)
(5) 在使用指针前作检测
包括两个检测,一是检测指针本身,二是检测指针所指向变量的取值范围。指针本身主要是看指针是否为空,我还记得360一面的面试官让我手写一个字符串查找的小程序,我还没写完他就打断我,说我忘了检测指针是否为空了,汗啊……这个条件一定要判断,不然会给面试官留下粗心的坏印象的。检测指针所指向变量的取值范围,其实就是检测变量是否是合法的,比如有的变量的最大值不可能超过360,但是*pointer后的值却超过了360,这就出现bug了。可以用断言,比如assert(*pointer <= 360)。
(6) 链表操作中画图对付指针
单向链表、双向链表、循环链表的操作包括插入、删除、查找结点等,这些操作空想会挺麻烦的,但若画个图,就简单多了。注意这里的操作代码有前后顺序之分,比如删除结点时什么时候该断开这个结点是有说法的,这里就不举例子了,很多数据结构书上都会介绍到的。
下面谈谈C++中的指针与引用的区别,这是我舍友今年去微软面试的一道面试题,题面其实很直观,学过C++的同学也能答上个一两条,但若是能给出全面的答案,其实并不容易。本章给出了两个最重要区别,第一个区别是引用必须总是引用一个对象,不可能“悬空的”,比如int& a,这语法上就通不过的,int& a = b,将a引用b才是可以的(这时候a是b的别名,就好比某个同学身份证上的名字和乳名一样,其实是一个人的两种叫法);但指针可以“悬空”,比如int* p,指向“空”也可以,比如int* p = NULL。第二个区别是引用确定在声明的时候,即从它诞生之日起,就不可以改变了,int& a = b,之后再有a = c语法上就通不过了;但指针可以随时改变它的指向,比如int* p = &a,接下来p = &b也是合法的。
下面给出比较全面的回答:
i. 从现象看,指针在运行时可以改变其所指向的值,而引用一旦与某个对象绑定后就不能再改变;
ii. 从内存分配上,程序为指针分配空间,而不为引用分配空间;
iii. 从编译上看,指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值 。
第三部分:全局数据
本章的最后花了不少篇幅来介绍全局数据,其实也就是一句话,尽量少用全局变量。因为它不是线程安全的(即多个线程万一对这个变量同时操作,会出bug),另外,全局变量还破坏了代码的模块化,阻碍了代码的重用。但我也不得不承认,在写过的程序里使用过全局变量,因为它可以简化对极其常用的数据的使用,如果不用全局变量,可能需要传递多个函数参数。本书推荐将全局变量写在类中,可以作为类的静态成员来使用,当然不是把一堆不相关的全局变量放到一个不相关的类中,而是有规律的组织这些相关的变量。此外,用g_前缀来区分,并提供一份注释良好的清单,会减轻全局变量的副作用。
<end>