第 3 章知识点总结
-
- [3.1 .1 命名空间的using 声明](#3.1 .1 命名空间的using 声明)
- [3.2.1 定义和初始化 string 对象](#3.2.1 定义和初始化 string 对象)
- [3.2.2 string 对象上的操作](#3.2.2 string 对象上的操作)
- [3.2.3处理 string 对象中的字符](#3.2.3处理 string 对象中的字符)
- [3.3.0 标准库类型 vector](#3.3.0 标准库类型 vector)
- [3.4.0 迭代器](#3.4.0 迭代器)
- [3.5.1 定义和初始化数组](#3.5.1 定义和初始化数组)
- [3.5.3 指针和数组](#3.5.3 指针和数组)
- [3.6.0 多维数组](#3.6.0 多维数组)
3.1 .1 命名空间的using 声明
提醒
头文件不应包含 using 声明,因为头文件的内容会拷贝到所有引用它的文件夹中,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
3.2.1 定义和初始化 string 对象
c
string s1 // 默认初始化,s1是空串
string s2(s1) // s2 是 s1 的副本
string s3("value") // s3 是字面值"value"的副本,除了字面值最后的那个空字符外
string s3 = "value" // 等价于 s3("value")
string s4(n, 'c') // 把 s4 初始化为由连续 n 个字符c组成的串
直接初始化和拷贝初始化
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization) ,编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)
当初始值只有一个时,使用直接初始化或拷贝初始化都可以。如果初始化要用到的值有多个,一般来说只能使用直接初始化的发生
c
string s3 = "value"; // 拷贝初始化
string s4(n, 'c'); // 直接初始化,把 s4 初始化为由连续 n 个字符c组成的串
string s8 = string(10, 'c'); // 拷贝初始化
// 等价于
string temp(10, 'c');
string s8 = temp;
3.2.2 string 对象上的操作
提醒
由于 size 函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数可能产生意想不到的结果。例如,假设 n 是一个具有负值的 int,则表达式 s.size() < n 的判断结果几乎是 true。这是因为负值 n 会自动转化成一个比较大的无符号值。
因此如果一个表达式中已经有了 size() 函数就不要再使用 int 了,这样可以避免混用 int 和 unsigned 可能带来的问题。
比较 string 对象( vector 也一样 )
① 如果两个 string 对象的长度不同,而且较短 string 对象的每个字符都与较长 string 对象对应位置上的字符相同,就说较短 string 对象小于较长 string 对象。
② 如果两个 string 对象在某些对应的位置上不一致,则 string 对象比较的结果其实是 string 对象中第一队相异字符比较的结果。
c
string str = "Hello";
string phrase = "Hello World";
string slang = "Hiya";
// 比较结果
slang > phrase > str
字面值和 string 对象相加
当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+) 的两侧的运算对象至少有一个是 string 对象
c
string s1 = "hello", s2 = "world";
string s4 = s1 + ", " ; // 正确
string s5 = "hello" + ", "; // 错误:两个都不是 string 对象
string s6 = s1 + ", " + "world"; // 正确:每个加法运算符都有一个运算对象是 string
string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加
// string 加法的工作机理和连续输入连续输出是一样的(即从左到右),所以:
s6 = (s1 + ", ") + "world"; // 括号里可以相加,加完仍为 string对象,可继续加
s7 = ("hello" + ", ") + s2; // 括号内加不了
提醒
C++ 语言中的字符串字面值并不是标准库类型 string 的对象。切记,字符串字面值与string 是不同的类型
3.2.3处理 string 对象中的字符
建议
使用 C++ 版本的 C 标准库头文件,C 语言的标准库形如 name.h ,C++ 则将这些文件命为 cname。也就是去掉了 .h 的后缀,而在文件名之前添加了字母 c。使用使用 C++ 版本的 C 标准库头文件可统一。
处理每个字符?使用基于范围的 for 语句
如果相对 string 对象中每个字符做点儿什么,目前最好的办法是使用 范围 for(range for)语句。 这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作。
c
// 范围 for 语句语法形式
for (declaration : expression) {
statement
}
// 其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration 部分的变量会被初始化为 expression 部分的下一个元素值。
// eg 逐行输出 string 对象中的字符
string str("some string");
for (auto c: str){
cout << c <<endl;
}
使用范围 for 改变字符串中的字符
如果想要改变 string 对象中字符的值,必须把循环变量定义成引用类型。记住,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。
c
// 将字符串改写为大写
string s("Hello World!!!");
for (auto &c : s) {
c = toupper(c); // c 是一个引用,因此赋值语句将改变 s 中字符的值
}
cout << s <<endl;
只处理一部分字符?
① 使用下标
② 使用迭代器
题目
c
// 以下程序合法,输出 \0;
string s;
cout << s[0] <<endl;
3.3.0 标准库类型 vector
C++ 语言既有类模板(class template),也有函数模板,其中 vector 是一个类模板。
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。
c
vector<int> ivec;
vector<vector<string>> file;
注意
vector 对象可以动态的增长,但是有一些副作用:
① 不能在范围 for 循环中向 vector 对象添加元素。
② 任何一种可能改变 vector 对象容量的操作,比如 push_back,都会使该 vector 对象的迭代器失效。
3.4.0 迭代器
建议
如果对象只需读操作而无须写操作的话最好使用常量类型(比如const_iterator)。为了便于专门得到const_iterator类型的返回值,可用 cbegin() 和 cend() 函数
c
auto it3 = v.cbegin();
解引用和成员访问操作
c
// 检查对象是否为空?
(*it).empty() // 正确,解引用it,再调用empty()函数判断
*it.empty() // 错误:会先试图访问 it 的 empty 的成员,再解引用。而 it 是迭代器没有empty 成员
// 为了简化上述表达式,可以用 箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起:
it->empty() == (*it).empty()
注意
在迭代器的算术运算中,要保证迭代器一直有效(不能小于begin 也不能大于 end,可以等于end)。如用迭代器进行二分法处理时有:
c
// 获取中间值,beg 、end 和 min 都是迭代器
mid = beg + (end - beg) / 2;
// 而不能用 直接相加除以2,这样迭代器相加不成立,指针加指针无意义:
min = (beg + end) / 2; // 错误
3.5.1 定义和初始化数组
特点
① 全局数组,未初始化时,默认值都是 0;(多维数组一样)
② 局部数组,未初始化时,默认值为随机的不确定的值;
③ 局部数组,初始化一部分时,未初始化的部分默认值为 0;
④ 定义数组时必须指定数组的类型,不允许用 auto 关键字由初始值的列表推断类型;
⑤ 和 vector 一样,数组的元素应为对象,不存在引用的数组;
⑥ 当用字符串字面值初始化数组时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去;
⑦ 不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值(vector可以)
c
const char a4[6] = "Danile"; // 错误:没有空间可以存放空字符
理解复杂的数组声明
c
// 读取方法:从(括号)里到外,从右往左
int *ptrs[10]; // ptrs 是含有 10 个整型指针的数组
int &refs[10] = /* ? */; // 错误:不存在引用的数组
int (*Parray)[10] = &arr; // Parray 为指针 指向一个含有 10 个整数的数组
int (&arrRef)[10] = arr; // arrRef 引用一个含有 10 个整数的数组
int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有 10 个指针
3.5.3 指针和数组
数组特性
在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。
c
string nums[] = {"one","two","three"};
string *p2 = nums; // 等价于 *p2 = &nums[0]
在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。所以当使用数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组:
c
int ia[] = {0,1,2,3,4,5,6,7,8,9};
auto ia2(ia); // ia2 是一个整型指针,指向 ia 的第一个元素
ia2 = 42; // 错误:ia2 是一个指针,不能用 int 值给指针赋值
//对于语句2 ,编译器实际执行的初始化过程类似下面这种形式:
auto ia2(&ia[0]); // 显然 ia2 的类型是 int*
必须指出的是,当使用 decltype 关键字时上述转换不会发生,decltype(ia) 返回的类型是由 10 个整数构成的数组:
c
// ia3 是一个含有 10 个整数的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确:把 i 的值赋给 ia3 的一个元素
指针也是迭代器
指针和迭代器一样,可以获取尾元素之后的那个不存在的元素地址:
c
int *e = &ia[10];
// 等价于
int *e = end(ia);
指针运算
只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一个位置,就能利用关系运算符对其进行比较。例如可用如下方式遍历数组中的元素:
c
int *b = ia, int *e = ia + 10;
while (b < e) {
++b;
}
// 注意:如果两个指针分别指向不相关的对象,则不能比较它们
必须说明的是,上述指针运算同样适用于空指针和所指对象并非数组的指针。在后一种情况下,两个指针必须指向同一个对象或该对象的下一个位置。如果 p 是空指针,允许给 p 加上或减去一个值为 0 的整型变量表达式。两个指针也允许彼此相减,结果当然是 0 。
解引用和指针运算的交互
c
int ia[] = {0,2,4,6,8};
int last = *(ia + 4); // 正确:last = ia[4] = 8
last = *ia + 4; // 正确:last = ia[0] + 4 = 4
下标和指针
虽然标准库类型 string 和 vector 也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而数组内置的下标运算无此要求,可以为负数。当然结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。
c
int *p = &ia[2]; // p 指向索引为 2 的元素
int j = p[1]; // 等价于 *(p + 1),就是 ia[3] 表示的那个元素
int k = p[-2]; // k = ia[0]
3.6.0 多维数组
严格来说,C++ 语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用多维数组有大益处:
c
// 读法:从里到外,从左边往右读[]
int ia[3][4]; // 大小为 3 的数组,每个元素是含有 4 个整数的数组
// 大小为 10 的数组,它的每个元素都是大小为 20 的数组
// 这些数组的元素是含有 30 个整数的数组
int arr[10][20][30] = {0};
使用范围 for 语句处理多维数组
c
// 遍历二维数组ia[3][4]
size_t cnt = 0;
for (auto &row : ia) { // 注意,此 & 不可省略,即使循环中没有如何读写操作
for (auto &col : row) { // 当无读写操作时,此 & 可以省略
/* */
}
}
注意,对第一个for 语句一定得将控制变量声明成引用类型,这是为了避免数组被自动转成指针。第一个循环语句是遍历 ia 的所有元素,注意这些元素实际上是大小为 4 的数组。若 row 不是引用类型,编译器初始化 row 时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的 row 的类型就是 *int,显然内层的循环就不合法了。
指针和多维数组
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
c
int ia[3][4];
int (*p)[4] = ia; // p 为指针,指向含有 4 个整数的数组
p = &ia[2]; // p 指向 ia 的尾元素
// 注意语句2 中的圆括号必不可少
int *p[4]; // 整型指针的数组
通过使用 auto 或者 decltype 就能尽可能地避免在数组面前加上一个指针类型了:
c
// 输出 ia 中每个元素的值,每个内层数组各占一行
// p 指向含有 4 个整数的数组
for (auto p = begin(ia); p != end(ia); ++p ) {
// q 指向内层数组的首元素
for (auto q = begin(*p); q != end(*p); ++q){
cout<< *q << ' ';
}
cout<< endl;
}
题目
编写3个不同版本的程序,令其均能输出ia的元素。版本1使用范围for语句管理迭代过程;版本2和版本3都使用普通for语句,其中版本2要求使用下标运算符,版本3要求使用指针。此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto关键字和decltype关键字。
c
#include <iostream>
#include <cstddef>
#include <iterator>
using std::cout;
using std::endl;
using std::begin;
using std::end;
int main()
{
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
for(const int (&i)[4]:ia)
for(int j:i)
cout << j << " ";
cout << endl;
for(size_t i = 0;i < 3;++i)
for(size_t j = 0;j < 4;++j)
cout << ia[i][j] << " ";
cout << endl;
for(int (*i)[4] = begin(ia);i != end(ia);i++)
for(int *j = begin(*i);j != end(*i);j++)
cout << *j << " ";
cout << endl;
return 0;
}
改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。
c
#include <iostream>
#include <cstddef>
#include <iterator>
using std::cout;
using std::endl;
using std::begin;
using std::end;
int main()
{
typedef int int_array[4]; // int_array 是 int [4] 的别名
// using int_array = int[4];
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
for(const int_array &i:ia) // 等价于 const int (&i)[4]:ia
for(int j:i)
cout << j << " ";
cout << endl;
for(size_t i = 0;i < 3;++i)
for(size_t j = 0;j < 4;++j)
cout << ia[i][j] << " ";
cout << endl;
for(int_array *i = begin(ia);i != end(ia);i++)
for(int *j = begin(*i);j != end(*i);j++)
cout << *j << " ";
cout << endl;
return 0;
}
再一次改写程序,这次使用auto关键字。
c
#include <iostream>
#include <cstddef>
#include <iterator>
using std::cout;
using std::endl;
using std::begin;
using std::end;
int main()
{
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
for(const auto &i:ia)
for(int j:i)
cout << j << " ";
cout << endl;
for(size_t i = 0;i < 3;++i)
for(size_t j = 0;j < 4;++j)
cout << ia[i][j] << " ";
cout << endl;
for(auto *i = begin(ia);i != end(ia);i++)
for(auto j = begin(*i);j != end(*i);j++)
cout << *j << " ";
cout << endl;
return 0;
}