C++防灾——为指针成员分配专门的存储空间
在C++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。
这样做的目的在于,要保证指针指向的存储单元能够由类本身控制。
如果这种情形处理不好,将可能会造成灾难性的后果,尽管多数情况程序看上去执行还算正常(这种错误是真正可怕的错误)。
为了帮助读者理解,本文将从实例出发,展示不用这种处理的灾难性后果,同时给出正确处理的方法演示。
一、一个编译正确,运行也正确的坏程序
//例程1 #include <iostream> using namespace std; class IntArray { public: IntArray(){arr_point=NULL; arr_len=0;} IntArray(int a[], int n); void showArray(); private: int *arr_point; //数组的首地址 int arr_len; }; IntArray::IntArray(int a[], int n) { arr_point=a; //这是灾难的源头 arr_len=n; } void IntArray::showArray() { for (int i=0; i<arr_len; ++i) cout<<*(arr_point+i)<<' '; //或cout<<arr_point[i]<<' ' cout<<endl; return; } int main() { int x[]={1,2,3,4,5}; IntArray arr(x,5); arr.showArray(); // 输出1 2 3 4 5 system("pause"); return 0; }
这个程序在执行main()函数时,第31行利用定义好的 x 数组,新建了arr 对象。第33行arr.showArray();输出的结果表明,对象的创建是正确的。
然而,这的确是个正确的坏程序。大多数情况不会出问题。但是,有时,无法预料到是何时,运行结果可能会不正确;甚至,有其他意外。
这不是无中生有,危言耸听。
让我们逐渐接近内幕。
二、让面向对象的机制失效的程序
//例程2 #include <iostream> using namespace std; class IntArray { public: IntArray(){arr_point=NULL; arr_len=0;} IntArray(int a[], int n); void showArray() const; private: int *arr_point; //数组的首地址 int arr_len; }; IntArray::IntArray(int a[], int n) { arr_point=a; arr_len=n; } void IntArray::showArray() const { for (int i=0; i<arr_len; ++i) cout<<*(arr_point+i)<<' '; //或cout<<arr_point[i]<<' ' cout<<endl; return; } int main() { int x[]={1,2,3,4,5}; const IntArray arr(x,5); arr.showArray(); //输出1 2 3 4 5 x[3]=999; arr.showArray(); //输出的是1 2 3 999 5 !!!!!! system("pause"); return 0; }
【运行结果】
1 2 3 4 5
1 2 3 999 5
请按任意键继续. . .
【一点说明】
其实还是上面的程序,只在main()中多加了两个语句。结果,在没有对 arr 对象做任何操作的前提下,arr 的值却变了!对象的封装性何在?!对象值的改变没有通过类的内部操作完成,也不是通过调用公共接口完成。而是,在arr 没有参与的情况下,变化已经发生。明明你买了一只烤鸭放在自家的冰箱里,取出来的却是一坨NF!
更为严重的是,例程2中甚至将showArray成员声明为const成员函数(第9和24行),将arr对象声明为const对象(第35行)。常对象不允许修改的底线也被挑战了,且得逞了!
这还不是最严重的!
三、这个类会酿成灾难
//例程3 ……//类的定义与例程2完全相同 int main() { int *x=new int[5]; for (int i=0; i<5; ++i) x[i]=i+1; //x是通过动态分配空间获得的,后面的释放从机制上是合法的 const IntArray arr(x,5); arr.showArray(); //输出1 2 3 4 5 delete [] x; //释放x,x可以由操作系统分配作其他用途(很正常,main中不再用局部变量x,及早释放,可以挪作他用。如果x数组很大,效益可观) arr.showArray(); //这是灾难发生的部位:输出结果不可预料,可能导致生产线停车、火车驶上了不该行驶的车道、火箭失控…… system("pause"); return 0; }
【运行结果】
1 2 3 4 5
1 2 3 999 5
-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多多的程序。
五、正确的做法
//例程4 #include <iostream> using namespace std; class IntArray { public: IntArray(){arr_point=NULL; arr_len=0;} IntArray(int a[], int n); ~IntArray(); void showArray() const; private: int *arr_point; //数组的首地址 int arr_len; }; IntArray::IntArray(int a[], int n) { arr_point=new int[n]; //arr_point指向了属于自己的新空间 for (int i=0; i<n; ++i) *(arr_point+i)=*(a+i); //将数组a中元素逐个赋值 arr_len=n; } IntArray::~IntArray() //由于在类中涉及动态分配存储空间,在析构函数中将对应空间释放 { if (!arr_point) // 等同于if (arr_point!=NULL) delete [] arr_point; //释放在类的生命周期中分配的,arr_point指向的空间 } void IntArray::showArray() const { for (int i=0; i<arr_len; ++i) cout<<*(arr_point+i)<<' '; //或cout<<arr_point[i]<<' ' cout<<endl; return; } int main() { int *x=new int[5]; for (int i=0; i<5; ++i) x[i]=i+1; const IntArray arr(x,5); arr.showArray(); // 输出1 2 3 4 5 x[3]=999; arr.showArray(); // 输出1 2 3 4 5, arr使用专属的存储空间! delete [] x; arr.showArray(); // 输出1 2 3 4 5, arr使用专属的存储空间!! system("pause"); return 0; }
【运行结果】
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
请按任意键继续. . .
【解释】
程序的关键是IntArray类的构造函数和析构函数。在构造函数中,为arr_point指向的空间专门分配存储单元并赋值,从而这块存储区域成为相应对象的专属操作对象,不通过面向对象的机制,不能访问这儿的空间。尽管在main()函数中涉及的 x 数组的值修改,甚至释放 x 所占的空间,但此时,x 和arr 对象已经完全没有任何关系,对arr_point 所指向的空间没有任何的影响。程序中的各实体之间的“耦合”达到最小,各自按照各自的机制运行。
下面的图示进一步说明了例程中内存空间的变化。
六、补充一个例子:当指针指向字符时
#include <iostream> #include <string.h> #include <iomanip> using namespace std; class CPerson { protected: char *m_szName; char *m_szId; int m_nSex;//0:women,1:man int m_nAge; public: CPerson(char *name,char *id,int sex,int age); void Show(); ~CPerson(); }; CPerson::CPerson(char *name,char *id,int sex,int age) { m_szName=new char[strlen(name)+1]; //分配正好大小的空间,根据形参name指向的字符串 strcpy(m_szName,name); //字符串的复制 m_szId=new char[strlen(id)+1]; //指针成员都这样处理 strcpy(m_szId,id); m_nSex=sex; m_nAge=age; } void CPerson::Show() { cout<<setw(10)<<m_szName<<setw(25)<<m_szId; //setw:设置输出数据的宽度,使用时应#include <iomanip.h> if(m_nSex==0) cout<<setw(7)<<"women"; else cout<<setw(7)<<"man"; cout<<setw(5)<<m_nAge<<endl; } CPerson::~CPerson() { delete [ ]m_szName; //析构函数中要释放动态分配的空间 delete [ ]m_szId; } int main() { char name[10],id[19]; int sex,age; cout<<"input name,id,sex(0:women,1:man),age:\n"; cin>>name>>id>>sex>>age; CPerson person(name,id,sex,age); person.Show(); system("pause"); return 0; }【说明】
此例是博文《 第10周-任务2-CEmployee类继承CPerson类》程序中的一部分,该文以CPerson为基类作了派生。
在以前的博文中,《第9周-任务4-二维数组类》也涉及到了本文所讲的内容,请参考。
七、总结
重申本文中心:在C++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。