C++|[C/C++]详解STL容器1--string的功能和模拟实现(深浅拷贝问题)

本文介绍了string类的常用接口的使用,并对其进行了模拟实现,对模拟实现中涉及到的深浅拷贝问题进行了解析。
目录
一、string类
1. C语言中的字符串
2. C++中的string类
二、string类的常用接口的使用
1. string类对象的常见构造
2. string类对象的容量操作
3. string类对象的访问及遍历操作
4.string类对象的修改操作
5. string类非成员函数
6.使用实例
三、模拟实现
1. sring类的深浅拷贝问题
2. 浅拷贝
3. 深拷贝
(1)传统写法的string类
(2)现代写法的string类
3. 写时拷贝
4.模拟实现完整代码
一、string类 1. C语言中的字符串 在C语言中,字符串是以'\0'结尾的一些字符的集合,C标准库还提供了str系列的库函数,但是这些库函数与字符串不太符合OOP的思想,底层空间需要用户自己管理,可能会造成越界访问。
2. C++中的string类 C++ 大大增强了对字符串的支持,除了可以使用C风格的字符串,还可以使用内置的 string 类。string 类处理起字符串来会方便很多,完全可以代替C语言中的字符数组或字符串指针。
string是表示字符串的字符串类,该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。不能操作多字节或者变长字符的序列。在底层实际是:basic_string模板类的别名,typedef basic_stringstring;
二、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<

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 <

三、模拟实现 上文对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.模拟实现完整代码 【C++|[C/C++]详解STL容器1--string的功能和模拟实现(深浅拷贝问题)】下面给出模拟实现的完整代码以及需要注意的点
#include #include #include #includeusing 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,则返回零;若str1str2,则返回正数 }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; }}


    推荐阅读