这一节主要总结string类的常见接口,以及完成了string类的模拟实现。
目录
标准库的String类
为了符合C++面向对象的特性,引入了string类,string是表示字符串的字符串类。下面说明一下string类的特点:
1.string是表示字符串的字符串类
2.string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数
3.string类独立于所使用的编码来处理字节,如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
提示: 在使用string类时,必须包含#include头文件以及using namespace std;
string类常见接口
string类对象的常见构造
(重点)1.string()
默认构造,构造空的string类对象,即空字符串
(重点)2.string(const string& str)
拷贝构造,使用str拷贝构造string类对象
(重点)3.string(const char* s)
使用C-string来构造string类对象
4.string(const string& str, size_t pos, size_t len=npos)
从str的第pos个位置,取len个字符构造,如果len超过字符串长度,那么只取到字符串末尾,如果len未给出,那么采用默认参数npos(0xFFFFFFFF)
5.string(const char* s, size_t n)
使用C-string的前n个字符来构造string类对象
6.string(size_t n, char c)
string类对象包含n个字符c
void test_string1()
{
string s0;
string s1("hello world");
string s2(s1);
string s3(s1, 5, 3);
string s4(s1, 5, 10);
string s5(s1, 5);
string s6(10, '$');
cout << s0 << endl;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
}
string析构函数:~string
在string类对象生命周期结束时,会被自动调用。
string类对象的容量操作
1.size()和length()
这两个均可以返回字符串有效字符长度,功能一样。可以通过size遍历字符串。有两个功能一样的函数是由于历史原因造成的!
2.max_size()
返回string最大的大小
3.capacity()
返回string类对象的容量大小。
//查看扩容机制
void test_string4()
{
string s;
size_t sz = s.capacity();
cout << "capacity change:" << sz << endl;
cout << "making s growing" << endl;
for (int i = 0; i < 100; i++)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed" << sz << endl;
}
}
}
虽然是15、31...,但是没有包括'\0',所以实际大小应该是16、32、48...,第一次是2倍扩容,以后每次是1.5倍扩容(VS平台下)。但是在Linux平台下,扩容机制不太一样:
这就说明,STL是一个标准,具体怎么实现由编译器决定!
4.clear()
清除对象的数据,但是不会清空间。
5.shrink_to_fit()
在clear()后,为了释放一些已开辟的空间,可以缩容
6.reserve()
如果提前知道要开辟空间的大小,可以用reserve()为字符串提前预留空间,防止频繁扩容,因为扩容一般是异地扩容,效率低下。
避免了频繁扩容!
还有一个问题,reserve会不会缩容呢?不会的!当reserve()比capacity大,才会扩容!
- resize()
调整字符串大小。
如果采用第一种,那么默认用'\0'插入,第二种用指定的char插入。
string类对象的访问及遍历操作
1.operator[],返回pos位置的字符
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
2.迭代器,begin+end,可以**通过迭代器的方式遍历字符串,**begin获取第一个字符的迭代器,end获取最后一个字符下一个位置的迭代器
string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1 << " ";
++it1;
}
我们感觉到迭代器很像指针,但是并不是指针!
3.rbegin+rend,支持倒着遍历,循环中用++rit,而不是--rit,对于反向迭代器而言,++就是倒着走
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
除了正向和反向的正常iterator外,还有正向和反向的const_iterator ,是用来为const string构造迭代器,其特点是只能读不能写:
//只读
const string s3("hello world");
string::const_iterator it3 = s3.begin();
while (it3 != s3.end())
{
//*it3 += 3;//报错,因为只读
cout << *it3 << " ";
}
cout << endl;
//string::const_reverse_iterator
4.用范围for遍历字符串
for (auto s : s1)
{
cout << s << " ";
}
但是范围for并没有什么特别之处,其底层是迭代器!
string类对象的修改操作
1.pushback()
在字符串末尾尾插字符
2.append()
在字符串后追加字符串
3.operator+=
在字符串后追加字符串str
4.assign()
用新内容覆盖原字符串
5.insert()
在某一个位置插入字符串
注意:insert()尽量不要使用,因为在使用过程中会挪动数据,效率低。
6.erase()
删除字符
同样,erase()函数能不用就不用。
7.replace()
字符的替换
8.find()
查找某个字符的位置
小练习:将一个字符串中的空格替换成"%20"。
总结一下,insert、erase、replace能少用就少用,因为基本都要挪动数据,效率不高。
9.c_str
返回C格式字符串
在C++程序中,我们有可能会调用C语言的函数,比如fopen函数,但是在程序中文件名可能是string对象,需要转换成C字符串才行。
void test_string()
{
string filename("test.cpp");
FILE* fout = fopen(filename.c_str(), "r");
}
10.find
找到某一个字符或字符串第一次出现的位置
例如:获取后缀
void test_string()
{
string s1("test.cpp");
size_t pos = s1.find('.');
if (pos != string::npos)
{
string suffix = s1.substr(pos);
cout << suffix << endl;
}
else
{
cout << "没有后缀" << endl;
}
}
11.rfind
从后往前找某一个字符或字符串第一次出现的位置
例如:获取后缀"file.c.tar.zip"的zip
void test_string()
{
string s1("test.c.tar.zip");
size_t pos = s1.rfind('.');
if (pos != string::npos)
{
string suffix = s1.substr(pos);
cout << suffix << endl;
}
else
{
cout << "没有后缀" << endl;
}
}
string类非成员函数
1.operator+
尽量少用,因为传值返回,导致深拷贝效率低
2.operator>>&&operator<<
输入和输出运算符重载
3.getline
获取一行字符串。弥补了cin>>的缺点,cin遇到了空格或者换行就提取结束。
4.== != > >= < <=
大小比较
string类的模拟实现
经典的string类问题
首先来看一个经典关于浅拷贝的问题:
class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范,使用strlen时会涉及解引用
String(const char* str = "")
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
说明 :在上面的String类中没有显式定义拷贝构造和赋值运算符重载,因此,当使用s1去构造时s2时,编译器会调用默认构造函数。这样导致的问题是,**s1和s2指向同一块内存空间,在释放空间时,同一块空间会被释放两次而引起程序崩溃,**这种拷贝方式,称为浅拷贝。
浅拷贝
浅拷贝:也称值拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
可以用深拷贝解决浅拷贝 问题,也就是:每个对象都有一份独立的资源,不要和其他对象共享。
深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
传统版写法的String类
namespace ghs
{
class string
{
public:
string(const char* str="")
:_size(strlen(str))
{
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
string(const string& str)
{
_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
//s1 = s2
string& operator=(const string& str)
{
char* tmp = new char[str._capacity + 1];
strcpy(tmp, str._str);
delete[] _str;
_str = tmp;
_size = str._size;
_capacity = str._capacity;
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const int npos;
};
const int string::npos = -1;
}
在传统版的写法中,拷贝构造和赋值构造都是通过自己手动开空间,然后使用strcpy函数拷贝完成的,下面的现代写法在实现上更为简洁:
现代版写法的String类
namespace ghs
{
class string
{
public:
string(const char* str="")
:_size(strlen(str))
{
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//s1 = s2
//string& operator=(const string& str)
//{
// string ss(str);
// swap(ss);
// return *this;
//}
//上面的再优化,这种更推荐
string& operator=(string ss)//传值传参,调用拷贝构造
{
swap(ss);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const int npos;
};
const int string::npos = -1;
}
在现代写法中,拷贝构造是通过调用string(const char* str="")构造函数来生成临时的string对象(tmp),再将*this和tmp交换,完成拷贝。在赋值构造中,使用传值传参,其实调用拷贝构造,生成临时对象ss,再将ss和*this交换,完成拷贝。
总结:传统版的现代版的效率一样,只是现代版调用了之前已经实现好的函数,所以看起来更加简洁!
string类的模拟实现
namespace ghs
{
class string
{
public:
/*string()
:_str(nullptr)
,_size(0)
,_capacity(0)
{}*/
/*string()
:_str(new char[1])
,_size(0)
,_capacity(0)
{
_str[0] = '\0';
}*/
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="")
:_size(strlen(str))
{
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
//string(const string& str)
//{
// _str = new char[str._capacity + 1];
// strcpy(_str, str._str);
// _size = str._size;
// _capacity = str._capacity;
//}
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//s1 = s2
//string& operator=(const string& str)
//{
// char* tmp = new char[str._capacity + 1];
// strcpy(tmp, str._str);
// delete[] _str;
// _str = tmp;
// _size = str._size;
// _capacity = str._capacity;
// return *this;
//}
string& operator=(const string& str)
{
string ss(str);
swap(ss);
return *this;
}
//上面的再优化,这种更推荐
//string& operator=(string ss)//传值传参,调用拷贝构造
//{
// swap(ss);
// return *this;
//}
const char* c_str()const
{
return _str;
}
size_t size()const
{
return _size;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t sz)
{
if (sz > _capacity)
{
char* p = new char[sz + 1];
strcpy(p, _str);
delete[] _str;
_str = p;
_capacity = sz;
}
}
void push_back(char ch)
{
//扩容2倍
/*if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';*/
insert(_size, ch);
}
void append(const char* str)
{
//扩容
//size_t len = strlen(str);
//if (_size + len > _capacity)
//{
// reserve(_size + len);
//}
//strcpy(_str + _size, str);
///*for (size_t i = 0; i < len; i++)
//{
// push_back(str[i]);
//}*/
//_size += len;
insert(_size, str);
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
size_t capacity()const
{
return _capacity;
}
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
for (size_t end = _size+1; end > pos; end--)
{
_str[end] = _str[end - 1];
}
_str[pos] = ch;
_size++;
}
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(len + _size);
}
for (size_t end = _size+len; end > pos+len-1; end--)
{
_str[end] = _str[end-len];
}
/*for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}*/
strncpy(_str + pos, str, len);
_size += len;
}
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || len>=_size-pos)
{
_size = pos;
_str[_size] = '\0';
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
void resize(size_t n, char ch = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
size_t find(char ch, size_t pos = 0)const
{
assert(pos < _size);
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
{
assert(pos < _size);
char* tmp = strstr(_str+pos, str);
if (tmp != nullptr)
{
return tmp - _str;
}
else
{
return npos;
}
}
string substr(size_t pos = 0, size_t len = npos)
{
string s;
if (len == npos || len >= _size-pos)
{
for (size_t i = pos; i < _size; i++)
{
s += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
s += _str[i];
}
}
return s;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
/*bool operator==(const string& str)
{
int ret = strcmp(_str, str._str);
return ret == 0;
}*/
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const int npos;
};
const int string::npos = -1;
void swap(string& x, string& y)
{
x.swap(y);
}
bool operator==(const string& s1,const string& s2)
{
int ret = strcmp(s1.c_str(), s2.c_str());
return ret == 0;
}
bool operator<(const string& s1, const string& s2)
{
int ret = strcmp(s1.c_str(), s2.c_str());
return ret < 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
ostream& operator<<(ostream& out, const string& str)
{
for (auto ch : str)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& str)
{
char ch;
str.clear();
//in >> ch;
ch = in.get();
char buff[100];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 99)
{
buff[99] = '\0';
str += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return in;
}
istream& getline(istream& in, string& str)
{
char ch;
str.clear();
char buff[100];
//in >> ch;
/*ch = in.get();
while (ch != '\n')
{
str += ch;
ch = in.get();
}*/
ch = in.get();
size_t i = 0;
while (ch != '\n')
{
buff[i++] = ch;
if (i == 99)
{
buff[99] = '\0';
str += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return in;
}
}