C++入门篇string的模拟实现

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++入门篇string的模拟实现相关的知识,希望对你有一定的参考价值。

文章目录

前言

本篇文章讲解的内容主要是深浅拷贝string类的实现.


深浅拷贝

概念:

**浅拷贝:**只将对象的值拷贝过来,存在一定的隐患.

**深拷贝:**给每个对象单独分配资源,一般对象涉及资源管理都会用深拷贝.

什么意思呢?

我们假设自己定义一个类,如下:

 class String

public:
	String(const char* s = "123456"): 
	:_size(strlen(_s)),_capacity(_size)
     
        _s(new char[strlen(s)]+1);
        _str[_size] = '\\0';
        strcpy(_s,s);
	
	~String()
	
		delete[] _s;
		_size = _capacity = 0;
	
private:
	char* _s;
	int _size;
	int _capacity;
;

如果我们使用该类创建对象,然后再释放资源,看看会怎样?

int main()

    String s1;
	String s2 = s1;
    return 0;
 


编译器会提示异常,触发断点,而这个原因就是因为浅拷贝.我们一开始创建了对象s1并给其赋值"123456",然后创建了对象s2并对其拷贝构造,在这个过程中,我们把s1的各个成员的值全对应的给了s2的成员.也就是说s1的_s和s2的_s指向的是同一块空间,当main函数结束时,s2先调用析构函数,对其_s进行释放,然后s1又调用析构函数,在对其_s进行释放,而同一块空间是不能释放两次的,因此产生错误.

那我们怎么解决呢? 答:深拷贝,也就是每个对象都有单独的属于自己的一份空间.

因此该类的拷贝构造函数应该如下:

String(const String& t)
    :_size(strlen(_s)),_capacity(_size);   //确定一下大小

        _s(new char[strlen(t._s)+1]);    //然后给_s开辟一块空间,注意哦~,我这里多开了一块空间,是为了储存\\0
        strcpy(_s,t._s);             //然后把值拷贝过去.
        _str[_size] = '\\0';

该类的赋值运算符=重载如下:

String& operator=(String& t)

    if(this != &t)          //如果是同一个对象,自己赋值给自己就没有什么意义
        char* tmp = new char[strlen(t._s)+1];   //先开辟空间,然后用一个临时变量接收.
        delete[] _s;         //直接删除this原来的资源,然后重建一个和t._s一样大的空间
        _s = tmp;            //_s再接收
        strcpy(_s,t._s);
        _size = _capacity = strlen(t._s);
        _str[_size] = '\\0';
    
    return *this;

上面的两个例子都是为了解决浅拷贝问题而进行设计的,其写法是没有问题的(传统写法),但是我们更喜欢一种现代写法,

String(const String& t):_s(nullptr)   //必须有这一置空操作,因为_s开始是个随机数,交换给tmp._s后,被释放会引起大问题

    String tmp(t._s);                 //直接利用构造函数,给tmp对象开辟了一块空间,并把值穿进去.
    swap(_s,tmp._s);                  //然后交换_s和tmp._s,就相当于给this->_s开辟了一块空间,当拷贝函数结束,tmp就会被自动释放.


String& operator=(String t)      //注意一下哦~~~,我的形参写的是String t,本质上是利用的拷贝构造

    swap(_s,t._s);       //交换资源
    return *this;

咦~?,为啥线代写法的operator=操作不需要把_s初始化为nullptr?因为这是赋值操作,即对象都互相存在,也就是说_s本身是有资源的,交换后并不会造成对象t释放时候,其内部会释放一个随机的地址.

讲解完深浅拷贝的原理和应用以后,我们就开始string类的实现吧.


string的实现

结构定义

我们在上一节讲解了string的使用,其中涉及到长度,容量操作时候,博主说明了string的底层实现其实是顺序表.那么其结构大致如下:

namespace mystring  //因为string系统已经有了,为了防止重名,博主便重新定义了一块命名空间

	class string
	
	public:

	private:
		char* _str;
		int _size;
		int _capacity;
	;


构造函数

在讲解string的使用时候,博主为大家演示过几个比较常用的构造函数,我们在这里实现的话,也就实现几个比较常用的吧:

  • string (const char* s); / /接收c格式的字符串

  • string (size_t n, char c); / /接收n个字符c

  • string() / /空字符串.

接收c格式的字符串,代码如下:

string(const char* s)
    :_size(strlen(s))   //给_size有效长度

    _str = new char[_size+1];      //给_str一个strlen(s)+1长度的空间
    strcpy(_str,s);               //拷贝字符串
    _capacity =  _size;           //更新容量
    _str[_size] = '\\0';

接收n个字符c,代码如下

string(size_t n,char c)
    :_size(n)     //更新有效长度

        _str = new char[_size+1];  //给_str一个n+1长度的空间
        for(int i= 0;i<n;i++) _str[i] = c;
        _size = _capacity = n;   //更新capacity

空字符串,代码如下:

string()
    :_str(new char[1])

	*_str = '\\0';
    _size = _capacity = 1;


拷贝构造

博主在最上面讲解深浅拷贝时候,已经说明了该函数,因此,博主这里便直接贴代码了

string(const string& t):_str(nullptr)  //必须有这一置空操作,因为_str开始是个随机数,交换给tmp._str后,被释放会引起大问题

    string tmp(t._str);                //直接利用构造函数,给tmp对象开辟了一块空间,并把值穿进去.
    swap(_str,tmp._str);   //然后交换_s和tmp._s,就相当于给this->_s开辟了一块空间,当拷贝函数结束,tmp就会被自动释放.
    swap(_size,tmp._size);
    swap(_capacity,tmp._capacity);

我们已经知道,拷贝构造需要深拷贝,而赋值也需要深拷贝,但是现代写法需要交换,因此博主便把交换操作封装一下,然后拷贝构造代码如下:

void Swap(string& a,string& b)

    swap(a._str,b._str);
    swap(a._size,b._size);
    swap(a._capacity,b._capacity);


string(const string& t):_str(nullptr)  //必须有这一置空操作,因为_str开始是个随机数,交换给tmp._str后,被释放会引起大问题

    string tmp(t._str);                //直接利用构造函数,给tmp对象开辟了一块空间,并把值传进去.
	Swap(*this,tmp);


析构函数

直接释放资源即可

~string()

    delete[] _str;
    _str = nullptr;
    _size = _capacity = 0;


赋值重载

深浅拷贝章节,博主已经讲解了,便直接贴代码:

string& operator=(string t)

    Swap(*this,t);  //注意哦,博主调用的是Swap,而不是swap.
    return *this;


[]访问

在c++中,string的operator[]有两个重载,分别是:

  • char& operator[] (size_t pos); //支持读和写
  • const char& operator[] (size_t pos) const; //只支持读

所以支持读和写的重载如下:

char& operator[] (size_t pos)
    return _str[pos];

只支持读如下:

const char& operator[] (size_t pos) const
    return _str[pos];


改变容量

  • 大家还记得reserve吗?当给他传入一个n,且n大于该对象的capacity时候,才会增容.
void reserve(size_t n)

    if(n > _capacity)
        char* tmp = new char[n+1];        //注意哦~,这里必须要多开一个空间存储\\0
        strcpy(tmp,_str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
        _str[n] = '\\0';                  //多开的那个空间存储位置
    

  • 而相比于reserve来说,resize的功能就比较多了,且其分为三种情况
    • 当n小于size,则size等于n.
    • 当n>size但是小于capacity时,size仍等于n,但是这个时候即使你传入另一个参数ch,也没有用
    • 当n大于size时候,会增容,然后多出的空间会用ch初始化,ch如果不传,就是\\0,最后size等于n,
void resize(size_t n, char ch = '\\0')

    if (n > _capacity) reserve(n);                               //大于容量时,需要增容
    for (size_t i = _size; i < n; i++) _str[i] = ch;             //只有当n大于size时才起作用
    _size = n;                     //最后size等于n.
    _str[_size] = '\\0';            //给字符串加一个\\0

有人可能会问了,不说说分三种情况吗? 其实这里博主做了一个精简的处理,因为无论什么情况,都会有size等于n的操作,所以博主把这一步直接放到了外面.


字符和字符串拼接

对于字符和字符串拼接,用的做多有以下几个:

  • +=
  • push_back
  • append
  • void push_back(char c); //拼接字符

对于拼接字符操作来说,本质是上给数组中第size位置赋值,那么一定会遇到容量是否充足问题,所以是需要考虑此方面问题的:

void push_back(char c)

    if(_size >= _capacity)      
		int newcapacity =  _capacity == 0? 4 : _capacity*2;       //这一步是为了防止_capacity如果为0,开不了空间
		reserve(newcapacity);
    
    _str[_size] = c;
    _size++;
    _str[_size]= '\\0';      //给字符串末尾加\\0

而拼接字符串则有比较多的重载,博主这里直接写一个比较常用的(拼接字符串或者拼接string对象)

  • void append(const char* s)

  • void append(const string s)

代码如下:

void append(const char* s)
    int len = strlen(s);
    if(_size + len > _capacity )
    
        reserve((_size + len)*2);  //开大点
    
    strcpy(_str+_size,s);
    _size += len;
    _str[_size] = '\\0';


//同理,另一个重载只需要把char* 改成string就行

有了上面写好的push_backappend函数,那么我们便可以直接复用他们进行实现operator+=

string& operator+=(char c)

    push_back(c);
    return *this;


string& operator+=(const char* s)

    append(s);
    return *this;


查询大小

这里的大小有两个接口,分别是size和capacity.

size_t size() const 

    return _size;


size_t capacity() const

    return _capacity;

迭代器

我们在上一章节string的使用时候,就讲解了迭代器目前阶段是可以看作为指针的,而实际上在内部实现string时候,也差不多就是指针.

typedef char* iterator;
typedef const char* const_iterator;

iterator begin() return _str;
iterator end() return _str+_size;
iterator begin() const return _str;
iterator end() const return _str+_size;

现在我们再看看,之前是怎么使用迭代器的:

mystring::string s("123456");
mystring::string::iterator itb = s.begin();
mystring::string::iterator ite = s.end();
while(itb!=ite)

    cout<<*itb<<" ";
    itb++;

对照一下迭代器的使用方式和实现方式,是不是现在能够理解了呢?


插入和删除

  • insert(),有两个重载,分别是在某位置插入字符和字符串
void insert(size_t pos, const char c)

    if (_size == _capacity)         //检查容量
        int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
    
    
    for (int i = _size; i > pos; i--) _str[i] = _str[i - 1];   //把pos位置后的数据集体移动一位
    _str[pos] = c;   //插入数据
    _size++;
    _str[_size] = '\\0';   //给末尾加上\\0


void insert(size_t pos, const char* s)     

    size_t len = strlen(s);
    if (len + _size > _capacity) 
        reserve(len + _size);
    
    for (int i = _size + len - 1; i > pos + len - 1; i--)     //把pos到pos+len-1位置的数据集体后移strlen(s)位
        _str[i] = _str[i - len];
    

    for (int i = pos + len - 1, j = len - 1; j >= 0; j--, i--)    //放入数据
        _str[i] = s[j];
    
    _size += len;
    _str[_size] = '\\0';

  • erase(),从pos位置开始删除,len个,如果len不写,末尾删除pos后的全部
void erase(size_t pos, size_t len = npos)     //npos的定义为 size_t npos = -1;

    if (len == pos || pos + len >= _size) 
        _size = pos, _str[pos] = '\\0';
        return;
    

    for (int i = pos; i < _size; i++) 
        _str[i] = _str[i + len];
    
    _size -= len;
    _str[_size] = '\\0';


clear()

void clear()

    _str[0] = '\\0';
    _size = 0;

以上是关于C++入门篇string的模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

C++入门篇之内存处理

C++入门篇之内存处理

C++ 中string类的三种模拟实现方式

C++从入门到入土第十一篇:string模拟实现(续)

C++从入门到入土第十篇:string模拟实现

[ C++ ] string类常见接口及其模拟实现