C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
字符串转整形数字
字符串相加
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
string库的学习:string类的文档介绍
在库中我们可以看到,string类的构造函数是很丰富的.
重点掌握框起来的四个,其他的忘记了咱可以查文档.
(constructor) 函数名称 | 功能说明 |
---|---|
string() (重点) | 构造空的string类对象,即空字符串 |
string(const char * s) (重点) | 用C-string来构string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) (重点) | 拷贝构造函数 |
void test1()
{
//无参构造 string();
string s1;
cout << "s1= " << s1 << endl;
//拷贝构造 copy, string (const string& str);
s1 += "NiNi_suanfa"; //下面会讲,这里是为了s1里面有数据,方便拷贝构造
string s2(s1);
cout << "s2= " << s2 << endl;
//用另一个string类的字串初始化 string (const string& str, size_t pos, size_t len = npos); //substring(子字符串):从pos位置开始,到后面len个位置这个区间的字符串 用作构造
string s3(s1, 6,4);
cout << "s3= " << s3 << endl;
//使用字符串进行初始化 from c-string: string (const char* s);
string s4("NiNi_suanfa,123456");
cout << "s4= " << s4 << endl;
//from sequence: //string(const char* s, size_t n);
string s5("NiNi_suanfa,123456",7);//不常用
cout << "s5= " << s5 << endl;
//string(size_t n, char c); //fill :使用n个c字符构造
string s6(5, 'X');//不常用
cout << "s6= " << s6 << endl;
}
运行结果:
s1=
s2= NiNi_suanfa
s3= uanf
s4= NiNi_suanfa,123456
s5= NiNi_su
s6= XXXXX
我们看一下库中对capacity(容量)的相关操作有哪些.
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间** |
resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
其实 size()
和 length()
并没有本质区别.
都是用于返回 string 中字符串的有效字符长度.
但是,由于 string 实现的比较早,当时设计的是 length()
,后来 STL 出来以后,为了统一,增加了 size()
接口.
string s1;
string s2("hello");
//size和length并没有什么区别.
cout << s1.size() << " " << s1.length() << endl;
cout << s2.size() << " " << s2.length() << endl;
运行结果
0 0
5 5
resize()
:改变字符串的 有效字符长度,不够的地方用第二个参数填充 string s3("Hello World.");
s3.resize(5); //将字符串的有效字符长度改为5
cout << s3 << endl;
string s4("Hello World.");
s4.resize(25,'x'); //将字符串的有效字符长度改为25,不够的地方用字符'x'填充
cout << s4 << endl;
运行结果:
Hello
Hello World.xxxxxxxxxxxxx
resize()的改变会影响capacity(容量)吗?
string s5("HELLO CSDN!!!");
cout << "s5.capacity=" << s5.capacity() << endl;
s5.resize(25, 'x'); //增大size的大小
cout << "s5.capacity=" << s5.capacity() << endl;
s5.resize(5, 'x'); //再减小size的大小
cout << "s5.capacity=" << s5.capacity() << endl; //并没有缩容
运行结果:
s5.capacity=15
s5.capacity=31
s5.capacity=31
当然,如果容量太小,不足以存储有效字符,必然是会扩容的!
扩容选择:(扩容方式是未定义的)
扩容是 按有效字符长度扩容 。按之前容量的1.5倍扩容,更或者是2倍扩容 。
reserve()
:请求改变 容量 的大小. string s6("HELLO CSDN!!!");
cout << "s6.capacity=" << s6.capacity() << endl;
s6.reserve(50);
cout << "s6.capacity=" << s6.capacity() << endl;
s6.reserve(30);
cout << "s6.capacity=" << s6.capacity() << endl; //并没有缩容
//一般都是不缩容的,缩容行为是未定义的.
s6.clear();
s6.reserve(0);
cout << "s6.capacity=" << s6.capacity() << endl; //这里缩容了
s6.capacity=15
s6.capacity=63
s6.capacity=63
s6.capacity=15
是否缩容是未定义行为,取决于编译器;
这里 如果不清楚数据,直接将reserve(0),依旧不会缩容.
流提取 会覆盖 原数据
若 clear()中 不将 str[0]=‘\0’,而是会将 size = 0 ;
则 c_str() 是 遇到 ‘\0’ 才停
而 s1 是一个一个字符这样拷贝过去,但因为size=0 所以终止了.
string s7;
cout << s7.empty() << endl;
s7 += "HELLO";
cout << s7.empty() << endl;
cout << "s7.size=" << s7.size() << endl;
cout << "s7.capacity" << s7.capacity() << endl;
s7.clear();
cout << "s7.size=" << s7.size() << endl;
cout << "s7.capacity" << s7.capacity() << endl;
运行结果:
1
0
s7.size=5
s7.capacity15
s7.size=0
s7.capacity15
显然 clear
只是清除有效字符,将字符清零,并不会影响capacity容量 。
size()
和 length()
底层实现原理是一样的,都是返回有效的字符个数. 只是为了STL的接口相统一.resize(size_t n)
默认用 0
来填充多出的元素空间
resize(size_t n)
与 resize(size_t n, char c)
都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。
注意:resize
在 改变元素个数 时
(1) 如果是将 元素个数增多,可能会改变底层容量的大小,不然存储不了那么多有效字符.
(2) 如果是将 元素个数减少,底层空间总大小不变 。
reserve((size_t res_arg=0))
函数 是 请求改变string的容量.clear
只是清除有效字符,将字符清零,并不会影响capacity容量.迭代器才是容器访问的主流形态 【通用性】
迭代器都是左闭右开,begin 左闭,end 右开(最后一个位置的下一个位置)
const_iterator it :本质 保护 迭代器指向的数据 * it 不能修改
const iterator it :保护的 选代器本身 不能修改 it不能修改
下标访问符 方括号[ ]
重载注意有两种重载:
void test3()
{
string s1("This is a little boy");
string::iterator it = s1.begin(); //s1.begin()会返回有效字符串中第一个元素的位置
while (it != s1.end()) //s1.end()会返回有效字符串最后一个元素的位置的后一个位置
{
cout << *it ;
it++;
}
cout << endl;
string::reverse_iterator rit = s1.rbegin(); //反向迭代器
while (rit != s1.rend()) //s1.rend()就相当于封装了s1.begin(),会返回有效字符串中第一个元素的位置
{
cout << *rit;
rit++;
}
cout << endl;
cout << "s1.begin=" << *(s1.begin()) << endl;
cout << "s1.end=" << *(s1.end()-1) << endl; //不可直接访问s1.end(),因为不是有效字符,而是最后一个有效字符的下一个位置.
cout << "s1.rbegin=" << *(s1.rbegin()) << endl;
cout << "s1.rend=" << *(s1.rend()-1) << endl; //这里为什么是+1而不是-1,留在后面的专门反向迭代器讲解
//可以像数组一样用下标直接访问
cout << s1[0] << endl;
cout << s1[3] << endl;
cout << s1[8] << endl;
}
下标 + [ ]
对比:下标+[ ]
只适用于 部分容器,底层物理有一定连续迭代器才是容器访问主流形态
interator 迭代器 用法像指针一样,用指针的方式进行遍历访问
#define _CRT_SECURE_NO_WARNINGS
#include
using namespace std;
#include
// 测试string容量相关的接口
// size/clear/resize
void Teststring1()
{
// 注意:string类对象支持直接用cin和cout进行输入和输出
string s("hello, bit!!!");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << s << endl;
// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
// “aaaaaaaaaa”
s.resize(10, 'a');
cout << s.size() << endl;
cout << s.capacity() << endl;
// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
// "aaaaaaaaaa\0\0\0\0\0"
// 注意此时s中有效字符个数已经增加到15个
s.resize(15);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
// 将s中有效字符个数缩小到5个
s.resize(5);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
}
//====================================================================================
void Teststring2()
{
string s;
// 测试reserve是否会改变string中有效元素个数
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
s.reserve(50);
cout << s.size() << endl;
cout << s.capacity() << endl;
}
// 利用reserve提高插入数据的效率,避免增容带来的开销
//====================================================================================
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
// 构建vector时,如果提前已经知道string中大概要放多少个元素,可以提前将string中空间设置好
void TestPushBackReserve()
{
string s;
s.reserve(100);
size_t sz = s.capacity();
cout << "making s grow:\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的遍历
// begin()+end() for+[] 范围for
// 注意:string遍历时使用最多的还是for+下标 或者 范围for(C++11后才支持)
// begin()+end()大多数使用在需要使用STL提供的算法操作string时,比如:采用reverse逆置string
void Teststring3()
{
string s1("hello Bit");
const string s2("Hello Bit");
cout << s1 << " " << s2 << endl;
cout << s1[0] << " " << s2[0] << endl;
s1[0] = 'H';
cout << s1 << endl;
// s2[0] = 'h'; 代码编译失败,因为const类型对象不能修改
}
void Teststring4()
{
string s("hello Bit");
// 3种遍历方式:
// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
// 另外以下三种方式对于string而言,第一种使用最多
// 1. for+operator[]
for (size_t i = 0; i < s.size(); ++i)
cout << s[i] << endl;
// 2.迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << endl;
++it;
}
// string::reverse_iterator rit = s.rbegin();
// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
auto rit = s.rbegin();
while (rit != s.rend())
cout << *rit << endl;
// 3.范围for
for (auto ch : s)
cout << ch << endl;
}
//
// 测试string:
// 1. 插入(拼接)方式:push_back append operator+=
// 2. 正向和反向查找:find() + rfind()
// 3. 截取子串:substr()
// 4. 删除:erase
void Teststring5()
{
string str;
str.push_back(' '); // 在str后插入空格
str.append("hello"); // 在str后追加一个字符"hello"
str += 'b'; // 在str后追加一个字符'b'
str += "it"; // 在str后追加一个字符串"it"
cout << str << endl;
cout << str.c_str() << endl; // 以C语言的方式打印字符串
// 获取file的后缀
string file("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size() - pos));
cout << suffix << endl;
// npos是string里面的一个静态成员变量
// static const size_t npos = -1;
// 取出url中的域名
string url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;
size_t start = url.find("://");
if (start == string::npos)
{
cout << "invalid url" << endl;
return;
}
start += 3;
size_t finish = url.find('/', start);
string address = url.substr(start, finish - start);
cout << address << endl;
// 删除url的协议前缀
pos = url.find("://");
url.erase(0, pos + 3);
cout << url << endl;
}
int main()
{
return 0;
}
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串str |
c_str (重点) | 返回C格式字符串 |
find + npos (重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
void test4()
{
string s1("hello C");
cout << "s1=" << s1 << endl;
//尾插一个字符
s1.push_back('S');
s1.push_back('D');
s1.push_back('N');
cout << "s1=" << s1 << endl;
cout << "----------------------------------" << endl;
string s2("hello C");
cout << "s2=" << s2 << endl;
s2.append("SDN"); //追加字符串
cout << "s2=" << s2 << endl;
cout << "----------------------------------" << endl;
string s3("hello C");
cout << "s3=" << s3 << endl;
s3 += "SDN"; //最喜欢使用这个,易读也简单
cout << "s3=" << s3 << endl;
}
小结:
push_back
一次插入一个字符太麻烦了;append
虽然可以追加字符串,但是终究是没有 +=
来的香.
其它的以 assign 为例,一般用不到 (因为实现的有些冗余,可以用别的函数代替) ,实在要用查库即可:
void test5()
{
string str("This is a little boy");
string s1,s2,s3;
s1.assign(str);
s2.assign(str, 8, string::npos);
s3.assign(5, 'c');
cout << "s1=" << s1 << endl;
cout << "s2=" << s2 << endl;
cout << "s3=" << s3 << endl;
}
运行结果:
s1=This is a little boy
s2=a little boy
s3=ccccc
c_str
:为了与C语言兼容,返回C形式的常量字符串.
find
:可以查找目标字符/字符串.
string substr (size_t pos = 0, size_t len = npos) const
:从pos往后len个字符,返回这段被切割的字符串的副本.
void test6()
{
string s1("This is a little boy");
const char* arr = s1.c_str(); //返回C形式的常量字符串
cout << "arr=" << arr << endl;
string s2("This is a little boy");
cout << s2.find('i') << endl; //查找目标字符
cout << s2.find("little") << endl; //查找目标字符串
string s3("[email protected]");
int pos1 = s3.find('@');
int pos2 = s3.find(".com");
string s4, s5, s6;
s4 = s3.substr(0, pos1-1); //从0位置开始,往后pos-1个字符
s5 = s3.substr(pos1, s3.size() - pos2 - 1);
s6 = s3.substr(pos2); //第二个参数为往后的字符个数,不写,默认为npos
cout << "s4= " << s4 << endl;
cout << "s5= " << s5 << endl;
cout << "s6= " << s6 << endl;
}
运行结果:
arr=This is a little boy
2
10
s4= 321xxxxxx
s5= @qq
s6= .com
流提取 会覆盖 原数据
若 clear()中 不将 str[0]=‘\0’,而是会将 size = 0 ;
则 c_str() 是遇到 ‘\0’ 才停
所以还能读到原来的数据
而s1 是一个一个字符这样拷贝过去,但因为size=0 所以终止了
getchar()是C语音的
in.get()是C++的
可以设置成和C语言io流同步
npos
是 -1 ,只不过类型是 const size_t,所以是整数的最大值,通常表示字符串的结尾或无效位置 。npos定义在std命名空间中,通常用于字符串的查找操作。若len = npos 了npos = -1 就已经是最大值了 再加上pos 就越界溢出了
在这里 npos = -1 不叫初始化,而是 缺省值 =》会走 初始化列表
但这是 static 静态全局变量 属于整个类 而不是属于某个对象 ,并不是单独给某一个对象进行初始化 。
所以 static静态全局变量 要 类里声明 类外定义 。
但这里是有一个 特殊处理:加上 const 就能 在类里初始化了
编译器 的特殊处理:既算是声明,也算是定义(带有很强的误导性)
并且 只支持整数,浮点数不支持 。
函数 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> (重点) | 输入运算符重载 |
operator<< (重点) | 输出运算符重载 |
getline (重点) | 获取一行字符串 |
relational operators (重点) | 大小比较 |
上面的几个接口大家了解一下,下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的
操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
+
运算符重载 与 getline ()+
运算符重载void test7()
{
string s1("HELLO ");
string s2("CSDN");
string s3;
s3 = s1 + s2; //因为是传值返回,所以效率不高,建议少用
cout << s3 << endl;
string name;
cout << "Please, enter your full name: ";
getline(cin, name);
cout << "Hello, " << name << "!\n";
}
浅拷贝:也称 位拷贝,编译器只是将对象中的值拷贝过来。
问题出现的情况:
如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。
问题解决
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了
如果一个类中 涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。 一般情况都是按照 深拷贝方式 提供。
有个浅拷贝 传值返回 返回的是 str的拷贝 没有写拷贝构造
出现的状况:str里有指向一段空间的指针 ,而浅拷贝 直接将该空间的地址赋值给 临时对象也指向这个空间
s1出了作用域 对象自动调用了析构函数,内置类型默认不处理,空间被销毁,但s2浅拷贝的指针还指向这块空间 出现了野指针的问题
若 s1 远远大于 s3 ,s3太小了,直接拷贝过去是对后面的空间的一种浪费;s3大于s1,则会造成越界。
所以直接开辟新空间,释放旧的空间,简单明了就好了
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
cin scanf 规定:输入多个值时,默认空格和换行作分隔符,所以碰到 空格 和 换行 \n 会忽略掉
现代写法:复用
传参 临时拷贝 复用拷贝构造
s 出了作用域 调用析构函数
上面这个版本不需要检查是否自己调用自己,因为这里是 传值拷贝 。
注意:这是权限的放大:
返回的是str的临时拷贝,临时对象具有常性,不能被 引用 ,就算引用也得加const 。给引用尽量要给const 。
char 1字节 UTF-8
wchar(宽字符) 2字节 GBK
char16 2字节 UTF-16
char32 4字节 UTF-32
windows 用的是GBK(国标
ASCII (American Standard Code for Information Interchange):美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准 ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符
编码表值 – 符号对应的表 就叫 编码表
编码是与符号对应的关系
多个字节 对应一个中文汉学
一般常见的汉字 可以考虑用 2个字节对应一个汉字
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字
符串的存储空间:
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类的使用还是需要多多练习,可以试着写一下相关的oj题练一下手,后续会模拟实现string类,加深对string类的理解.
string相关习题【我会补充链接在这里】