C++之spring
C++之spring
string类对象的访问及遍历操作
operator[]
返回pos位置的字符,const string类对象调用
这是一个既可以写也可以读的库函数,const修饰的内容是不可以更改的,所以是读
C++类与对象里要想普通对象和const修饰的对象同时重载
第二种访问及遍历操作就是:下标+[]
代码如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
}
还有种写法:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
s1[i]++;
}
}
这种写法使自定义类型像数组一样访问及遍历,也能得到相同的结果:
接下来再来引入一个概念:迭代器
可以理解为像指针一样的东西,它在堆上面开辟空间,因为可以动态调整大小
代码示例如下:
//迭代器
string::iterator it1 = s1.begin();
//这么写是因为定义在string的类域里面
while (it1 != s1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
}
这里需要特别强调的是:end是最后一个数据的下一个位置,不是最后一个数据
运行结果如下:
可能string这里会显得迭代器有点繁琐,但是它是适用于所有容器通用的方法,下标+[]只适用于string,vector这样的底层是连续的物理结构,但是像链表就不太适合,运算符重载的效率就会大大降低,因为链表的地址没有大小关系
顺便说一句,begin和end相当于数学里面左闭右开的逻辑关系
可能会觉得while(it1!=s1.end())是否可以变成<,不建议这样,<只适合地址有大小关系的情况,!=更通用些
迭代器使用的例子:
#include <iostream>
#include <list>
using namespace std;
list <int> lt;
//尾插
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
//迭代器的使用
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
//用法类似于指针的解引用
//但是并不是指针,底层是运算符重载
}
cout << endl;
迭代器的逆置访问
例子如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
打印结果:
反向,因为方向也是相反的,所以不是–而是++
const迭代器
const string s2(s1);
//string::const_iterator it1 = s2.begin(); 加上const,因为这是不能被修改的
auto it1 = s2.begin();//换成auto关键字,提高效率,毕竟是块语法糖
while (it1 != s2.end())
{
//*it1 += 1; 常量不能被修改
cout << *it1 << " ";
++it1;
}
cout << endl;
//反向
//string::const_reverse_iterator rit1 = s2.rbegin();
auto rit1 = s2.rbegin();
while (rit1 != s2.rend())
{
cout << *rit1 << " ";
++rit1;
}
cout << endl;
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个 不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型 指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期 推导而得。
**用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加& **
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
**auto不能作为函数的参数,可以做返回值,但是建议谨慎使用 **
auto不能直接用来声明数组
示例如下:
int i=0;
int j=i;
//自动推导类型
auto z=i;
auto x=1.1;//double
auto p=&i;//int*
int &r1=i;
auto r2=r1;//int
auto &r3=r1;//int&
应用场景:
以后会学到map:它的底层涉及到红黑树,代码非常地长
std::map<std::string, std::string> dict;
std::map<std::string, std::string>::iterator dit = dict.begin();
auto dit = dict.begin();
//会简便非常多
这是一种语法糖,可以简化代码,是懒人的福音
auto能否做函数的形参?
C++20才支持
auto能否作返回值?
可以,但是不是特别好,因为万一嵌套不写注释,就会很麻烦:
.....
auto func3()
{
auto y=func4();
}
auto func2()
{
auto x=func3();
}
范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
例子1:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
//范围for
for (char ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
运行结果:
但其实日常中我们会更喜欢前面的类型是auto,会更加地方便,让编译器在编译的时期进行推导
和引用结合的情况:
1)修改内容:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
//范围for
for (auto& ch : s1)
{
ch++;
cout << ch << " ";
}
cout << endl;
}
运行结果:
不加上引用,就不会有影响,因为这是赋值拷贝,并没有改变内容本身
2)容器比较大的情况
例如上面提到的map容器
并不是所有情况都适合使用范围for
因为它的底层是迭代器,迭代器只支持容器,范围for有个例外,支持数组
例子如下:
int a[] = { 1,2,3,4,5,6 };
for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
{
cout << a[i] << " ";
}
cout << endl;
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
运行结果:
也就是说范围for适用于容器和数组
string类对象的容量操作
size和length
功能都是返回字符串有效字符长度
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接 口保持一致,一般情况下基本都是用size()
capacity
主要作用是扩容
样例代码如下:
string s1;
size_t old = s1.capacity();
cout << "capacity:" << old << endl;
for (size_t i = 0; i < 100; i++)
{
s1 += 'x';
if (s1.capacity() != old)
{
cout << "capacity:" << s1.capacity() << endl;
old = s1.capacity();
}
}
运行结果:
可以发现,除了第一次是两倍大小的关系,其他情况下都是1.5倍左右,因为VS会预留个buffer,大小为16个字节大小,如果刚开始的string字符串大小小于16个字节,放到buffer里面,大于16个就浪费这个buffer,加上这个buffer是为了防止频繁地调用堆里面的存储
具体来说,原因如下:
vs和g++下string结构的说明
注意:
下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
- **vs下string的结构 **
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的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个 指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小
字符串有效长度
引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
指向堆空间的指针,用来存储字符串。
reserve
功能:为字符串预留空间
在VS中,reserve预留空间会比实际的要大
代码如下:
string s1;
s1.reserve(200);
size_t old = s1.capacity();
cout << "capacity:" << old << endl;
for (size_t i = 0; i < 100; i++)
{
s1 += 'x';
if (s1.capacity() != old)
{
cout << "capacity:" << s1.capacity() << endl;
old = s1.capacity();
}
}
运行结果如下:
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参 数小于string的底层空间总大小时,reserver不会改变容量大小。
string类对象的修改操作
push_back
在字符串后尾插字符
append
在字符串后追加一个字符串
operator+=(重点,比上面两个库函数好)
在字符串后追加字符串str
int main()
{
string s1("hello");
s1.push_back(',');
s1.push_back('w');
cout << s1 << endl;
s1.append("orld");
cout << s1 << endl;
s1.append(10, '!');
cout << s1 << endl;
string s2("hello smile hello world");
s1.append(s2.begin()+6, s2.end());
cout << s1 << endl;
string s3("hello");
s3 += ',';
s3 += "world";
cout << s3 << endl;
return 0;
}
运行结果:
一道OJ题:仅仅反转字母
给你一个字符串 s
,根据下述规则反转字符串:
- 所有非英文字母保留在原有位置。
- 所有英文字母(小写或大写)位置反转。
返回反转后的 s
。
示例 1:
输入:s = "ab-cd"
输出:"dc-ba"
示例 2:
输入:s = "a-bC-dEf-ghIj"
输出:"j-Ih-gfE-dCba"
示例 3:
输入:s = "Test1ng-Leet=code-Q!"
输出:"Qedo1ct-eeLg=ntse-T!"
提示
1 <= s.length <= 100
s
仅由 ASCII 值在范围[33, 122]
的字符组成s
不含'\"'
或'\\'
代码:
class Solution {
public:
string reverseOnlyLetters(string s) {
int n = s.size();
int left = 0, right = n - 1;
while (true) {
while (left < right && !isalpha(s[left])) { // 判断左边是否扫描到字母
left++;
}
while (right > left && !isalpha(s[right])) { // 判断右边是否扫描到字母
right--;
}
if (left >= right) {
break;
}
swap(s[left], s[right]);
left++;
right--;
}
return s;
}
};
双指针法,看下标
一道OJ题:字符串相加
给定两个字符串形式的非负整数 num1
和num2
,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger
), 也不能直接将输入的字符串转换为整数形式。
示例 1:
输入:num1 = "11", num2 = "123"
输出:"134"
示例 2:
输入:num1 = "456", num2 = "77"
输出:"533"
示例 3:
输入:num1 = "0", num2 = "0"
输出:"0"
提示:
1 <= num1.length, num2.length <= 104
num1
和num2
都只包含数字0-9
num1
和num2
都不包含任何前导零
代码如下:
class Solution {
public:
string addStrings(string num1, string num2) {
//这是我们需要返回的字符串,要接受的对象
string str;
//因为扩容会浪费一定的空间,会导致效率降低,所以用了库函数里面的函数,更加合理地使用空间
//+1是为了防止越界
str.reserve(max(num1.size(),num2.size())+1);
//end是有效字符串最后一位的下一位,所以要-1防止越界
int end1=num1.size()-1,end2=num2.size()-1;
//进位
int next=0;
//循环的条件是要继续下去,平常可能会想着结束的条件不一样,是反的
while(end1>=0||end2>=0||next)
{
//这里是字符串接收,end计算完了还要记得--,字符串‘0’是拿ASCLL码表,0是48
int x1=end1>=0?num1[end1--]-'0':0;
int x2=end2>=0?num2[end2--]-'0':0;
//ret是要接收的数
int ret=x1+x2+next;
//进位将数取整
next=ret/10;
//这个是个位,例如9+4,要余3
ret=ret%10;
//库函数,头插,尾插会错误,如11,231,会变成242
str.insert(0,1,'0'+ret);
}
//这是为了处理1+9,没有进位的情况
if(next==1)
{
str+='1';
//这里引入了一个库函数,进行逆置,是为了降低时间复杂度,使其为O(N)
reverse(str.begin(),str.end());
//str.insert(str.begin(),'1');,时间复杂度为O(N^2)
}
return str;
}
};
int x1=end1>=0?num1[end1--]-'0':0;
int x2=end2>=0?num2[end2--]-'0':0;
//ret是要接收的数
int ret=x1+x2+next;
//进位将数取整
next=ret/10;
//这个是个位,例如9+4,要余3
ret=ret%10;
//库函数,头插,尾插会错误,如11,231,会变成242
str.insert(0,1,'0'+ret);
}
//这是为了处理1+9,没有进位的情况
if(next==1)
{
str+='1';
//这里引入了一个库函数,进行逆置,是为了降低时间复杂度,使其为O(N)
reverse(str.begin(),str.end());
//str.insert(str.begin(),'1');,时间复杂度为O(N^2)
}
return str;
}