初步了解STL和string

什么是STL?

STL(standard template libaray - 标准模板库) :是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。

STL的版本更替

原始版本

Alexander Stepanov、Meng Lee在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要像原始版本一样做开源使用。HP版本------所有STL实现版本的始祖。

P.J.版本

由P.J.Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。

RW版本

由Rouge Wage公司开发,继承自HP版本,被C++Builder采用,不能公开或修改,可读性一般。

SGI版本

由Silicon Graphics Computer Systems, Inc公司开发,继承自HP版本。被GCC(Linux)采用, 可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格上看, 阅读性非常高。

string类

C语言中的字符串。C语言中,字符串是以\0结尾的一些字符的集合,为了操作方便,C标准库中提供的str系类的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要自己管理,稍不留神还可能越界访问。

cpp 复制代码
void test_string()
{
	string s1;//默认构造
	string s2("hello world");//用const char* 的带参构造
	string s3(s2);//拷贝构造

	string s4(s2, 6, 5);//从s2的第6位开始向后拷贝5个字符
	//如果拷贝的长度超过了字符串的实际长度,则拷贝到字符串结尾为止
	//如果第三个参数省略,默认拷贝到字符串结尾


	string s5("hello world", 5);
	//取字符串前五个字符初始化

	string s6(10, 'x');
	//取连续n个字符x进行初始化

	s2[0] = 'x';//s2[0]的返回值是传引用返回,因此可以用来修改内容

	int x = s2.size();//返回字符串长度
}

上面我们结合具体代码简要看了一些string的相关用法,结合之前写过的内容,我们可以发现string的底层逻辑大概是以下这样:

cpp 复制代码
class String
{
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

在遍历string容器的整个过程中,我们提供了下面三种方式:

  1. 下标 + []
  2. 迭代器
  3. 范围for

第一种遍历方式我们可以类比于数组,数组怎么访问,我们这里也可以怎么访问,第三种范围for适用于容器或数组的遍历,使用简单。

这里我们着重介绍一下最重要的------迭代器。迭代器提供了一种通用的访问容器的方式,可以通过它访问所有容器。它类似于指针但又不一定是指针。
正向迭代器:

cpp 复制代码
string::iterator it = s.begin(); 

这里我们用迭代器定义了严格对象it,返回开始位置的迭代器。也可以写成auto it = s2.begin();。相当于指针,可以改变容器的元素。

但值得注意的是:当我们使用s.end()时,end返回的时最后一个位置的下一个位置。
反向迭代器:

cpp 复制代码
string::reverse_iterator rit = s.rbegin();

这里返回的是最后一个元素的迭代器,相反的是:s.rend();返回的就变成了第一个元素的前一个位置,当我们使用反向迭代器的++时,反向迭代器是向前移动的,即反向迭代器是倒着走的。
const迭代器:

当我们遇到这样的字符串的定义时:

cpp 复制代码
const string s("hello world");

此时,字符串被const修饰时,我们不能使用普通迭代器,需要使用const迭代器,相比于普通迭代器,只需要在iterator前面加上const_即可。

cpp 复制代码
string::const_iterator cit = s.begin();

和其他被const修饰的成员语言,cit这个迭代器只能读,不能写。

同样的,const迭代器和普通迭代器一样有着正向和反向之分。

cpp 复制代码
string::const_reverse_iterator dit = s.rbegin();

综上所述,迭代器有四种:分别为iteratorreverse_iteratorconst_iteratorconst_reverse_iterator。这里我们只是简单介绍一下迭代器,在后面我们可以更加深刻的领悟迭代器的魅力。

cpp 复制代码
void test_string()
{
	string s("hello world");

	//遍历容器的方式
	//1.下标+[]
	for (size_t i = 0; i < s.size(); i++)
		cout << s[i] << " ";
		
	//2.迭代器
	//正向迭代器
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		it++;
	}
	//反向迭代器
	string::reverse_iterator rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << " ";
		rit++;
	}

	//const迭代器
	const string s1("hello world");
	string::const_iterator cit = s1.begin();
	while (cit != s1.end())
	{
		cout << *cit << " ";
		cit++;
	}

	string::const_reverse_iterator dit = s1.rbegin();
	while (dit != s1.rend())
	{
		cout << *dit << " ";
		dit++;
	}

	//3.范围for
	//范围for使用于容器或数组的遍历,使用简单
	for (auto ch : s)
	{
		cout << ch << " ";
	}
}
补充:auto关键字

在这里补充2个C++11的小语法。

在早期C / C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,auto有了全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

auto声明指针类型时,用autoauto* 没有任何区别, 但用auto声明引用类型时则必须加&。当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

注:auto不能作为函数的参数, 可以做返回值, 但是建议谨慎使用。auto不能直接用来声明数组

cpp 复制代码
void test()
{
	int a = 1;
	auto b = a;
	auto c = 'd';
	auto d = 3.12;
	//不能写成auto e;编译器无法推导出类型,无法分配空间

	//typeid可以查看变量的类型
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

	//auto不能定义数组
	//void func(auto a)错误,不能用作参数,但是可以用作返回值
	//auto func()正确,可以用作返回值,但建议谨慎使用
}

下面我们用代码中讲解的方式大致了解一下使用相关的内容:

cpp 复制代码
void test_string()
{
	string s("hello world");
	//length和size的区别:
	//length不具有通用性,只有string类有length方法,而size具有通用性,几乎所有的容器都有size方法。
	//length和size的功能是一样的,都是返回字符串的长度。但最好用size。
	cout << s.length() << endl;
	cout << s.size() << endl;
	
	//capacity:返回字符串容量
	cout << s.capacity() << endl;
	
	
	//reserve:提前开空间,避免扩容,提高效率。当空间小于当前字符串长度是,不会缩小容量,但当当前空间大于字符串且缩小后的空间大于字符串长度时,会缩小容量。不会影响字符串!
	//         每个编译器不同,看具体情况
	string s;
	s.reserve(100);//提前开100的空间,当push的时候不再扩容(没满)

	//resize:改变字符串的大小,可以变大也可以变小。变大时,默认用'\0'填充,也可以指定填充字符
	s.resize(10);//变成10个字符,默认用'\0'填充


	//clear:清空数据,但一般不清理容量
	s.clear();


	//empty:判断是否为空
	if (s.empty());
}

关于insert

尽量减少insert的使用,过度使用会产生效率问题

cpp 复制代码
void test_string()
{
	string s = "1234567";

	s.push_back('8');//在字符串末尾尾插一个字符
	
	s.append("000");//在字符末尾尾插一个字符串
	s += "000";
	//appned作用和 s += 一样,建议使用 += 运算符更加简洁


	s.insert(3, "xxx");//在下标3的位置之前插入字符串"xxx"
	s.insert(0, "hello");//可以这样实现头插,但是实现效率较低

}

关于erase

erase的使用同样需要注意,也可能导致效率问题

cpp 复制代码
void test_string()
{
	string s = "1234567";

	s.erase(6, 1);//从第六个位置开始,删掉一个元素

	s.erase(0, 1);
	s.erase(s.begin());//这两种操作可以使用头删

	s.erase(--s.end());
	s.erase(s.size() - 1, 1);//这两种操作可以实现尾删

	s.erase(3);//默认删除下标为3之后的所有元素

}

关于replace

替换字符串中的内容,也容易导致效率问题

cpp 复制代码
void test_string7()
{
	string s = "hello world hello china";
	s.replace(5, 1, "%%");//将下标为5开始往后一个字符替换为%%

	string::npos;//表示字符串的最大下标值,实际上是一个无符号整型的最大值

	size_t pos = s.find("hello");//查找字符串hello第一次出现的位置,默认是从头开始查找


	//将空格替换为%%
	//方法一:
	size_t ss = s.find(' ');
	while (ss != string::npos)
	{
		s.replace(ss, 1, "%%");
		ss = s.find(' ', ss + 2);//从当前位置的下两个位置查找
	}
	//方法二:
	string tmp; tmp.reserve(s.size());//提前开空间,避免扩容
	for (auto ch : s)
	{
		if (ch == ' ')
			tmp += "%%";
		else
			tmp += ch;
	}
	swap(tmp, s);



	size_t pos1 = s.find("he", 5);//从下标5开始查找hello
	size_t pos2 = s.rfind("he");//从后往前查找he第一次出现的位置

	string sf = s.substr(6, 5);//从下标6开始往后取5个字符,返回一个新的字符串,如果没有第二个参数或第二个参数超出范围,则取到字符串结尾


	//find_first_of:查找字符串中第一个出现的指定字符的位置
	string str("Please, replace the vowels in this sentence by asterisks.");
	size_t found = str.find_first_of("abcd");//找到字符串中a、b、c、d中第一个出现的位置,第一个找到的是abcd中任意一个都可以
	while (found != string::npos)
	{
		str[found] = '*';
		found = str.find_first_of("abcd", found + 1);//向后找其中的任意一个元素
	}
	//find_last_of:和find_first_of类似,不过是从后往前找
}

string的底层模拟实现:

这里我们简要提及一下封装后可以进行string的模拟实现,通过模拟实现,我们可以更清晰的了解string的运行原理。

关于封装:

迭代器的设计也是封装的一种体现

屏蔽了底层实现细节,提供了统一的类似访问容器的方式

不需要关心容器底层结构和实现细节

达到的遍历互通的效果,不考虑底层(底层已经根据不同的容器封装好),可以直接使用

cpp 复制代码
namespace yyyy
{
	class string
	{
	public:
		typedef char* iterator;//自己封装出一个迭代器
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}//实现迭代器:迭代器本质上模拟的是指针的行为
		
		string()
			:_str(new char[1]{'\0'})//不能直接给空指针,至少给一个字符
			,_size(0)
			,_capacity(0)
		{}

		//短小频繁调用的函数,可以直接定义到类里面,默认是inline
		string(const char* str = "")//这里全缺省不能给nuplltr空指针
		{
			_size = strlen(str);
			//_capacity不包含\0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);//拷贝字符串,连\0也会拷贝
		}

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

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

		size_t size() const
		{
			return _size;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);//断言,防止发生越界
			return _str[pos];
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);//断言,防止发生越界
			return _str[pos];
		}
	private:
		char* _str;//指向字符串空间的首地址
		size_t _size;//字符串的长度
		size_t _capacity;//字符串的容量
	};
}

这样我们最简单的string的模拟实现就完成了。

希望对你有所帮助,本文完。

相关推荐
二川bro37 分钟前
字符串格式化进阶:Python f-string性能优化
开发语言·python
LitchiCheng42 分钟前
Mujoco 机械臂 OMPL 进行 RRT 关节空间路径规划避障、绕障
开发语言·人工智能·python
烤麻辣烫43 分钟前
黑马程序员苍穹外卖(新手)DAY10
java·开发语言·学习·spring·intellij-idea
waves浪游1 小时前
进程控制(上)
linux·运维·服务器·开发语言·c++
程序员三明治1 小时前
【Java】synchronized关键字详解:从字节码到对象头与锁升级
java·开发语言·juc·synchronized··锁升级
y***54881 小时前
Rust在嵌入式中的实时操作系统
开发语言·后端·rust
老虎06271 小时前
Java基础面试题(11)—Java(泛型)
java·开发语言·windows
froginwe111 小时前
ASP ADO:深入解析ActiveX数据对象在ASP中的应用
开发语言
Petrichor_H_1 小时前
DAY 43 复习日
开发语言·python