🍇第3章 字符串、向量和数组
String、vector是俩种最重要的标准库类型,String支持可变长字符串
,vector支持可变长的集合
迭代器是一种与 string 和 vector 配套的标准库类型。常用于访问string中的字符或vector中的元素
内置数组是一种基础的类型,string和vector都是对它的某种抽象。
🍌3.1 命名空间的 using 声明
- 使用某个命名空间:例如 using std::cin表示使用命名空间std中的名字cin。
- 头文件中不应该包含using声明。这样使用了该头文件的源码也会使用这个声明,对于某些程序来说,可能会名字冲突,带来风险。
using std::cin;
🍌3.2 标准库类型string
- 标准库类型string表示可变长的字符序列。
- string定义在命名空间std中。
(包含头文件)
- string对象:不同于字符串字面值。
#include <string>
using std::string
🍑3.2.1 定义和初始化string对象
string 默认初始化是一个空字符串
初始化string
对象的方式:
初始化方式 | 解释 |
---|---|
string s1 | 默认初始化,s1 是个空字符串 |
string s2(s1) | s2 是 s1 的副本 |
string s2 = s1 | 等价于 s2( s1 ),s2 是 s1 的副本 |
string s3("value") | s3 是字面值 “value” 的副本,除了字面值最后的那个空字符外 |
string s3 = "value" | 等价于 s3("value"),s3 是字面值 "value" 的副本 |
string s4(n, 'c') | 把 s4 初始化为由连续 n 个字符 c 组成的串 |
注意:
- 在进行拷贝操作时,是不包含最后一个空字符串的
- 使用字符串字面值或字符数组初始化 string 对象时,string 对象中是不包含末尾的空字符的,它会将字符数组中末尾的空字符去掉。
初始化方式
- 拷贝初始化
- 直接初始化
- 列表初始化
string s5 = "hello"; //拷贝初始化
string s6("hello"); //直接初始化
string s7{hello}; //列表初始化
🍑3.2.2 string对象上的操作
string 常用操作
string 常用操作 | |
---|---|
getline(is, s) | 从is中读取一行赋给s,返回is (is 是输入流) |
s.empty() | s 为空返回true,否则返回false |
s.size() | 返回 s 中字符的个数 |
s[n] | 返回 s 中第 n 个字符的引用,位置 n 从0计起 |
s1+s2 | 返回 s1 和 s2 连接后的结果 |
s1=s2 | 用 s2 的副本代替 s1 中原来的字符 |
s1==s2 | 如果 s1 和 s2 中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感 |
s1!=s2 | 同上 |
<, <=, >, >= | 利用字符在字典中的顺序进行比较,且对字母的大小写敏感(对第一个不相同的位置进行比较) |
注意
:
- 通过
cin >> string
,在读取string对象时,string对象会自动忽略开头的空白(空格、换行符、等)并从第一个真正的字符串开始读,知道遇见下一个空白为止,因此不能使用cin 读取句子
,但是可以读取单词
; - 字符串字面值与 string 是两种不同的类型
string s;
cin >> s; //输入hello world
cout << s << endl; //输出为hello
读写string对象
使用cin
和 cout
来读写string对象
使用getline 函数读取一行
getline()
函数定义在头文件string
中- getline()参数是一个输入流和一个string对象,函数从给定的输入流中读取内容,知道遇到换行符位置(
注意是换行符和上面空白字符不一样
) - 读取到文件末尾结束
注意
:getline 会将换行符也读入,但是不将换行符存入 string 对象。触发 getline() 函数返回的那个换行符实际上被丢弃掉了。得到的string对象中不包含换行符。
string s; //读取到文件末尾结束
while(getline(cin, s)) //输入为hello world
cout << s << endl; //输出为hello world
string::size_type 类型
- string 中的size() 返回值类型是
string:: size_type
- string::size_type是一个无符号值,能够存下任何string对象的大小,所以用于存放string类的size函数返回值的变量,都应该是string::size_type类型的
- c++11规定允许使用
auto
和declltype
来获取此类型 - 在具体使用时,通过作用域操作符来表明 size_type 是在类 string 中定义的。
auto len = s.size();// len 的类型是 string::size_type
注意:
在一条表达式中已经有了size()函数,就不要使用int了,这样可以避免混用int 和 unsigned可能会带来问题。
俩个string对象相加
- 俩个string对象相加得到一个新的string对象,其内容是把左侧的运算符对象与右侧的运算符对象串接而成
string s1 = "hello,";
string s2 = "world";
string s3 = s1 + s2;
cout << s3 << endl; //hello,world
字面值和string对象相加
切记字面值和string是不一样的
- 字面值分为:字符字面值和字符串字面值
俩个string对象相加
string s1 = "hello,";
string s2 = "world";
string s3 = s1 + s2;
cout << s3 << endl; //hello,world
string对象和字符字面值相加
- 当把string对象和字符字面值混在一起的时候,必须保证每个加法运算(+)的俩侧的运算对象
至少有一个是string
string s4 = s1 + "," //正确,左侧为string对象,右侧为字符字面值
string s5 = "hello" + "," //错误,左右俩侧都是字符字面值
string s6 = s1 + "," + "world" //正确
string s7 = "hello" + "," + s1 //错误,俩个字面值不能相加
//等价于string s7 = ("hello" + ",") + s1
🍑3.2.3 处理string对象中的字符
cctype 头文件中有下列标准库函数来处理 string 中的字符。
cctype头文件中的函数 | 解释 |
---|---|
isalnum(c) |
当c 是字母或数字时为真 |
isalpha(c) |
当c 是字母时为真 |
iscntrl(c) |
当c 是控制字符时为真 |
isdigit(c) |
当c 是数字时为真 |
isgraph(c) |
当c 不是空格但可以打印时为真 |
islower(c) |
当c 是小写字母时为真 |
isprint(c) |
当c 是可打印字符时为真 |
ispunct(c) |
当c 是标点符号时为真 |
isspace(c) |
当c 是空白时为真(空格、横向制表符、纵向制表符、回车符、换行符、进纸符) |
isupper(c) |
当c 是大写字母时为真 |
isxdigit(c) |
当c 是十六进制数字时为真 |
tolower(c) |
当c 是大写字母,输出对应的小写字母;否则原样输出c |
toupper(c) |
当c 是小写字母,输出对应的大写字母;否则原样输出c |
建议
:使用 c++ 版本的标准库头文件,即 cname 而非 name.h 类型的头文件。cname 头文件中的名字都从属于命名空间 std;
范围for循环
- for (auto c : str)
- for (auto &c : str)
string str("hello world");
for(auto c:str) // 对于str中的每个字符
cout << c << endl; // 输出当前字符,后面紧跟一个换行符 hello world
- 当要
改变 string 对象中的值
时,需要把循环变量定义成引用类型
。必须通过显示添加 & 符号来声明引用类型。 - 不能在范围 for 语句中改变所遍历序列的大小。
for(auto &c:s)
c = toupper(c); // 小写转换为大写
访问string中的某一个字符,有俩种方式 1. 可以通过[ ], 2.可以通过迭代器
- s[0] 表示第一个字符
- s[size() - 1] 表示最后一个字符
- string对象的下标是从 0 开始到 size() - 1 结束
- 索引必须大于等于 0 小于 size,使用索引前最好用 if(!s.empty()) 判断一下字符串是否为空。
- 任何表达式只要是整型值就可以作为索引。索引是无符号类型 size_type;
🍌3.3 标准库类型 vector
- vector 是一个
类模板
而非类型,vector是一个类型。 - vector同时也是一个
容器
,可以容纳各种数据类型 - 本身是一个类模板,但是可以实例化出一个类
🍑3.3.1 定义和初始化vector对象
vector <int> v1; //vector默认初始化是一个0.
vector<string> v2(v1); // v2=v1
vector<string> v2 = v1; //等价于v2(v1)
vector<string> v3(10,"dainian"); // 10个string
vector<string> v4(10); // 10个空string
vector<string> v5{"a","hahah"}; // 列表初始化
vector<string> v5 = {"a","hahah"}; //等价上面
列表初始化
- 使用一对
{}
来表示列表初始化,初始化过程会尽量把花括号内的值当作一个初始值列表来处理。 如果花括号内的值不能用来列表初始化,比如对一个 string 的 vector 初始化,但是花括号内的值为整型
vector v {10}; // v 有 10 个默认初始化的元素
vector v {10, "hello"}; // v 有 10 个值为 "hi" 的元素
**值初始化**
如果vector 对象的元素是内置类型,比如int 则元素初始值为0,如果是类类型,则由类默认初始化。
### 🍑3.3.2 像vector对象中添加元素
- v.push_back(e) 在尾部增加元素。
- 为了可以使得vector高效增长,通常先定义一个空 vector,然后在添加元素会更快速。
- 如果插入的vector中的初始值值都是一样的此时初始化时确定大小与值会更快,如果初始值值不一样,即使已知大小,最好也先定义一个空的 vector,再添加元素。
### 🍑3.3.3 其他vector操作
v.size(); //返回v中元素的个数
v.empty(); //如果v不含有任何元素,返回真;否则返回假
v.push_back(t); //向v的尾端添加一个值为t的元素
v[n] //返回v中第n个位置上元素的引用
v1 = v2 //用v2中的元素拷贝替换v1中的元素
v1 = {a,b,c...} //用列表中元素的拷贝替换v1中的元素
v1 == v2 // v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
v1 != v2
<,<=,>, >= //以字典顺序进行比较
- 可以用范围 for 语句处理 vector 序列的元素,范围for语句内不应该改变其遍历序列的大小。
- vector对象(以及string对象)的下标运算符,只能对确知已存在的元素执行下标操作`(只能访问元素,或者更改已经存在的元素)`,但是`不能用于添加元素。`
## 🍌3.4 迭代器介绍
- 所有标准库容器都可以使用迭代器,但是只有少数几种才同时支持下标操作,`如果是容器,尽量都采用迭代器进行操作`
- 类似于指针类型,迭代器也提供了对对象的间接访问。(可以访问某个元素,也可以移动迭代器,`好比指针`)
- 迭代器分为有效和无效。
### 🍑3.4.1 使用迭代器
- begin 返回指向第一个元素的迭代器
- end 返回指向最后一个元素的下一位的迭代器(通常被称为尾后迭代器)
auto b = v.begin(), e = v.end() //返回的是iterator类型
auto c = v.cbegin(), f = v.cend() //返回的是const_iterator类型
注意:如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器
**迭代器运算符**
| 运算符 | 解释 |
| ---------------- | -------------------------------------- |
| *iter | 返回迭代器iter所指向的**元素的引用** |
| iter->mem | 等价于(*iter).mem |
| ++iter | 令iter指示容器中的下一个元素 |
| - -iter | 令iter指示容器中的上一个元素 |
| iter1 == iter2 | 判断两个迭代器是否相等 |
| iter1 != iter2 | 判断两个迭代器是否不相等 |
**迭代器类型**
迭代器分为俩种类型:
- terator
- const_iterator
vector::iterator it; //it能读写
string::iterator it;
vector::const_iterator it; //it只能读
string::const_iterator;
**begin和end返回值类型**
- begin 和 end返回值具体类型由对象是否是常量来决定,如果对象是常量则,begin和end返回const_iterator,否则相反。
vector v;
const vector v1;
auto it = v.begin(); //it类型是iterator
auto it1 = v1.begin(); //it1类型为const_iterator
**注意:凡是使用了迭代器进行循环,都不要向迭代器所属的容器中添加或者删除元素,可以更改元素,都则,迭代器不知道指向哪个元素,迭代器会失效**
**迭代器运算**
string 和 vector支持的迭代运算。注意不能将俩个迭代器相加。
`vector`和`string`迭代器支持的运算:
| 运算符 | 解释 |
| -------------------- | ------------------------------------------------------------ |
| iter + n | 迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。 |
| iter - n | 迭代器减去一个证书仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。 |
| iter1 += n | 迭代器加法的复合赋值语句,将`iter1`加n的结果赋给`iter1` |
| iter1 -= n | 迭代器减法的复合赋值语句,将`iter2`减n的加过赋给`iter1` |
| iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。 |
| >、>=、<、<= | 迭代器的关系运算符,如果某迭代器
- difference_type:保证足够大以存储任何两个迭代器对象间的距离,可正可负。
- 迭代器只有相减,没有相加。
## 🍌3.5 数组
- 数组的大小,长度固定,不可以变,相当于vector的低级版
### 🍑3.5.1 定义和初始化内置数组
- 数组的维度必须是个常量表达式
int a[0]; //数组的维度必须是个常量表达式
unsigned cnt = 42 //不是常量表达式
constexpr unsigned cnt = 42; //是常量表达式
string strs[get_size()]; //当get_size()是constexpr时,是常量表达式
- 数组和vector的元素必须是对象,不能是引用
- 数组不能用auto关键由初始值列表推断类型
**字符数组的特殊性**
char a1[] = {'c','+', '+' }; //列表初始化,没有空字符,维度是3
char a2[] = "c ++"; //有空字符串,维度是4;
const char a4[3] = "c++"; //错误,没有空间存放空字符
不能用数组为另一个数组赋值或拷贝。可以按元素一个一个拷贝,但不能直接拷贝整个数组。
**理解复杂的数组声明**
因为数组本身是一个对象,所以允许定义数组的指针以及数组的引用。
`从数组的名字开始右内向外的顺序比较好理解`
int *ptr[10]; // ptrs是一个含有10个整型指针的数组
int (*ptr)[10] = &arr; // ptrs是一个指针,指向一个含有10个整数的数组
int (&ptr)[10] = &arr; // ptrs是一个引用,引用一个含有10个整数的数组
例如:
- 先读(),*ptr是一个指针,在往右看,这个指针指向一个数组,在往左看,这个数组是 int 型的, 所以int `(*ptr)[10] = &arr`; ptrs是一个指针,指向一个含有10个整数的数组
### 🍑3.5.2 访问数组元素
- 数组的元素可以使用范围`for语句`或者`下标运算符`来进行访问
- 在使用数组下标时候,通常定义为`size_t类型`(是一种无符号类型,他被设计的足够大以便能表示内存中任意对象的大小)
**数组相比vector的缺点是什么**
- 数组的大小是确定的。
- 不能随意增加元素。
- 不允许拷贝和赋值。
数组不能直接拷贝,而是需要每个元素拷贝。而vector可以直接拷贝
//数组需要每个元素进行拷贝
int arr[10];
for (int i = 0; i < 10; ++i) arr[i] = i;
int arr2[10];
for (int i = 0; i < 10; ++i) arr2[i] = arr[i];
//vector可以直接拷贝
vector<int> v(10);
for (int i = 0; i != 10; ++i) v[i] = arr[i];
vector<int> v2(v);
for (auto i : v2) cout << i << " ";
### 🍑3.5.3 指针和数组
- 在大多数情况,使用数组类型的对象其实是使用一个指向该数组首元素的指针
- 标准库类型(如 string、vector 等)的下标都是无符号类型,而数组内置的下标没有这个要求。
- 指向数组元素的指针等价于 vector 中的迭代器
- 指针访问数组:在表达式中使用数组名时,名字会自动转换成指向数组的第一个元素的指针。
- 使用数组时,编译器一般会把它转换成指针。
### 🍑3.5.4 c风格字符串
- c++是由c继承过来的c风格字符串
- c 风格字符串`不是一种类型,而是一种写法`,是为了表达和使用字符串而形成的一种约定俗成的写法。
- 按此习惯书写的字符串存放在字符数组中并以`空字符('\0')`结束。
- c++ 支持 c 风格字符串,但是最好不要使用,极易引发程序漏洞,对大多数应用来说,使用标准库 string比使用C风格字符串更安全、更高效。
| 函数 | 介绍 |
| ---------------- | ------------------------------------------------------------ |
| strlen(p1) | 返回`p1`的长度,空字符不计算在内 |
| strcmp(p1, p2)| 比较`p1`和`p2`的相等性。如果`p1==p2`,返回0;如果`p1>p2`,返回一个正值;如果`p1<p2`,返回一个负值。 |
| strcat(p1, p2) | 将`p2`附加到`p1`之后,返回`p1` |
| strcpy(p1, p2) | 将`p2`拷贝给`p1`,返回`p1`
- 上述表中所列的函数不负责验证其字符串参数
- 传入参数的指针必须指向以空字符结束的数组。必须确保数组足够大。
char ca[] = {"hello", "world"} //不以空字符结束
cout << strlen(ca) << endl; //错误:ca没有以空字符结束
`对于 string,可以使用 s = s1 + s2,s1 > s2 等加和与比较,而 c 风格字符串不行,因为他们实际上是指针。`
### 🍑3.5.5与旧代码的接口
**string对象和C风格字符串的混用**
- 可以使用字符串字面值来初始化 string 对象或与 string 对象加和,所有可以用字符串字面值的地方都可以使用以空字符结束的字符数组来代替。
- 反过来不能使用 string 对象初始化字符数组,必须要用 **c_str()** 函数将 string 对象转化为 c 风格字符串
string s ("hello world");
const char str = s; //错误,不能用string对象初始化char
const char* cp = s.c_str(); // s.c_str() 返回一个指向以空字符结束的字符数组的指针。
**使用数组初始化 vector 对象**
- 不允许使用一个数组为另一个内置类型的数组赋值
- 可以使用数组来初始化 vector 对象,用两个指针来表明范围(左闭合区间)
int arr[] = {0, 1, 2, 3, 4, 5};
vector ivec(begin(arr), end(arr));
**建议不要使用 c 风格字符串和内置数值,都使用标准库容器**
## 🍌3.6 多维数组
严格来说 C++ 中没有多维数组,那实际是数组的数组。
int arr20[40] //将所有元素初始化为 0
**多维数组的初始化**
//显示初始化所有元素
int a3 = {1,2,3,4,5,6,7,8,9,10,11,12}
int a3 = {{1,2,3},{4,5,6},{7,8,9},{10,11,12}} //与上面等价
//显示初始化部分元素
int a3 = {{0},{1},{2}}; //只是初始化了每一行的第一个元素
**多维数组的下标引用**
int arr3;
arr[0];//这是一个有四个元素的一维数组
arr0;//第一行第一列的元素
**使用范围 for 语句处理多维数组**
- c ++11中可以使用范围 for 语句处理多维数组。
- `注意范围 for 语句中改变元素值要显示使用 & 符号声明为引用类型。`
- `注意使用范围 for 循环处理多维数组时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。`
- 因为如果不声明为引用类型,编译器会自动将控制变量转换为指向数组首元素的指针,就不能在内层继续使用范围 for 循环处理该控制变量了。
constexpr size_t rowCnt = 3, colCnt = 4;
int ia rowCnt;
for(auto& row : arr) //给每一行赋值
for(auto col : row) //给每一列进行赋值,例如先赋值[0][0],[0][1],[0][2]
输出 arr 的元素有四种方法
- 范围 for 语句-不使用类型别名
- 范围 for 语句-使用类型别名
- 普通 for 循环
- 指针
// 范围 for 语句-不使用类型别名
for (const int (&row)[4] : arr)
for (int col : row)
cout << col << " ";
cout << endl;
// 范围 for 语句-使用类型别名
using int_array = int[4];
for (int_array &p : ia)
for (int q : p)
cout << q << " ";
cout << endl;
// 普通 for 循环
for (size_t i = 0; i != 3; ++i)
for (size_t j = 0; j != 4; ++j)
cout << arr[i][j] << " ";
cout << endl;
// 指针
for (int (*row)[4] = arr; row != arr + 3; ++row)
for (int *col = *row; col != *row + 4; ++col)
cout << *col << " ";
cout << endl;
**指针vs引用**
- 引用总是指向某个对象,定义引用时没有初始化是错的。
- 给引用赋值,修改的是该引用所关联的对象的值,而不是让引用和另一个对象相关联
**动态数组**
- 使用 `new`和 `delete`表达和c中`malloc`和`free`类似的功能,即在堆(自由存储区)中分配存储空间。
- 定义: `int *pia = new int[10]; `10可以被一个变量替代。