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

C++ 同时被 3 个专栏收录
17 篇文章 2 订阅
9 篇文章 0 订阅
4 篇文章 1 订阅

本文介绍了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_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+ endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
范围forC++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;
  }

}  

  • 9
    点赞
  • 3
    评论
  • 7
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论3
请先登录 后发表评论~
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页

打赏作者

TT在长大

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值