String(深浅拷贝、字符串增删查改、写时拷贝)
程序员文章站
2022-04-30 10:40:17
...
浅拷贝与深拷贝
所谓浅拷贝是指将对象的每一个值拷贝给另一个对象那么就存在两个问题:
如果是动态申请的空间值拷贝后会对该空间析构两次,程序崩溃;
一个对象值的改变会用影响另一个对象。
class String //管理字符串的类
{
public :
char *c_str()
{
return _str;
}
~String() //析构函数
{
if (_str)
{
delete[] _str;
_str = NULL;
}
}
private :
char *_str;
};
String(char *str)//不带参构造函数
:_str(str) //浅拷贝
{
}
str2是动态开辟,可修改并且不会影响对象s1。
深拷贝
String()//对象里是空串
:_str(new char[1]) //为了与delete[]匹配
{
_str[0] = '\0';
}
String(char *str = "")//缺省构造函数,隐含有'\0';不能给'\0'.类型不匹配
:_str(new char[strlen(str)+1])//深拷贝,先开一样大的空间,+1为了存'\0'
{
strcpy(_str, str);//再拷贝
}
//上述两个构造函数只能存在一个,否则对于无参函数,无法识别调用哪一个构造函数
总结:深拷贝即先开一样大空间再拷贝。
拷贝构造、赋值和operator[ ]
String(const String& s)//拷贝构造
:_str(new char[strlen(s._str)+1])//先开一样大空间
{
strcpy(_str, s._str);//拷贝
}
String& operator=(const String &s)//赋值
{ //可以用void返回,但用string& 返回是为了连续赋值
if (this != &s)//防止自己给自己赋值,如果自己给自己赋值,delete[]后,将变为随机值
{
delete[]_str;//先将对象原空间释放
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
size_t Size()//计算串大小
{
return strlen(_str);
}
char& operator[](size_t pos)//operator[]可以像数组一样访问元素
{
assert(pos < Size()); //要调用Size()函数,可以考虑用空间换时间
return _str[pos];//出了作用域还存在,用&返回
}
现代写法(拷贝、赋值)
现代写法即剥削,剥削其他变量。
class String
{
public:
String(char *str = "")//缺省构造函数,隐含有'\0';不能给'\0'.类型不匹配
:_str(new char[strlen(str) + 1])//深拷贝,先开一样大的空间
{
strcpy(_str, str);//再拷贝
}
String(const String& s)//拷贝构造
:_str(NULL)//初始化为空,保证在交换后tmp._str不为随机值,可以析构
{
String tmp(s._str);//构造,使tmp.str=s._str
swap(_str, tmp._str);//库里有swap函数,this->_str剥削tmp._str
}
String& operator=(const String &s)//赋值
{
//不用判断this != &s,因为析构tmp对this没有影响
String tmp(s._str);//构造 一样大的空间但不指向同一段空间
swap(_str, tmp._str); //剥削
return *this;
}
//还可写为:
String& operator =( String s)//对象s是临时变量,相当于上例的tmp
{
swap(_str, s._str);//剥削
return *this;
}
~String() //析构函数
{
if (_str)
{
delete[] _str;
_str = NULL;
}
}
char *c_str()
{
return _str;
}
private:
char *_str;
};
字符串类增删查改
class String1
{
public:
String1(char *str = "") //构造
{
_capacity = strlen(str);//_capacity不包括'\0'
_size = _capacity;
_str = new char[_capacity + 1];//+1存'\0'
strcpy(_str, str);
}
//拷贝构造现代写法
String1(const String1& s)//拷贝构造 剥削
:_str(NULL) //需要初始化为空,交换后,tmp._str为空
,_size(0)
,_capacity(0)
{
String1 tmp(s._str);//构造tmp
swap(_str, tmp._str);//剥削
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
String1(const String1& s)//拷贝构造
{
_capacity = strlen(s._str);
_size = _capacity;
_str = new char[_capacity + 1];//+1存'\0'
strcpy(_str, s._str);
}
String1& operator=(String1 s)//赋值 s是临时变量
{
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
return *this;
}
bool operator < (const String1& s)//小于运算符重载
{
const char *str1 =this->_str;
const char *str2 = s._str;
while (*str1&&*str2)
{
if (*str1 < *str2)
return true;
else if (*str1 > *str2)
return false;
else
{
str1++;
str2++;
}
}
if (*str2)//这时*str1='\0'
{
return true;
}
else //*str2='\0' 或者*str1=*str1='\0'
return false;
}
bool operator==(const String1& s)
{
const char *str1 =this->_str;
const char *str2 = s._str;
while (*str1&&*str2)
{
if (*str1 == *str2)
{
str1++;
str2++;
}
else
return false;
}
if (*str1 == '\0'&& *str2 == '\0')
return true;
else
return false;
}
bool operator <= (const String1& s)
{
return ((*this < s) || (*this == s));
}
bool operator > (const String1& s)// >
{
return !(*this <= s);
}
bool operator >= (const String1& s)
{
return !(*this < s);
}
bool operator != (const String1& s)
{
return !(*this == s);
}
//注:运算符重载进行赋用时,没有函数压栈,即没有调用函数,由于是类内函数,默认为内联函数,直接展开
void Expand(size_t n)
{
if (n > _capacity)
{
char *tmp = new char[n + 1];//多开一个存'\0'
strcpy(tmp, _str);//需要将原数据拷到新空间
delete[]_str;
_str = tmp;
_capacity = n; //_capacity不包括'\0'
}
}
void PushBack(char ch)//尾插
{
//if (_size == _capacity) //需要增容
//{
// Expand(_capacity * 2);//开2倍空间 即this->Append(_capacity * 2)
//}
//_str[_size] = ch;
//_size++;
//_str[_size] = '\0';//注意加上'\0'
Insert(_size, ch);//直接用插入
}
void Append(const char *str)//拼接字符串
{
//size_t len = strlen(str);
//if (_size + len > _capacity)//==时刚刚好,因为_capacity不算'\0'
//{
// Expand(_size + len);
//}
//strcpy(_str + _size, str);//strcpy有拷贝'\0'
//_size += len;
Insert(_size, str);//直接用插入
}
String1 operator+(const char* str) //+运算符重载
{
String1 tmp(*this); //拷贝构造
tmp.Append(str);
return tmp; //拷贝构造
}
String1& operator+=(const char* str) //+=运算符重载
{
Append(str); //即this->Append(str)
return *this;
}
void Insert(size_t pos, char ch)//在坐标pos处插入ch
{
assert(pos <= _size);//等于就是尾插
if (_size == _capacity)
{
Expand(_capacity * 2);
}
int end = _size;//_str[_size]是'\0'
//将数据向移
while (end >= (int)pos)
//需要将pos强转为int,否则类型提升,当pos=0,end不会小于0,陷入死循环
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = ch;
_size++;
}
void Insert(size_t pos, const char* str)//在坐标pos处插入字符串str
{
assert(pos <= _size);//等于时是拼接
size_t len = strlen(str);
if (_size + len > _capacity) //== 时刚刚好,因为_capacity不算'\0'
{
Expand(_size + len);
}
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
end--;
}
for (size_t i = 0; i < len; i++)
{
_str[pos++] = str[i];
}
_size += len;
}
void Erase2(size_t pos, int len = 1)//在坐标为pos位置删除长度为len字符串
{
assert(pos < _size);
int cur = pos;
while (_str[cur + len])
{
_str[cur] = _str[cur + len];
cur++;
}
_str[cur] = '\0';
_size -= len;
}
void Erase1(size_t pos, int len = 1)//在坐标为pos位置删除长度为len字符串
{
assert(pos < _size);
int end = pos + len;
int tail = _size - pos - len;
for (int i = 0; i <= tail; i++)
{
_str[pos] = _str[end];
pos++;
end++;
}
_size -= len;
}
int Find(const char* sub)//查找子串第一次出现的位置
{
int subindex = 0;
int strindex = 0;
int cur = strindex;
int sublen = strlen(sub);
while (cur< _size)
{
strindex = cur;
subindex = 0;
while (subindex < sublen)
{
if (_str[strindex] == sub[subindex])
{
strindex++;
subindex++;
}
else
break;
}
if (subindex == sublen)
return cur;
else
cur++;
}
return -1; //没有找到
}
~String1() //析构函数
{
if (_str)//为空就不析构
{
delete[] _str;
_str = NULL;
}
}
char *c_str()
{
return _str;
}
private:
char *_str;
size_t _size;//标识大小 否则要用strlen,以空间换时间
size_t _capacity; //标识容量,这样可以一次开合理空间,否则一次开一次,以空间换时间
};
字符串增删查改测试用例
void Test4()
{
String1 s1("hello");
cout << s1.c_str() << endl;
String1 s2(s1);
String1 s3;
s3 = s2;
s3.PushBack('m');
cout << s3.c_str() << endl;//hellom
s3 = s1 + " world";
cout << s3.c_str() << endl;//hello world
s1 += " world";
cout << s1.c_str() << endl;
//在+运算符重载中,拷贝构造了两次,对于字符串来说拷贝构造代价较大,
//而+=运算符重载中,没有拷贝构造,所以一般将+运算符重载写成下列形式
//若给s3加" happy",下列形式只有一次拷贝构造
String1 s4(s3);//拷贝构造
s4 += " happy";
cout << s4.c_str() << endl; //hello world happpy
/*s3 += "happy everday";
if (s3 != s4)
printf("yes\n");
else
printf("no\n");*/
s4.Insert(2, 'm');
cout << s4.c_str() << endl;
s4.Insert(3, "pick");
cout << s4.c_str() << endl;
s4.Erase1(8, 3);
cout << s4.c_str() << endl;
cout << s4.Find("happy") << endl;
cout << s4.Find("pick") << endl;
cout << s4.Find("world") << endl;
cout << s4.Find("hello") << endl;
}
扩展Resize和Reserve
两个函数均为类内函数
void Reserve(size_t n)//增容到n
{
if (n > _capacity)
{
Expand(n); //Expand一般为私有
}
}
void Resize(size_t n, char ch = '0')//将容量设置为n,即可以缩容,也可以扩容,并且初始化
{
if (n < _size)
{
_size = n;//直接将_size设置为n
_str[_size] = '\0';//注意是字符串
}
else // _size<n<_capacity 或者n>_capacity
{
if (n > _capacity) //若n>_capacity,先增容
Expand(n);
//现在_size<n<_capacity 或者n>_capacity初始化逻辑一样
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
有一个面试题为将0-N的数字拼接为字符串?
void Test5()
{
const size_t N = 10000;
char buff[128];
int begin1 = GetTickCount();
String1 s1;
for (size_t i = 0; i < N; i++)
{
_itoa(i, buff, 10);//将整形转为十进制字符串
s1 += buff; //依次将字符串加在后面
}
int end1= GetTickCount();
cout << end1 - begin1 << endl;//20313 也就是20秒
//将大部分时间用在+=的开空间上了,如果我们能预先将空间开好,时间将会大大缩短
int begin2 = GetTickCount();
String1 s2;
s2.Reserve(1 * 9 + 2 * 90 + 3 * 900 + 4 * 9000 + 5 * 90000 + 1);
//在这里根据N直接定死空间,可以写算法表示这个公式开合适空间
for (size_t i = 0; i < N; i++)
{
_itoa(i, buff, 10);//将整形转为十进制字符串
s2 += buff; //依次将字符串加在后面
}
int end2= GetTickCount();
cout << end2 - begin2 << endl; //31 即还没有1秒
}
写时拷贝
尽管深拷贝可以解决多次析构同一空间造成程序崩溃、一个改变会另一个,但是深拷贝需要频繁的开空间;写时拷贝是浅拷贝,但是有一个变量(count)记录该空间有多少对象指向,可以解决多次析构同一空间。
接下来将会从count类型为静态,malloc,和_str共用空间具体分析。
_count为static int
class String
{
public:
String( const char* str=" ")//构造函数
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
_count = 1;//构造一个即该空间有一个对象指向
}
String(const String& s)//写时拷贝
:_str(s._str)//浅拷贝
{
_count++;
}
~String()
{
if (--_count == 0)//表明是最后一个对象
{
delete[]_str;
printf("析构\n");
}
}
private:
char *_str;
static int _count;
};
int String::_count = 0;//静态变量在类外初始化
只会析构一次,由监视可以看到_count的变化,_count是公用的。
_count是malloc
class String
{
public:
String(const char* str = " ")//构造函数
:_str(new char[strlen(str) + 1])
, _pcount(new int(1))//申请空间并初始化为1
{
strcpy(_str, str);
}
String(const String& s)//写时拷贝
:_str(s._str)
,_pcount(s._pcount)
{
(*_pcount)++;
}
~String()
{
if (--(*_pcount) == 0)
{
delete[] _str;
delete _pcount;
printf("析构\n");
}
}
private:
char* _str;
int * _pcount;
};
析构两次
count和_str在同一空间
class String
{
public:
int& GetRefCount() //出了作用域还存在
{
return *((int*)(_str - 4));//找到计数
}
String(const char* str = "")//构造
:_str(new char[strlen(str)+5]) //+5:其中
{
_str += 4;
strcpy(_str,str);
GetRefCount() = 1;
}
String(const String& s)//写时拷贝
:_str(s._str)
{
GetRefCount()++;
}
String& operator = (String& s)//赋值
{
if (_str != s._str)
{
if (--GetRefCount() == 0)
{
delete[] (_str-4); //需要将前面计数空间释放
}
_str = s._str;
++GetRefCount();
}
return *this;
}
~String()
{
if (--GetRefCount() == 0)
{
delete[](_str - 4);
printf("析构\n");
}
}
private:
char* _str;
};
解决浅拷贝一个修改影响另一个
由于浅拷贝共用一个空间,一个改变会改变另一个,所以当要“写”时,如果该空间有多个对象指向则重开空间。
void CopyOnWrite()
{
if (GetRefCount() > 1)
//证明该段空间还有其他对象,不能随便写,就要为该对象重新开空间
{
String tmp(_str); //tmp出作用域会调析构函数
swap(_str, tmp._str);//而在交换后,tmp析构时就将写之前的计数器减1
/*--GetRefCount();
char *tmp = new char[strlen(_str) + 5];
strcpy(tmp + 4, _str);
_str = tmp + 4;
GetRefCount() = 1;*/
}
}
char& operator[](size_t pos)
{
CopyOnWrite();
return _str[pos];
}
对于写时拷贝也有字符串的增删查改,第一医院增删改需要添加CopyOnWrite();查找时不用。
在写时拷贝中,只有需要“写”时才开空间,“读”不开空间。
下一篇: 代码审计入门