C++的STL标准模板库容器--string类

目录

浅浅介绍一下什么是STL:

string类需要知道的小知识

auto和范围for:

string类的常用接口:

实现一个string类:

[1. 成员变量和构造函数,拷贝构造,析构函数](#1. 成员变量和构造函数,拷贝构造,析构函数)

[2. string类对象的容量操作](#2. string类对象的容量操作)

<1>size,capacity,empty,clear(初始化)

<2>reserve(扩容),resize

[3. string类对象的访问和遍历](#3. string类对象的访问和遍历)

[4. string类对象的修改操作](#4. string类对象的修改操作)

[<1>push_back(尾插), append, operator+=](#<1>push_back(尾插), append, operator+=)

<2>insert(插入),erase(删除),operator=(赋值)

<3>c_str(返回C格式字符串)和find(查找)

<4>substr(复制子字符串)

[5. string类非成员函数](#5. string类非成员函数)


浅浅介绍一下什么是STL:

STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。STL有六大组件:仿函数,算法,迭代器,空间适配器,容器,配接器

本篇讲的就是STL容器部分的string类

string类需要知道的小知识

auto和范围for:

(1)在本篇的学习中将使用到关键字auto,它的作用是自动识别类型

注意:

1.使用auto声明指针类型,auto和auto*没有任何区别,但如果修饰引用类型则必须加上&

2.auto在对同一行声明多个变量时,变量必须是相同的类型,否则会报错,因为编译器实际只能对第一个类型进行推导,然后用推导出来的类型定义其他变量

//会出现编译报错:在声明符列表中,auto必须始终推导为同一类型
auto a = 1, b = 2.1;//在这之前已经推导出了int

3.auto不能够作为函数的参数,但可以作为返回值,想设为返回值请谨慎使用

4.auto不能设为数组

(2)范围for
C++11中引入了基于范围的for循环,for循环后的括号由冒号" :"分为两部分:第一部分是范围
内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。

1.对于一个知道范围的集合而言,就可以使用范围for

2.范围for可以作用到数组和容器上进行遍历

3.范围for的底层实际上就是替换为迭代器

int main()
{
    int a[]={1,2,3,4,5};
    for(auto& a1:a)
    {
        cout<<a1<<" ";
    }
    return 0;
}

string类的常用接口:

string是一个比较早的容器所以在部分设计上会出现冗余的情况,我只会讲一些比较常用的接口,其他的接口请自行查看文档

string类接口详细文档传送门:string - C++ 参考 (cplusplus.com)

1.string类对象的常见构造

string() //构造出一个空的string对象

string(const char* ch) //使用一个字符串进行构造对象

string (size_t n,char c) //string类对象中包含n个字符c,这个基本不实现

string (const string& st) //拷贝构造

2.string类对象的容量操作

size //返回字符串有效长度

capacity //返回空间总大小

empty //判断字符串是否为空

clear //清空字符串

reserve //空间扩容

resize ​​​​​​//将字符串大小调整为n个字符的长度,多出的空间用字符c填充

3.string类对象的访问和遍历

operator[] //返回pos位置的字符引用

begin //获取字符串起始位置字符的迭代器

end //获取字符串最后一位的下一位置的迭代器

4.string类对象的修改操作

operator=//将一个对象赋值给另一个对象

push_back //尾插一个字符或者字符串

append //在字符串后追加一个字符串
operator+= //在字符串后面追加一个字符或者字符串

c_str //返回C格式字符串

find+pos //在起始位置加上pos后的下标开始往后查找字符或者字符串出现的起始位置

substr //从对象的字符串的pos位置开始向后截取n个字符,然后返回

insert//在pos位置上插入一个字符或者字符串

5.string类非成员函数

operator>> //输入重载

operator<< //输出重载

实现一个string类:

这里都是我自己实现各项功能,实现参数部分可能会和文档有差异,请见谅

1. 成员变量和构造函数,拷贝构造,析构函数
string类主要有三个成员变量,还会设置一个静态成员常量,分别是指向数组的指针char*,记录有效字符数的_size,和容量_capacity,静态成员常量npos

构造函数上我们不使用初始化列表,参数设成一个带有缺省值的char*指针,这样可以同时解决单字符初始化,字符串初始化,空字符串对象三种初始化

注意:

1.npos固定为-1,因为在参数里会使用它作为缺省值,无符号整形的符号位无效所以当缺省值有效时它的大小是:4294967295。它的作用就是给一个很大的值,在后面的函数实现中erase(删除)和substr(复制子字符串)会使用到

2.字符串的结尾都会跟上一个'\0'它也是占空间的,但记录容量的_capacity并不会记录这个空间,所以在new开空间的时候要额外增加一个空间给'\0',如果不开这个空间就有两个结果:

<1>不在字符串尾部增添'\0'导致循环越界访问直至遇到'\0';

<2>增添'\0'的位置已经被别的变量申请走了,越界增添'\0'导致破坏了其他资源的完整性

class string
{
public:
    string(const char*ch="")    //注意这里给的缺省值是没有有效字符的哦
    {
        size_t size=strlen(ch);
        _str=new char[size+1];  //加1为'\0'开空间
        strcpy(_str,ch);
        _size=_capacity=size;
    }
private:
	char* _str;
	size_t _size;
	size_t _capacity;
	static const int npos = -1;
}

拷贝构造现在也不应该是浅拷贝,因为出现了空间申请,需要使用深拷贝,现在我们实现一个swap(交换函数)来辅助进行拷贝构造的实现。

注意:直接使用库里的swap对对象(参数是对象)也可以完成交换,但是和我自己实现交换过程有很大差别,库里的实现是对对象的拷贝再交换,这样会导致频繁的触发拷贝构造(深拷贝)导致降低效率,所以不直接交换对象,交换成员变量

出现了资源申请析构函数也不能使用自动生成的了,需要自己实现

void swap(string& ch)
{
	std::swap(_str, ch._str);            //交换指针
	std::swap(_size, ch._size);          //交换大小
	std::swap(_capacity, ch._capacity);  //交换容量
}
string(const string& str)
{
    //std::swap(*this,str);   频繁触发拷贝构造会出现效率降低的问题
	string temp(str._str);    //这里是用str的字符串初始化一个string对象
	swap(temp);               //现在交换temp和this指针指向的对象
}
~string()
{
    if(!empty())
    {
	    delete[] _str;
	    _str = nullptr;
	    _size = _capacity = 0;
    }
}
2. string类对象的容量操作
<1>size,capacity,empty,clear(初始化)

这四个都是比较简单的成员函数,直接给大家看实现,一看很快就能理解了

size_t size()        //返回有效字符个数
{
    return _size;
}
size_t capacity()    //返回容量大小
{
    return _capacity;
}
bool empty()         //判空
{
    return _size==0;
}
void clear()
{
    _str[0]='\0';    //这里要重载[]返回当前位置的引用才能直接修改
    _size=0;
}
<2>reserve(扩容),resize

剩下的这两个,reserve负责扩容比较耳熟能详;resize它能调整字符串的长度,如果给的n大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展,直到达到n的长度,如果给的n小于当前长度,那么缩短为当前n个字符,并且删除第n个字符(包括n)以外的字符

下面直接看实现来理解更方便:

void reserve(size_t n)
{
    if(n>_capacity)
    {
        char* newstr=new char[n+1];    //这里也不要忘了'\0'的位置
        strcpy(newstr,_str);           //拷贝一下旧空间
        delete[] _str;                 //删除旧空间
        _str=newstr;                   //指向新空间
        _capacity=n;                   //更新容量 
    }
}
void resize(size_t n, char c='0')    
{
	if (n > _capacity)                 //如果容量不够就扩容
	{
		reserve(_capacity * 2);
	}
	if (n < size())                    
	{
		_str[n] = '\0';                //小于的话直接在n位置加'\0'
		_size = n;                     //更新有效字符数
	}
	else
	{
		for (int i = size(); i <= n; i++)    
		{
			_str[i] = c;     
		}
        _size=n;            //更新_size
        _str[_size]='\0';   //不要忘了'\0'
	}
}
3. string类对象的访问和遍历

这里就会涉及到迭代器的使用了。迭代器的主要目的是为了屏蔽底层细节,让代码有更好封装性和模块化,能够让使用者不要直接面对复杂的底层内部实现,只需要通过简化后的接口进行操作,使其更容易使用和理解,它就是封装的一种体现方式

class string
{
public:
	typedef char* iterator;    //自定义命名char*
	iterator begin()    //返回字符串起始位置的迭代器
	{
		return _str;    
	}
	iterator end()     //返回字符串末尾下一位的迭代器
	{
		return _str+_size;
	}
    const iterator begin()const    //const迭代器
    {
	    return _str;
    }
    const iterator end()const
    {
	    return _str + _size;
    }
}

operator[]:重载方括号方便字符串的访问和修改

char& operator[](size_t pos)    //运算符重载
{
    assert(pos < _size);        //当前下标必须有效
    return _str[pos];           //返回当前下标字符的引用 
}
4. string类对象的修改操作
<1>push_back(尾插), append, operator+=

这三个成员函数在实现上都有着十分相似的情况,甚至可以相互复用所以直接放到一起讲

void push_back(const char ch)
{
	if (_size == _capacity)    //判断容量是否足够
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}	
    _str[_size++] = ch;    //尾插一个字符
	_str[_size] = '\0';
}

string& operator+=(const char ch)    //复用push_back完成尾插一个字符
{
	push_back(ch);        
	return *this;
}

string& operator+=(const char* ch)
{
	size_t len = strlen(ch);        //计算字符串长度
	if (_size+len > _capacity)      //判断容量是否足够
	{
		reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
	}
	memcpy(_str + _size, ch, sizeof(char) * len);    //拷贝空间
	_size += len;                                    //更新_size
	_str[_size] = '\0';                              //尾部增添'\0'  
	return *this;        
}

void append(const char* str)      //在字符串后面增添字符串
{
	*this+=str;    //调用operator+=
}

void push_back(const char* ch)    //这个和append就一样
{
	*this += ch;    //调用operator+=
}

在实现完后就可以发现这些函数的功能重叠了好多,append和push_back尾插字符串功能就重叠了,这就是因为比较早出现的原因出现的设计冗余

<2>insert(插入),erase(删除),operator=(赋值)

insert(插入)在代码上容量扩容方面和上面基本一致,但注意不要把insert和前面的功能搞混,insert是插入字符串中的有效字符,上面的三个都是尾插需要在末尾手动添加'\0',假设调用insert时pos位置是尾巴,在代码段末端增加'\0'的代码那就和上面的功能无异,那pos在中间时那不就暴毙了,搁中间加'\0'那就错了,这也是为什么尾插无法复用insert,所以单独拉出来讲

void insert(size_t pos, char ch)    //在pos位置插入一个字符
{
	assert(pos <= _size);      //pos位置必须有效
	if (_size == _capacity)     //判断容量是否足够
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	size_t end = _size + 1;    //找到'\0'位置的下一个位置
	while (end >= pos)         //把pos位置后的字符(包括'\0')全部后移
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;            //插入字符
	_size++;                   //_size加1 
}

void insert(size_t pos, const char* ch)
{
	assert(pos <= _size);            //pos位置必须有效
	size_t len = strlen(ch);         //计算插入字符串长度
	if (_size + len > _capacity)     //容量大小是否足够
	{
		reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
	}
	int end = _size + len;           //'\0'位置加上字符串长度  
	while (end >= pos + len )        //将包括'\0'的字符全部后移
	{
		_str[end] = _str[end - len];
		end--;
	}
	memcpy(_str + pos, ch, sizeof(char) * len);    //直接拷贝进去
	_size += len;                    //_size增加插入字符串长度
}

erase(删除)的主要作用:要求在pos位置开始向后删除len个字符,len有缺省值,当缺省值有效时删除pos位置包括pos位置往后的所有字符,因为len此时超级大

void erase(size_t pos, int len=npos)
{
	assert(pos < _size);        //pos位置必须有效
	if (len >= _size - pos)     //如果删除字符数大于剩下字符数
	{
		_str[pos] = '\0';       //直接pos位置给'\0'
		_size = pos;            //更新_size
	}
	else
	{
		for (size_t i = pos + len; i <= _size; i++)    //将pos+len的数据前移
		{ 
			_str[i - len] = _str[i];
		}
		_size -= len;    //更新_size
	}
}

operator=(赋值):这个就很简单了,使用赋值对象构造一个对象,然后交换被赋值对象和构造出来的对象即可

string& operator=(string temp)    //这里不用引用直接传参触发拷贝构造
{
	swap(temp);                   //交换(形参不影响实参)
	return *this;
}
<3>c_str(返回C格式字符串)和find(查找)

在没有重载输出(cout)之前 cout<<st1(string类对象) 肯定是不行的,因为它是一个对象并不是字符串,c_str就是专门解决这个问题,它会返回字符串的起始位

const char* c_str()
{
	return _str;
}
//没重载cout之前 cout<<st1<<endl; 无法打印字符串
//cout<<st1.c_str()<<endl; 这样就可以了

find(查找):返回在pos位置往后查找字符或者字符串出现的第一位置的下标,

查找一个字符:直接for循环遍历查找,找到返回下标

查找字符串:调用strstr函数直接查找字符串出现第一个位置的指针,通过这个指针减去起始位就是下标

下面看实现:

size_t find(char ch, size_t pos)
{
	assert(pos < _size);        //pos位置必须有效
	for (size_t i = pos; i < _size; i++)
	{
		if(_str[i]==ch)
		{
			return i;          //返回下标
		}
	}
}
	
size_t find(const char* ch, size_t pos)
{
	assert(pos < _size);
	const char* fin = strstr(_str + pos, ch);  //找到并返回出现的第一个位置的指针
	if (fin == nullptr)        //空代表没找到
	{
		return npos;           //返回npos
	}
	else
	{
		return fin - _str;     //当前指针减去起始位置得出下标返回
	}
}
<4>substr(复制子字符串)

substr的主要作用:要求指定位置开始往后截取n个字符并返回,如果n没有给指定的长度那么缺省值有效,默认从指定位置开始向后截取全部字符返回,下面看实现理解更方便

string substr(size_t pos, size_t len=npos)
{
	assert(pos < _size);        //依旧是pos位置必须有效
	//len大于剩余长度,更新len
	if (len > _size-pos)        
	{
		len = _size-pos;
	}
	string sub;                        //构造一个新的string类对象
	sub.reserve(len);                  //给len长度的空间
	for (size_t i = 0; i < len; i++)   //从pos位置开始向后截取len次字符 
	{
		sub += _str[pos + i];          //尾插
	}
	return sub;    //返回当前类对象
}
5. string类非成员函数

比较主要的就是重载输入和输出了,这个比较简单直接看实现来理解

ostream& operator<<(ostream& out, const string& s)
{
    for(auto ch:s)    //使用范围for自动识别类型,自动迭代,自动判断停止
    {
        cout<<ch;
    }
    out << endl;
    return out;
}
istream& operator>>(istream& in, string& s)
{
    s.clear();        //输入自然要对当前对象进行初始化
    char ch;
    ch=in.get()
    while(ch != ' ' && ch != '\n')    //这个是判断什么时候停下来,我假设
    {                                 //遇到空格和换行就停止
        s+=ch;
        ch=in.get();
    }
    return in; 
}

本篇文章到这里主要内容已经讲完了,希望能够对你产生帮助,感谢阅读

需要看全部实现可以往下翻下面有完整实现


我自己实现的一个string类

//头文件
#include <iostream>
#include <assert.h>
using namespace std;

namespace cool
{
	class string
	{
	public:
		//简易迭代器
		typedef char* iterator;
		iterator begin()               //普通迭代器
		{
			return _str;
		}
		iterator end()
		{
			return _str+_size;
		}
		const iterator begin()const    //const迭代器
		{
			return _str;
		}
		const iterator end()const      
		{
			return _str + _size;
		}
		string(const char*str="")      //构造
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}		    
		~string()                      //析构     
		{
			delete[] _str;
			_str = nullptr;
			_capacity = _size=0;
		}
		void swap(string& str)         //交换函数
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}
		string(const string& str)      //拷贝构造
		{
			string temp(str._str);
			swap(temp);
		}
		const char* c_str()const       //返回C格式字符串
		{
			return _str;
		}
		size_t size()const             //返回有效个数
		{
			return _size;
		}
        bool empty()                   //判空
        {
            return _size==0;
        }
		void clear()                   //初始化
		{
			_str[0] = '\0';
			_size = 0;
		}
		void reserve(size_t n)         //扩容
		{
			if (n > _capacity)
			{
				char* temp = new char[n+1];
				strcpy(temp, _str);
				delete[]_str;
				_str = temp;
				_capacity = n;
			}
		}
        void push_back(char ch);                  //尾插
        void push_back(const char* ch);          //尾插的函数重载

		string& operator+=(const char ch);        //重载+=
		string& operator+=(const char* ch);       //+=的函数重载
		string& operator=(string temp);           //重载=

        char& operator[](size_t pos)              //重载[]
		const char& operator[](size_t pos)const;  //const类型的[]重载

		void append(const char* str);             //字符串后增加一个字符串

		void insert(size_t pos, char ch);         //插入
		void insert(size_t pos, const char *ch);  //插入的函数重载  
		void erase(size_t pos,int len=npos);      //删除  

		size_t find(char ch, size_t pos = 0);           //查找 
		size_t find(const char* ch, size_t pos = 0);    //查找的函数重载
		string substr(size_t pos = 0, size_t len=npos); //复制一个子字符串   
	private:

		char* _str=nullptr;
		size_t _size=0;
		size_t _capacity=0;
		static const size_t npos=-1;
	};

    //重载输入输出
	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);

    void test();
    void test1();
    void test2();
    void test3();
    void test4();
}

//实现文件
namespace cool
{
	void string::push_back(const char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size++] = ch;
		_str[_size] = '\0';
	}

	void string::push_back(const char* ch)
	{
		*this += ch;
	}

    string& string::operator+=(const char ch)
    {
	    push_back(ch);
	    return *this;
    }

    string& string::operator+=(const char* ch)
    {
	    size_t len = strlen(ch);
	    if (_size+len > _capacity)
	    {
	    	reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
	    }
	    memcpy(_str + _size, ch, sizeof(char) * len);
	    _size += len;
	    _str[_size] = '\0';
	    return *this;
    }

    string& string::operator=(string temp)
    {
    	swap(temp);
    	return *this;
    }
    
    char& string::operator[](size_t pos)
	{
		assert(pos < _size);
		return _str[pos];
	}

	const char& string::operator[](size_t pos)const
	{
		assert(pos < _size);
		return _str[pos];
	}
    
    void string::append(const char* str)
    {
	    *this+=str;    
    } 
    
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		size_t end = size() + 1;
		while (end >= pos)
		{
			_str[end] = _str[end - 1];
			end--;
		}
		_str[pos] = ch;
		_size++;
	}

	void string::insert(size_t pos, const char* ch)
	{
		assert(pos <= _size);
		size_t len = strlen(ch);
		if (_size + len > _capacity)
		{
			reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
		}
		size_t end = _size + len;
		while (pos + len <= end)
		{
			_str[end] = _str[end - len];
			end--;
		}
		memcpy(_str + pos, ch, sizeof(char) * len);
		_size += len;
	}

	void string::erase(size_t pos, size_t len)
	{
		assert(pos <= _size);
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i <= _size; i++)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;
		}
	}

    size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch) 
			{
				return i;
			}
		}
		return npos;
	}

	size_t string::find(const char* ch, size_t pos)
	{
		assert(pos < _size);
		const char* ptr = strstr(_str + pos, ch);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}
    
    string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len >_size - pos)
		{
			len = _size - pos;
		}
		string sub;
		sub.reserve(len);
		for (size_t i = 0; i <len; i++)
		{
			sub += _str[pos + i];
		}
		return sub;
	}

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		const int N = 256;
		char buff[N];
		int i=0;
		char ch;
		ch = in.get();
		while (ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		return in;
	}  
    //测试用例
    void test( )
    {
	    string s1;
	    string s2("h");
	    string s3("hello");
	    cout << s1 << endl;
	    cout << s2 << endl;
	    cout << s3 << endl;
    }
    //尾插测试
    void test1()
    {
	    string s1;
	    s1.push_back('h');
	    cout << s1 << endl;
	    s1.push_back("ello");
	    cout << s1 << endl;
	    s1.append(" world");
	    cout << s1 << endl;
    }
    //插入删除测试
    void test2()
    {
    	string s1("hello world");
	    s1.insert(5, 'x');
    	cout << s1 << endl;
    	s1.insert(0, "xxxx");
	    cout << s1 << endl;

    	s1.erase(0, 4);
    	cout << s1 << endl;
    }
    //拷贝和赋值测试
    void test3()
    {
    	string s1("hello world");
	    string s2 = s1;
	    string s3;
	    s3 = s2;
	    cout << s1 << endl;
	    cout << s2 << endl;
	    cout << s3 << endl;
    }
    //查找和返回字符串测试
	void test4()
	{
		string st("hello.cpp");
		size_t fin = st.find('.');
		string st1 = st.substr(fin);
		cout << st1 << endl;
	}
}
相关推荐
Lenyiin2 小时前
《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现
数据结构·c++·stl
程序猿练习生2 小时前
C++速通LeetCode中等第5题-无重复字符的最长字串
开发语言·c++·leetcode
无名之逆3 小时前
云原生(Cloud Native)
开发语言·c++·算法·云原生·面试·职场和发展·大学期末
好蛊3 小时前
第 2 课 春晓——cout 语句
c++·算法
景小雨5 小时前
【数据结构与算法】排序算法之快速排序(简)
c++·算法·排序算法·快速排序
鸽嗷高.6 小时前
C++伟大发明--模版
开发语言·c++
weixin_486681147 小时前
C++系列-STL容器中统计算法count, count_if
开发语言·c++·算法
基德爆肝c语言7 小时前
C++入门
开发语言·c++
怀九日7 小时前
C++(学习)2024.9.18
开发语言·c++·学习·面向对象·引用·
一道秘制的小菜7 小时前
C++第七节课 运算符重载
服务器·开发语言·c++·学习·算法