字符串、向量和数组
1 命名空间的using声明
- 命名空间可以帮助我们避免不经意的名字定义冲突,已使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间std中。
- std::cout,通过 作用运算符 :: 来指出我们想使用定义在命名空间 std 中的 cout。
注1: 最好每个名字都使用独立的using声明,如 using std::cin,若将命名空间所有名字取出可以使用 using namespace xxx。
因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
注2: 头文件不应包含using声明。
2 标志库类型string
string 表示可变长的字符序列,使用string需要包含string头文件。string定义在命名空间std中。
2.1 定义和初始化string对象
sting s1; // 默认初始化,s1是一个空串
sting s2(s1); // s2是s1的副本
string s2 = s1; // 等价于s2(s1), s2是s1的副本
sting s3("value"); // s3是字面值"value"的副本,除了最后的那个空字符外
string s3 = "value"; // 等价于 s3("value"),s3是字面值"value"的副本
string s4(n, 'c'); // 把s4初始化为由连续n个字符c组成的串
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,则执行的是直接初始化。
2.2 string对象上的操作
getline读取一整行
getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止( 注: 换行符也被读入,但是并没有被存储到string中)。
getline只要一遇到换行符就结束读取操作并返回结果,如果输入一开始就是换行符,那么所得的结果就是个空string。
getline(cin, line);
注: 触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。
string 的 size 操作
size 函数返回 string对象的长度(即string对象中字符的个数)
string::size_type类型
实际上 string 中的 size 函数返回的是 string::size_type 类型的值。
auto len = line.size(); // len的类型是 string::size_type
由于size函数返回的是一个无符号整数型,故表达式中不能混用带符号数和无符号数。例如,建设n是一个具有负值的int,则表达式 s.size() < n 的判断几乎肯定是true。这是因为负值 n 会自动地转换成一个比较大的无符号值。
注: 如果表达式中以及有了 size() 函数就不要再使用 int 了,这样可以避免混用 int 和 unsigned 可能带来的问题。
字面值和string对象相加
当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+) 的两次的运算对象至少有一个是string
string s1 = "hello", s2 = "world"
sting s3 = s1 + ", " + s2 + '\n'; // 正确
string s4 = "hello" + ", "; // 错误,两个运算对象都不是string
// 正确: 每个加法运算符都有一个运算对象是 string
sting s5 = s1 + ", " + "world";
stirng s6 = "hello" + ", " + s2; // 错误: 不能把字面值相加
s5 的初始化形式之所以正确是因为,它的工作机理和连续输入连续输出是一样的,可以用如下的形式分组:
sting s5 = (s1 + ", ") + "world";
所以可以看作两次加法都拥有一个string对象参与。
2.3 处理string对象中的字符
头文件cctype
函数名 | 功能 |
---|---|
isalunum(c) | 数字或字母 |
isalpha(c) | 字母 |
iscntrl(c) | 控制字符 |
isdigit(c) | 数字 |
isgraph(c) | 不是空格,但可以打印时 |
islower(c) | 小写字母 |
isprint(c) | 大打印字符,即使是空格 |
ispunct(c) | 标点符号 |
isspace(c) | 空白 |
isupper(c) | 大写字母 |
isxdigit(c) | 十六进制数字 |
tolower(c) | 如果是大写字母,输出小写 |
toupper(c) | 如果是小写字母,输出大写 |
注: C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。一般来说,C++程序应该使用名为 cname 的头文件而不使用 name.h 的形式,标准库中的名字总能在命名空间std中找到。 |
范围for语句
范围for 语句,遍历给定序列中的每个元素并对序列中的每个值进行某种操作,其语法形式是
for (declaration: expression)
statement
其中 expression 部分是一个对象,用于表示一个序列。 declaration 部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代, declaration 部分地变量会被初始化为 experssion 部分的下一个元素值。
3 标准库类型 vector
标准库类型 vector 表示对象的集合,也被称为 容器, 包含在头文件 vector 中是一个 类模板,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。
模板本身不是类或函数,相反可以将模板看作编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为 实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
注: vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型。
vector 能容大绝大多数类型的对象作为其元素, 但是因为引用不是对象,所以不存在包含引用的 vector。
3.1 定义和初始化 vector 对象
vector<T> v1; // v1是一个空vector,它的元素类型是T
vector<T> v2(v1); // v2中包含有v1所有元素的副本
vector<T> 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对象的,如果使用花括号,可以表述成我们想列表初始化该vector对象。
3.2 向vector对象中添加元素
可以利用 vector 的成员函数 push_back 向其中添加元素,push_back 负责把一个值当成 vector 对象的尾元素“压到” vector 对象的尾端。
在定义vector对象的时候,没有必要设定其大小,事实上如果这么做性能可能会更差。
注: 如果循环体内包含有向 vector 对象添加元素的语句,则不能使用范围for循环, 范围for语句体内不应该改变其所遍历序列的大小。
size 返回 vector 对象中元素的个数, 返回值的类型是由 vector 定义的 size_type 类型。
vector<int>::size_type // 正确
vector::size_type // 错误
注: 要使用size_type, 需要首先指定它是由那种类型定义的。
不能用下标形式添加元素
vector 对象、string 对象的下标运算符可用于访问已存在的元素,而不能用于添加元素。试图用下标去访问一个不存在的元素将引发错误,而且这种错误不会被编译器发现,而是再运行时产生一个不可预知的值。
这种通过下标访问不存在的元素的行为非常常见,而且会产生非常严重的后果。所谓的 缓冲区溢出 指的就是这类错误。
缓冲区溢出 是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。危害有以下两点:
- 程序崩溃,导致拒绝服务
- 跳转并且执行一段恶意代码
确保下标合法的一种有效手段就是尽可能使用范围 for 语句。
4 迭代器
不仅可以通过下标运算符来访问 string 和 vector 的元素,还有另一种更通用的机制, 迭代器。所有标准库容器都可以使用迭代器,只有少数几种才同时支持下标运算符。
4.1 使用迭代器
获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员,如 begin 和 end。 begin 负责返回指向第一个元素的迭代器,end 负责返回指向容器 “尾元素的下一个位置” 的迭代器。这样的迭代器没有实际含义,仅是一个标记,所以end返回的迭代器常被称作 尾后迭代器或尾迭代器。
// b表示v的第一个元素,e表示v尾元素的下一个位置
auto b = v.begin(), e = v.end(); // b 和 e 的类型相同
注: 若容器为空,则 begin 和 end 返回的是同一个迭代器,尾迭代器。
迭代器运算符
操作 | 含义 |
---|---|
*iter | 返回迭代器item所指元素的引用 |
item->mem | 解引用iter并获取该元素名为mem的成员 |
++item | 令iter指示容器中的下一个元素 |
--item | 令iter指示容器中的上一个元素 |
迭代器类型
可以使用 iterator 和 const_iterator 来标识迭代器的类型
vector<int>::iterator it1; // it1能读写vector<int>的元素
string::iterator it2; // it2能读写string中的字符
vector<int>::const_iterator it3; // it3只能读vector<int>的元素
string::const_iterator it4; // it4只能读string中的字符
const_iterator 和 常量指针 差不多,如果 vector对象或string对象是常量,那么只能使用const_iterator。
begin和end运算符
begin 和 end 返回的具体类型由对象是否是常量决定,如果是常量返回 const_iterator,如果不是常量返回iterator。
如果对象只需要读操作而无须写操作的话最好使用常量类型。为了便于专门得到const_iterator类型的返回值,C++11引入了两个新函数 分别是 cbegin 和 cend,上述两个新函数无论对象本身是否是常量返回值都是 const_iterator
结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型是类,就可能希望进一步访问它的成员。
(*it).empty() // 解引用it,然后调用结果对象的empty成员
*it.empty() // 错误,试图访问it的名为empty的成员,但it是个迭代器,
注意(*it).empty()中的圆括号必不可少,该表达式的含义是先对it解引用,然后解引用的结果在执行点运算符。如果不适用圆括号,点运算符将由it来执行,而非it解引用的结果。
为了简化上述表达式,C++语言定义了箭头运算符(->),箭头运算符把解引用和成员访问两个操作结合在一起。即 it->mem 和 (*it).mem 表达的意思相同。
某些对vector对象的操作会是迭代器失效
- 在范围for循环中向vector对象添加元素
- 使用push_back向vector添加元素
注: 但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
4.2 迭代器运算
迭代器距离
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器距离。所谓距离就是右侧的迭代器向前移动多少位置能追上左侧迭代器,其类型是名为 difference_type 的带符号整形数,string 和 vector 都定义了 difference_type, 因为这个距离可正可夫,所以 difference_type 是带符号类型的。
5 数组
数组大小确定不变,不能随意向数组中增加元素。如果不清楚元素的确切个数使用 vector,否则使用数组。
5.1 定义和初始化内置数组
数组是一种复合类型。声明形如 a[d],其中 a 是数组名字,d 是数组维度。唯独说明了数组中元素的个数,因此必须大于0,编译的时候维度应该是已知的,也就是说维度必须是一个常量表达式。
unsigned cnt = 42; // 不是常量表达式
constexpr usigned sz = 42; // 是常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整形指针的数组
string bad[cnt]; // 错误,cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确
字符数组的特殊性
字符数组有一种额外的初始化形式,可以使用字符串字面值对其初始化,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串其他字符一样被拷贝到字符数组中去。
char a1[] = {'c', '+', '+'}; // 列表初始化,没有空字符
char a1[] = {'c', '+', '+'}; // 列表初始化,含有显示的空字符
char a3[] = "c++"; // 自动添加表示字符串结束的空字符
const char a4[3] = "c++"; // 错误 没有空间可存放空字符
理解负责的数组声明
int *ptr[10]; // ptr时含有10个整形指针的数组
int &refs[10] = /* ?*/; // 错误 不存在引用的数组
int (&Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
Parray: *Parray意味着Parray是个指针,接下来看右侧,可知道Parray是个指向大小为10的数组的指针,最后看左边,知道数组中的元素时int,就可以知道Parray是要给指针,它指向一个int数组,数组中包含10个元素
int *(&arry)[10] = ptr;
按照从内向外顺序阅读,首先知道arry是一个引用,然后观察右边知道,arry引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素时指向int的指针,这样arry就是一个含有10个int型指针的数组的引用。
注: 不存在引用的数组,且要想理解数组声明的含义,最好的办法是从数组的名字看是,按照由内向外、由右向左的顺序阅读。
5.2 访问数组元素
在使用数组下标的时候,通常将其定义为 size_t 类型。size_t 是一种机器相关的无符号类型,在 cstddef 头文件中定义了 size_t 类型。
5.3 指针和数组
数组有一个特性,在用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。
string *p = &num[0];
string *p = num;
二者等价。
由此可知,在一些情况下数组的操作实际上是指针的操作,也就是当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。
int ia[] = {1, 2, 3};
auto ia2(ia); // ia2是一个整形指针,指向ia的第一个元素
ia2 = 42; // 错误,ia2是一个指针,不能透过int给指针赋值
编译器执行初始化的过程类似于下面的形式
auto ia2(&ia[0]); // 显然ia2的类型是int*
但是,当使用decltype关键字时,上述转换不会发生,decltype(ia)返回的类型是由10个整数构成的数组
指针也是迭代器
指向数组元素的指针拥有更多功能,vector 和 string 迭代器支持的运算,数组的指针全都支持,同时也拥有 begin 和 end 两个函数 定义在iterator。。
int ia[] = {1, 2, 3};
int *begin = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向ia尾元素的下一位置的指针
指针运算
和迭代器一样,两个指针相减的结果是它们之间的距离,参与的两个指针必须指向同一个数组当中的元素,两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,和 size_t 一样, ptrdiff_t也是定义在 cstddef 头文件中的相关类型。因为差值可能为负,所以 ptrdiff_t 是一种带符号类型,
5.4 C风格字符串
尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格自负床不仅使用起来不方便,而且极易引发程序漏洞,这是诸多安全问题的根本原因,如果需要使用 可引用 cstring 头文件。
5.5 与旧代码的接口
有一些C++程序实际上是与C语言或其他语言的接口程序,当然也无法使用C++标准库。因此,现代的C++程序不得不与那些充满了数组和/或C风格字符串的代码衔接,为使这一工作简单易行,C++提供了一组功能。
混用string对象和C风格字符串
之前有说可以使用字符串字面值来初始化string对象,但是更一般的情况是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
- 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不是两个运算对象都是):在string对象的复合赋值运算中允许使用以空字符数组作为右侧的运算对象。
上述性质反过来就不成立了:如果程序的某处需要一个C风格字符串,无法直接用string对象来代替它。例如,不能用string对象直接初始化指向字符的指针。为了完成该功能,string专门提供了一个名为 c_str 的成员函数
char *str = s;//错误:不能用string对象初始化char*
const char *str = s.c_str(); //正确
c_str函数的返回值是一个C风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char*,从而确保我们不会改变字符数组的内容。
无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。 如果执行完 c_str()函数后,想使用其返回的数组,最好将该数组重新拷贝一份。
使用数组初始化vector对象
要实现这一目的,只需指明要拷贝区域的首元素地址和尾后元素地址就可以了
int int_arr[] = {0,1,2,3,4,5};
//ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));
用于创建ivec的两个指针实际上指明了用来初始化的值在数组int_arr中的位置,其中第二个指针应指向待拷贝区域尾元素的下一个位置。用于初始化vector对象的值也可能仅是数组的一部分
vector<int> subVec(int_arr + 1, int_arr + 4);
注: 现在C++程序应尽量使用 vector 和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。
6 多维数组
严格来说,C++中并没有多维数组,多维数组其实是数组的数组。
多维数组的初始化
int ia[2][3] = { // 两个元素,每个元素都是大小为3的数组
{1, 2, 3}, // 初始化第一个元素
{3, 4, 5} // 初始化第二个元素
}
// 内层嵌套的花括号并非必需的
int ia[2][3] = {1, 2, 3, 3, 4, 5}; // 与上方方法等价
使用范围 for 语句处理多维数组
将外层循环的控制变量声明成引用类型,是为了避免数组被自动转换成指针,假设不使用引用类型,则代码描述成
for (auto row: ia)
for (auto col: row)
此时程序将无法通过编译。这是因为,编译器初始化row时会自动将这些数组形式的元素转换成指向该元素内首元素的指针。这样得到的 row 的类型就是 int*,这样内层的循环就不合法了。
注: 要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
指针和多维数组
因为多维数组实际上是数组的数组,所以由多维数组名转换的来的指针实际上是指向第一个内层数组的指针。
注: 在声明中,圆括号必不可少。
int *ip[4]; // 整形指针的数组
int (*ip)[4]; // 指向含有4个整数的数组