[C/C++]详解STL容器1--string的功能和模拟实现(深浅拷贝问题)
本文介绍了string类的常用接口的使用,并对其进行了模拟实现,对模拟实现中涉及到的深浅拷贝问题进行了解析。目录一、string类1. C语言中的字符串2. C++中的string类二、string类的常用接口的使用1. string类对象的常见构造2. string类对象的容量操作3. string类对象的访问及遍历操作4.string类对象的修改操作5. string类非成员函数6.使用实例三
本文介绍了string类的常用接口的使用,并对其进行了模拟实现,对模拟实现中涉及到的深浅拷贝问题进行了解析。
目录
一、string类
1. C语言中的字符串
在C语言中,字符串是以'\0'结尾的一些字符的集合,C标准库还提供了str系列的库函数,但是这些库函数与字符串不太符合OOP的思想,底层空间需要用户自己管理,可能会造成越界访问。
2. C++中的string类
C++ 大大增强了对字符串的支持,除了可以使用C风格的字符串,还可以使用内置的 string 类。string 类处理起字符串来会方便很多,完全可以代替C语言中的字符数组或字符串指针。
string是表示字符串的字符串类,该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。不能操作多字节或者变长字符的序列。在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
二、string类的常用接口的使用
1. string类对象的常见构造
(constructor)函数名 | 功能说明 |
---|---|
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) | 拷贝构造函数 |
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("abcdef"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
2. string类对象的容量操作
函数名 | 功能说明 |
---|---|
size | 返回字符串有效字符长度,一般用作返回容器大小的方法 |
length | 返回字符串有效字符长度,一般用作返回一个序列的长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
clear | 清空有效字符 |
reserve | 为字符串预留空间 |
resize | 将有效字符的个数该成n个,多出的空间用字符c填充 |
这里的size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致。
clear()只是将string中有效字符清空,不改变底层空间大小。
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
3. string类对象的访问及遍历操作
函数名 | 功能说明 |
---|---|
operator[] | 返回pos位置的字符,const string类对象调用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
三种迭代
void Teststring()
{
string s("hello world");
// 3种遍历方式:
// 1. for+operator[]
for(size_t i = 0; i < s.size(); ++i)
cout<<s[i]<<endl;
// 2.迭代器
string::iterator it = s.begin();
while(it != s.end())
{
cout<<*it<<endl;
++it;
}
string::reverse_iterator rit = s.rbegin();
while(rit != s.rend())
cout<<*rit<<endl;
// 3.范围for
for(auto ch : s)
cout<<ch<<endl;
}
4.string类对象的修改操作
函数名 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符 |
append | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串 |
c_str | 返回C格式字符串 |
find + npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般
情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
5. string类非成员函数
函数名 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operators | 大小比较 |
6.使用实例
int main()
{
/*****************构造**********************/
string s1; //无参
string s2("zhtzhtzht"); //带参
string s3(s2); //拷贝构造
string s4 = "zhtzhtzhtzht";
//substring ,给多了或者给string::npos 都是走到尾
string s5(s4, 3, 5); //从3开始5个
cout << s5 << endl;
string s6("123456", 3); //取前三个构造
cout << s6 << endl;
/*************三种遍历***************/
//1.下标+【】
for (size_t i = 0; i < s2.size(); i++)
{
cout << s2[i] << " ";
}
cout <<endl;
//2.迭代器,可以写
//[begin(),end()) end()返回的是最后一个下一个位置
//counst 只能用counst_iterator 遍历,只读不可写
//counst 对象就自动是counst迭代器
string::iterator it = s2.begin(); //正向
while (it != s2.end())
{
cout << *it << " ";
++it;
}
cout << endl;
string::reverse_iterator rit = s2.rbegin(); //反向
while (rit != s2.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
//3. C++11 提供 范围FOR
//依次取容器中的数据,赋值给E,自动判断结束
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
s3.push_back('a'); //尾插一个字符
s3.push_back('b');
s3.append("qqqqq"); //尾插字符串
cout << s3 << endl;
s3.append(s2); //尾插对象,也可以迭代器
cout << s3 << endl;
//+=
//实际最喜欢的
s3 += ' ';
s3 += "zzzaa";
s3 += s2;
cout << s3 << endl;
//尽量少用,底层用数组实现
s3.insert(0, " ztzt "); //指定位置插入,可实现头插
s3.insert(3, " qqqq ");
cout << s3 << endl;
s3.erase(0, 1); //头删
s3.erase(3, 5); //第三个位置删5个
cout << s3 << endl;
s3.erase(3); //第三个后全删
s3.erase(); //从0到npos全删,默认是0开始
cout << s3 << endl;
string s7(s2);
cout << s7.size() << endl;
cout << s7.capacity() << endl; //空间大小
s7.clear(); //清空
cout << s7.size() << endl;
cout << s7.capacity() << endl;
string s8;
cout << s8.size() << endl;
cout << s8.capacity() << endl;
s8.resize(20,'x'); //插入n个x,默认\0;已有的话追加,把总空间变成指定的
cout << s8.size() << endl;
cout << s8.capacity() << endl;
s8.reserve(50); //不影响已有的
cout << s8.size() << endl;
cout << s8.capacity() << endl;
cout << s8 << endl; //重载的<<
cout << s8.c_str() << endl; //c的方式,配合C使用的接口
string filename = "test.cpp";
//找文件后缀
size_t pos = filename.find('.'); //找位置 rfind反着找
if (pos != string::npos)
{
string suff = filename.substr(pos);
}
return 0;
}
三、模拟实现
上文对string类进行了简单的介绍,接下来模拟实现string类的主要函数。在此之前,必须提到一个经典问题。
1. sring类的深浅拷贝问题
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");
string s2(s1);
}
上述代码会崩溃,string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
2. 浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,所以当继续对资源进项操作时,就会发生发生了访问违规。
为了解决浅拷贝问题,所以C++中引入了深拷贝。
3. 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。
显式地定义拷贝构造函数,它除了会将原有对象的所有成员变量拷贝给新对象,还会为新对象再分配一块内存,并将原有对象所持有的内存也拷贝过来。这样做的结果是,原有对象和新对象所持有的动态内存是相互独立的,更改一个对象的数据不会影响另外一个对象。
(1)传统写法的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;
};
(2)现代写法的string类
class string
{
public:
string(const char* str = "")
{
if(nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
string(const string& s)
: _str(nullptr)
{
string strTmp(s._str);
swap(_str, strTmp._str);
}
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
~string()
{
if(_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
3. 写时拷贝
写时拷贝是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
4.模拟实现完整代码
下面给出模拟实现的完整代码以及需要注意的点
#include<string.h>
#include<assert.h>
#include<iostream>
#include<string>
using std::cout;
using std::endl;
namespace zht
{
class string
{
public:
typedef char* iterator; //容器迭代器本质上是指针,通过typedef给char*重定义关键字
typedef const char* const_iterator;//迭代器需要提供const型,const 迭代器与普通迭代器在编译器处理时会进行修饰,构成了函数重载
friend std::ostream& operator<<(std::ostream& out, const string& s); //为了方便内部引用,所以要设置为友元
friend std::istream& operator>>(std::istream& in, string& s);
iterator begin() // 开始
{
return _str;
}
const_iterator begin() const //需要提供const类型迭代器,权限只能缩小不能放大,所以在处理const类型的问题时需要使用const类型的迭代器
{
return _str;
}
iterator end() //结束
{
return _str + _size; //迭代器结束实在空间的最后一位的后一个
}
const_iterator end() const
{
return _str + _size;
}
// operator&
string(const char* str = "") //构造函数,现代写法,减少创建的临时对象的个数
:_str(new char[strlen(str) + 1])
{
_size = strlen(str);
_capacity = _size;
strcpy(_str,str);
}
//void swap(string& s)
//{
// ::swap(_str,s._str);
//::swap(_size,s._size);
//::swap(_capacity,s._capacity);
//}
//开空间
void reserve(std::size_t n)
{
if(n > _capacity) //当N大于最大容量时扩容
{
char* tmp = new char[n + 1]; //创建N+1个空间,需要保存\0.
strncpy(tmp, _str, _size + 1); //将原空间中的数据拷贝到新的中
delete []_str;
_str = tmp; //更新
_capacity = n;
}
}
//开空间 + 初始化,重置capacity
void resize(std::size_t n, char ch = '\0')
{
//三情况,1.小于当前的字符串长度,2.大于字符串长度但是小于空间大小;3.大于空间大小
if(n < _size) //1.直接在n处加\0
{
_size = n;
_str[n] = '\0';
}
else
{
if(n > _capacity) //3.扩容,然后与2.合并
{
reserve(n);
}
for(std::size_t i = _size; i < _capacity; i++) //从当前字符串向后覆盖
{
_str[i] = ch;
}
_str[_capacity] = '\0';
_size = n;
}
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s) //拷贝构造函数,现代写法,通过创建一个新对象,交换,达到拷贝构造的目的
:_str(NULL)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
//binstring& operator+= (char ch)
//{
//}
string& operator=(string s) // = 运算符重载
{
swap(s);
return *this;
}
~string()
{
delete [] _str;
_str = NULL;
_size = 0;
_capacity = 0;
}
void clear()
{
_size = 0;
_str[0] = '\0';
}
//可读可写
char& operator[](std::size_t i)
{
assert(i < _size); //\0,所以闭区间
return _str[i];
}
//只读
const char& operator[](std::size_t i) const
{
assert(i < _size);
return _str[i];
}
///返回对象中的字符串,用const
const char* c_str() const
{
return _str;
}
//pos位置插入
string& insert(std::size_t pos, char ch)
{
assert(pos <= _size); //可以尾插,所以可以等于
//先判断是否需要扩容
if(_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//将数据后移
char* end = _size + _str; //从\0开始挪
while (end >= _str + pos)//pos位需要挪
{
*(end + 1) = *end; //end向后挪也就是end-1
--end; //再向前
}
*(_str + pos) = ch;
_size++;
return *this;
}
//插入字符串
string& insert(std::size_t pos,const char* str)
{
assert(pos <= _size);
std::size_t len = strlen(str);
if(_size + len > _capacity)//可能会直接大于
{
reserve(_size + len);
}
char* end = _size + _str;
while(end >= pos + _str)
{
*(end + len) = *end;
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
void push_back(char ch) //尾插字符
{
insert(_size,ch);
}
void append(const char* str) //尾插字符串
{
insert(_size, str);
}
string& operator+=(char ch) //重载+=字符
{
push_back(ch);
return *this;
}
string& operator+=(const char* str) //重载+=字符串
{
append(str);
return *this;
}
string& erase(std::size_t pos,std::size_t len = -1)
{
assert(pos < _size);
//两种情况:
//1.剩余长度小于需要删除的
//2.剩余长度大于需要删除的
std::size_t LeftLen = _size - pos;
if(LeftLen <= len) // 小于,全删除
{
_str[pos] = '\0';
_size = pos;
}
else //大于,len位向前补。
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
std::size_t find (char ch, std::size_t pos = 0)
{
assert(pos < _size);
for(std::size_t i = pos; i < _size; ++i)
{
if(_str[i] == ch)
{
return i;
}
}
return -1;
}
std::size_t find (const char* str, std::size_t pos = 0)
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str); //函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。
if(ret)
{
return ret - _str;
}
else{
return -1;
}
}
std::size_t size() const
{
return _size;
}
private:
char* _str; //字符串指针
std::size_t _size; //使用的空间大小
std::size_t _capacity; //空间大小
};
inline bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0; //strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数
}
inline bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
inline bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
inline bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
inline bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
inline bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
std::ostream& operator<<(std::ostream& out, const string& s)
//因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。
//但是实际使用中cout需要是第一个形参对象,才能正常使用。
//友元函数可以访问
{
for(auto ch : s) //使用范围for遍历字符串
{
out << ch; //输出到输出流
}
return out;
}
std::istream& operator>>(std::istream& in,string& s)
{
s.clear();
char ch;
ch = in.get();
while(ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
}
更多推荐
所有评论(0)