讨论字符串
- C语言中的字符串
- C++中的字符串
- `string`类的构造函数
- `string`类对象的容量操作
- `string`类对象元素访问及遍历
- 迭代器
- `string`类对象的修改操作
- `string`类的模拟实现
- 成员变量
- 构造函数
- 析构函数
- 扩容/预留空间
- `push_back`
- 遍历
- `operator[]`
- 迭代器
- `append`
- 任意位置插入数据和删除数据
- 查找元素
- 拷贝构造
语言是由一句一句字符串组成的,编程中,我们想要输出一句话也少不了字符串的加入。
C语言中的字符串
C语言中没有字符串数据类型,但是存在字符串,使用
""
括起来的即字符串。字符串是以\0
结尾的字符的集合。
// C语言中字符串的表现形式。
"abcd"; // 字符串常量
char str[] = "abcd";
char* strs = "abcd"; // str指向字符串首位置地址
C++中的字符串
C++标准库中的
string
类,就是字符串类。
- 根据文档介绍我们可以得知,字符串是表示字符序列的类。
string
在底层实际是basic_string
模板类的别名,typedef basic_string<char> string;
- 由于
string
类在C++标准库中,因此在使用时需要使用std::string
或者using namespace std;
。
string
类的构造函数
string
类的构造函数众多,可以通过字符串构造,可以通过一部分字符串构造,还可以使用迭代器区间构造。
#include <iostream>
using namespace std;
int main()
{
string s; // 调用默认构造函数
cout << s << endl;
string s1("hello world");
cout << s1 << endl;
string s2("hello world", 2, 5);
cout << s2 << endl;
const char* str = "welcome";
string s3(str);
cout << s3 << endl;
string s4(str, 2);
cout << s4 << endl;
string s5(10, 'x');
cout << s5 << endl;
return 0;
}
string
类对象的容量操作
函数名 | 功能 |
---|---|
size | 返回字符串长度。 |
length | 返回字符串长度,和size 作用一样。 |
capacity | 返回容器容量。 |
empty | 判断字符串是否为空。 |
clear | 清空字符串中数据。 |
reserve | 扩容/为字符串预留空间。 |
resize | 将有效字符个数设为n 个,多出的空间用字符c 填充。 |
max_size | 返回字符串最大长度。(基本没用) |
shrink_to_fit | 缩容。(轻易不会选择缩容) |
size()
底层和length()
完全相同,size()
只是为了和其他容器接口保持一致。
int main()
{
string s("hello world");
cout << s.size() << endl;
cout << s.length() << endl;
return 0;
}
vs编译器下,
string
对象的初始capacity
是15,以接近1.5
倍的方式扩容。
int main()
{
string s;
int capacity = s.capacity();
cout << s.capacity() << endl;
for (int i = 0; i < 1000; i++)
{
int newCap = s.capacity();
if (capacity != newCap)
{
cout << s.capacity() << endl;
capacity = newCap;
}
s.push_back(i);
}
return 0;
}
在g++13编译器下,初始容量为15,后面以2倍方式扩容。
clear()
清空内容,但不会影响容量。
int main()
{
string s("hello worldxxxxxxxxxxx");
cout << s.capacity() << endl;
s.clear();
cout << s.capacity() << endl;
return 0;
}
reserve(size_t n)
可以指定字符串对象的初始容量,若n
比初始容量小,则初始容量还是默认值。若比初始容量大,则字符串对象预先开出指定大小容量。
int main()
{
string s;
s.reserve(1);
cout << s.capacity() << endl;
s.reserve(100);
cout << s.capacity() << endl;
return 0;
}
resize(size_t n)
/resize(size_t n, char c)
,n
如果比现字符串长度小,则发生截断,若比现字符串长度长 ,则用字符c
填充。
int main()
{
string s1("hello world");
s1.resize(4);
cout << s1 << endl;
string s2("no");
s2.resize(10, '!');
cout << s2 << endl;
return 0;
}
string
类对象元素访问及遍历
函数名 | 功能 |
---|---|
operator[] | 返回pos 位置的字符。 |
迭代器begin 和end | begin 获取第一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器。 |
反向迭代器rbegin 和rend | 逆向遍历。 |
范围for | C++11支持的语法,底层基于迭代器。 |
运算符重载
operator[]
,也可以使用const
修饰的对象使用。
void print_string(const string &s) {
for (int i = 0; i < s.size(); ++i) {
cout << s[i] << " ";
}
cout << endl;
}
int main() {
string s("hello world");
for (int i = 0; i < s.size(); ++i) {
s[i] += 1;
cout << s[i] << " ";
}
cout << endl;
print_string(s);
return 0;
}
迭代器
根据手册,
begin
迭代器返回指向第一个字符的位置,end
迭代器返回指向末尾字符后面的位置。
int main() {
string s("hello world");
string::iterator it = s.begin();
while (it != s.end()) {
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
C++11实现范围
for
遍历方式,底层基于迭代器。只要实现迭代器,就可以使用范围for
。
string
类对象的修改操作
函数名 | 功能 |
---|---|
push_back | 在字符串末尾处插入字符。 |
append | 在字符串末尾处插入字符串。 |
operator+= | 在字符串末尾处插入字符串。 |
c_str | 返回C语言格式字符串。 |
find | 从pos 位置开始查找字符,返回该位置下标。 |
rfind | 反向查找。 |
substr | 从pos 位置开始,截取n个字符,返回截取后的字符串。 |
find()
的返回值,若没有找到字符,则返回string::npos
。string::npos
是string
类的静态成员变量,值是-1。
int main() {
string s("hello world");
size_t pos = s.find('n');
if (pos != string::npos) {
cout << "找到了" << endl;
} else {
cout << "没找到" << endl;
}
return 0;
}
string
类的模拟实现
成员变量
模拟实现的
string
类的成员变量,由三部分组成,分别是存放字符串的字符指针、字符串长度变量以及字符串容量变量。
char *_str;
size_t _size;
size_t _capacity;
构造函数
编写构造函数时,需要思考深浅拷贝问题,如果是浅拷贝,则在调用析构函数时会出现问题。因此,成员变量
_str
一定要申请自己的空间,不能直接拷贝传参的地址。
string(const char *str = "")
: _size(strlen(str))
, _capacity(_size) {
_str = new char[_capacity + 1]; // 进行深拷贝
for (size_t i = 0; i < _size; i++) {
_str[i] = str[i];
}
}
析构函数
_str
是通过动态开辟内存空间,因此在析构函数中需要释放空间。
~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
扩容/预留空间
传入数字
n
,如果n
比容量_capacity
大,则扩容。
void reserve(size_t n) {
if(n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str); // 拷贝原有字符
delete[] _str;
_str = tmp;
_capacity = n;
}
}
push_back
先需要判断容量是否满足条件,判断条件是
_size == _capacity
代表容量满了。
void push_back(char ch) {
// 先判断容量
if(_size == _capacity) {
int newCapacity = _capacity == 0 ? 5 : 2 * _capacity;
reserve(newCapacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0'; // 确保末尾位置是结束位
}
遍历
operator[]
只需要返回指定下标元素即可。
char &operator[](size_t pos) {
return _str[pos];
}
const char &operator[](size_t pos) const {
return _str[pos];
}
迭代器
此处迭代器采用指针方式,将
char*
作别名iterator
。
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;
}
- 只要迭代器实现,范围
for
就可以调用。
append
- 首先还是需要考虑容量问题,但这次不是简单的扩容2倍了,需要按照传入的字符串来判断扩容容量。
string& append(const char* str) {
int size = strlen(str);
reserve(size + _size);
strcpy(_str + _size, str);
return *this;
}
- 因此,
operator+=
可以直接复用。
string &operator+=(char ch) {
push_back(ch);
return *this;
}
string &operator+=(char *str) {
append(str);
return *this;
}
string &operator+=(const string &s) {
int size = s._size;
reserve(size + _size);
strcpy(_str + _size, s._str);
return *this;
}
任意位置插入数据和删除数据
- 插入数据还是需要优先判断扩容,和前面差别不大,只是需要考虑数据挪动。
void insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
int newCapacity = _capacity == 0 ? 5 : 2 * _capacity;
reserve(newCapacity);
}
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
_str[_size] = '\0';
}
void insert(size_t pos, const char *str) {
assert(pos <= _size);
int size = strlen(str);
reserve(_size + size);
size_t end = _size + size;
while (end > pos) {
_str[end] = _str[end - size];
--end;
}
strncpy(_str + pos, str, size);
_size += size;
_str[_size] = '\0';
}
void erase(size_t pos) {
assert(pos <= _size);
size_t begin = pos;
while(begin < _size - 1) {
_str[begin] = _str[begin + 1];
++begin;
}
_size--;
_str[_size] = '\0';
}
查找元素
创建
string
类的静态成员函数npos
,并设定值为-1。
static const size_t npos; // 声明
const size_t string::npos = -1;
size_t find(char ch, size_t pos = 0) const {
for (size_t i = pos; i < _size; ++i) {
if(_str[i] == ch) {
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0) const {
const char* ret = strstr(_str + pos, str);
if(ret) {
return ret - _str;
} else {
return npos;
}
}
拷贝构造
比较容易想到的方法就是将
char*
一个字符一个字符拷贝。
string(const string &s)
: _size(strlen(s._str)), _capacity(_size) {
_str = new char[_capacity + 1];
for (size_t i = 0; i < _size; i++) {
_str[i] = s._str[i];
}
}
也可以复用构造函数,构建一个临时的
string
对象,再交换this
和临时对象,达到拷贝构造的效果。
string(const string &s)
: _str(nullptr), _size(0), _capacity(_size) {
string tmp(s._str);
swap(tmp);
}