同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或者拷贝是完全可行的,这个拷贝过程只需要拷贝数据成员,而函数成员是共用的。在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为拷贝构造函数。也就是说当拿一个对象初始化另一个对象时,就会调动拷贝构造函数。
class Object
{
private:
int value;
public:
Object(int x=0):value(x)
{
std::cout<<"create obj:"<<this<<" "<<value<<std::endl;
}
~Object()
{
std::cout<<"destroy obj:"<<this<<" "<<value<<std::endl;
}
int Getvalue()
{
return value;
}
};
int main()
{
Object a(10);
Object x=a;//调用拷贝构造函数
Object b(a);//调用拷贝构造函数,该语句和上面这条语句是等价的
return 0;
}
任何一个类中有六种默认的缺省函数,分别如下:
class Object
{
private:
int value;
public:
Object (){}//构造函数
~Object(){}//析构函数
Object(const Object &obj):value(obj.value){}//拷贝构造函数
Object & operator=(const Object &obj)//赋值运算符的重载函数
{
value=obj.value;
return *this;
}
Object *operator&()//重载普通取地址符函数
{
return this;
}
const Object * operator&() const//重载常性取地址符函数
{
return this;
}
};
当拿一个对象去初始化一块空间时,要调动拷贝构造函数,我们来分析一段代码,分析拷贝构造函数的调动情况:
class Student
{
private:
int id;
public:
Student(int x=0):id(x)
{
std::cout<<"create student"<<this<<std::endl;
}
Student(const Student &stu):id(stu.id)
{
std::cout<<"copy create student"<<this<<std::endl;
}
~Student()
{
std::cout<<"destroy student"<<this<<std::endl;
}
};
int main()
{
Student stu1;//调用缺省构造函数
Student stu2(10);//调用一个参数的构造函数
Student stu3(stu2);//调用拷贝构造函数
return 0;
}
当程序进入主函数,先给stu1分配空间,然后调用缺省构造函数创建stu1这个对象,同样的给stu2分配空间,调用含有一个参数的构造函数创建stu2对象,接下来,系统为stu3分配空间,然后拿stu2这个对象去初始化stu3所占的这个空间,**此时调用拷贝构造函数来完成。**当程序遇到return 0结束时,依次销毁对象stu3,stu2,stu1,并释放他们所占的相应的空间。
一般的,当一个对象的生存期受函数影响,但在函数调用结束之后我们依然要使用这个函数里面被销毁的对象时,系统就会在主函数所在的栈帧上分配一个空间,调用拷贝构造函数将要被销毁的对象拷贝构造到在主函数所分配的空间中,然后在主函数中这个临时对象被接收以后,它的生存期也结束了,调用析构函数销毁这个临时对象并释放它对应的空间。具体分析如下:
Student fun()
{
Student stu(100);
return stu;
}
int main()
{
Student stu1;
stu1=fun();
return 0;
}
拷贝构造函数共有两个方面的使用,第一个就是上面说到的当函数的返回值是类的对象,函数执行完返回给调用者时使用。第二个使用点就是当函数的形参是类的对象,调用函数时,进行实参与形参结合使用,这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。
拷贝构造函数还有一个需要注意的地方,就是拷贝构造函数的参数必须使用引用,如果在拷贝构造函数中不使用引用,而是将一个真实的类的对象作为参数传递到拷贝构造函数中,会引起无穷递归,而引用相当于一个指针,只是将实参对象的地址传给了形参,而不会产生一个对象的副本。
除此之外,在拷贝构造函数的参数中使用const这个关键字更有好处,不允许去修改实参对象,提高安全性。
当我们对字符串进行拷贝的时候如果不注意会出现浅拷贝的问题从而使得程序奔溃,我们来看看这段代码:
class Object
{
private:
char* str;
public:
Object(const char* s)
{
if (s != nullptr)
{
int len = strlen(s) + 1;
str = new char[len];//在堆区开辟空间
strcpy_s(str, len, s);
}
else
{
str = new char[1];
*str = '\0';
}
}
~Object()
{
if (str != nullptr)
{
delete[]str;
}
str = nullptr;
}
};
int main()
{
Object obja("lizhuni");
Object objb(obja);//调用缺省拷贝构造函数
return 0;
}
由于obja这个对象在调用构造函数对str进行初始化的时候在堆区开辟空间并将字符串"lizhuni"放进去,而objb这个对象通过调动拷贝构造函数让str和obja这个对象的str指针指向同一块堆区空间,目前看来这是没有问题的,但是当对象生存周期结束调动析构函数销毁对象时就会出现问题,由于两个对象都要销毁,因此它们会释放同一块资源,这就导致同一块空间被释放了两次,因此程序就会崩溃,这也就是浅拷贝带来的问题。
如下图所示:
因此我们对上面程序做个改进,也就是将浅拷贝变成深拷贝:
Object(const Object& obj)
{
int len = strlen(obj.str) + 1;
str = new char[len];
strcpy_s(str, len, obj.str);
}