1、为什么要学习班? C语言中的字符串
在C语言中,字符串是以'\0'结尾的字符的集合。 为了操作方便,C标准库提供了一些str系列库函数,但这些库函数与字符串分离,不是很具体。 它符合OOP的思想,底层空间需要用户自己管理。 如果你不小心,你可能会越界访问。
算法题的运用
在OJ中,关于字符串的问题基本都是以类的形式出现,而在平时的工作中,为了简单、方便、快捷,基本都是使用类。 很少有人使用C库中的字符串操作函数。
2.标准库中的类
我们可以使用这个网站-C++来了解它在对方库中是如何定义的。
文档介绍
总结:
使用类时,必须包含#和using std;
底层介绍
我们从文档介绍中知道的类是一个类模板,大致如下:
typedef basic_string string;
template
class basic_string
{
private:
T* str;
//...
};
我们从来没有想过为什么类型是T,类型不就是char类型吗? 还有其他类型吗?
这个地方和有关。 计算机是美国人发明的。 早期,计算机只显示英文。 显示英文比较简单。 只需要26个英文字母即可组合。 算上大小写字母和标点符号,总共只有128个,足以代表常见的英语。 计算机通过建立数值与相应符号之间的映射关系来存储英文(仅二进制)代码。 这种映射关系称为编码表。 英语的编码表是ASCII表,用来表示英语。
ASCII码值的含义是:你所代表的一个字符存储在内存中时对应的整数值(以二进制存储)
例如1:大写字母A的ASCII码表示它以数字65的二进制形式存储在机器内存中。
eg2:对于这个字符串数组,实际上存储的是这些字符对应的值(语言规定字符串以\0结尾)
int main()
{
char str[] = "hello";
return 0;
}
后来为了在全世界普及计算机,就不能只适用于英语了。 于是,出现了一个全局的文本编码表,包括ASCII、utf-8、utf-16、utf-32等。
中文编码
在英语中,每个字母对应一个值,一个字节有8位,有2^8(256)种状态,所以英语很容易表达。 如果用一个字符来表示一个汉字,最多只能表示256个汉字,这是远远不够的。 一个字节不够,所以两个字节一起使用。 两个字节有2^16种状态,可以表示65000多个汉字。 但是,如果表示一个汉字的位数越多,就意味着该汉字所占用的空间。 越大,这就越糟糕。 UTF-8的意思是用2个字节来写一些常见的汉字,用3或4个字节来写一些生僻的字符,并规定了一系列的规则。 不同的值对应不同的汉字...,Linux下默认是utf-8。
eg:存储的是对应的值。 如果要显示的话,用这些值去对应的表中查一下即可。
int main()
{
char str[] = "喝水";
return 0;
}
另外一个有趣的事情是,当我们使用++时,文本的内容会发生变化。
所有编译器都允许您选择编码。 如果编译器的编码与你写的不对应,就会出现乱码。 国内读者较多,所以我定制了中文的编码表--gdk,默认编码是gdk,
由于一些编码的原因,有些字符串会用两个字符来表示,所以有; 如果有,建议使用(匹配)
头文件
头文件是#. 使用时记得添加。
如果不包含头文件的话,这样写是可以编译通过的。
但是使用流插入和流提取时会报错()
但不是
博主还是建议包含这个头文件,总比没有好。
3.类和构造函数的常用接口描述
给星的很重要,其他的都可以理解
(1) 无参数构造、带参数构造、复制构造
int main()
{
string s1; //无参构造
string s2("hello world"); //带参的
string s3(s2); //拷贝构造
cin >> s1;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
(2)
string (const string& str, size_t pos, size_t len = npos);
pos 表示从该位置开始。 Len 代表副本的长度。 这个len给出了一个默认值npos,表示取从该位置开始的所有后续字符,直到结束。 ps:npos的值为-1,对于来说-1就是整数的最大值(-1的补码都是1,对于来说就是整数的最大值)
int main()
{
string s2("hello world"); //带参的
string s4(s2, 2, 6);
cout << s4 << endl;
}
s4:从s2的第二个位置开始,复制以下6个字符。
例如2:
int main()
{
string s2("hello world"); //带参的
string s5(s2, 2, 100);
cout << s5 << endl;
string s6(s2, 2);
cout << s6 << endl;
}
↑s5:从s2的第二个位置开始,复制接下来的100个字符。
s6:从s2的第二个位置开始,复制所有后续字符(证明默认值npos)。
(3)
string (const char* s, size_t n);
int main()
{
string s2("hello world"); //带参的
string s7("hello world",3);
cout << s7 << endl;
}
s7:初始化hello world的前三个字母,
(4)
string (size_t n, char c);
int main()
{
string s8(10, '!'); //看起来有用实际没用
cout << s8 << endl;
}
s8: 使用10!初始化类对象的容量操作
(1)计算物体的长度尺寸和长度
int main()
{
string s1;
cin >> s1;
//不包含最后作为标识符的\0,算的是有效字符的长度
cout << s1.size() << endl; //重要
cout << s1.length() << endl; //了解
}
(2)
功能:告诉我们这个字符串可以有多长(与内存有关)
(3)
功能:告诉我们这个字符串的容量有多大(即最多可以存储多少个字符)
虽然显示为15,但实际空间是16,因为有一个\0,但容量是指可以存储多少个有效字符。
(4)清除
功能:清除所有有效数据并预留空间
(5)中的容量增加如下。 以下是自动扩容的程序。 从结果可以得出,产能增幅一般会是1.5倍。
void TestPushBcak()
{
string s;
size_t sz = s.capacity();
cout << "capacity changed: " << sz << endl;
cout << "making s grow:\n";
for (int i = 0; i < 1000; ++i)
{
s += 'c';
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
int main()
{
TestPushBcak();
}
但每一次容量的增加都是有代价的,而且这个代价还不小。 如果您已经知道需要多少空间,则可以利用降低容量增加的成本。
它只打开空间并影响容量,容量是可以更改的。
void TestPushBcak()
{
string s;
s.reserve(1000); //申请至少能存储1000个字符的空间
size_t sz = s.capacity();
cout << "capacity changed: " << sz << endl;
cout << "making s grow:\n";
for (int i = 0; i < 1000; ++i)
{
s += 'c';
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
int main()
{
TestPushBcak();
}
它将字符串中的有效字符数更改为 n。 当字符数增加时,会给额外的空间赋予一个初始值,用于初始化。 默认初始值为/0;
一开始,s1中的大小为0和15
使用后大小扩展为100,由于大小变化而变化,这100个大小用/0填充。
还可以自定义初始值
与 rsize 的异同
它只是简单地改变容量。 rsize 改变有效字符数,从而改变容量,并初始化改变后的有效字符数。
void Test()
{
string s1;
s1.reserve(100);
string s2;
s2.resize(100);
}
int main()
{
Test();
}
使用扩容,不会影响之前的数据
但如果减小大小,数据就会被删除。
更有用的是。
(7) 空
功能:判断字符串是否为空
类对象的访问和遍历操作
(1) [ ] 遍历字符串
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
eg1:遍历字符
int main()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++) //读每个位置的字符
{
//cout << s1.operator[](i) << " ";//本质调用
cout << s1[i] << " ";
}
cout << endl;
}
eg2:修改字符,每个字符+1后改变
int main()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++) //读每个位置的字符
{
//cout << s1.operator[](i) << " ";//本质调用
cout << s1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < s1.size(); i++) //写每个位置的字符,operator[]的返回值是这个地方的引用
{
s1[i] += 1;
}
cout << endl;
cout << s1 << endl;
}
为什么可以修改呢?
因为[]的返回值是char类型的引用。
//实际底层实现
char& operator[](size_t pos)
{
//...
return _str[pos];
}
//底层是一个字符串数组,如果想修改第i个位置的字符,就返回这第[i]个位置的引用
编译器会将其转换为s1.[](i),然后这个函数调用就会有一个返回值。 该返回值是对第 i 个位置处的字符的引用。 如果添加它,则赋值可以修改第 i 个字符。 value,所以这里参考的是修改这个地方的值。 当作用域超出作用域时,对象还在,因为这个数组是在堆上打开的,超出作用域时堆不受影响。
这里的引用返回不是为了减少副本,而是为了支持修改对象
(2)迭代器遍历
使用方法及说明:
eg:修改和遍历字符
int main()
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = s1.begin();
while (it != s1.end())
{
*it += 1;
++it;
}
cout << endl;
it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
}
它有点像指针,但不一定是指针。 这里可以认为是一个指针。 该迭代器是嵌入类型,并且是在类中定义的,因此必须指定类域。
将迭代器视为类似指针的类型
PS:第二次调用的时候,就不用再写::了,也不需要定义了,但是还是要重新初始化一次。
(3)Range for自动向后迭代并自动判断结束(仅C++11中支持)eg1:遍历字符
int main()
{
string s1("hello world");
for (auto e : s1)
{
cout << e << " ";
}
}
eg2:修改字符
int main()
{
string s1("hello world");
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
for (auto& e : s1)
{
e += 1;
}
for (auto e : s1)
{
cout << e << " ";
}
}
(4)反向迭代器向后遍历
int main()
{
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
}
ps: 也可以使用auto自动推出它是一个迭代器。
问题1:迭代器遍历的意义是什么?
因为,无论向前还是向后遍历,下标+[]都好用,为什么还需要迭代器呢? 为此,使用下标和 [ ] 就足够了,并且确实可以不使用迭代器。 但是其他容器(数据结构)呢?
迭代器的意义在于所有容器都可以使用迭代器来修改访问。 而如果你知道一个容器的迭代器,你几乎就可以知道其他容器的迭代器。 所以对于 ,你必须能够使用迭代器,但是对于 ,一般我们还是喜欢下标+[ ]。
例如:list、map/set,不支持下标+[]遍历
问题2:使用迭代器时,可以将!=改为<或>吗?
答:对于 来说是可以的,因为它的底层是数组,但是对于其他容器来说是不可能的,所以建议使用 != ,不建议使用 > 或