前言:上一篇文章实现了string的深拷贝写法;那我们能不能用浅拷贝写string类呢?当然可以;
一、
(1)
当我们需要对拷贝之后的对象进行修改时,采用深拷贝的方式; 如果不需要修改,只是输出字符串的内容时,或者是当偶尔修改的的时候,我们再采用深拷贝的方法;对于这两种情况,我们在实现拷贝构造函数时采用浅拷贝的方式。在采用浅拷贝的方式以后,那么两个对象的成员变量指针指向同一块字符串空间;根据上一篇的文章,我们知道这样的话,析构时必然会发生错误;那么怎么避免析构时出错;请看下文;
(2)
写之前,还是先说一下,为什么要实现string类的写时拷贝
原理:浅拷贝+引用计数;
因为在现实生活中,我们有很多情况下用一个对象来拷贝构造另一个对象的之后,我们只是把拷贝构造来的对象用一下而已,不会对他的字符串进行修改操作;那么这样的话,我们其实不用深拷贝那么麻烦。按照上一篇的深拷贝,如果我们要用一万个对象,那我们开辟一万个对象空间,但是得到之后只是用对象里面保存的字符串,并没有其他的操作;这样的话会很浪费空间;而且还要拷贝字符串,程序效率也会降低,所以我们想到了一种实现方式;就是说
,只有当我们要对拷贝来的字符串进行修改时,才采用深拷贝的方式,其余情况都采用浅拷贝;这种只有写的时候才进行深拷贝的方式叫做写时拷贝;
(3)实现写时拷贝的时候,因为采用了浅拷贝的方式,所以,为了避免析构的时候出现错误,上篇文章的析构函数就用不了;要在其基础上修改;我们可以在成员变量中添加一个变量count,用来记录拷贝的次数;每拷贝一次,我们就count++;然后在析构函数内部也加上if的判断语句;当我们每次析构一个对象的时候,就–count;当count等于0的时候,再进入if语句内部;释放空间;将变量置空;
再此,我给出了三种方案,其中有二个是错的;但都是经典的错误写法。所以把三种方法都说一下
二、两种经典的错误实现方法:
(1)方案一:错误写法
方案一:(错误)
#include<iostream>
using namespace std;
class String
{
public:
String(const char* str="")//构造函数
:_str(new char[strlen(str)+1])
,_count(1)
{
strcpy(_str,str);
}
String( String& s)//拷贝构造---浅拷贝
:_str(s._str)
{
_count=++(s._count);
}
String& operator=(String s)//赋值操作符重载---现代写法
{
std::swap(_str,s._str);
return *this;
}
~String()//析构函数
{
if (--_count==0)
{
delete[] _str;
_str=NULL;
}
}
private:
char* _str;
int _count;
};
int main()
{
String s1("abcdef");
String s2(s1);
return 0;
}
分析:
分析:首先这种方案是不对的,为什么呢?我们确实是添加了一个计数器_count;在构造函数里面;每次构造一个对象;_count赋值为1;在析构函数内部也加入了if(–_count=0)的判断机制,避免一块空间被多次释放;但是真的达到这个效果了吗?其实没有,因为在拷贝构造函数当中,我们每次++被拷贝的_count然后赋给要接受拷贝的对象的_count;当程序走完,此时两个_count都变为2;
当析构各自的对象时,这时,这两个对象的_conut各自都为2;当调用析构函数时,–_count以后,都不能够释放空间;这里的意思就是,当S2调用析构函数后,他没有释放空间,自己的_count变为了1;
但是接下来,当s1掉用析构函数时,它的_count不是1,仍然是2,–_count以后仍然不能够进入if语句释放空间;
所以,这种方法不对;那我们能不能定义一个_count使其能够被所有对象共用;当一个对象调用析构函数时,–_count;等到下一次另一个对象调用时,在上一次减了基础上再减呢;当然可以;我们把_count定义为局部静态变量;
(2)方案二:错误写法
在类中数据成员的声明前加上static,该成员是类的静态数据成员,必须在类外进行定义;
★★类中静态数据成员和非静态数据成员的区别:★★
①对于非静态数据成员,每个类对象都有自己的拷贝.
②而静态数据成员被当做是类的成员,无论这个类被定义了多少个,静态数据成员都只有一份拷贝, 为该类型的所有对象所共享(包括其派生类).所以,静态数据成员的值对每个对象都是一样的,它的值可以更新. 静态数据成员不属于某个类对象所私有,所以不能再在初始化列表中初始化
#include<iostream>
using namespace std;
#include<cassert>
class String
{
public:
String(const char* str="")//构造函数
:_str(new char[strlen(str)+1])
{
_count=1;
strcpy(_str,str);
}
String(const String& s)//拷贝构造---浅拷贝
:_str(s._str)
{
++_count;
}
String& operator=(String s)//赋值操作符重载---现代写法
{
std::swap(_str,s._str);
return *this;
}
~String()//析构函数
{
if (--_count==0)
{
delete[] _str;
_str=NULL;
}
}
private:
char* _str;
static int _count;
};
int String:: _count=1;
int main()
{
String s1("abcdef");
String s2(s1);
String s3("HELLO");
return 0;
}
分析: 这种写法表面上看上去好像是对的;其实也存在问题;如过只看我上面的两行测试代码;好像没有错误;当s1创建时,静态成员变量_count变为了1;然后构造S2之后,_count++;变为2;这时这个_count是两个对象的共用成员变量,
然后s2先调用析构函数,–_count变为1,
然后s1再调用类的析构函数,此时的_count为1,–_count以后,_count 变为0;这时,才将s1和s2共同指向的空间释放掉,避免一块空间,被多次释放;
but,当我们在这后面,再创建一个对象s3时,这就不对了,创建s3的时候,调用构造函数,将静态变量_count变为1;
此时类的共用成员变量_count变为了1;当从后往前析构时;
S3调用析构函数_conut变为0;s3的空间成功释放;
但是当s2调用析构函数时,此时计数器_count=0;–count之后变为-1,不会进入if判断语句内部释放空间;
S1调用析构函数时,_count变为-2;更不会析构s1和S2共同指向的空间。所以错误
三、正确的写时拷贝写法
(1)方案三:正确写法
#include<iostream>
using namespace std;
#include<cstring>
class String
{
public:
String(const char* str="")//构造
:_str(new char[strlen(str)+1])
,_PCount(new int(1))
{
strcpy(_str,str);
}
String(const String& s)//拷贝构造----浅拷贝
:_str(s._str)
,_PCount(s._PCount)
{
++_PCount[0];
}
String& operator=(const String& s)//赋值运算符重载
{
if (this!=&s)
{
delete[] _str;
delete _PCount;
_str=s._str;
_PCount=s._PCount;
++_PCount[0];
}
return *this;
}
~String()//析构
{
if (--_PCount[0]==0)
{
delete[] _str;
delete _PCount;
_str=NULL;
_PCount=NULL;
}
}
private:
char* _str;
int* _PCount;
};
void Test()
{
String s1("abcdef");
String s2(s1);
String s3("ABCEDF");
String s4;
s4=s1;
}
int main()
{
Test();
return 0;
}
分析:
正确写法(每段字符串内存加上属于自己的计数器指针,这样的话,只有当拷贝构造的时候,两个对象的计数器指针才会指向同一个计数器内存,让其++,如果构建新的对象,那么他的计数器为构造函数1,析构时,是自己的1变为0,不会影响拷贝构造的两个对象的计数器变化,这样就能到达:没有拷贝构造的对象,一次把自己的空间释放掉,而拷贝构造的对象,所有对象的计数器都指向同一个计数器空间,释放该计数器的字符串内存;也不会出错)
图示说明:
①
String s1("abcdef");
String s2(s1);
String s3("ABCEDF");
②赋值运算符重载详解
String& operator=(const String& s)//赋值运算符重载
{
if (this!=&s)
{
delete[] _str;
delete _PCount;
_str=s._str;
_PCount=s._PCount;
++_PCount[0];
}
return *this;
}
测试
String s1("abcdef");
String s2(s1);
String s3;
s3=s1;
(2)方案三写时拷贝的改进
前面的方案三,我们是用两个指针,分别管理内存和计数器,这样每次构造函数的时候,要分别开辟空间给各自的指针;这样做降低效率;我们可以只开辟一次空间;里面既存计数器又存字符串;在开辟的空间的前四个字节存储计数器,后面存储字符串;
#include<iostream>
using namespace std;
#include<cstring>
#include<cassert>
class String
{
public:
String(const char* str="")//构造
:_str(new char[strlen(str)+1+4])
{
cout<<"构造"<<endl;
_str+=4;//走到数据区首地址
strcpy(_str,str);
GetCount()=1;//计数器区域赋值为1
}
String(const String& s)//拷贝构造
:_str(s._str)
{
cout<<"拷贝构造"<<endl;
++GetCount();//将所指向的共同内存的计数器加+
}
String& operator=(String& s)//赋值运算符重载---赋值的对象计数器要++;被赋值的对象计数器要++
{
if (this!=&s)
{
cout<<"构赋值运算符重载"<<endl;
/*判断被赋值的对象的计数器--,是否为0;如果为0,就要释放他的(计数区+数据区)
如果没有到达0;则只是给计数器减1;*/
if (--GetCount()==0)
{
delete[] (_str-4);//回到内存的首位置
}
++(s.GetCount());
_str=s._str;
}
return *this;
}
~String()//析构
{
cout<<"析构"<<endl;
if (--GetCount()==0)
{
cout<<"释放"<<endl;
delete[] (_str-4);
_str=NULL;
}
}
char& operator[](size_t index)// 写时拷贝
{
cout<<"重载[]"<<endl;
assert(index>=0 && index<(int)strlen(_str));
if (GetCount()=1)//如果该对象的计数器为1
{
return _str[index];//直接返回index位置的值,进行修改
}
如果不为1
--GetCount();//先给对象的计数器--
char* tmp=new char[strlen(_str)+1+4];//然后开辟一个同样大小的空间;用指针指着
strcpy(tmp+4,_str);//给这块空间字符串区域拷贝原空间的字符串
_str=tmp+4;//将指针指向新开辟字符串的首位置
GetCount()=1;//将指针的计算器置为1
return _str[index];//返回index位置的值
}
private:
int& GetCount()//取计数器的值
{
return *((int*)_str-1);
}
private:
char* _str;
};
void Test()
{
String s1("abcdef");
String s2(s1);
String s3(s2);
s3[3]='q';
}
int main()
{
Test();
return 0;
}
部分函数过程图示:
①拷贝构造函数
②赋值运算符重载
四、写时拷贝比普通拷贝的效率高
#include<iostream>
using namespace std;
#include<cstring>
#include<cassert>
#include<Windows.h>
namespace WriteCopy
{
class String
{
public:
String(const char* str="")//构造
:_str(new char[strlen(str)+1+4])
{
_str+=4;
strcpy(_str,str);
GetCount()=1;
}
String(const String& s)
:_str(s._str)
{
++GetCount();
}
~String()//析构
{
if (--GetCount()==0)
{
delete[] (_str-4);
_str=NULL;
}
}
private:
int& GetCount()//取计数器的值
{
return *((int*)_str-1);
}
private:
char* _str;
};
}
namespace MyString
{
class String
{
public:
String(const char* str="")
:_str(new char[strlen(str)+1])
{
strcpy(_str,str);
}
String(const String& s)
:_str(new char[strlen(s._str)+1])
{
strcpy(_str,s._str);
}
~String()
{
if (NULL!=_str)
{
delete[] _str;
_str=NULL;
}
}
private:
char* _str;
};
}
void Test()
{
int start=GetTickCount();
WriteCopy::String s1("abcdef");
for (int i=0;i<10000000;i++)
{
WriteCopy::String s2(s1);
}
int end=GetTickCount();
cout<<"写时拷贝"<<end-start<<endl;
start=GetTickCount();
MyString::String s3("abcdef");
for (int i=0;i<10000000;i++)
{
MyString::String s4(s3);
}
end=GetTickCount();
cout<<"普通拷贝"<<end-start<<endl;
}
int main()
{
Test();
return 0;
}
end!!!
接下来模拟实现c++库中的string类;