首先我们来补充 2 个 C++11 的小语法,方便我们后面的学习~
早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量 。使用auto替换长类型就十分方便~
》 C++11 中,标准委员会变废为宝赋予了 auto 全新的含义即: auto 不再是一个存储类型 指示符,而是 作为一个新的类型指示符来指示编译器 , auto 声明的变量必须由编译器在编译时期 推导而得 。
》 用auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &
》 当在 同一行声明多个变量 时,这些 变量必须是相同的类型 ,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量 。
》 C++11 auto不能作为函数的参数(后面C++20支持) 》 auto可以做返回值,建议谨慎使用
》 auto不能直接用来声明数组
这里我们结合具体的例子来看看auto的使用~
#include<iostream>
using namespace std;
//void test1(auto a)//err——C++11 auto不能作为函数的参数
//{
// //....
//}
auto test2()//auto可以做返回值,建议谨慎使用
{
return 10;
}
int main()
{
//auto根据右边的数据类型推导得到
int a = 1;
auto b = a;
//auto和auto*都可以声明指针类型
auto c = &a;//auto右边可以是指针类型,也可以是其他类型
auto* d = &a;//auto*右边必须是指针类型
//用auto声明引用类型时则必须加&
auto& e = a;
auto aa = 1, bb = 2;
//auto cc = 3, dd = 4.4;//err——在声明列表中,auto必须始终推导为同一类型
//auto f;//err——无法推导类型
//auto a[] = { 1,2,3,4,5 };//err——auto不能直接用来声明数组
cout << test2() << endl;
return 0;
}
谨慎使用 auto做返回值!!!
例:
#include<iostream>
using namespace std;
auto func1()
{
//......
return 1;
}
auto func2()
{
//......
return func1();
}
auto func3()
{
//......
return func2();
}
auto func4()
{
//......
return func3();
}
int main()
{
auto ret = func4();
return 0;
}
当上面的代码想要知道ret的类型,就需要知道func4()返回值的类型,层层寻找直到func1()才能够知道类型,当程序十分复杂的时候,就不方便~所以我们要谨慎使用 auto做返回值!!!
对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误~
因此 C++11 中引入了基于范围的 for 循环~
》 for循环后的括号由冒号 “ : ” 分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围 ,自动迭代,自动取数据,自动判断结束。
》按值拷贝 在范围for循环中,每次迭代到的元素都会被按值拷贝到循环变量中~这意味着,如果对循环变量进行修改,不会影响到容器中的原始元素。如果需要修改容器中的元素,可以通过在循环变量前加上引用符号&来实现。这样,循环变量就会直接引用容器中的元素,从而允许在循环体内对元素进行修改。
》 范围for 可以作用到数组和容器对象上进行遍历
》 范围for 的底层比较简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到~
接下来,我们来看看范围for的使用例子
#include<iostream>
#include<string>
using namespace std;
int main()
{
int a[] = { 1,2,3,4,5,6 };
// C++98的遍历
cout << "C++98的遍历" << endl;
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
{
a[i] *= 2;
}
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
{
cout << a[i] << " ";
}
cout << endl;
// C++11的遍历
cout << "C++11的遍历" << endl;
//使用范围for
for (auto& e : a)//传引用——修改数据修改数组里面元素
{
e *= 2;
}
for (auto e : a)//传参——自动取数据赋值给e
{
cout << e << " ";//打印e
}
cout << endl;
for (auto e : a)//传参——调用拷贝构造,无法修改数组里面元素
{
e *= 2;
}
for (auto e : a)//传参——自动取数据赋值给e
{
cout << e << " ";//打印e
}
cout << endl;
string str("Hello World!");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
//使用int/char
cout << "使用int/char" << endl;
//也可以使用int/char,类型匹配就可以了
// 使用auto更加方便——自动取数据推导类型
for (int e : a)
{
cout << e << " ";
}
cout << endl;
for (char e : str)
{
cout << e << " ";
}
cout << endl;
return 0;
}
我们可以发现使用范围for遍历数组和容器更加方便快捷!~
学习string类,我们就需要查阅文档,小编这里在cplusplus官方网站上查阅string类的接口~
string类的头文件是string
这里介绍几个比较重要的接口~
查阅文档:
构造比较重要的接口就是 无参构造、有参构造、拷贝构造 ~
我们包头文件来简单使用一下~
#include<iostream>
#include<string>//string类头文件
using namespace std;
int main()
{
string s1;//调用无参构造
string s2("Hello Xiaodu!!!");//调用有参构造
string s3(s2);//调用拷贝构造
string s4(s2, 6, 100);//取一部分进行拷贝进行初始化
//从第6个位置开始,拷贝100个字符,如果没有100个取到字符串末尾就可以了
string s5(6, 'd');//使用6个字符‘d’进行初始化
string s6(s2, 6);//从第6个位置开始拷贝字符直到末尾
//string 支持流插入、流提取运算符,我们可以直接使用
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
return 0;
}
string类对[ ]也进行了运算符重载,所以我们这里也就可以直接使用下标 +[ ]进行遍历和修改~
我们也可以发现,它重载了普通版本和const版本~使用的时候会匹配最合适的~
#include<iostream>
#include<string>//string类头文件
using namespace std;
void test1()
{
string s1("Hello Xiaodu!!!");
cout << s1 << endl;
//遍历+修改
//下标 + []
for (size_t i = 0; i < s1.size(); i++)
{
s1[i] += 1;//实现修改
}
//范围for
for (auto e : s1)
{
cout << e << " ";//打印出来
}
cout << endl;
//const 修饰
const string s2("hhhhhh");
//下标 + []
//for (int i = 0; i < s2.size(); i++)
//{
// s2[i] += 1;//const修饰,不可以实现修改
//}
for (size_t i = 0; i < s2.size(); i++)
{
cout << s2[i] << " ";//可以访问打印
}
cout << endl;
}
int main()
{
test1();
return 0;
}
迭代器是一个行为像指针的对象~迭代器模式实现集合遍历,隐藏内部细节,提供统一接口~
迭代器是一个左闭右开的区间—— [ ) begin() 返回第一个位置的迭代器 end()不是最后一个位置的迭代器,而是最后一个位置的迭代器 我们知道字符串末尾的‘\0’是字符串结束标志,并不是有效字符~
迭代器同样分为两种,一种是普通迭代器——可读可写,一种是const 迭代器——可读不可写~
例:
void test2()
{
string s1("AAAAAA");
cout << s1 << endl;
//使用迭代器遍历+修改
//迭代器是一个左闭右开的区间
//[ )
string::iterator it1 = s1.begin();//begin() 返回第一个位置的迭代器
while (it1 != s1.end())//end()不是最后一个位置的迭代器
{
//类似于指针的使用——这里遍历+修改
//普通迭代器可读可写
*it1 += 1;
cout << *it1 << " ";
++it1;//迭代器——行为像指针的对象
}
cout << endl;
//const 修饰的迭代器可读不可写
//const string::iterator it2 = s1.begin();//err 错误的const修饰,这里是修饰迭代器本身,但是遍历需要++
//我们希望里面的内容不可以修改
string::const_iterator it2 = s1.cbegin();//string类里面提供了const_iterator
while (it2 != s1.cend())
{
//*it2 += 1; //const_iterator不可以修改
cout << *it2 << " ";
++it2;
}
cout << endl;
}
画图理解:
同时还有反向迭代器——反过来访问
//反向迭代器
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
*rit += 1;
cout << *rit << " ";
++rit;
}
画图理解:
具体实现可能会因为不同的容器有一些不一样~
我们也可以使用范围for遍历和修改~需要注意的是我们前面提到过范围for是按值拷贝,在循环变量前加上引用符号&才可以修改容器中的元素
void test3()
{
string s1("AAAAAA");
for (auto e : s1)
{
e += 1;
cout << e << " ";
}
cout << endl;
cout << s1 << endl;
//在循环变量前加上引用符号&才可以修改容器中的元素
for (auto& e : s1)
{
e += 1;
cout << e << " ";
}
cout << endl;
cout << s1 << endl;
}
string类里面有很多的关于容量的接口~我们一起来看看~
下面我们来进行简单测试,有些具体的内容,我们会在底层实现的时候更加清晰~
#include<iostream>
#include<string>//string类头文件
using namespace std;
int main()
{
string s1("AAAAAA");
cout << "s1 " << s1 << endl;
//字符串的有效长度
cout << "s1.size() = " << s1.size() << endl;
cout << "s1.length() = " << s1.length() << endl;
//返回字符串对象能包含的最大字符数
cout << " s1.max_size() = " << s1.max_size() << endl;
//返回字符串在不分配更多内存的情况下能包含的最大字符数
cout << "s1.capacity() = " << s1.capacity() << endl;
//检查字符串是否为空(即长度是否为0)
cout << "s1.empty() = " << s1.empty() << endl;
//缩容:请求减少字符串的容量以匹配其当前长度
string s2(100, 'A');
cout << "before s2.shrink_to_fit(),s2.capacity() = " << s2.capacity() << endl;
s2 = s1;
s2.shrink_to_fit();
cout << "after s1.shrink_to_fit(),s2.capacity() = " << s2.capacity() << endl;
//清除字符串的内容,使其变为空字符串,不改变字符串的容量
s1.clear();
cout << "s1 = " << s1 << endl;
cout << "s1.size() = " << s1.size() << endl;
cout << "s1.capacity() = " << s1.capacity() << endl;
return 0;
}
接下来,我们来看看比较特殊的reserve的使用~
#include <iostream>
#include <string>
using namespace std;
int main()
{
//vs2022 测试
string s1("AAAAAA");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//当n小于等于capacity的时候,capacity不会发生变化
//也就是不会进行缩容
s1.reserve(5);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(15);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//希望容量为16,事实上容量变为31,会多给一些
//不同平台处理情况不一样
s1.reserve(16);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(25);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(36);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//size不会变化
return 0;
}
提供了这个接口,在不同平台下,结果可能会不一样~比如在g++平台下,当n小于capacity的时候就会进行缩容~
有修改容量的肯定也就有修改字符串长度的——resize
我们可以调用两种函数~我们一起来看看它的使用~
#include <iostream>
#include <string>
using namespace std;
int main()
{
//vs2022 测试string类的resize
string s1("AAAAAA");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//n比原来的size小,删除数据
s1.resize(5);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//n比原来的size大,但是小于容量
// 插入数据——vs平台默认为空字符,不会显示出来
s1.resize(12);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//n比原来的size大,并且大于原来容量,插入数据并且扩容
s1.resize(25);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//另外一种函数调用
string s2("BBBBBB");
cout << s2 << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
s2.resize(5, 'c');
cout << s2 << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
//插入数据,插入我们想要的字符
s2.resize(12, 'c');
cout << s2 << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
s2.resize(25, 'c');
cout << s2 << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;
return 0;
}
[ ]这个运算符重载相信就不用多说了,我们来看看一个我们以前没有见过的——at
事实上,at与[ ]的使用是类似的~ 》在C++的
std::string
类中,成员函数at
用于访问字符串中指定位置的字符~ 》与operator[]
不同的是,如果指定的位置超出了字符串的范围,at
会抛出一个std::out_of_range
异常,而operator[]
则可能导致未定义行为(例如访问越界内存),会直接断言报错~
例:
#include <iostream>
#include <string>
#include <stdexcept> // for std::out_of_range
int main() {
std::string str = "Hello, World!";
// 使用at访问字符串中的字符
try {
char ch = str.at(7); // 获取索引为7的字符,即 'W'
std::cout << "Character at index 7: " << ch << std::endl;
// 尝试访问超出范围的索引
char out_of_range_ch = str.at(20); // 这将抛出std::out_of_range异常
}
catch (const std::out_of_range& e)
{
std::cerr << "Error: " << e.what() << std::endl; // 输出异常信息
}
return 0;
}
我们来看看back和front,看起来这个名字我们就知道它可以访问字符串的第一个和最后一个字符,我们需要注意的是末尾的‘\0'不是有效字符~
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("Hello,World!");
//访问字符串第一个字符
cout << "s1.front() " << s1.front() << endl;
//访问字符串最后一个字符
cout << "s1.back() " << s1.back() << endl;
//末尾的\0不是有效字符
return 0;
}
看这名字,我们就可以很清楚的知道这是用来尾插和尾删一个字符的~
例:
void test1()
{
string s1("HHHHHH");
cout << s1 << endl;
//尾插一个字符
s1.push_back('H');
cout << s1 << endl;
//尾删一个字符
s1.pop_back();
cout << s1 << endl;
}
append我们一样可以用来string添加字符~
我们来看看使用:
//append
void test2()
{
string s1("HHHHHH");
cout << s1 << endl;
string s2("ABCDEF");
//s1末尾添加字符串
s1.append("AAA");
cout << s1 << endl;
//s1末尾添加字符串的前面2个字符
s1.append("AAA", 2);
cout << s1 << endl;
//取s2的一部分进行插入
s1.append(s2, 3, 2);//向s1插入第三个位置后面的两个字符
cout << s1 << endl;
//s1末尾添加2个字符G
s1.append(2, 'G');
cout << s1 << endl;
}
这几个比较简单,我们来看看它还可以支持插入一个迭代器区间~
void test3()
{
string s1;
s1.push_back('A');
s1.push_back('A');
s1.push_back('A');
s1.push_back('A');
s1.append("BBBB");
cout << s1 << endl;
string s2("abcdefg");
//取一部分
s1.append(s2.begin() + 3, s2.end());
cout << s1 << endl;
s1.append(s2.begin(), s2.end());
cout << s1 << endl;
}
+=运算符的重载也让我们修改字符串更加方便~
void test4()
{
string s1("AAAAAA");
cout << s1 << endl;
s1 += "abc";
cout << s1 << endl;
s1 += 'e';
cout << s1 << endl;
string s2("BBB");
s1 += s2;
cout << s1 << endl;
}
从这个名字就可以知道,它是用来插入数据的~我们来看看它具体的接口~
我们可以发现insert的接口也是十分丰富的,我们一下子肯定也记不住,最好的办法是边学边记,在实践中学会查文档,加深理解记忆~
这里我们来简单使用几个~
void test5()
{
string s1("AAAAAA");
cout << s1 << endl;
s1.insert(s1.begin(), 'B');//支持迭代器
cout << s1 << endl;
s1.insert(s1.end(), 'B');//支持迭代器
cout << s1 << endl;
s1.insert(3, "CCC");
cout << s1 << endl;
s1.insert(1, "D");
cout << s1 << endl;
}
有插入数据,那么就有删除数据了~
erase的接口就比较少,我们来简单使用一下~
void test6()
{
string s1("ABCDEFG");
cout << s1 << endl;
s1.erase(s1.begin());//同样支持迭代器
cout << s1 << endl;
s1.erase(s1.end() - 1);//同样支持迭代器
cout << s1 << endl;
s1.erase(1, 2);//删除第一个位置后面两个字符
//使用下标更好理解——删除从下标为1开始的两个字符
cout << s1 << endl;
s1.erase(1);//使用默认参数,删除第一个位置后面的所有字符
//删除从下标为1开始的后面所有字符
cout << s1 << endl;
}
assign类似于赋值操作,但是它的接口多样,可以满足我们的不同需求,我们来看看它们的接口~
void test7()
{
string s1("ABCDEF");
string s2("BBBBBB");
string s3("CCCCCC");
string s4("DDDDDD");
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
//普通赋值
s2 = s1;
//assign——接口更加多样
s3.assign(s1);
s4.assign(s1.begin(), s1.end() - 3);
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
}
replace我们知道有代替的意思,C++里面的striing类的replace也就是可以进行替换,我们来看看它丰富的接口~
void test8()
{
string s1("ABCDEF");
string s2("BBBBBB");
string s3("CCCCCC");
string s4("DDDDDD");
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
s2.replace(1, 3, s1);//从下标为1的位置开始的3个字符替换成s1
s3.replace(s3.begin(), s3.end(), s1);
s4.replace(1, 4, 3, 'X');//从下标为1的位置开始的4个字符替换成3个X字符
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
}
replace接口十分丰富,在我们需要的时候查文档就好了,同时需要注意谨慎使用replace,因为它涉及到数据的移动,它的效率也是比较低的~
swap也就是交换字符串~在后面底层实现的时候我们会更加清楚~
void test9()
{
string s1("AAA");
string s2("BBBBBB");
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
s2.swap(s1);
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
}
数据修改的接口就告一段落了,还是一样,它们的接口很多,我们不需要全部熟记,在需要的时候查一查文档也是没有问题的~
string
对象相同的字符序列,这个指针可以用于需要C风格字符串的API调用~c_str()
,返回一个指向字符串内部数据(不包括终止的空字符)的指针。与c_str()
不同的是,data()
不保证返回的指针指向以空字符结尾的数组,但实际上在C++11及更高版本中,返回的指针确实是以空字符结尾的。string
对象关联的分配器对象的副本。分配器用于管理内存分配和释放。string
对象复制到另一个字符数组中。这个函数通常用于需要直接操作字符数组的场景。string
对象中的位置。如果找到,返回子字符串或字符第一次出现的位置的索引(下标);否则,返回string::npos
。find()
,但返回子字符串或字符最后一次出现的位置的索引(下标)string
对象中第一个出现的字符,并返回其位置索引。find_first_of()
,但从string
对象的末尾开始搜索。string
对象中第一个出现的字符,并返回其位置索引。find_first_not_of()
,但从string
对象的末尾开始搜索。string
对象与另一个string
对象或C风格字符串进行比较。根据比较结果返回负值、零或正值,分别表示小于、等于或大于。测试:
void test1()
{
string s1("AAAAAA");
string s2("AAAAAA");
if (s1.c_str() == s2.c_str())
cout << "true" << endl;
else
cout << "false" << endl;
if (s1.data() == s2.data())
cout << "true" << endl;
else
cout << "false" << endl;
}
void test2()
{
string s1("Hello World! Hello Xiaodu!");
cout << s1.find('!') << endl;//默认从下标为0的位置开始找
cout << s1.find("Hello") << endl;//默认从下标为0的位置开始找
cout << s1.rfind('!') << endl;//倒着找
cout << s1.rfind("Hello") << endl << endl;
cout << s1.find_first_of('!') << endl;
cout << s1.find_last_of('!') << endl;
cout << s1.find_first_not_of('!') << endl;
cout << s1.find_last_not_of('!') << endl;
}
copy和substr的使用
void test3()
{
//copy从字符串复制字符序列
char buffer[20];
std::string str("Test string...");
//从位置为5的字符开始复制8个字符到buffer
std::size_t length = str.copy(buffer, 8, 5);
buffer[length] = '\0';
std::cout << "buffer contains: " << buffer << '\n';
//substr生成子字符串
string s1 = "Hello Xiaodu!";
//返回从指定位置开始、指定长度的子字符串
//从位置为6的字符开始复制7个字符给s2
string s2 = s1.substr(6, 7);
cout << s1 << endl;
cout << s2 << endl;
}
比较字符串除了compare以外,还有一些比较关系的运算符重载
使用:
void test4()
{
//compare的使用
string s1 = "Hello Xiao!";
string s2 = "Hello Dary!";
if (s1.compare(s2))
{
cout << "s1 > s2" << endl;
}
else
{
cout << "s1 < s2" << endl;
}
//比较运算符重载
if (s1 > s2)
{
cout << "s1 > s2" << endl;
}
else
{
cout << "s1 < s2" << endl;
}
string s3("Ada!");
if (s1 < s3)
{
cout << "s1 < s3" << endl;
}
else
{
cout << "s1 > s3" << endl;
}
}
这些运算符重载相信也就不用多说了,我们进行直接使用~
void test5()
{
string s1 = "Hello Xiaodu!";
string s2 = "!!";
string s3 = s1 + s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
getline
的主要作用是从输入流中读取一行文本,直到遇到换行符(\n
),也可以我们自己选择读取结束的字符,换行符本身不会被包含在读取的字符串中。getline
通常与std::string
对象一起使用,因为它能够自动调整字符串的大小以适应读取的内容~
接下来,我们进行简单的使用:
1.
void test6()
{
// 创建一个string对象来存储输入的行
string inLine;
// 提示用户输入一行文本
cout << "请输入一行文本(按Enter结束输入): ";
// 使用getline从标准输入读取一行文本,直到遇到换行符
getline(cin, inLine);
// 打印出用户输入的文本
cout << "您输入的文本是: " << inLine << endl;
}
2.
void test7()
{
// 创建一个string对象来存储输入的行
string inLine;
// 提示用户输入一行文本
cout << "请输入一行文本(按#结束输入): ";
// 使用getline从标准输入读取一行文本,直到我们选择的结束符
getline(cin, inLine, '#');
// 打印出用户输入的文本
cout << "您输入的文本是: " << inLine << endl;
}
C++中string类的接口我们已经梳理得差不多了,我们后面的博客将会进行string类底层实现的讲解,让我们string类更加清晰~我们下次再见~