【什么情况下会用到c++中的拷贝构造函数】:
1)用已经存在的同类的对象去构造出另一个新的对象
2)当函数的形参是类的对象时,这时调用此函数,使用的是值的拷贝,也会调用拷贝构造函数
3)当函数的返回值是类的对象时,这时当函数调用完后,会将函数的对象拷贝构造出一个临时的对象并传给函数的返回处
【浅拷贝】:(位拷贝(值拷贝))
1、概念:所谓的浅拷贝就是当在进行对象的复制时,只是进行对象的数据成员的拷贝,其中默认的拷贝构造函数也是浅拷贝。大多数情况下,使用浅拷贝是没有问题的,但是当出现动态成员,就会出现问题。
2、关于浅拷贝的使用举例:
#include<iostream> using namespace std; class Test { public: //构造函数 Test(int a) :_a(a) {} //拷贝构造函数 Test(const Test& x) { _a = x._a; } private: int _a; }; int main() { Test b(10); Test c(b); return 0; }
3、浅拷贝的缺陷:
浅拷贝对于指针成员不可行。多个对象共用同一块空间,同一内存地址,但是在调用析构函数释放空间的时候,多次调用析构函数,这块空间被释放了多次,此时程序就会崩溃。
【引用计数的拷贝】:
1、(怎么引入的)概念:因为浅拷贝的缺陷,所以在这个时候我们就引入了引用计数的拷贝。
【说明】:引用计数的拷贝是用来解决浅拷贝存在的问题的,所以它也是一种浅拷贝
2、如何实现:我们为每个内存的字符数组添加一个引用计数pcount,即就是在构造函数申请空间的时候多申请出来4个字节。表示有多少个对象使用这块内存,有多少个对象使用,就让pcount值加1,当对象被析构的时候,让pcount值减1,当pcount值为0的时候,将这块内存释放掉。当然pcount也要实现内存共享,所以它也是一个堆中的数据,每个对象都有一个指向它的指针。
3、【说明】:但在此时,pcount的类型的选取,就会要有所考虑?
1)如果选取int类型:(不采取)
#include<iostream> using namespace std; class String { public: //构造函数 String(const char* ptr = "") { if(ptr == NULL) { _ptr = new char[1]; _pcount = 1; *_ptr = '\0'; } else { _pcount = 1; _ptr = new char[strlen(ptr)+1]; strcpy(_ptr,ptr); } } //拷贝构造函数 String(String& s) :_ptr(s._ptr) ,_pcount(s._pcount) { _pcount++; } //赋值运算符重载 String& operator=(const String& s) { if(this != &s) { if(--_pcount == 0) { delete[] _ptr; //delete _pcount; } else { _ptr = s._ptr; _pcount = s._pcount; (_pcount)++; } } return *this; } //析构函数 ~String() { if((0 == --_pcount) && _ptr!= NULL) { delete[]_ptr; //delete _pcount; _ptr = NULL; } } //重载[] char& operator[](size_t size) { if(--_pcount >1) { char* ptemp = new char[strlen(_ptr)+1]; int pcount = 1; strcpy(ptemp,_ptr); _pcount--; _ptr = ptemp; _pcount = pcount; } return _ptr[size]; } private: char*_ptr; int _pcount; }; void FunTest() { String s1("hello"); String s2(s1); String s3(s2); s3 = s2; } int main() { FunTest(); return 0; }调试:
(注意这里我将断点就走到s2,意在说明问题):本来增加s2的时候,两个对象的计数应该是一样的,但是现在一个是1,一个是2,不同步,我们了解到这两个对象的计数变量的地址是不一样的。说明此pcount是公共的,可以被多个对象同时访问。
2)如果选取的是static类型的:(不采取)
#include<iostream> using namespace std; class String { public: //构造函数 String(const char* ptr = "") { if(ptr == NULL) { _ptr = new char[1]; _pcount = 1; *_ptr = '\0'; } else { _pcount = 1; _ptr = new char[strlen(ptr)+1]; strcpy(_ptr,ptr); } } //拷贝构造函数 String(String& s) :_ptr(s._ptr) { _pcount++; //因为是静态的,所以直接进行计数的增值就可以了 } //赋值运算符重载 String& operator=(const String& s) { if(this != &s) { if(--_pcount == 0) { delete[] _ptr; //delete _pcount; } else { _ptr = s._ptr; _pcount = s._pcount; (_pcount)++; } } return *this; } //析构函数 ~String() { if((0 == --_pcount) && _ptr!= NULL) { delete[]_ptr; //delete _pcount; _ptr = NULL; } } //重载[] char& operator[](size_t size) { if(--_pcount >1) { char* ptemp = new char[strlen(_ptr)+1]; int pcount = 1; strcpy(ptemp,_ptr); _pcount--; _ptr = ptemp; _pcount = pcount; } return _ptr[size]; } private: char*_ptr; static int _pcount; }; int String::_pcount = 0; void FunTest() { String s1("hello"); String s2(s1); String s3(s2); s3 = s2; String s4("world"); String s5(s4); } int main() { FunTest(); return 0; }调试:
先走到s3,然后走到s4,用s4来构造s5,结果就不对了,走到s4的时候,计数器又变成了1,说明这5个对象公用一个pcount,不能实现引用计数
3)那么我们这样想:如果一个对象第一次开辟空间存放字符串再开辟一块新的空间存放新的额引用计数,当它拷贝构造其它对象时让其它对象的引用计数都指向存放引用计数的同一块空间,pcount设置成int*就可以啦,但是这种方式有缺陷。(读者可以自己实现下)
缺陷一:每次new两块空间,创建多个对象的时候效率比较低
缺陷二:它多次分配小块空间,容易造成内存碎片化,导致分配不出来大块内存
4、代码实现:(所以我将优化版的代码贴出来,其实就是仿照new的底层实现,开辟一块空间,但是它的头几个字节用于计数)
#include<iostream>
using namespace std;
class String
{
public:
String(char *ptr = "")
{
if(ptr == NULL)
{
_ptr = new char[strlen(ptr)+5];
_ptr = new char[5];
*(_ptr+4) = '\0';
}
else
{
_ptr = new char[strlen(ptr)+5];
*((int*)_ptr) = 1;
_ptr += 4;
strcpy(_ptr,ptr);
}
}
String(const String& s)
:_ptr(s._ptr)
{
(*((int*)(_ptr-4)))++;
}
String& operator=(const String& s)
{
if(this != &s)
{
if(--(*((int*)(_ptr-4))) == 0)
{
delete[]_ptr;
}
else
{
_ptr = s._ptr;
++(*(int*)(_ptr-4));
}
}
return *this;
}
~String()
{
if((_ptr != NULL) && ((--(*((int*)(_ptr-4)))) == 0))
{
delete[]_ptr;
_ptr = NULL;
}
}
private:
char* _ptr;
};
void Funtest()
{
String s1("hello");
String s2(s1);
String s3(s2);
s3 = s2;
}
int main()
{
Funtest();
return 0;
}
【深拷贝】:(址拷贝)
1、概念:采取在堆中申请新的空间来存取数据,这样数据之间相互独立。址拷贝。
2、举例:(string类中的拷贝构造函数)
String(const String& s)
{
_ptr = new char[strlen(s._ptr)+1];
strcpy(_ptr,s._ptr);
}
【写时拷贝】:
1、(如何引入的)概念:但是当其中一个对象改变它的值时,其他对象的值就会随之改变,所以此时我们采取这样一种做法,就是写时拷贝。
2、核心思想:
(写入时拷贝)如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),它们会共同获取相同的指针指向的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程中对其他调用者都是透明的。做法的优点:如果调用者没有修改该资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源
(写时拷贝)指用浅拷贝的方法拷贝其他对象,多个指针指向同一块空间,只有当对其中一个对象修改时,才会开辟一个新的空间给这个对象,和它原来指向同一空间的对象不会收到影响。
3、做法:给要改变值的那个对象重新new出一块内存,然后先把之前的引用的字符数据复制到新的字符数组中,这就是写时拷贝。注意,同时还要把之前指向的内存的引用计数减1(因为它指向了新的堆中的字符数组),并在堆中重新new一个块内存,用于保存新的引用计数,同时把新的字符数组的引用计数置为1。因为此时只有一个对象(就是改变值的对象)在使用这个内存。
4、代码实现:
#include<iostream>
using namespace std;
class String
{
public:
//构造函数
String(const char* ptr = "")
{
if(ptr == NULL)
{
_ptr = new char[1];
_pcount = new int(1);
*_ptr = '\0';
}
else
{
_pcount = new int(1);
_ptr = new char[strlen(ptr)+1];
strcpy(_ptr,ptr);
}
}
//拷贝构造函数
String(String& s)
:_ptr(s._ptr)
,_pcount(s._pcount)
{
(*_pcount)++;
}
//赋值运算符重载
String& operator=(const String& s)
{
if(this != &s)
{
if(--(*_pcount) == 0)
{
delete[] _ptr;
delete _pcount;
}
else
{
_ptr = s._ptr;
_pcount = s._pcount;
(*_pcount)++;
}
}
return *this;
}
//析构函数
~String()
{
if(0 == --(*_pcount) && _ptr!= NULL)
{
delete[]_ptr;
delete _pcount;
_ptr = NULL;
}
}
//重载[]
char& operator[](size_t size)
{
if(--(*_pcount) >1)
{
char* ptemp = new char[strlen(_ptr)+1];
int* pcount = new int(1);
strcpy(ptemp,_ptr);
(*_pcount)--;
_ptr = ptemp;
_pcount = pcount;
}
return _ptr[size];
}
private:
char*_ptr;
int* _pcount;
};
void FunTest()
{
String s1("hello");
String s2(s1);
String s3(s2);
s3 = s2;
String s4(s1);
s1[0] = 'y';
}
int main()
{
FunTest();
return 0;
}
经调试,结果如下:
【注意】:因为不止一个对象使用这一块内存,当修改自己的时,也等于修改了他人的。在向这块存储单元写之前,应该确信没有其他人使用它。如果引用计数大于1,在写之前必须拷贝这块存储单元,这样就不会影响他人了。