C++防灾——为指针成员分配专门的存储空间

 原文链接:http://blog.csdn.net/sxhelijian/article/details/7492646在C++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。  这样做的目的在于,要保证指针指向的存储单元能够由类本身控制。  如果这种情形处理不好,将可能会造成灾难性的后果,尽管多数情况程序看上去执行还算正常(这种错误是真正可怕的错误)。  为了帮助读者理解,本文将从实例出发,展示不用这种处理的灾难性后果,同时给出正确处理的方法演示。   一、一个编译正确,运行也正确的坏程序 

  1. //例程1  
  2. #include <iostream>  
  3. using namespace std;  
  4. class IntArray  
  5. {  
  6. public:  
  7.     IntArray(){arr_point=NULL; arr_len=0;}  
  8.     IntArray(int a[], int n);  
  9.     void showArray();  
  10. private:  
  11.     int *arr_point;  //数组的首地址  
  12.     int arr_len;  
  13. };  
  14.   
  15. IntArray::IntArray(int a[], int n)  
  16. {  
  17.     arr_point=a;  //这是灾难的源头  
  18.     arr_len=n;  
  19. }  
  20.   
  21. void IntArray::showArray()  
  22. {  
  23.     for (int i=0; i<arr_len; ++i)  
  24.         cout<<*(arr_point+i)<<' '//或cout<<arr_point[i]<<' '  
  25.     cout<<endl;  
  26.     return;  
  27. }  
  28.   
  29. int main()  
  30. {     
  31.     int x[]={1,2,3,4,5};  
  32.     IntArray arr(x,5);  
  33.     arr.showArray();  // 输出1 2 3 4 5  
  34.     system("pause");  
  35.         return 0;  
  36. }  

  这个程序在执行main()函数时,第31行利用定义好的 x 数组,新建了arr 对象。第33行arr.showArray();输出的结果表明,对象的创建是正确的。  然而,这的确是个正确的坏程序。大多数情况不会出问题。但是,有时,无法预料到是何时,运行结果可能会不正确;甚至,有其他意外。  这不是无中生有,危言耸听。  让我们逐渐接近内幕。   二、让面向对象的机制失效的程序 

  1. //例程2  
  2. #include <iostream>  
  3. using namespace std;  
  4. class IntArray  
  5. {  
  6. public:  
  7.     IntArray(){arr_point=NULL; arr_len=0;}  
  8.     IntArray(int a[], int n);  
  9.     void showArray() const;  
  10. private:  
  11.     int *arr_point;  //数组的首地址  
  12.     int arr_len;  
  13. };  
  14.   
  15.   
  16. IntArray::IntArray(int a[], int n)  
  17. {  
  18.     arr_point=a;   
  19.     arr_len=n;  
  20. }  
  21.   
  22.   
  23. void IntArray::showArray() const  
  24. {  
  25.     for (int i=0; i<arr_len; ++i)  
  26.         cout<<*(arr_point+i)<<' '//或cout<<arr_point[i]<<' '  
  27.     cout<<endl;  
  28.     return;  
  29. }  
  30.   
  31.   
  32. int main()  
  33. {     
  34.     int x[]={1,2,3,4,5};  
  35.     const IntArray arr(x,5);  
  36.     arr.showArray();  //输出1 2 3 4 5  
  37.     x[3]=999;  
  38.     arr.showArray();  //输出的是1 2 3 999 5 !!!!!!  
  39.     system("pause");  
  40.         return 0;  
  41. }  

 【运行结果】1 2 3 4 51 2 3 999 5请按任意键继续. . .【一点说明】  其实还是上面的程序,只在main()中多加了两个语句。结果,在没有对 arr 对象做任何操作的前提下,arr 的值却变了!对象的封装性何在?!对象值的改变没有通过类的内部操作完成,也不是通过调用公共接口完成。而是,在arr 没有参与的情况下,变化已经发生。明明你买了一只烤鸭放在自家的冰箱里,取出来的却是一坨NF!   更为严重的是,例程2中甚至将showArray成员声明为const成员函数(第9和24行),将arr对象声明为const对象(第35行)。常对象不允许修改的底线也被挑战了,且得逞了!  这还不是最严重的!   三、这个类会酿成灾难 

  1. //例程3  
  2.   
  3. ……//类的定义与例程2完全相同  
  4.   
  5. int main()  
  6. {     
  7.     int *x=new int[5];  
  8.     for (int i=0; i<5; ++i)  
  9.         x[i]=i+1;  //x是通过动态分配空间获得的,后面的释放从机制上是合法的  
  10.     const IntArray arr(x,5);  
  11.     arr.showArray();  //输出1 2 3 4 5  
  12.     delete [] x;      //释放x,x可以由操作系统分配作其他用途(很正常,main中不再用局部变量x,及早释放,可以挪作他用。如果x数组很大,效益可观)  
  13.     arr.showArray();  //这是灾难发生的部位:输出结果不可预料,可能导致生产线停车、火车驶上了不该行驶的车道、火箭失控……  
  14.     system("pause");  
  15.         return 0;  
  16. }  

【运行结果】1 2 3 4 51 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多多的程序。   五、正确的做法 

  1. //例程4  
  2. #include <iostream>  
  3. using namespace std;  
  4. class IntArray  
  5. {  
  6. public:  
  7.     IntArray(){arr_point=NULL; arr_len=0;}  
  8.     IntArray(int a[], int n);  
  9.     ~IntArray();  
  10.     void showArray() const;  
  11.   
  12. private:  
  13.     int *arr_point;  //数组的首地址  
  14.     int arr_len;  
  15. };  
  16.   
  17. IntArray::IntArray(int a[], int n)  
  18. {  
  19.     arr_point=new int[n];  //arr_point指向了属于自己的新空间  
  20.     for (int i=0; i<n; ++i)   
  21.         *(arr_point+i)=*(a+i);   //将数组a中元素逐个赋值  
  22.     arr_len=n;  
  23. }  
  24.   
  25. IntArray::~IntArray() //由于在类中涉及动态分配存储空间,在析构函数中将对应空间释放  
  26. {  
  27.     if (!arr_point) // 等同于if (arr_point!=NULL)   
  28.         delete [] arr_point; //释放在类的生命周期中分配的,arr_point指向的空间  
  29. }  
  30.   
  31. void IntArray::showArray() const  
  32. {  
  33.     for (int i=0; i<arr_len; ++i)  
  34.         cout<<*(arr_point+i)<<' '//或cout<<arr_point[i]<<' '  
  35.     cout<<endl;  
  36.     return;  
  37. }  
  38.   
  39. int main()  
  40. {     
  41.     int *x=new int[5];  
  42.     for (int i=0; i<5; ++i)  
  43.         x[i]=i+1;  
  44.     const IntArray arr(x,5);  
  45.     arr.showArray();   // 输出1 2 3 4 5  
  46.     x[3]=999;  
  47.     arr.showArray();   // 输出1 2 3 4 5, arr使用专属的存储空间!  
  48.     delete [] x;  
  49.     arr.showArray();   // 输出1 2 3 4 5, arr使用专属的存储空间!!  
  50.     system("pause");  
  51.     return 0;  
  52. }  

 【运行结果】1 2 3 4 51 2 3 4 51 2 3 4 5请按任意键继续. . .【解释】  程序的关键是IntArray类的构造函数和析构函数。在构造函数中,为arr_point指向的空间专门分配存储单元并赋值,从而这块存储区域成为相应对象的专属操作对象,不通过面向对象的机制,不能访问这儿的空间。尽管在main()函数中涉及的 x 数组的值修改,甚至释放 x 所占的空间,但此时,x 和arr 对象已经完全没有任何关系,对arr_point 所指向的空间没有任何的影响。程序中的各实体之间的“耦合”达到最小,各自按照各自的机制运行。  下面的图示进一步说明了例程中内存空间的变化。   六、补充一个例子:当指针指向字符时 

  1. #include <iostream>  
  2. #include <string.h>  
  3. #include <iomanip>   
  4. using namespace std;  
  5.   
  6. class CPerson   
  7. {  
  8. protected:  
  9.     char *m_szName;  
  10.     char *m_szId;  
  11.     int m_nSex;//0:women,1:man  
  12.     int m_nAge;  
  13. public:  
  14.     CPerson(char *name,char *id,int sex,int age);  
  15.     void Show();  
  16.     ~CPerson();  
  17. };  
  18.   
  19. CPerson::CPerson(char *name,char *id,int sex,int age)  
  20. {  
  21.     m_szName=new char[strlen(name)+1];  //分配正好大小的空间,根据形参name指向的字符串  
  22.     strcpy(m_szName,name);     //字符串的复制  
  23.     m_szId=new char[strlen(id)+1];    //指针成员都这样处理    
  24.     strcpy(m_szId,id);  
  25.     m_nSex=sex;  
  26.     m_nAge=age;  
  27. }  
  28.   
  29. void CPerson::Show()  
  30. {  
  31.     cout<<setw(10)<<m_szName<<setw(25)<<m_szId; //setw:设置输出数据的宽度,使用时应#include <iomanip.h>   
  32.     if(m_nSex==0)  
  33.         cout<<setw(7)<<"women";  
  34.     else  
  35.         cout<<setw(7)<<"man";  
  36.     cout<<setw(5)<<m_nAge<<endl;  
  37. }  
  38.   
  39. CPerson::~CPerson()  
  40. {  
  41.     delete [ ]m_szName;   //析构函数中要释放动态分配的空间  
  42.     delete [ ]m_szId;  
  43. }  
  44.   
  45. int main()  
  46. {  
  47.     char name[10],id[19];  
  48.     int sex,age;  
  49.     cout<<"input name,id,sex(0:women,1:man),age:\n";  
  50.     cin>>name>>id>>sex>>age;  
  51.     CPerson person(name,id,sex,age);  
  52.     person.Show();  
  53.     system("pause");  
  54.     return 0;  
  55. }  

【说明】   此例是博文《 第10周-任务2-CEmployee类继承CPerson类》程序中的一部分,该文以CPerson为基类作了派生。  在以前的博文中,《第9周-任务4-二维数组类》也涉及到了本文所讲的内容,请参考。    七、总结  重申本文中心:在C++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。