还没两天呢又到这里来写BLOG了,没办法,《C++ Primer Plus》的第12章确实很不好学。关于类和动态内存分配的内容,很多东西你好像是看懂,但等到你真正自己动手编程时才发现自己根本没有真正理解。这不,我在做书后第3道编程练习题时又碰了钉子。这道题要求把前几章的一个例子程序改造一下,把私有类成员里原来固定长度的自动变量char[]字符串改成char*,等到使用时再用new分配合适的内存空间。看似很简单,我刷刷刷地写好了,以下是头文件中的类声明:
//stock3.h #ifndef STOCK3_H_ #define STOCK3_H_ #include <iostream> class Stock { private: char *company; int shares; double share_val; double total_val; void set_tot(){total_val=shares*share_val;} public: Stock(); //构造函数#1 Stock(const char *co,int n=0,double pr=0.0); //构造函数#2 ~Stock(); //析构函数 void buy(int num,double price); void sell(int num,double price); void update(double price); const Stock &topval(const Stock &s)const; friend std::ostream &operator<<(std::ostream &os,const Stock &st); }; #endif
这个声明和原来相比改动不大,仅仅是把字符串company的类型从原来定长的char[50]改成了char*,外加多了一个析构函数。不过,因为涉及到分配动态内存,因此两个构造函数的定义有较大的改动。以下是两个构造函数外加析构函数的定义:(其它与本文无关的方法函数在此省略)
Stock::Stock() //构造函数#1 { company=new char[8]; std::strcpy(company,"no name"); shares=0; share_val=0.0; total_val=0.0; } Stock::Stock(const char *co, int n, double pr) //构造函数#2 { int len=std::strlen(co); company=new char[len+1]; std::strcpy(company,co); if(n<0) { std::cerr<<"Number of shares can't be negative." <<company<<" shares set to 0./n"; shares=0; } else shares=n; share_val=pr; set_tot(); } Stock::~Stock() //析构函数 { delete[] company; }
OK,到此为止好像一切都很完美。加上原来给的主程序,感觉应该成功了:
#include <iostream> #include "stock3.h" const int STKS=4; int main() { using std::cout; using std::ios_base; Stock stocks[STKS]={ Stock("NanoSmart",12,20.0), Stock("Boffo Objects",200,2.0), Stock("Monolithic Obelisks",130,3.25), Stock("Fleep Enterprises",60,6.5) }; cout.precision(2); cout.setf(ios_base::fixed,ios_base::floatfield); cout.setf(ios_base::showpoint); cout<<"Stock holdings:/n"; int st; for(st=0;st<STKS;st++) cout<<stocks[st]; Stock top=stocks[0]; for(st=1;st<STKS;st++) top=top.topval(stocks[st]); cout<<"/nMost valueable holding:/n"; cout<<top; system("pause"); return 0; }
此时悲剧出现了,在执行完系统暂停system("pause");按任意键恢复运行后,屏幕上跳出了一个刺眼的错误消息框:
编程序的人都晓得,程序不怕编译错误,就怕那些谁也说不清的bug,而且这种bug还是系统崩溃的bug,未提供可供直接参考的错误信息,让人头疼,并且无从下手。不过它是在系统暂停恢复后出现的,主程序里系统暂停语句后就是return语句了,也就是说,很可能是析构函数出了问题。为了验证这个假设,我在析构函数处设置了断点。果不其然,程序走到析构函数时中断后,按F11执行单步跟踪程序居然不往下走,按两下后那个错误就跳了出来。
问题好像是出在析构函数,可是析构函数总共就一句话,不那么写你还能怎么写呢?我感觉到可能问题还出在更深层的地方,于是上CSDN里寻求帮助,没找到问题的直接答案,但是找到了一些很有帮助的信息——有些高手说出现这种崩溃错误的原因很可能是delete试图去释放一个已经不饿释放了的指针!
同一个指针被释放两次?难道说析构函数调用的次数比构造函数还要多?我想看看内存中类存储的情况,于是把断点往前设了几个语句。通过在中断时检查变量监视器可以很清楚的了解到内存利用的情况:
有没有发现bug?其中有两个对象的不但内容一样,company成员还指向了同一个地址!(见红框)也就是说,这些对象在逐个析构时company的那个地址真的会被delete两次啊!有木有!
top这个对象哪里来的?我当时一时想不起来,原来它在主程序中间的位置才出现的,那是一个求最大值对象的循环,top用于记录值最大的那个对象,而且原题中没有说要对这个功能模块进行修改,我也就依葫芦画瓢想也没想就抄了上去。现在再来仔细看一遍这段程序:
Stock top=stocks[0]; //对象初始化 for(st=1;st<STKS;st++) top=top.topval(stocks[st]); //对象的复制 cout<<"/nMost valueable holding:/n"; cout<<top;
它第1句使用了等号进行了对象的初始化,第3句也用了等号进行了对象的复制。然而,斯蒂芬老师在书中提到,这些复制都是所谓的浅复制 ——对于company成员,它只是简单地把内存地址赋给新的对象的company成员,自己却未去开辟空间存储它。也就是说,两个对象中的company指针指向了同一段内存,那么析构时出问题也便是必然的了。
怎么解决呢?书里说的很清楚,对于第一种在初始化时的复制使用复制构造函数,第二种普通的赋值要用重载=号的方法。两者所要完成的任务差不多,都是显式地要告诉C++不要仅仅复制指针,而要连内容一起也复制进来,这就是所谓的深复制 。下面是复制构造函数和“=”号重载的定义:
Stock::Stock(const Stock &st) //复制构造函数 { company=new char[std::strlen(st.company)+1]; std::strcpy(company,st.company); shares=st.shares; share_val=st.share_val; set_tot(); } Stock &Stock::operator =(const Stock &st) //“=”号重载 { if(this==&st) //防止自我复制 return *this; delete[] company; company=new char[std::strlen(st.company)+1]; std::strcpy(company,st.company); shares=st.shares; share_val=st.share_val; set_tot(); return *this; }
这样一来,问题就解决了。从变量将监视器中看出,两个Stock对象的company成员使用了不同的地址:(见红框)
至此,我不得不佩服斯蒂芬老师的过人之处,这道编程题表面上是考如何使用动态内存,实际上醉翁之意不在酒,题目的精华实际上完全在那个题中好像只字未提的“=”号上。通过这个题,我对于复制构造函数有了深刻而鲜活的认知。高,实在是高!