前提说明
- 该模拟只是实现了
string容器
的基础、常用接口; - 该模拟采用多文件编程,在
.h
头文件中实现类的定义,在.cpp
源文件实现接口函数的定义; - 为了避免与STL库中的
string
类起冲突,我们将该模拟string实现在一个自定义命名空间中; string
容器在底层是以动态顺序表实现的,因此其成员变量同顺序表结构一样;
"mystring.h"头文件
:string类的定义和类成员函数的声明
#pragma once //防止头文件重复包含
#include <iostream>
// 将模拟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 = '\0'); //修改对象有效元素个数
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的浅拷贝问题
#include<iostream>
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.拷贝构造函数——使用编译器默认生成的
// 3.赋值重载函数——使用编译器默认生成的
// 4.析构函数
~myString()
{
if (_str)
{
delete[] _str;
_str = nullptr; //C++的NULL实际是整型0
}
}
// 成员变量:仅作测试,只有一个数组指针
private:
char* _str;
};
测试1:通过该类实现一个对象拷贝,一定会报错:
原因分析:
测试2:通过该类对象进行赋值,同样会报错:
原因分析:
问题解决
对于类来说,一旦涉及堆内存的管理,用户一定要显示提供:构造函数
、拷贝构造函数
、赋值重载函数
、析构函数
2. 深拷贝解决
- 深拷贝实现
拷贝构造函数
、赋值重载函数
- 本质:让每一个对象拥有独立的资源;
- 下面是代码实现
普通代码:
// 模拟实现string的深拷贝
#include<iostream>
#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<iostream>
#include<string>
#include<cstdio>
using namespace std;
// 测试是否深拷贝
void TestCopy()
{
string s1(20, 'a');
string s2(s1);
// 成员_str为私有,无法直接访问
//printf("&s1: %p\n", s1._str);
// 可通过string的c_str函数
printf("&s1: %p\n", s1.c_str());
printf("&s2: %p\n", s2.c_str());
}
int main()
{
TestCopy();
return 0;
}
- VS的
R.J.版本STL
按照深拷贝方式实现string
类:
- Linux的
SGI版本的STL
按照写时拷贝方式实现string
类: