STL(标准模板库)中的 string
容器是C++中用于处理字符串的类,提供了丰富的功能来替代C风格的字符数组,具有更高的安全性和便捷性。以下是对 string
容器的核心介绍:
基本特性
-
头文件:需包含
<string>
。 -
本质:是
basic_string<char>
的别名,管理动态字符数组,自动处理内存(无需手动分配/释放)。 -
优势:支持动态扩容、丰富的成员函数、操作符重载,避免缓冲区溢出。
接下来是string的一些接口函数,我们挑几个函数进行实现,达到让我们自己写的string可以使用一些基本功能即可。
string类的常用接口说明
- string类对象的成员变量
private:
char* _str;
size_t _size;
size_t _capacity;
_str用来存放字符串,_size记录字符串的长度,_capacity记录为该字符串开辟的空间。
- string类对象的构造函数
//string()
// :_str(new char[1])
// ,_size(0)
// ,_capacity(0)
//{
// _str[_size] = '\0';
//}
//string(const char* s)
// :_size(strlen(s))
// ,_capacity(_size)
//{
// _str = new char[_capacity + 1];
// strcpy(_str, s);
//}
string(const char* str = "")
{
size_t len = strlen(str);
_capacity = len == 0 ? 3 : len;
_str = new char[_capacity + 1];
strcpy(_str, str);
_size = len;
}
我在第一次写string构造函数的时候,就是上面注释内容一样,构造了两个构造函数进行初始化,但是不断学习发现,可以通过缺省参数实现两个函数的归整,这样就简化了代码。还有一点就是,我们在new(堆)申请空间时,会预先开辟的空间(_capacity)多开辟一个空间,这样做的目的是为了存放'\0'。
深浅拷贝问题
可以看到非常完美,两个变量都申请到了自己的空间,现在,我们可以回忆一下类和对象的时候关于拷贝构造的问题了,记得当时一看编译器自动生成的默认构造貌似很完美,不需要我们人为操作就可以实现,现在我们尝试用s1来初始化s2,看看会有什么情况?
奇怪了,程序怎么突然崩溃了呢?明明两个结果都打印出来了,为什么最后还是崩溃了呢?其实,报错的地方在析构函数。
这就是经典的深浅拷贝问题了,涉及内存申请的时候就会有这个问题,就比如成员变量中的_str就涉及内存申请的问题。因此如果我们不手动写一个拷贝构造,系统默认的就是值拷贝,这就导致s1和s2两个内容中的成员变量都是同一个_str,当s1进行析构并成功释放空间并将_str改为nullptr时,当s2进行析构时_str为nullptr,这时候编译器是无法释放一个地址为nullptr的内存,所以就会导致崩溃!
举个例子就是,当你每天看到你舍友谈了一个很好的对象,善解人意,体贴入微,你就想也找这么好的对象,于是,你就按照这样的标准开始找对象,恰巧不巧的,你找到了你舍友对象,刚开始还相安无事,各做各的(相当于执行c_str函数),突然有一天,你舍友知道你也谈了一个,于是你们两准备把各自的女朋友带过来,这时候本来4个人的见面,只见了三个,这就有点尴尬了(类似析构的时候),这个时候你就和你的舍友开始拳脚问候了(崩溃了)。所以,值拷贝就是你们两个找到了相同的对象,因此,为了维护和谐的舍友关系,这时候我们就需要进行深拷贝,就是各找各的,找的对象都是类型差不多,厉害的话,即使找到了一对双胞胎,也是一人一个,没有冲突,这就是深浅拷贝问题。
所以这个时候我们就需要手动写一个拷贝构造,不然就会造成浅拷贝的问题。
string(const string& s)
:_size(s._size),_capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
这次就可以明显看到,_str指向了不同的空间,这样就不会在析构的时候出问题,这样我们就可以各自找到各自的对象了,不会因为找到同一个大打出手了。
- string类对象的容量操作
- size()和length()底层实现原理完全相同,引入size()是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- resize是将字符串的有效个数改为n个,如果元素的个数增多,会改变底层容量的大小,但如果将元素的个数减少,底层空间大小不变。
- reserve是为string预留空间,不改变有效元素的个数,当reserve的参数小于string的底层总大小时,reserve不会改变容量大小。
size_t capacity() const
{
return _capacity;
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
void reserve(size_t n = 0)
{
if (n > capacity())
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char c = '\0')
{
if (n < size())
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_size < n)
{
_str[_size] = c;
_size++;
}
_str[_size] = '\0';
}
}
注意,加上是std::这个域作用限定符就是库中的string。
看到这里就有人奇怪了,你说的没错,确实resize和reserve当小于容量大小时,不会改变_capacity,但是为什么这里这两个值不同呢?这其实是因为扩容机制有所不同导致的,现在看看g++和VS下的扩容机制是什么样的?分别在vs和gcc下跑下面这段代码
g++和vs下的扩容机制是什么样的?
int main()
{
std::string s;
size_t old = s.capacity();
cout << "初始化:" << old << endl;
for (int i = 0; i < 100; i++)
{
s.push_back('x');
if (old != s.capacity())
{
old = s.capacity();
cout << "扩容:" << old << endl;
}
}
return 0;
}
VS2022:
LINUX:
可以看到VS下string的扩容机制将近是1.5倍扩容,而g++下string的扩容机制是2倍扩容。
一般情况下,如果我们知道要求的字符个数有多少,可以提前开好空间,这样就可以减少扩容的代价,提高代码执行时间。
- string类对象的访问及遍历操作
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
范围for的底层就是迭代器,所以实现了迭代器,范围for也就可以使用了。
begin就是_str的首元素地址,end则是'\0'的位置,因为iterator是char*,所以指针++,就是按照char的字节数进行++,所以这样迭代器就跑起来了。
这里分别实现了一个const迭代器和普通迭代器,功能就是能不能修改的问题。
当修改const迭代器,就会发生如下情况:
- string类对象的修改操作
void push_back(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
_size += 1;
_str[_size] = '\0';
}
void append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& insert(size_t pos, char c)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[end] = c;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
if (len == npos ||pos + len > _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
const char* c_str() const
{
return _str;
}
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (int i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
while (pos < _size)
{
if (strncmp(_str + pos, s,strlen(s)) == 0)
{
return pos;
}
pos++;
}
return npos;
}
在实现删除时需要一个引入静态的成员变量。
因此,在类的成员变量处增加了一个静态的成员变量
这时候就有点奇怪了,为什么npos不在构造函数处初始化呢,为什么你要在类外定义呢?这就要为我们看看这个在类内特殊的成员变量了。
静态成员变量
在C++中,静态成员变量是类的特殊成员:
1. 基本概念
-
类级别共享:静态成员变量属于类本身,而非类的对象。所有对象实例共享同一份静态成员变量,生命周期与程序一致。
-
唯一存储:无论创建多少对象,静态成员变量仅存在一个副本。
2. 声明与定义
-
类内声明:使用
static
关键字在类内声明。class string { private: static size_t npos; // 类内声明 };
-
类外定义:必须在类外单独定义(分配存储空间),且无需重复
static
关键字。size_t string::npos = -1; // 类外定义并初始化
string模拟实现的完整代码
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;
namespace buluo
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
bool operator!=(const string& s)
{
return strcmp(_str, s._str) != 0;
}
//string()
// :_str(new char[1])
// ,_size(0)
// ,_capacity(0)
//{
// _str[_size] = '\0';
//}
//string(const char* s)
// :_size(strlen(s))
// ,_capacity(_size)
//{
// _str = new char[_capacity + 1];
// strcpy(_str, s);
//}
string(const char* str = "")
{
size_t len = strlen(str);
_capacity = len == 0 ? 3 : len;
_str = new char[_capacity + 1];
strcpy(_str, str);
_size = len;
}
string(const string& s)
:_size(s._size),_capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
string& operator=(const string& s)
{
char* tmp = new char[s.capacity() + 1];
if (tmp == nullptr)
{
assert(tmp);
}
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s.size();
_capacity = s.capacity();
return *this;
}
size_t capacity() const
{
return _capacity;
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
void reserve(size_t n = 0)
{
if (n > capacity())
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char c = '\0')
{
if (n < size())
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_size < n)
{
_str[_size] = c;
_size++;
}
_str[_size] = '\0';
}
}
void push_back(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
_size += 1;
_str[_size] = '\0';
}
void append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& insert(size_t pos, char c)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[end] = c;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
if (len == npos ||pos + len > _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
const char* c_str() const
{
return _str;
}
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (int i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
while (pos < _size)
{
if (strncmp(_str + pos, s,strlen(s)) == 0)
{
return pos;
}
pos++;
}
return npos;
}
~string()
{
delete[] _str;
_capacity = _size = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
};
size_t string::npos = -1;
}