3.字符串、向量和数组
字符串、向量和数组
string 和 vector 都是对 array 的某种抽象
1 命名空间的 using 声明
域操作符 std
std::cout;
using namespace std;
cout<<"hello"<<endl;
Tips:
头文件不应该包含 using 声明,因为会被拷贝到引用该头文件的文件中。这可能会造成不必要的麻烦。
Tips:
不要用 using namespace std; (命名污染, “霸权”, 使得所有库函数默认都是 std 这家公司的, 其他 namespace 公司的同名函数就被偷梁换柱了, 并且此时编译器可能不报错的)
正确做法:
一条条写 using std::cin, 后面再省略 cin 也很方便
2 标准库类型 string
长度可变的字符序列,需要包含 string 头文件
2.1 定义和初始化 string 对象
# include <iostream>
# include <string>
using namespace std;
int main()
{
string s1;
string s2 = s1;
string s3 = "hello";
string s4(10, 'c');
return 0;
}
3 string 对象上的操作
操作 | 描述 |
---|---|
os << s |
将s 写到输出流os 当中,返回os |
is >> s |
从is 中读取字符串赋给s ,字符串以空白分割,返回is |
getline(is, s) |
从is 中读取一行赋给s ,返回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 |
同上,判断它们不相等 |
< , <= , > , >= |
利用字符在字典中的顺序进行比较,且对字母的大小写敏感(对第一个不相同的位置进行比较) |
- string io:
- 执行读操作
>>
:忽略掉开头的空白(包括空格、换行符和制表符),直到遇到下一处空白为止。 getline
:读取一整行,得到的 string 对象中包括空白符,并不包含换行符
- 执行读操作
s.size()
返回的时string::size_type
类型,记住是一个无符号类型的值,不要和int
混用s1+s2
使用时,保证至少一侧是 string 类型。string s1 = "hello" + "world" // 错误,两侧均为字符串字面值
- C++语言中,字符串字面值并不是 stirng 对象,是 char[]
string s5=s3+"hello";//正确
string s6="hello"+"world";//错误error: invalid operands to binary expression ('const char [6]' and 'const char [6]')
3.1 处理 string 对象中的字符
-
ctype.h vs. cctype:C++修改了 c 的标准库,名称为去掉
.h
,前面加c
。 -
尽量使用 c++版本的头文件,即
cctype
如 c++版本为
cctype
,c 版本为ctype.h
cctype
头文件中定义了一组标准函数:
函数 | 解释 |
---|---|
isalnum(int c) |
形参是 int,因为 char 底层存储就是 int;当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 |
- 遍历字符串:使用范围 for(range for)语句:
for (auto c: str)
,或者for (auto &c: str)
使用引用直接改变字符串中的字符。 (C++11) str[x]
,[]输入参数为string::size_type
类型,给出int
整型也会自动转化为该类型
tips
unsigned int 和 int 不要互转!! 不同编译器对其解释是不一样的!
4 标准库类型 vector
vector 是一个容器,也是一个类模板,编译器根据模板创建类或函数的过程称为实例化 (instantiation)
4.1 定义和初始化 vector 对象
#include <vector>
然后using std::vector;
- 容器:包含其他对象。
- 类模板:本身不是类,但可以实例化 instantiation出一个类。
vector
是一个模板,vector<int>
是一个类型。 - vector 是模板,通过将类型放在类模板名称后面的尖括号中来指定类型
- 例如
vector<int>
、vector<vector<int>>
- 例如
- 引用不是对象,所以不存在包含引用的 vector
4.1.1 初始化 vector 对象常见方法
方法 | 解释 |
---|---|
vector<T> v1 |
v1 是一个空vector ,它潜在的元素是T 类型的,执行默认初始化 |
vector<T> v2(v1) |
v2 中包含有v1 所有元素的副本 |
vector<T> v2 = v1 |
等价于v2(v1) ,v2 中包含v1 所有元素的副本 |
vector<T> v3(n, val) |
v3 包含了 n 个重复的元素,每个元素的值都是val |
vector<T> v4(n) |
v4 包含了 n 个重复地执行了值初始化的对象 |
vector<T> v5{a, b, c...} |
v5 包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector<T> v5={a, b, c...} |
等价于v5{a, b, c...} |
- 列表初始化:
vector<string> v5{"a", "an", "the"};
(C++11)
4.2 向 vector 对象中添加元素
v.push_back(e)
在尾部增加元素。
4.3 其他 vector 操作
操作 | 解释 |
---|---|
v.emtpy() |
如果v 不含有任何元素,返回真;否则返回假 |
v.size() |
返回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
对象(以及string
对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能通过下标向 vector 添加元素。
关于 vector(vector 对象能够高效增长)
C++标准要求 vector 应该能在运行时高效快速地添加元素。因此既然 vector 对 象能高效地增长,那么在定义 vector 对象的时候设定其大小也就没什么必要了,事实 上如果这么做性能可能更差。只有一种例外情况,就是所有(all)元素的值都一样。一 旦元素的值有所不同,更有效的办法是先定义一个空的 vector 对象,再在运行时向其 中添加具体值。此外,vector 还提供了方法,允许我们进一步提升动态添加元素的性能。 开始的时候创建空的 vector 对象,在运行时再动态添加元素,这一做法与 C 语官 及其他大多数语言中内置数组类型的用法不同。特别是如果用惯了 C 或者 Java,可以 预计在创建 vector 对象时顺便指定其容量是最好的。然而事实上,通常的情况是恰 恰相反
警告 ⚠️:范围循环语句体内不应该改变其遍历序列的大小,也就是说,不要在 for 中出现有可能更改 vector 对象容量的代码
5 迭代器介绍
容器类型内置的“指针”,所有标准库的容器都可以使用迭代器
严格来说,虽然 string 对象支持很多与容器类似的操作,但是 string 对象不属于容器
5.1 使用迭代器
-
vector<int>::iterator it
。 -
auto b = v.begin();
返回指向第一个元素的迭代器。 -
auto e = v.end();
返回指向最后一个元素的下一个(哨兵,尾后,one past the end)的迭代器(off the end), 无实际含义, 标记我们处理完了所有元素 -
如果容器为空,
begin()
和end()
返回的是同一个迭代器,都是尾后迭代器。 -
使用解引用符
*
访问迭代器指向的元素。(*it).empty()
-
养成使用迭代器和
!=
的习惯(泛型编程)。 -
容器:可以包含其他对象;但所有的对象必须类型相同。
-
迭代器(iterator):每种标准容器都有自己的迭代器。
C++
倾向于用迭代器而不是下标遍历元素。 -
const_iterator:只能读取容器内元素不能改变。
-
箭头运算符: 解引用 + 成员访问,
it->mem
等价于(*it).mem
-
谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
-
使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。
-
有迭代器的类型都拥有 begin 和 end 成员
- begin:返回指向第一个元素(或字符)的迭代器
- end:尾后迭代器,即尾元素的下一个位置(一个本不存在的元素)
尾后迭代器 并不实际指示某一个元素,所以不能对其进行递增或解引用
使用迭代器而非下标遍历的意图, 源于迭代器定义的广泛性
尽可能多使用迭代器, 这是一个 best practice
标准容器迭代器的运算符:
运算符 | 解释 |
---|---|
*iter |
返回迭代器iter 所指向的元素的引用 |
iter->mem |
等价于(*iter).mem |
++iter |
令iter 指示容器中的下一个元素 |
--iter |
令iter 指示容器中的上一个元素 |
iter1 == iter2 |
判断两个迭代器是否相等 |
// print each line in text up to the first blank line
for (auto it = text.cbegin(); it != text.cend() && !(*it).empty(); ++it)
cout << *it << endl;
5.1.1 迭代器类型
- 拥有迭代器的标准类型使用 iterator 和 const _iterator(和常量指针差不多)
- 有些迭代器,我们不清楚也不在意它是什么类型,这些迭代器类型简单认为是 auto 即可
- 如果对象是常量,begin 和 end 返回 const_iterator,否则返回 iterator:
- 有时候我们希望即使对象不是常量,我们也要使用 const_iterator
- C++11
- 使用
vector.cbegin()
vector.cend()
得到 const_iterator
任何一种可能改变 vector 对象容量的操作,都会使得对应的迭代器失效
重申一遍,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
5.2 迭代器运算
vector
和string
迭代器支持的运算:
运算符 | 解释 |
---|---|
iter + n |
迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。 |
iter - n |
迭代器减去一个整数仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。 |
iter1 += n |
迭代器加法的复合赋值语句,将iter1 加 n 的结果赋给iter1 |
iter1 -= n |
迭代器减法的复合赋值语句,将iter2 减 n 的加过赋给iter1 |
iter1 - iter2 |
两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。 |
> 、>= 、< 、<= |
选代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个运代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
掌握迭代器的二分搜索
迭代器相减之间的距离 difference_type 是有符号类型
int sought = 1;
vector<int> text;
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg) / 2;
while (mid != end && *mid != sought)
{
if (sought < *mid)
{
end = mid;
} else
{
beg = mid + 1;
}
mid = beg + (end - beg) / 2;
}
6 数组
复合类型 (声明形如:T a[d])
- T:元素类型
- a:数组名称
- d:元素个数(必须是常量表达式)
6.1 定义和初始化内置数组
- const 只限制只读,并不要求在编译时就确定,可以在运行时确定。只有 constexpr 才可以表示数组大小
- 不存在引用数组
- 可以使用列表初始化,但必须指定数组类型,不允许使用 auto
- 字符数组的特殊性:字符串字面值的结尾处还有一个空字符
- 不允许拷贝和赋值
6.1.1 理解复杂的数组声明
int *ptrs[10]; // ptrs是含有10个元素(整型指针)的数组
int &refs[10]; // 错误:不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组,本质是指针
int (&arrRef)[10] = arr; // Parray引用一个含有10个整数的数组,本质是引用
int *(&arry)[10] = ptrs; // arry是数组的引用,该数组包含10个指针,本质是引用
6.2 访问数组元素
- 数组下标的类型:
size_t
。 - 字符数组的特殊性:结尾处有一个空字符,如
char a[] = "hello";
。 - 用数组初始化
vector
:int a[] = {1,2,3,4,5}; vector<int> v(begin(a), end(a));
6.3 指针和数组
- 使用数组的时候,编译器一般会把它转换成指针
- 指针也是迭代器
- 标准库类型限定使用的下标必须是无符号类型,而内置的下标可以处理负值。
- 指针访问数组:在表达式中使用数组名时,名字会自动转换成指向数组的第一个元素的指针。
6.4 C 风格字符串
- c 风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。
- c 风格字符串的处理函数定义在 cstring 头文件(string.h 的 c++版本)中。
函数 | 介绍 |
---|---|
strlen(p) |
返回p 的长度,空字符不计算在内 |
strcmp(p1, p2) |
比较p1 和p2 的相等性。如果p1==p2 ,返回 0;如果p1>p2 ,返回一个正值;如果p1<p2 ,返回一个负值。 |
strcat(p1, p2) |
将p2 附加到p1 之后,返回p1 |
strcpy(p1, p2) |
将p2 拷贝给p1 ,返回p1 |
作为参数的字符串,必须以空字符串结束
6.5 与旧代码的接口
C++提供库函数,把 string 转换成 C 风格字符串
string s("hello");
const char *str = s.c_str();
如果后续的操作改变了 s 的值,c_str 所返回的数组将失效。
7 多维数组
严格来说 C++没有多维数组。多维数组理解为数组的数组,编译器会转成指针
- 初始化
int ia2[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
}
- 多维数组与指针
- 类型别名简化多维数组的指针
- 使用范围 for 语句时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
8 延伸扩展:
8.1 指针 vs 引用
- 引用总是指向某个对象,定义引用时没有初始化是错的。
- 给引用赋值,修改的是该引用所关联的对象的值,而不是让引用和另一个对象相关联。
8.2 指向指针的指针
- 定义:
int **ppi = π
- 解引用:
**ppi
8.3 动态数组
- 使用
new
和delete
表达和 c 中malloc
和free
类似的功能,即在堆(自由存储区)中分配存储空间。 - 定义:
int *pia = new int[10];
10 可以被一个变量替代。 - 释放:
delete [] pia;
,注意不要忘记[]
。
9 小结
string 和 vector 是两种最重要的标准库类型。string 对象是一个可变长的字符序列,vector 对象是一组同类型对象的容器。
迭代器允许对容器中的对象进行间接访问,对于 string 对象和 vector 对象来说,可以通过迭代器访问元素或者在元素间移动。
数组和指向数组元素的指针在一个较低的层次上实现了与标准库类型 string 和 vector 类似的功能。一般来说,应该优先选用标准库提供的类型,之后再考虑 C++ 语言内置的低层的替代品数组或指针。
10 术语表
begin: 是 string 和 vector 的成员,返回指向第一个元素的迭代器。也是一个标准库函数,输入一个数字,返回指向该数字首元素的指针。
缓冲区溢出(buffer overflow): 一种严重的程序故障,主要的原因是试图通过一个越界的索引访问容器内容,容器类型包括 string,vector 和 数组等。
C 风格字符串(C-style string): 以空字符结束的字符数组。字符串字面值是 C 风格字符串,C 风格字符串容易出错。
类模板(class template): 用于创建具体类型的模板。要想使用类模板,必须提供关于类型的辅助信息。例如,要定义一个 vector 对象需要指定元素的类型:vector 包含 int 类型的元素。
编译器扩展(compiler extension): 某个特定的编译器为 C++ 语言额外增加的特性。基于编译器扩展编写的程序不易移植到其他的编译器上。
容器(container): 是一种类型,其对象容纳了一组给定类型的对象。 vector 是一种容器类型。
拷贝初始化(copy initialization): 使用赋值号( = )的初始化形式。新创建的对象是初始值的一个副本。
difference_type: 由 stirng 和 vector 定义的一种带符号整数类型,表示两个迭代器值之间的距离。
直接初始化(direct initialization): 不使用赋值号( = )的初始化形式。
empty: 是 string 和 vector 的成员,返回一个布尔值。当对象的大小为 0 时返回真,否则返回假。
end: 是 string 和 vector 的成员,返回一个尾后迭代器。也是一个标准库函数,输入一个数组,返回指向该数组尾元素的下一个位置的指针。
getline: 在 string 头文件中定义的一个函数,以一个 istream 对象和一个 stirng 对象为输入参数。割爱函数首先读取输入流的内容直到遇到换行符停止,然后将读入的数据存入 string 对象,最后返回 istream 对象。其中换行符读入但是不保留。
索引(index): 是下标运算符使用的值。表示要在 string 对象,vector 对象或者数组中访问的一个位置。
实例化(instantiation): 编译器生成一个指定的模板类或函数的过程。
**迭代器(iterator):**是一种类型,用于访问容器中的元素或者在元素之间移动。
迭代器运算( iterator arithmetic): 是 string 或 vector 的迭代器的运算:迭代器与整数相加或相减得到一个新的迭代器,与原理的迭代器相比,新迭代器向前或向后移动了若干个位置。两个迭代器相减得到它们之间的距离,此时它们必须指向同一个容器的元素或该容器尾元素的下一位置。
以空字符结束的字符串(null-terminated string): 是一个字符串,它的最后一个字符后面还跟着一个空字符(’ \0 ‘)。
尾后迭代器(off-the-end iterator): end 函数返回的迭代器,指向一个并不存在的元素,该元素位于容器尾元素的下一个位置。
指针运算(pointer arithmetic): 是指针类型支持的算术运算。指向数组的指针所支持的运算种类与迭代器运算一样。
prtdiff_t: 是 cstddef 头文件中定义的一种与机器实现有关的带符号整数类型,它的空间足够大,能够表示数组中任意两个指针之间的距离。
push_back: 是 vector 的成员,向 vector 对象的末尾添加元素。
范围 for 语句(range for): 一种控制语句,可以在值的一个特定集合内迭代。
size : 是 string 和 vector 的成员,分别返回字符的数量或元素的数量。返回值的类型是 size_type。
size_t: 是 cstddef 头文件中定义的一种与机器实现有关的无符号整数类型,它的空间足够大,能够表示任意数组的大小。
string : 是一种标准库类型,表示字符的序列。
using 声明(using declaration): 令命名空间中的某个名字可悲程序直接使用。 using **命名空间 :: ** 名字 上述语句的作用是令程序可以直接使用 名字,而无须写它的前缀部分 命名空间::
值初始化(value initialization): 是一种初始化过程。内置类型初始化为 0,类类型由类的默认构造函数初始化。只有当类包含默认构造函数时,该类的对象才会被值初始化。对于容器的初始化来说,如果只说明了容器的大小而没有指定初始值的话,就会执行值初始化。此时编译器会生成一个值,而容器的元素被初始化为该值。
vector: 是一种标准库类型,容纳某指定类型的一组元素。
++运算符(++ operator): 是迭代器和指针定义的递增运算符。执行 ”加 1 “ 操作使得迭代器指向下一个元素。
[ ] 运算符( [ ] operator): 下标运算符。obj[j]得到容器对象 obj 中位置 j 的那个元素。索引从 0 开始, 第一个元素的索引是 0,尾元素的索引是 obj.size( ) - 1。下标运算符的返回值是一个对象。如果 p 是指针, n 是整数, 则 p[n] 与 *(p+n) 等价。
-> 运算符( -> operator): 箭头运算符,该运算符综合了解引用操作和点操作。a -> b 等价于 (*a).b。
« 运算符(« operator): 标准库类型 string 定义的输出运算符,负责输出 string 对象中的字符。
» 运算符(» operator): 标准库类型 string 定义的输入运算符,负责读入一组字符,遇到空白停止,读入的内容赋给运算符右侧的运算对象,该运算对象应该是一个 string 对象。
! 运算符(! operator): 逻辑非运算符,将它的运算对象的布尔值取反。如果运算对象是假,则结果为真,如果运算对象是真,则结果为假。
&& 运算符(&& operator): 逻辑与运算符,如果两个运算对象都是真,结果为真。只有当左侧运算对象为真时才会检查右侧运算对象。
|| 运算符( || operator): 逻辑或运算符,任何一个运算对象时真,结果就为真。只有当左侧运算对象为假时才会检查右侧运算对象。第 3 章 字符串, 向量和数组