Vulkan

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++中,当类中有指针类型的数据成员时,必须注意在构造函数中,分配专门的存储单元,并将地址赋值给指针型数据成员。



posted on 2012-09-11 16:32  Vulkan  阅读(228)  评论(0编辑  收藏  举报

导航