string介绍
C++ string:在C语言基础上,提供了一个 类的封装,内部通常包含:
一个指向动态内存的 char*(保存字符串内容);
一个记录当前字符串长度的 _size;
一个记录已分配空间大小的 _capacity(一般比 _size 大,用于减少频繁扩容)
string 会 自动管理内存:当字符串变长时,会重新分配更大的空间,并把原来的内容复制过去。
为了效率,string 通常采用 按需扩容:容量不足时,不是只增加 1 个,而是成倍增加(比如翻倍)。
std::string 提供了很多便利的功能:
构造:直接用字面量初始化(string s = “hello”;)。
访问:下标 [],迭代器,at()。
修改:append、insert、erase、replace。
比较:==、!=、< 等重载运算符。
拼接:支持 +、+=。
安全性:自动管理内存,避免手动 new/delete。
string用法及常用函数
初始化
int main()
{
string s1;
string s2("张三");
string s3("hello world");
string s4(10, '*');
string s5(s2);
string s6(s3, 6, 5);//区间初始化 6:pos 5:len
}
赋值
int main()
{
s1 = s2;
s1 = "1111";
s1 = '2';
}
增(各种插入)
push_back
string s1("hello");
//尾插一个字符
s1.push_back(' ');
append
//尾插一个字符串
s1.append("world");
//插入一个字符串前几个字符
s1.append("hello world",5); //插入字符串的前五个字符
//插入一个string区间
s1.append(s2,3,6);// pos: 3 len: 6
重载+=
s1 += ' ';
s1 += "world";
s1 += s1;
insert
s1.insert(0, "hello");//pos + 字符串(指定位置插入字符串)
s1.insert(0,"hello world",5);// pos + 字符串 + len(插入字符串区间)
s1.insert(0,s2);//pos + string
s1.insert(0, s2,2,5);//pos + string + string的区间
s1.insert(0,10,'x');// pos + n(个数) + char
s1.insert(s1.beign(),10,'x'); //迭代器
删
erase
int main()
{
string s1("hello world");
s1.erase(5, 1);
cout << s1 << endl;
s1.erase(5);
cout << s1 << endl;
string s2("hello world");
s2.erase(0,1);
cout << s2 << endl;
s2.erase(s2.begin());
cout << s2 << endl;
s2.erase(s2.begin(),s2.end());//全删
s2 = '0';
cout << s2 << endl;
}
实际效果:
迭代器
介绍:
迭代器(Iterator)就是 用来遍历容器元素的一种对象。
它相当于一个“指针”,但比普通指针更灵活、更安全,能屏蔽底层容器的具体实现方式。
比如 vector、list、map 这些 STL 容器,内部结构不一样,但都能用迭代器来统一遍历。
功能:
输入迭代器:只读,单向移动(常见于 istream_iterator)。
输出迭代器:只写,单向移动(常见于 ostream_iterator)。
前向迭代器:可读可写,单向遍历。
双向迭代器:可读可写,能前进和后退(如 list 的迭代器)。
随机访问迭代器:支持 + - [] < > 等操作(如 vector、deque 的迭代器)。
常见几种迭代器
//iterator
//const_iterator
//reverse_iterator(反向)
//const_reverse_iterator
打印
string::iterator it = s1.begin();
while (it != s1.end())
{
(*it)--;
++it;
}
it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
范围for
范围for底层就是迭代器,不过比较简单
打印:
for (auto ch : s1)//for(char ch:s1)
{
cout << ch << " ";
}
cout << endl;
修改:(需要引用)
for (auto& ch : s1)
{
ch++;
}
迭代器+算法容器
反转:
reverse(v.begin(), v.end());
reverse(lt.begin(), lt.end());
排序:
sort(s1.begin(),s1.end());
…………
reserve & resize
reserve 开空间
//开空间
s1.reserve(100);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
resize 开空间+初始值
//开空间+填值初始化
s1.resize(200,'x');
cout << s1.size() << endl;
cout << s1.capacity() << endl;//扩容
s1.resize(20);//可以删除数据,不缩容
cout << s1.size() << endl;
cout << s1.capacity() << endl;
上述为基本常用功能·,其他接口自行去文档查找。
模拟实现
前提准备
namespace bit
{
class string
{
private:
size_t _size;
size_t _capacity;
char* _str;
static size_t npos;
public:
………………
}
size_t string:: npos = -1;
}
构造函数
字符串构造
string(const char* str = "")//相当于\0
:_size(strlen(str)),_capacity(_size)
{
_str = new char[_capacity + 1];
memcpy(_str, str,_size+1);
// 如果 只拷贝_size 则 _str[_size] = '\0';
}
我们也可以使用for循环逐个拷贝进去
for (size_t i = 0; i < _size; i++)
{
_str[i] = s[i];
}
_str[_size] = '\0';
字符构造
string(const char* str, size_t n)
:_size(n),_capacity(_size)
{
_str = new char[_capacity + 1];
memcpy(_str, str, n);
_str[_size] = '\0';//注意添加
}
PS1:为什么一个是_size + 1 一个是 _size。
二者差异在memcpy的拷贝区间,因为s自带’\0’,所以拷贝到_size + 1.
但是字符没有‘\0’,所以要手动添加。
PS2: 为何不推荐使用strcpy?
1. 若字符串中包含’\0’字符(虽然这种情况较为少见),拷贝过程会提前终止,导致无法完整复制。
2. 为保持代码一致性及美观性,建议避免使用该函数。
拷贝构造函数
传统写法:
string(const string& str)
: _size(str._size), _capacity(str._capacity)
{
_str = new char[_capacity + 1];
memcpy(_str, str._str, _size + 1);
}
现代写法:
string(const string& str)
:_str(nullptr),_size(0),_capacity(0)
{
string tmp(str._str);
swap(tmp);
}
PS:
若 _str、_size 和 _capacity 未被初始化,可能导致以下问题:
访问非法内存地址(野指针)
_size 和 _capacity 值异常,引发数组越界
将对象初始化为空状态可确保:
swap 操作时数据始终有效
避免未初始化值带来的风险
重载=
传统写法:
string& operator=(const string& str)
{
if (this == &str) return *this;
delete[]_str;
_str = new char[str._capacity + 1];
memcpy(_str, str._str, str._size + 1);
_size = str._size;
_capacity = str._capacity;
return *this;
}
现代写法:
void swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);//不能直接交换,会无限循环
}
string& operator=(string str)//形参(传值)拷贝了一个str
{
if (this == &str) return *this;
swap(str);//this->swap(tmp);
return *this;
}
PS1: 为什么不能直接交换(this)和(str)?
由于std::swap在交换this和str时会涉及赋值操作,这会触发重载的=运算符,导致无限递归。
PS2: 为什么要传递形参str?
形参str是实参的副本,通过交换这个副本既能实现赋值目的,又不会影响原始实参的值。
析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
重载[]
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
字符串输出
const char* c_str()const { return _str;}
PS:
采用字符串形式输出,便于使用字符串处理函数。
直接支持打印功能,无需专门重载流输出和流插入操作符。
cout << s.c_str() << endl;
开空间
reserve
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
memcpy(tmp, _str, n+1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
PS:
调用reserve扩容时,需要先将原有数据拷贝到新空间,且仅当n大于当前_capacity时执行才有实际意义。
resize
void resize(size_t n,char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
PS:使用resize缩小空间时需手动添加’\0’进行截断。
增
push_back
void push_back(char c)
{
if (_size == _capacity)
{
//2倍扩容
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size++] = c;
_str[_size] = '\0';//插入字符需要手动补充'\0'
}
append
void append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
memcpy(_str + _size, s,len+1);
_size += len;
}
void append(const string& str) { append(str.c_str());}
PS:插入流程如下:首先检查空间是否合适,接着通过reserve调整空间大小,最后执行数据插入操作。
+=
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& operator+=(string& str)
{
append(str);
return *this;
}
PS:实现+=比较简单,主要是函数push_back 和 append 的复用。
删
erase
void erase(size_t pos, size_t len = npos)
{
assert(pos < size());
if (len == npos || len + pos >= _size)
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
PS:
检查位置:assert(pos < size()); 确保删除起点合法。
判断删除范围:
如果删到末尾(len == npos 或者超出范围),直接把 _size 截到 pos。
否则就把后面的内容往前搬。
更新 _size:调整字符串长度并补 ‘\0’ 结尾(end==_size 可以添加到’\0’)。
迭代器
begin
end
iterator begin(){ return _str;}
iterator end(){ return _str+size();}
const_iterator begin()const { return _str;}
const_iterator end()const{ return _str+size();}
PS:STL中的end()返回的是容器末尾元素的下一个位置,因此这里使用_str+_size表示。
PS:STL迭代器分为只读迭代器和可读写迭代器两种类型,因此需要区分这两类。
查
find
size_t find(const char* s, size_t pos = 0)const
{
const char* ptr = strstr(_str + pos, s);
return ptr ? (ptr - _str) : npos;
}
size_t find(string& str, size_t pos = 0)const
{
return find(str.c_str(), pos);
}
字符串匹配
size_t find(const char* s, size_t pos = 0)const
{
size_t len1 = _size;
size_t len2 = strlen(s);
if (len2 == 0) return pos;
if (len1 < pos || len1 < len2 + pos) return npos;
for (int i = pos; i < len1; i++)
{
size_t j = 0;
while (j < len2 && _str[i + j] == s[j])
j++;
if (j == len2) return i;
}
PS:
合法性检查:
assert(pos < _size); 确保起始位置不越界。
确定实际长度 n:
如果 len == npos 或者 pos + len > _size,就截到末尾。
否则用传入的 len。
创建新字符串 tmp:
预留空间 reserve(n),避免多次扩容。
逐个拷贝字符:
从 _str[pos] 开始,拷贝 n 个字符到 tmp。
返回结果:
返回新的子串 tmp。
字串提取
substr
string substr(size_t pos = 0, size_t len = npos) const
{
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len > _size)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = 0 ; i < n; i++)
{
tmp += _str[pos + i];
}
return tmp;
}
PS:
检查位置合法:assert(pos < _size);
确定实际截取长度:
如果 len == npos 或超范围,就取 _size - pos。
否则用传入的 len。
创建结果字符串:预留空间 reserve(n)。
逐个字符拷贝:把 _str[pos ~ pos+n-1] 复制进 tmp。
返回子串。
总结
下文是vector的讲解和模拟实现,如果有余力将完成string的其余实现和部分题目。