您的位置:首页 > 编程语言 > C语言/C++

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

2012-09-11 16:32 363 查看
在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
; //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++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: