第五章——循环和关系表达式

本章内容包括:
*for 循环
*表达式和语句
*递增运算符和递减运算符:++和–
*组合赋值运算符
*复合语句(语句块)
*逗号运算符
*关系运算符:>、>=、==、<=、<和!=
*while循环
*typedef工具
*do while 循环
*字符输入方法get()
*文件尾条件
*嵌套循环和二维数组

计算机要做除了存储数据外的其他工作,比如对数据进行分析、合并、重组、抽取、修改、推断、合成以及其他操作。有时甚至会歪曲和破坏数据,不过我们应当尽量防止这种行为的发生。为了发挥其强大的操控能力,程序需要有执行重复的操作和进行决策的工具。当然,C++提供了这样的工具。事实上,它使用与常规C语言相同的for循环、while循环、do while循环、if语句和switch语句。

5.1 for循环
很多情况下都需要程序执行重复的任务,如将数组中的元素加起来或将歌颂生产的赞歌打印20份,C++中的for循环可以轻松的完成这些任务。
程序清单:了解for循环所做的工作

#include <iostream>
int main()
{
	using namespace std;
	int i;
	for(i=0;i<5;i++)
		cout<<"C++ knows loops.\n";
	cout<<"C++ knows when to stop.\n";
	return 0;
}

在这里插入图片描述
程序说明:
该循环首先将整数变量i设置为0,这是循环的初始化(loop initialization)部分。然后,循环测试(loop test)部分检查i是否小于5,如果确实小于5,则程序将执行接下来的语句——循环体,然后,程序使用循环更新(loop update)部分将i加1,i++。
这里使用了++运算符——递增运算符(increment operator),它将操作数的值+1。递增运算符并不仅限于用于for循环。例如,在程序中,可以使用i++;来替换语句i=i+1;将i加1后,便结束了循环的第一个周期。
接下来,循环开始了新的周期,将新的i值与5进行比较。由于新值也小于5,因此循环打印另一行。然后再次将i+1,从而结束这一周期。这样又进入了新的一轮测试、执行语句和更新i值。这一过程将一直进行下去,直到循环将i更新到5为止。这样,接下来的测试失败,程序将执行循环后的语句。

5.1.1 for循环的组成部分
for循环为执行重复的操作提供了循环渐进的步骤。它是如何工作的呢?for循环的组成部分完成下面这些步骤。
1.设置初始值
2.执行测试,看看循环是否应当继续进行
3.执行循环操作
4.更新用于测试的值
C++循环设计中包括了这些要素,很容易识别。初始化、测试和更新操作构成了控制部分,这些操作由括号括起。其中每部分都是一个表达式,彼此由分号隔开。控制部分后面的语句叫做循环体,只要测试表达式为true,它便被执行:

for(initialization;test-expression;update-expression)
	body

C++语法将整个for看作一条语句——虽然循环体可以包含一条或多条语句。
循环只执行一次初始化。通常,程序使用该表达式将变量设置为起始值,然后用该变量计算循环周期。
test-expression(测试表达式)决定循环体是否被执行。通常,这个表达式是关系表达式,即对两个值进行比较。这个例子将i的值同5作比较,看i是否小于5,。如果结果比较为真,则程序将执行循环体。实际上,C++并没有将test-expression的值限制为只能为真或假。可以使用任意表达式,C++将把结果强制转换为bool类型。因此,值为0的表达式将被转换为bool值false,导致循环结束。如果表达式的值为非零,则被强制转换为bool值true,循环将继续进行。

程序清单:通过表达式i用作测试条件来演示了这一特点。更新部分i–与i++相似,只是每使用一次,i值就减1…

#include <iostream>
int main()
{
	using namespace std;
	cout<<"Enter the starting countdown value:";
	int limit;
	cin>>limit;
	int i;
	for(i = limit;i;i--) //quits when i = 0
		cout<<"i = "<<i<<"\n";
	cout<<"Done now that i = "<<i<<"\n";
	return 0;
}

在这里插入图片描述
注意,循环在i变为0后结束。
关系表达式(如i<5)是如何得到循环终止值0的呢?在引入bool类型之前,如果关系表达式为true,则被判定为1;如果为false,则被判定为0。因此,表达式3<5的值为1,而5<5的值为0。然而,C++添加了bool类型后,关系表达式就判定为bool字面值的true和false,而不是0和1了。这种变化不会导致不兼容的问题,因为C++程序在需要整数值的地方把true和false分别转换为1和 0,而在需要bool值的地方把0转换为false,非零转换为true。
for循环是入口条件循环,这意味着在每轮循环之前,都将计算测试表达式的值,当测试表达式为false时,将不会执行循环体。例如,假设重新运行上面的程序,将起始值设为0,则由于测试条件在首次被判定为false,循环体将不会被执行。
for语句看上去有些像函数调用,因为它使用一个后面跟一对括号的名称。然而,for是C++的一个关键字,因此编译器不会将for视为一个函数,这还防止将函数命名为for。

我们使用for循环完成更多的工作。用一个循环来计算连续阶乘的值,并将这些值存储在数组中。然后,用另一个循环来显示结果。
程序清单:使用循环来计算并存储前16个阶乘。
#include <iostream>
const int ArSize = 16;
int main()
{
	long long factorials[ArSize];
	factorials[1] = factorials[0]=1LL;
	for (int i =2;i<ArSize;i++)
		factorials[i]=i*factorials[i=1];
	for (int i = 0;i<ArSize;i++)
		std::cout<<i<<"!="<<factorials[i]<<std::endl;
	return 0;
}
	通常,定义一个const值来表示数组中的元素个数是个好办法。在声明数组和引用数组长度时,可以使用const值。

程序清单:按照用户选择的步长值将循环计数递增。使用表达式i=i+by,其中by是用户选择的步长值。

#include <iostream>
int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	cout<<"Enter an integer: ";
	int by;
	cin>>by;
	cout<<"Counting by "<<by<<"s:\n";
	for(int i = 0;i<100;i=i+by)
		cout<<i<<endl;
	return 0;
}

在这里插入图片描述

for循环提供了一种依次访问字符串中每个字符的方式。例如,让用户输入一个字符串,然后按相反的方向逐个字符的显示该字符串。在这个例子中,可以使用string对象,也可以使用char数组,因为它们都能够使用数组表示法来访问字符串中的字符。
程序清单:

#include <iostream>
#include <string>
int main()
{
	using namespace std;
	cout<<"Enter a word: ";
	string word;
	cin>>word;

	for (int i = word.size() - 1;i >= 0;i--)
		cout<<word[i];
	cout<<"\nBye.\n";
		return 0;
}

在这里插入图片描述
递增运算符++和递减运算符–
C++中有多个常被用在循环中的运算符。递增运算符和递减运算符这两个运算符执行两种极其常见的循环操作:将循环计数加1或减1。然而,它们还有很多特点,这两个运算符都有两种变体。前缀prefix版本位于操作数前面,如++x:后缀postfix版本位于操作数后面,如x++。两个版本对操作数的影响是一样的,但是影响的时间不同。这就像对于财产来说,清理草坪前付钱和清理草坪之后付钱的最终结果是一样的,只是支付钱的时间不同。

程序清单:演示递增运算符的这种差别

#include <iostream>
int main()
{
	using namespace std;
	int a = 20;
	int b = 20;
	cout<<"a = "<<a<<": b = "<<b<<endl;
	cout<<"a++ = "<<a++<<": ++b= "<<++b<<endl;
	cout<<"a = "<<a<<": b = "<<b<<endl;
	return 0;
}

在这里插入图片描述
组合赋值运算符
C++有一种合并了加法和赋值操作的运算符,能够更简洁地完成这种任务:
i += by
+=运算符将两个操作数相加,并将结果赋给左边的操作数。这意味着左边的操作数必须能够被赋值,如变量、数组元素、结构成员或通过对指针解除引用来标识的数据:

int k = 5;
k += 3;	//OK,k set to 8
int *pa=new int[10];		//pa points to pa[0]
pa[4] = 12;
pa[4] += 6;		//ok,pa[4] set to 18
*(pa + 4) += 7;		//ok,pa[4] set to 25
pa += 2;			//ok,pa points to the former pa[2]
34 += 10;			//quite wrong

复合语句(语句块):
编写C++for语句的格式比较严格,因为循环体必须是一条语句。如果要在循环体中包含多条语句,这将很不方便。C++避开了这种限制的方式,通过这种方式可以在循环体中包含任意多条语句。方法是通过用两条花括号来构造一条复合语句。即代码块,代码块由一对花括号和它们包含的语句组成,被视为一条语句,从而满足句法的要求。

程序清单:使用花括号将3条语句合并为一个代码块。循环体便能够提示用户、读取输入并进行计算。

#include <iostream>
int main()
{
	using namespace std;
	cout<<"The Amazing Accounto will sum and average ";
	cout<<"five numbers for you.\n";
	cout<<"Please enter five values:\n";
	double number;
	double sum = 0.0;
	for(int i =1;i<=5;i++)
	{
		cout<<"Value "<<i<<": ";
		cin>>number;
		sum += number;
	}
	cout<<"Five exquisite choices indeed!";
	cout<<"They sum to "<<sum<<endl;
	cout<<" and average to "<<sum/5<<".\n";
	cout<<"The Amazing Accounto bids you adieu!.\n";
	return 0;
}

在这里插入图片描述
程序清单:在声明语句中声明一个外部语块中有的变量,在声明位置到内部语句块结束的范围之内,新变量将隐藏旧变量;然后旧变量再次可见。

#include <iostream>
int main()
{
	using std::cout;
	using std::endl;
	int x = 20;
	{
		cout<<x<<endl;
		int x = 100;
		cout<<x<<endl;
	}
	cout<<x<<endl;
	return 0;
}

在这里插入图片描述
其他语句技巧——逗号运算符
语句块允许把两条或更多的语句放到按C++句法只能放一条的地方。逗号运算符对表达式完成同样的任务,允许将两个表达式放到C++句法只允许放一个表达式的地方。例如,假设有一个,每轮都将一个变量加1,而将另一个变量减1,。在for循环控制部分的更新部分中完成这两项工作将非常方便,但循环句法只允许这里包含一个表达式。在这种情况下,可以使用逗号运算符将两个表达式合并为一个:

++j,++i		//two expressions count as one for syntax purposes

逗号并不总是逗号运算符。例如,下面这个声明中的逗号将变量列表中相邻的名称分开:

int i,j;		//comma is a separator here,not an operator

程序清单:使用两次逗号运算符,该程序将一个string类对象的内容反转

#include <iostream>
#include <string>
int main()
{
	using namespace std;
	cout<<"Enter a word: ";
	string word;
	cin>>word;
	char temp;
	int i,j;
	for(j=0,i=word.size()-1;j<i;--i,++j)
	{
		temp=word[i];
		word[i]=word[j];
		word[j]=temp;
	}
	cout<<word<<"\nDone|n";
	return 0;
}

在这里插入图片描述
注意:赋值、比较和可能犯的错误
不要混淆等于运算符(==)与赋值运算符(=)。

musicians == 4;	//comparison,musicians是否等于4,该表达式的值是true或false
musicians = 4;	//assignment,该表达式将4赋给musicians

for循环的灵活设计让用户很容易出错。如果不小心遗漏了==运算符中的一个等号,则for循环的测试部分将是一个赋值表达式,而不是关系表达式,此时代码仍是有效的。这是因为可以将任何有效的C++表达式用作for循环的测试条件。非零值为true,零值为false。将4赋给musicians的表达式的值为4,因此被视为true。如果以前使用过用=判断是否相等的语言,如Pascal或BASIC,则尤其可能出现这样的错误。
警告:不要使用=来比较两个量是否相等,而要使用= =

假设要知道字符数组中的字符串是不是mate。如果word是数组名,下面的测试可能并不像我们预想的那样工作:

word=="mate"

请记住,数组名是数组的地址。同样,用引号括起来的字符串常量也是其地址。因此,上面的关系表达式不是判断两个字符串是否相同,而是查看它们是否存储在相同的地址上。两个字符串的地址是否相同呢?答案是否定的,虽然它们包含相同的字符。
由于C++将C-风格字符串视为地址,因此如果使用关系运算符来比较它们,将无法得到满意的结果。
相反,应使用C-风格字符串库中的strcmp()函数来比较。该函数接受两个字符串地址作为参数。这意味着参数可以是指针、字符串常量或字符串组名。如果两个字符串相同,该函数将返回零;如果第一个字符串按字母顺序排在第二个字符串之前,则strcmp()将返回一个负数值;如果第一个字符串按字母顺序排在第二个字符串之后,则strcmp()返回一个正数值。实际上,“按系统顺序排序”比“按字母顺序排序”更准确。这意味着字符是根据字符的系统编码来进行比较的。例如,使用ASCII码时,所有大写字母的编码都比小写字母小,所以按排列顺序,大写字母将位于小写字母之前。因此,字符串“Zoo”在字符串“aviary”之前。根据编码进行比较还意味着大写字母和小写字母是不同的,因此字符串“FOO”和字符串“foo”不同。
在有些语言中,存储在不同长度的数组中的字符串彼此不相等。但是C-风格字符串是通过结尾的空值字符定义的,而不是由其所在数组的长度定义的。这意味着两个字符串即使被存储在长度不同的数组中,也可能是相同的:

char big[80]="Daffy";	//5 letters plus
char little[6]="Daffy";	//5 letters plus

顺便说一句,虽然不能用关系运算符来比较字符串,但却可以用它们来比较字符,因为字符实际上是整型。因此下面的代码可以用来显示字母表中的字符,至少对于ASCII字符集和Unicode字符集来说是有效的:

for(ch='a';ch<='z';ch++)
	cout<<ch;

程序清单:在for循环的测试条件中使用了strcmp()。该程序显示一个单词,修改其首字母,然后再次显示这个单词,这样循环往复,知道strcmp()确定该单词与字符串“mate”相同为止。注意,该程序清单包含了文件cstring,因为它提供了strcmp()的函数原型。

#include <iostream>
#include <cstring>
int main()
{
	using namespace std;
	char word[5]="?ate";
	for(char ch='a';strcmp(word,'mate');ch++)
	{
		cout<<word<<endl;
		word[0]=ch;
	}
	cout<<"After loop ends,word is "<<word<<endl;
	return 0;
}

在这里插入图片描述
程序说明:该程序有几个有趣的地方。其中之一是测试。我们希望只要word不是mate,循环就继续进行。也就是说,我们希望只要strcmp()判断出两个字符串不同,测试就继续进行,最显而易见的测试是这样的:

strcmp(word,"mate")= 0 	//strings are not the same

如果字符串不相等,则该语句的值为1,否则为零。但使用strcmp(word,“mate”)本身将如何呢?如果字符串不相等,则它的值为非零(true);如果字符串相等,则它的值为零(false)。实际上,如果字符串不同,该返回true,否则返回false。因此,可以只用这个函数,而不是整个关系表达式。这样得到的结果将相同,还可以少输入几个字符。另外,C和C++程序员传统上就是用这种方式使用strcmp()的。

检测相等或排列顺序
可以使用strcmp()来测试C-风格字符串是否相等(排列顺序)。如果str1和str2相等,则下面的表达式为true:

strcmp(str1,str2) == 0

如果str1和str2不相等,则下面两个表达式都为true:

strcmp(str1,str2)!=0
strcmp(str1,str2)

如果str1在str2的前面,则下面的表达式为true:

strcmp(str1,str2)<0

因此,根据要如何设置测试条件,strcmp()可以扮演==,!=、<和>运算符的角色

程序清单:使用string对象而不是char数组

#include <iostream>
#include <string>
int main()
{
	using namespace std;
	string word = "?ate";
	for (char ch = 'a';word != "mate";ch++)
	{
		cout<<word<<endl;
		word[0]=ch;
	}
	cout<<"After loop ends,word is "<<word<<endl;
	return 0;
}

在这里插入图片描述
5.2 while循环
while循环是没有初始化和更新部分的for循环,它只有测试条件和循环体:

while (test condition)
	body
	首先,程序计算圆括号内的测试条件表达式。如果该表达式为true,则执行循环体中的语句。与for循环一样,循环体也由一条语句或两个花括号定义的语句块组成。执行完循环体后,程序返回测试条件,对它进行重新评估。如果该条件为非零,则再次执行循环体。测试和执行将一直进行下去,直到测试条件为false为止。显然,如果希望循环最终能够结束,循环体中的代码必须完成某种影响测试条件表达式的操作。例如,循环可以将测试条件中使用的变量加1或从键盘输入读取一个新值。和for循环一样,while循环也是一种入口条件循环。因此,如果测试条件一开始便为false,则程序将不会执行循环体。

程序清单:使用一个while循环。

#include <iostream>
const int ArSize = 20;
int main()
{
	using namespace std;
	char name[ArSize];
	cout<<"Your first name,please: ";
	cin>>name;
	cout<<"Here is your name,verticalized and ASCIIized:\n";
	int i = 0;
	while(name[i]!='\0')
	{
		cout<<name[i]<<": "<<int(name[i])<<endl;
		i++;
	}
	return 0;
} 

在这里插入图片描述
程序说明:verticalized和ASCIIized并不是真正的单词,甚至将来也不会是单词。不过它们确实在输出中添加了一种“可爱”的氛围。

while(name[i] != '\0'

它可以测试数组中特定的字符是不是空值字符。为使该测试最终能成功,循环体必须修改i的值,这是通过在循环体结尾将i加1来实现的。省略这一步将导致循环停留在同一个数组元素上,打印该字符及其编码,知道强行终止该程序。导致死循环是循环最常见的问题之一。通常,在循环体中忘记更新某个值时,便会出现这样的情况。
可以这样修改while行:

while (name[i])

经过这种修改后,程序的工作方式将不变。这是由于name[i]是常规字符,其值为该字符的编码——非零值或true。然而,当name[i]为空值字符时,其编码将为0或false。这种表示法更为简洁。
要打印字符的ASCII码,必须通过强制类型转换将name[i]转换为整型。这样cout将把值打印成整数,而不是将它解释为字符编码。
不同于C-风格字符串,string对象不使用空字符来标记字符串末尾,因此要将程序清单转换为使用string类的版本,只需用string对象替换char数组即可。
for与while
在C++中,for和while循环本质上是相同的。

for (init-expression;test-expressions;update-expressions)
{
	statement(s)
}

上面的for循环可以改写成这样:

init-expression;
while(test-expression)
{
	statement(s)
	update-expression;
}

同样,下面的while循环:

while (test-expression)
	body

可以改写成:

for(;test-expression)
	body

for循环需要3个表达式(从技术角度来说,它需要1条后面跟两个表达式的语句,不过它们可以是空表达式,只有两个分号是必需的。另外省略for循环中的测试表达式时,测试结果将为true;其次,在for循环中,可使用初始化语句声明一个局部变量,但在while循环中不能这样做;最后,如果循环体中包括continue语句,情况将稍有不同。通常,程序员使用for循环来为循环计数,因为for循环格式允许将所有的相关的信息——初始值、终止值和更新计数器的方法——放在同一个地方。在无法预先知道循环将执行的次数时,程序员使用while循环。

提示:在设计循环时,记住下面几条指导原则
*指定循环终止的条件
*在首次测试之前初始化条件
*在条件被再次测试之前更新条件
for循环的一个优点是,其结构提供了一个可实现上述3条指导原则的地方,因此有助于程序员记住应该这样做,但这些指导原则也适用于while循环。

错误的标点符号
for循环和while循环都由用括号括起的表达式和后面的循环体(包含一条语句)组成。这条语句可以是语句块,其中包含多条语句。记住,语句块是由花括号,而不是由缩进定义的。

i=0;
while(name[i]!='\0');
	cout<<name[i]<<endl;
	i++;
cout<<"Done\n";

缩进表明。该程序的作者希望i++语句是循环体的组成部分。然而,由于没有花括号,因此编译器认为循环体仅由最前面的cout语句组成。因此,该循环将不断的打印数组的第一个字符。该程序不会执行i++语句,因为它在循环的外面。
下面的例子说明了另外一个潜在的缺陷:

i=0;
while (name[i]!='\0');
{
	cout<<name[i]<<endl;
	i++;
}
cout<<"Done\n";

这一次,代码正确的使用了花括号,但还插入了一个分号。记住,分号是结束语句,因此该分号将结束while循环。换句话说,循环体为空语句,也就是说,分号后面没有任何内容。这样,花括号中所有的代码现在位于循环的后面,永远不会被执行。该循环不执行任何操作,是一个死循环。
等待一段时间,编写延时循环
有时候,我们希望程序能够等待一段时间,while循环可用于这种目的。一种用于个人计算机的早期技术是,让计算机进行计数,以等待一段时间:

long wait = 0;
while (wait<10000)
	wait++;

这种方法的问题是,当计算机处理器速度发生变化时,必须修改计数限制。例如,有些为IBM PC编写的游戏在速度更快的机器上运行时,其速度将快得无法控制;另外,有些编译器可能修改上述代码,将wait设置为10000,从而跳过该循环。更好的方法是让系统时钟来完成这种工作。
ANSI C和C++库中有一个函数有助于完成这样的工作。这个函数名为clock(),返回程序开始执行后所用的系统时间。这有两个复杂的问题:首先,clock()返回时间的单位不一定是秒;其次,该函数的返回类型在某些系统上可能是long,在另一些系统上可能是unsigned long或其他类型。
但头文件ctime(较早的实现中time.h)提供了这些问题的解决方案。首先,它定义了一个符号常量——CLOCKS_PER_SEC,该常量等于每秒钟包含的系统单位时间数。因此,将系统时间除以这个值,可以得到秒数。或者将秒数乘以CLOCK_PER_SEC,可以得到以系统时间单位为单位拟的时间。其次,ctime将clock_t作为clock()返回类型的别名,这意味着可以将变量声明为clock_t类型,编译器将把它转换为long、unsigned int或适合系统的其他类型。

程序清单:演示了如何使用clock()和头文件ctime来创建延迟循环。

#include <iostream>
#include <ctime>
int main()
{
	using namespace std;
	cout<<"Enter the delay time,in seconds: ";
	float secs;
	cin>>secs;
	clock_t delay = secs*CLOCKS_PER_SEC;
	cout<<"starting\a\n";
	clock_t start=clock();
	while (clock()-start<delay)
		;
	cout<<"done \a\n";
	return 0;
}

在这里插入图片描述
该程序以系统时间单位为单位计算延迟时间,避免了在每轮循环中将系统时间转换为秒。

类型别名
C++为类型建立别名的方式有两种。一种是使用预处理器:

#define BYTE char 	//preprocessor replaces BYTE with char

这样,预处理器将在编译程序时用char替换所有BYTE,从而使BYTE称为char的别名。
第二种是使用C++和C的关键字typedef来创建别名。例如,要将byte作为char的别名,可以:

typedef char byte;	//makes byte an alias for char

下面是通用格式:
typedef typeName aliasName;
换句话说,如果要将aliasName作为某种类型的别名,可以声明aliasName,如同将aliasName声明为这种类型的变量那样,然后在声明语句的前面加上关键字typedef。例如,要让byte_pointer成为char指针的别名,可以将byte_pointer声明为char指针,然后在前面加上typedef:

typedef char*byte_pointer;	//pointer to char type

也可以使用#define,不过声明一系列变量时,这种方法不适用。

#define PLOAT_POINTER float*FLOAT_POINTER pa,pb;

预处理器置换将该声明转换为这样:

float*pa,pb;	//pa a pointer to float,pb just a float

typedef 方法不会有这样的问题,它能够处理更复杂的类型别名,这使得与使用#define相比,使用typedef是一种更佳的选择——有时候,这也是唯一的选择。
注意:typedef不会创建新类型,而只是为已有的类型建立一个新的名称。如果将word作为int的别名,则cout将把word类型的值视为int类型。

5.3 do while循环
前面已经学习了for循环和while循环,第三种循环就是do while 循环,它不同于另外两种循环,因为它是出口条件循环。这意味着这种循环将首先执行循环体,然后再判定测试表达式,决定是否应继续执行循环。如果条件为false,则循环终止;否则,进入新一轮的执行和测试。这样的循环通常至少执行一次,因为其程序流必须经过循环体后才能到达测试条件。

do 
	body
while(test_expression);

循环体是一条语句或用括号括起的语句块。
通常,入口条件循环比出口条件循环好,因为入口条件循环在循环开始之前对条件进行检查。

程序清单:请求用户输入时,程序必须先获得输入,然后对它进行测试。

#include <iostream>
int main()
{
	using namespace std;
	int n;
	cout<<"Enter numbers in the range 1-10 to find";
	cout<<"my favorite number\n";
	d0
	{
		cin>>n;
	}while(n!=7);
	cout<<"Yes,7 is my favorite.\n";
	return 0;
}

在这里插入图片描述

奇特的for循环
虽然不是很常见,但有时出现下面这样的代码:

int I=0;
for(;;)
{
	I++;
	if(30>=1)
		break;
}

上述的代码基于这样一个事实:for循环中的空测试条件被视为true,这个例子既不易于阅读,也不能用作边编写循环的通用模型。

int I=0do{
	I++;
while (30>I);

同样,第二个例子使用的while循环可以表达得更清晰:

while(I<30)
{
	I++;
}

通常,编写清晰、容易理解的代码比使用语言的晦涩特性来显示自己的能力更为有用。

5.4 基于范围的for循环
基于范围的for循环,这简化了一种常见的循环任务:对数组的每个元素执行相同的操作。

double prices[5]={4.99,10.99,6.87,7.99,8.49};
for (double x;prices)
	cout<<x<<std::endl;
	其中,x最初表示数组prices的第一个元素。显示第一个元素后,不断执行循环,而x依次表示数组的其他元素。因此,上述代码显示全部5个元素,每个元素占据一行。总之,该循环显示数组中的每个值。
	要修改数组的元素,需要使用不同的循环变量语法:
for(double &x:prices)
	x=x*0.80;
符号&表明x是一个引用变量,这种声明让接下来的代码能够修改数组的内容,而第一种语法不能。
还可结合使用基于范围的for循环和初始化列表:
for(int x :{3.5.2.8.6)})
	cout<<x<<" ";
cout<<'\n';

然而,这种循环主要用于各种模板容器类

5.5 循环和文本输入
知道循环的工作原理后,来看一看循环完成的一项最常见最重要的任务:逐字符地读取来自文件或键盘的文本。例如,编写一个能够计算输入中字符数、行数和字数的程序。传统上,C++和C语言一样,也使用while循环来完成这类任务。

使用原始的cin进行输入
如果程序要使用循环来读取来自键盘的文本输入,则必须有办法知道何时停止读取。如何知道这一点呢,一种方法是选择某个特殊字符——有时被称为哨兵字符,将其作为停止标记。

程序清单:在遇到#字符时停止读取输入。该程序计算读取的字符数,并回显这些字符,即在屏幕上显示读取的字符。按下键盘上的键不能自动将字符显示到屏幕上,程序必须通过回显输入字符来完成这项工作。通常,这项任务由操作系统处理。运行完毕后,该程序将报告处理的总字符数。

#include <iostream>
int main()
{
	using namespace std;
	char ch;
	int count = 0;
	cout<<"Enter characters; enter # to quit:\n";
	cin>>ch;
	while (ch!='#')
	{
		cout<<ch;
		++count;
		cin>>ch;
	}
	cout<<endl<<count<<" characters read\n";
	return 0;
}

在这里插入图片描述
程序说明:
请注意该程序的结构。该程序在循环之前读取第一个输入字符,这样循环可以测试第一个字符。这很重要,因为第一个字符可能是#。由于使用的是入口循环,因此在这种情况下,能够正确的跳过整个循环。由于前面已经将变量count设置为0,因此count的值也是正确的。
如果读取的第一个字符不是#,则程序进入该循环,显示字符,增加计数,然后读取下一个字符。最后一步是极为重要的,没有这一步,循环将反复处理第一个输入字符,一直进行下去。有了这一步后,程序就可以处理到下一个字符。
注意,该循环设计遵循了前面指出的几条指导原则。结束循环的条件是最后读取的一个字符是#。该条件是通过在循环之前读取一个字符进行初始化的,而通过循环体结尾读取下一个字符进行更新。
上面的做法合情合理。但为什么程序在输出时省略了空格呢?原因在cin。读取char值时,与读取其他类型一样,cin将忽略空格和换行符。因此输入中的空格没有被回显,也没有被包含在计数内。
更为复杂的是,发送给cin的输入被缓冲。这意味着只有在用户按下回车键后,它输入的内容才会被发送给程序。这就是在运行程序时,可以在#后面输入字符的原因。按下回车键后,整个字符序列将被发送给程序,但程序在遇到#字符后将结束对输入的处理。

使用cin.get(char)进行补救
通常,逐个字符读取输入的程序需要检查每个字符,包括空格、制表符和换行符。cin所属的istream类(在iostream中定义)中包含一个能够满足这种要求的成员函数。具体的说,成员函数cin.get(ch)读取输入中的下一个字符,即使它是空格,并将其赋给变量ch。使用这个函数调用替换cin>>ch,可以修补上面程序清单出现的问题。

#include <iostream>
int main()
{
	using namespace std;
	char ch;
	int count = 0;
	cout<<"Enter characters;enter # to qiut:\n";
	cin.get(ch);
	while(ch!='#')
	{
		cout<<ch;
		++count;
		cin.get(ch);
	}
	cout<<endl<<count<<" characters read\n";
	return 0;
}

在这里插入图片描述
程序说明:现在,该程序回显了每个字符,并将全部字符计算在内,其中包含空格。输入仍被缓冲,因此输入字符个数仍可能比最终到达程序的要多。
如果熟悉C语言,可能以为这个程序存在严重的错误,cin.get(ch)调用将一个值放在ch变量中,意味着将修改该变量的值。在C语言中,要修改变量的值,必须将变量的地址传递给函数。而上面的程序调用cin.get()时,传递的是ch,而不是&ch。在C语言中,这样的代码无效,但在C++中有效,要函数将参数声明为引用即可。引用是C++在C语言基础上新增的一种类型。头文件iostream cin.get(ch)的参数声明为引用类型,因此该函数可以修改其参数的值。

使用哪一个cin.gegt()
在第四章中,有这样的代码:

char name[ArSize];
...
cout<<"Enter your name\n";
cin.get(name,ArSize).get();

最后一行相当于两个连续的函数调用

cin.get(name,ArSize);
cin.get();

cin.get()的一个版本接受两个参数:数组名(字符串(char类型)的地址)和ArSize(int类型的整数)。(记住,数组名是其第一个元素的地址,一年春字符数组名的类型为char。)接下来,程序使用了不接受任何参数的cin.gte()。而最近,我们这样使用过cin.get():

char ch;
cin.get(ch);

这里cin.get接受一个参数。
在C语言中,如果函数接受char指针和int参数,则使用该函数时,不能只传递一个参数(类型不同)。但在C++中,可以这样做,因为该语言支持被称为函数重载的OOP特性。函数重载允许创建多个同名函数,条件是它们的参数列表不同。例如,如果在C++中使用cin.get(name,ArSize),则编译器将找到使用char*和int作为参数的cin.gte()版本;如果使用cin.get(),编译器将使用接受一个char参数的版本,如果没有提供参数,则编译器将使用不接受任何参数的cin.get()版本。函数重载允许对多个相关的函数使用相同的名称,这些函数以不同方式或针对不同类型执行相同的基本任务。另外,通过使用istream类中的get()示例,我们将逐渐习惯函数重载。为区分不同的函数版本,我们在引用它们时提供参数列表。因此,cin.get()指的是不接受任何参数的版本,而cin.get(char)则指的是接受一个参数的版本。

文件尾条件
从前面的程序看到,使用诸如#等符号来表示输入结束很难让人满意,因为这样的符号可能就是合法输入的组成部分,其他符号(如@和%)也如此。如果输入来自于文件,则可以使用一种功能更强大的技术——检测文件尾(EOF)。C++输入工具和操作系统协同工作,来检测文件尾并将这种信息告知程序。
乍一看,读取文件中的信息似乎同cin和键盘输入没什么关系,但其实存在两个相关的地方。首先,很多操作系统(包括Unix、Linux和Windows命令提示符模式)都支持重定向,允许用文件替换键盘输入。例如,假设在Windows中有一个名为gofish.exe的可执行程序和一个名为fishtale的文本文件,则可以在命令提示符模式下输入下面的命令:

gofish<fishtale

这样,程序将从fishtale文件获取输入。<符号是Unix和Windows命令提示符模式的重定向运算符。
其次,很多操作系统都允许通过键盘来模拟文件尾条件。在Unix中,可以在行首按下Ctrl+D来实现:
在Windows命令提示符模式下,可以在任意位置按Ctrl+Z和Enter。有些C++实现支持类似的行为,即使底层操作系统并不支持。键盘输入的EOF概念实际上是命令行环境遗留下来的。然而,用于Mac的Symantec C++模拟了UNIX,将CTRL+D视为仿真的EOF。Metrowerks Codewarrior能够在Macintosh和Windows环境下识别Ctrl+Z。用于PC的Microsoft Visual C++、Borland C++和GNU C++都能够识别行首的Ctrl+Z,但用户必须按下回车键。总之,很多PC编程环境都将Ctrl+Z视为模拟的EOF,但具体细节(必须在行首还是可以在任何位置,是否必须按下回车键等)各不相同。
如果编程环境能够检测EOF,可以在类似于上面的程序使用重定向文件,也可以使用键盘输入,并在键盘输入中模拟EOF。这一点似乎很有用,来看一下酒精怎么做。

检测到EOF后,cin将两位(eofbit和failbit)都设置为1.可以通过成员函数eof()来查看eofbit是否被设置:如果检测到EOF,则cin.eof()将返回的bool值true,否则返回false。同样,如果eof()和fail()方法报告最近读取的结果;也就是说,它们在事后报告,而不是预先报告。因此应将cin.eof()或cin.fail()测试放在读取后,

程序清单:使用的是fail(),而不是eof(),因为前者可用于更多的实现中。

#include <iostream>
int main()
{
	using namespace std;
	char ch;
	int count = 0;
	cin.get(ch);
	while (cin.fail() == false)
	{
		cout<<ch;
		++count;
		cin.get(ch);
	}
	cout<<endl<<count<<" characters read\n";
	return 0;
}

在这里插入图片描述
这里是Windows系统上运行该程序,因此可以按下Ctrl+Z和回车键来模拟EOF条件。请注意,在Unix和类Unix系统中,用户应按Ctrl+Z组合键将程序挂起,而命令fg恢复执行程序。
通过使用重定向,可以用该程序来显示文本文件,并报告它包含的字符数。

1、EOF结束输入
前面指出过,cin方法检测到EOF时,将设置cin对象中一个指示EOF条件的标记。设置这个标记后,cin将不读取输入,再次调用cin也不管用。对于文件输入,这是有道理的,因为程序不应该读取超出文件尾的内容。然而,对于键盘输入,有可能使用模拟EOF来结束循环,但稍后要读取其他输入。cin.clear()方法可能清除EOF标记,使输入继续进行。要记住的是,在有些系统中,按Ctrl+Z实际上将结束输入和输出,而cin.clear()将无法恢复输入和输出。

2、常见的字符输入做法
每次读取一个字符,直到遇到EOF的输入循环的基本设计如下:

cin.get(ch);
while (cin.fail() == false)
{
	...
	cin.get(ch);
}
	可以在上述代码中使用一些简捷方式,运算符可以将true切换为false或将false切换为true。可以使用此运算符将上述的while测试改写为:
while (!cin.fail())
方法cin.get(char)的返回值是一个cin对象。然而istream类提供了一个可以将istream对象(如cin)转换为bool值的函数;当cin出现在需要bool值的地方(如在while循环的测试条件中)时,该转换函数将被调用。另外,如果最后一次读取成功了,则转换得到的bool值为true;否则为false。这意味着可以将上述的while测试改写为这样:
while (cin)

这比!cin.fail()或!cin.eof()更通用,因为它可以检测到其他失败的原因,如磁盘故障。
最后,由于cin.get(char)的返回值为cin,因此可以将循环精简成这种格式:

while (cin.get(ch))
{
...
}

这样,cin.get(char)只被调用一次,而不是两次;循环前一次、循环结束后一次。为判断循环测试条件,程序必须首先调用cin.get(ch)。如果成功,则将值放入ch中。然后,程序获得函数调用的返回值,即cin。接下来,程序对cin进行bool转换,如果输入成功,则结果为true,否则为false。三条指导原则(确定结束条件、对条件进行初始化以及更新条件)全部被放在循环测试条件中。

另一个cin.get()版本

不接受任何参数的cin.get()成员函数返回输入中的下一个字符。也就是说,这样使用:
ch = cin.get()

该函数的工作方式与C语言中的getchar()相似,将字符编码作为int值返回;而cin.get(ch)返回一个对象,而不是读取的字符。同样,可以使用cout.put()函数来显示字符:

cout.put(ch);

该函数的工作方式类似C语言中的putchar(),只不过其参数类型为char,而不是int。

注意:最初,put()成员只有一个原型——put(char)。可以传递一个int参数给他,该参数将被强制转换为char,C++标准还要求只有一个原型。然而,有些C++实现都提供了3个原型:put(char)\put(signed char)和put(unsigned char)。在这些实现中,给put()传递一个int参数将导致错误消息,因为转换int的方式不止一种。使用强制类型转换的原型可使用int参数。

	为成功的使用cin.get()。需要知道其如何处理EOF条件。当该函数到达EOF时,将没有可返回的字符。相反,cin.get()将返回一个用符号常量EOF表示的特殊值。该常量是在头文件iostream中定义的。EOF值必须不同于任何有效的字符值,以便程序不会将EOF与常规字符混淆。通常,EOF被定义为值-1,因为没有ASCII码为-1的字符,但并不需要知道实际的值,而只需要在程序中使用EOF即可。
char ch;
cin.get(ch);
while (cin.fail() == false)
{
	cout<<ch;
	++count;
	cin.get(ch);
}

可以使用int ch,并用cin.get()代替cin.get(char),用cout.put()代替cout,用EOF测试代替cin.fail()测试:

int ch;
ch = cin.get();
while(ch != EOF)
{
	cout.put(ch);
	++count;
	ch=cin.get();
}

如果ch是一个字符,则循环将显示它。如果ch为EOF,则循环将结束。
提示:需要知道的是,EOF不表示输入中的字符,而是指出没有字符。
除了当前所做的修改外,关于使用cin.get()还有一个微妙的问题。由于EOF表示的不是有效字符编码,因此可能不与char类型兼容。例如,在有些系统中,char类型是没有符号的,因此char变量不可能为EOF值(-1)。由于这种原因,如果使用cin.get()(没有参数)并测试EOF,则必须将返回值赋给int变量,而不是char变量。另外,如果将ch的类型声明为int,而不是char,则必须在显示ch时将其强制转换为char类型。

程序清单:使用cin.get()方法。还通过将字符输入与while循环测试合并在一起,使代码更为简洁。

#include <iostream>
int main(void)
{
	using namespace std;
	int ch;
	int count = 0;

	while ((ch=cin.get()) != EOF)
	{
		cout.put(char(ch));
		++count;
	}
	cout<<endl<<count<<" characters read\n";
	return 0;
}

在这里插入图片描述
子表达式ch=cin.get()两端的括号导致程序首先计算该表达式。为此,程序必须首先调用cin.get()函数。
然后将该函数的返回值赋给ch。由于赋值语句的值为左操作数的值,因此整个子表达式变为ch的值。如果这个值是EOF,则循环结束,否则继续。该测试条件中所有的括号都是必不可少的。如果省略其中的一些括号。

while (ch=cin.get() != EOF)
	由于!=运算符的优先级高于=,因此程序将首先对cin.get()的返回值和EOF进行比较。比较的结果为false或true,而这些bool值将被转换为0或1,并本质赋给ch。
	另一方面,使用cin.get(ch)(有一个参数进行输入时),将不会导致任何类型方面的问题。前面讲过,cin.get(char)函数在到达EOF时,不会将一个特殊值赋给ch。事实上,在这种情况下,他不会将任何值赋给出ch.ch不会被用来存储非char值。表5.3总结了cin.get(char)和cin.get()之间的差别。

在这里插入图片描述
对于应使用cin.get()还是cin.get(char),我们应该使用字符参数的版本更符合对象方式,因为其返回值是istream对象。这意味着可以将它们拼接起来。例如,下面的代码将输入中的下一个字符读入到ch1中,并将接下来的一个字符读入到ch2中:

cin.get(ch1).get(ch2);

这是可行的,因为函数调用cin.get(ch)返回一个cin对象,然后便可以通过该对象调用get(ch2)。
get()的主要用途是能够将stdio.h的getchar()和putchar()函数转换为iostream的cin.get()和cout.put()方法。只要用头文件iostream替换stdio.h,并用作用相似的方法替换所有的getchar()和putchar()即可。如果旧的代码使用int变量进行输入,而所用的实现包含put()的多个原型,则必须做进一步的调整。

5.6 嵌套循环和二维数组
for循环是一种处理数组的工具。
一维数组是每个数组都可以看做是一行数据。
二维数组更像是一个表格——既有数据行又有数据列。
例如,可以用二维数组来表示6个不同地区每季度的销售额,每一个地区占一行数据。也可以用二维数组来表示RoboDork在计算机游戏板上的位置。
C++没有提供二维数组类型,但用户可以创建每个元素本身都是数组的数组。例如,假设要存储5个城市在4年间的最高温度,这样可以声明数组:

int maxtemps[4][5];
	该声明意味着maxtemps是一个包含4个元素的数组,其中每个元素都是一个由5个整数组成的数组。可以将maxtemps数组看作由4行组成,其中每一行有5个温度值。
	表达式maxtemps[0]是maxtemps数组的第一个元素,因此maxtemps[0]本身就是由5个int组成的数组。maxtemps[0]数组的第一个元素是maxtemps[0][0],该元素是一个int。因此,需要使用两个下标来访问int元素。可以认为第一个下标表示行,第二个下标表示列。
	假设要打印数组所有的内容,可以用一个for循环来改变行,用另一个被嵌套的for循环来改变列:
for (int row = 0;row<4;row++)
{
	for(int col = 0;col<5;++col)
		cout<<maxtemps[row][col]<<"\t";
	cout<<endl;
}

对于每个row值,内部的for循环将遍历所有的col值。这个示例在每个值之后打印一个制表符(使用C++转义字符表示时为\t),打印完每行后,打印一个换行符。

1、初始化二维数组
创建二维数组时,可以初始化其所有元素,这项技术建立在一维数组初始化技术的基础之上:提供由逗号分隔的用花括号括起的值列表:

// initializing a one-dimensional array
int btus[5] = {23,26,24,31,28};
	对于二维数组来说,由于每一个元素本身就是一个数组 ,因此可以使用与上述代码类似的格式来初始化每一个元素,因此,初始化由一系列逗号分隔的一维数组初始化组成:
int maxtemps[4][5] =
{
	{96,100,87,101,105},
	{96,98,91,107,104},
	{97,101,93,108,107},
	{98,103,95,109,108}
};

可将数组maxtemps包含4行,每行包含5个数字。{94,98,87,103,101}初始化第一行,即maxtemps[0]。作为一种风格,如果可能的话,每行数据应各占一行,这样阅读起来将更容易。

2、使用二维数组
程序清单:初始化一个二维数组,并使用了一个嵌套循环,这一次,循环的顺序相反,将列循环放在外面,将行循环放在内面。另外,它还使用了C++ 常用的做法,将一个指针数组初始化为一组字符串常量。也就是说,将cities声明为一个char指针数组。这使得每个元素都是一个char指针,可被初始化为一个字符串的地址。程序将cities初始化为字符串"Gribble City"的地址等等。因此,该指针数组的行为与字符串数组类似。

#include <iostream>
const int Cities = 5;
const int Years = 4;
int main()
{
	using namespace std;
	const char * cities[Cities] = 
	{
		"Gribble City",
		"Gribbletown",
		"New Gribble",
		"San Gribble",
		"Gribble Vista"
	};
	int maxtemps[Years][Cities] = 
	{
		{96,100,87,101,105},
		{96,98,91,107,104},
		{97,101,93,108,107},
		{98,103,95,109,108}
	};
	cout<<"Maximum temperatures for 2008_2011\n\n";
	for(int city = 0;city<Cities;++city)
	{
		cout<<cities[city]<<":\t";
		for(int year = 0;year<Years;++year)
			cout<<maxtemps[year][city]<<"\t";
		cout<<endl;
	}
	return 0;
}

在这里插入图片描述
在输出中使用制表符比使用空格可使数据排列更有规则。然而,制表符设置不相同,因此输出的外观将随系统而异。
在这个例子中,可以使用char数组的数组,而不是字符串指针数组。在这种情况下声明如下:

char * cities[Cities][25] = 
	{
		"Gribble City",
		"Gribbletown",
		"New Gribble",
		"San Gribble",
		"Gribble Vista"
	};

上述方法将全部5个字符串的最大长度限制为24个字符。指针数组存储5个字符串的地址,而使用char数组时,将5个字符串分别复制到5个包含25个元素的char数组中。因此,从存储空间的角度说,使用指针数组更为经济;然而,如果要修改其中的任何一个字符串,则二维数组是更好的选择。令人惊讶的是,这两种方法使用相同的初始化列表,显示字符串的for循环代码页相同。

另外,还可以使用string对象数组,而不是字符串指针数组。

const char * cities[Cities] = 
	{
		"Gribble City",
		"Gribbletown",
		"New Gribble",
		"San Gribble",
		"Gribble Vista"
	};

如果希望字符串是可修改的,则应省略限定符const。使用string对象数组时,初始化列表和用于显示字符串的for循环代码与前两种方法中相同。在希望字符串是可修改的情况下,string类自动调整大小的特性将使这种方法比使用二维数组更为方便。

5.7 总结
C++提供了3种循环:for循环、while循环和do while循环。如果循环测试条件为true或非零,则循环将重复执行一组指令;如果测试条件为false或0,则结束循环。for循环和while循环都是入口条件循环,这意味着程序将在执行循环体中的语句之前检查测试条件。do while循环是出口条件循环,这意味着将在执行循环体中的语句之后检查条件。
每种循环的句法都要求循环体由一条语句组成。然而,这条语句可以是复合语句,也可以是语句块(由花括号括起的多条语句)。
关系表达式对两个值进行比较,常被用作循环测试条件。关系表达式是通过使用6中关系运算符之一构成的:<,<=,==,>=,>或!=。关系表达式的结果为bool类型,值为true或false。
许多程序都逐字节地读取文本输入或文本文件,istream类提供了多种可完成这种工作的方法。如果ch是一个char变量,则下面的语句将输入中的下一个字符读入到ch中:

cin>>ch;

然而,它将忽略空格、换行符和制表符。下面的成员函数调用读取和输入中的下一个字符(而不管该字符是什么),并将其存储到ch中:

cin.get(ch);

成员函数调用cin.get()返回下一个输入字符——包括空格、换行符和制表符,因此,可以这样使用它:

ch=cin.get();

cin.get(char)成员函数调用通过返回转换为false的bool值来指出已到达EOF,而cin.get()成员函数调用则通过返回EOF值来指出已到达EOF,EOF是在文件iostream中定义的。
嵌套循环是循环中的循环,适合用于处理二维数组。

5.8 复习题
1、入口条件和出口条件循环之间的区别是什么?各种C++循环分别属于其中哪一种?
答:输入条件循环在进入输入循环体之前将评估测试表达式,如果条件最初为false,则循环不会执行其循环体。退出条件循环在处理循环体之后评估测试表达式。因此,即使测试表达式最初为false,循环也将执行一次。for和while循环都是输入条件循环,而do while循环是退出条件循环。
2、如果下面的代码片段是有效程序的组成部分,它将打印什么内容?

int i;
for(i = 0;i<5;i++)
	cout<<i;
	cout<<endl;

打印
01234
3、如果下面的代码片段是有效程序的组成部分,它将打印什么内容?

int j;
for(j=0;j<11;j+=3)
	cout<<j;
cout<<endl<<j<<endl;

打印:
0369
12

4、如果下面的代码片段是有效程序的组成部分,它将打印什么内容?

int j=5;
while (++j<9)
	cout<<j++<<endl;

打印:
6
8

5、如果下面的代码片段是有效程序的组成部分,它将打印什么内容?

int k =8;
do 
	cout<<"k="<<k<<endl;
while (k++<5);

打印:
k=8
6、编写一个打印1、2、4、8、16、32、64的for循环,每轮循环都将计数变量的值乘以2

for (int num =1;num<=64;num*=2)
	cout<<num<<" ";

7、如何在循环体中包括多条语句?
答:将语句放在一对大括号中将形成一个复合语句或代码块
8、下面的语句是否有效?如果无效,原因是什么?如果有效,它将完成什么工作?

int x=(1,024);

答:有效,1,024由1和024组成,用逗号运算符连接,值为右侧表达式的值,024,八进制为20,因此该声明将该值20赋给x.
下面的语句又如何呢?

int y;
y=1,024;

答:有效,运算符优先级将其判定为:(y=1),024;
9、在查看输入方面,cin>>ch同cin.get(ch)和ch=cin.get()有什么不同?
答:cin>>ch将跳过空格、换行符和制表符,其他两种格式将读取这些字符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值