本篇文章讲解的内容主要是深浅拷贝和string类的实现.
概念:
浅拷贝只将对象的值拷贝过来,存在一定的隐患.
深拷贝给每个对象单独分配资源,一般对象涉及资源管理都会用深拷贝.
什么意思呢?
我们假设自己定义一个类,如下:
class String
{
public:
String(const char* s = "123456")
:_size(strlen(_s)),_capacity(_size)
{
_s = new char[strlen(s)+1];
_s[_size] = '\0';
strcpy(_s,s);
}
~String()
{
delete[] _s;
_size = _capacity = 0;
}
private:
char* _s;
int _size;
int _capacity;
};
如果我们使用该类创建对象,然后再释放资源,看看会怎样?
int main()
{
String s1;
String s2 = s1;
return 0;
}
编译器会提示异常,触发断点,而这个原因就是因为浅拷贝.我们一开始创建了对象s1并给其赋值"123456",然后创建了对象s2并对其拷贝构造,在这个过程中,我们把s1的各个成员的值全对应的给了s2的成员.也就是说s1的_s
和s2的_s
指向的是同一块空间,当main函数结束时,s2先调用析构函数,对其_s
进行释放,然后s1又调用析构函数,在对其_s
进行释放,而同一块空间是不能释放两次的,因此产生错误.
那我们怎么解决呢? 答:深拷贝,也就是每个对象都有单独的属于自己的一份空间.
因此该类的拷贝构造函数应该如下:
String(const String& t)
:_size(strlen(_s)),_capacity(_size); //确定一下大小
{
_s = new char[strlen(t._s)+1]; //然后给_s开辟一块空间,注意哦~,我这里多开了一块空间,是为了储存\0
strcpy(_s,t._s); //然后把值拷贝过去.
_s[_size] = '\0';
}
该类的赋值运算符=
重载如下:
String& operator=(String& t)
{
if(this != &t){ //如果是同一个对象,自己赋值给自己就没有什么意义
char* tmp = new char[strlen(t._s)+1]; //先开辟空间,然后用一个临时变量接收.
delete[] _s; //直接删除this原来的资源,然后重建一个和t._s一样大的空间
_s = tmp; //_s再接收
strcpy(_s,t._s);
_size = _capacity = strlen(t._s);
_s[_size] = '\0';
}
return *this;
}
上面的两个例子都是为了解决浅拷贝问题而进行设计的,其写法是没有问题的(传统写法),但是我们更喜欢一种现代写法,
String(const String& t):_s(nullptr) //必须有这一置空操作,因为_s开始是个随机数,交换给tmp._s后,被释放会引起大问题
{
String tmp(t._s); //直接利用构造函数,给tmp对象开辟了一块空间,并把值穿进去.
swap(_s,tmp._s); //然后交换_s和tmp._s,就相当于给this->_s开辟了一块空间,当拷贝函数结束,tmp就会被自动释放.
}
String& operator=(String t) //注意一下哦~~~,我的形参写的是String t,本质上是利用的拷贝构造
{
swap(_s,t._s); //交换资源
return *this;
}
咦~?,为啥线代写法的operator=
操作不需要把_s
初始化为nullptr
?因为这是赋值操作,即对象都互相存在,也就是说_s
本身是有资源的,交换后并不会造成对象t
释放时候,其内部会释放一个随机的地址.
讲解完深浅拷贝的原理和应用以后,我们就开始string类的实现吧.
我们在上一节讲解了string的使用,其中涉及到长度,容量操作时候,博主说明了string的底层实现其实是顺序表.那么其结构大致如下:
namespace mystring //因为string系统已经有了,为了防止重名,博主便重新定义了一块命名空间
{
class string
{
public:
private:
char* _str;
int _size;
int _capacity;
};
}
在讲解string的使用时候,博主为大家演示过几个比较常用的构造函数,我们在这里实现的话,也就实现几个比较常用的吧:
string (const char* s);
/ /接收c格式的字符串
string (size_t n, char c);
/ /接收n个字符c
string()
/ /空字符串.
接收c格式的字符串,代码如下:
string(const char* s)
:_size(strlen(s)) //给_size有效长度
{
_str = new char[_size+1]; //给_str一个strlen(s)+1长度的空间
strcpy(_str,s); //拷贝字符串
_capacity = _size; //更新容量
_str[_size] = '\0';
}
接收n个字符c,代码如下
string(size_t n,char c)
:_size(n) //更新有效长度
{
_str = new char[_size+1]; //给_str一个n+1长度的空间
for(int i= 0;i<n;i++) _str[i] = c;
_size = _capacity = n; //更新capacity
}
空字符串,代码如下:
string()
:_str(new char[1])
{
*_str = '\0';
_size = _capacity = 1;
}
博主在最上面讲解深浅拷贝时候,已经说明了该函数,因此,博主这里便直接贴代码了
string(const string& t):_str(nullptr) //必须有这一置空操作,因为_str开始是个随机数,交换给tmp._str后,被释放会引起大问题
{
string tmp(t._str); //直接利用构造函数,给tmp对象开辟了一块空间,并把值穿进去.
swap(_str,tmp._str); //然后交换_s和tmp._s,就相当于给this->_s开辟了一块空间,当拷贝函数结束,tmp就会被自动释放.
swap(_size,tmp._size);
swap(_capacity,tmp._capacity);
}
我们已经知道,拷贝构造需要深拷贝,而赋值也需要深拷贝,但是现代写法需要交换,因此博主便把交换操作封装一下,然后拷贝构造代码如下:
void Swap(string& a,string& b)
{
swap(a._str,b._str);
swap(a._size,b._size);
swap(a._capacity,b._capacity);
}
string(const string& t):_str(nullptr) //必须有这一置空操作,因为_str开始是个随机数,交换给tmp._str后,被释放会引起大问题
{
string tmp(t._str); //直接利用构造函数,给tmp对象开辟了一块空间,并把值传进去.
Swap(*this,tmp);
}
直接释放资源即可
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
深浅拷贝章节,博主已经讲解了,便直接贴代码:
string& operator=(string t)
{
Swap(*this,t); //注意哦,博主调用的是Swap,而不是swap.
return *this;
}
在c++中,string的operator[]有两个重载,分别是:
char& operator[] (size_t pos);
//支持读和写const char& operator[] (size_t pos) const;
//只支持读所以支持读和写的重载如下:
char& operator[] (size_t pos){
return _str[pos];
}
只支持读如下:
const char& operator[] (size_t pos) const{
return _str[pos];
}
reserve
吗?当给他传入一个n,且n大于该对象的capacity时候,才会增容.void reserve(size_t n)
{
if(n > _capacity){
char* tmp = new char[n+1]; //注意哦~,这里必须要多开一个空间存储\0
strcpy(tmp,_str);
delete[] _str;
_str = tmp;
_capacity = n;
_str[n] = '\0'; //多开的那个空间存储位置
}
}
\0
,最后size等于n,void resize(size_t n, char ch = '\0')
{
if (n > _capacity) reserve(n); //大于容量时,需要增容
for (size_t i = _size; i < n; i++) _str[i] = ch; //只有当n大于size时才起作用
_size = n; //最后size等于n.
_str[_size] = '\0'; //给字符串加一个\0
}
有人可能会问了,不说说分三种情况吗? 其实这里博主做了一个精简的处理,因为无论什么情况,都会有size等于n的操作,所以博主把这一步直接放到了外面.
对于字符和字符串拼接,用的做多有以下几个:
+=
push_back
append
void push_back(char c);
//拼接字符对于拼接字符操作来说,本质是上给数组中第size位置赋值,那么一定会遇到容量是否充足问题,所以是需要考虑此方面问题的:
void push_back(char c)
{
if(_size >= _capacity){
int newcapacity = _capacity == 0? 4 : _capacity*2; //这一步是为了防止_capacity如果为0,开不了空间
reserve(newcapacity);
}
_str[_size] = c;
_size++;
_str[_size]= '\0'; //给字符串末尾加\0
}
而拼接字符串则有比较多的重载,博主这里直接写一个比较常用的(拼接字符串或者拼接string对象)
void append(const char* s)
void append(const string s)
代码如下:
void append(const char* s){
int len = strlen(s);
if(_size + len > _capacity )
{
reserve((_size + len)*2); //开大点
}
strcpy(_str+_size,s);
_size += len;
_str[_size] = '\0';
}
//同理,另一个重载只需要把char* 改成string就行
有了上面写好的push_back
和append
函数,那么我们便可以直接复用他们进行实现operator+=
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
这里的大小有两个接口,分别是size和capacity.
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
我们在上一章节string的使用时候,就讲解了迭代器目前阶段是可以看作为指针的,而实际上在内部实现string时候,也差不多就是指针.
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() {return _str;}
iterator end() {return _str+_size;}
iterator begin() const {return _str;}
iterator end() const {return _str+_size;}
现在我们再看看,之前是怎么使用迭代器的:
mystring::string s("123456");
mystring::string::iterator itb = s.begin();
mystring::string::iterator ite = s.end();
while(itb!=ite)
{
cout<<*itb<<" ";
itb++;
}
对照一下迭代器的使用方式和实现方式,是不是现在能够理解了呢?
void insert(size_t pos, const char c)
{
if (_size == _capacity) { //检查容量
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
for (int i = _size; i > pos; i--) _str[i] = _str[i - 1]; //把pos位置后的数据集体移动一位
_str[pos] = c; //插入数据
_size++;
_str[_size] = '\0'; //给末尾加上\0
}
void insert(size_t pos, const char* s)
{
size_t len = strlen(s);
if (len + _size > _capacity) {
reserve(len + _size);
}
for (int i = _size + len - 1; i > pos + len - 1; i--) { //把pos到pos+len-1位置的数据集体后移strlen(s)位
_str[i] = _str[i - len];
}
for (int i = pos + len - 1, j = len - 1; j >= 0; j--, i--) { //放入数据
_str[i] = s[j];
}
_size += len;
_str[_size] = '\0';
}
pos
位置开始删除,len
个,如果len
不写,末尾删除pos
后的全部void erase(size_t pos, size_t len = npos) //npos的定义为 size_t npos = -1;
{
if (len == pos || pos + len >= _size) {
_size = pos, _str[pos] = '\0';
return;
}
for (int i = pos; i < _size; i++) {
_str[i] = _str[i + len];
}
_size -= len;
_str[_size] = '\0';
}
void clear()
{
_str[0] = '\0';
_size = 0;
}