在c++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。
这样做的目的在于,要保证指针指向的存储单元能够由类本身控制。
如果这种情形处理不好,将可能会造成灾难性的后果,尽管多数情况程序看上去执行还算正常(这种错误是真正可怕的错误)。
为了帮助读者理解,本文将从实例出发,展示不用这种处理的灾难性后果,同时给出正确处理的方法演示。
一、一个编译正确,运行也正确的坏程序
这个程序在执行main()函数时,第31行利用定义好的 x 数组,新建了arr 对象。第33行arr.showarray();输出的结果表明,对象的创建是正确的。
然而,这的确是个正确的坏程序。大多数情况不会出问题。但是,有时,无法预料到是何时,运行结果可能会不正确;甚至,有其他意外。
这不是无中生有,危言耸听。
让我们逐渐接近内幕。
二、让面向对象的机制失效的程序
【运行结果】
1 2 3 4 5
1 2 3 999 5
请按任意键继续. . .
【一点说明】
其实还是上面的程序,只在main()中多加了两个语句。结果,在没有对 arr 对象做任何操作的前提下,arr 的值却变了!对象的封装性何在?!对象值的改变没有通过类的内部操作完成,也不是通过调用公共接口完成。而是,在arr 没有参与的情况下,变化已经发生。明明你买了一只烤鸭放在自家的冰箱里,取出来的却是一坨nf!
更为严重的是,例程2中甚至将showarray成员声明为const成员函数(第9和24行),将arr对象声明为const对象(第35行)。常对象不允许修改的底线也被挑战了,且得逞了!
这还不是最严重的!
三、这个类会酿成灾难
-17891602 -17891602 -17891602 -17891602 -17891602
【解释】
在注释中已经指出了灾难所在,会得出错误的结果,灾难甚至可能是程序停止执行,意外退出。也有可能输出还会“正确”,而“正确”的惟一解释是这段程序太短了,arr中的arr_point指向的空间恰好还没有被操作系统分配作其他用途。当例程3的第12行和第13行中间插入了其他代码,完成了一些操作,甚至转移过流程,谁也说不清到执行第13行时,原先x曾经占用的内存的作用。的确,arr中的arr_point指向的是一个谁都说不清楚正在作何用的空间!!这个例子所示的只是显式地、有意地让灾难发生。在实际的项目中,类似 delete
[ ] x; 的操作可能不在这里发生,可能根本不是由于delete造成。乐观些想这个问题, 如果在灾难发生前我们觉察出了问题,要在几万行代码中找到问题的根源,也是一件相当不易的事情,需要会出巨大的成本。
而这一切,如果能遵循本文开头的嘱咐,原来是不会发生的。
四、深刻理解:错误是这样发生的
用例程1来说明问题。执行例程1时,发生的主要事情如图所示:
所以,在例程2中,main()函数可以修改 x[3] 的值;例程3中,x 数组已经被释放了,arr 对象仍然“一往无前”地将之用作数组。后一种情况是灾难性的,前 种情况也千万不要将之用作为技巧:看,我能够绕开c++的限制修改对象成员指向的值(有些hacker的感觉?)。在工程中,切忌将不同实体间的联系复杂化,这是一种复杂化的表现,多种机制瞎搅乎的结果,必定是质量低下、破绽百出、bug多多的程序。
五、正确的做法
程序的关键是intarray类的构造函数和析构函数。在构造函数中,为arr_point指向的空间专门分配存储单元并赋值,从而这块存储区域成为相应对象的专属操作对象,不通过面向对象的机制,不能访问这儿的空间。尽管在main()函数中涉及的 x 数组的值修改,甚至释放 x 所占的空间,但此时,x 和arr 对象已经完全没有任何关系,对arr_point 所指向的空间没有任何的影响。程序中的各实体之间的“耦合”达到最小,各自按照各自的机制运行。
下面的图示进一步说明了例程中内存空间的变化。
六、补充一个例子:当指针指向字符时
【说明】
七、总结
重申本文中心:在c++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。