C++STL:string类的模拟实现
Posted 山舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++STL:string类的模拟实现相关的知识,希望对你有一定的参考价值。
文章目录
前言
在【C++】STL(一)string类的使用一文中已经对string类进行了简单的介绍,一般来说只要会正常使用即可,下面来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
一、四个默认成员函数
1.构造函数
构造函数的实现思路就是给_str新开一片空间,并把参数中的字符串拷贝到新开的空间内。
代码如下:
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])//开空间,空间的大小+1是为了给\\0留位置
{
strcpy(_str, str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
注意参数的默认值不能给nullptr,因为若不传参使用默认值时,strlen(str)会对空指针进行解引用操作从而报错,这里最好用"",也即一个仅有\\0的空字符串。
另一种写法如下
代码如下:
class String
{
public:
String(const String& s)
:_str(nullptr)
{
//用s的_str产生一个临时的String对象
String tmp(s._str);
//交换tmp的_str和*this的_str即可达到目的
swap(_str, tmp._str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
注意这是构造函数,所以新产生的对象在初始化前_str内存储着随机值,交换后tmp(临时对象,生命周期仅在这个函数体内)出作用域时调用析构函数会释放一段随机空间,这是非法访问,所以在初始化列表内给_str以nullptr,这样在析构函数调用时对nullptr进行delete[]没有问题。
2.析构函数
析构函数比较简单,只需释放空间并将指针置空即可。
代码如下:
class String
{
public:
~String()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
3.拷贝构造
这里会设计到深浅拷贝的问题。
1.浅拷贝
先看一下若不写拷贝构造函数,使用编译器默认生成的是否能满足要求。
代码如下:
int main()
{
String s1("hello world");
String s2(s1);
return 0;
}
运行后程序会崩溃,那么原因是什么呢?
运行如下:
调试会发现,s1、s2中各自的_str指向同一片空间,也就是说在析构函数时会对同一块空间delete[]两次,这显然是不合法的,根本原因在于编译器给出的默认构造函数只进行值拷贝,即把s1中_str的值拷贝给s2中的_str,这样一来两个String对象中的_str指向同一块空间,不仅修改时会互相影响,在调用析构函数时程序更是会崩溃。
调试结果如下:
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一个对象的资源已经被释放,当继续对资源进项操作时,就会发生发生了访问违规。
2.深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般都是按照深拷贝方式提供。
代码如下:
class String
{
public:
//注意不要与构造函数混淆
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
这样一来,两个String对象的_str指向的空间便成了独立的两块,互不影响。
调试结果如下:
另一种写法如下:
代码如下:
class String
{
public:
// 注意这里不传引用
String& operator=(String s)
{
swap(_str, s._str);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
形参列表里的s是一个临时产生的String对象,在该函数结束后会调用析构函数,将s与*this的_str交换后,this内的_str便和希望得到的内容一样,而原来this的_str内的内容随着对象s析构函数的调用消失。
4.赋值运算符重载
若不写而使用编译器默认生成的赋值运算符重载,则同样会发生上面浅拷贝的问题,所以这里仍要考虑深拷贝。
代码如下:
class String
{
public:
String& operator=(const String& s)
{
if (this != &s)//防止自己给自己赋值
{
delete[] _str;//释放原来的空间
_str = new char[strlen(s._str) + 1];//开辟新空间
strcpy(_str, s._str);//拷贝字符串
}
return *this;
}
private:
char* _str;
};
二、string类的其它函数
上面的四个默认成员函数主要是操作_str,基本不涉及_size和_capacity,下面的函数则需要用到,所以在private的成员变量中需要添加。
则上面的四个函数也需要稍微改动。
代码如下:
class String
{
public:
String(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
String tmp(s._str);
//交换三个成员变量
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
String& operator=(String s)
{
//交换三个成员变量
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
return *this;
}
~String()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;//当前字符的个数
size_t _capacity;//最多能存储的字符个数
};
1.一些简单的函数
(1)size、capacity、c_str和clear
这几个函数非常简单,这里不作过多解释。
代码如下:
class String
{
public:
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
const char* c_str() const
{
return _str;
}
//删除_str内的所有内容
void clear()
{
_size = 0;
_str[_size] = '\\0';
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)比较函数
下面这些函数时比较两个string类的(实质是用strcmp比较两个对象的_str),代码较短且常用,所以可以设计为内联函数。
代码如下:
inline bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
inline bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
inline bool operator<=(const string& s1, const string& s2)
{
return (s1 < s2) || (s1 == s2);
}
inline bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
inline bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
inline bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
2.三种遍历方式
(1)下标+[]
要用到[]则需要进行重载,这样便可通过下标访问字符串。
代码如下:
class String
{
public:
//返回引用保证可读可写
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)迭代器
string类的迭代器就是一个char*指针(但其它容器不一定是),所以实现起来比较简单。
代码如下:
class String
{
public:
//typedef为char*改名
typedef char* Iterator;
//返回字符串第一个位置
Iterator begin()
{
return _str;
}
//返回字符串最后一个字符的下一个位置
Iterator end()
{
return _str + _size;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(3)范围for
对于范围for的遍历方式,只要支持迭代器就可以使用,因为范围for的遍历方式最终会被编译器转换为迭代器。同时由于这个原因,模拟实现的迭代器命名必须与标准一致,begin、end名称不可修改,随意更换begin、end的大小写或单词拼写都将时范围for无法运行。
3.reserve和resize
(1)reserve
reserve会改变字符串能存储的最大有效字符个数。
注意拷贝字符串时要用strncpy把_size个字符全部拷贝,不能用strcpy,因为strcpy遇到’\\0’拷贝即结束,但string类字符串的结束是_size限定的。
代码如下:
class String
{
public:
//开空间,扩容_capacity
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//注意给'\\0'留位置
strncpy(tmp, _str, _size + 1);//不能用strcpy
delete[] _str;
_str = tmp;
_capacity = n;
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)resize
resize会改变字符串存储的有效字符个数,同时可能会改变_capacity。若有多余的空间会将其初始化为指定的内容。
代码如下:
class String
{
public:
//开空间+初始化。改变_size,可能改变_capacity且初始化
void resize(size_t n, char val = '\\0')//默认值给'\\0'
{
//n < _size时直接修改_size并截止字符串
if (n < _size)
{
_size = n;
_str[_size] = '\\0';
}
else
{
if (n > _capacity)
reserve(n);
//将多余的位置用val填充
for (size_t i = _size; i < n; i++)
_str[i] = val;
_size = n;//修改_size
_str[_size] = '\\0';//结束字符串
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
4.插入数据
(1)push_back插入字符
代码如下:
class String
{
public:
void push_back(const char ch)
{
//空间不够则扩容
if (_size == _capacity)
{
//若原来空间为0则设为4,否则将容量翻倍
size_t newcapacity = (_capacity == 0) ? 4 : (2 * _capacity);
reserve(newcapacity);
}
//直接添加到_str后即可
_str[_size] = ch;
_size++;
_str[_size] = '\\0';
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)append添加字符串
代码如下:
class String
{
public:
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len >= _capacity)//空间不够则扩容
reserve(_size + len);
//将str拷贝到_str之后
strcpy(_str + _size, str);
_size += len;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(3)operator+=
这里注意代码复用。
代码如下:
class String
{
public:
String& operator+=(const char ch)
{
push_back(ch);
return *this;
}
String& operator+=(const char* str)
{
append(str);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(4)insert在任一位置插入字符或字符串
代码如下:
class String
{
public:
String& insert(size_t pos, const char ch)
{
assert(pos <= _size);
if (_size == _capacity)//空间不够则扩容
{
size_t newcapacity = (_capacity == 0) ? 4 : (2 * _capacity);
reserve(newcapacity);
}
size_t i = 0;
//腾出一个位置来填充字符
for (i = _size + 1; i > pos; i--)
_str[i] = _str[i - 1];
_str[pos] = ch;
_size++;
return *this;
}
//在pos之前插入字符串
String& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len + _size > _capacity)//空间不够则扩容
reserve(len + _size);
size_t i = 0, j = 0;
//先挪动数据,腾出len个空间
for (i = len + _size; i >= pos + len; i--)
_str[i] = _str[i - len];
//将str的内容补充到腾出的len个空间
for (i = pos, j = 0; j < len; j++)
_str[i++] = str[j];
_size += len;
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
5.erase
代码如下:
class String
{
public:
String& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
size_t leftLen = _size - pos;//从pos位置到字符串结尾剩余的长度
//若要求删除的长度比leftLen长,则全部删除
if (len >= leftLen)
{
_str[pos] = '\\0';
_size = pos;
}
else
{
//从pos开始删除len个字符
for (size_t i = pos; i < pos + len; i++)
_str[i] = _str[i + len];
_size -= len;
}
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;//添加一个静态成员变量
};
//设置为无符号整型的最大值
const size_t String::npos = -1;
6.find
代码如下:
class String
{
public:
//从pos开始查找字符ch,找不到返回npos,找到返回下标
size_t findC++STL详解—— string类的模拟实现
[C/C++]详解STL容器1--string的功能和模拟实现(深浅拷贝问题)
C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现
C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现