c++ String 《string的模拟实现》《string类中的深浅拷贝》《string的常用接口说明》《为什么要学习string》
一。为什么我们要学习string?
1.C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
2.string类的常用接口说明,
(1)string类对象的常用构造
函数名称 功能说明
string() 构造空的string类对象,即空字符串
string(const char* s) 用C-string来构造string类对象
string(size_t n, char c) string类对象中包含n个字符c
string(const string&s) 拷贝构造函数
string(const string&s, size_t n) 用s中的前n个字符构造新的string类对象
void TestString()
{
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(10, 'a'); // 用10个字符'a'构造string类对象s3
string s4(s2); // 拷贝构造s4
string s5(s3, 5); // 用s3中前5个字符构造string对象s5
}
(2)string类对象的容量操作
函数名称 功能说明
size_t size() const 返回字符串有效字符长度
size_t length() const 返回字符串有效字符长度
size_t capacity ( ) const 返回空间总大小
bool empty ( ) const 检测字符串释放为空串,是返回true,否则返回false
void clear() 清空有效字符
void resize ( size_t n, char c ) 将有效字符的个数该成n个,多出的空间用字符c填充
void resize ( size_t n ) 将有效字符的个数改成n个,多出的空间用0填充
void reserve ( size_t res_arg=0 ) 为字符串预留空间
#include<string> //这个后没有没有<.h>
#include<iostream>
using namespace std;
#include<cstring>
void TestString()
{
string s("Hello world!!");
cout << s.length() << endl;
cout << s.size() << endl;
cout << s.capacity() << endl; //string类直接支持cin和cout;
cout << s << endl; //没有与这个相同的类型;
s.clear();//把这里面的内容清空之后,长度与字符个数清空,但是底层容量没有大的变化。
cout << s.length() << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
s.resize(10, 'a'); //把s中有效字符数增加到15个,多余的用a补上
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl; //为什么打印不出来?
s.resize(15); //把s中的有效字符增加15个,多余的用\0补上;
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
s.resize(5); //把s中的有效字符减少到5个;
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
}
这几个的代码实现很简单,而且容易看懂,不难,下面我就把实现过的代码exe看一下;
void TestString2()
{
//测试reserve是否影响string的有效个数
string s("hello world");
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
//判断缩小reserve的大小,是否会改变容量的大小;
s.reserve(50);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << 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不会改变容量大小。
(3)string类对象的访问操作
函数名称 功能说明
char& operator[] ( size_t pos ) 返回pos位置的字符,const string类对象调用
const char& operator[] ( size_t pos ) const返回pos位置的字符,非const string类对象调用
void TestString3()
{
string s1("hello bit");
const string s2("hello world");
cout << s1 << " " << s2 << endl;
s1[0] = 'H';
cout << s1 << endl;
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
//s2[0] = 'H'; //编译失败,因为const类型不能被修改;
}
(4)string类对象的修改操作
函数名称 功能说明
void push_back(char c) 在字符串后尾插字符c
string& append (const char* s); 在字符串后追加一个字符串
string& operator+=(const string& str) 在字符串后追加字符串str
string& operator+=(const char* s) 在字符串后追加C个数字符串
string& operator+=(char c) 在字符串后追加字符c
const char* c_str( )const 返回C格式字符串
size_t find (char c, size_t pos = 0)const 从字符串pos位置开始往后找字符c,
返回该字符在字符串中的位置
size_t rfind(char c, size_t pos = npos) 从字符串pos位置开始往前找字符c,
返回该字符在字符串中的位置
string substr(size_t pos = 0, size_t n = npos)const 在str中从pos位置开始,截取n个字符,
然后将其返回
void TestString4()
{
string str;
str.push_back(' ');
str.append("hello");
str.push_back(' ');
str += 'b';
str += 'it';
cout << str << endl;
//cout << c_str()<<endl;这个为什么会报错,我实在是想不通;
//获取文件的后缀<.cpp>
string file("test.cpp");
size_t pos = file.rfind('.');
string dpf(file.substr(pos, file.size() - pos)); //关键使用"substr"接口
cout << dpf << endl;
//取出dpf中的域名
string dpf1("http://www.cplusplus.com/reference/string/string/npos/");
cout << dpf1 << endl;
size_t start = dpf1.find('://');
if (start == string::npos)
{
cout << "dpf1" << endl;
return;
}
start += 2; //这里为什么会是+3,而不是别尼?
size_t finsh = dpf1.find('/', start);
string address = dpf1.substr(start ,finsh - start);
cout << address << endl; //这里打印的就是去掉前面的http后面的域名;
//删除dpf1的协议前缀;
pos = dpf.find("://");
dpf1.erase(0, pos + 5);
cout << dpf1 << endl;
}
在这里直接使用,练习接口的实现,就可以完成对string类的书写,下面就来看一下生成的程序。
根据这几张图片的实现,我们是否就可以知道其中接口的使用。
//利用reserve提高效率,避免开空间带来的开销:
void Testpushback()
{
string s;
size_t sz = s.capacity();
cout << "making s group\n";
for(int i = 0 ; i < 100; i++)
{
s += 'c';
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity change = " << sz << endl;
}
}
}
void Testpushback_p()
{
string s;
s.reserve(100);
size_t sz = s.capacity();
cout << "making s group\n";
for (int i = 0; i < 100; i++)
{
s += 'c';
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity change = " << sz << endl;
}
}
}
这两个一个是使用reserve的,一个是没有使用的,我们看一下生成的程序;
这个图片是第一段代码的,它在不停的自己开辟空间,而且没有用的直接浪费了我,下面我们看一下reserve的使用生成程序:
我们可以清楚的看到,reserve它只是调用了一个应该生成的大小,而且没有开辟多余的空间。
注意:
- 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
二,string类的模拟操作
(1)经典的string类问题
class String
{
public:
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);
}
试一试上面的代码,通过编译,我们可以知道这段代码是编译不过的,那是因为s1与s2公用一块内存,当释放一块时,另一块也就释放掉了,导致程序崩溃,这样的方式叫做《浅拷贝》。
(2)浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。要解决浅拷贝问题,C++中引入了深拷贝。
(3)深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
下面我们就可以看一下string的最基本的四大接口实现:
先是普通版:
class String
{
public:
String(const char* str = "") //构造
{
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str)+1];
strcpy(_str, str);
}
String(const String& s) //拷贝构造
:_str(new char[strlen(s._str)+1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pstr = new char[strlen(s._str) + 1];
strcpy(pstr, s._str);
delete[]_str;
_str = pstr;
}
return *this;
}
~String() //析构
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
private:
char* _str;
};
下面是现代版的string类:
int main()
{
String s1(“hello world”);
String s2(s1);
return 0;
}
现代版string的基本接口实现;
class String
{
public:
String(const char* str = " ")
{
if (str != nullptr)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
//String& operator=(const String& s) // 在这里交换函数怎么实现不了;
//{
// swap(_str, s._str);
// return *this;
//}
String& operator=(const String& s)
{
if (this != &s)
{
/*char* pstr = new char[strlen(s.str) + 1];
strcpy(pstr, s._str);
delete[]_str;
_str = pstr;*/
String strTmp(s);
swap(_str, strTmp._str);
}
return *this;
}
~String()
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
private:
char* _str;
};
其实,我感觉是没有什么改变的,就是《拷贝构造》和《赋值运算》哪里有一点不一样。
第三点还有写实拷贝,但是这个我们还是了解一下就可以了。
写实拷贝:https://coolshell.cn/articles/12199.html
四 ,string类的模拟实现
#include<string> //这个后没有没有<.h>
#include<iostream>
using namespace std;
#include<cstring>
#include<assert.h>
namespace bit
{
class String
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
String(char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// copy(s1)
String(const String& s)
:_str(NULL)
, _size(0)
, _capacity(0)
{
String tmp(s._str);
this->Swap(tmp);
}
// copy = s2;
String& operator=(String s)
{
this->Swap(s);
return *this;
}
void Swap(String& s)
{
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
}
~String()
{
if (_str)
{
delete[] _str;
_size = 0;
_capacity = 0;
}
}
bool operator<(const String& s)
{
int tmp = 0;
while (tmp < _size && tmp < s._size)
{
if ((*_str + tmp) != (*s._str + tmp))
{
if ((*_str + tmp) >= (*s._str + tmp))
{
return false;
}
else
return true;
}
tmp++;
}
if (_size < s._size)
{
return true;
}
else
{
cout << "tem:" << tmp << endl;
return false;
}
}
bool operator>(const String& s)
{
return !(*this < s || *this == s); //因为this是一个指针;
}
bool operator<=(const String& s)
{
return !(*this > s);
}
bool operator>=(const String& s)
{
return !(*this < s);
}
bool operator==(const String& s)
{
int tmp = 0;
if (_size != s._size)
{
return false;
}
while (tmp != _size)
{
if ((*_str + tmp) != (*s._str + tmp))
{
return false;
}
tmp++;
}
return true;
}
bool operator!=(const String& s)
{
return !(*this == s);
}
void Reserve(size_t n)
{
if (n == _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
void PushBack(char ch)
{
if (_size >= _capacity)
{
Reserve(_capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size + 1] = '\0';
cout << _str << endl;
}
void Append(const char* str)
{
size_t n = strlen(str);
if (_size + n > _capacity)
{
Reserve(_size + n);
}
strcpy(_str + _size, str);
_size += strlen(str);
cout << _str << endl;
}
String& operator+=(char ch);
String& operator+=(const char* str);
void Insert(size_t pos, char ch) // 在pos位置上插入字符c/字符串str,并返回该字符的位置
{
if (_size == _capacity)
{
Reserve(_capacity * 2);
}
String s(*this);
char* tmp = s._str + pos;
_str[pos] = ch;
strcpy(_str + pos + 1, tmp);
cout << _str << endl;
}
void Insert2(size_t pos, const char* str) // 在pos位置上插入字符c/字符串str,并返回该字符的位置
{
_size += strlen(str);
while (_size == _capacity)
{
Reserve(_capacity * 2);
}
int tmp = _size;
while (tmp != pos - 1)
{
_str[tmp + strlen(str)] = _str[tmp];
--tmp;
}
int tmp2 = 0;
while (tmp2 < strlen(str))
{
_str[pos + tmp2] = str[tmp2];
tmp2++;
}
/*int i=strlen(ch);
String s(*this);
char* tmp = s._str + pos;
strcpy(_str+pos, ch);
strcpy(_str+pos+i,tmp);*/
cout << _str << endl;
}
void Erase(size_t pos, size_t len);
size_t Find(char ch, size_t pos = 0) // 返回c在string中第一次出现的位置
{
int i = 0;
for (i = 0; i< _size; ++i)
{
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
else
pos++;
}
return -1;
}
//cout<<<<endl;
}
size_t Find2(const char* str, size_t pos = 0) // 返回子串str在string中第一次出现的位置
{
if (strlen(str) > _size)
{
return -1;
}
int tmp = 0;
while (pos < _size)
{
tmp = 0;
if (_str[pos] == str[0])
while (tmp <= strlen(str))
{
if (_str[pos + tmp] != str[tmp])
{
break;
}
tmp++;
if (tmp == strlen(str))
{
return pos;
}
}
pos++;
}
}
char* c_str()
{
return _str;
}
char& operator[](size_t pos)
{
//assert(pos < _size);
return _str[pos];
}
size_t Size()
{
return _size;
}
size_t Capacity()
{
return _capacity;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
};
size_t String::npos = -1;
void TestString1()
{
String s1("hello");
String s2("word");
s1.PushBack('h');
s1.Append("hidasfa");
s1.Insert2(3,"fdsa");
//cout << s1.Find2("ll", 0);
cout<<(s1 < s2)<<endl;
cout<<(s1 == s2)<<endl;
cout<<(s1 <= s2)<<endl;
String copy(s1);
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << copy.c_str() << endl;
copy = s1 = s2;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << copy.c_str() << endl;
String::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
}
int main()
{
bit::TestString1();
return 0;
}
下面我们看一下生成的可执行程序:
当然,我这里的接口不是最完整的,但是我们常用的接口都已经实现了,仅供参考,如果你还有想实现别的接口,自己可以实现一下;
完成了以上的代码,你也可以写一篇博客,记录一下《我们相互关注一下,哈哈》;