C++ Primer 总结索引 | 第三章:字符串、向量和数组

1、string:可变长度的字符串;vector:可变长度的集合

2、内置类型是由C++语言 直接定义的。这些类型(如数字 和 字符)体现了 计算机硬件本身具备的能力。标准库 定义了另外一组 具有更高级性质的类型,未直接实现到 计算机硬件中

1、命名空间的using声明

1、是一种 访问库中名字 的简单方法

2、库函数 基本上属于命名空间std
如:std::cin表示 从标准输入中 读取内容。作用域操作符::表示 编译器应该从 操作符左侧名字所示的作用域中 寻找 右侧那个名字。因此,std::cin的意思是 要使用 命名空间std中的名字 cin

3、可以直接访问 命名空间中的名字:using namespace::name

4、每个名字都需要 独立的using声明,每个using声明引入 命名空间中的 一个成员

5、在main函数外 直接cin / cout / endl 不可用

#include <iostream>
// 通过下列using声明,可以使用标准库中的名字
using std::cin;
using std::cout; using std::endl;
int main()
{
   
	cout << "Enter two numbers: " << endl;
	int v1, v2;
	cin >> v1 >> v2;
	cout << "The sum of " << v1 << "and" << v2 << " is " << v1 + v2 << endl;
	return 0;
}

既可以 一行只放一条using声明语句,也可以 一行放多条
用到的每个名字 都必须有 自己的声明语句,而且 每句话 都以分号结束

5、头文件不应该 包含using声明,头文件的内容 会拷贝到 所有引用它的文件里。如果头文件里有某个using声明,那么使用了该头文件的文件 都会有这个声明,可能会产生 名字冲突

2、标准库类型string

标准库类型string表示 可变长的 字符序列

2.1 定义 和 初始化string对象

1、如果提供了 一个字符串字面值,则 该字面值中除了 最后那个空字符外 其他所有字符 都被拷贝到新创建的string对象中(string最后会添加空字符,但是不算在size里面)

2、初始化string对象的方式:

string s1  默认初始化,s1是个空串
string s2(s1)  s2是s1的副本
string s2 = s1  等价于s2(s1),s2是s1的副本
string s3("value")  s3是字面值"value"的副本,除了字面值最后的 那个空字符外
string s3 = "value"  等价于s3("value"),s3是字面值"value"的副本
string s4(n, 'c')  把s4初始化为 由连续n个字符c组成的字符串

3、直接初始化 和 拷贝初始化:使用等号初始化 一个变量,实际上执行的是 拷贝初始化,编译器 把等号右侧的初始值 拷贝到 新创建的对象中。如果 不使用等号,则 执行的是 直接初始化
初始值只有一个时,直接初始化 和 拷贝初始化 都行。像s4那样 初始化要用到的值 有多个,只能使用 直接初始化,非要用 拷贝初始化,需要显式地 创建一个(临时)对象 用于拷贝:string s8 = string(10, 'c'); 实际上,用10和c参数 创建出来 string对象,把这个 string对象 拷贝给了s8

2.2 string对象上的操作

语句 操作
os << s 将s写到输出流os中,返回os
is >> s 从is中读取字符串赋给s,字符串以 空白 分隔,返回is
getline(is, s) 从is中读取一行赋给s,返回is
s.empty() s为空 返回true,否则返回false
s.size() 返回s中字符的个数
s[n] 返回s中第n个字符的引用,位置n从0计起
s1 + s2 返回s1和s2连接后的结果
s1 = s2 用s2的副本代替s1中原来的字符
s1 == s2 / s1 != s2 如果s1和s2所含的字符完全一样,则相等;string对象的相等性判断 对字母的大小写敏感
<,<=,>,>= 利用字符在字典中的顺序 比较,且对 字母的大小写 敏感

1、IO操作符 读写string对象:string对象 会自动忽略开头的空白(即空格符、换行符、制表符等)从第一个 真正的字符 开始读起,直到遇见 下一处空白 为止(跟int “>>“输入一样)
如:输入是”(/t) Hello World! (\t)” (开头和结尾 空格),则输出将是"Hello",输出中 没有空格

2、和 内置类型的输入输出 操作一样,string对象的此类操作 返回 运算符左侧的运算对象 作为其结果,因此 多个输入输出 可以连写 在一起(按1所述的原则 分开):

string s1, s2;
cin >> s1 >> s2; // 把第一个输入读入到s1中,第二个输入读入到s2中
cout << s1 << s2 << endl;

输入是"(/t) Hello World (/t)“,输出将是"HelloWorld”

3、读取 未知数量的string对象

while (cin >> word) // 反复读取,直至到达文件结尾
	cout << word << endl;

4、希望最终得到的字符串 保留输入时的 空白符,使用 getline函数 代替原来的>>运算符,来 读取一整行。getline函数的参数是 一个输入流 和 一个string对象,函数从 给定的输入流 中 读入内容,直到 遇到 换行符 为止(注意 换行符 也被读进来了),然后 把所读的内容 存入到那个string对象中 去(注意不存换行符,触发getline函数 返回的那个换行符 被丢弃掉了)
getline只要 一遇到 换行符 就结束读取操作 并返回结果,如果 输入一开始就是 换行符,那么 所得的结果 是个空string

5、和 输入运算符 一样,getline也会 返回它的流参数,所以 也能用getline的结果 作为条件
例:让每次读入一行,输出一行

string line;
while (getline(cin, line))
	cout << line << endl;

使用endl结束当前行 并刷新显示 缓冲区

6、string的empty操作:empty函数 根据string对象 是否为空 返回一个对应的布尔值,empty是string的 一个成员函数,使用 点操作符 指明是 哪个对象 执行了empty函数
例:让5中的程序 只输出非空行

// 遇到空行直接 跳过
string line;
while (getline(cin, line))
	if (!line.empty())
		cout << line << endl;

其中的 逻辑非运算符返回 与其运算对象 相反的结果

7、string的size操作:size函数返回 string对象的长度(即string对象中 字符的个数)line.size()

8、string::size_type类型:size函数返回的是 一种新的类型:size_type
string类 及 其他大多数标准库类型 都定义了 几种配套类型,这些配套类型 体现了 标准库类型与机器无关的特性,size_type就是其中的一种,通过 作用域操作符 来表明名字size_type 是在类string中定义的
size_type类型 是 一个无符号类型的值,且足够放得下 任何string对象的 大小。所有 用于存放string类的size函数返回值 的变量,都应该是 string::size_type类型的

9、C++11允许 编译器通过auto或者decltype 来推断变量的类型:auto len = line.size(); // len的类型是string::size_type

10、由于size函数 返回的是 一个无符号整型数,因此 表达式中不能混用 带符号数 和 无符号数
如:如果n是一个 负值int,则表达式s.size() < n的判断结果 几乎肯定是true,因为 负值n 会自动地转换为 一个比较大的 无符号值
已经有了size()函数 就不要再使用 int了,可以 避免混用int和unsigned可能带来的问题

11、比较string对象:比较字符串的运算符包括:==,!=,<,<=,>,>=,这些比较运算符 逐一比较string对象中的字符,并且对大小写敏感

12、string对象相等 意味着 它们的长度相同 并且 所包含的字符 也完全相同
比较运算符都按照 大小写敏感的 字典顺序:
(1)如果 两个string对象的长度不同,并且 较短string对象的每个字符 都与 较长string对象对应位置上的字符 相同,就说 较短string对象 小于 较长string对象
(2)如果两个string对象 在某些对应的位置上 不一致,则string对象比较的结果 其实是string对象中 第一对相异字符 比较的结果

string str = "Hello";
string phrase = "Hello World";
string slang = "Hiya";

根据(1),对象str 小于 对象phrase
根据(2),对象slang 既大于str 也大于phrase (i(9)> e(5))

13、两个string对象相加:把左侧的运算对象 与 右侧的运算对象 串联而成

string s1 = "hello, ", s2 = "world\n";
string s3 = s1 + s2; // s3的内容是"hello, world\n"

14、字面值 和 string对象 相加:标准库允许把 字符串字面值 和 字符字面值 转换为string对象
注意:把string对象 和 字符字面值 及 字符串字面值 混在一条语句中使用时,必须 确保 每个(注意)加法运算符(+)的两侧的 运算对象 至少有一个是string
如:string s7 = "hello" + ", " + s2; // 错误:不能把字面值 直接相加
其实其工作机理 和 连续输入 连续输出 是一样的,可以按以下 形式 分组:
string s7 = ("hello" + ", ") + s2; (cin >> s1) >> s2; 一样

15、string类的输入运算符和getline函数分别是如何处理空白字符:输入运算符碰到空格、回车和制表符,忽略前面空白;getline碰到回车,不忽略空白

16、比较输入的两个字符串是否等长,如果不等长,输出长度较大的那个字符串

#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
int main()
{
   
    std::string str_in1, str_in2;

    cin >> str_in1 >> str_in2;
    if (str_in1 != str_in2)
    {
   
        if (str_in1 > str_in2)
        {
   
            cout << str_in1 << endl;
        }
        else
        {
   
            cout << str_in2 << endl;
        }
    }
    /*
    * 输入:guyujian gumin(遇到空格/tab/回车就结束)
    * 输出:guyujian
    */
    return 0;
}

2.3 处理string对象中的字符

1、能改变 某个字符的特性:在cctype头文件中 定义了 一组标准库函数 处理

函数 作用
isalnum(c) 当c是字母 或 数字时为真
isalpha(c) 当c是字母时为真
iscntrl(c) 当c是控制字符时为真
isdigit(c) 当c是数字时为真
isgraph(c) 当c不是空格 但可打印时 为真
islower(c) 当c是小写字母时为真
isprint(c) 当c是可打印字符时为真(即c是空格 或 c具有可视形式)
ispunct(c) 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种)
isspace(c) 当c是空白时为真(即c是空格、横向制表符(\t)、纵向制表符(\v)、回车符、换行符、进纸符中的一种)
isupper(c) 当c是大写字母时为真
isxdigit(c) 当c是十六进制数字时为真
tolower(c) 如果c是大写字母,输出对应的小写字母;否则原样输出c
toupper(c) 如果c是小写字母,输出对应的大写字母;否则原样输出c

2、横向制表符(\t)与纵向制表符(\v)
横向制表符:每8个字符 可以看作一个水平制表符,如果遇到\t之前 未满8个字符,则\t就 补空格 直到满8个
纵向制表符:让"\v"后面的字符 从下一行、"\v"前一个字符所在列的后一列 开始输出
如:输入:“01\v2345”
输出:

01
  2345

3、使用c++版本的c标准库头文件:C++标准库中 除了定义C++语言特有功能外,也兼容了C语言的标准库
C++将C语言标准库 去掉了.h后缀,而在文件名name之前 添加了字母c,表示这是一个属于C语言标准库的头文件

cctype头文件 和 ctype.h头文件 的内容是一样的,从命名规范上 更符合C++语言的要求。在名为cname的头文件中定义的名字 从属于命名空间std,而定义在名为.h的头文件中的 则不然

使用cname头文件 标准库中的名字 总能在 命名空间std中找到,如果 使用.h形式的头文件,就要时刻牢记 哪些是从C语言继承来的,哪些C++语言所独有的

4、处理每个字符:使用基于范围的for语句,遍历 给定序列中的每个元素 并对序列中的每个值 执行某个操作

for (declaration : expression)
	statement

expression部分 是一个对象,表示 一个序列。declaration部分 负责定义一个变量,该变量将被 用于访问序列中的 基础元素
每次迭代,declaration部分的变量 都会被初始化为expression部分 的下一个元素值

string str("some string");
// 每行输出str中的一个字符
for (auto c : str) // 对于str中的每个字符执行操作
	cout << c << endl;

c的类型是char,每次迭代,str的下一个字符 被拷贝给c
例:使用范围for语句 和 ispunct函数 来统计string对象中标点符号的个数

string s("Hello World!!!");
// 使punct_cnt的类型 和 s.size()的返回类型一样,string::size_type
decltype(s.size()) punct_cnt = 0;
for (auto c : s) {
    // 对于s中的每个字符
	if (ispunct(c)) // 如果该字符是标点符号
		++punct_cnt;
cout << punct_cnt << " punctuation characters in " << s << endl;

5、使用范围for语句改变字符串的字符:想要改变,必须把 循环变量定义成 引用类型,所谓引用 只是给定对象的 一个别名
例:把字符串改为 大写字母的形式,可以使用 标准库函数toupper,该函数接受 一个字符,然后输出其对应的 大写形式(空格保留)

string s("Hello World!!!");
for (auto &c : s) // 对于s中的每个字符(注意c是引用)
	c = toupper(c); // c是一个 引用,因此赋值语句将 改变s中的字符的值
cout << s << endl;

6、只处理一部分字符:只是访问一个字符,或者 访问多个字符 但遇到某个条件就要停下来
想要访问string对象中的 单个字符 有两种方式:一种是 使用下标,另外一种是 使用迭代器

7、下标运算符([ ]):接受的输入参数是 string::size_type 类型的值,这个参数表示 要访问的字符的位置;返回值是 该位置上字符的引用。string对象 下标必须 大于等于0 小于s.size(),不能越界

8、下标的值:被称作 下标 或 索引,任何表达式 只要它的值 是一个整型值 就能作为索引。如果某个索引是 带符号类型的值 会自动转换为 由string::size_type表达的无符号类型
如:

if (!s.empty())
	cout << s[0] << endl; // 输出s的第一个字符

只要对string对象 使用了下标,就要确认 那个位置上 确实有值;只要 字符串不是常量,就能为 下标运算符 返回的字符 赋新值
如:将字符串的首字符改成大写形式

string s("some string");
if (!s.empty())
	s[0] = toupper(s[0]); // 为s是第一个字符 赋一个新值

9、使用下标执行迭代:把s的 第一个词 改写成大写形式:

// 依次处理s中的字符 直至处理完全部字符 或者 遇到一个空白
for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index)
	s[index] = toupper(s[index]);

输出结果:“SOME string”
使用逻辑与(&&)运算符时,只有左侧运算对象为真 才会检查 右侧运算符情况
保证了 只有当下标取值 在合理范围之内时 才会使用 该下标去访问字符串,只有index在s.size()之前 才会执行s[index]

注意使用下标时 必须确保在 合理范围之内:设下标类型为string::size_type,此类型为无符号数,可以确保下标 不会小于0,代码 只需要保证下标 小于size()的值就行(c++标准不要求标准库检查 下标是否合法,一旦超出范围结果不可预知)

10、使用下标 执行随机访问:通过计算得到下标值,直接获取对应位置的 字符
例:把一串 0-15 之间的 十进制数 转换成 对应的16进制形式,只需要 初始化一个字符串 令其存放16个 十六进制数字

const string hexdigits = "0123456789ABCDEF"; // 可能的十六进制数字,常量,不变了
string res; // 保存结果十六进制字符串
string::size_type n; // 保存输入流读取的数
while (cin >> n)
	if (n < hexdigits.size()) // 忽略无效输入
		result += hexdigits[n]; // 得到对应的十六进制数字
cout << result << endl;

输入:12 0 5
输出:C05

11、编写一段程序,使用范围for语句将字符串内所有字符用X代替

#include <iostream>
#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

int main() {
   
	string str = "a 34"; // 读入看见空格断开 不代表字符串不能有空格
	for (auto &c : str) {
    // 注意使用引用
		c = 'x';
	}
	cout << str << endl;
	return 0;
}

如果将循环控制的变量 从auto改为char 将发生什么
改为char后,在范围for循环中,改变的str中字符c的 副本,因此,str 并不会改变

用while循环重写程序

#include <iostream>
#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

int main() {
   
	string s = "a 34";
	decltype(s.size()) n = 0; // 利用下标循环的递增
	while (s[n] != '\0') {
    // 字符串结尾\0
		s[n] = 'x';
		n++;
	}
	cout << s << endl;
	return 0;
}

12、下面语句合法吗?

const string s = "Keep out!";
for(auto &c : s){
    /* ... */ }

合法,对const char的引用,但是不能改变c
不是说常量不能引用,引用必须绑定到 const对象上

3、标准库类型vector

1、标准库类型vector表示 对象的集合,所有对象的类型 相同。集合中每个对象 都有一个 与之相对的索引,索引 用于访问对象

2、vector容纳着 其他对象,所以被称为 容器

3、使用vector,包含适当的 头文件

#include <vector>
using std::vector;

4、c++既有 类模板 又有 函数模板,vector是一个 类模板

模板本身不是 类或函数,可以 将模板 看作为 编译器生成类或函数 编写的一份说明。编译器 根据模板 创建类或函数的过程 称为实例化,当使用模板时,需要指出 编译器 应该把类或函数 实例化成何种类型

对于类模板 来说,通过提供一些额外信息 来指定模板 到底实例化成 什么样的类,需要提供哪些信息 由模板来决定
提供信息的方式:模板名字后面跟 尖括号,在括号内放上信息
例:vector,提供的额外信息是 vector内所存放 对象的类型:

vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象

编译器 根据模板vector 生成了三种不同的类型:vector<int>、vector<Sales_item>、vector<vector<string>>
vector是模板而非类型,由vector生成的类型 必须包含vector中元素的类型,例如:vector<int>

引用不是对象,不存在 包含引用的vector,其他大多数(非引用)内置类型 和 类类型 都可以构成vector对象,甚至 组成vector的元素 也可以是vector

3.1 定义和初始化vector对象

1、和任何一种类型 一样,vector模板控制着 定义和初始化向量 的办法
初始化vector对象的方法:

代码 作用
vector<T> v1 v1是一个空vector,它潜在的元素是T类型,执行 默认初始化
vector<T> v2(v1) v2中包含有v1所有元素的 副本
vector<T> v2 = v1 等价于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对象的元素 拷贝给 另一个vector对象,新vector对象的元素 就是 原vector对象 对应的副本元素,两个vector对象的类型必须相同:vector<int> ivec; vector<int> ivec2(ivec); // 正确,把元素拷贝 vector<string> svec(ivec2); // 错误:svec的元素是string对象,不是int

2、列表初始化vector对象(c++11):用花括号括起来的 0个或多个初始元素值 被赋给vector对象

vector<string> articles = {
   "a", "an", "the"};

3、大多数情况下上述初始化方法 可以互相等价使用,除了:
(1)使用拷贝初始化时(即使用=时)只能提供一个初始化值
(2)对于类内初始值,只能 使用拷贝初始化 或 使用花括号的形式 初始化
(3)对于初始化元素的 列表,则只能 把初始值都放在 花括号里 进行列表初始化,而不能放在 圆括号里

vector<string> v1{
   "a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误

4、创建指定数量的元素:用vector对象容纳的元素数量 和 所有元素的统一初始值 来初始化vector对象

vector<string> svec(10, "hi"); // 10个string类型的元素,每个都被初始化为"hi"

5、值初始化:只提供vector对象 容纳元素的数量 而不用略去 初始值。库会 创建一个 值初始化的元素初值,把它赋给 容器中的所有元素,初值由vector对象中的元素类型 决定

vector<int> ivec(10); // 10个元素,每个都初始化为0
vector<string> svec(10); // 10个元素,每个都是空的string对象

注意:1、有些类要求必须 明确提供初始值 2、如果只提供 元素的数量 而没有设定初始值,只能使用 直接初始化,不能用 拷贝初始化:vector<int> vi = 10; // 错误

6、列表初始值 还是 元素数量:初始化的真实含义 依赖于传递初始值时 用的是 花括号 还是 圆括号
如果用的是 圆括号,提供的值 用来构造vector对象;如果是 花括号,尽可能当成元素初始值 的列表,无法执行 才考虑 其他初始化方式(用这样的值 构造vector对象)

vector<string> v5("hi"); // 列表初始化:v5有一个元素
vector<string> v6("hi"); // 错误,不能用 字符串字面值 构建vector对象

vector<string> v7{
   10}; // v7有10个默认初始化元素
vector<string> v8{
   10, "hi"}; // v8有10个值为"hi"的元素
vector<string> v9{
   10}; // 包含10个元素,空字符串

只有v5是列表初始化,要想列表初始化 vector对象,花括号里的值 必须与元素类型相同
确认无法执行 列表初始化后,编译器会尝试用默认值 初始化vector对象

3.2 向vector对象中添加元素

1、对vector来说,直接初始化 适用于三种情况:1、初始值已知 且数量较少 2、初始值是另一个vector对象的副本 3、所有元素的初始值都一样

2、更好的初始化方法:先创建一个空vector,然后在运行时 利用vector的成员函数push_back向其中 添加元素。push_back负责 把一个值当成vector对象的尾元素 “压到” vector对象的 “尾端”

在运行时 才知道vector对象中元素的 个数,也应该 使用上述方法 创建vector对象 并为其赋值

// 从标准输入中 读取单词,将其作为vector对象的元素 存储
string word; 
vector<string> text; // 空vector对象
while (cin >> word) {
   
	text.push_back(word); // 把word添加到text后面
}

3、vector对象能高效增长,所以 在定义vector对象的时候 设定其大小 也就没什么必要了,这么做可能性能更差,除非所有元素的值都一样
一旦元素的值 有所不同,更有效的方法 是先定义一个 空的vector对象,再在 运行时 向其中 添加具体值

4、范围for语句体内 不应该改变其 所遍历序列的大小

3.3 其他vector操作

1、vector提供的操作 大多数跟 string相关操作 类似

代码 操作
v.empty() 如果v不含有任何元素,返回真;否则返回假
v.size() 返回v中元素的个数
v.push_back(t) 向v的尾部 添加一个值为t 的元素
v[n] 返回v中的第n个位置上元素 的引用
v1 = v2 用v2中元素的拷贝 替换v1中的元素
v1 = {a, b, c…} 用列表中元素的拷贝 替换v1中的元素
v1 == v2 v1和v2相等 当且仅当它们的元素数量相同 且 对应位置的元素都相同
v1 != v2 不相等
<, <=, >, >= 以字典顺序进行比较

2、访问vector对象中元素的方法 和 访问string对象中字符的方法差不多,通过元素在vector对象中的位置。利用范围for处理vector对象中的 所有元素:

vector<int> v{
   1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto &i : v) // 对于v中的每个元素,注意i是个引用
	i *= i; 
for (auto i : v)
	cout << i << " "; 
cout << endl;

3、size()返回vector对象中元素的个数,返回值的类型 是由vector定义的size_type类型

要使用size_type,首先 指定它是由哪种类型定义的。vector对象的类型 总是包含着元素的类型

vector<int>::size_type // 正确
vector::size_type // 错误

4、各个相等性运算符 和 关系运算符 也与string的相应运算符 功能一致。关系运算符 按照字典序比较,相同位置上元素一致的情况下,元素较少的 小于 元素较多的;若元素值有区别,vector对象的大小关系 是由第一对 相异的元素值的大小关系 决定

只有当 元素值可比较时,vector对象 才能被比较

5、计算vector内对象的索引:只要vector对象 不是一个常量,就能向 下标运算符返回的元素 赋值
例:分数分段统计个数:
计算得到了 分数段索引(score / 10),就能把它作为 vector对象的下标,进而 获取该分数段的计数值 并加1:

// 以10分为一个分数段 统计成绩的数量:0~9,10~19,...,90~99,100
vector<unsigned> scores(11, 0); // 11个分数段,全部初始化为0
unsigned grade;
while (cin >> grade) {
    // 读取成绩
	if (grade <= 100) // 只处理有效成绩
		++scores[grade / 10]; // 将对应分数段的计数值加1,下标范围一定是合法的
}

6、不能用下标形式 添加元素:vector对象(以及string对象)的下标运算符 可用于 访问已存在的元素,而不能用于 添加元素

vector<int> ivec;
for (decltype(ivec.size()) ix = 0; ix != 10; ix++)
	ivec[ix] = ix; // 严重错误:ivec不包含任何元素

正确的方法 是使用push_back

7、用下标形式 去访问 一个不存在的元素 将引发错误,不过 这种错误不会被编译器发现,而是在运行时 产生一个不可预知的值

缓冲区溢出:通过下标访问 不存在的元素
例:下面的程序合法吗?如果不合法,你准备如何修改?

vector<int> ivec;
ivec[0] = 42;

不合法,第2行改为:ivec.push_back(42);

8、从cin读入一组词并把它们存入一个vector对象,然后设法把所有词都改为大写形式。输出改变后的结果,每个词占一行

#include <iostream>
#include <string>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::vector;

int main() {
   
	vector<string> vec;
	string str;
	while (cin >> str) {
   
		vec.push_back(str);
	}
	for (auto &s : vec) {
   
		for (auto& c : s) {
    
	// 两轮for循环,外层vector中的string,内层string中的char,注意都要带引用
			c = toupper(c);
		}
		cout << s << endl;
	}
	return 0;
}

4、迭代器

1、迭代器:除 使用下标运算符 访问string对象的字符 或 vector对象的元素 之外,实现同样的目的 更通用的机制是迭代器

除了vector之外,标准库 还定义了 其他几种容器。所有 标准库容器 都可以使用 迭代器,但只有少数几种 才同时支持下标运算符。string对象不属于 容器类型,但是string支持很多 与容器类型 类似的操作。vector支持 下标运算符,这点跟string一样;string 和 vector也同时支持 迭代器

2、类似于 指针类型,迭代器 也提供了 对对象的间接访问。其对象是 容器中的元素 或者 string对象中的字符。使用迭代器 可以访问某个元素,迭代器 也能从一个元素 移动到 另一个元素

3、迭代器有 有效和无效之分,这一点 和指针差不多。有效的迭代器 或者指向某个元素,或者指向容器中尾元素 的下一个位置;其他情况 都属于无效

4.1 使用迭代器

1、和指针不一样的是,获取迭代器 不是使用 取地址符,有迭代器的类型(v) 同时拥有 返回迭代器的成员(v.begin())。比如:这些类型 都拥有名为begin 和 end的成员,其中begin成员 负责返回指向 第一个元素(或第一个字符)的迭代器

// b表示v的第一个元素,e表示v尾元素的下一个位置
auto b = v.begin(), e = v.end(); // b和e的类型相同

end成员 负责返回 指向容器(或string对象)尾元素的下一个位置,该迭代器指示的是 容器的一个本不存在的 “尾后”元素
这样的迭代器 没什么实际含义,仅是一个标记,表示 已经处理完了 容器中的 所有元素。end成员 返回的迭代器 称为 尾后迭代器 或者简称为 尾迭代器。特殊情况下 如果容器为空,则begin 和 end 返回的是 同一个迭代器

一般来说 不在意 / 清楚 迭代器的准确类型

2、如果 容器为空,则begin和end返回的是 同一个迭代器,都是 尾后迭代器

3、迭代器运算符:如果两个迭代器指向的元素相同 或 都是同一个容器的尾后迭代器,则他们相等;和指针相似,通过解引用迭代器来获取它所指示的元素

代码 作用
*iter 返回迭代器iter所指元素 的引用
iter->mem 解引用iter并获得该元素 名为mem的成员,等价于(*iter).mem
++iter 令iter指示容器中的下一个元素
–iter 令iter指示容器中的上一个元素
iter1 == iter2 判断两个迭代器是否相等
iter1 != iter2 不相等

4、利用 下标运算符 把string对象的第一个字母改写为 大写形式

string s("some thing");
if (s.begin() != s.end()) {
    // 要先检查:确保s非空
	auto it = s.begin();
	*it = toupper(*it); // 使用当前所指元素的 引用,把当前字符改成 大写形式
}

在if内部,声明了一个迭代器变量it并把begin返回的结果 赋给它,这样就得到了指示s中 第一个字符的迭代器接下来通过 解引用运算符 将第一个字符更改为 大写形式

5、将迭代器 从一个元素移动到另外一个元素:迭代器使用 递增运算符 来从一个元素 移动到 另一个元素,迭代器的递增 就是将迭代器“向前移动一个位置”

end返回的迭代器 并不实际指向某个实际元素,不能对其进行 递增 或 解引用操作
例:利用迭代器及其 自增运算符 将string对象中第一个单词 改写为大写形式

// 依次处理s的字符 直至 处理完全字符 或者遇到空白
for (auto it = s.begin(); it != s.end() && !isspace(*it); it++)
	*it = toupper(*it); // 将当前字符 改成 大写形式

6、泛型编程:C++习惯性使用 !=,更愿意使用 迭代器 而非下标 是因为:这种编程风格 在标准库 提供的所有容器上 都有效
所有 标准库容器的迭代器 都定义了 == 和 !=,但是它们中的大多数没有定义<运算符。养成 迭代器和!=的习惯,就不用在意 用的到底是 哪种容器类型

7、迭代器类型:就像不知道string和vector的size_type成员 到底是什么类型一样,也不知道 迭代器的精确类型。那些拥有 迭代器的标准库类型 使用 iterator 和 const_iterator 来表示 迭代器的类型

vector<int>::iterator it; // it能读写vector<int>的元素
string::iterator it2; // it2能读写string对象中的字符

vector<int>::const_iterator it3; // it3 只能读元素,不能写元素
string::const_iterator it4; // it4 只能读字符,不能写字符

const_iterator和常量指针 差不多,能读取 但不能修改它的元素值

8、begin 和 end运算符:begin和end返回的 具体类型 是由对象是否是常量决定,如果 对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator

vector<int> v;
const vector<int> cv; 
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator

为了专门得到const_iterator类型的返回值,C++11引入了两个新函数,分别是cbegin,cend:

auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator

类似于begin和end,上述两个新函数也分别返回 指示容器第一个元素 或 最后一个元素下一个位置的迭代器。有所不同的是,不论 vector对象 或 string对象 本身是否是常量,返回值都是const_iterator

9、结合 解引用和成员访问 操作:解引用迭代器 可获得迭代器所指对象,如果该对象的类型恰好是类,就可以进一步 访问它的成员
例:检查是否为空,令it是该 vector对象的迭代器:(*it).empty()
注意,*it外面的圆括号不能丢,如果 不加圆括号,点运算符将由it来执行
先对it解引用,然后解引用的结果 再执行点运算符

10、为了简化 9例 中的运算符,定义了 箭头运算符(->):把 解引用 和 成员访问 两个操作结合在一起,it->mem(*it).mem等价
例:利用迭代器遍历text,直到遇到 空字符串 或 结尾为止

for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
	cout << *it << endl;

11、某些对vector对象的操作 会使迭代器失效:虽然vector对象 可以动态的增长
(1)不能在范围for中 向vector对象添加元素
(2)任何一种 可能改变vector对象容器的操作,比如push_back,都会使 该vector对象的迭代器失效
但凡 使用了迭代器的循环体,都不要 向迭代器所属的容器 添加元素

12、迭代器运算:迭代器的递增运算 令迭代器每次移动 一个元素,所有的 标准库容器 都有支持 递增运算的迭代器,也能用==和!=对 任意标准库类型的 两个有效迭代器

string和vector的迭代器 提供了更多额外的运算符,一方面可以使得 迭代器的每次移动 跨过多个元素,另外 也支持迭代器 进行关系运算,这些运算被称作 迭代器运算

vector和string迭代器支持的运算:

代码 作用
iter + n 迭代器 加上一个整数 仍得一个迭代器,迭代器 指示的新位置 与原来比向前移动了若干元素 结果迭代器或 指示容器内的一个元素,或指示容器尾元素的下一个位置
iter - n 减去,向后移动
iter1 += n 复合赋值语句,将iter1加n的结果赋给iter1
iter1 -= n
iter1 - iter2 两个迭代器相减的结果 是他们之间的距离,将运算符右侧的迭代器 向前移动差值个元素 将得到左侧的迭代器,参与运算的两个迭代器 必须指向的是 同一个容器中的元素 或者 尾元素的下一个位置
>、>=、<、<= 迭代器的关系运算符,如果 某迭代器指向的容器位置 在另一个迭代器所指的位置 之前,则说前者 小于 后者。参与运算的两个迭代器 必须指的是 同一个元素中的元素 或者 尾元素的下一个位置

例:
相加相减:auto mid = vi.begin() + vi.size() / 2;
如果vi有20个元素,mid等于vi.begin() + 10,已知下标从0开始,迭代器所指元素是vi[10]

iter1 - iter2所得结果是 两个迭代器的距离,其类型是 名为difference_type的 带符号整数,string和vector都定义了difference_type

13、使用迭代器运算的例子
(1)利用迭代器 完成二分搜索

// beg和end表示搜索范围,前闭后开
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg) / 2; // 中间点
// 当还有元素尚未检查 并且 还没找到sought时执行循环
while (mid != end && *mid != sought) {
   
	if (sought < *mid)
		end = mid;
	else 
		beg = mid + 1;
	mid = beg + (end - beg) / 2;
}

为什么用的是 mid = beg + (end - beg) / 2, 而非 mid = (beg + end) / 2
迭代器相加不成立,指针加指针无意义

(2)用迭代器 重新实现 划分分数段程序

#include <iostream>
#include <vector>

using std::cout; using std::cin; using std::endl;
using std::vector;

int main()
{
   
	vector<int> vec;
	int tmp;
	while (cin >> tmp) {
   
		vec.push_back(tmp);
	}
	vector<int> res(11, 0);
	for (auto iter = vec.begin(); iter != vec.end(); iter++) {
   
		(*(res.begin() + (*iter / 10)))++; // 最外面那个括号不能少,不然会先执行++再执行*
	}
	for (auto iter = res.begin(); iter != res.end(); iter++) {
   
		cout << *iter << " ";
	}
	return 0;
}

5、数组

1、数组是一种类似于 标准库类型vector 的数据结构

与vector 相似的地方是:数组也是 存放类型相同的对象的容器,这些对象 本身没有名字,需要 通过其所在位置 访问

与vector 不同的地方是:数组的大小 确定不变,不能 随意向数组中 添加元素。因为 数组的大小固定,因此 对特殊的应用 来说程序运行时性能较好,损失灵活性

5.1 定义 和 初始化内置数组

1、数组的声明:a[d] 其中a是数组的名字,d是数组的维度
之前初始化的时候 类型总是在变量名的左边,数组元素的类型在左边,大小在方括号内 放在变量名的右边的

维度说明了 数组中元素的个数,必须大于0,数组中元素个数 是 数组类型的一部分,编译时 维度应该是已知的,维度必须是一个常量表达式

unsigned cnt = 42; // 不是常量表达式
string bad[cnt]; // 错误

constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 正确
int *parr[10]; // 含有42个整型指针的数组

string strs[get_size()]; // 当get_size是constexpr时正确;否则错误

2、和内置类型变量一样(string, vector<int>就不是内置类型变量,所以不会),在函数内部 定义了某种内置类型的数组,默认初始化 会令数组含有 未定义的值

3、定义数组的时候 必须指定 数组的类型,不允许用 auto关键字 由初始列表推断类型

4、和vector一样,数组的元素 应该为对象,不存在引用的数组

5、显式 初始化数组:
1)对数组进行 列表初始化,允许忽略 数组的维度,如果 声明时没有指明,编译器根据 初始值的数量推测
2)如果指明了维度,初始值总数量 不应该超出指定的大小。如果维度 比提供的初始值 数量大,用提供的初始值 初始化 靠前的元素,剩下的元素 初始化为默认值(string是空字符,int是0)

const unsigned sz = 3;
int ial[sz] = {
   0, 1, 2}; // 3个元素,值为0,1,2

int a2[] = {
   0, 1, 2}; // 维度是3的数组
int a3[5] = {
   0, 1, 2}; // 等价于a3[] = {0, 1, 2, 0, 0}
string a4[3] = {
   "hi", "bye"}; // 等价于a4[] = {"hi", "bye", ""}

int a5[2] = {
   0, 1, 2}; // 错误

数组初始化默认值:
1)当数组在全局变量区定义时,默认初值为0。
2)当数组在局部变量区定义时,如果不写“ {} ”,则默认的初值为乱码
3)当数组在局部变量区定义时,如果写“ {} ”或给某几位赋值时,则其他未赋值的数默认初值为0

6、字符数组的特殊性:字符数组除列表初始化外 有特殊的初始化形式,用字符串字面值初始化,且 自动添加 表示字符串结束的 空字符(字符串字面值自带的结尾处的 空字符 也会像字符串的其他字符一样 被拷贝到 字符数组中)而列表初始化不会

char a1[] = {
   'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {
   'C', '+', '+', '\0'}; // 列表初始化,有显式的空字符

char a3[] = "C++"; // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // 错误:没有空间可存放空字符,6个字符,数组的大小 至少为7

7、不允许拷贝 和 赋值:不能将数组的内容 拷贝给其他数组作为 其初始值,也不能用 数组 为其他数组赋值

int a[] = {
   0, 1, 2}; 
int a2[] = a; // 错误:不允许使用一个数组 初始化 另一个数组
a2 = a; // 错误:不能把一个数组 直接赋值给 另一个数组

8、复杂的数组声明(引用,指针与括号)
定义存放指针的数组 比较直接,定义 数组的指针和引用 要用括号

int *ptrs[10]; // ptrs是含有10个整数指针的数组
int &refs[10] = /* */; // 错误:不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组

int (*Parray)[10]:圆括号括起来的部分,*Parray意味着Parray是个指针,右边 可知Parray是个指向大小为10的数组的指针,左边 可知数组中的元素是int

9、对修饰符的数量没有限制

int *(&array)[10] = ptrs; // arry是数组的引用,该数组含有十个指针

理解数组声明的含义:由内而外读语句,括号内array是个引用,右边 知道array引用的对象 是一个大小为10的数组,左边 知道数组的元素类型是指向int的指针

10、下列数组中元素的值

string sa[10];
int ia[10];
int main() {
   
    string sa2[10];
    int ia2[10];
}

sa:空字符串;
ia:0;

sa2:空字符串;
ia2:不确定值

5.2 访问数组元素

1、使用数组下标时,定义为size_t类型
size_t:是一种机器相关的类型,被设计的足够大 以便能表示内存中 任意对象的大小,在cstddef头文件中 定义了size_t类型

2、记录各分数段成绩的个数

// 以10分为一个分数段统计成绩的数量
unsigned scores[11] = {
   }; //全部初始化为0
unsigned grade; 
while (cin >> grade) {
   
	if (grade <= 100)
		++scores[grade / 10]; // 当前分数段的计数值+1
}

与vector实现不同:scores的声明;这里使用的下标运算符 由C++直接定义的,这个运算符能用在 数组对象 上,vector里的下标运算符 是库模板vector定义的,只能用于vector类的运算对象

3、范围for语句的使用:与vector / string一样,需要遍历数组所有元素时 使用

for (auto i : scores)
	cout << i << " "; 

4、定义一个含有10个int的数组,令每个元素的值就是其下标值:for(size_t i = 0;i<10;i++) ia[i] = i;
刚刚创建的数组拷贝给另一数组:不能整体直接拷贝,只能一个个元素拷贝:

int ia2[10];
for(size_t i = 0;i<10;++i) 
	ia2[i] = ia[i];

5.3 指针和数组

1、一些情况下 数组的操作 实际上是 指针的操作
1)使用数组名字的地方 编译器一般会 把它替换为 一个指向数组首元素的指针:string *p = nums;
对数组的元素 使用 取地址符 就能得到指向该元素的指针:string *p = &nums[0]; // 跟上面的代码 等价

2)

int ia[] = {
   0, 1, 2 ,3, 4};
auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素

使用ia作初始值时,编译器实际执行的初始化过程 类似于:auto ia2(&ia[0]); // 显然ia2的类型时int*
当使用decltype关键字时 上述转换不会发生,decltype(ia)返回的类型 是由4个整数构成的数组:decltye(ia) ia3 = {0, 1, 2, 3};

2、指针也是迭代器:指向数组元素的指针 有更多功能。vector和string的迭代器 支持的运算,数组的指针 全都支持
例:允许 使用递增运算符 将指向数组元素的指针 向前移动到 下一个位置上

int arr[] = {
   0, 1, 2, 3};
int *p = arr; // p指向arr的第一个元素
++p; // p指向arr[1]

像使用迭代器 遍历vector对象中的元素一样,使用 指针 也能遍历 数组中的元素。前提是 先获得 指向数组第一个元素的指针 和 指向数组尾元素的下一位置的指针
可以获取数组元素之后 那个不存在的元素地址:使用下标运算索引 一个不存在的元素

int *e = &arr[4]; // 指向arr尾元素的下一个位置的指针

就像尾后迭代器一样,尾后指针 也不指向 具体的元素,不能对尾后指针 执行解引用 或 递增的操作
利用上面的尾后指针写循环,输出所有arr元素

for (int *b = arr; b != e; b++)
	cout << *b << endl;

3、标准库函数begin 和 end:C++11引入,跟容器中的两个同名 成员函数 功能一致。使用形式 是将数组(不是 指向数组第一个元素的指针) 作为它们的参数,这两个函数定义在iterator头文件中

int ia[] = {
   0,1,2,3};
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向arr尾数组的下一位置的指针

使用begin和end两个函数 易于写出一个循环 处理数组中的元素

int *pbeg = begin(arr), *pend = end(arr);
while (pbeg != pend && *pbeg >= 0) // pbeg != pend确保可以安全地对pbeg解引用
	++pbeg; 

4、指针运算:指向数组元素的指针 可以执行 所有之前提过的迭代器运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针 和 用在迭代器上意义完全一致

给一个指针 加上 / 减去 某整数值,结果仍是指针。给指针加上一个整数,得到的新指针 仍需指向同一数组的其他元素 或者 同一数组尾元素的下一个位置

和迭代器一样,两个指针相减的结果是它们之间的距离auto n = end(arr) - begin(arr); 结果的类型是 ptrdiff_t 标准库类型,和 size_t 一样,ptrdiff_t 也是一种定义在 cstddef 头文件中的机器相关的类型。因为差值可能为负,所以 ptrdiff_t 是一种带符号类型

两个指针指向 同一个数组的元素,或者 指向该数组的尾元素的下一个位置,就能利用 关系运算符 对其进行比较;两个指针 分别指向不相关的对象,则 不能比较它们
指针的比较运算 也适用于空指针 和 所指对象不是数组的指针。如果p是空指针,允许给p加上或减去一个值为 0 的整型常量表达式,两个空指针也可以 相减,结果当然是0;对于所指对象不是数组的指针,两个指针必须指向 同一个对象 或 该对象的下一个位置

5、解引用 和 指针运算的交互

int ia[] = {
   0, 2, 4, 6, 8};
int last = *(ia + 4); // 正确,把last初始化成ia[4]的值

表达式 含有解引用运算符 和 点运算符,最好在必要的地方 加上圆括号

last = *ia + 4; // 正确,last = ia[0] + 4

6、下标 与 指针:对数组 执行下标运算 其实是对 指向元素的指针 执行下标运算

int i = ia[2];  // ia换成指向数组首元素的指针,ia[2]得到(ia + 2)所指的元素

int *p = ia; // p指向ia的元素
i = *(p + 2); // 等价于i = ia[2]

只要指针指向的是 数组中的元素(或者 数组中尾元素的下一个位置),都可以执行下标运算

int *p = &ia[2]; // p指向索引为2的元素
int j = p[1]; // p[1]等价于*(p + 1),就是ia[3]表示的那个元素
int k = p[-2]; // p[-2]是ia[0]表示的那个元素

标准库类型 与 内置类型 下标运算的区别:标准库类型 如string和vector限定使用的下标必须是 无符号类型,而 内置的下标运算可以为负值

7、p1 += p2 - p1; == p1 += (p2 - p1)

8、利用指针将数组中的元素置为0

#include <iostream>
#include <iterator>

using std::endl;
using std::cout;
using std::end;

int main() {
   
	int a[5] = {
   };
	for (size_t i = 0; i < 5; i++) a[i] = i;
	for (int aa : a) {
   
		cout << aa << " ";
	}
	cout << endl;
	for (int *p = a; p != end(a); p++) {
    
	// 注意end()定义在iterator头文件中,使用时别忘了using std::end;在标准空间中
		*p = 1;
	}
	for (int aa : a) {
   
		cout << aa << " ";
	}
	return 0;
}

9、编写一段程序,比较两个数组是否相等。再写一段程序,比较两个vector对象是否相等(vector直接有 运算符== 比较)
begin / end函数的参数是数组,如果直接作为参数传入就是指针了,而且进行引用 进行值传递时 别忘了数组的类型包含 数组的大小,数组的大小是必须要包含的

begin / end作为形参 要传入 有两种方法
1)迂回传指针

#include <iostream>
#include <vector>
#include <iterator>

using std::cout;
using std::endl;
using std::vector;
using std::end;
using std::begin;

bool compare_int(int *ia1_begin,int *ia1_end,int *ia2_begin,int *ia2_end)
{
   
    cout << ia1_end - ia1_begin << " " << ia2_end - ia2_begin <<endl;
    if((ia1_end - ia1_begin) == (ia2_end - ia2_begin))
    {
   
        for(int *i = ia1_begin,*j = ia2_begin;i != ia1_end,j != ia2_end;++i,++j)
        {
   
            if(*i != *j)
            {
   
                return false;
            }
        }
        return true;
    }else
    {
   
        return false;
    }
}

int main()
{
   

    int ia1[10] = {
   1,2,3};
    int ia2[9] = {
   1,2,3};

    if(compare_int(begin(ia1),end(ia1),begin(ia2),end(ia2)))
    {
   
        cout << "equal" << endl;
    }else
    {
   
        cout << "not equal" << endl;

    }

    vector<int> iv1{
   1,2,3,4};
    vector<int> iv2{
   1,2,3};
    if(iv1 == iv2)
    {
   
        cout << "equal" << endl;
    }else
    {
   
        cout << "not equal" << endl;
    }

    return 0;
}

2)传数组的引用

// https://zhuanlan.zhihu.com/p/438555365
#include <iostream>
#include <vector>
#include <iterator>

using std::cout;
using std::endl;
using std::vector;
using std::end;
using std::begin;

bool compare_int(int(&ia1)[10], int(&ia2)[9]) // 数组类型不光带元素类型,还要带元素个数
{
   
    if (end(ia1) - begin(ia1) == end(ia2) - begin(ia2)) // end / begin的参数就是单纯 数组,不自动转化为指向数组第一个元素的指针
    {
   
        for (int* i = begin(ia1), *j = begin(ia2); i != end(ia1), j != end(ia2); ++i, ++j)
        {
   
            if (*i != *j)
            {
   
                return false;
            }
        }
        return true;
    }
    else
    {
   
        return false;
    }
}

int main()
{
   

    int ia1[10] = {
    1,2,3 };
    int ia2[9] = {
    1,2,3 };

    if (compare_int(ia1, ia2))
    {
   
        cout << "equal" << endl;
    }
    else
    {
   
        cout << "not equal" << endl;

    }
}

5.4 C风格字符串

1、尽管C++支持 C风格字符串,但在C++程序中 最好不要使用

2、字符串字面值 是一种通用结构的实例,这种结构 是C++由C继承而来的C风格字符串。C风格字符串不是一种类型,而是为了表达和使用 字符串而形成的 约定俗成的写法。按照这个习惯书写的字符串 存放在字符串数组中 并以空字符结束,即 在字符串最后一个字符后面 跟着一个空字符(‘\0’)

3、C标准库String函数(C风格字符串的函数):定义在cstring头文件(C语言头文件string.h的C++版本)中

代码 作用
strlen§ 返回p的长度,空字符不计算在内
strcmp(p1, p2) 比较p1和p2的相等性。如果p1==p2,返回0;如果p1>p2,返回正值;如果p1<p2,返回负值
strcat(p1, p2) 将p1附加到p2之后(p1最后的空格去了),返回p1
strcpy(p1, p2) 将p2拷贝给p1,返回p1

传入此类函数的指针 必须指向 以空字符作为结束的数组

char ca[] = {
   'C', '+', '+'}; // 不以空字符结束
cout << strlen(ca) << endl; // 严重错误:ca没有以空字符结束

strlen函数 将有可能沿着ca在内存中的位置 不断向前寻找,直到遇到 空字符 才停下来

4、C风格字符串 与 标准库string对象操作区别
4.1、C风格字符串 与 标准库string对象操作区别:比较函数
比较标准库string对象的时候,用的是普通的关系运算符 和 相等性运算符

string s1 = "A string";
string s2 = "A different";
if (s1 < s2) // false, d < s

这些运算符用在 两个C风格字符串上,实际上 比较的是 指针而非字符串本身

const char ca1[] = "A string";
const char ca2[] = "A different"; 
if (ca1 < ca2) // 未定义的;试图比较两个无关地址

当使用数组名字的时候 其实真正使用的是 指向数组首元素的指针,上面的if条件 实际上比较的是 两个const char*的值
想要比较 两个C风格字符串 需要调用strcmp函数

4.2、C风格字符串 与 标准库string对象操作区别:连接或拷贝字符串
两个string对象的连接:string largeStr = s1 + "," + s2;
同样的操作放到两个C风格字符串:cal1 cal2 这两个数组就产生错误,cal1 + cal2试图将 两个指针 相加,是非法的

为什么两个指针相加没有意义:指针相加实际为地址相加

正确的方法是使用 strcat函数 和 strcpy函数,还必须提供一个 用于存放结果字符串的数组,该数组 必须足够大 以便容纳下 结果字符串 及 末尾的空字符

// largeStr大小不够 引发严重错误,largeStr内容 一旦改变,必须 重新检查其空间是否足够
strcpy(largeStr, ca1);
strcpy(largeStr, ",");
strcpy(largeStr, ca2);

5、程序的输出结果是什么

const char ca[] = {
    'h', 'e', 'l', 'l', 'o' };
const char *cp = ca;
while (*cp) {
   
    cout << *cp << endl;
    ++cp;
}

没有’\0’,循环可能不会停止,将会打印出垃圾信息(存在风险)

6、编写一段程序,比较两个string对象。再编写一段程序,比较两个C风格字符串的内容
char aa[] = { 'a','z', '\0'}; // 要自己加,不然一直aa大(一路读下去了) char bb[] = "az"; // 自带终止符

#include <iostream>
#include <string>
#include <cstring>

using std::endl;
using std::cout;
using std::string;

int main()
{
   
	string a = "abb";
	string b = "az";
	if (a == b) {
   
		cout << "a is equal to b" << endl;
	}
	else if (a > b){
   
		cout << "a is bigger than b" << endl;
	}
	else {
   
		cout << "a is smaller than b" << endl;
	}

	char aa[] = {
    'a','z', '\0'}; // 要自己加,不然一直aa大(一路读下去了)
	char bb[] = "az"; // 自带终止符
	if (strcmp(aa, bb) == 0) {
   
		cout << "aa is equal to bb" << endl;
	}
	else if (strcmp(aa, bb) > 0) {
   
		cout << "aa is bigger than bb" << endl;
	}
	else {
   
		cout << "aa is smaller than bb" << endl;
	}
	return 0;
}

7、定义两个字符数组并用 字符串字面值 初始化它们;接着再定义一个字符数组 存放前面两个数组连接后的结果。使用 strcpy 和 strcat 把前两个数组的内容 拷贝到第三个数组当中

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstring>


using std::cout;
using std::endl;
using std::string;

int main()
{
   
    char ca1[] = "aaa";
    char ca2[] = "abc";
    char ca3[7]; // 刚刚好7个

    strcpy(ca3, ca1);
    strcat(ca3, ca2);

    cout << ca3 << endl;

    return 0;
}

3.5 与旧代码接口

1、混用string对象 和 C风格字符串
之前介绍过可以使用 字符串字面值 来初始化string对象 string s("Hello World");

更一般的情况:任何出现字符串字面值的地方 都可以用 以空字符结束的字符数组 来替代
1)允许 使用以空字符结束的字符数组 来初始化string对象 或 为string对象赋值
2)在string对象的加法运算中 允许使用 以空字符结束的字符数组 作为其中的 一个运算对象(不能 两个运算对象都是);在string对象的复合赋值运算中 允许使用 以空字符结束的字符数组 作为右侧的运算对象

以上性质反过来不成立:如果程序某处 需要C风格字符串,无法直接使用string对象 代替它。例如:不能用string对象 直接初始化 指向字符的指针 char *str = s; // 错误

为了用string对象 初始化 指向字符的指针,使用c_str函数:const char *str = s.c_str();
c_str函数的返回值 是一个C风格的字符串,即 函数的返回结果是一个指针,该指针 指向一个以空字符结束的 字符数组,该指针的类型是 const char*(避免更改字符数组的内容),而这个被指向的数组所存数据 恰好与 string对象一样

无法保证c_str 函数返回的数组 一直有效,如果后续的操作 改变了s的值 就可能让之前返回的数组 失去效用
所以执行完c_str()函数后 程序想一直 都能使用其返回的数组,最好 将该数组 重新拷贝一份

2、使用数组初始化vector对象:
不允许 使用一个数组为 另一个内置类型的数组 赋初值
不允许 使用vector对象 初始化数组

允许 使用数组 初始化 vector对象
只要指明 要拷贝区域的首元素地址 和 尾后地址

int int_arr[] = {
   0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));

用于 创建ivec的两个指针 实际上指明了用来 初始化的值在int_arr中的位置
用于 初始化vector对象的值 也可能仅是 数组的一部分

// 拷贝三个元素:int_arr[1]、int_arr[2]、int_arr[3]
vector<int> subVec(int_arr + 1, int_arr + 4);

尽量使用标准库类型而非数组

3、汇总
用整型数组初始化一个vector对象

int ia[] = {
   1,2,3};
vector<int> iv(begin(ia),end(ia));

将含有整数元素的vector对象拷贝给一个整型数组

vector<int> iv(3,2);
int ia[3];
for(int i = 0;i < 3;++i) ia[i] = iv[i];

3.6 多维数组

1、C++没有多维数组,多维数组其实是 数组的数组
当一个数组的元素 仍是数组时,使用两个维度定义:一个维度表示 数组本身大小,另一个维度 表示其元素(也是数组)的大小。对于二维数组来说,第一个维度 称为行,第二个维度 称为列

2、多维数组的初始化:使用花括号括起来的 一组值 初始化多维数组

int ia[3][4] = {
    // 三个元素,每个元素都是大小为4的数组
	{
   0, 1, 2, 3}, // 第一行的初始值
	{
   4, 5, 6, 7}, // 第二行的初始值
	{
   8, 9, 10, 11} // 第三行的初始值
};

内层嵌套着的花括号非必须,等价语句:int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

跟一维数组一样,初始化时 不需要把所有元素都包含在初始化列表之内,其他未列出的元素 执行相同的默认值初始化,这种情况下就不能省略 内层的花括号

// 显式的初始化 每行的首元素
int ia[3][4] = {
   {
   0}, {
   4}, {
   8}};
// 显式的初始化第一行,其他元素执行默认初始化,其他元素被初始化为0
int ix[3][4] = {
   0, 3, 6, 9};

3、多维数组的下标引用:数组的每个维度 对应一个下标运算符。表达式含有的下标运算符数量 比 数组的维度小,则表达式的结果 将是一个给定索引 处的内层数组

// 用arr的首元素 为ia最后一行的最后一个元素 赋值
ia[2][3] = arr[0][0][0];
// 把row绑定到 ia的第二个4元素数组上
int (&row)[4] = ia[1];

ia[2][3]:ia[2]得到数组ia的最后一行,此时返回的是 表示ia最后一行的 那个一维数组;对这个一维数组 再取下标,得到编号为3的元素

使用两层嵌套for循环 处理多维数组的元素,将元素索引 作为它的值

constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt]; // 12个未初始化的元素
// 每一行
for (size_t i = 0; i != rowCnt; i++) {
   
	// 对于行内的每一列
	for (size_t j = 0; j != colCnt; j++) {
   
		ia[i][j] = i * colCnt + j;
	}
}

4、范围for语句 处理多维数组:C++11新增范围for语句
同上面的两层嵌套for循环,也是将元素索引 作为它的值

size_t cnt = 0;
for (auto &row : ia) // 对于外层数组的每一个元素
	for (auto &col : row) {
    // 对于内层数组的每一个元素
		col = cnt; // 将下一个值赋给该元素
		++cnt;
	}

跟上面两层嵌套for循环 不同的是:把管理数组索引的任务 交给了系统来完成。要改变元素的值,就需要 把控制变量row和col声明成 引用类型。第一个for循环 遍历ia的所有元素,所以 row的类型是 含有4个整数的数组的引用。第二个for循环 遍历那些4元素数组中的某一个,所以 col的类型是 整数的引用

还有一个深层次的原因 促使这么做:

for (const auto &row : ia) // 对于外层循环的 每一个元素
	for (auto col : row) // 对于内层循环的 每一个元素
		cout << col << endl;

循环中 没有任何写操作,还是必须把 外层循环的控制变量 声明成了引用类型,这是 为了避免数组 被自动转成指针
假设不用引用类型

for (auto row : ia)
	for (auto col : row)

程序无法通过编译
第一个循环 遍历ia所有元素,这些元素为大小为4的数组。因为row不是 引用类型,所以编译器初始化row时 会自动将这些数组形式的元素(和其他类型的数组 一样)转换成 指向该数组内 首元素的指针
这样得到的 row 类型就是 int*,内层循环就不合法了,编译器 将试图在 一个int*内遍历

要使用范围for语句 处理多维数组,除了 最内层的循环外,其他所有循环的控制变量 都应该是 引用类型

5、指针和多维数组:程序 使用多维数组的名字时,自动将其 转换成 指向数组首元素的指针
因为多维数组实际上是 数组的数组,由 多维数组名 转换得来的指针 实际上是指向第一个内层数组的指针

int ia[3][4]; // 大小为3的数组,每个元素含有 4个整数的数组
int (*p)[4] = ia; // p指向含有4个整数的数组 的指针
p = &ia[2]; // p指向ia的尾元素

通过使用auto 或 decltype就能尽可能 避免在数组前面 加上一个指针类型
注意p,q 都是指针,但指向不同

// 输出ia中 每个元素的值
// p指向 含有4个整数的数组
for (auto p = ia; p != ia + 3; p++)
	// q指向 4个整数数组内的元素,即q指向 一个整数
	for (auto q = *p; q != *p + 4; q++)
		cout << *q << ' ';

当然也可以通过 标准库函数begin 和 end实现,p和q指向的东西 一致,但内层for循环里面的*p含义不一样,上面代码是 指向数组内元素的指针,而下面代码就是 解引用 数组

// p指向 含有4个整数的数组
for (auto p = begin(ia); p != end(ia); p++)
	// q指向 4个整数数组内的元素,即q指向 一个整数
	for (auto q = begin(*p); q != end(*p); q++)
		cout << *q << ' ';

6、类型别名 简化 多维数组的指针:将4个整数组成的数组 命名为 int_array

using int_array = int[4]; // 新标准下类型别名的声明
typedef int int_array[4]; // 直观理解就是typedef int[4] int_array,与上面的一句等价

for (int_array *p = ia; p != ia + 3; p++) {
   
	for (int *q = *p; q != *p + 4; q++) 
		cout << *q << ' ';
	cout << endl;
}

7、编写3个不同版本的程序,令其均能输出ia的元素。版本1 使用 范围for语句 管理迭代过程;版本2 和 版本3 都使用 普通for语句,其中版本2要求使用 下标运算符,版本3要求使用 指针

#include <iostream>
#include <iterator>

using std::cout;
using std::cin;
using std::endl;
using std::end;
using std::begin;

int main()
{
   
	int ia[3][4] = {
   
		{
   0,1,2,3},
		{
   4,5,6,7},
		{
   8,9,10,11}
	};

	for (int(&i)[4] : ia) {
    // 注意类型是int &[4] 不是[3]
		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++) {
    
		// begin / end的参数就是单纯一个数组,数组名不转化为指向第一个元素的指针
		for (int *j = begin(*i); j != end(*i); j++) {
   
			cout << *j << " ";
		}
		cout << endl;
	}
	return 0;
}

6、小结与术语解释

1、小结:数组 和 指向数组元素的指针 在一个较低的层次上 实现了与标准库类型 string 和 vector 类似的功能。一般优先选用 标准库提供的类型

2、C风格字符串:以空字符结束的 字符数组。字符串字面值 是C风格字符串,C风格字符串容易出错

3、difference_type:由string和vector定义的一种 带符号整数类型,表示两个迭代器之间的距离

4、prtdiff_t:是cstddef头文件中定义的 一种与机器实现有关的 带符号整型类型,它的空间足够大,能够表示 数组中任意两个指针之间的距离

5、size:是string和vector的成员,分别返回字符的数量 或 元素的数量。返回值的类型是 size_type
size_t:是cstddef头文件中定义的 一种与机器实现有关的 无符号整数类型,它的空间足够大,能够表示任意数组的大小

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-01-10 22:12:01       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-10 22:12:01       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-10 22:12:01       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-10 22:12:01       18 阅读

热门阅读

  1. mysql常见问题

    2024-01-10 22:12:01       34 阅读
  2. 向爬虫而生---Redis 拓宽篇3 <GEO模块>

    2024-01-10 22:12:01       33 阅读
  3. Qt基础-QtGlobal常用的全局函数及随机数产生实例

    2024-01-10 22:12:01       35 阅读
  4. 学习记录685@获取第三方文件后转存入自己服务器

    2024-01-10 22:12:01       36 阅读
  5. vue3利用自定义事件和v-model实现父子传参

    2024-01-10 22:12:01       37 阅读
  6. PAT (Basic Level)|1004成绩排名 c++满分题解

    2024-01-10 22:12:01       32 阅读
  7. flask flask-sqlalchemy sqlit3

    2024-01-10 22:12:01       32 阅读
  8. Linux kernel 学习笔记

    2024-01-10 22:12:01       46 阅读