C++ Primer 5th 阅读笔记:字符串,vector 和数组

发布时间 2023-05-21 19:49:17作者: Revc

前言

C++ 定义了丰富的抽象数据类型。
string 支持变长字符串。
vector 支持变长集合。
迭代器用于访问容器中的元素,比如,string 中的字符和vector 中的元素。
stringvector 都基于更加原始的数组类型。

使用 using 声明来导入命名空间的名称

通过如下的语句,
using namespace::name;
来直接导入命名空间下的指定名称到当前作用域下。
导入多个名称需要编写多条 using 语句。

注意:头文件应该避免使用 using 声明,因为这可能导致命名冲突。

string

初始化 string 对象的几种方式:

string s1           ; // Default initialization; s1 is the empty string.
string s2(s1)       ; // s2 is a copy of s1.
string s2 = s1      ; // Equivalent to s2(s1), s2 is a copy of s1.
string s3("value")  ; // s3 is a copy of the string literal, not including the null.
string s3 = "value" ; // Equivalent to s3("value"), s3 is a copy of the string literal.
string s4(n, 'c')   ; // Initialize s4 with n copies of the character 'c'.

直接初始化和拷贝初始化

直接初始化:通过向构造器直接提供参数来初始化对象。
拷贝初始化:通过拷贝另一个对象的来初始化对象,这可能会涉及到类型转换。

一般情况下,直接初始化更高效和不容易出错。在初始值为字面量或者常量表达式时,也可以使用拷贝初始化。

string 支持的操作

string::size_type 类型

string 的返回值类型不是 int 而是 string::size_type

string::size_type 是无符号类型,能够存储任何字符串的大小。

字符串字面量

出于历史原因,为了保持和 C 的兼容,字符串字面量的类型不是 std::string

头文件

C++ 标准库整合了 C 标准库。
C 标准库的头文件格式为 <name>.h
C++ 对这些头文件进行了进一步的封装,封装后的头文件名为 c<name>

cctype

range for(范围 for)

range for 语句提供了一种遍历序列中元素的方法。语法形式如下:

for (element : sequence)
  statement(s)

通过定义循环变量为引用类型,来修改序列中的元素。

下标操作符

我们可以通过下标操作符([]) 访问特定的字符。
下标操作符接受 string::size_type 的值作为参数。
下标越界是未定义的行为。

vector

vector 是一组相同类型的对象的集合。
集合中的每一个对象都关联了一个索引。
vector 是一个类模板。C++ 有类模板和函数模板。
编写模板需要对 C++ 有深厚的理解。
模板本身不是类或者函数。模板用于生成类或者函数,这个过程叫做实例化(instantiation)。
我们需要提供额外的信息来帮助模板进行实例化,这些额外信息包含在一对尖括号内,尖括号位于模板名称之后。
vector 的尖括号内指定的包含的对象的类型。
在早期中的 C++ 中,如果 vector 的对象类型也是 vector,我们需要在两个右尖括号之间插入一个空格。
比较 vector<vector<int>>(新)和vector<vector<int> >(旧)。

vector 初始化


列表初始化



如果没有提供初始值,vector 会根据数据类型进行默认初始化。

上面的初始化形式存在两个局限性:

  1. 一些类的初始化,必须显式地提供初始值,没有默认初始化。
  2. 我们只能通过直接初始化来进行。下面的复制初始化是非法的表示。

汇总

列表初始化和直接初始化

如果花括号中的元素类型和 vector 的元素类型不匹配,编译器会尝试用这些值去构造 vector 对象。

vector 增长很高效

我们通常不需要指定 vector 的大小。

添加元素的注意事项

在使用范围 for 循环时,不要向 vector 中添加元素。

vector 支持的一些其他操作

如果元素支持关系运算符,vector 也支持关系运算符。
如果元素支持相等运算符,vector 也支持相关运算符。

vector 下标/索引

vector 的索引对应的类型为 size_type
为了使用 size_type,我们必须提供元素的类型,

注意:下标操作符不会增加元素,下标操作符只能获取已经存在的元素。

尝试访问不存在的元素,是一种未定义的行为(编译器无法检测到)。是一种常见且危险的程序错误。这可能会导致缓冲区溢出(Buffer Overflow)。这个错误常常会引起安全性的问题。
如果可能,尽可能使用范围 for 来替代索引的使用。

迭代器

迭代器提供了一种间接访问容器元素的方式。
我们可以通过迭代器来获取一个元素,或者从一个元素移动到另一位元素。
一个合法的迭代器,要么表示一个元素,要么表示最后一个元素的后一位;除此之外的其他迭代器都是非法的迭代器。

begin() 返回表示第一个元素的迭代器对象。
end() 返回表示超过最后一个元素的迭代器对象。

迭代器支持的操作

注意:解引用一个无效的迭代器或者一个超出末端的迭代器会导致未定义的行为。

迭代器的类型

  • 一般迭代器:可读可写
  • const 迭代器:可读不可写

如果容器本身是 const,那么只能通过 const 迭代器进行访问。begin() 函数也只会返回 const_iterator 类型的迭代器。

如果我们不想对元素进行修改,我们应该尽可能使用 const 迭代器。
C++ 11 引入了两个新的函数:cbegin()cend()

-> 操作符

如果我们通过迭代器访问对象的成员。我们可以通过这样的语句,

(*it).empty()

注意:上面的语句不等价于

*it.empty() // error

因为 * 的优先级比 . 低。

箭头运算符 -> 合并了解引用和成员访问运算符。也就是说 it->mem 等价于 (*it).mem

注意

改变容器大小的操作会使得迭代器无效化。

vector 和 string 的迭代器支持的一些其他操作

除了一些通用的迭代器操作,vectorstring 的迭代器还支持一些额外的操作。这些操作也被称为迭代器算术操作。

iter1 - iter2 返回的类型为 difference_type,这是一个有符号数,因为相减的结果可能是一个负数。

数组

数组包含了一组相同类型的未命名对象。
数组的大小固定。
数组通常拥有更好的运行性能。
数组没有 vector 灵活。
数组是一种复合类型。复合类型包括数组类型、引用类型、指针类型、类类型、函数类型等。
数组的大小必须大于 0。
数组的元素数量是数组的一部分,所以必须在编译阶段就知道数组的大小。
数组的大小可以是字面量或者一个常量表达式。
数组内的元素都会进行默认初始化,但是函数内的内建类型的数组的值是未定义的。
因为引用不是对象,没有包含一组引用的数组。

数组可以使用列表初始化。数组大小必须满足:

  • 必须大于列表中的初始值个数;未指定初始值的元素会进行 值初始化
  • 留空。

值初始化:内建类型会被初始化为0,类类型会使用默认构造器进行初始化。

字符数组可以使用字符串字面量进行初始化。字符串字面量使用 \0(零字符)结尾。

数组不支持赋值拷贝。(不可复制!)

复合数组声明:

数组支持范围 for 循环和下标操作符。下标操作符中的值的类型为 size_t
size_t 是一个平台相关的无符号变量。size_tunsigned int 有所不同,size_t 的取值范围是目标平台下最大可能的数组尺寸。一些平台下 size_t 的范围小于 int 的正数范围, 又或者大于unsigned int.
size_t 定义在 cstddef

大多数(不是所有)情况下,数组名等价于指向对一个元素的指针。

这种说法在以下两种情况下不成立:
1、sizeof(a);
2、&a;
来源:c中,数组名跟指针有区别吗?


对于数组, auto 会返回指针类型。
对于数组,decltype 会返回数组类型。

指针是迭代器的一种。

超出末端指针可以通过 end() 函数来获取。这个函数定义在 iterator 文件头中。


指针支持这些算数操作:


两个指针相减的结果的类型是 ptrdiff_tptrdiff_t 是一个平台相关的有符号类型,被定义在头文件 cstddef

空指针和非数组的指针可以使用算数操作。

指针可以使用下标操作符,指针的下标(内建下标操作符——没有经过重载的)可以是负数

注意:vectorstring 的下标操作符不可以是负数。

C 风格的字符串

尽量避免在 C++ 中使用 C 风格的字符串。

C 风格的字符串的一组函数

C 风格的字符串必须以零字符结尾,否则会导致程序运行错误。

不能使用比较运算符来比较C 风格的字符串。

而要使用 strcmp 函数。

C 风格的字符串函数很容易导致安全问题,比如拼接字符串的目标字符数组大小不足。

推荐:对于绝大部分程序,请使用 string 类型。

接入旧代码

很多早期的 C++ 代码没有使用 string 或者 vector

可以通过以下的方式来混用 string 和 C 风格的字符串(主要是把 C 风格字符串转换成 string 类型):

  • 可以将字符串字面量作为 string 的初始值;
  • 可以将 C 风格的字符串变量赋值给 string 变量;
  • 可以使用 + 或者 += 运算符,对 C 风格的字符串和 string 变量 进行拼接。

我们可以通过 c_str 使得 string 变量返回一个 C 风格的字符串(指针,指向字符数组的第一个字符)。指针的类型为 const char *,这阻止我们修改这个字符数组的内容。
c_str 返回的字符数组不保证长期有效,如果后面我们修改的 string 变量的值,可能会导致c_str 返回的字符数组失效。

我们可以使用数组来初始化 vector 变量:

建议:尽量使用库类型(如,stringvector)来替代数组。

多维数组

C++ 中没有多维数组,实际上这些“多维数组”是数组的数组。

初始化多维数组的方式:

如果下标运算符少于数组的维度,则会返回一个子数组。

通常使用一对嵌套的 for 循环来遍历二维数组的元素:

可以使用范围 for 循环来替代普通的 for 循环:

多维数组的范围 for 循环,外层 for 循环必须是引用类型,否则会将数组类型转换指针类型,指针类型是无法被用于范围 for 循环的。

下面的代码错误:

我们可以使用指针来遍历多维数组: