STL 即标准模板库(Standard Template Library),是 C++ 标准库的一部分,它提供了一系列通用的模板类和函数,大大提高了程序员的开发效率和代码的可维护性。以下是关于 STL 的详细介绍:
https://legacy.cplusplus.com/reference/stl/?kw=stl
容器(Containers):用于存储和管理数据的对象,如 vector、list、queue、stack、set、map等。 迭代器(Iterators):用于遍历容器中的元素,相当于一种广义的指针。迭代器提供了一种统一的方式来访问不同类型容器中的元素,使得算法可以独立于容器的具体实现进行编写。 算法(Algorithms):STL 提供了大量通用的算法,如排序、搜索、复制、删除等,这些算法可以作用于各种容器。例如,std::sort 函数可以对多种容器中的元素进行快速排序。 仿函数(Function objects):也称为仿函数,是一种重载了函数调用运算符
()
的类对象,可以像函数一样被调用。 适配器(Adapters):用于对容器、迭代器或函数对象进行适配和转换,使其能够满足特定的需求。例如,stack
和queue
实际上是对其他容器的适配器,它们通过适配底层容器来实现特定的数据结构功能。
代码复用性高:STL 中的容器、算法等都是通用的模板,可以在不同的项目中重复使用,避免了重复编写类似的代码,提高了开发效率。 可维护性好:由于 STL 是标准库的一部分,具有较高的稳定性和可靠性。使用 STL 编写的代码结构清晰,易于理解和维护。 跨平台性:STL 是 C++ 标准的一部分,因此在不同的操作系统和编译器上具有良好的兼容性,可以保证代码在不同环境下的一致性。
STL是我们作为C++开发学习者,必须要掌握的内容,有了它,我们可以站在巨人的肩膀上,夸健步如飞的快速开发!下面就让我们进入STL的第一个话题string。
参考文档:
cplusplus.com/reference/string/string/?kw=string
string本质上是basic_string类模板的实例化,头文件为<string>。
cplusplus.com/reference/string/basic_string/
下面我重点讲解几个常用的构造函数
构造空的striing,即空字符串
//空字符串
string s;
用c-string,即字符串来构造string。
string s("helloworld");
用n个字符char构造string
//n个字符char构造
string s(10, 'a');
拷贝构造,用已经创建的string对象构造string
string s1("helloworld");
//拷贝构造
string s2(s1);
我们也是挑出几个重点常用的讲解:
返回string中有效字符的个数(不包含'\0')
string s("helloworld");
cout << s.size() << endl;
返回string中有效字符的个数(不包含'\0') size()与length()方法底层实现原理完全相同,引入size()的原因是为了与STL中其他容器的接口保持一致,一般情况下基本都是用size()。
string s("hello");
cout << s.length() << endl;
返回string的总容量大小
//有效字符个数size为 10,容量capacity为15
string s("helloworld");
cout << s.capacity() << endl;
检测string是否为空字符串,是返回true,否则返回false。
//有效字符个数size为 10,容量capacity为15
string s("helloworld");
cout << s.capacity() << endl;
清空string中所有有效字符,但是不影响容量。
string s1("helloworld");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.clear();
//清空后为空字符
cout << s1.size() << endl;
cout << s1.capacity() << endl;
为string字符串预留n个容量,不改变有效元素个数。 注意:当预留的容量n小于string当前字符串容量时,reserve(),不会改变容量大小! 如果n大于当前字符串容量,则该函数使容器将其容量增加到n个字符(或更大)
n小于当前字符串容量的情况:
string s("hello");
//观察当前容量
cout << s.capacity() << endl;
//如果n小于当前容量,则不会更改容量
s.reserve(10);
cout << s.capacity() << endl;
输出:
n小于当前字符串容量的情况:
string s("hello");
//观察当前容量
cout << "string的初始容量:"<<s.capacity() << endl;
//如果n大于当前容量,则会更改容量为n,或者更大
s.reserve(20);
cout <<"reserve更改后的容量:"<< s.capacity() << endl;
输出:
这里我们reserve(20),但实际容量增加到了31,说明实际可能会更改比n更大!
将有效字符的个数该成n个,多出的空间用'\0'或者字符c填充. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不 同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数 增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4.2.7.1、resize(size_t n)
string s("hello");
cout <<"s初始的有效字符个数:"<< s.size() << endl;
s.resize(10);
cout << "resize后s的有效字符个数:" << s.size() << endl;
我们可以看到resize(size_t n)确实将s的有效字符个数size,更改为n。
输出:
那么多余的容量是否由'\0'来填充呢?通过调试观察:
resize前的有效字符:
resize后的有效字符:
4.2.7.2、resize(size_t n, char c)
void Test()
{
string s("hello");
cout <<"s初始的有效字符个数:"<< s.size() << endl;
s.resize(10,'a');
cout << "resize后s的有效字符个数:" << s.size() << endl;
}
我们可以看到resize(size_t n,char c)确实也将s的有效字符个数size,更改为n。
输出:
那么多余的容量又是如何处理的呢?
resize前的有效字符:
resize后的有效字符:
多余的字符确实是用参数char c来填充的!
4.2.7.3、resize对capacity的影响
如果resize的大小没有超出容量大小,则不会影响capacity。 如果超出容量大小,则会更改容量。
string s("hello");
cout <<"s初始的有效字符个数:"<< s.size() << endl;
cout << "s初始的容量大小:" << s.capacity() << endl;
s.resize(20,'a');
cout << "resize后s的有效字符个数:" << s.size() << endl;
cout << "resize后s的容量:" << s.capacity() << endl;
输出:
operator[],支持我们像访问数组元素一样,访问string中pos位置的字符。注意下标和数组一样,也是从0开始!
string s("helloworld");
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
}
cout << endl;
输出:
除了访问,operator[ ]也支持修改string中pos位置的字符。
string s("helloworld");
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
s[i]+=1;
}
cout << endl;
cout << s << endl;
输出:
利用正向迭代器对string进行访问。迭代器同样也支持修改string的字符。
void Test()
{
string s("A character sequence");
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
//修改该位置的字符
*it += 1;
//迭代器往后遍历
++it;
}
cout << endl;
cout <<s<< endl;
}
输出:
利用反向迭代器遍历,与正向迭代器同理。这里注意反向迭代器是倒着遍历string,与begin()到end()一致,rbegin()到rend()迭代器的操作也是++。
string s("A character sequence");
auto it = s.rbegin();
while (it != s.rend())
{
cout << *it << " ";
*it += 1;
//反向迭代器也是++
++it;
}
cout << endl;
cout << s << endl;
输出:
string s("hello _zwy!");
for (auto ch : s)
{
cout << ch << " ";
ch += 1;
}
cout << endl;
输出:
在string字符串的后面追加一个字符c。
string s("helloworld");
s.push_back('a');
cout << s << endl;
s.push_back('b');
cout << s << endl;
s.push_back('!');
cout << s << endl;
输出:
在string的后面追加字符串
string s("hello");
//追加一个 char* 字符串
s.append("world");
cout << s << endl;
//追加一个string对象
string s1("!world");
s.append(s1);
cout << s << endl;
//追加n个字符char
s.append(3, '!');
cout << s << endl;
//追加char*字符串的 前n个字符,超出字符串长度,可能会抛异常
s.append("append", 7);
cout << s << endl;
输出:
字符以及字符串的拼接。
string s1("hello");
string s2("world");
//+=string 对象
s1 += s2;
//+=字符c
s1 += '!';
//+= char* 字符串
s1 += "operator+=";
cout << s1 << endl;
返回该字符串的地址,或者指针。
string s("helloworld!");
//字串串的地址会打印出整个字符串
cout << s.c_str() << endl;
//强转为void* 打印地址
cout << (void*)s.c_str() << endl;
输出:
在字符串指定位置(默认从头开始查找)pos开始往后查找字符串内容,返回第一个匹配的第一个字符的位置。如果没有找到返回npos。
string s1("helloworld!");
string s2("hello");
//从默认位置在s1中向后查找string对象
size_t ret1 = s1.find(s2);
cout << "s1在s1中出现的位置:" << ret1 << endl;
//从s1中下标为5向后查找字符 'o'
size_t ret2 = s1.find('o', 5);
cout <<"字符'o'在s1中的位置:" <<ret2 << endl;
//从s1中下标为1向后查找char* 字符串
size_t ret3 = s1.find("world", 1);
cout << "字符串world在s1中的位置:" << ret3 << endl;
//从s1中下标为3向后查找char* 字符串的前n个字符
size_t ret4 = s1.find("loworld!good", 3, 5);
cout << "字符串loworld!good的前5个字符在s1的位置:"<< ret4 << endl;
输出:
在字符串指定位置(默认从最后2开始查找)pos开始往前查找字内容符串,返回第一个匹配的第一个字符的位置。如果没有找到返回npos。
string s("hello world!!");
//从下标为5的位置往前查找字符串"hella"
size_t ret1 = s.rfind("hella",5);
cout << "字符串hella在s中的位置:"<<ret1 << endl;
//从下标为5的位置往前查找字符串"hella"的前四个字符
size_t ret2 = s.rfind("hella", 5,4);
cout << "字符串hella的前四个字符在s中的位置:" << ret2 << endl;
string s1("llo wor");
//从默认位置往前查找s1
size_t ret3 = s.rfind(s1);
cout << "string对象s1在s中的位置:" << ret3 << endl;
//从下标为9的位置往前查找字串'r'
size_t ret4=s.rfind('r', 9);
cout << "字符r在s中的位置:" << ret4 << endl;
输出:
在str中从pos位置开始,截取n个字符,然后将其返回,如果截取位置超过当前字符串长度会抛出out of range异常
string str("We think in generalities, but we live in details.");
//截取str前10个字符
string s1 = str.substr(0, 10);
cout << s1 << endl;
//截取str从下标5开始的15个字符
string s2 = str.substr(5, 15);
cout << s2 << endl;
//截取整个字符串
string s3 = str.substr();
cout << s3 << endl;
//截取长度超出字符串长度,会截取整个字符串
string s4 = str.substr(3,100);
cout << s4 << endl;
//如果截取位置超过当前字符串长度 会抛出out of range异常
//string s4 = str.substr(80,20);
//cout << s4 << endl;
输出:
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间 当字符串长度小于16时,使用内部固定的字符数组来存放. 当字符串长度大于等于16时,从堆上开辟空间。
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建 好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。 其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的 容量,还有一个指针做一些其他事情。 故总共占16+4+4+4=28个字节.
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个 指针,该指针将来指向一块堆空间,内部包含了如下字段: 空间总大小 、字符串有效长度 、引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
}
指向堆空间的指针,用来存储字符串。
// 为了和标准库区分,此处使用String
class String
{
public:
//以下为错误的string构造函数
/*String()
:_str(new char[1])
{
*_str = '\0';
}
String(const char* str = "\0")
String(const char* str = nullptr)*/
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 world!!!");
String s2(s1);
}
大家观察一下上述代码有什么问题吗? 上述String类没有显实现义拷贝构造函数与赋值运算符重载,此时编译器会生成默认的,当 用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内 存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致
多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该
资源已被释放,所以当继续对资源进行操作时,就会发生发生了访问违规。
s1和s2指向同一块内存空间,管理同一片资源,Test函数结束时,需要将当前栈帧中的s1对象和s2对象销毁,先销毁s2,当s2对象析构时,会将这片内存空间里面的资源释放,s2成功销毁,s1中的_pStr就成为野指针,销毁s1时就会发生错误,s1无法正常析构。解决的问题就是采用深拷贝!
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。 深拷贝是指在拷贝一个对象时,不仅仅复制对象本身的数据成员的值,对于对象中那些指向其他资源(如动态分配的内存的指针成员,会重新分配相应的资源,并将原对象所指向资源中的数据完整地复制到新分配的资源中,使得新对象和原对象拥有完全独立的、相同内容的资源副本,彼此之间对各自资源的操作不会相互影响。
当s1和s2都拥有自己的资源时,各自独立,析构时会不影响,就可以解决浅拷贝带来的问题!实现深拷贝,就需要我们自己实现拷贝构造函数和赋值重载!
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;
};
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(nullptr)
{
String strTmp(s._str);
swap(_str, strTmp._str);
}
// 对比下和上面的赋值那个实现比较好?
//String& operator=(String s)
//{
// swap(_str, s._str);
// return *this;
//}
String& operator=(const String& s)
{
if(this != &s)
{
String strTmp(s);
swap(_str, strTmp._str);
}
return *this;
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
};
现代写法,用拷贝对象了一个临时对象strTmp,swap交换底层指针的方式来实现拷贝构造,避免了不必要的拷贝,临时对象出了函数就销毁,赋值重载也利用交换临时对象指针的方式实现,由于sawp交换的成本很低,大大提高了拷贝构造以及赋值重载的效率!
第二种赋值重载较于第一种的优点: 避免不必要拷贝:const String& 作为参数,避免了按值传递带来的额外拷贝开销。只有在确实需要进行数据交换时(即当前对象和传入对象不同时),才通过创建一个临时对象strTmp并进行swap操作来完成赋值,这种方式在性能上通常更优,尤其是对于较大的String类对象。 正确性保障:通过if (this!= &s)的判断,避免了对象自我赋值的情况。自我赋值如果处理不当,可能会导致资源释放错误、内存泄漏等问题,这里的判断确保了在自我赋值时不会执行不必要的操作。
头文件:
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace zwy
{
class String {
public:
typedef char* iterator;
typedef const char* const_iterator;
/* String()
:_str(new char[1] {'\0'})
, _size(0)
, _capacity(0)
{}
String(char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity+1];
strcpy(_str, str);
}*/
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() 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()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
//ͳд
//s1(s2)
/* String(const String& str)
{
_str = new char [str._capacity + 1];
strcpy(_str,str._str);
_size = str._size;
_capacity = str._capacity;
}*/
void Swap(String& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std:: swap(_capacity, str._capacity);
}
//ִд
String(const String& str)
{
String tmp(str._str);
Swap(tmp);
}
//String& operator=(const String& str)
//{
// if (this != &str)
// {
// delete[] _str;
// _str = new char[str._capacity + 1];
// strcpy(_str, str._str);
// _size = str._size;
// _capacity = str._capacity;
// }
// return *this;
//}
//s1=s2
/* String& operator=(const String& str)
{
if (this != &str)
{
String tmp(str._str);
Swap(tmp);
}
return *this;
}*/
String& operator=( String tmp)
{
Swap(tmp);
return *this;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
const char* c_str() const
{
return _str;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
//bool empty()const;
void reserve(size_t n);
void push_back(char ch);
String& operator+=(char ch);
String& operator+=(const char* str);
void append(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, 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;
};
ostream& operator<<(ostream& out, const String& str);
istream& operator>>(istream& in, String& str);
bool operator<(const String& s1, const String& s2);
bool operator<=(const String& s1, const String& s2);
bool operator>(const String& s1, const String& s2);
bool operator>=(const String& s1, const String& s2);
bool operator==(const String& s1, const String& s2);
bool operator!=(const String& s1, const String& s2);
}
源文件:
#include"String.h"
namespace zwy {
const size_t String::npos = -1;
void String::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void String::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
String& String:: operator+=(char ch)
{
push_back(ch);
return *this;
}
String& String::operator+=(const char* str)
{
append(str);
return *this;
}
void String::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
void String::insert(size_t pos, char ch)
{
assert(pos < _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
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* str)
{
assert(pos < _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end>pos+len-1)
{
_str[end] = _str[end-len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_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 = 0; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t String::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
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;
}
bool operator<(const String& s1, const String& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const String& s1, const String& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const String& s1, const String& s2)
{
return !(s1 <= s2);
}
bool operator>=(const String& s1, const String& s2)
{
return !(s1 < s2);
}
bool operator==(const String& s1, const String& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const String& s1, const String& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, const String& str)
{
for (auto ch : str)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, String& str)
{
//
str.clear();
char ch;
const int N = 256;
char buff[N];
int i = 0;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
str += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return in;
}
}
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。 引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
以下给大家分享两篇有关写实拷贝的文章:有兴趣的同学可以加餐阅读:
C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell
有关string类的讲解到这里就结束了,string是C++中非常基础也非常重要的内容,希望大家都能掌握,接下来会给大家带来C++ STL中其他容器的深度讲解,创作不易,还请多多支持。关注博主,为你带来更多优质内容 !
如上讲解如有不足之处,还望各位大佬评论区赐教点拨,感激不尽!