好长时间没写C++了,今天写了个很简单的String类,竟然调试了半天,最终发现了一个十分隐蔽的陷阱,写出来供大家分享。
C++中类的拷贝构造函数的作用就是通过类的一个对象来实例化另一个对象。下面是我写的一个MyString类,头文件MyString.h:
#include <iostream>
using namespace std;
class MyString
{
public:
MyString();
MyString(const MyString& str);//这里就是我们研究的地方
MyString(char *p);
~MyString();
friend ostream& operator<<(ostream& out,MyString& ms)
{
return out<<"m_Data="<<ms.m_Data;
}
private:
int m_Length;
char* m_Data;//注意这里是指针,不是数组
};
再看看第一个类的实现版本:
#include <cstring>
#include "MyString.h"
using namespace std;
MyString::MyString():m_Length(1),m_Data(new char[m_Length])
{
memcpy(m_Data,"",m_Length);
}
MyString::MyString(const MyString& str)
:m_Length(str.m_Length),m_Data(new char [m_Length])//这里使用的是new char[],另外说一声,这里的m_Length和m_Data的初始化顺序不能改变,原因自己想就知道了
{
memcpy(m_Data,str.m_Data,m_Length);
}
MyString::MyString(char *p):m_Length(strlen(p)+1),m_Data(new char[m_Length])
{
memcpy(m_Data,p,m_Length);
}
MyString::~MyString()
{
//注意此处;
delete [] m_Data;
}
使用一个简单的例子来测试(test.cpp):
#include "MyString.h"
#include <iostream>
using namespace std;
int main()
{
MyString b("backup");
MyString a(b);
cout<<a<<endl;
}
结果就是大家熟悉的
再看一下MyString类的第二个实现版本(MyString.cpp)
#include <cstring>
#include "MyString.h"
using namespace std;
MyString::MyString():m_Length(1),m_Data(new char[m_Length])
{
memcpy(m_Data,"",m_Length);
}
MyString::MyString(const MyString& str)
:m_Length(str.m_Length),m_Data(str.m_Data)//这里直接复制指针;
{
//memcpy(m_Data,str.m_Data,m_Length);
}
MyString::MyString(char *p):m_Length(strlen(p)+1),m_Data(new char[m_Length])
{
memcpy(m_Data,p,m_Length);
}
MyString::~MyString()
{
//注意此处;
delete [] m_Data;
}
运行结果就是:
原因其实很简单,设置断点调试一下就知道了。
在第二个版本中,我们直接复制指针,之后的输出没有问题("m_Data=backup"),而根据拷贝构造函数,对象a和b都指向同一个字符数组,执行b的析构函数之后该字符数组的内存空间就没有了,再执行a的析构函数,delete不存在的内存空间就会出现错误了啦。
总结一句:C++还真不愧数一数二难的编程语言,错误都较难发现而且一般都想不到。