本篇博客从深浅拷贝入手,详细谈论了string类的各项实现。在引用计数与写时拷贝的设计时提供了多种方案。将以导图顺序展开。
在开始所有编码之前,我们必须搞清楚什么是浅拷贝/深拷贝,先来看下面的代码。
再来看类中的情况 由于没有定义构造函数,下面演示浅拷贝
定义了拷贝构造函数,我们再来看效果
那么到底什么是深|浅拷贝呢?
类中的指针变量复制时会在堆内存中申请新的空间来存储就叫深拷贝。
不论什么类型的变量,只是进行简单的赋值叫浅拷贝。
可见两者的不同就在于处理指针的时候。
基于这个概念,我们来看深拷贝String的传统写法与现代写法,模拟实现的string均以String形式出现。
#pragma once
#include <iostream>
using namespace std;
class String
{
public:
String(const char * str)
{
size_t len = strlen(str);
_str = new char[len + 1];
memcpy(_str, str, len + 1);
}
拷贝构造 传统写法
//String(const String &s)
//{
// //String(s._str);//拷贝构造不可以 出作用域销毁 要交换
// size_t len = strlen(s._str);
// _str = new char[len + 1];
// memcpy(_str, s._str, len + 1);
//}
//String& operator=(const String & s)
//{
// if (this != &s)
// {
// int len = strlen(s._str);
// char *tmp = new char[len + 1];
// memcpy(tmp, s._str, len + 1);
// delete[] _str;
// _str = tmp;
// }
// return *this;
//}
//现代写法
String(const String &s)
:_str(NULL)
{
String tmp(s._str);
swap(tmp._str, _str);
}
String & operator=(String s)
{
swap(s._str, _str);
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
当然,也可模拟STL中string的各种接口进行增删查改,这时增加了两个变量用于字符串有效数据计数和内存容量计数。
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
class String
{
public:
String(const char * str)
:_size(strlen(str))
, _capacity(_size + 1)
, _str(new char[_capacity])
{
memcpy(_str, str, _capacity);
}
String(const String &s)
:_str(NULL) //delete[] 空指针会
{
String tmp(s._str);
Swap(tmp);
}
String& operator=(String s)
{
Swap(s);
return *this;
}
void Swap(String &s) //不加& 调拷贝构造调构造死循环
{
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
}
~String()
{
delete[] _str;
_str = NULL;
_size = _capacity = 0;
}
public:
void PushBack(char ch)
{
_CheckCapacity();
_str[_size++] = ch;
_str[_size] = '\0';
}
void Insert(size_t pos, char ch)
{
_CheckCapacity();
size_t end = _size++;
while (end >= pos) //想清楚
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = ch;
++_size;
}
void Insert(size_t pos, const char* str)
{
assert(pos <= _size); //可以包含\0的位置实现尾插
size_t len = strlen(str);
_CheckCapacity(len+1);
size_t end = _size;
while (end >= pos)
{
_str[end + len] = _str[end];
--end;
}
for (int i = 0; i < len; ++i)
{
_str[pos+i] = *(str+i);
}
// char* dst = _str+pos;
// while (*str)
// *dst++ = *str++;
_size += len;
}
void PopBack()
{
assert(_size >0);
_str[--_size] = '\0';
}
//删除pos位置这个数据
void Erase(size_t pos)
{
assert(_size > 0 && pos < _size);
for (size_t i = pos; i < _size; i++) //头脑清楚啊,兄弟
{
_str[i] = _str[i + 1];
}
--_size;
}
// 删除pos位置开始往后的n个数据 算pos不--算
void Erase(size_t pos, size_t n)
{
assert(_size>0 && pos < _size);
if (pos + n >= _size) //pos位置和后面的n个字符,+n是应该填到pos位置的字符若与size当下标时的值相等即直接赋\0
{
_str[pos] = '\0';
}
else
{
for (size_t i = pos; i < (_size-n+1); ++i)
{
_str[i] = _str[i + n];
}
}
}
bool operator>(const String& s)
{
char *s1 = _str;
char *s2 = s._str;
while (*s1 && *s2)
{
if (*s1 > *s2)
{
return true;
}
else if (*s1 < *s2)
{
return false;
}
else
{
s1++;
s2++;
}
}
if (*s2)//s2不为空 返回false
{
return false;
}
else if (*s1 == NULL && *s2 == NULL) //两个都不为空 相等
{
return false;
}
return true;
}
bool operator<(const String& s)
{
return !(*this > s || *this == s); //对象是*this;
}
bool operator==(const String& s)
{
char *s1 = _str;
char *s2 = s._str;
while (*s1++ == *s2++);
if (*s1=='\0' || *s2 == '\0') //判断假执行要显示判断
{
return false;
}
else
{
return true;
}
}
bool operator>=(const String& s)
{
return *this > s || *this == s;
}
bool operator<=(const String& s)
{
return *this < s || *this == s;
}
char& operator[](size_t pos) //引用可写
{
return _str[pos];
}
size_t Size()
{
return _size;
}
char* GetStr()
{
return _str;
}
String operator+(const String& s)
{
String tmp(*this);
tmp.Insert(tmp._size, s._str);
return tmp;
}
String operator+(const char* str)
{
String tmp(*this);
tmp.Insert(tmp._size, str);
return tmp;
}
String& operator+=(const String& s)
{
Insert(_size, s._str);
return *this;
}
private:
void _CheckCapacity(size_t needSize = 1)
{
if (_size + needSize >= _capacity) //都当个数来看 size表示的是有效字符个数, _capacity表示的是_size+1 >是以防远远超出
{//多数出错都在这里,要是扩容机制刚好等于需要的内存呢
_capacity *= 3+2;
_str = (char *)realloc(_str,_capacity);
}
}
private:
size_t _size;
size_t _capacity;
char* _str;
};
下面给出String引用计数实现的四种设计思路,前两种因无法实现功能pass。
1.使用普通成员变量计数
class String
{
public:
String(const char* str = "")
:count(1)
{
_str = new char[strlen(str + 1)];
}
String(String &s)
{
++s.count;
count = s.count;
_str = new char[strlen(s._str)];
}
String& operator=(String& s)
{
++s.count;
count = s.count;
String tmp(s._str);
swap(_str,tmp._str);
}
~String()
{
if (--count == 0)
{
delete[] _str;
_str = NULL;
}
}
private:
int count;
char* _str;
};
由于变量构造,析构的时候减了各自的引用计数,相互之间没有协同,达不到效果。
2.使用静态变量计数
class String
{
public:
String(const char* str)
{
_count = 1;
_str = new char[strlen(str)+1];
strcpy(_str, str);
}
String(const String& s)
{
++_count;
_str = new char[strlen(s._str)+1];
strcpy(_str, s._str);
}
String& operator=(const String &s)
{
String tmp(s);
swap(_str, tmp._str);
}
~String()
{
if (--_count == 1)
{
delete[] _str;
_str = NULL;
}
}
size_t getCount()
{
return _count;
}
protected:
static size_t _count;
char *_str;
};
size_t String::_count = 0;
静态成员为所有对象共享,不同对象之间会出现干扰。
3.利用指针变量将同类对象的计数指针指向同一内存,起到同步效果。
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+1])
{
_refCount = new int(1);
strcpy(_str, str);
}
String(String &s)
{
++(*s._refCount);
_refCount = s._refCount;
_str = s._str;
}
String& operator=(String s)
{
Realease();
_refCount = s._refCount;
++(*s._refCount);
_str = s._str;
}
char& operator[](size_t index)
{
if (*_refCount > 1)
{
String tmp(_str);
Swap(tmp);
}
return _str[index];
}
~String()
{
Realease();
}
void Realease()
{
if (--(*_refCount) == 0)
{
delete[] _str;
_str = NULL;
}
}
void Swap(String &s)
{
swap(s._refCount, _refCount);
swap(_str, s._str);
}
int getCount()
{
return *_refCount;
}
private:
int* _refCount;
char* _str;
};
方案可行。
4.模拟new[],在构造时,str头部多开四个字节存当前对象的个数,str一直指向内存+4的位置
class String
{
public:
String(const char * str)
{
size_t len = strlen(str);
_str = new char[len + 5];
*(int *)_str = 1;
_str += 4; //引用计数是不暴漏给外部的,返回的指针同new[]的机制一样
strcpy(_str, str);
}
//传统写法 String的现代写法?
String(const String &s) //拷贝构造
{
++(*(int *)(s._str - 4)); //新构造的对象被赋值,赋值对象加1
_str = s._str;
}
//现代写法
String& operator=(const String&s)
{
if (this != &s)
{
Release();
String tmp(s); //到这里用构造还是拷贝构造就不同了吧
swap(tmp._str, _str);
}
return *this;
}
~String()
{
Release();
}
void Release()
{
if (--(*(int*)(_str - 4)) == 0)
{
delete[] (_str - 4);
_str = NULL;
}
}
char& operator[](size_t pos) //加了const还能改变吗
{
CopyOnWrite();
return _str[pos];
}
String& operator+=(const String &str)
{
*this = *this + str;
return *this;
}
String operator+(const String &str) //是不是要用realloc //拼接两个字符串
{
String tmp(_str);
String next(str);
size_t len1 = strlen(tmp._str);
char* first = tmp._str + len1;
char* second = next._str;
while (*first++ = *second++);
return tmp; //返回值为该类型的对象
}
void CopyOnWrite()
{
if ((*(int*)(_str-4)) > 1)
{
String tmp(_str);
swap(tmp._str, _str);
}
}
private:
char* _str;
};
接下来探究windows平台和Linux平台下的测试string类库的实现机制 测试用例都很简单。
常见的面试题就是实现一个简单的string类,跑校招的时候就遇到了让写的。下面参考别人的博客给出相对合理的I写法。
#include <iostream>
using namespace std;
class String
{
public:
String(const char* str = "")//考虑到构造一个空字符串 包含'\0'
: data_(new char[strlen(str) + 1]) //为了修改字符串重新拷贝
{
strcpy(data_, str);
}
String(const String& rhs) //深拷贝
: data_(new char[rhs.size() + 1])
{
strcpy(data_, rhs.c_str());
}
~String()
{
delete[] data_;
}
String& operator=(String rhs)
{
swap(rhs);
return *this;
}
size_t size() const
{
return strlen(data_);
}
const char* c_str() const
{
return data_;
}
void swap(String& rhs)
{
std::swap(data_, rhs.data_);
}
private:
char* data_;
};
很简单有没有。