什么时候需要复制构造函数? 复制构造函数是实现一个类所必不可少的吗? 同问,什么时候需要重载赋值操作符? 赋值操作符重载是否必须实现?
要回答本文引出的问题,我们首先编写一个有问题的类String,为了聚焦问题本身,这个类设计得很简单,它内部使用char指针保存字符串内容,并打印之.下面是类头文件String.h:
#ifndef String_H_
#define String_H_
class String{
public:
String();
explicit String(const char *str);
~String();
void Print() const;
private:
char *m_pBuf;
static unsigned s_nObj;
};
#endif
实现文件String.cpp:
#include "String.h"
#include
#include
using namespace std;
unsigned String::s_nObj = 0;
String::String()
{
cout<<"String default constructor invoked"<<endl;
m_pBuf = new char[1];
m_pBuf[0] = 0;
}
String::String(const char *str)
{
String::s_nObj ++;
cout<<"String constructed, s_nObj="<<String::s_nObj<<endl;
size_t sz = strlen(str);
m_pBuf = new char[sz+1];
strcpy(m_pBuf, str);
}
String::~String()
{
String::s_nObj --;
cout<<"String destructed, s_nObj="<<String::s_nObj<<endl;
delete []m_pBuf;
}
void String::Print() const
{
cout<<&m_pBuf<<":"<<m_pBuf<<endl;
}
说明:
这个类声明了一个构造函数,函数形参为指向const的char指针.类成员m_pBuf为指向char类型的指针,在构造函数中,使用new操作符创建char内存块,然后将传进来的实参拷贝到m_pBuf.
随后,使用Print函数打印字符串内容,注意,这个类在析构函数中释放m_pBuf,以期在对象销毁时释放内存.
下面是使用这个类的main函数:
#include
#include "String.h"
using namespace std;
void Show(const String str);
int main()
{
cout<<"example of use of String class"<<endl;
cout<<endl;
{
String string1("Hello world");
string1.Print();
cout<<endl;
Show(string1);
cout<<endl;
String string3(string1);
string3.Print();
cout<<endl;
String string4 = string1;
string4.Print();
cout<<endl;
}
return 0;
}
void Show(const String str)
{
str.Print();
}
对程序运行结果的说明:
咋一看,程序运行结果输出界面上除了我们自己打印的信息之外,还多了一堆运行时错误信息,其中最显眼的信息莫过于 "double free or corruption"
,说明程序释放了已经释放了的内存.而从我们对象计数全局变量s_nObj的打印信息上看,对象计数溢出了,这更说明了析构函数被对用了多次,即delete与new调用的次数不匹配.
进一步跟踪,发现我们实现的构造函数打印的信息次数和析构函数打印的信息并没有成对出现.
原来,在我们实现类时,如果没有提供一种叫做复制构造函数时,编译器自动为我们生成默认的复制构造函数,其原型为
type_name &type_name(const type_name &obj);
默认的复制构造函数在函数体执行时,逐成员复制非指针和引用类的成员,而指针类的成员也将按值复制,即只简单复制指针的地址,这样就导致了目标对象的指针成员变成了指向源对象指针成员的指针,指向与源对象指针成员的同一个内存块.
但是,对象在销毁时,都无一例额外的调用了析构函数,并且都执行了delete操作,多个对象试图delete同一个内存块,系统出于数据的保护,这就抛出了异常.
知道了问题根源,解决问题就容易得多了,那就是实现复制构造函数和重载赋值操作符,对指针成员进行深拷贝,添加复制构造函数和赋值操作符重载后的String类如下:
String.h
#ifndef String_H_
#define String_H_
class String{
public:
String();
explicit String(const char *str);
//添加复制构造函数
String(const String &s);
~String();
//添加赋值操作符重载
String &operator=(const String &s);
void Print() const;
private:
char *m_pBuf;
static unsigned s_nObj;
};
#endif
String.cpp
#include "String.h"
#include
#include
using namespace std;
unsigned String::s_nObj = 0;
String::String()
{
cout<<"String default constructor invoked"<<endl;
m_pBuf = new char[1];
m_pBuf[0] = 0;
}
String::String(const char *str)
{
String::s_nObj ++;
cout<<"String constructed, s_nObj="<<String::s_nObj<<endl;
size_t sz = strlen(str);
m_pBuf = new char[sz+1];
strcpy(m_pBuf, str);
}
//实现复制构造函数
String::String(const String &s)
{
String::s_nObj ++;
cout<<"String copy constructed, s_nObj="<<String::s_nObj<<endl;
size_t sz = strlen(s.m_pBuf);
m_pBuf = new char[sz+1];
strcpy(m_pBuf, s.m_pBuf);
}
String::~String()
{
String::s_nObj --;
cout<<"String destructed, s_nObj="<<String::s_nObj<<endl;
delete []m_pBuf;
}
//实现赋值操作符重载
String &String::operator=(const String &s)
{
if(this == &s){
return *this;
}
delete []m_pBuf;
size_t sz = strlen(s.m_pBuf);
m_pBuf = new char[sz+1];
strcpy(m_pBuf, s.m_pBuf);
return *this;
}
void String::Print() const
{
cout<<&m_pBuf<<":"<<m_pBuf<<endl;
}
如果类中声明了指针成员,则必须实现复制构造函数和重载赋值操作符,否则很有可能因为在复制对象或进行对象赋值时,由于对象析构导致两次释放内存,进而程序崩溃(视编译器而定).