- 前提说明
- 特别注意构造接口模拟
- 1. 注意浅拷贝问题
- 2. 深拷贝解决
- 3. 写时拷贝解决
- 4. 不同平台的构造方法
- 模拟string的完整源码
前提说明
- 该模拟只是实现了string容器的基础、常用接口;
- 该模拟采用多文件编程,在.h头文件中实现类的定义,在.cpp源文件实现接口函数的定义;
- 为了避免与STL库中的string类起冲突,我们将该模拟string实现在一个自定义命名空间中;
- string容器在底层是以动态顺序表实现的,因此其成员变量同顺序表结构一样;
"mystring.h"头文件:string类的定义和类成员函数的声明
#pragma once //防止头文件重复包含 #include// 将模拟string类实现在自己的命名空间中 namespace mySpace { class string { // 友元函数的声明: friend std::ostream& operator<<(std::ostream& _cout, const mySpace::string& s); friend std::istream& operator>>(std::istream& _cin, mySpace::string& s); public: // 指针是天然的迭代器 typedef char* iterator; //将迭代器定义为char*指针 // 所有成员函数的声明: // 1.构造和析构 string(const char* s = ""); //带参构造函数 string(const string& s); //拷贝构造函数 string& operator=(const string& s); //赋值重载函数 ~string(); //析构函数 // 2.迭代器——仅实现正向迭代器 iterator begin()const; iterator end()const; // 3.容量 size_t size()const; //返回对象有效元素个数 size_t capacity()const; //返回对象容量大小 bool empty()const; //判断对象是否为空串 void resize(size_t n, char c = ' '); //修改对象有效元素个数 void reserve(size_t n); //修改对象容量大小 // 4.元素访问 char& operator[](size_t index); //普通对象的元素访问 const char& operator[](size_t index)const;//const对象的元素访问 // 5.修改 void push_back(char c); //对象尾插字符 string& operator+=(char c); //对象拼接字符 string& operator+=(const char* str); //对象拼接字符串 void append(const char* str); //对象拼接字符串 void clear(); //清理对象 void swap(string& s); //交换两对象 const char* c_str()const; //string对象转为char*字符串 // 6.其他 // 返回c在string中第一次出现的位置 size_t find(char c, size_t pos) const; // 返回子串s在string中第一次出现的位置 size_t find(const char* s, size_t pos = 0) const; // 在pos位置上插入字符c,并返回该字符的位置 string& insert(size_t pos, char c); // 在pos位置上插入字符串str,并返回该字符的位置 string& insert(size_t pos, const char* str); // 删除pos位置上的元素,并返回该元素的下一个位置 string& erase(size_t pos, size_t len = 1); // 运算符重载 bool operator<(const string& s); bool operator<=(const string& s); bool operator>(const string& s); bool operator>=(const string& s); bool operator==(const string& s); bool operator!=(const string& s); private: // 成员变量的定义: // string底层通过动态顺序表实现,因此其成员变量同顺序表结构一样 char* _str; //指向堆的数组 size_t _size; //有效元素大小 size_t _capacity; //堆上数组空间大小 }; } // 测试函数的声明 extern void Test_string_1();
特别注意构造接口模拟 1. 注意浅拷贝问题
这里先模拟实现一个简易的string类:
// 模拟实现string的浅拷贝问题 #includeusing namespace std; class myString { // 成员函数: public: // 1.构造函数 myString(const char* str = "") { // 防止传入空指针,导致strlen函数报错 if (nullptr == str) { str = " "; } // 为对象开辟堆上数组空间 _str = new char[strlen(str) + 1]; // 拷贝数据 strcpy(_str, str); } // 2.拷贝构造函数——使用编译器默认生成的 // 3.赋值重载函数——使用编译器默认生成的 // 4.析构函数 ~myString() { if (_str) { delete[] _str; _str = nullptr; //C++的NULL实际是整型0 } } // 成员变量:仅作测试,只有一个数组指针 private: char* _str; };
测试1:通过该类实现一个对象拷贝,一定会报错:
原因分析:
测试2:通过该类对象进行赋值,同样会报错:
原因分析:
问题解决
对于类来说,一旦涉及堆内存的管理,用户一定要显示提供:构造函数、拷贝构造函数、赋值重载函数、析构函数
2. 深拷贝解决
- 深拷贝实现拷贝构造函数、赋值重载函数
- 本质:让每一个对象拥有独立的资源;
- 下面是代码实现
普通代码:
// 模拟实现string的深拷贝 #include#pragma warning(disable:4996) using namespace std; class myString { // 成员函数: public: // 1.构造函数 myString(const char* str = "") { // 防止传入空指针,导致strlen函数报错 if (nullptr == str) { str = " "; } // 为对象开辟堆上数组空间 _str = new char[strlen(str) + 1]; // 拷贝数据 strcpy(_str, str); } // 2.拷贝构造函数 // 为新对象在堆上开辟一段新空间 myString(const myString& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str, s._str); } // 3.赋值重载函数 myString& operator=(const myString& s) { // 避免自我赋值 if (this != &s) { // 先通过临时变量开辟新空间 // 防止赋值失败导致原数据丢失 char* temp = new char[strlen(s._str) + 1]; strcpy(temp, s._str); delete[]_str; _str = temp; } return *this; } // 4.析构函数 ~myString() { if (_str) { delete[] _str; _str = nullptr; //C++的NULL实际是整型0 } } // 成员变量:仅作测试,只有一个数组指针 private: char* _str; };
进阶代码:
- 上面的普通代码其实重复操作非常多,代码冗余;
- 通过swap函数,巧妙复用代码,提高代码简洁性;
先实现一个string类的swap成员函数:
// 5.交换函数——借助std自带的swap函数
void String_swap(myString& s)
{
std::swap(_str, s._str);
}
优化拷贝构造函数:
// 2.1 拷贝构造函数的优化
myString(const myString& s)
:_str(nullptr) //_str初始必须赋nullptr,否则交换后的临时对象temp释放时会出错
{
// 步骤1:
// 调用构造函数,实例化临时对象temp
// 且对象temp的数据同对象s
myString temp(s._str);
// 步骤2:
// 交换this对象和temp对象的内容
// 临时对象temp会自动析构
String_swap(temp);
}
优化赋值重载函数:
// 3.1 赋值重载函数的优化
// 第一种:同拷贝构造函数的优化思想一样
myString& operator=(const myString& s)
{
if (this != &s)
{
myString temp(s._str);
String_swap(temp);
}
return *this;
}
// 第二种:巧妙利用值传递,传值传参自动调用构造函数生成临时对象
myString& operator=(myString s)
{
// 形参对象的地址肯定与原对象不是同一个
// 因此无需判断是否自我赋值
String_swap(s);
return *this;
}
【注】后面的string模拟类还是使用普通方法,方便理解
3. 写时拷贝解决
只涉及写时拷贝概念,具体实现暂时不谈
- 概念:写时拷贝就是若两对象相同,且不修改,那么共用一个数据内存,并延迟析构;
- 实现:在浅拷贝基础上,增加一个计数器(用来指示该资源被调用的个数);
- 优点:
注意:
- 如果共享的资源需要修改时,就需要给修改的对象创建一个新的资源,以避免影响其他共享原资源的对象内容;
- 写时拷贝就是当修改时才进行拷贝(创建空间);
- 并不推荐大量采用写时拷贝机制,因为涉及线程安全问题;
4. 不同平台的构造方法
验证思路:
- 实例化一个大小>15的string对象;
- 拷贝构造一个新的string对象;
- 打印两对象地址,观察是否一致:
若地址不同:采用深拷贝;
若地址相同:采用写时拷贝;
测试代码:
#include#include #include using namespace std; // 测试是否深拷贝 void TestCopy() { string s1(20, 'a'); string s2(s1); // 成员_str为私有,无法直接访问 //printf("&s1: %pn", s1._str); // 可通过string的c_str函数 printf("&s1: %pn", s1.c_str()); printf("&s2: %pn", s2.c_str()); } int main() { TestCopy(); return 0; }
- VS的R.J.版本STL按照深拷贝方式实现string类:
- Linux的SGI版本的STL按照写时拷贝方式实现string类:
模拟string的完整源码
string模拟实现-git



