string类的灵魂三问:是什么?为什么?怎么办?
1 为什么要学习 string 类(为什么)?
** C语言中的字符串**
-
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
-
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
2 标准库中的 string 类(是什么)?
2.1 string 类
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>
string; - 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
2.2 string 类的常用接口
2.2.1 string 类对象的常用构造函数
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1;//默认构造 空字符串
string s2("czh");//用C字符串构造
string s3(s2);// 拷贝构造函数
return 0;
}
2.2.2 string 类对象的容量操作
string s1("chenzhihao");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.length() << endl;
cout << s1.capacity() << endl;
//将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
s1.clear();
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//将s1中有效字符个数增加到15个,用'a'进行填充
s1.resize(15, 'a');
cout << s1 << endl;
//将s1中有效字符个数增加到20个,多出位置用缺省值'\0'进行填充
s1.resize(20);
cout << s1 << endl;
s1.resize(5);
cout << s1 << endl;
//reserve并不会改变string中的有效元素个数
string s;
cout << s.size() << endl;
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
//reserve参数小于string的底层空间大小时,并不会将空间缩小
s.reserve(50);
cout << s.capacity() << endl;
利用 reserve 可以提高插入数据的效率,避免增容带来的开销,前提是你知道自己要多大的空间
void testPushBack1()
{
string s;
size_t cap = s.capacity();
cout << "cap increasing:" << endl;
for (int i = 0; i < 100; i++) {
s.push_back('c');
if (cap != s.capacity()) cout << "cap inc" << endl;
}
}
上面的代码,持续的增容会带来系统开销的增大。
void testPushBackReserve()
{
string s;
s.reserve(100);
size_t cap = s.capacity();
for (int i = 0; i < 100; i++) {
s.push_back('c');
if (cap != s.capacity()) cout << "cap inc" << endl;
}
}
并没有增容,减少了系统开销
总结:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一
致,一般情况下基本都是用size()。 - clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
2.2.3 string 类对象的访问和遍历操作
第一种遍历方式 :operator[]
void TestString1() {
string s1("chenzhihao");
const string s2("hello czh");
for (size_t i = 0, j = 0; i < s1.size(),j < s2.size(); i++,j++) {
cout << s1[i] << endl;
cout << s2[j] << endl;
}
}
第二种遍历方式 :迭代器
void TestString2() {
string s1("chenzhihao");
// 正向迭代器
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 反向迭代器
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
}
**第三种遍历方式 :范围 for **
void TestString3() {
string s1("chenzhihao");
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
}
2.2.4 string 类对象的修改操作
void TestString4()
{
string str;
str.push_back('c');
str.append("henzhihao");
str += 'a';
str += "hahaha";
cout << str << endl;
cout << str.c_str() << endl;
//获取file的后缀
string file("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size() - pos));
cout << file << endl;
cout << suffix << endl;
}
void TestString5()
{
// 取出url中的域名
string url("http://www.cplusplus.com/reference/string/string/find/");
size_t pos = url.find(':');
size_t start;
if (pos != string::npos) {
start = pos + 3;
}
size_t end = url.find('/',start);
string domainName = url.substr(start, end - start);
cout << "domainName : " << domainName << endl;
cout << url << endl;
// 删除url的协议前缀
string urlcopy(url);
urlcopy.erase(0, start);
cout << urlcopy << endl;
}
3 string类的模拟实现(怎么办)?
3.1 经典的 string 类问题
我们自己动手实现了一个 string 类,
namespace czh{
class string{
public:
/*string()
:_pStr(new char[1])
{
*_pStr = '\0';
}*/
string(const char* str = "")
{
if (str == nullptr)
{
assert(false);
return;
}
_pStr = new char[strlen(str) + 1];
strcpy(_pStr, str);
}
~string()
{
if (_pStr)
{
delete[] _pStr;
_pStr = nullptr;
}
}
private:
char *_pStr;
};
}
int main()
{
czh::string s1("chenzhihao");
czh::string s2(s1);
return 0;
}
编译没有问题,当运行可执行程序的时候,结果程序崩溃,那么这是为什么呢?
我们观察上述的代码,这个string类没有显式定义其拷贝构造函数和赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造,最终导致的问题是s1、s2公用一块内存空间,当函数结束时,需要将s1和s2销毁掉,先析构s2,s2将其_pStr所指向的空间释放掉,s2对象成功销毁,但是s1中的_pStr称为野指针,当析构s1时候重复释放空间就引起了程序的崩溃,这种拷贝方式称为浅拷贝。
3.2 浅拷贝
上面的问题引入了浅拷贝的概念,那么再详细介绍下:
浅拷贝也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中涉及管理资源,最后会导致多个对象共享同一份资源,当一个对象被销毁的时就会将该资源释放掉,而此时另一些对象不知道资源已经被释放掉,以为还有效,所以当继续对该资源进行释放的时,就发生了违规访问。为了解决浅拷贝的问题,C++中引入了深拷贝。
3.3 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出,一般都是按照深拷贝方式提供。每个string类对象都要用空间类存放字符串,而s2要s1拷贝构造出来,因此给每个对象独立分配资源,保证多个对象不会因共享资源而造成多次释放造成程序崩溃问题。
namespace czh{
class string{
public:
/*string()
:_pStr(new char[1])
{
*_pStr = '\0';
}*/
string(const char* str = "")
{
if (str == nullptr)
{
assert(false);
return;
}
_pStr = new char[strlen(str) + 1];
strcpy(_pStr, str);
}
//加上了显式定义的拷贝构造程序可以正常运行
string(const string& s)
{
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr, s._pStr);
}
~string()
{
if (_pStr)
{
delete[] _pStr;
_pStr = nullptr;
}
}
private:
char *_pStr;
};
}
int main()
{
czh::string s1("chenzhihao");
czh::string s2(s1);
return 0;
}
加上赋值运算符重载的代码
namespace czh{
class string{
public:
/*string()
:_pStr(new char[1])
{
*_pStr = '\0';
}*/
string(const char* str = "")
{
if (str == nullptr)
{
assert(false);
return;
}
_pStr = new char[strlen(str) + 1];
strcpy(_pStr, str);
}
//加上了显式定义的拷贝构造程序可以正常运行
string(const string& s)
{
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr, s._pStr);
}
//赋值运算符重载
string& operator=(const string& s)
{
if (this != &s)
{
char * temp = new char[strlen(s._pStr) + 1];
strcpy(temp, s._pStr);
delete[] _pStr;
this->_pStr = temp;
}
return *this;
}
~string()
{
if (_pStr)
{
delete[] _pStr;
_pStr = nullptr;
}
}
private:
char *_pStr;
};
}
int main()
{
czh::string s1("chenzhihao");
czh::string s2(s1);
czh::string s3;
s3 = s1;
return 0;
}
3.3.1 现代版写法的 string 类
这里涉及到赋值运算符重载的一个异常安全性问题,要想在赋值运算符函数中实现异常安全性,我们有两种方法。一种简单的方法是先用 new 分配空间,再用 strcpy 分配内容,再用 delete 释放已有的内容,再让 _str 指向的新分配的内容。这样就保证了只有分配空间和内容成功之后再释放原来的内容,也就是当分配内存失败的时候我们可以确保 czh::string 的实例不会被修改,我们还有一种更好的办法即先创建一个临时实例,再交换临时实例和原有实例的资源指向。下面是这种思路的string类实现
namespace czh{
class string{
public:
/*string()
:_pStr(new char[1])
{
*_pStr = '\0';
}*/
string(const char* str = "")
{
if (str == nullptr)
{
assert(false);
return;
}
_pStr = new char[strlen(str) + 1];
strcpy(_pStr, str);
}
//加上了显式定义的拷贝构造程序可以正常运行
/*string(const string& s)
{
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr, s._pStr);
}*/
//现代版本的拷贝构造函数
string(const string& s)
:_pStr(nullptr)
{
string strTmp(s._pStr);
swap(_pStr, strTmp._pStr);
}
//赋值运算符重载
/*string& operator=(const string& s)
{
if (this != &s)
{
char * temp = new char[strlen(s._pStr) + 1];
strcpy(temp, s._pStr);
delete[] _pStr;
this->_pStr = temp;
}
return *this;
}*/
//现代版本的赋值运算符重载
string& operator=(const string& s)
{
if (this != &s)
{
string strTmp(s);
swap(_pStr, strTmp._pStr);
}
return *this;
}
~string()
{
if (_pStr)
{
delete[] _pStr;
_pStr = nullptr;
}
}
private:
char *_pStr;
};
}
int main()
{
czh::string s1("chenzhihao");
czh::string s2(s1);
czh::string s3;
s3 = s1;
return 0;
}
现代版本的赋值运算符重载函数中,我们先创建了一个临时实例 strTmp,接着把 strTmp._pStr 和实例自身的 _pStr 进行交换。由于 strTmp 是一个局部变量当程序运行到 if 外面的时候也就出该对象(变量)的作用域,就会自动调用 strTmp 的析构函数,把 strTmp._pStr 所指向的内存释放掉,又由于strTmp._pStr 所指向的内存就是实例之前的 _pStr 的内存,这就相当于自己调用析构函数释放(this)实例的内存。这两个方法都考虑到了代码的异常安全性问题。
4 自己实现 string类
string.h
#pragma once
#include <string.h>
#include <assert.h>
#include <utility>
namespace czh{
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;
}
//构造
string(const char* str = "")
{
if (str == nullptr)
{
assert(false);
return;
}
_size = strlen(str);
_capacity = _size;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//拷贝构造
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
/*_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);*/
string strTmp(s._str);
std::swap(_str, strTmp._str);
}
//析构
~string()
{
if (_str != nullptr)
{
delete[] _str;
_str = nullptr;
}
_size = _capacity = 0;
}
//[]运算符重载
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
//赋值运算符重载
string& operator=(const string& s)
{
//保证不给自己赋值
if (this != &s)
{
/*char* temp = new char[strlen(s._str) + 1];
strcpy(temp, s._str);
delete[] _str;
_str = temp;*/
string strTmp(s);
std::swap(_str, strTmp._str);
}
return *this;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char *temp = new char[n];
strcpy(temp, _str);
delete[] _str;
_str = temp;
}
}
void push_back(char c)
{
if (_size == _capacity)
{
reserve(2 * _capacity);
}
_str[_size] = c;
_size++;
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(const string& s)
{
append(s._str);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
测试
#include "string.h"
#include <iostream>
using namespace std;
void PrintString(const czh::string& str)
{
czh::string::const_iterator begin = str.begin();
czh::string::const_iterator end = str.end();
while (begin != end)
{
cout << *begin << " ";
++begin;
}
cout << endl;
}
int main()
{
czh::string s1 = "CHENZHIHAO";
czh::string s2(s1);
czh::string s3("hello");
s3 = s1;
//PrintString(s1);
for (auto& e : s1)
{
e += 32;
cout << e << " ";
}
return 0;
}