C++在三种情况下会调用拷贝构造函数(可能有纰漏),第一种情况是函数形实结合时,第二种情况是函数返回时,函数栈区的对象会复制一份到函数的返回去,第三种情况是用一个对象初始化另一个对象时也会调用拷贝构造函数。
除了这三种情况下会调用拷贝构造函数,另外如果将一个对象赋值给另一个对象,这个时候回调用重载的赋值运算符函数。
无论是拷贝构造函数,还是重载的赋值运算符函数,我记得当时在上C++课的时候,老师再三强调,一定要注意指针的浅层复制问题。
这里在简单回忆一下拷贝构造函数中的浅层复制问题
首先看一个浅层复制的代码:
#include
#include
#include
#include
using namespace std;
class Str{
public:
char *value;
Str(char s[])
{
cout<<"调用构造函数..."<<endl;
int len = strlen(s);
value = new char[len + 1];
memset(value,0,len + 1);
strcpy(value,s);
}
Str(Str &v)
{
cout<<"调用拷贝构造函数..."<<endl;
this->value = v.value;
}
~Str()
{
cout<<"调用析构函数..."<<endl;
if(value != NULL)
delete[] value;
}
};
int main()
{
char s[] = "I love BIT";
Str *a = new Str(s);
Str *b = new Str(*a);
delete a;
cout<<"b对象中的字符串为:"<<b->value<<endl;
delete b;
return 0;
}
输出结果为:
调用构造函数...
调用拷贝构造函数...
调用析构函数...
b对象中的字符串为:
调用析构函数...
首先结果并不符合预期,我们希望b对象中的字符串也是I love BIT但是输出为空,这是因为b->value和a->value指向了同一片内存区域,当delete a的时候,该内存区域已经被收回,所以再用b->value访问那块内存实际上是不合适的,而且,虽然我运行时程序没有崩溃,但是程序存在崩溃的风险呀,因为当delete b的时候,那块内存区域又被释放了一次,两次释放同一块内存,相当危险呀。
因此对于类中的指针数据成员,必须采用深层复制的方式进行拷贝,深层复制的代码如下:
#include
#include
#include
#include
using namespace std;
class Str{
public:
char *value;
Str(char s[])
{
cout<<"调用构造函数..."<<endl;
int len = strlen(s);
value = new char[len + 1];
memset(value,0,len + 1);
strcpy(value,s);
}
Str(Str &v)
{
cout<<"调用拷贝构造函数..."<<endl;
int len = strlen(v.value);
value = new char[len + 1];
memset(value,0,len + 1);
strcpy(value,v.value);
}
~Str()
{
cout<<"调用析构函数..."<<endl;
if(value != NULL)
{
delete[] value;
value = NULL;
}
}
};
int main()
{
char s[] = "I love BIT";
Str *a = new Str(s);
Str *b = new Str(*a);
delete a;
cout<<"b对象中的字符串为:"<<b->value<<endl;
delete b;
return 0;
}
运行结果为:
调用构造函数.. .
调用拷贝构造函数...
调用析构函数...
b对象中的字符串为:l love BIT
调用析构函数...
有时候我们会遇到这样一种情况,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么我们可以对指针进行浅复制,这样就避免了新的空间的分配,大大降低了构造的成本。
但是上面提到,指针的浅层复制是非常危险的呀。没错,确实很危险,而且通过上面的例子,我们也可以看出,浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间(同时也是b->value指向的空间)
所以我们可以把上面的拷贝构造函数的代码修改一下:
#include
#include
#include
#include
using namespace std;
class Str{
public:
char *value;
Str(char s[])
{
cout<<"调用构造函数..."<<endl;
int len = strlen(s);
value = new char[len + 1];
memset(value,0,len + 1);
strcpy(value,s);
}
Str(Str &v)
{
cout<<"调用拷贝构造函数..."<<endl;
this->value = v.value;
v.value = NULL;
}
~Str()
{
cout<<"调用析构函数..."<<endl;
if(value != NULL)
delete[] value;
}
};
int main()
{
char s[] = "I love BIT";
Str *a = new Str(s);
Str *b = new Str(*a);
delete a;
cout<<"b对象中的字符串为:"<<b->value<<endl;
delete b;
return 0;
}
运行结果为:
调用构造函数...
调用拷贝构造函数...
调用析构函数...
b对象中的字符串为:l love BIT
调用析构函数...
修改后的拷贝构造函数,采用了浅层复制,但是结果仍能够达到我们想要的效果,关键在于在拷贝构造函数中,最后我们将v.value置为了NULL,这样在析构a的时候,就不会回收a->value指向的内存空间。
这样用a初始化b的过程中,实际上我们就减少了开辟内存,构造成本就降低了。
但要注意,我们这样使用拷贝构造函数有一个前提是:用a初始化b后,a我们就不需要了,最好是初始化完成后就将a析构。如果说,我们用a初始化了b后,仍要对a进行操作,用这种浅层复制的方法就不合适了。
所以C++引入了移动构造函数,专门处理这种用a初始化b后就将a析构的情况。
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。移动构造函数的例子如下:
#include
#include
#include
#include
using namespace std;
class Str{
public:
char *str;
Str(char value[])
{
cout<<"普通构造函数..."<<endl;
str = NULL;
int len = strlen(value);
str = (char *)malloc(len + 1);
memset(str,0,len + 1);
strcpy(str,value);
}
Str(const Str &s)
{
cout<<"拷贝构造函数..."<<endl;
str = NULL;
int len = strlen(s.str);
str = (char *)malloc(len + 1);
memset(str,0,len + 1);
strcpy(str,s.str);
}
Str(Str &&s)
{
cout<<"移动构造函数..."<<endl;
str = NULL;
str = s.str;
s.str = NULL;
}
~Str()
{
cout<<"析构函数"<<endl;
if(str != NULL)
{
free(str);
str = NULL;
}
}
};
int main()
{
char value[] = "I love zx";
Str s(value);
vector<Str> vs;
//vs.push_back(move(s));
vs.push_back(s);
cout<<vs[0].str<<endl;
if(s.str != NULL)
cout<<s.str<<endl;
return 0;
}
在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
使用move()函数,可以将一个左值变成右值,因此a=move(b)调用的是移动构造函数。
移动构造函数是c++11的新特性,移动构造函数传入的参数是一个右值 用&&标出。
首先讲讲拷贝构造函数:拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。