《C++Primer 第五版》——第三章 字符串、向量和数组

《C++Primer 第五版》——第三章 字符串、向量和数组

  • 3.0 前言
  • 3.1 命名空间的 using 声明
  • 3.2 标准库类型 string
    • 3.2.1 定义和初始化 string 对象
    • 3.2.2 string 对象上的操作
    • 3.2.3 处理 string 对象中的字符
  • 3.3 标准库类型 vector
    • 3.3.1 定义和初始化 vector 对象
    • 3.3.2 向 vector 对象中添加元素
    • 3.3.3 其它 vector 操作
  • 3.4 迭代器介绍
    • 3.4.1 使用迭代器
    • 3.4.2 迭代器运算
  • 3.5 数组
    • 3.5.1 定义和初始化内置数组
    • 3.5.2 访问数组元素
    • 3.5.3 数组与指针
    • 3.5.4 C风格字符串
    • 3.5.5 与旧代码的接口
  • 3.6 多维数组

3.0 前言

  1. 内置类型是直接由C++直接定义的,而标准库定义了另外一组更高级性质的类型,它们尚未被实现到计算机硬件中。
  2. string 和 vector :string 表示可变长的字符串,vector 存放的是某种类型对象的可变长序列。

3.1 命名空间的 using 声明

  1. 除了使用 namespace::成员 的形式以外,还可以通过 using 指令来指定命名空间甚至是命名空间中的具体成员 。格式分别如下:
using namespace::name;  //这是指定命名空间内的具体成员
using namespace;        //这是只指定命名空间
  1. 使用 using 声明后,就无需使用前缀 namespace:: 即可使用。
  2. 头文件不应该包含 using 声明 。由于包含多个头文件,如果使用 using 声明可能会导致相同标识符的冲突。

3.2 标准库类型 string

  1. 标准库类型 string 表示可变长的字符串,使用 string 类型必须使用 头文件 string
  2. 作为标准库的一部分,string 类型定义在命名空间 std 中。

3.2.1 定义和初始化 string 对象

  1. 如何初始化类的对象是由类本身决定的。一个类可以定义多个初始化对象的方式,只是这些方式之间要有区别。
  2. 几种初始化 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 
  1. 使用赋值运算符(=)初始化一个对象,实际上执行的是 拷贝初始化(copy initialization) ,直接将等号右侧对象的值拷贝到左侧对象中。不使用=,则执行 直接初始化(direct initialization)
  2. 当初始值只有一个时,通常使用拷贝初始化。
  3. 当初始值像上面代码中的 (n,‘c’) 一样,具有多个值,一般只能使用直接初始化。 但也可以使用拷贝初始化,只是要显式地创建一个对象用于拷贝。 比如:
string a = string(10, 's');

其本质等价于:

string temp (10,'c');
string a = temp;

3.2.2 string 对象上的操作

  1. 一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义函数方法,也能定义 << 、 + 等各种运算符在该类对象上的新含义。
  2. 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顺序排序
  1. 空白 :即空格、制表符、换行符等。
  2. 可以使用 IO 运算符来读写 string 对象。在执行读取操作(cin >> )时,string 对象会自动忽略输入流开头的空白,并从第一个真正的字符开始读取,直到遇到下一处空白。
  3. 与内置类型的输入输出一样, string 对象的 IO 操作也是返回(>>、<<)运算符左侧的运算对象为表达式结果,所以可以将多个 string 对象连接在一起。 如:
string a,b,c;
cin >> a >> b >> c;
cout << a << b << c;
  1. 使用 getline 函数代替原来的 >> 运算符,可以将带有大部分空白符(不包括换行符)的字符串赋值给 string 对象。 getline 函数的参数是一个标准输入流( istream 对象)和一个 string 对象。 getline 将从输入流中读取字符, 当遇到换行符就结束读取操作,将没有换行符的字符串赋值给 string 对象,哪怕输入流开头就是换行符(赋值一个空字符串给 string 对象) 。而 getline 函数的返回值 与 istream 对象的输入操作一样, 也是输入流对象( istream 对象)
  2. getline 函数会丢弃它在输入流中检测到的换行符
  3. string 对象的成员函数 empty 根据 string 对象是否为空返回对应的布尔值。 空返回 true ,非空返回 false 。 string 对象使用点运算符(.)就可以调用,比如: s.empty()
  4. string 对象的成员函数 size 返回 string 对象中字符的总个数。
  5. 而 size 成员函数返回值的类型是类 string 里定义的 size_type 类型。 在标准库里,它是一个无符号类型(unsigned),而且大小足够放下任何 string 对象的字符总个数。
  6. string 类定义了几种用于比较字符串的运算符。这些运算符将逐一比较 string 对象中的字符,并且对大小敏感。
  7. == 和 != 分别检验两个 string 对象相等或不相等,从字符的总个数和按相同顺序上的字符是否相同。
  8. <=、>=、> 和 < 都按照字典顺序的规则:
    1. 如果两个 string 对象的长度不同,而且两个 string 对象相同位置上的字符相同,则较短的 string 对象小于较长 string 对象。
    2. 如果两个 string 对象在某些位置上不同,则 string 对象比较的结果其实就是 string 对象中第一对不同字符比较的结果(字符比较是指编码值的比较)。
  9. 对 string 对象而言, 允许把一个 string 对象的值赋值给另一个 string 对象。 比如 s1 = s2 ;
  10. 两个 string 对象相加得到一个新的 string 对象,而结果一个新 string 对象,内容是两个 string 对象内字符按顺序相连的字符串。
  11. 标准库允许把字符串字面值和字符字面值转换成 string 对象。当把 string 对象和字符字面值以及字符串字面值混在同一天语句中使用时,必须保证(+)的两个运算对象至少有一个是 string 对象。
  12. 注意:C++中的字符串字面值的类型不是 string 类型,是最后一个元素为空字符的 low-level const 字符数组

3.2.3 处理 string 对象中的字符

  1. 头文件 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 是小写字母,则返回对应的大写字母,否则原样输出
  1. C++11 新提供了一种新语句: 范围 for (range for)语句 。该语句会遍历给定序列中的所有元素,而且可以对每个元素执行相同的循环体代码。
    格式如下:
for (declaration : expression)
  statement

其中 expression 部分是一个对象,用于表示一个序列。 declaration 部分负责定义一个变量,它将被用于访问序列中的元素, 确保类型相融最简单的方法是使用 auto 类型说明符
注意:

  1. 每次迭代 , decvlaration 部分的变量会被初始化为 expression 部分的下一个元素值。
  2. 如果想通过 range for 修改对象内的值,就得将 range for 的变量声明为引用类型
  3. range for 语句不能修改序列的长度
int n[] = {2,3,4,5,6,7,8,9,0};
for (auto &nn : n)
{
	nn += nn;
}
  1. 访问 string 对象中的单个字符有两种方式: ①使用 下标运算符([ ]) ;②使用迭代器。
  2. 下标运算符接受的参数应该是 size_type 类型的值 ,表达式的 返回值是该位置上字符的引用 。所以最好不用带符号类型,以免为负数。
  3. 如果对无值的位置使用下标运算符([ ]),则结果是未定义的。
  4. 逻辑运算符:与(&&)、或(|)、非(!),返回值为布尔类型。C++规定只有当左侧运算对象为真时,才会检查右侧运算对象。
  5. 可以通过 string 类的成员函数 push_back 向 string 对象尾部添加单个字符。

3.3 标准库类型 vector

  1. 标准库类型 vector 表示对象的集合,其中所有的对象的类型都相同。集合中每个元素都有对应的下标。 通常 vector 也被称为 “容器”(container)
  2. 如果要使用 vector 类型,则要包含同名 头文件 vector
  3. C++ 既有 类模板(class template) ,也有 函数模板 。其中 vector 就是一个类模板。
  4. 模板本身不是类或函数 ,可以将其看作编译器生成的类或函数编写的一份指南。 编译器根据模板创建类或函数的过程通过类创建对象的过程 都被称为 实例化(instantiation)
  5. 当使用模板时,需要指出编译器应把类或函数实例化成什么类型。
    比如 vector :
vector<int> in1;
vector<string> s1;
vector<vector<string>> vecs1;   //元素类型为 vector 对象
  1. 由于引用不是对象,所以 不存在包含引用的 vector
  2. 存在元素类型为 vector 的 vector 对象。
  3. 老式标准是,如果 vector 对象的元素类型是 vector (或其它模板类型),则声明的时候必须在外层 vector 与其元素类型之间添加一个空格。
vector<vector<int> > a;

3.3.1 定义和初始化 vector 对象

  1. 常用的初始化 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......}  //与上条语句等价
  1. 可以通过 vector v1; 的方式默认初始化 vector 对象,从而创建一个指定类型的空 vector 。
  2. vector 类型允许将一个 vector 对象赋值给另一个元素为同类型的 vector 对象。
  3. C++11 提供列表初始化用于 vector 对象的初始化。
  4. 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在用括号里。
  5. 可以通过圆括号来初始化 vector 对象一组指定数量的元素值,第一个参数是指定的个数,第二个参数是元素值。 使用圆括号初始化时不能使用拷贝初始化(不能用 = )。
    正确和错误的例子:
vector<T> vv(n, val);      //正确
vector<T> vvss = (n, val); //错误
  1. 如果 只提供 vector 对象的元素数量 而不进行自定义的初始化, 一般只能用圆括号(C++11起支持花括号{})进行直接初始化(不能用 = )
    但是有 例外 , 如果 花括号 内有初始值,但类型不能用于列表初始化,却可以用于只指定元素数量直接初始化(不可以使用 = )或指定元素值和数量拷贝初始化或直接初始化,此时可以用于构造 vector 对象。全部情况如下比如:
vector<string> s1{"正确"};
vector<string> s2("错误");   //报错
vector<string> s3{10};       //只指定元素数量直接初始化
vector<string> s4{10, "正确,指定元素值和数量直接初始化"};
vector<string> s5 = {10, "正确,指定元素值和数量拷贝初始化"};
  1. 只提供 vector 对象的元素数量而不提供初始值,此时库会创建一个 值初始化(value-initialized) 的元素初值,并把它赋给容器中的所有元素,该值由元素类型决定。 前提是该类型支持默认初始化。
    《C++Primer 第五版》——第三章 字符串、向量和数组_第1张图片

3.3.2 向 vector 对象中添加元素

  1. 当 vector 对象的元素过多时, 先定义一个空的 vector 对象 ,然后通过以某个可控制的表达式为 while 语句的条件以方便结束,每次迭代通过 vector 类的成员函数 push_back(word) 将 word 添加到 vector 对象的结尾。
  2. 要求:如果循环体内部包含有向 vector 对象添加元素的语句,则不能使用范围 for 语句。 之前在 range for 也讲到其不能用于改变序列长度。

3.3.3 其它 vector 操作

  1. 常用的 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 对象的元素数量和对应元素是否有不同
><>=<=     按字典顺序比较
  1. 可以使用范围 for 语句处理 vector 对象的每一个元素。
  2. 能否比较的前提:两个 vector 的元素为同类型。
    同样地, vector 对象的比较也遵循字典顺序
    1. 如果两个 vector 对象的长度不同,而且两个 vector 对象相同位置上的值相同,则较短的 vector 对象小于较长 vector 对象。
    2. 如果两个 vector 对象在某些位置上不同,则 vector 对象比较的结果其实就是 vector 对象中第一对不同值比较的结果(。
  3. vector 对象的下标类型是头文件 vector 里定义的 size_type 类型。
  4. 不能通过下标的形式为 vector 对象添加新元素,vector 和 string 对象的下标运算符只能用于访问已存在的元素。 否则会导致 缓冲区溢出(buffer overflow)

3.4 迭代器介绍

  1. 迭代器(iterator): 可使用迭代器的类型是 标准库容器或 string 类 ,可使用迭代器访问对象中的某个元素,迭代器也能从一个元素移动到另一个元素。迭代器类似指针类型,提供了对对象的间接访问。
  2. 标准库定义了多个容器模板,每个容器都可以使用迭代器,但只有少数几种才同时支持下标运算符。
  3. 迭代器有有效与无效之分 ,有效的迭代器是 指向某个元素或指向容器中尾元素的下一个位置 ,其它情况都为无效。

3.4.1 使用迭代器

  1. 有迭代器的类型,同时拥有返回迭代器的函数成员 。比如 这些类型都有名为 begin 和 end 的成员函数 。begin() 返回指向第一个元素(或字符)的迭代器。 end() 返回指向容器或 string 对象 “尾元素的下一位置(one past the end)” 的迭代器,这个迭代器通常被称为 尾后迭代器(off-the-end iterator)

    如果容器或 string 对象为空,则成员函数 begin 和 end 返回的迭代器是 同一个迭代器

  2. 尾后迭代器指向的是容器或 string 对象的一个不存在的元素,该迭代器只能用作标记,表示处理完了迭代器。
  3. 迭代器支持的一些运算:
*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 对象的尾后迭代器,则相同。
  1. 通过解引用运算符( * )使迭代器获取它所指向元素的值,但是该迭代器必须合法且确实指向某个元素。解引用一个非法迭代器或者尾后迭代器都是未定义的行为。
  2. 迭代器通过递增运算符(++)从一个元素移动到下一个元素。
  3. 因为 尾后迭代器并不指向实际的某个元素 ,所以 不能使用解引用的操作。
  4. 拥有迭代器的标准库类型使用 iteratorconst_iterator 来表示迭代器类型。其中 iterator 类型的迭代器是 可以访问并修改其指向的元素 ,而 const_iterator 类型的迭代器 只能访问而不能修改其指向的元素
  5. 如果容器对象或 string 对象是常量,只能使用 const_iterator ;如果容器对象或 string 对象是不常量,则既能使用 iterator 也能使用 const_iterator 。
  6. 如何定义一个迭代器:
vector<int>::iterator it1;             //定义一个 iterator 类型的迭代器 it1 ,指向 vector 对象
string::iterator it2;                  //定义一个 iterator 类型的迭代器 it2 ,指向 string 对象
auto it3 = a.begin();                  //定义一个 iterator 类型的迭代器 it3 ,指向 a 的第一个元素

vector<int>::const_iterator it4;       //定义一个 const_iterator 类型的迭代器 it4 ,指向 vector 对象
string::const_iterator it5;            //定义一个 const_iterator 类型的迭代器 it5 ,指向 string 对象
  1. begin() 和 end() 返回的迭代器类型由对象是否为常量决定。如果对象是常量,则返回 const_iterator 类型的迭代器,如果对象不是常量,则返回 iterator 类型的迭代器。
  2. C++11 新增两个容器模板和 string 类的成员函数: cbegincend 。它们的作用与 begin 和 end 一样, 但是返回的迭代器类型一定为 const_iterator
  3. 箭头运算符(->) :把 先解引用和再成员访问 两个操作结合起来。
  4. 可能改变 vector 对象长度的操作,比如 push_back ,都会使得之前定义的该 vector 对象的迭代器失效。

3.4.2 迭代器运算

  1. 所有的标准库容器都支持 ++ 。也能用关系运算符,对任意标准库类型的,指向同一个容器对象的两个有效迭代器进行比较。
  2. vector 和 string 的迭代器支持的运算:
iter + n            迭代器加上一个整数的结果是一个迭代器。迭代器指向的元素位置与原来相比向后移动 n 个元素。
iter - n            迭代器减去一个整数的结果是一个迭代器。迭代器指向的元素位置与原来相比向前移动 n 个元素。
iter += n           迭代器加法的复合赋值语句
iter -= n           迭代器减法的复合赋值语句
iter1 - iter2       相减的前提是指向同一个容器对象或 string 对象的元素,或者指向同一个容器对象或 string 对象的尾元素的下一位。两个迭代器相减的结果是它们之间的距离。
<><=>=  迭代器的比较,如果两个迭代器所指向的位置,越靠前的越小。前提是指向同一个容器对象或 string 对象的元素,或者指向同一个容器对象或 string 对象的尾元素的下一位。
  1. 迭代器相减的结果 是一个名为 difference_type 类型的 带符号整数 。容器 vector 和 string 类都定义了 difference_type 。
  2. 使用迭代器运算的一个经典算法是二分法。 中间点一般mid = beg + (end - beg)/2 ,而非 mid = (end + beg)/2
    原因是:
    1. 前者不会产生溢出,而后者可能会。
    2. 前者适用于对迭代器的操作(迭代器相减),而后者(迭代器相加)不行,这个操作未定义,编译报错。

3.5 数组

  1. 数组是一种类似标准库类型 vector 的数据结构,也是存放类型相同的对象的容器。是通过地址访问的。
  2. 数组的大小是不变的。

3.5.1 定义和初始化内置数组

  1. 数组是一种复合类型。数组的声明类似于 T a[d] ,其中 T 是存放的数据类型,a 是数组名,d 是数组的维度,也就是元素个数。 d 必须大于 0 。
  2. 数组的维度必须是常量表达式。
  3. 如果数组存放的数据的类型有定义默认初始化,则按照相应规则执行定义或未定义的默认初始化。
  4. 数组的元素应该为对象,所以 不存在存放数据类型为引用的数组
  5. 不允许使用 auto 来通过初始值推断数组存放的数据类型。
  6. 当对数组初始化时,未被初始化的元素将被初始化为默认初始值。
    数组初始化形式有两种,但都是同一种方式(聚合初始化,数组的聚合初始化对于数组的每一个元素采取了复制初始化的方式):
// 下面两种初始化形式是一样的,都是聚合初始化
int a[10] = {1, 2, 3}; //初始化前三个元素,其它被初始化为 int 的默认初始值
int a[10]  {1, 2, 3};  

由上面的可知, 如果想初始化全部为 0 ,可以使用空的列表初始化:

unsigned scores[11]{};
unsigned scores[11] = {};
  1. 如果没有指明数组长度,编译器会通过初始值的长度推测出来。
    比如:
char ss[] = "12345"; //一共有6个元素,最后一个为空字符'\0'
  1. 对于字符数组,可以使用特殊的初始值(字符串字面值)来初始化。字符数组的长度为字符串的字符个数 + 1,因为字符串字面值的最后一个字符是空字符。
  2. 注意:C++中的字符串字面值的类型不是 string 类型,是最后一个元素为空字符的字符数组 !!!
  3. 不能将数组的内容通过用数组名赋值的方式拷贝给另外的数组。比如 a = b; ,这是错误的。 因为只使用数组名时,通常会转换成指向数组首元素的指针。并且数组名不是左值,不可以改变。
  4. 理解复杂的数组声明 :从数组的名字开始按照从内到外的顺序阅读。
    比如:
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 访问数组元素

  1. 数组元素可以通过下标运算符([ ])、范围 for 语句和解引用运算符( * )来访问。
  2. 使用数组下标时,通常将其定义为 size_t 类型。 size_t 是 一种与机器相关的无符号类型 ,该类型被设计得 足够大以便表示内存中任意对象的大小(数据大小) 。该类型在头文件 cstddef 中定义。
  3. 数组的下标是否在合理范围内有程序员负责检查。
  4. 大多数常见的问题都是源于缓冲区的溢出错误。 当数组或其它类似数据结构的下标越界并试图访问非法内存区域时,就会产生该类错误。

3.5.3 数组与指针

  1. C++中,数组名储存数组首元素的地址。 所以数组名其实就是一个指向数组首元素的指针。
  2. 数组名的类型,其实就是维度+元素类型 ,例如 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] 。
  1. 数组名和&数组名 :设数组名为 array ,那么 array 和 &array 的值实际上是相等的,当然是数值相等,而它们代表的含义则是完全不同的: array 的值是数组首元素的地址, &array 的值是一个指向数组的指针;假设 array 是一个 int[10]类型的数组,那么 array 的类型为 int[10], &array 的类型为 int[10]*,是一个指针
    例如有以下计算结果:
    《C++Primer 第五版》——第三章 字符串、向量和数组_第2张图片

  2. 容器 vector 和 string 类的迭代器支持的运算,数组的指针全都支持。

  3. 尾后指针: 数组尾元素指向的下一个位置,尾后指针不指向具体元素,所以 不能执行解引用或递增操作

  4. C++11新增了关于数组的函数 begin() 和 end() ,与容器模板和 string 类不同的是,它们为 非成员函数返回值分别是首元素和尾元素下一位置的指针(地址) 。它们定义于头文件 iterator

  5. 指针比较: 仅当内置指针指向同一对象的成员或同一数组的元素时,才严格定义指向对象的指针的关系比较(用于小于或大于比较),数组下标越高越大。

  6. 指针相减的前提是两个指针 指向同一类型对象或数组(维度可以不一样) ,相减的结果的类型是一种名为 ptrdiff_t 的标准库类型。与 size_t 一样定义在 cstddef 头文件,因为结果可能为负值,所以它是一种 带符号整型 ,表示两指针之间差了多少个该类型元素。

  7. 虽然指向相同类型不同对象的指针之间可以比较,但是 两个指向不同对象的指针比较一般是没有意义的

  8. 直接比较两个无关指针(即不指向同一对象或数组)将产生未定义的行为

  9. 指针运算使用于 空指针

  10. 解引用和指针运算的交互: a[4] 等价于 *(a+4)

  11. 标准库类型限定使用的下标类型必须是无符号类型(也就是不能为负数) ,而 内置类型的下标不作限制比如数组的下标可以处理负值。

int a[4];
int* p = &a[3];
int d = p[-2];

3.5.4 C风格字符串

  1. 字符串字面值是一种通用结构的实例,这种结构是从C++从C语言继承而来的 C风格字符串(C-style character string)
  2. C风格字符串不是一种类型,而是一种写法——以空字符(‘\0’)结尾的 char 数组。
  3. 关于操作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 
  1. C++中的 字符串字面值const char 数组 ,是 low-level const 。
  2. C风格字符串的比较不能像 string 对象一样使用关系运算符进行比较 ,因为这样实际上比较的是指针而非字符串本身。
const char ca1[] = "hanhan";
const char ca2[] = "asasas";
ca1 < ca2 ; //比较的是地址,没有意义

3.5.5 与旧代码的接口

  1. 混用 string 对象和C风格字符串
    1. 允许使用C风格字符串来初始化 string 对象或为 string 对象赋值;
    2. 标准库允许把字符串字面值和字符字面值转换成 string 对象。当把 string 对象和字符字面值以及字符串字面值混在同一天语句中使用时,必须保证(+)的两个运算对象至少有一个是 string 对象。
    3. string 对象在使用复合赋值运算符时,右侧运算对象可以是C风格字符串。
  2. 不可以用 string 对象来初始化C风格字符串。
  3. 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" 
  1. 不允许使用一个 vector 对象初始化一个数组。
  2. 允许使用一个数组来初始化 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 多维数组

  1. 多维数组其实是数组的数组。
  2. 对于二维数组,常把第一个维度称作行,第二个维度称作列。
  3. 多维数组的初始化:
    比如:
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 内第一个二维数组的引用
  1. 通常会使用两层嵌套的 for 语句来处理二维数组,外层循环行,内层循环列。
  2. 如何 使用嵌套的 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];

你可能感兴趣的:(C++,Primer,读书笔记,c++)