C++ string类(1)—初始化、容量操作、迭代器

C++ string类(1)—初始化、容量操作、迭代器

目录

前言

一、string类

二、初始化

1、无参或带参

2、用字符串变量初始化

3、用字符串初始化

4、指定数量字符

三、容量操作

1、size

2、push_back

3、append

4、+=运算符

5、vs下string的结构

6、g++下string的结构

7、reserve

8、resize

四、迭代器

1、正向迭代器

2、反向迭代器

3、const迭代器 (正向反向)

五、OJ练习

反转字符

找出字符串中出现一次的字符

前言

string类模板如下,为什么会有好几个呢?

这些不同的string类模板是为了处理不同的字符编码和字符集。每个模板都专门用于处理特定类型的字符数据。

std::string:这是最常见的string类模板,用于处理ASCII字符集。它使用单字节字符表示,适用于大多数常规字符串操作。

std::wstring:这是宽字符版本的string类模板,用于处理Unicode字符。它使用wchar_t类型来表示字符,适用于需要处理多语言字符集的情况。

std::u16string:这是用于处理UTF-16编码的字符串的模板。UTF-16使用16位编码表示字符,适用于处理较大的字符集,如大部分Unicode字符。

std::u32string:这是用于处理UTF-32编码的字符串的模板。UTF-32使用32位编码表示字符,适用于处理包含所有Unicode字符的字符集。

这些不同的string类模板提供了对不同字符编码和字符集的支持,以便在处理不同类型的文本数据时能够正确地表示和操作字符。通过选择适当的string类模板,可以确保在不同的应用场景中正确处理和操作字符数据。

我们先来了解一下计算机存储字符的方式:

计算机中都是二进制形式,无法直接存储字母和符号,这时就需要一个映射表,ASCll就诞生了。

为了可以在计算机上显示多个国家的文字,unicode诞生了

统一码(Unicode),也叫万国码、单一码,由统一码联盟开发,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。

统一码是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode字符编码方案分为UTF-8、UTF-16和UTF-32,它们被设计用于在计算机系统中表示和处理不同范围的Unicode字符。

UTF-8:UTF-8是一种变长编码方案,它使用1到4个字节来表示不同的Unicode字符。它是最常用的Unicode编码方案之一,因为它可以兼容ASCII字符集,并且在表示常见字符时比较节省空间。UTF-8适用于在存储和传输文本数据时节省空间的情况,特别是在互联网和计算机网络中广泛使用。

UTF-16:UTF-16是一种定长或变长编码方案,它使用16位编码来表示Unicode字符。对于大部分常见的Unicode字符,UTF-16使用16位编码表示,但对于一些较少使用的字符,它需要使用两个16位编码来表示。UTF-16适用于需要处理较大字符集的情况,如多语言文本处理和国际化应用。

UTF-32:UTF-32是一种定长编码方案,它使用32位编码来表示Unicode字符。每个Unicode字符都使用32位编码表示,无论字符是否常见。UTF-32适用于需要处理包含所有Unicode字符的字符集的情况,如某些特定领域的文本处理和字符级操作。

这些不同的Unicode编码方案提供了不同的权衡和适用性,根据具体的需求和应用场景,可以选择适当的编码方案来表示和处理Unicode字符。

其中,UTF-8使用的最多 。

我们中国也有针对汉字的GBK编码,对一些生僻字提供了支持。

一、string类

文档介绍

字符串是表示字符序列的类,标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。 string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。 string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:

string是表示字符串的字符串类 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。 string在底层实际是:basic_string模板类的别名,typedef basic_string string; 不能操作多字节或者变长字符的序列。

在使用string类时,必须包含#include头文件string

二、初始化

默认构造函数 string():创建一个空字符串对象。

复制构造函数 string(const string& str):通过复制另一个字符串对象 str 来创建一个新的字符串对象。

子字符串构造函数 string(const string& str, size_t pos, size_t len = npos):从字符串对象 str 的指定位置 pos 开始,创建一个新的字符串对象,长度为 len。如果未提供 len 参数,默认创建到字符串末尾的子字符串。

从C-String构造函数 string(const char* s):从以空字符结尾的C字符串 s 创建一个新的字符串对象。

从序列构造函数 string(const char* s, size_t n):从C字符串 s 的前 n 个字符创建一个新的字符串对象。

填充构造函数 string(size_t n, char c):创建一个包含 n 个字符 c 的新字符串对象。

范围构造函数 template string(InputIterator first, InputIterator last):通过迭代器范围 [first, last) 中的字符创建一个新的字符串对象。这个构造函数可以接受不同类型的迭代器,例如指针、容器的迭代器等。

1、无参或带参

既可以无参初始化, 也可以带参初始化。

int main()

{

string s1;

string s2("hello world");

string s3 = "hello";

return 0;

}

我们也可以通过[ ]操作符访问字符串某个位置。

#include

int main()

{

string s2("hello world");

for (size_t i = 0; i < s2.size(); ++i) {

s2[i]++;

}

cout << s2 << endl;

return 0;

}

我们可以通过流插入运算符<<输出string类对象。

这是因为string类中对流插入运算符<<进行了重载,流提取也进行了重载。

int main()

{

string s2;

cin >> s2;

for (size_t i = 0; i < s2.size(); ++i) {

s2[i]++;

}

cout << s2 << endl;

return 0;

}

2、用字符串变量初始化

用一个字符的指定位置开始指定字符个数为另一个字符初始化。

int main()

{

string s3 = "hello";

string s4(s3, 2, 3);

cout << s4 << endl;

return 0;

}

如果取字符的个数超过字符总长度,则取到末尾即可。

int main()

{

string s3 = "hello";

string s4(s3, 2, 3);

cout << s4 << endl;

string s5(s3, 2, 10);

cout << s5 << endl;

return 0;

}

第三个参数取字符整数可以缺省,那么从指定位置开始取到末尾结束。

int main()

{

string s3 = "hello";

string s4(s3, 2, 3);

cout << s4 << endl;

string s5(s3, 2);

cout << s5 << endl;

return 0;

}

3、用字符串初始化

也可以把要赋值的字符串直接放在第一个参数位置,第二个参数为赋值字符个数。

int main()

{

string s7("hello world", 5);

cout << s7 << endl;

string s8("hello world", 5 , 6);

cout << s8 << endl;

return 0;

}

第一个构造函数中,除字符串如果只有一个参数,则默认从字符串第一个字符开始,赋值第一个参数大小长度。第二个构造函数中,除字符串如果有两个参数,第一个参数为指定起始位置,第二个参数为指定的初始化长度,长度大于实际字符串的长度,则截断为实际字符串的长度。

4、指定数量字符

int main()

{

string s9(10, '$');

cout << s9 << endl;

return 0;

}

三、容量操作

1、size

size()与length()底层实现原理完全相同,都用于获取字符串有效字符长度,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。

int main()

{

string s1("hello world");

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

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

return 0;

}

下面两个了解即可 :

max_size()函数返回一个无符号整数,表示字符串对象可以容纳的最大字符数。这个值通常取决于系统的限制,因此可能会因操作系统和编译器而异。capacity()函数返回一个无符号整数,表示字符串对象当前分配的内存空间大小。这个值可能大于字符串实际包含的字符数,因为字符串类通常会预留一些额外的空间以便进行扩展。

int main()

{

string s1("hello world");

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

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

return 0;

}

64位下输出结果:

2、push_back

添加单个字符到字符串末尾

int main()

{

string s1("hello");

s1.push_back(' ');

s1.push_back('!');

cout << s1 << endl;

return 0;

}

3、append

我们还可以使用append在字符串末尾添加单个字符或字符串 ,这种是最常用的形式。

int main()

{

string s1("hello");

s1.push_back(' ');

s1.push_back('!');

cout << s1 << endl;

s1.append("world");

cout << s1 << endl;

}

4、+=运算符

我们还可以使用+=运算符在字符串末尾添加字符或字符串,+=底层是调用push_back或append,在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。

int main()

{

string s1("hello");

s1 += ' ';

s1 += '!';

s1 += "world";

cout << s1 << endl;

}

5、vs下string的结构

先看下面代码:

int main()

{

string s;

size_t sz = s.capacity();

cout << "making s grow:\n";

cout << "capacity changed: " << sz << '\n';

for (int i = 0; i < 100; ++i)

{

s.push_back('c');

if (sz != s.capacity())

{

sz = s.capacity();

cout << "capacity changed: " << sz << '\n';

}

}

}

上述演示了如何使用push_back()函数向字符串对象中添加字符,并观察字符串的容量变化

首先,在main()函数中声明了一个size_t类型的变量sz,并将其初始化为字符串对象s的容量。然后,通过使用cout对象和<<运算符,输出一条提示信息"making s grow:"。接下来,使用一个循环,从0到99,逐个向字符串对象s中添加字符'c'。在每次添加字符之后,通过比较之前记录的容量sz和当前字符串对象s的容量,来判断容量是否发生了变化。如果容量发生了变化,将新的容量值赋给sz,并使用cout对象和<<运算符输出一条提示信息"capacity changed:",以及新的容量值。

这里输出的容量不包括 \0,也就是实际容量要再加1.

在监视中可以看到,实际上将字符存入了_Buf数组中。

在C++的实现中,std::string类通常使用两个数组来存储字符串的字符。当字符串的长度小于等于15个字符时(第十六个为\0),字符串的字符被存储在一个名为_Buf的内部固定大小数组中,该数组长度为15。这样可以避免动态内存分配,提高性能。

当字符串的长度超过15个字符时,字符串的字符将被存储在一个名为_Ptr的动态分配的数组中,该数组的长度将根据需要进行动态调整。这样可以容纳更长的字符串,并且可以根据需要动态分配内存。

这种设计可以在字符串较短时节省内存,并在字符串较长时提供足够的存储空间。具体的实现可能会因编译器和标准库的不同而有所差异,但这种区分短字符串和长字符串的策略是常见的优化技术之一

vs下string的结构:string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定string中字符串的存储空间:

当字符串长度小于等于15时,使用内部固定的字符数组来存放 当字符串长度大于等于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个字节。

输出查看一下string类对象s的大小

string s;

cout << sizeof(s) << endl;

可以看到是28,那么它在堆上起始就是大小32。

观察扩容情况可以发现从大小32开始,每次1.5倍扩容。

6、g++下string的结构

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指

针将来指向一块堆空间,内部包含了如下字段:

空间总大小 字符串有效长度 引用计数

struct _Rep_base

{

size_type _M_length;

size_type _M_capacity;

_Atomic_word _M_refcount;

};

指向堆空间的指针,用来存储字符串。

7、reserve

在已知所需空间大小时,我们可以使用reserve提前开辟空间,减少扩容,提高效率。

使用reverse一次性开辟100个字符空间。

int main()

{

string s;

s.reserve(100);

size_t sz = s.capacity();

cout << "making s grow:\n";

cout << "capacity changed: " << sz << '\n';

for (int i = 0; i < 100; ++i)

{

s.push_back('c');

if (sz != s.capacity())

{

sz = s.capacity();

cout << "capacity changed: " << sz << '\n';

}

}

}

这时就不需要一次次扩容了。

对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

8、resize

resize将字符串大小调整为n个字符的长度。

如果n小于当前字符串长度,则将当前值缩短到前n个字符,删除第n个字符以外的字符。如果n大于当前字符串长度,则通过在末尾插入尽可能多的字符0来扩展当前内容,以达到n的大小。如果指定了填充字符c,则新元素被初始化为c的副本,否则,它们是值初始化的字符(空字符)。

int main()

{

// 扩容

string s1("hello world");

s1.reserve(100);

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

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

// 扩容+初始化

string s2("hello world");

s2.resize(100);

cout << s2.size() << endl;

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

return 0;

}

resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。 reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小。在Visual Studio中,std::string的实现可能会有特定的内部行为,包括如何管理内存的分配和扩容。输出结果表明,当请求更多容量时(使用reserve(100)或resize(100)),std::string可能会预分配比请求更多的内存,这是为了减少将来可能的内存重新分配次数,提高效率。在这两种情况下,std::string的容量是相同的(111),这表明Visual Studio的std::string实现在内部管理容量时可能有一个基于当前请求和可能的未来需求的预测模型,从而选择了一个“圆整”后的容量值来避免频繁的内存分配和复制操作,这是一种常见的性能优化手段。这种预分配策略在不同的编译器和库实现中可能会有所不同,具体取决于实现的细节。

四、迭代器

1、正向迭代器

begin指向字符串第一个字符,end指向字符串最后一个字符的后一个位置,功能和指针类似。

int main()

{

string s1("hello world");

string::iterator it = s1.begin();

while (it != s1.end()) {

cout << *it << " ";

++it;

}

return 0;

}

范围for底层就是调用迭代器实现的。

int main()

{

string s1("hello world");

string::iterator it = s1.begin();

while (it != s1.end()) {

cout << *it << " ";

++it;

}

cout << endl;

for (auto ch : s1) {

cout << ch << " ";

}

cout << endl;

}

2、反向迭代器

rbegin指向字符串最后一个字符,rend指向字符串的第一个字符的前一个位置。

int main()

{

string::reverse_iterator rit = s1.rbegin();

while (rit != s1.rend()) {

cout << *rit << " ";

++rit;

}

cout << endl;

return 0;

}

3、const迭代器 (正向反向)

const正向反向迭代器只能遍历和读数据。

int main()

{

string s1("hello world");

string::const_iterator it = s1.begin();

while (it != s1.end()) {

cout << *it << " ";

++it;

}

cout << endl;

string::const_reverse_iterator rit = s1.rbegin();

while (rit != s1.rend()) {

cout << *rit << " ";

++rit;

}

cout << endl;

cout << s1 << endl;

return 0;

}

这时可以借助auto自动推导类型。

int main()

{

//string::const_iterator it = s1.begin();

auto it = s1.begin();

while (it != s1.end()) {

cout << *it << " ";

++it;

}

cout << endl;

//string::const_reverse_iterator rit = s1.rbegin();

auto rit = s1.rbegin();

while (rit != s1.rend()) {

cout << *rit << " ";

++rit;

}

cout << endl;

return 0;

}

五、OJ练习

反转字符

917. 仅仅反转字母 - 力扣(LeetCode)

采用快速排序的思路。

class Solution {

public:

string reverseOnlyLetters(string s) {

size_t begin = 0, end=s.size()-1;

while(begin

while(begin

++begin;

while(begin

--end;

swap(s[begin],s[end]);

++begin;

--end;

}

return s;

}

};

找出字符串中出现一次的字符

387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

采用计数排序的思路。

class Solution {

public:

int firstUniqChar(string s) {

int count[26]={0};

for(auto ch:s){

count[ch-'a']++;

}

for(int i=0;i

if(count[s[i]-'a']==1)

return i;

}

return -1;

}

};

相关推荐

相亲对象第一次见面就约我泡温泉,这是几个意思?_手机网易网
联想笔记本怎么进入pe系统_联想笔记本电脑怎么进入PE?
打印机如何远程控制打印?不在家/公司时,异地怎么连接使用打印文件