C++初阶学习————STL (string使用及简单的模拟实现)


C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
string类的形式出现在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。

  1. string是表示字符串的字符串类
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char,char_traits,allocator> string;
  4. 不能操作多字节或者变长字符的序列。
    在使用string类时,必须包含#include头文件以及using namespace std;

string的常用接口

1.string类对象的构造函数

1.string类对象的常见构造

	string s1;
	string s2("hello world");
	string s3(s2);

在这里插入图片描述
基本常用的构造函数有三种:
1.string():构造空的string类对象,即空字符串
2.string(const char* s):用C语言的string来构造string类对象
3.string(const string&s) :拷贝构造
4.string(size_t n, char c) :string类对象中包含n个字符c(不常用)

2.string类对象的容量操作

1.获取字符串有效元素个数

	string s1("hello");
	cout << s1.size() << endl;	推荐
	cout << s1.length() << endl;

在这里插入图片描述

2.获取当前容量

cout << s1.capacity() << endl;

在这里插入图片描述
实际容量是16,因为还有个\0

3.清空字符串 clear

s1.clear();

但是容量并未清理,只是清理了size
在这里插入图片描述

4.判断字符串是否为空 empty

4.判断字符串是否为空 empty
检测字符串是否为空串,是返回true,否则返回false

cout << s1.empty() << endl;
//清空字符串
s1.clear();
cout << s1.empty() << endl;

在这里插入图片描述

5.reserve请求更改容量

5.reserve
此函数对字符串长度没有影响,也不能更改其内容。
如果n大于当前字符串容量,则该函数将容器的容量增加到n个字符(或更大),小于当前容量则不执行操作

string s2("hello");
s2.reserve(100);

在这里插入图片描述

6.resize将有效字符的个数改成n个,多出的空间用字符c填充

6.resize 将有效字符的个数改成n个,多出的空间用字符c填充
若更改空间容量之前有字符串,则不会更改原来的
若之前是空,则会把所有开辟的空间初始化给的字符

string s2("hello");
s2.reserve(100);
s2.resize(1000,'x');

在这里插入图片描述
若比当前容量小,则会删除数据(size)

3. string类对象的访问及遍历操作

1.返回pos指定下标位置 operator[ ]

1.返回pos指定下标位置 operator[ ]
有两个重载
1.返回值和形参都可修改—————— char& operator[] (size_t pos)
2.返回值和形参都加const修饰———— const char& operator[] (size_t pos) const;

	string s1("hello world");
	for(size_t i = 0;i < s1.size();i++)
	{
		cout << s1.operator[](i) << " ";
		s1.operator[](i)++;
	}
	cout << endl;
	for(size_t i = 0;i < s1.size();i++)
	{
		cout << s1.operator[](i) << " ";
	}

在这里插入图片描述

2.迭代器 begin、end

2.迭代器 beginend
begin获取第一个字符位置的迭代器
end获取最后一个字符下一个位置的迭代器
暂时可以理解为指向首字符的一个指针和指向\0位置的一个指针

	string s1("hello world");
	string::iterator it = s1.begin();
	while(it != s1.end())
	{
		(*it)++;
		it++;
	}
	cout << endl;

	it = s1.begin();
	while(it != s1.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;

在这里插入图片描述
在这里插入图片描述

3.反向迭代器 rbegin、 rend

3.反向迭代器 rbeginrend
rbegin获取最后一个字符位置的迭代器
rend获取第一个字符的前一个位置的迭代器

	string s1("hello world");
	string::reverse_iterator rit = s1.rbegin();
	while(rit != s1.rend())
	{
		cout << *rit++ << " ";
	}
	cout << endl;

在这里插入图片描述
在这里插入图片描述

4.范围for

自动往后迭代,自动判断结束 (自动根据获取的类型获取范围和位置)
注意:C++11以上才支持这种写法
可以看成是反回下标位置的引用,就可以更改读取位置本身的数据了

	for(auto& e: s1)
	{
		e += 1;
	}
	for(auto e : s1)
	{
		cout << e << " "
	}
	cout << endl;

注意:
迭代器属于内嵌在类中的
迭代器相比operator[ ]确实是没优势,但是operator[ ]只有数组能用,而其他结构如链表用迭代器就很便利了,所以是迭代器是通用的方式
C++11中还有cbegin cend 代表是const修饰的,但是普通版本也可以自动识别const修饰的

4. string类对象的修改操作

1.push_back在字符串后尾插字符c

1.push_back 在字符串后尾插字符c

	string s1("hello world");
	s1.push_back('a');

在这里插入图片描述

2.append 在字符串后追加一个字符串

2.append 在字符串后追加一个字符串

	string s1("hello world");
	s1.push_back('a');
	s1.append("ni hao");

在这里插入图片描述

3.operator+=在字符串后追加字符串str

3.operator+= 在字符串后追加字符串str

	string s1("hello world");
	s1+=' ';
	s1+="ni hao";

在这里插入图片描述

4.c_str返回C语言格式的字符串(字符串的首地址,char*)

4.c_str 返回C语言格式的字符串(字符串的首地址,char*)
在这里插入图片描述

5.find查找 ,substr取指定位置+指定长度的字符串内容

5.find查找 ,substr取指定位置+指定长度的字符串内容
在这里插入图片描述
pos是要取的位置,不给值默认从0开始
len是要取得长度,不给值默认有多少取多少

在这里插入图片描述
共有4种:默认都是从pos位置开始查找
(1)查找一个string类对象的字符串
(2)查找一个字符串
(3)查找字符串的一部分,n就是要查找的长度
(4)查找一个字符

返回值:如果找到就返回第一个匹配的位置(字符串的第一个字符的位置),没找到就返回npos
npos是 unsigned int类型的,默认是-1,换成无符号整型就是一个很大的数

find+substr组合应用
例如:分别取出一个网址的协议名,域名,路径

	string url("https://fanyi.baidu.com/#en/zh/ERROR%3A%20%20%20%20PREPROCESSOR%3A%20MACROS%20TOO%20NESTED");
	取协议
	size_t pos1 = url.find(':');
	string protocol = url.substr(0,pos1-1);
	取域名
	size_t pos2 = url.find('/',pos1+3);
	string domain = url.substr(pos1+3,pos2 - (pos1+3));
	取资源
	string uri = url.substr(pos2+1);

在这里插入图片描述
除了find还有rfind,是从后往前找就不做介绍了

6.头插 insert、删除erase

6.头插 insert,删除erase

	string s1("hello world");
	s1.insert(0,1,'x');			在指定位置,指定插入个数,指定要插入的 字符
	s1.insert(s1.begin(),'y');	给迭代器位置,指定要插入的字符
	s1.insert(1,"test");		在指定位置,插入字符串
	
	s1.erase(0,1);				头删
	s1.erase(s1.size()-1,1);	尾删
	s1.erase(3);				从第三个位置往后删直到结束

头删,头插效率很低,因为要把数据整体往后挪,所以尽量不要用

另外一些常见功能

operator>> 输入运算符重载

operator>> 输入运算符重载

operator<< 输出运算符重载

operator<< 输出运算符重载

getline 获取一行字符串

getline 获取一行字符串
在这里插入图片描述

relational operators 大小比较

relational operators 大小比较
在这里插入图片描述

string模拟实现

class My_string
{
public:
private:
	char* _str;			字符串
	size_t _capacity;	容量
	size_t _size;		下标/字符个数
	static const size_t _nops;
};
static const Hx::My_string::_nops = -1;

1.基础功能

构造函数

char* strcpy(char* temp1,const char* temp2)
{
	assert(temp2);
	char* ret = temp1;
	while(*temp1++ = *temp2++)
	{}
	return ret;
}
int strlen(const char* temp)
{
	assert(temp);
	int count = 0;
	while(temp[count])
	{
		count++;
	}
	return count;
}

My_string(const char* str = "")		
	:_str(new char [strlen(str) + 1])
	,_size(strlen(str))
	,_capacity(_size)
{
	strcpy(_str,str);
}
"""\0"都代表空字符串,只存一个\0 
nullptr NULL都通过不了,且\0等同于NULL

构造函数在申请内存的时候要多申请一个,是留给\0的

析构函数

~My_string()
{
	delete[] _str;
	_str = nullptr;
	_size = 0;
	_capacity = 0;
}

析构的时候需要释放申请的动态内存,并把指针置空

拷贝构造

拷贝构造传统写法
My_string(const My_string& s)
	:_str(new char[s._capacity+1])
	,_size(s._size)
	,_capacity(_size)
{
	strcpy(_str,s._str);
}

拷贝构造现代写法
void My_swap(My_string& s)
{
	swap(_str,s._str);
	swap(_capacity,s._capacity);
	swap(_size,s._size);
}

My_string(const My_string& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	My_string temp(s.c_str);
	My_swap(temp);
}

拷贝构造涉及到深拷贝:对动态内存中的数据拷贝,但不能把地址也拷贝,否则会导致,一块动态内存能被多个对象改变,且析构多次
传统写法:
(1)先用被拷贝的对象容量值给即将要实例化的对象开辟空间
(2)再把size、capacity全都拷贝
(3)把被拷贝对象的字符串,拷贝给即将实例化的对象(可以用strcpy、也可以遍历)
现代写法:
(1)用被拷贝对象的字符串构造一个临时对象,这样就能自动计算出size、capacity
(2)需要写一个类域内部应用的swap函数接口,再利用库函数中的swap把成员函数分别交换

C++中的swap源码
在这里插入图片描述
为什么不用C++库函数中的swap直接交换对象,因为这里只是对成员变量交换,而用库中的代价很大,传递的是两个类对象, 会完成三次深拷贝,库函数的swap应尽量对内置变量使用

赋值重载

传统写法
My_string& operator=(const My_string& s)
{
	if(this != &s)
	{
		delete[] _str;
		_str = new char[s._capacity+1];// 或strlen(s._str)+1
		_size = s._size;
		_capacity = s._capacity;
		strcpy(_str,s._str);
	}
	return *this;
}

改进1
My_string& operator=( My_string& s)
{
	if(this != &s)
	{
		char* temp = new char[s._capacity+1];
		strcpy(temp,s._str);
		delete[] _str;
		_size = s._size;
		_capacity = s._capacity;
		_str = temp;
	}
	return *this;	
}

改进2
My_string& operator=( My_string& s)
{
	if(this != &s)
	{
		My_string temp(s);
		My_swap(temp);
	}
}

改进3
My_string& operator=( My_string s)
{
	My_swap(s);
	return *this;
}

测试功能
在这里插入图片描述

赋值重载需要注意的是,若赋值的目标对象与源对象的长度和容量相同,是可以直接cpy字符串的,但是若出现一大一小的情况就需要额外考虑了
大赋小:需要增容
小赋大:需要缩小
所以它们都需要,释放目标动态内存,用源对象容量大小新开辟一块空间给目标空间
再把size、capacity、字符串复制到目标对象
传统写法:
(1)先创建一个临时指针,接收新开辟的动态内存(用源对象的capacity+1)
(2)拷贝字符串到新的动态内存中
(3)释放目标对象的动态内存
(4)把新动态内存地址、源对象的capacity、size赋给目标对象
(5)利用隐藏this与传递进来的对象别名判断是否自己给自己赋值
注意:要先用临时变量去开辟空间,切记不要 先释放,若释放失败则会抛异常,不会执行后续操作了,数据也就丢了,而先开辟,开辟失败时原来保存的数据不会收到影响
现代写法:
(1)传参设置为传值,传值等于是用传进来的值拷贝构造的临时对象;
(2)用临时对象与目标对象的引用交换,临时对象在调用结束时会销毁并调用析构函数,就把交换给的动态内存释放掉了

在这里插入图片描述

2.访问遍历操作

获取size

size_t size() const
{
	return _size;
}

size的位置是\0的位置,因为数组下标是0开始的

c_str 按C格式读取字符串

const char* c_str() const
{
	return _str;
}

C语言中,读取一个字符串只需要他的首地址

下标引用符[ ]重载

char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

const char& operator[](size_t pos)const
{
	assert(pos < _size);
	return _str[pos];
}

begin、end 迭代器

迭代器是获取首位、末尾位置地址的,关键字:iterator、const_iterator

typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str+_size;
}

const_iterator begin()const
{
	return _str;
}

const_iterator end()const
{
	return _str+_size;
}

测试功能
在这里插入图片描述

3.容量操作

reserve 为字符串预留空间

void reserve(size_t n)
{
	if(n > _capacity)
	{
		char* temp = new char[n+1];
		strcpy(temp,_str);
		delete[] _str;
		_capacity = n;
		_str = temp;
	}
}

给对象指定大小,若n比原有的容量大则增容,重新申请容量为n大小的内存,并把原字符串拷贝,再更新capacity的数值;若n比原有的容量小,则不执行任何操作
增容时注意深拷贝的问题,先申请后释放,因为有可能申请失败
在这里插入图片描述

resize 将有效字符的个数该成n个,多出的空间用字符c填充

void resize(size_t n,char ch = '\0')
{
	if(n <= _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		if(n > _capacity)
		{
			reserve(n);
		}
		memset(_str+_size,ch,n-_size);
		_size = n;
		_str[_size] = '\0';
	}
}

分三种情况:
(1)n小于capacity,则只需要把size的位置缩小为n就行了
(2)n等于capacity,不执行任何操作(可以把小于和等于合为一种情况操作)
(3)n大于capacity,那就需要扩容了,先深拷贝原来的动态内存的字符串,在将size到n之间全置为ch,最后改变size的位置并注意不要丢失\0
在这里插入图片描述

clear 清除

void clear()
{
	assert(_size);
	_size = 0;
}

清除只需要把size置0,并把0位置改为\0即可

empty 判断空字符串

bool empty() const
{
	return _size == 0;
}

判断size是否为0即可

4.增删查改

push_back 、append 尾插

void push_back(char ch)
{
	if( _size == _capacity )
	{
		reserve(_capacity == 0 ? 4 : _capacity*2);
	}	
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

void append(const char* s)
{
	int len = strlen(s);
	if( _size+len > _capacity )
	{
		reserve(_capacity == 0 ? 4 : _size+len);
	}	
	strcpy(_str+_size,s);
	_size += len;
}

尾插入一个字符:
(1)要先进行增容判断(可以复用reserve)
(2)再把这一个字符拷贝上去,size增1
注意strcpy的起始位置是size的位置
尾插入一个字符串:
与插入字符大同小异
(1)需要先算出字符串长度+当前长度size,判断是否增容
(2)用strcpy把字符串拷贝上去
(3)size更新,更新的长度为 size+计算的字符串长度(不需要单独设置\0,因为会把\0拷贝的)

+=操作符重载

My_string operator+=(char ch)
{
	push_back(ch);
	return *this;
}

My_string operator+=(const char* s)
{
	append(s);
	return *this;
}

+=只是空壳,重载复用的尾插字符和字符串

find 查找

char* my_strstr(const char* temp1,const char* temp2)
{
	assert(temp1 && temp2);
	char* t1 = temp1;
	char* t2 = temp2;
	char* ret = t1;
	while(*t1 && *t2)
	{
		ret = t1;			//用来记录比较前的首地址位置。如果字符串元素都相等,就返回比较的第一个元素首地址
		while(*t1 == *t2)	//只要不相等就退出		
		{
			t1++;
			t2++;
			if('\0' == *t2)	//当源字符串已经为\0时,证明相等,则退出
				return ret;
			if('\0' == *t1)	//当目标字符串遍历出\0,就没必要再缩进剩余几个字符进行比较了,直接退出
				return NULL;
		}
		t2 = temp2;		//若比较不相等,则源字符串复位,等待下次比较
		t1 = ret;		//遍历指针回到第一次比较的位置
		t1++;			//证明上一次比较,此位置比较不出来相等,就缩一位
	}
	return NULL;
}

size_t find(char ch)
{
	for(size_t i = 0;i < _size;i++)
	{
		if(_str[i] == ch)
			return i;
	}
	return _nops;
}

size_t find(const char* s,size_t pos = 0)
{
	char* ptr = strstr(_str,s);	并返回在_str中第一次匹配的地址
	if(ptr)
		return ptr-_str;
	return _npos;
		
}

默认从0开始找
查找一个字符:直接遍历查找,或者用迭代器,并返回找的的下标
查找一个字符串:复用strstr,返回找到的首字符的下标
若没找到反回的都是npos(size_t 类型的,值是-1,也就是无符号整型最大值)

insert 指定下标插入

My_string& insert(size_t pos,char ch)
{
	assert(pos <= _size);
	if(_size == _capacity)
	{
		reserve(_capacity == 0? 4 : _capacity*2);
	}
	
/*	size_t end = _size;
	while(end >= pos)
	{
		_str[end+1] = _str[end];
		--end;		
	}								无符号整型在变为-1时是会变为正数的最大值
	int end = _size;
	while(end >= pos)				把end变为int,但是在比较时会发生算术转换,还是会转换为unsigned int
	{
		_str[end+1] = _str[end];
		--end;		
	}
*/ 
	size_t end = _size;
	while(end >= pos)
	{
		_str[end+1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	_size++;
	return *this;
}


My_string& insert(size_t pos,char* s)
{
	assert(pos <= _size);
	size_t len = strlen(s);
	if(_size+len > _capacity)
	{
		reserve(_capacity == 0? 4 : _size+len);
	}
	size_t end = _size + len;
	while( end-len >= pos )
	{
		_str[end] = _str[end - len];
		end--;
	}
	strncpy(_str+pos,s,len);		//不可以使用strcpy
	_size += len;
	return *this;
}

前插入一个字符:
(1)先进行增容判断
(2)在把插入位置到结尾整体往后挪1个位置(利用下引用符、迭代器都可以)
(3)插入字符
(4)更新size
在这里插入图片描述

前插入一个字符串:
(1)先计算要插入的字符串长度
(2)计算的长度+当前size 判断是否增容
(3)先把pos到结尾的字符串往后挪,与挪字符类似,字符是挪1,字符串是挪字符串长度(类似希尔排序中的 +gap预排序)
(4)strcnpy把字符串插入到对象的字符串中,不能用strcpy,会把\0也拷贝的(注意起始位置是pos位置)
(5)更新size(size+计算的长度)

erase 指定删除

指定删除的位置及个数

My_string& erase(size_t pos = 0,size_t len = _nops)
{
	assert(pos < _size);
	if(len == _npos || len + pos >= _size)
	{
		_size = pos;
		_str[pos] = '\0';
	}
	else
	{
		strcpy(_str + pos,_str + pos + len);
		_size -= len;
	}
	
	return *this;
}

分三种情况:
(1)全删(pos = 0),直接在初始位置置0
(2)从中途开始删,删到最后,也是直接在pos位置置\0
(3)删一部分,没超过size,从pos位置整体往前挪len个,更新size
在这里插入图片描述

5.string类非成员函数

比较大小

之前日期类练习时的运算符都重载成成员,这里的运算符就重载成全局的来练习

int strcmp(const char* temp1,const char* temp2)
{
	assert(temp1 && temp2);
	while(*temp1 && *temp2)
	{
		if(*temp1 != *temp2)
		{
			return *temp1 - *temp2;
		}
		temp1++;
		temp2++;
	}
	return *temp1 - *temp2;
}
bool operator<(const My_string& s1,const My_string& s2)
{
	size_t i = 0;
	while( i < s1.size() && i < s2.size() )
	{
		if(s1[i] < s2[i])
			return true;
		else if(s1[i] > s2[i])
			return false;
		else
			i++;
	}
	if(i == s1.size() && i < s2.size())
		return true;
	else
		return false;
}
或直接strcmp(s1.c_str(),s2.c_str());
bool operator==(const My_string& s1,const My_string& s2)
{
	return strcmp(s1.c_str(),s2.c_str()) == 0;
}
bool operator!=(const My_string& s1,const My_string& s2)
{
	return !(s1 == s2);
}
bool operator<=(const My_string& s1,const My_string& s2)
{
	return (s1 == s2) || (s1 < s2);
}
bool operator>=(const My_string& s1,const My_string& s2)
{
	return !(s1 < s2);
}
bool operator>(const My_string& s1,const My_string& s2)
{
	return !(s1 <= s2);
}

+加运算符重载

My_string operator+(const My_string& s,const char ch)
{
	My_string temp = s;
	temp += ch;
	return temp;
}
My_string operator+(const My_string& s,const char* str)
{
	My_string temp = s;
	temp += str;
	return temp;
}

流插入

ostream& operator<<(ostream& out,My_string& s)
{
	
	for(size_t i = 0;i < s.size();i++)	z这个是不管中间是否是\0,都输出,直到结尾
	{
		out << s[i] ;
	}
	out << endl;
	return out;
}

out << s.c_str() << endl; error 这个格式是遇到\0结束
这里需要的是无论中途是否有\0都要读取到size位置

流提取

istream& operator>>(const istream& in,My_string& s)
{
	char ch = in.get();						每次获取一个字符
	while(ch != ' ' || ch != '\n')			当出现换行和空格结束
	{
		s += ch;							每次插入一个字符
		ch = in.get();						更新字符
	}
	return in;
}

和C语言输入字符串类似scanf(“%s”)可输入字符串,不可中途用空格和回车
get就是每次接收一个字符,当读取到空格或者换行就退出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值