C++第三章:字符串、向量和数组

一、命名空间的using声明

  • using声明:using声明后,无需使用专门的前缀(形如命名空间::)也能使用所需的名字了。
  • 第一种:using namespace 命名空间名 ;
#include <iostream>
using namespace std;
int main()
{
	int i = 10;
	cout << i << endl;
	return 0;
}
  • using直接声明整个命名空间,我们可以在程序中直接使用命名空间中的名字而无需前缀。如上述代码using了C++标准库的命名空间std,因此在程序中使用cout、cin和endl等名字时可以直接使用了。
  • 第二种:using 命名空间::名字 ;
#include <iostream>
using std::cout;
using std::endl;
int main()
{
	int i = 10;
	cout << i << endl;
	return 0;
}

每个名字独立using声明

  • using直接声明整个命名空间引入了很多名字,这违背了我们使用命名空间的初衷:我们应使名字的作用域范围尽可能小。因此对于上文代码,如果我们常用cout和endl,那么分别单独声明它们即可。
  • 注意:每一个名字必须单独声明在一条语句中。如下代码是错误示例:
using std::cout, std::cin; // 错误:cout和cin应当分别单独using声明

头文件不应包含using声明

  • 头文件一般不应该使用using声明,因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

二、标准库类型string

  • 标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。因此如果你需要使用string类型,可在代码中添加:
#include <string>	// 必须
using std::string;	// 非必须

2.1 定义和初始化string对象

  • 如何初始化类的对象是由类本身决定的。一个类可以定义很多种初始化对象的方式,只不过这些方式之间必须有所区别:或者是初始值的数量不同,或者是初始值的类型不同。
  • 初始化string对象最常用的方法如下:
初始化格式初始化结果
string s1 ;空字符串
string s2(st) ;st的值(st为字符串)
string s3 = st ;st的值(st为字符串)
string s4(“value”) ;value
string s5 = “value” ;value
string s6(n, ‘c’) ;n个c
  • 注意字符串字面值"value"严格来说其字符串最后还有一个’\0’表示字符串的结束,当将字符串字面值赋值给string对象时它会抛弃’\0’。
  • 示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s1;			 // 默认初始化,s1为空字符串
	string s2(s1);		 // 使用s1初始化s2,s2字符串内容等于s1
	string s3 = s1;		 // 将s1的字符串内容拷贝到s3中
	string s4("value");	 // 使用字符串字面值"value"初始化s3
	string s5 = "value"; // 将"value"字面值拷贝到s4中初始化s4
	int n = 5;			
	string s6(n, 'c');	 // 调用string类定义的初始化方法
	std::cout << "s1:" << s1 << std::endl;
	std::cout << "s2:" << s2 << std::endl;
	std::cout << "s3:" << s3 << std::endl;
	std::cout << "s4:" << s4 << std::endl;
	std::cout << "s5:" << s5 << std::endl;
	std::cout << "s6:" << s6 << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

直接初始化和拷贝初始化

  • 使用"=“进行的初始化就叫拷贝初始化,不使用”="进行的初始化就是直接初始化。
  • 拷贝初始化:编译器把等号右侧的初始值拷贝到新创建的对象中去。
  • 对于单个初始值就可以进行初始化的情况,直接使用"="进行拷贝初始化可以完成,使用直接初始化也可以完成。但是对于有多个值,比如要初始化一个拥有99个c的字符串序列,我们还是使用直接初始化更方便。(ps:手打99个也没问题)

2.2 string对象上的操作

  • 一个类既要定义初始化其对象的方式,也要定义对象上能执行的操作。对象能执行的操作包括:通过函数名调用的操作、通过"+ - * / <<"等各种运算符操作。
  • 重点:类可以定义<<、+等各种运算符在该类对象上的新含义。
  • 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对象

  • 使用ID操作符读写string对象。示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s;
	std::cin >> s;
	std::cout << "s:" << s << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意:使用cin读入string对象时,string对象会自动忽略开头的空白(空格、换行符等),并从第一个真正的字符开始读起,直到遇到下一处空白为止。

读取未知数量的string对象

  • 程序如下:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s;
	while (std::cin >> s)
		std::cout << s << std::endl;
	return 0;
}
  • 上述程序在读取文件时,当读取到文件的末尾时会遇到文件结束标记,最终终止循环结束文件的读入。
  • 模拟读取一个文件中的字符串,最后打印出读取的所有字符串。实验代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	int n = 0;
	string s[100];
	while (std::cin >> s[n])
		n++;
	int i = 0;
	while (i < n)
	{
		std::cout << "s[" << i << "]:" << s[i] << std::endl;
		i++;
	}
	return 0;
}
  • 实验结果:
    在这里插入图片描述
  • 注意文件结束符(ctrl+z)需要另起一行输入,否则无效无法将cin的状态变为无效,因此也无法终止循环。(原因未知)

使用getline读取一整行

  • 使用cin直接读取时我们无法读取到空白字符,因为cin跳过了所有空白字符。如果我们需要读取一整行,无论它里面是否有空白字符,那么可以使用getline函数。
  • getline函数遇到换行符时会结束读取操作并返回结果,如果输入为空行那么赋值后得到的是个空string。
  • 示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string line;
	while (std::getline(std::cin, line))
	{
		std::cout << line << std::endl;
	}
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意getline函数返回的那个换行符实际上被丢掉了,得到的string对象不包含换行符。我们使用endl手动换行并且刷新显示缓冲区。

string的empty和size操作

  • 使用empty可以判断读入的行是否为空行。示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string line;
	while (std::getline(std::cin, line))
	{
		if (!line.empty())// 判断此次读取的文件行是否为空
			std::cout << "line:" << line << std::endl;
		else
			std::cout << "line is empty" << std::endl;
	}
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意:箭头所指位置直接回车输入了一个空行。
  • 使用size可以判断读入的行中字符的长度。示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string line;
	while (std::getline(std::cin, line))
	{
		if (line.size() >= 10)	// 判断此次读取的文件行长度
			std::cout << "line:" << line << std::endl;
		else
			std::cout << "line very short" << std::endl;
	}
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 以上对两种函数的应用重点是告诉我们要学会通过使用string类的函数,来辨别和使用读取到的文件信息。
  • "!"为逻辑非运算符。

string::size_type类型

  • 长话短说:string类的函数size返回的类型为string::size_type,关键点在于它是一个无符号整形数。无符号整形数和有符号整型数进行运算很可能会出现意想不到的结果。
  • 比如对于运算语句:
	s.size() < n ;
  • 当n为一个int类型的负值时,这个表达式的判断结果几乎肯定是true。因为负数会自动转换成一个比较大的无符号值。
  • 因此,如果一条表达式里有string类型的size函数,就不要使用其他有符号类型了,例如int。

字面值和string对象相加

  • 即使一种类型并非所需,我们也可以使用它,前提是该种类型可以自动转换为所需的类型。
  • 标准库允许把字符字面值和字符串字面值转换成string对象,因此在任何需要string对象的地方我们都可以使用字符字面值和字符串字面值。示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s = "abc";	
	s += "123";	// 使用字符串字面值代替string对象与s进行+=复合赋值运算
	s += 'Q';	// 使用字符字面值代替string对象与s进行+=复合赋值运算
	std::cout << s << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意:"+=“是复合赋值运算符。string类型对象定义了”+“运算符,string类型对象和string类型对象进行”+“运算时会进行字符串的拼接操作。而string对象”+“字符串字面值,是根据C++标准,通过把字符串字面值或字符字面值自动转换为的string对象,再将string对象和string对象进行”+“运算完成的。因此在包含string和字符或字符串字面值的表达式中,每一个加法运算符即”+"号两侧的运算对象至少有一个得是string。示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s1 = "abc" + "def";		// 错误,字符串字面值之间无法进行"+"运算
	string s2 = "abc" + s1 + "def"; // 正确
	return 0;
}
  • 因为某些历史原因,也为了和C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记字符串字面值与string是不同类型。

2.3 处理string对象中的字符

cctype头文件

  • 如果要判断string中每个字符的类型,可以使用cctype头文件。
  • C++标准库兼容了C语言的标准库。C++将C语言的name.h头文件命名为cname,这里的c表示它是一个属于C语言标准库的头文件。
  • 头文件cctype和ctype.h中的内容相同。我们应尽可能使用cname头文件,因为在cname头文件中定义的名字从属于命名空间std,而定义在.h头文件中的名字不是。

基于范围的for语句

  • 使用基于范围的for语句访问string对象中的每一个字符:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s = "123456";
	// 基础元素类型:序列对象
	for (char ch:s)
	{
		std::cout << ch;
	}
	std::cout << std::endl;

	// 基础元素类型:序列对象
	int a[6] = { 1,2,3,4,5,6 };
	for (int i : a)
	{
		std::cout << i << " ";
	}
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • string序列的基本元素类型是char,当我们不知道基本元素是什么类型时,我们可以使用auto类型说明符。
  • 当我们想要处理一个序列时,我们需要获取的应该是序列中每个基本元素的引用,因此需要写为:基本类型&。示例代码:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s = "ABCDEFG";
	// 基础元素类型:序列对象
	for (char& ch : s)
	{
		// 将大写字母转换为小写字母(大小写字母的ASII码差32,小写字母ASII码值更大)
		ch += 32;
	}
	// 查看修改后的s
	std::cout << s << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

下标运算符

  • 下标运算符 " [] "接收的输入参数是string::size_type类型的值,它是一个无符号整数类型,因此只能表达大于或等于0的值。
  • 任何表达式只要它的值是一个整形值就能作为索引,如果某个索引是带符号的值那么会自动转换为string::size_type类型。(因此如果索引为负会转换为一个很大的值)
  • 下标的值称作 “下标” 或 “索引” ,下标的取值范围应该是0到size()-1,如果超出范围进行访问会引发不可预知的结果。因此使用下标运算符是需要检测下标是否合法,如果非法会产生不可预知的后果。
  • 范围for循环会遍历序列中的每一个基本元素,如果我们只想处理部分元素,那么可以结合下标使用普通for循环实现:
#include <iostream>
#include <string>
using std::string;
int main()
{
	string s = "ABCDEFG";
	// 下标结合普通for循环遍历序列一半的元素(整数/2截取,7/2=3.5存储为整数3)
	for (int i = 0; i < s.size() / 2; i++)
	{
		// 将大写字母转换为小写字母(大小写字母的ASII码差32,小写字母ASII码值更大)
		s[i] += 32;
	}
	std::cout << s << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

三、标准库类型vector

  • 标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中每个对象都有一个与之对应的索引,索引用于访问对象。因为vector “容纳着” 其他对象,所以它也常被称作容器。
  • 要想使用vector,可以添加如下代码:
#include <vector>	// 必须包含对应头文件
using std::vector;	// 非必须使用using声明vector,以便省略前缀vector::
  • C++语言既有类模板、函数模板。注意模板本身不是类或函数,相反可以把模板看作编译器生成类或函数的一份说明。
  • vector是模板而非数据类型,由vector生成的类型必须包含vector中元素的类型。
  • 编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
  • 对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式是固定的:在模板名字后跟一对尖括号,在括号内放上信息。
  • 以vector模板为例,提供额外信息创建类的过程如下所示:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;

int main()
{
	// 根据模板vector生成不同的类型
	vector<int> int_arr;			// 生成vector<int>类型 
	vector<string> str_arr;			// 生成vector<string>类型
	vector<vector<string>> file;	// 生成vector< vector<string> >类型
	return 0;
}
  • 上述代码分别以int、string、vector< string > 类型指定vector中存放对象的类型,并以此来声明了三个变量:int_arr、str_arr和file。三个变量分别对应的类型是vector< int >、vector< string >和vector< vector< string > >。int_arr表示存储int类型对象的一个集合,str_arr和file同理。
  • vector能容纳绝大多数类型的对象作为其元素,但因为引用不是对象,因此不存在包含引用的vector。
  • 大多数(非引用)内置类型和类类型都可以构成vector对象,甚至组成vector的元素也可以是vector。在早期版本的C++标准中,如果vector的元素还是vector(或者其他模板类型),则必须在外层vector对象的右尖括号和其元素之间添加一个空格,例如:
vector<vector<string> > file;	
  • 某些编译器可能仍需老师的声明语句来处理上一点提到的情况,如果报错请在右侧添加一个空格。

3.1 定义和初始化vector对象

  • vector中的元素可以依据索引来获取,因此它们是有序的,我们可以把一个vector看作是一组顺序排列元素所构成的向量。
  • vector虽然是模板类型而非类类型,但是vector模板控制着定义和初始化向量(元素)的方法。
初始化方法说明
vector v1 ;定义一个空vector,元素为T类型,执行默认初始化
vector v2(v1) ;v2中包含v1所有元素的副本
vector v3(v1) ;等价于 v2(v1)
vector v4(n, val) ;v4中包含了n个值为val的T类型对象
vector v5(n) ;v5中包含了n个执行了值初始化的T类型对象
vector v6{a,b,c…} ;v6中包含了a,b,c…等对象
vector v7 = {a,b,c…} ;等价于v6
  • 可以在声明vector时指定所有元素的初始值,也可以在程序运行时高效地往vector对象中添加元素。事实上最常见的就是先创建一个空vector,然后运行时获取到元素的值再逐一添加。
  • 可以把一个vector对象赋值给另一个vector对象,这样被赋值的vector中所有元素的值和赋值的vector一样。示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;

int main()
{
	vector<int> int_arr1{ 1,2,3,4,5,6,7 };
	vector<int> int_arr2 = int_arr1;	// 把int_arr1赋值给int_arr2

	int_arr1 = {2,3,4,5,6,7,8};			// 修改int_arr1
	
	std::cout << "int_arr1:";			// 分别查看int_arr1和int_arr2中元素值
	for (int i = 0; i < 7; i++)			
		std::cout << int_arr1[i] << " ";
	
	std::cout << std::endl << "int_arr2:";
	for (int i = 0; i < 7; i++)
		std::cout << int_arr2[i] << " ";
	std::cout << std::endl;

	return 0;
}
  • 运行结果:
    在这里插入图片描述

列表初始化和值初始化

  • 列表初始化方法很简单,在变量名后跟一对花括号,括号内依次写出每个元素的值即可。注意花括号内每个值的类型都必须和T的类型相同,或者能自动转换成T的类型。示例代码:
	// 列表中每个值的类型都为int
	vector<int> int_arr{ 1,2,3,4,5,6,7 };
	// 列表中每个值(字符串字面值)都可以自动转换为string类型
	vector<string> str_arr{"abc","defg","hijk"}; 
  • 值初始化是初始化时只提供了元素的数量而省略了初始值。这时库会创建一个值初始化的元素初值,并把它赋值给容器中每个元素。这个初值由vector对象中元素的类型决定,如果元素的类型是内置类型,比如int则元素初始值自动设为0。如果元素是某种类类型,比如string,则元素由类默认初始化。
  • 注意:有一些类没有提供默认初始化的方法,这时不仅需要提供元素的数量,也要提供元素的初始值。示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;

int main()
{
	vector<int> int_arr(10);	// 只提供数量进行值初始化
	for (int i : int_arr)		// 查看值初始化后元素的值
	{
		std::cout << i << " ";
	}
	std::cout << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

特殊的列表初始化

  • 当列表中的值与T即元素类型不相符时,编译器会确认无法执行列表初始化,则编译器会尝试使用默认值初始化vector对象。示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;

int main()
{
	// 10不为且无法转换为string类型,编译器尝试解读为:str_arr(10,"abc")
	vector<string> str_arr{10,"abc"};	
	for (string i : str_arr)		// 查看元素的值
	{
		std::cout << i << " ";
	}
	std::cout << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

3.2 向vector对象中添加元素

push_back函数

  • 使用vector的成员函数push_back向其中添加元素。push_back函数把一个值当作vector对象的尾元素,把元素添加到vector对象的尾部。示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;

int main()
{
	vector<int> int_arr;
	int i;
	while (std::cin >> i)
	{
		// 根据输入动态添加元素到int_arr的末尾
		int_arr.push_back(i);
	}
	std::cout << "int_arr:";
	for (int a : int_arr)
		std::cout << a << " ";
	std::cout << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意动态向vector添加元素的性能不一定比在创建vector时就指定其容量慢。C++标准要求vector应该能在运行时高效快速的添加元素,因此没必要在定义时设置vector对象的大小,这样的性能可能更差。
  • 注意在范围for循环中不应改变遍历序列的大小。因此如果for中需要使用push_back则不应该使用范围for循环。(push_bak是在vector末尾添加一个元素的位置,一定会使得vector对象包含元素的数量增加)
  • 示例代码,向原本有元素的vector对象添加元素:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;

int main()
{
	vector<int> int_arr{1,2,3,4};	// 原本包含1 2 3 4
	int i;
	while (std::cin >> i)
	{
		// 根据输入动态添加元素到int_arr的末尾
		int_arr.push_back(i);
	}
	std::cout << "int_arr:";	// 查看int_arr中包含的元素
	for (int a : int_arr)
		std::cout << a << " ";
	std::cout << std::endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

3.3 其他vector操作

  • vector支持的操作:
操作含义
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 == v2v1和v2相等当且仅当它们的元素数量相同并且对应位置的元素值相同
v1 != v2
<,<=,>,>=按照字典顺序进行比较
  • 实验代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	vector<int> int_arr1;
	cout << "int_arr1 is empty:" << int_arr1.empty() << endl;
	int_arr1 = { 1,2,3,4,5,6,7 };
	vector<int> int_arr2(4, 0);

	// 使用只有4个元素的int_arr2赋值给int_arr1
	int_arr1 = int_arr2;
	// 查看int_arr1的长度是否因赋值发生改变

	// 使用只有两个元素的列表赋值给int_arr1
	cout << "int_arr1 size :" << int_arr1.size() << endl;
	int_arr1 = { 1,2 };
	// 查看int_arr1的长度是否因赋值发生改变
	cout << "int_arr1 size :" << int_arr1.size() << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意:vector.size函数返回值的类型为vector定义的size_type。
  • 对vector对象的比较其实是基于vector元素的。比如如果定义对象 “vector< string > s1,s2;”,那么比较s1和s2其实是比较它们中对应的每个string元素的值。而string元素的比较是由string类定义的,无论是比较大小还是比较相等都是由类定义的。而有些类没有定义类对象之间比较、相等等关系,因此也就无法比较使用这种类作为元素的vector对象。

不能用下标形式添加元素

  • 只能使用push_back来动态改变vector对象的大小,下标形式只能访问已经存在的元素。如果使用未存在的下标会产生很严重的后果。所谓缓冲区溢出(buffer overflow)指的就是这类错误。因此我们使用下标时必须确认下标是合法的。

四、 迭代器介绍

  • 除了vector之外,标准库还定义了其他几种容器。只有少数几种容器才支持下标运算符,但是所有标准库的容器都可以使用迭代器。严格来说string对象不属于容器类型,但string支持很多和容器类型类似的操作,并且string类型也支持迭代器。
  • 迭代器类似于指针,也实现了对对象的间接访问。迭代器的对象是容器中的元素或string对象中的字符,使用迭代器能够访问元素并且也能从一个元素移到另一个元素。
  • 迭代器分为有效迭代器和无效迭代器,有效迭代器指向某个元素或者指向容器中尾元素的下一位置,其他情况的迭代器都属于无效迭代器。

4.1 使用迭代器

  • 迭代器既是一种概念,而C++标准库中所有的容器都实现了迭代器。使用迭代器访问vector对象:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个vector<int>类型的对象arr1
	vector<int> arr1{ 1,2,3,4,5,6,7 };
	// 获取arr1的迭代器
	auto id1 = arr1.begin(); // 获取容器第一个元素的位置
	auto id2 = arr1.end();	 // 获取容器尾元素下一位置
	// 只要迭代器id1没有到达尾元素下一位置id2
	while (id1 != id2)
	{
		// 输出迭代器指向的元素 * id1
		cout << *id1 << " ";
		// 使迭代器指向下一个元素
		++id1;
	}
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • end()函数返回的是容器的尾后迭代器,特殊情况下如果容器为空,则begin和end函数返回的都是尾后迭代器。这里我们暂时用auto去推断迭代器类型,下面我们会介绍迭代器的类型。
  • 无论是end还是begin函数返回的容器迭代器,都是迭代器类型。标准容器迭代器可以使用以下运算符:
使用运算符含义
*iter返回迭代器iter所指元素的引用
iter->mem解引用iter并获取该元素名为mem的成员
++iter令iter指示容器的下一个元素
–iter令iter指示容器的上一个元素
iter == ite2当且仅当两个迭代器指向同一个元素或它们都是同一个容器的尾后迭代器时相等
iter != ite2否则不相等
  • 迭代器也可以通过解引用来获取它所指示的元素,指向解引用的迭代器必须合法并确实指示着某个元素。end返回的尾迭代器并不实际指示某个元素,因此不能对其进行递增和解引用操作。对非法迭代器执行解引用操作的结果是未知的。
  • 标准库中容器的迭代器都定义了 “==” 和 “!=” 运算符,但是大多数没有定义 “<” 运算符。因此我们需要养成使用迭代器和 “!=” 的习惯,这样就不用太在意容器的具体类型。

迭代器类型

  • 实际上拥有迭代器的标准类库使用iterator和const_iterator来表示迭代器的类型。const_iterator和常量指针差不多,能读取但不能修改它所指示的元素值。如果vector或string对象是const常量类型,那么迭代器只能使用const_iterator类型。

begin和end运算符

  • begin和end返回的具体类型由对象是否是常量决定,如果是则返回const_iterator,否则返回iterator。
  • 有时候这种默认行为并非我们所要的,有时对于vector对象或者string对象不是常量,我们也希望限定我们在某处的代码只能读元素而非修改元素。C++11新标准引入了cbegin和cend函数,不论对象本身是否是常量,返回的值都为const_iterator类型。

解引用和成员访问操作

  • 解引用即可获得迭代器所指的对象。如果对象的类型恰好是类,我们可能希望访问它的成员。示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义类型为vector<string>类型的容器arr
	vector<string> arr(10, "hello world!");
	
	// 定义 容器类型vector<sring>中的 迭代器iterator类型,并初始化为arr第一个元素的位置
	vector<string>::iterator iter = arr.begin();
	// 根据迭代iter器检查迭代器指示对象是否为空(先解引用获得迭代器对象,再调用对象的成员函数empty)
	if ((*iter).empty())
		cout << "*iter is empty" << endl;
	else
		cout << "*iter is:" << *iter << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 为什么要写成 " (*iter).empty() " 的形式?因为不加括号iter会先与"."运算符进行结合运算,这是由于运算符优先级顺序导致的,因此我们需要加一个括号先让iter进行解引用获得指示的对象,再调用对象的成员函数。为了方便,C++定义了箭头运算符(->),iter->empty() 等价于刚才的复杂表达式。
  • 修改上文代码,使用箭头运算符:
if (iter->empty())
		cout << "*iter is empty" << endl;
	else
		cout << "*iter is:" << *iter << endl;

迭代器失效

  • vector可以动态增长,但是如果在使用迭代器的同时改变了容器的大小,那么迭代器将会失效。
  • 谨记,凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

4.2 迭代器运算

  • vector和string迭代器支持的运算
运算含义
iter + n迭代器前进n个位置后得到的迭代器
iter - n迭代器后退n个位置后得到的迭代器
iter += n迭代器前进n个位置
iter -= n迭代器后退n个位置
iter1 - iter2计算两个迭代器间的距离,表示iter1向后移动多少个距离等于iter2
<,<=,>,>=迭代器所处位置越前则越小
  • 注意:两个迭代器之间进行运算的前提是,两个迭代器都指向同一个容器。

二分查找

#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个顺序排列的数组(二分查找的要求)
	vector<int> arr = { 1,2,3,4,5,6,7,8,9,10 };
	// 定义要查找的值
	int sought = 7;
	// 定义查找范围处于beg到end之间,mid为范围中间处的值
	vector<int>::const_iterator beg = arr.cbegin();
	vector<int>::const_iterator end = arr.cend();
	vector<int>::const_iterator mid = beg + (end - beg) / 2;
	// 当mid查找完整个数组或查找到sought时停止
	while (mid != end && *mid != sought)
	{
		// 如果查找区域中间的值大于sought
		if (sought < *mid)
			end = mid;	   // 则更新查找区域为beg到mid
		else
			beg = mid + 1; // 否则更新区域为mid+1到end
		// 更新区域后计算新的中间位置
		mid = beg + (end - beg) / 2;
	}
	// 输出查找到的值
	cout << *mid << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

注意事项

  • 对于迭代器类型的定义,如果是string对象的迭代器就声明为:
	string::iterator iter;
  • 如果是vector< int >对象的迭代器就声明为:
	vector<int>::iterator iter;
  • 注意const_iterator迭代器的声明为:
	vector<int>::const_iterator iter;
  • 即对于模板容器的迭代器定义也需要使用模板容器命名空间中的迭代器类型。还有对于const_iterator迭代器不要错误,示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	vector<int> arr;
	vector<int>::const_iterator iter1 = arr.cbegin();	// 正确
	const vector<int>::iterator iter2 = arr.cbegin();	// 错误
	return 0;
}

五、数组

  • 数组是一种长度不变的数据结构,因为数组大小固定,因此对某些特殊的应用来说程序运行时性能较好。
  • 数组灵活性低于vector,如果不清楚元素的确切个数,请使用vector。

5.1 定义和初始化数组

  • 数组是一种复合类型,声明形式如:a[d] 。其中a是数组的名字,而d是数组的长度。因此d必须大于0,并且d必须在编译时已知,即d为一个常量表达式。
  • 未显示初始化的数组将被默认初始化。如果定义内置类型的数组, 并且定义在函数内部,那么默认初始化会令数组含有未定义的值。
  • 数组定义的示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int a[10];
	int* p[20];
	double b[30];
	string str[40];
	return 0;
}

显式初始化数组元素

  • 可以对数组的元素进行列表初始化,在列表初始化时可以省略数组的长度。如果省略数组的长度,编译器使用列表中初始值的个数为数组的长度。如果不省略数组长度,那么数组长度必须大于等于列表中初始值的个数,如果大于,那么列表中的初始值仅初始化数组最前面的一些元素。
  • 示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int a[] = { 1,2,3,4 };	// a的长度为4
	int b[5] = { 1,2,3,4 };	// b的长度为5,仅初始化前4个数,剩余的数被初始化为默认值0
	for (int i : b)
		cout << i << " ";
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

字符数组的特殊性

  • 字符数组有一种额外的初始化形式,即用字符串字面值对数组进行初始化,这种初始化方法要注意字符串字面值多包含了一个未显示的字符’\0’,这可能会导致你的数组无法存储下字符串字面值而引发错误。示例如下:
    在这里插入图片描述

不允许拷贝和赋值

  • 不能将一个数组拷贝给另一个数组,也无法使用一个数组给另一个数组赋值。
  • 有些编译器扩展允许使用数组给另一个数组赋值,但最好避免使用,因为含有非标准特型的程序很可能在其他编译器上无法正常工作。

复杂的数组声明

  • 声明一个指针数组
	int* p[10];
  • 不存在引用数组
	int& r[10];	// 错误:无法声明引用数组
  • 声明一个指针,它指向一个 包含10个整数的数组
	int arr[10];
	int(*q)[10] = &arr;
  • 声明一个引用,它绑定一个 包含10个整数的数组
	int arr[10];
	int(&e)[10] = arr;
  • 声明一个引用,它绑定一个 包含10个整形指针的数组 (声明对修饰符的数量没有限定,可以继续添加*或&)
	int* (&w)[10] = p;
  • 我们通常定义一个指针只为了指向一个简单的int或者float,这时我们直接int * 即可。但如果我们想要使用一个指针或引用指向一个数组怎么办?我们知道数组有对应的类型,比如int,类型规定了从每个元素在内存上的跨度,数组也有首地址和长度,因此数组的内存区域为[首地址,首地址+单个元素长度 * 数组长度]。因此我们要使用一个指针或引用来描述一个数组,我们也必须指定出这些信息。
  • 针对 " int(*q)[10] = &arr; ",我们从里往外分析,q先与 * 结合表示它是一个指针,再与[10]结合表示它指向的内容是个包含10个元素的数组,再与int结合分析出元素的类型是int。因此q就是一个指向 包含10个int数组 的指针。其实我们可以看到q的定义指明了刚才讨论 “表达一个数组” 所需的所有信息,因此q可以指向一个数组,也因此我们直接取arr的地址赋值给q即可。注意此时(*q)等价于arr,因此(*q)[d]等价于使用arr[d]。示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个整形数组
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	// 定义一个整形指针数组
	int* p[10];
	// 使整形指针数组中每个指针指向整形数组中对应索引下的元素
	for (int i = 0; i < 10; i++)
		p[i] = &arr[i];

	// 定义一个指向 包含10个整形数组 的指针q,并使q指向arr 
	int(*q)[10] = &arr;
	for (int i = 0; i < 10; i++)
		cout << (*q)[i] << " "; // q指向arr,*q等价于arr,因此(*q)[d]等价于arr[d] 

	
	// 定义一个绑定 包含10个整形数组 的引用e,并使e绑定arr
	int(&e)[10] = arr;
	cout << endl;
	for (int i = 0; i < 10; i++)
		cout << e[i] << " "; // e就是arr的别名,之所以用[10]声明是为了说明绑定数组的大小 


	// 定义一个绑定 包含10个整形指针数组 的引用w,并使w绑定p
	int* (&w)[10] = p;
	cout << endl;
	for (int i = 0; i < 10; i++)
		cout << *(w[i]) << " "; // w就是p的别名,因此*(p[i])等价于*(w[i])

	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 记住:对于复杂的数组声明,从里往外由近及远一步步分析起来会更清楚。

5.2 访问数组元素

  • 数组下标的类型为size_t,是一种与机器相关的无符号类型,它别设计的足够大以便能表示内存中任意对象的大小。再cstddef头文件中定义了size_t类型。
  • 使用下标访问数组元素的示例代码:
#include <iostream>
#include <string>
#include <vector>	
using std::vector;	
using std::string;
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个整形数组
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	// 使用下标访问数组元素
	cout << arr[1] << " " << arr[3] << " " << arr[5] << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

检查下标的值

  • 在使用下标访问数组前,你应该先检查下标的值是否合法,否则数组下标越界会导致缓冲区溢出。

5.3 指针和数组

  • 在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的地址。
  • 因此使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。必须指出的是,当使用decltype关键字时返回的类型是数组。
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int int_arr1[]{ 2,3,4,5,6,7,8,9,10 };
	// 使用auto 推断数组得到指向数组首元素的指针(也即数组首地址)
	auto p1 = int_arr1;					
	// 使用decltype推断数组得到相同的数组类型
	decltype(int_arr1) int_arr2 = { 2,3,4,5,6,7,8,9,10 };
	
	// p1是指向数组首地址的指针,通过p1+i得到第i个元素的地址,再通过*解引用获得元素的值
	cout << "*p1:";
	for (int i = 0; i < 9; i++)
		cout << *(p1 + i) << " ";

	// int_arr2是数组,直接使用下标访问
	cout << endl << "int_arr2:";
	for (int i = 0; i < 9; i++)
		cout << int_arr2[i] << " ";

	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

指针也是迭代器

  • 实例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int int_arr1[]{ 1,2,3,4,5,6,7,8,9,10 };
	// 令p1指向数组首地址
	int* p1 = int_arr1;
	// 令p2指向数组尾元素的下一位置
	int* p2 = &int_arr1[10];
	// 只要p1不等于p2就打印出p1指向元素的值
	for (; p1 != p2; p1++)
		cout << *p1 << " ";
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

标准库函数begin和end

  • 上文代码通过取一个不存在元素的地址,得到数组尾元素的下一位置,可视这样的做法容易出错且不安全。C++11新标准引入了begin和end函数,它们分别返回数组的首地址和尾元素后下一地址。实例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int int_arr1[]{ 1,2,3,4,5,6,7,8,9,10 };
	// 令p1指向数组首地址
	int* p1 = std::begin(int_arr1);
	// 令p2指向数组尾元素的下一位置
	int* p2 = std::end(int_arr1);
	// 只要p1不等于p2就打印出p1指向元素的值
	for (; p1 != p2; p1++)
		cout << *p1 << " ";
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

  • 注意:对于尾后指针,不能执行解引用和递增操作。(递增尾后指针的操作会导致无法辨别指针是否有效,很可能导致前指针访问无效地址)

指针运算

  • 和迭代器一样,指针也可以做运算。两个指针相减的结果是它们之间的距离,注意参与运算的指针必须指向同一个数组。两个指针相减的结果是一种名为ptrdiff_t的标准库类型,和size_t一样,它也是一种定义于cstddef头文件中机器相关的类型。由于差值可能为负,因此ptrdiff_t是一种带符号类型。
  • 要注意对指针解引用和加法运算的顺序,先加再解引用是先移动指针再查找指向的对象,先解引用再加是先查找指向的对象后再加上一个常量。

下标和指针

  • vector和string的下标必须是无符号类型,这是由标准库限定的。
  • 内置的下标索引不是无符号类型,可以处理负数,示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int int_arr[]{ 1,2,3,4};
	int* p = &int_arr[2];
	cout << p[-2] << " " << p[-1] << " " << p[0] << " " << p[1] << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

5.4 C风格字符串

cstring头文件

  • 字符串字面值是一种通用结构的实例,这种结构是C++由C继承而来的C风格字符串。C风格字符串不是一种类型,而是一种约定俗成的写法。C风格字符串是一个字符数组,字符串从字符数组首字符开始,到字符数组的中含有’\0’的位置截至。一般使用指针来操作这些字符串。
  • C++头文件cstring继承于C语言的头文件string.h,其中包含了可以对字符串进行比较、拷贝、计算长度等一系列函数。如果需要使用cstring头文件对C风格字符串进行处理,请查阅cstring头文件相关函数的功能和使用方法。
  • 建议使用string,避免使用C风格字符串。

5.5 与旧代码的接口

string对象和C风格字符串

  • C风格字符串是以空字符即’\0’结尾的一个字符数组。
  • 允许使用C风格字符串初始化string对象或给string对象赋值。在string对象的加法中允许某一侧的对象是C风格字符串,在string对象的复合赋值运算中允许使用C风格字符串作为右侧的运算对象。示例代码:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
	// 定义一个C风格字符串
	char arr1[] = "hello";
	
	// 使用C风格字符初始化string对象
	string str = arr1;
	cout << str << endl;

	// 使用string对象和C风格字符串进行"+"运算
	char arr2[] = "world!";
	cout << str + arr2 << endl;

	// 使用string对象和C风格字符串进行复合赋值即"+="运算
	str += arr2;
	cout << str << endl;

	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意,我们可以使用C风格字符串初始化string对象,但是不能使用string对象初始化C风格字符串。我们可以通过string类的c_str()成员函数返回一个const char*指针,它是一个C风格字符串,并且其中的内容和string对象中的内容一致(不考虑添加的空白字符’\0’)。示例代码:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
	string str{ "Hello World!" };
	// 使用string.c_str()函数返回const char*即C风格字符串
	const char* arr = str.c_str();

	// 查看C风格字符串中的内容
	for (int i = 0; i < str.size(); i++)
		cout << arr[i];

	// 改变string对象的内容,会导致其返回的C风格字符串也发生改变
	str = "abc";
	cout << endl;
	// 查看C风格字符串是否受到影响
	for (int i = 0; i < str.size(); i++)
		cout << arr[i];

	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 当你使用c_str获得一个C风格字符串时,如果后面你还需要用到它,那么请你使用一个字符数组将返回的C风格字符串内容记录保存下来,否则随着string对象的改变你的C风格字符串也会改变。

使用数组初始化vector对象

  • C++不允许使用数组初始化另一个数组,但是可以使用数组初始化vector对象,只需指明数组中首元素地址和尾后元素地址即可。示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int int_arr[]{ 2,3,4,5,6,7,8,9,10 };
	// 数组初始化vector对象的形式为: vector<T> vec( begin(arr), end(arr) );
	std::vector<int> int_v(std::begin(int_arr), std::end(int_arr));
	for (auto i : int_v)
		cout << i << " ";
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 建议:尽量使用标准库而非数组。使用指针和数组很容易出错,现代C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针。应该尽量使用string,避免使用C风格的基于数组的字符串。

六、多维数组

  • 严格来说C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。
  • 当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)的大小。示例代码:
#include <iostream>

int main()
{
	int a[10][10];
	int b[10][10][10];
	int c[10][10][10][10];
	return 0;
}

多维数组的初始化

  • 初始化为默认值,示例如下:
#include <iostream>

int main()
{
	int c[10][10][10][10] = {};	// 假装赋值,数组就会把所有元素初始化为默认值0
	cout << c[2][2][2][2] << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 顺序初始化 (没指定初始值的元素值为0) :
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int c[10][10] = { 1,2,3,4,5,6,7 };
	cout << c[0][0] << " " << c[0][1] << " " << c[0][2] << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 按行初始化 (可以跳过一些行,直接初始化下一行,当然每行还是要按顺序来):
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	int c[10][10] =
	{
		{1,2,3,4,5,6,7,8,9,10},	// 使用{}可以更直观的初始化一行
		{1},	// 使用{}可以初始化一行的一部分就结束这一行的初始化
		{1,2}	// 因为上一行的初始化用了{},因此即使它没有初始化完,现在的数字将初始化下一行
				// 如果每一行不使用{},而采取一个{}的顺序初始化方法,则这行的1,2会初始化c[1][2]和c[1][3]
	};
	cout << c[0][0] << " " << c[0][1] << endl;
	cout << c[1][0] << " " << c[1][1] << endl;
	cout << c[2][0] << " " << c[2][1] << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

多维数组与范围for语句

  • 使用范围for语句遍历多维数组,示例代码:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个多维数组并初始化
	int a[2][2] =
	{
		{1,2},
		{3,4}
	};
	// 使用范围for语句遍历多维数组
	for (auto& p : a)// 这里的一个p代表数组一行
	{
		for (auto i : p)	// i代表行上每一个元素
			cout << i << " ";
		cout << endl;
	}
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 注意:使用范围for语句处理多维数组,除了最内层循环外,其他所有循环的控制变量都应该是引用类型。因此上文代码中p必须是引用类型,而i可以是引用类型也可以不是,改写上文代码i为引用类型会得到一样的结果。

指针和多维数组

  • 程序使用多维数组的名字时,会自动将其转换成指向数组首元素的指针。
  • 定义指向多维数组的指针时,请记住多维数组实际上是数组的数组。即多维数组名转换得来的指针,实际上是指向内层数组的指针。
  • 使用指针访问多维数组数据,示例代码:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个多维数组并初始化
	int a[2][2] =
	{
		{1,2},
		{3,4}
	};
	// *p说明p是一个指针,[2][2]说明p指向一个2×2的数组,int说明数组中的元素为int类型
	int(*p)[2][2] = &a;		// 因为a是一个2×2的int数组,所以我们可以让p指向a(即p=&a)
	// p的定义说明了指向对象的一切信息,现在p指向a,因此(*p)等价于a
	for (int i = 0; i < 2; i++) 
	{
		for (int j = 0; j < 2; j++)
			cout << (*p)[i][j] << " ";	// (*p)=a 则 (*p)[i][j]=a[i][j]
		cout << endl;
	}
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 使用auto使我们不用在意指针和指向对象的具体类型,就能简单方便的遍历数组。示例代码:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
	// 定义一个多维数组并初始化
	int a[2][2] =
	{
		{1,2},
		{3,4}
	};
	// *p说明p是一个指针,[2][2]说明p指向一个2×2的数组,int说明数组中的元素为int类型
	int(*p)[2][2] = &a;		// 因为a是一个2×2的int数组,所以我们可以让p指向a(即p=&a)

	for (auto& arr : *p)
	{
		for (auto& i : arr)	// i也可以不为引用(因为它处于最内层循环)
			cout << i << " ";	
		cout << endl;
	}
	cout << endl;
	return 0;
}
  • 运行结果:
    在这里插入图片描述

类型别名简化多维数组的指针

  • 使用类型别名定义多维数组的指针可以使程序可读性更强:
using int_array = int[2];
typedef int[2] int_array; // 和上面的声明等价
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仰望—星空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值