string是一个类,准确的说string是一个类模板的实例化
类模板是basic_string,所以string是用char作为模板参数实例化的一个类对象
除了实例string,还有其他三个实例
wstring:针对宽字符的类(国标)
u16string:针对字符大小为2字节的类(utf-16)
u32string:针对字符大小为4字节的类(utf-32)
总之根据编码的标准不同,实例化的类也随之不同,string类的字符大小是1字节,是针对ASCII码的一个类。
上面是string的所用构造函数,下面列出几个常用的
void stringTest1()
{
string str1;// 构造一个空的string对象
string str2("hello world"); // 用const char *s构造一个string对象
cout << str2 << endl;
string str3(str2); // 用str2拷贝构造str3
cout << str3 << endl;
string str4 = str3; // 本质上不是赋值,而是拷贝构造
cout << str4 << endl;
// 不太常用
string str5("hello world", 3); // 用字符串的第3个到结束的字符构造
cout << str5 << endl;
string str6(10, 'x'); // 用十个x字符构造
cout << str6 << endl;
string str7(str2, 3); // 用str2的第3个到结束的字符构造
cout << str7 << endl;
string str8(str2, 3, 7); // 用str2的第3个到第7个的字符构造
cout << str8 << endl;
}
这里解释一下npos是什么
npos是一个无符号整型,但用-1初始化无符号整形得到的值是整形中的最大值(-1的补码是全1,用无符号的方式看待全1的二进制序列,这时-1就是整形中最大的数)。它意味着直到字符串结束,所以使用第3个构造函数,但不传第3个参数,默认会构造从pos位置到字符串结束的string对象。
库中对=进行了重载,总共有三种形式
void stringTest2()
{
string str1 = "hello world";
string str2; // 构造一个空的string对象
str2 = str1; // 将str1赋值给str2
cout << str1 << endl;
cout << str2 << endl;
str2 = "hello c++"; // 用const char*类型的字符串赋值给str2
cout << str2 << endl;
str2 = '!'; // 用字符赋值给str2
cout << str2 << endl;
}
void stringTest3()
{
// 遍历string的方式
string str = "hello";
// 第一种方式用下标遍历
for (size_t i = 0; i < str.size(); i++)
{
cout << str[i];
}
cout << endl;
// 第二种方式用迭代器
string::iterator it = str.begin();
while (it != str.end())
{
cout << *it;
it++;
}
cout << endl;
// 第三种方式范围for
for (auto& e : str)
{
cout << e;
}
cout << endl;
}
解释一下string的迭代器的两个接口,begin()返回的是string的第一个字符的位置的地址,end()返回的是最后一个字符位置的下一个位置的地址
所以当迭代器走到end()指向的地址时不能继续访问,否则会出现非法访问,这也是循环的结束条件
可以通过下标也能通过at接口遍历string
void stringTest4()
{
string str1 = "hello";
string str2 = "hello";
str1[7];
str2.at(7);
}
通过下标访问如果越界,程序会有越界提示
用at接口程序报的错就让人拿不准了
虽然用下标和at接口都能访问string的数据但使用下标访问能在出错时更快找到错误,所以推荐使用下标访问。
在使用反向迭代器时,需要输入较长的类型名,string::reverse_iterator,而我们可以使用auto来让编译器根据函数返回类型自动推导类型名,这样也提高了编程效率。
void stringTest5()
{
string str = "hello";
//string::reverse_iterator it = str.rbegin();
auto it = str.rbegin(); // 与上面的写法等价
while (it != str.rend())
{
cout << *it;
it++;
}
}
插入函数有很多重载版本
void stringTest6()
{
string str = "hello";
str.insert(0, 3, 'x'); // 向下标为0处插入3个x
cout << str << endl;
str.insert(0, " "); // 向下标为0处插入3个空格
cout << str << endl;
str.insert(str.begin() + 3, 3, 'y'); // 向下标为3处插入三个y
cout << str << endl;
str.insert(3, 3, 'z'); // 向下标为3处插入三个z
cout << str << endl;
}
void stringTest7()
{
string str = "hello";
str.erase(); // 不传参默认删除所有数据
cout << str << endl;
str = "hello";
str.erase(str.begin() + 2); // 删除下标为2处的字符
cout << str << endl;
str = "hello world";
cout << str << endl;
str.erase(2, 5); // 从下标为2向后删除5个字符
cout << str << endl;
str = "hello world";
cout << str << endl;
str.erase(str.begin() + 2, str.begin() + 5); // 删除从下标为2下标为5的字串
cout << str << endl;
}
void stringTest8()
{
string str1 = "hello";
string str2 = "world";
cout << str1 << endl;
cout << str2 << endl;
str1.swap(str2);
//swap(str1, str2); // 两种交换效率不同
cout << str1 << endl;
cout << str2 << endl;
}
使用string的提供的交换接口与标准库中自带的交换函数两者有区别吗?使用string提供的交换只是交换两个指向字符串的指针与两个空间大小,而标准库中的交换则是创建一个中间变量,需要调用拷贝构造,但拷贝是深拷贝需要开辟空间,这样一比较显然string提供的交换效率更高。
函数参数基本就是“要查找的字符/字符串”和“要开始查找的位置”,而开始查找的位置默认为0,就是从头开始查找。下面的代码是查找函数的使用
void stringTest9()
{
string file = "test.cpp.txt";
size_t pos = file.rfind('.');
if (pos != string::npos)
{
cout << file << "后缀:" << file.substr(pos) << endl;
}
string url = "https://cplusplus.com/reference/string/string/?kw=string";
size_t pos1 = url.find("://");
// 输出协议
if (pos1 != string::npos)
cout << url.substr(0, pos1 + 3) << endl;
else
cout << "非法url" << endl;
size_t pos2 = url.find('/', pos1 + 3);
// 输出域名
if (pos2 != string::npos)
cout << url.substr(pos1 + 3, pos2 - (pos1 + 3)) << endl;
else
cout << "非法url" << endl;
// 输出资源
cout << url.substr(pos2 + 1) << endl;
}
pushback向string中尾插一个字符。string像一个动态顺序表,有capacity和size保存顺序表的容量和当前的大小,由于string本身存储了一个\0,所以即使是空string也会向内存申请空间以保存\0。
在vs下string的初始容量是15,写一段代码验证capacity的增长
void stringTest10()
{
string str;
unsigned int size = str.capacity(); //先记录初始的容量
cout << "capacity:" << str.capacity() << endl;
for (size_t i = 0; i < 1000; i++)
{
str.push_back('c');
if (size != str.capacity()) // 当扩容时打印扩容后的容量
{
size = str.capacity();
cout << "capacity:" << str.capacity() << endl;
}
}
}
(在vs下)可以看到除了第一次的扩容其他扩容基本都是1.5倍扩,string有一个接口能改变string的容量,当直到string要存储字符串的长度时可以先改变它的容量,以节省扩容开辟空间的时间。
reserve有保留的意思,与reverse要注意区别
将string的初始容量改为1000后,与刚刚的程序相比,减少了多次扩容
类似的函数还有一个resize,重置string的size,如果只传要重置的大小,这些空间默认会初始化为\0
如果再传一个字符,函数会用该字符初始化空间
namespace myString
{
class string
{
public:
// 构造和析构
string(const char* str = ""); // string的构造,空串也开辟空间,只存储\0
~string(); // string的析构
string(const string& str); // string的拷贝构造
string& operator=(const string& str); // string的赋值
void swap(string& str);
// 修改
string& operator+=(const string& str); // string的追加
string& operator+=(char c); // string的追加
string& append(const char* str); // string的追加
string& append(char c); // string的追加
void push_back(char c); // string的尾插
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
string& erase(size_t pos, size_t n); // 从pos位置删除n个字符
const char* c_str() const { return _str; } // 返回c类型的字符串
// 容量
void resize(size_t n, char c = '\0'); // 修改string的大小
void reserve(size_t n); // 修改string的容量
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
// 迭代器
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); // 通过下标访问string
const char& operator[](size_t pos) const; // 通过下标访问string
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos;
};
const size_t string::npos = -1; // 静态变量在类中声明但没有定义,需要在类外定义
}
首先说明capacity表示的是string最大能存储的有效字符个数(不包括’\0’),size表示当前存储的有效字符个数,当然也不包括’\0’,str就是指向存储字符串的指针。
首先实现的是string的构造和析构,构造函数呢,实现成无参的,这样不仅可以构造空串还能传字符串进行构造。先strlen求传入字符串的长度,将长度赋值给_size和_capacity,如果是空串长度就是0,然后为_str分配空间,大小是长度+1,这个1用来存储’\0’,最后再拷贝传入字符串到_str中。
myString::string::string(const char* str)// 说明写了默认参数,定义不用也不能写
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
析构就是将_str释放,_size和_capacity重置为0。
myString::string::~string()
{
delete[] _str;
_capacity = _size = 0;
}
拷贝构造有两种写法一种是利用构造函数,一种是不利用构造函数,但实现的代码与构造函数重复度高,因此复用构造函数实现拷贝构造更方便。
myString::string::string(const string& str) // 传统写法
{
_size = str._size;
_capacity = str._capacity;
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
myString::string::string(const string& str) // 现代写法
{
string tmp(str._str); // 先用str的字符串构造tmp对象
// 此时的tmp就是str的复制,只要把tmp与this交换
swap(tmp);
}
当然还要实现swap函数,利用std库中的交换将两个指针交换,还有_capacity和_size也要交换
void myString::string::swap(string& str)
{
std::swap(_str, str._str);
std::swap(_capacity, str._capacity);
std::swap(_size, str._size);
}
=的重载同样也是两种写法,传统的繁琐,现代的简洁。不过对于任何=的重载都要注意连续复制的情况,因此函数需要返回赋值完成的对象。
对于传统赋值,先判断容量是否足够存储字符串,若不够需要释放之前的空间再开辟一块足够的空间存储。
myString::string& myString::string::operator=(const string& str)
{
if (&str != this) // 防止自己赋值给自己
{
if (str._size > _capacity)
_str = new char[str._size]; // 空间不够的扩容
_size = str._size;
_capacity = str._capacity;
strcpy(_str, str._str);
}
return *this;
}
// 现代写法
myString::string& myString::string::operator=(string str)
{
swap(str); // 形参不是引用,所有调用了拷贝构造,构造了str,所以str是一个复制
return *this;
}
显而易见,这样的复用构造函数让代码更简单也更简洁了
修改无非就是增加与删除,实现了在任意位置的插入与删除,其他的接口也就能复用这两个接口。
说白了,插入就是检查容量,移动数据,插入数据,三个步骤
myString::string& myString::string::insert(size_t pos, char c)
{
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : _capacity * 2);
size_t end = _size + 1; // _str[_size]是'\0',一起移动
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
return *this;
}
(这里需要补充size_t的一个注意点,如果上面end先指向_size - 1的位置,然后循环写为end >= pos,移动的代码写为 _str[end + 1] = _str[end],这就很有问题,当pos为0,end为0时再走一遍循环,而end-1为-1,对吗?
myString::string& myString::string::insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
reserve(_capacity + 1);
size_t end = _size - 1; // 错误示范
while (end >= pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = c;
_size++;
return *this;
}
end是无符号数size_t,所以-1存储到end中是一个很大的数,这样使得循环继续,但越界访问。因此不能这样写,总结:使用无符号数比较要特别注意“负数”问题)
插入的另一个重载:插入一串字符到string中,和插入一个字符类似,只是移动字符的距离变长了
myString::string& myString::string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (len + _size >= _capacity) // 检查扩容
{
reserve(len + _size);
}
size_t end = _size + len;
while (end - len + 1 > pos)
{
_str[end] = _str[end - len];
end--;
}
memcpy(_str + pos, str, len);
_size += len;
return *this;
}
删除字符:函数第一个参数是要删除字符的位置,后一个参数是要删除字符的个数。如果位置加上字符个数大于字符串长度,就是把该位置后面的字符全删除,直接在该位置上放个’\0’。但如果不是全删除就需要移动后面的字符覆盖前面的字符。
myString::string& myString::string::erase(size_t pos, size_t n)
{
assert(pos < _size);
if (n + pos >= _size)
{
_str[pos] = '\0';
_size = pos; // n的值可以是npos,不能减去npos因为npos可能是-1,最大的数
return *this;
}
else
{
size_t end = pos + n; // 用end移动数据
while (end <= _size) // 把'\0'也移过去
{
_str[end - n] = _str[end];
end++;
}
size -= n;
return *this;
}
}
npos是一个最大的数,无符号的-1,作为string的静态成员。
剩下的接口就是复用insert和erase函数了
myString::string& myString::string::operator+=(const string& str)
{
return insert(_size, str._str);
}
myString::string& myString::string::operator+=(char c)
{
return insert(_size, c);
}
myString::string& myString::string::append(const char* str)
{
return insert(_size, str);
}
myString::string& myString::string::append(char c)
{
return insert(_size, c);
}
void myString::string::push_back(char c)
{
insert(_size, c);
}
(范围for底层是迭代器,不实现迭代器就不能用范围for,并且迭代器的命名必须按约定走。假设我把模拟实现的迭代器屏蔽,程序报错)
reserve为string扩容,先检查n是否大于当前容量,如果大于则扩容,小于不缩容。先开辟新的空间,将原来空间的数据拷贝到新空间,再释放原来的空间。
void myString::string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize就是reserve加初始化了,没有给初始化的值就用’\0’初始化。
void myString::string::reserve(size_t n, char c)
{
if (n > _capacity)
reserve(n);
while (_size < n)
_str[_size++] = c;
_size = n; // 如果n小于_size,上面的while不会进去,但长度要减小
_str[n] = '\0'; // 最后再结束的地方放'\0'
}
}
重载[],通过下标访问字符串,但要注意如果string被const修饰则不能写入字符串,所以需要重载两个版本来支持const对象
char& myString::string::operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& myString::string::operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
cout的重载不能直接输出字符串,而是应该一个字符一个字符的输出,考虑到极端情况,string中存储了’\0’,直接输出的话字符串也就不完整
ostream& operator<<(ostream& out, const myString::string& str)
{
for (int i = 0; i < _size; i++)
{
out << _str[i];
}
return out; // 为了支持连续输出
}
cin的重载也是一个一个字符的读,直到读入的字符为空格或者换行,但是直接使用>>遇到空格和换行也是停止的,所以>>永远无法读入空格和换行,要用istream对象的get函数,每次读一个字符,但不会停止
istream& operator>>(istream& in, myString::string& str)
{
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
push_back(ch);
ch = in.get();
}
return in;
}