《C++Primer 第五版》——第三章 字符串、向量和数组
3.0 前言
- 内置类型是直接由C++直接定义的,而标准库定义了另外一组更高级性质的类型,它们尚未被实现到计算机硬件中。
- string 和 vector :string 表示可变长的字符串,vector 存放的是某种类型对象的可变长序列。
3.1 命名空间的 using 声明
- 除了使用
namespace::成员
的形式以外,还可以通过 using 指令来指定命名空间甚至是命名空间中的具体成员 。格式分别如下:
using namespace::name; //这是指定命名空间内的具体成员
using namespace; //这是只指定命名空间
- 使用 using 声明后,就无需使用前缀
namespace::
即可使用。 - 头文件不应该包含 using 声明 。由于包含多个头文件,如果使用 using 声明可能会导致相同标识符的冲突。
3.2 标准库类型 string
- 标准库类型 string 表示可变长的字符串,使用 string 类型必须使用 头文件 string 。
- 作为标准库的一部分,string 类型定义在命名空间 std 中。
3.2.1 定义和初始化 string 对象
- 如何初始化类的对象是由类本身决定的。一个类可以定义多个初始化对象的方式,只是这些方式之间要有区别。
- 几种初始化 string 对象的方式:
string s1; //默认初始化,s1 是个空字符串
string s2{ "ss" }; //s2 是字面值 "ss" 除了最后的空字以外的副本
string s3(s1); //s3 是 s1 的副本
string s4 = s1; //s4 是 s1 的副本
string s5("ss"); //s5 是字面值 "ss" 除了最后的空字以外的副本
string s6 = "ss"; //s6 是字面值 "ss" 除了最后的空字以外的副本
string s7 = ("ss"); //s7 是字面值 "ss" 除了最后的空字以外的副本
string s8 = { "ss" }; //s8 是字面值 "ss" 除了最后的空字以外的副本
string s9 (n, 'c'); //使用 n 个连续的 'c' 字符组成的字符串初始化 s9
- 使用赋值运算符(=)初始化一个对象,实际上执行的是 拷贝初始化(copy initialization) ,直接将等号右侧对象的值拷贝到左侧对象中。不使用=,则执行 直接初始化(direct initialization) 。
- 当初始值只有一个时,通常使用拷贝初始化。
- 当初始值像上面代码中的 (n,‘c’) 一样,具有多个值,一般只能使用直接初始化。 但也可以使用拷贝初始化,只是要显式地创建一个对象用于拷贝。 比如:
string a = string(10, 's');
其本质等价于:
string temp (10,'c');
string a = temp;
3.2.2 string 对象上的操作
- 一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义函数方法,也能定义 << 、 + 等各种运算符在该类对象上的新含义。
- string的部分操作:
os << s //将 s 写到输入流 os 中,并返回 os
is >> s // 将 is 输入流中读取一个字符串赋值给 string 对象 s ,该字符串在第一、二处空白之间,最后返回 is
getline(is, s) // 从 is 中读取一行字符串赋值给 s ,字符串以换行符为结尾
s.empty() // s 为空字符串返回 true ,否则返回 false
s.size() // 返回 string 对象内的字符个数
s[n] // 返回 s 中第 n+1 个字符的引用,n 从0开始
s1+s2 // 两个 string 对象相连,返回一个更长的 string
s1=s2 // 拷贝 s2 的值,然后赋值给 s1
s1==s2 // 判断两 string 对象是否完全相同。完全相同:长度相等,字符相同
s1!=s2 // 判断两 string 对象是否不相同
<,<=,>,>= // ①首先判断,谁长就谁大 ②如果一样长,按字典alphabet顺序排序
- 空白 :即空格、制表符、换行符等。
- 可以使用 IO 运算符来读写 string 对象。在执行读取操作(cin >> )时,string 对象会自动忽略输入流开头的空白,并从第一个真正的字符开始读取,直到遇到下一处空白。
- 与内置类型的输入输出一样, string 对象的 IO 操作也是返回(>>、<<)运算符左侧的运算对象为表达式结果,所以可以将多个 string 对象连接在一起。 如:
string a,b,c;
cin >> a >> b >> c;
cout << a << b << c;
- 使用 getline 函数代替原来的 >> 运算符,可以将带有大部分空白符(不包括换行符)的字符串赋值给 string 对象。 getline 函数的参数是一个标准输入流( istream 对象)和一个 string 对象。 getline 将从输入流中读取字符, 当遇到换行符就结束读取操作,将没有换行符的字符串赋值给 string 对象,哪怕输入流开头就是换行符(赋值一个空字符串给 string 对象) 。而 getline 函数的返回值 与 istream 对象的输入操作一样, 也是输入流对象( istream 对象) 。
- getline 函数会丢弃它在输入流中检测到的换行符
- string 对象的成员函数 empty 根据 string 对象是否为空返回对应的布尔值。 空返回 true ,非空返回 false 。 string 对象使用点运算符(.)就可以调用,比如:
s.empty()
。 - string 对象的成员函数 size 返回 string 对象中字符的总个数。
- 而 size 成员函数返回值的类型是类 string 里定义的 size_type 类型。 在标准库里,它是一个无符号类型(unsigned),而且大小足够放下任何 string 对象的字符总个数。
- string 类定义了几种用于比较字符串的运算符。这些运算符将逐一比较 string 对象中的字符,并且对大小敏感。
- == 和 != 分别检验两个 string 对象相等或不相等,从字符的总个数和按相同顺序上的字符是否相同。
- <=、>=、> 和 < 都按照字典顺序的规则:
- 如果两个 string 对象的长度不同,而且两个 string 对象相同位置上的字符相同,则较短的 string 对象小于较长 string 对象。
- 如果两个 string 对象在某些位置上不同,则 string 对象比较的结果其实就是 string 对象中第一对不同字符比较的结果(字符比较是指编码值的比较)。
- 对 string 对象而言, 允许把一个 string 对象的值赋值给另一个 string 对象。 比如
s1 = s2 ;
。 - 两个 string 对象相加得到一个新的 string 对象,而结果一个新 string 对象,内容是两个 string 对象内字符按顺序相连的字符串。
- 标准库允许把字符串字面值和字符字面值转换成 string 对象。当把 string 对象和字符字面值以及字符串字面值混在同一天语句中使用时,必须保证(+)的两个运算对象至少有一个是 string 对象。
- 注意:C++中的字符串字面值的类型不是 string 类型,是最后一个元素为空字符的 low-level const 字符数组
3.2.3 处理 string 对象中的字符
- 在 头文件 cctype 中定义了一组 标准库函数 : 用于处理知道并改变某个字符的工作。
头文件 cctype 中的部分函数及作用:
函数 | 作用 |
---|---|
isalnum( c ) | 当 c 是字母或数字时,返回 true ,否则返回 false |
isalpha( c ) | 当 c 是字母时,返回 true ,否则返回 false |
iscntrl( c ) | 当 c 是控制字符时,返回 true ,否则返回 false |
isdigit( c ) | 当 c 是数字时,返回 true ,否则返回 false |
isgraph( c ) | 当 c 不是空格但可以打印时,返回 true ,否则返回 false |
islower( c ) | 当 c 是小写字母时,返回 true ,否则返回 false |
isprint( c ) | 当 c 是可打印字符时,返回 true ,否则返回 false |
ispunct( c ) | 当 c 是标点符号时(即 c 不是控制字符、数字、字母、可打印空白中的一种),返回 true ,否则返回 false |
isspace( c ) | 当 c 是空白时,返回 true ,否则返回 false |
isupper( c ) | 当 c 是大写字母时,返回 true ,否则返回 false |
isxdigit( c ) | 当 c 是十六进制数字时,返回 true ,否则返回 false |
tolower(c ) | 如果 c 是大写字母,则返回对应的小写字母,否则原样输出 |
toupper( c ) | 如果 c 是小写字母,则返回对应的大写字母,否则原样输出 |
- C++11 新提供了一种新语句: 范围 for (range for)语句 。该语句会遍历给定序列中的所有元素,而且可以对每个元素执行相同的循环体代码。
格式如下:
for (declaration : expression)
statement
其中 expression 部分是一个对象,用于表示一个序列。 declaration 部分负责定义一个变量,它将被用于访问序列中的元素, 确保类型相融最简单的方法是使用 auto 类型说明符。
注意:
- 每次迭代 , decvlaration 部分的变量会被初始化为 expression 部分的下一个元素值。
- 如果想通过 range for 修改对象内的值,就得将 range for 的变量声明为引用类型 。
- range for 语句不能修改序列的长度
int n[] = {2,3,4,5,6,7,8,9,0};
for (auto &nn : n)
{
nn += nn;
}
- 访问 string 对象中的单个字符有两种方式: ①使用 下标运算符([ ]) ;②使用迭代器。
- 下标运算符接受的参数应该是 size_type 类型的值 ,表达式的 返回值是该位置上字符的引用 。所以最好不用带符号类型,以免为负数。
- 如果对无值的位置使用下标运算符([ ]),则结果是未定义的。
- 逻辑运算符:与(&&)、或(|)、非(!),返回值为布尔类型。C++规定只有当左侧运算对象为真时,才会检查右侧运算对象。
- 可以通过 string 类的成员函数 push_back 向 string 对象尾部添加单个字符。
3.3 标准库类型 vector
- 标准库类型 vector 表示对象的集合,其中所有的对象的类型都相同。集合中每个元素都有对应的下标。 通常 vector 也被称为 “容器”(container) 。
- 如果要使用 vector 类型,则要包含同名 头文件 vector 。
- C++ 既有 类模板(class template) ,也有 函数模板 。其中 vector 就是一个类模板。
- 模板本身不是类或函数 ,可以将其看作编译器生成的类或函数编写的一份指南。 编译器根据模板创建类或函数的过程 和 通过类创建对象的过程 都被称为 实例化(instantiation) 。
- 当使用模板时,需要指出编译器应把类或函数实例化成什么类型。
比如 vector :
vector<int> in1;
vector<string> s1;
vector<vector<string>> vecs1; //元素类型为 vector 对象
- 由于引用不是对象,所以 不存在包含引用的 vector 。
- 存在元素类型为 vector 的 vector 对象。
- 老式标准是,如果 vector 对象的元素类型是 vector (或其它模板类型),则声明的时候必须在外层 vector 与其元素类型之间添加一个空格。
vector<vector<int> > a;
3.3.1 定义和初始化 vector 对象
- 常用的初始化 vector 对象的方式: 假设 T 是元素类型
vector<T> v1; //v1 是一个空 vector 对象,潜在的元素是 T 类型,执行默认初始化。
vector<T> v2(v1); //v2 包含 v1 的全部副本
vector<T> v3 = v1; //等价于上条语句
vector<T> v4(n, val); //v4 包含了 n 个重复的元素,每个元素值为 val
vector<T> v5(n); //v5 包含了 n 个重复的元素,每个元素都执行了默认初始化
vector<T> v6{a,b,c......} //v6 包含了初始值个数的元素,每个元素都被赋予了相应的初始值
vector<T> v7 = {a,b,c......} //与上条语句等价
- 可以通过
vector<T> v1;
的方式默认初始化 vector 对象,从而创建一个指定类型的空 vector 。 - vector 类型允许将一个 vector 对象赋值给另一个元素为同类型的 vector 对象。
- C++11 提供列表初始化用于 vector 对象的初始化。
- 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在用括号里。
- 可以通过圆括号来初始化 vector 对象一组指定数量的元素值,第一个参数是指定的个数,第二个参数是元素值。 使用圆括号初始化时不能使用拷贝初始化(不能用 = )。
正确和错误的例子:
vector<T> vv(n, val); //正确
vector<T> vvss = (n, val); //错误
- 如果 只提供 vector 对象的元素数量 而不进行自定义的初始化, 一般只能用圆括号(C++11起支持花括号
{}
)进行直接初始化(不能用 = ) 。
但是有 例外 , 如果 花括号 内有初始值,但类型不能用于列表初始化,却可以用于只指定元素数量直接初始化(不可以使用 = )或指定元素值和数量拷贝初始化或直接初始化,此时可以用于构造 vector 对象。全部情况如下比如:
vector<string> s1{"正确"};
vector<string> s2("错误"); //报错
vector<string> s3{10}; //只指定元素数量直接初始化
vector<string> s4{10, "正确,指定元素值和数量直接初始化"};
vector<string> s5 = {10, "正确,指定元素值和数量拷贝初始化"};
- 只提供 vector 对象的元素数量而不提供初始值,此时库会创建一个 值初始化(value-initialized) 的元素初值,并把它赋给容器中的所有元素,该值由元素类型决定。 前提是该类型支持默认初始化。
3.3.2 向 vector 对象中添加元素
- 当 vector 对象的元素过多时, 先定义一个空的 vector 对象 ,然后通过以某个可控制的表达式为 while 语句的条件以方便结束,每次迭代通过 vector 类的成员函数 push_back(word) 将 word 添加到 vector 对象的结尾。
- 要求:如果循环体内部包含有向 vector 对象添加元素的语句,则不能使用范围 for 语句。 之前在 range for 也讲到其不能用于改变序列长度。
3.3.3 其它 vector 操作
- 常用的 vector 对象的操作:
v.empty() 检查 v 是否为空,是返回 true ,否返回 false
v.size() 返回 v 中元素个数,返回值类型为头文件 vector 里定义的 size_type 类型
v.push_back() 向 v 的结尾添加元素
v.[n] 返回 v 中第 n+1 个元素的引用,注意是引用
v1 = v2 将 v2 的值拷贝到 v1 中
v1 = {a,b,c......} 用列表中的元素进行赋值,不是添加
v1 == v2 判断两 vector 对象的元素数量和对应元素是否都相同
v1 != v2 判断两 vector 对象的元素数量和对应元素是否有不同
> 、 < 、 >= 、 <= 按字典顺序比较
- 可以使用范围 for 语句处理 vector 对象的每一个元素。
- 能否比较的前提:两个 vector 的元素为同类型。
同样地, vector 对象的比较也遵循字典顺序 :- 如果两个 vector 对象的长度不同,而且两个 vector 对象相同位置上的值相同,则较短的 vector 对象小于较长 vector 对象。
- 如果两个 vector 对象在某些位置上不同,则 vector 对象比较的结果其实就是 vector 对象中第一对不同值比较的结果(。
- vector 对象的下标类型是头文件 vector 里定义的 size_type 类型。
- 不能通过下标的形式为 vector 对象添加新元素,vector 和 string 对象的下标运算符只能用于访问已存在的元素。 否则会导致 缓冲区溢出(buffer overflow) 。
3.4 迭代器介绍
- 迭代器(iterator): 可使用迭代器的类型是 标准库容器或 string 类 ,可使用迭代器访问对象中的某个元素,迭代器也能从一个元素移动到另一个元素。迭代器类似指针类型,提供了对对象的间接访问。
- 标准库定义了多个容器模板,每个容器都可以使用迭代器,但只有少数几种才同时支持下标运算符。
- 迭代器有有效与无效之分 ,有效的迭代器是 指向某个元素或指向容器中尾元素的下一个位置 ,其它情况都为无效。
3.4.1 使用迭代器
- 有迭代器的类型,同时拥有返回迭代器的函数成员 。比如 这些类型都有名为 begin 和 end 的成员函数 。begin() 返回指向第一个元素(或字符)的迭代器。 end() 返回指向容器或 string 对象 “尾元素的下一位置(one past the end)” 的迭代器,这个迭代器通常被称为 尾后迭代器(off-the-end iterator) 。
如果容器或 string 对象为空,则成员函数 begin 和 end 返回的迭代器是 同一个迭代器 。
- 尾后迭代器指向的是容器或 string 对象的一个不存在的元素,该迭代器只能用作标记,表示处理完了迭代器。
- 迭代器支持的一些运算:
*iter 返回迭代器 iter 所指元素的引用
iter -> mem 解引用迭代器 iter 并获取该元素的名为 mem 元素的成员,等价于 (*iter).mem
++iter 令迭代器 iter 指向容器或 string 对象的下一个元素
iter++ 令迭代器 iter 指向容器或 string 对象的下一个元素
--iter 令迭代器 iter 指向容器或 string 对象的上一个元素
iter-- 令迭代器 iter 指向容器或 string 对象的上一个元素
iter1 == iter1 判断两个迭代器是否相等(或不相等)
iter1 != iter2 如果两个迭代器指向同一个容器或同一 string 对象的相同元素,或它们是同一个容器或同一 string 对象的尾后迭代器,则相同。
- 通过解引用运算符( * )使迭代器获取它所指向元素的值,但是该迭代器必须合法且确实指向某个元素。解引用一个非法迭代器或者尾后迭代器都是未定义的行为。
- 迭代器通过递增运算符(++)从一个元素移动到下一个元素。
- 因为 尾后迭代器并不指向实际的某个元素 ,所以 不能使用解引用的操作。
- 拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器类型。其中 iterator 类型的迭代器是 可以访问并修改其指向的元素 ,而 const_iterator 类型的迭代器 只能访问而不能修改其指向的元素 。
- 如果容器对象或 string 对象是常量,只能使用 const_iterator ;如果容器对象或 string 对象是不常量,则既能使用 iterator 也能使用 const_iterator 。
- 如何定义一个迭代器:
vector<int>::iterator it1; //定义一个 iterator 类型的迭代器 it1 ,指向 vector<int> 对象
string::iterator it2; //定义一个 iterator 类型的迭代器 it2 ,指向 string 对象
auto it3 = a.begin(); //定义一个 iterator 类型的迭代器 it3 ,指向 a 的第一个元素
vector<int>::const_iterator it4; //定义一个 const_iterator 类型的迭代器 it4 ,指向 vector<int> 对象
string::const_iterator it5; //定义一个 const_iterator 类型的迭代器 it5 ,指向 string 对象
- begin() 和 end() 返回的迭代器类型由对象是否为常量决定。如果对象是常量,则返回 const_iterator 类型的迭代器,如果对象不是常量,则返回 iterator 类型的迭代器。
- C++11 新增两个容器模板和 string 类的成员函数: cbegin 和 cend 。它们的作用与 begin 和 end 一样, 但是返回的迭代器类型一定为 const_iterator 。
- 箭头运算符(->) :把 先解引用和再成员访问 两个操作结合起来。
- 可能改变 vector 对象长度的操作,比如 push_back ,都会使得之前定义的该 vector 对象的迭代器失效。
3.4.2 迭代器运算
- 所有的标准库容器都支持 ++ 。也能用关系运算符,对任意标准库类型的,指向同一个容器对象的两个有效迭代器进行比较。
- vector 和 string 的迭代器支持的运算:
iter + n 迭代器加上一个整数的结果是一个迭代器。迭代器指向的元素位置与原来相比向后移动 n 个元素。
iter - n 迭代器减去一个整数的结果是一个迭代器。迭代器指向的元素位置与原来相比向前移动 n 个元素。
iter += n 迭代器加法的复合赋值语句
iter -= n 迭代器减法的复合赋值语句
iter1 - iter2 相减的前提是指向同一个容器对象或 string 对象的元素,或者指向同一个容器对象或 string 对象的尾元素的下一位。两个迭代器相减的结果是它们之间的距离。
< 、 > 、 <= 、 >= 迭代器的比较,如果两个迭代器所指向的位置,越靠前的越小。前提是指向同一个容器对象或 string 对象的元素,或者指向同一个容器对象或 string 对象的尾元素的下一位。
- 迭代器相减的结果 是一个名为 difference_type 类型的 带符号整数 。容器 vector 和 string 类都定义了 difference_type 。
- 使用迭代器运算的一个经典算法是二分法。 中间点一般 是
mid = beg + (end - beg)/2
,而非mid = (end + beg)/2
。
原因是:- 前者不会产生溢出,而后者可能会。
- 前者适用于对迭代器的操作(迭代器相减),而后者(迭代器相加)不行,这个操作未定义,编译报错。
3.5 数组
- 数组是一种类似标准库类型 vector 的数据结构,也是存放类型相同的对象的容器。是通过地址访问的。
- 数组的大小是不变的。
3.5.1 定义和初始化内置数组
- 数组是一种复合类型。数组的声明类似于
T a[d]
,其中 T 是存放的数据类型,a 是数组名,d 是数组的维度,也就是元素个数。 d 必须大于 0 。 - 数组的维度必须是常量表达式。
- 如果数组存放的数据的类型有定义默认初始化,则按照相应规则执行定义或未定义的默认初始化。
- 数组的元素应该为对象,所以 不存在存放数据类型为引用的数组 。
- 不允许使用 auto 来通过初始值推断数组存放的数据类型。
- 当对数组初始化时,未被初始化的元素将被初始化为默认初始值。
数组初始化形式有两种,但都是同一种方式(聚合初始化,数组的聚合初始化对于数组的每一个元素采取了复制初始化的方式):
// 下面两种初始化形式是一样的,都是聚合初始化
int a[10] = {1, 2, 3}; //初始化前三个元素,其它被初始化为 int 的默认初始值
int a[10] {1, 2, 3};
由上面的可知, 如果想初始化全部为 0 ,可以使用空的列表初始化:
unsigned scores[11]{};
unsigned scores[11] = {};
- 如果没有指明数组长度,编译器会通过初始值的长度推测出来。
比如:
char ss[] = "12345"; //一共有6个元素,最后一个为空字符'\0'
- 对于字符数组,可以使用特殊的初始值(字符串字面值)来初始化。字符数组的长度为字符串的字符个数 + 1,因为字符串字面值的最后一个字符是空字符。
- 注意:C++中的字符串字面值的类型不是 string 类型,是最后一个元素为空字符的字符数组 !!!
- 不能将数组的内容通过用数组名赋值的方式拷贝给另外的数组。比如
,这是错误的。 因为只使用数组名时,通常会转换成指向数组首元素的指针。并且数组名不是左值,不可以改变。a = b;
- 理解复杂的数组声明 :从数组的名字开始按照从内到外的顺序阅读。
比如:
int *ptrs[10]; // ptrs 是含有10个 int 指针的数组
int &refs[10] = /* ?*/; //错误,不存在引用的数组
int (*Parray)[10] = &arr; // Parray 指向一个含有10个 int 变量的数组
int (&arrRef)[10] = arr; // arrRef 引用一个含有10个 int 变量的数组
3.5.2 访问数组元素
- 数组元素可以通过下标运算符([ ])、范围 for 语句和解引用运算符( * )来访问。
- 使用数组下标时,通常将其定义为 size_t 类型。 size_t 是 一种与机器相关的无符号类型 ,该类型被设计得 足够大以便表示内存中任意对象的大小(数据大小) 。该类型在头文件 cstddef 中定义。
- 数组的下标是否在合理范围内有程序员负责检查。
- 大多数常见的问题都是源于缓冲区的溢出错误。 当数组或其它类似数据结构的下标越界并试图访问非法内存区域时,就会产生该类错误。
3.5.3 数组与指针
- C++中,数组名储存数组首元素的地址。 所以数组名其实就是一个指向数组首元素的指针。
- 数组名的类型,其实就是维度+元素类型 ,例如
int a[10];//此时a的类型为 int[10]
,并不是 int* ,只是有时候可以隐式将其转换成 int *。
当初始值为数组名时,使用 auto 推断,返回类型是一个指向数组元素类型的指针。
int scores [10]{};
auto b(scores); // b 的类型为 int * 。
使用 decltype() 推断数组名时,返回的是数组类型和维度与该数组相同的数组类型。
int scores [10]{};
decltype(scores) a; // a 的类型为 int [10] 。
-
数组名和&数组名 :设数组名为 array ,那么 array 和 &array 的值实际上是相等的,当然是数值相等,而它们代表的含义则是完全不同的: array 的值是数组首元素的地址, &array 的值是一个指向数组的指针;假设 array 是一个
int[10]
类型的数组,那么 array 的类型为int[10]
, &array 的类型为int[10]*
,是一个指针
例如有以下计算结果:
-
容器 vector 和 string 类的迭代器支持的运算,数组的指针全都支持。
-
尾后指针: 数组尾元素指向的下一个位置,尾后指针不指向具体元素,所以 不能执行解引用或递增操作。
-
C++11新增了关于数组的函数 begin() 和 end() ,与容器模板和 string 类不同的是,它们为 非成员函数 , 返回值分别是首元素和尾元素下一位置的指针(地址) 。它们定义于头文件 iterator 。
-
指针比较: 仅当内置指针指向同一对象的成员或同一数组的元素时,才严格定义指向对象的指针的关系比较(用于小于或大于比较),数组下标越高越大。
-
指针相减的前提是两个指针 指向同一类型对象或数组(维度可以不一样) ,相减的结果的类型是一种名为 ptrdiff_t 的标准库类型。与 size_t 一样定义在 cstddef 头文件,因为结果可能为负值,所以它是一种 带符号整型 ,表示两指针之间差了多少个该类型元素。
-
虽然指向相同类型不同对象的指针之间可以比较,但是 两个指向不同对象的指针比较一般是没有意义的 。
-
直接比较两个无关指针(即不指向同一对象或数组)将产生未定义的行为
-
指针运算使用于 空指针 。
-
解引用和指针运算的交互:
a[4]
等价于*(a+4)
。 -
标准库类型限定使用的下标类型必须是无符号类型(也就是不能为负数) ,而 内置类型的下标不作限制 。 比如数组的下标可以处理负值。
int a[4];
int* p = &a[3];
int d = p[-2];
3.5.4 C风格字符串
- 字符串字面值是一种通用结构的实例,这种结构是从C++从C语言继承而来的 C风格字符串(C-style character string) 。
- C风格字符串不是一种类型,而是一种写法——以空字符(‘\0’)结尾的 char 数组。
- 关于操作C风格字符串 的函数,被定义在 cstring 头文件中。这些函数的参数只能是C风格字符串,而不能是 string 类。
比如:
strlen(p) 返回 p 的长度,不包括空字符。
strcmp(p1, p2) 比较 p1 和 p2 是否相等,相等返回 0 ,如果 p1>p2 ,返回一个正值,如果 p1<p2 ,返回一个负值
strcat(p1, p2) 将 p2 附加到 p1 之后,返回 p1
strcpy(p1, p2) 将 p2 拷贝给 p1 ,返回 p1
- C++中的 字符串字面值 是 const char 数组 ,是 low-level const 。
- C风格字符串的比较不能像 string 对象一样使用关系运算符进行比较 ,因为这样实际上比较的是指针而非字符串本身。
const char ca1[] = "hanhan";
const char ca2[] = "asasas";
ca1 < ca2 ; //比较的是地址,没有意义
3.5.5 与旧代码的接口
- 混用 string 对象和C风格字符串
- 允许使用C风格字符串来初始化 string 对象或为 string 对象赋值;
- 标准库允许把字符串字面值和字符字面值转换成 string 对象。当把 string 对象和字符字面值以及字符串字面值混在同一天语句中使用时,必须保证(+)的两个运算对象至少有一个是 string 对象。
- string 对象在使用复合赋值运算符时,右侧运算对象可以是C风格字符串。
- 不可以用 string 对象来初始化C风格字符串。
- string 类 定义了一个 成员函数 c_str() 。 返回值是一个与 string 对象相同内容的C风格字符串的首地址 ,也就是一个指针 (const char * ) 。
该返回值是指向 string 对象的,当 string 对象发生改变时,之前 c_str() 返回指针指向的C风格字符串的内容也发生改变。
const char *a;
a = ss.c_str(); //此时 a 为 "hanhan"
ss = "ss"; //此时 a 为 "ss"
所以最好别像上面代码一样使用 c_str() ,一旦 string 对象被析构,之前 c_str 返回的指针就成了无效指针。
注意:一定要使用 strcpy() 函数等方法操作 c_str() 返回的指针
char a[20];
strcpy(a, ss.c_str()); //此时 a 为 "hanhan"
- 不允许使用一个 vector 对象初始化一个数组。
- 允许使用一个数组来初始化 vector 对象。 只需要数组首元素地址和尾后地址,然后使用圆括号直接初始化即可。
int a [] = {1,2,3,4};
std::vector<int> aa ( begin(a), end(a) );
用于初始化 vector 对象的值可能也仅仅是数组的一部分。
int b [] = {1,2,3,4,5,6,7};
std::vector<int> bb ( &b[3], &b[5] );
使用 (首地址,尾地址)
和 直接初始化 ,可以使用数组的一部分或整个数组初始化 vector 对象。
3.6 多维数组
- 多维数组其实是数组的数组。
- 对于二维数组,常把第一个维度称作行,第二个维度称作列。
- 多维数组的初始化:
比如:
int ia [3][4] = {
{1,2,3,4}
{1,2,3,4}
{1,2,3,4}
}
//等价于
int ia [3][4] = {1,2,3,4,1,2,3,4,1,2,3,4}
如果想初始化全部元素为默认初始值 ,可以像下面代码一样定义。这样就不怕是在块内还是在全局作用域了(内置类型在块内不会执行默认初始化)。
int b[4][2][2] = {};
与一维数组一样,无论以什么形式初始化多维数组,只要初始化时未被初始化的元素,就执行默认初始化。
4. 当 数组名含有的下标运算符数量 比数组的第二个维度(几维数组)小,表达的是一个内层数组;一样多,表达的就是一个给定类型的元素。
int a[3][3][3] = {};
int (&aa)[3][3] = a[0]; // aa 是指向 a 内第一个二维数组的引用
- 通常会使用两层嵌套的 for 语句来处理二维数组,外层循环行,内层循环列。
- 如何 使用嵌套的 range for 语句处理多维数组?
要使用范围 for 语句处理多维数组时,除了最内层的循环外,其他所有循环的控制变量都必须是引用类型。 。
如果要改变多维数组内元素的值,就得将最内层的控制变量声明为引用 。
比如二维数组:
int aa[3][3] = {};
for (auto &n : aa)
{
for (auto nn : n)
{
std::cout << nn <<std::endl;
}
}
如果不设置除了最内层外的控制变量为引用,会出现什么情况?——无法通过编译
for (auto n : aa)
for (auto nn : n)
因为 range for 语句中的对象没被声明成引用类型 ,编译器初始化它时, 会将数组形式的元素转换成指向该数组的首元素的指针 。比如在这里 n 会被声明为 int * ,而后续则是想在 int * 内遍历,这是错误的。
7. 多维数组的名字,其实也是指向数组首元素的指针,也就是指向第一个内层数组的指针。如此类推。
8. C++ 使用多维数组名时,通常也会自动将其转换成指向数组首元素的指针。
9. 使用类型别名,简化多维数组的指针声明。
using int_array4 = int[4];
等价于
typedef int int_array4 [4];