C++初探 5-1(for循环)

目录

注:

for循环

for循环的组成部分

1. 表达式和语句

2. 非表达式和语句 

3. 修改规则

for循环的使用例 - 阶乘的计算与存储

修改循环更新的步长

使用for循环访问字符串

递增运算符(++) 和 递减运算符(--)

副作用和顺序点

前缀格式和后缀格式

递增/递减运算符和指针

组合赋值运算符

复合语句(语句块)

其他语法技巧——逗号运算符

关系表达式

赋值、比较和可能出现的错误

C-风格字符串的比较

比较string类字符串


注:

        本笔记参考:《C++ PRIMER PLUS(第6版)》


        计算机除了存储数据外,还可以完成许多其他工作。而为了发挥其拥有的强大的操控能力,程序需要可执行重复操作和进行决策的工具。一般地,程序控制语句(for循环、while循环、if语句……)通过关系表达式逻辑表达式控制其行为。

for循环

        使用for循环可以令程序执行重复的任务,例如:

#include
int main()
{
	using namespace std;
	int i;
	for (i = 0; i < 5; i++)
		cout << "第 " << i + 1 << " 次循环。\n";
	cout << "循环结束。\n";
	return 0;
}

程序执行的结果:

C++初探 5-1(for循环)_第1张图片

【分析】

C++初探 5-1(for循环)_第2张图片

  该部分使用的递增(++)运算符,作用是将操作数的值加1。

        除此之外,进入该循环后,接下来执行的语句被称为 循环体 :

        循环的执行逻辑如下:

C++初探 5-1(for循环)_第3张图片

for循环的组成部分

        由上述可知,for循环的组成部分需要完成下面这些步骤:

  1. 设置初始值;
  2. 执行测试,判断循环是否继续进行;
  3. (若测试通过)执行循环操作;
  4. 更新用于测试的值。

        在C++的循环设计清晰地分出了控制部分和操作部分:

C++初探 5-1(for循环)_第4张图片

        初始化表达式:for循环只会执行一次初始化,通常,程序会通过初始化表达式将变量设置完毕,之后使用该变量进行循环周期的计算。

        测试表达式:该表达式决定循环的执行与否(通常,该表达式是一个关系表达式)。当测试表达式的值为 true 时,循环体就会被执行。

  实际上,C++的语法并没有限制测试表达式的值的类型,这意味着任意表达式都可以被用作测试表达式。而C++会将结果前置转换为bool类型的值。

例子:

#include
int main()
{
	using namespace std;

	cout << "请输入倒数开始的数字:";
	int limit;
	cin >> limit;

	int i = 0;
	for (i = limit; i; i--)
	{
		cout << "i = " << i << "\n";
	}
	cout << "现在i的值为 " << i << "\n";

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第5张图片

        注意:循环结束时,i = 0 。

【分析】

        for循环是入口条件循环(在每一次的循环之前, 都会计算测试表达式的值),因此,当测试表达式为false时,循环不会进行:

(此处,测试表达式在初次判定时即为false。)

        更新表达式会在每次循环结束(循环体执行完毕)时执行,类似于测试表达式,更新表达式也可以是任何有效的C++表达式,甚至于是其他的控制表达式。

关系表达式的终止值0

  在C++引入bool类型之前:

        若关系表达式为true,则判定为1;

        若关系表达式为false,则判定为0。

  在C++引入bool类型之后:

        关系表达式被判定为bool字面值,即true和false。(C++会在需要整数值时将true和false转换为1和0,在需要bool值时将0转换为false,非0转换为true。)

(ps:尽管fo语句有点类似于函数,但for是一个C++的关键字,因此编译器不会将for视为一个函数,并且会防止将函数命名为for。)

1. 表达式和语句

        对C++而言,任何值或者任何有效值与运算符的组合都是表达式。例如

表达式
10 10
28 * 20 560
22 + 27 49

        除此之外,还存在一些值不那么明显的表达式:

表达式 注释
x = 20

20

C++将赋值表达式的值定义为左侧成员的值。
maids = (cooks = 4) + 3

7

C++并不鼓励这种写法。
x = y = z = 0 0 赋值操作符是从右向左结合的。

例如:

#include
int main()
{
	using namespace std;
	int x = 0;

	cout << "表达式 x = 100 的值是 ";
	cout << (x = 100) << endl;
	cout << "此时 x = " << x << endl << endl;

	cout << "表达式 x < 3 的值是 ";
	cout << (x < 3) << endl;
	cout << "表达式 x > 3 的值是 ";
	cout << (x > 3) << endl << endl;

	cout.setf(ios_base::boolalpha);		//设置标记,命令cout输出true和false

	cout << "表达式 x < 3 的值是 ";
	cout << (x < 3) << endl;
	cout << "表达式 x > 3 的值是 ";
	cout << (x > 3) << endl << endl;

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第6张图片

【分析】

        在语句 cout << (x = 100) << endl; 中,为了判断表达式 x = 100 的值,程序将100赋给了x。这种判定表达式改变了内存中的数据的值,因此,认为这种表达式是存在副作用的(从C++的构造方式看来,赋值并不会是语句主要的作用。)

        表达式可以轻易转换成语句:

C++初探 5-1(for循环)_第7张图片

(所有表达式都可以成为语句,但得到的语句不一定有编程意义。)


2. 非表达式和语句 

        注意:任何表达式加上分号就可以成为语句,这是一个充分不必要条件。即

从语句中删除分号不一定能使其成为表达式,例如:

int a

        但 int a 不是表达式,它没有值,也不存在类似于下面这种操作:

b = int a * 1000;
int c = for (i = 0; i < 4; i++)    //同样地,for循环也不存在值

3. 修改规则

        C++在C语言的基础上添加了一项特性,对for循环进行了一些调整。不同于C,C++环境下,下方的语句是合法的:

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

        也就是说,在C++中,可以在for循环的初始化部分进行变量的声明

        但在第2点中提到过,声明并不能算是一种表达式。曾经,为了合法化这种非法情况,C++使用了一种只能出现在for语句中的表达式——声明语句表达式。但那已经被取消了,现在,C++通过修改for循环的句法达成目的:

C++初探 5-1(for循环)_第8张图片

        另一方面,这种新的语法声明的变量只会存在于for循环内部,当循环结束时,变量的生命周期也会结束。

C++初探 5-1(for循环)_第9张图片

  不过较早的C++可能会把通过新语法声明的变量i判定为是在循环之前声明的,此时声明的变量在循环结束后仍可使用。

for循环的使用例 - 阶乘的计算与存储

#include
const int ArSize = 16;					//全局变量的声明
int main()
{
	long long f[ArSize];
	f[1] = f[0] = 1LL;					//long long类型的 1

	for (int i = 2; i < ArSize; i++)	//计算
		f[i] = i * f[i - 1];

	for (int i = 0; i < ArSize; i++)	//打印
		std::cout << i << "! = " << f[i] << std::endl;

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第10张图片

【分析】

        由于 0 和 1 的阶乘都是 1,所以直接将 数组f 的前两个元素设置为 1:f[1] = f[0] = 1LL;

        上述代码演示了ArSize这种符号表示。通过这种使用const值的方式,使ArSize成为一个外部数据,可以更为方便地进行数组长度的设置(ArSize的生命周期是整个程序)

修改循环更新的步长

        通过改变更新表达式,可以改变循环更新的步长值,例如:

#include
int main()
{
	using std::cout;	//一个using声明
	using std::cin;
	using std::endl;

	cout << "请输入循环的步长:";
	int by;
	cin >> by;

	cout << "以 " << by << " 为步长进行循环:\n";
	for (int i = 0; i < 100; i = i + by)
		cout << i << endl;

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第11张图片

        当 i 的值为 108 时,循环终止。因为更新表达式可以是任意有效的表达式,所以还可以通过循环完成许多运算操作。

        注意:检测不等通常比检测相等更好。比如说上述程序中,如果步长是12,那么使用条件 i == 100 就不可行,因为 i 的取值不会等于100。

使用for循环访问字符串

        使用for循环也能够一次访问字符串中的每一个字符。例如:

  下方程序中,出现 word.size() ,这个类函数可以获得字符串中的字符数。

#include
#include
int main()
{
	using namespace std;

	cout << "请输入一个(英文)单词:";
	string word;
	cin >> word;

	cout << "按照逆序打印字符串:";
	for (int i = word.size() - 1; i >= 0; i--)
		cout << word[i];
	cout << "\n打印结束。\n";
	
	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第12张图片

        此处使用了string类存储字符串。通过将i设置为字符串的最后一个字符是索引,并使用递减(--)运算符来完成字符串的逆序打印。

递增运算符(++) 和 递减运算符(--)

        在之前的程序中出现过递增和递减这两个运算符,这两个运算符都有两个变体:

  • 前缀(prefix)版本 —— 位于操作数前面,如:++x;
  • 后缀(postfix)版本 —— 位于操作数后面,如:--x。

        这两种变体的作用是相同的,但是运算符执行的时间却有区别。

例子:

#include
int main()
{
	using std::cout;
	int a = 20;
	int b = 20;

	cout << "a   = " << a << ",  b = " << b << "\n";
	cout << "a++ = " << a++ << ",++b = " << ++b << "\n";
	cout << "a   = " << a << ",  b = " << b << "\n";

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第13张图片

        简单地说,a++ 表示先输出 a ,再执行++(递增运算符);++b 表示先执行++,再输出 b 。

        尽管递增和递减运算符十分巧妙,但也会存在失去控制的情况,如:

x = 2 * x++ * (3 - ++x);

        这时候,递增运算符的执行时机就会变得不明确,此时,上述语句会因为系统的不同而产生不同的结果。

副作用和顺序点

  • 副作用(side effect):在计算表达式时改变了某些东西(譬如:变量的值);
  • 顺序点(sequence point):是程序执行过程中的一个点(譬如:语句中的分号是一个顺序点,任何一个完整的表达式的末尾也是一个顺序点)。

  完整表达式的定义:不是另一个更大表达式的子表达式。

        顺序点分隔了程序,使C++在进入顺序点的下一步之前,完成对当前所有副作用的评估。

例如:

C++初探 5-1(for循环)_第14张图片

        按照之前的说法,在到达while循环的测试条件的末尾时,C++会遇到一个顺序点。此时C++评估之前的副作用(即guests++),因此,在该表达式的末尾,guests将被加1,再进入cout语句。

  因为此处所有的是后缀++,所以会先输出 比较的结果 ,然后再进行加1操作。

        再看下面的语句:

y = (4 + x++) + (6 + x++);

        在上述例子中,整条语句是一个完整表达式,分号表示了顺序点的位置。因此,C++只能保证在执行到下一条语句之前,对x的值会进行两次加1。

        但是C++并不能保证语句内部进行的运算过程,也就是说,x的值在何时进行递增实际上是无法确定的。在实际操作中,这种情况应该被避免。

  在C++11中,“顺序点”这个术语已经不再被使用,因为其难以描述多线程执行。取而代之的,出现了术语“顺序”。

前缀格式和后缀格式

        从逻辑上看,下方代码中的前缀格式和后缀格式并没有区别:

x++;
++x;
for (n = 1; n > 0; --n)
{
    //语句块
}

for (n = 1; n > 0; n--)
{
    //语句块
}

        这是因为上述情形中,更新表达式的值并未被使用,也就是说,上述表达式只存在副作用。无论是加1还是减1,都会在进入下一步之前完成,此时前缀格式和后缀格式的最终效果是相同的。

  但是,在面对类时,使用前缀格式或者后缀格式可能对执行速度具有细微的影响。如果针对类这样定义这些运算符:

        前缀函数:将值加1,返回结果;

        后缀函数:复制一个副本,将副本加1,返回副本。

  由此,对于类而言,前缀版本的效率就更高了。

递增/递减运算符和指针

        若将递增运算符用于指针,指针的值将会增加其指向类型确定数据类型所占用的字节数,递减运算符同理。例如:

int arr[5] = { 32, 241, 123, 16, 5 };
int* pt = arr;		//此时指针pt指向数组arr的首元素的地址,即指向arr[0],解引用得到 32
++pt;				//此时指针pt指向arr[1],解引用得到 241 

        还可以混合使用++、-- 和*运算符,此时需要注意运算符的优先级问题。

优先级 运算符 结合方向
后缀++、后缀-- 左→右
前缀++、前缀--、解引用* 右→左

        例如,解释 *++pt 的含义:

  1. 先将++应用于pt(++ 位于 * 的右边,先与 pt 结合),使 pt 的值增加一个int类型所占空间的大小;
  2. 在将*运用于pt,解引用,找到对应元素。
//接续之前的代码
int x = *++pt;    //增加指针的值,指向arr[2],解引用得到 123

---

        而 ++*pt 意味着先进行解引用,找到pt指向的值,再将该值加1:

//接续自上述代码
++*pt;        //增加指针指向的元素的值,此时pt指向arr[2],有 123 + 1 = 124

---

        而如果使用圆括号(),因为()优先级很高,所以可以通过它进行优先级的改变:

(*pt)++        //1. 进行解引用,2. 进行加1操作。此时有 124 + 1 = 125

---

        再看看后缀运算符:

x = *pt++;

        此处因为后缀运算符的优先级更高,所以应该先执行该运算符,但是后缀运算符的执行时机却是在语句结束之后。这意味着上述语句的执行结果应该是(假设之前 指针pt 指向 arr[2] ):

  1. x 的值被赋成 arr[2] ,即 25.4;
  2. 该语句执行完毕后,指针pt 指向 arr[3] 。

组合赋值运算符

        相比于之前使用的更新表达式,如:i = i + by; ,存在一种合并了加/减(乘/除/取模)和赋值操作的运算符,例如:

i += by;    //和 i = i + by 作用相同

        这种操作符同样可以被使用于变量、数组元素、结构成员或者被解引用的数据:

int main()
{
	//变量
	int k = 5;
	k += 3;					//k的值变为 8

	//数组
	int* pa = new int[10];	//pa指向 pa[0] 所在位置
	pa[4] = 12;
	pa[4] += 6;				//pa[4]的值变为 18

	//解引用
	*(pa + 4) += 7;			//pa[4]的值变为 25

	//指针
	pa += 2;				//指针pa指向 pa[2] 所在位置

	return 0;
}
操作符 作用(L-左操作数,R-右操作数)
*= 将 L * R 赋给 L
/= 将 L / R 赋给 L
%= 将 L % R 赋给 L
+= 将 L + R 赋给 L
-= 将 L - R 赋给 L

复合语句(语句块)

        for语句中,可以使用花括号来构造一条复合语句(语句块),代码由一对花括号和其包含的语句组成,它们被视为一条语句,例如:

#include
int main()
{
	using namespace std;

	cout << "请输入5个数字:\n";
	double number;
	double sum = 0.0;
	for (int i = 1; i <= 5; i++)
	{
		cout << "第" << i << "个数字:";
		cin >> number;
		sum += number;
	}

	cout << endl << "5个数字之和 = " << sum << endl;
	cout << "5个数字的平均值 = " << sum / 5 << endl;

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第15张图片

        如果在上述复合语句的语句块中定义一个变量,那么该变量的声明周期在离开循环时也会同时结束。这表明该变量仅在该语句块中可以被使用:

C++初探 5-1(for循环)_第16张图片

        另外,如果在语句块内部申请的变量拥有和外部变量相同的变量名,那么内部新申请的变量会是当前程序使用的变量。例如:

#include
int main()
{
	using namespace std;
	int x = 20;
	{
		cout << x << endl;
		int x = 10;			//新申请的变量
		cout << x << endl;
	}
	cout << x << endl;

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第17张图片

其他语法技巧——逗号运算符

        逗号运算符使程序能够允许将两个表达式放到C++语法只允许放一个表达式的位置。例如:

++j, --i

        但是,逗号不一定就是逗号运算符,在进行声明操作时,逗号负责将变量列表中相邻的名称分开:

int i, j;

使用例:将字符串逆序

#include
#include
int main()
{
	using namespace std;
	cout << "请输入一个单词(英文):";
	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 << "\n程序执行完毕。\n";

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第18张图片

【分析】

        程序使用了逗号运算符完成了两次初始化操作和循环的更新操作。

        在上述循环的语句块中,i 和 j 分别定位到了(字符串)数组的第一个和最后一个元素,然后分别向中间进行索引,直到 j < i 不成立为止,完成了整个字符串的遍历。

        需要注意的是:

char temp;
int i, j;
for (j = 0, i = word.size() - 1; j < i; --i, ++j)

        此处变量 temp 的声明 和 变量 i、j 的声明是分开的。因为这是两个不同类型的声明,如果一定要加在一起(char temp, int j, i;),会报错:

        不过在使用逗号表达式声明两个变量的同时,也可以对被声明的变量进行初始化(不过会比较乱)

int j = 0, i = word.size() - 1;

此时逗号只是一个列表分隔符,而不是逗号运算符。

        最后,可以在for循环的内部进行变量temp的声明:

char temp = word[i];

但因为每一次进入循环和结束循环时都会对变量temp进行分配和释放,所以这种方式其实要比在for循环之前声明temp更慢一些。

逗号运算符的一些相关知识

  1. 逗号运算符是一个顺序点,这意味着它会先确保第一个表达式计算完毕,再进行第二个表达式的计算(逗号表达式的运算顺序是从左到右的),例如这种表达式,它是安全的:

i = 20, j = 2 * i;    //将i设为20,j变为40

  2. 逗号表达式的值是其第二部分的值,例如上述表达式的值就是 j = 2 * i 的值,即 40 ;

  3. 逗号表达式拥有所有运算符中最低的优先级。

关系表达式

        C++提供了6种关系运算符来进行数字的比较,除了数字以外,还可用于比较:

  • 字符(提供ASCII码进行表示);
  • string对象;
  • ……

        但是对于C-风格字符串而言,这种运算符是不适用的。

        对于关系表达式,如果比较结果为真,则返回true,否则为false(在较老的实现中,关系表达式返回 1 表示true,返回 0 表示false)。

优先级 运算符 结合性 作用
< L-R 小于
<= 小于等于
>= 大于等于
> 大于
== L-R 等于
!= 不等于

使用例:

for (x = 20; x > 5; x--);			//循环,直到 x 小于等于 5
for (x = 1; y != x; ++x);			//循环,直到 y 等于 x
for (cin >> x; x == 0; cin >> x);	//循环,直到 x 接受的值为 0

        同时,因为关系运算符的优先级低于算术运算符,因此表达式

x + 3 > y - 2;

等价于

(x + 3) > (y - 2);

赋值、比较和可能出现的错误

赋值表达式(=)和等于表达式(==)的错误使用例:

#include
int main()
{
	using namespace std;
	int scores[10] = { 20, 20, 20, 20, 20, 19, 20, 18, 20, 20 };

	int i;
	cout << "正确的操作符使用:\n";
	for (i = 0; scores[i] == 20; i++)        //使用了等于运算符
		cout << "scores " << i << "是20\n";

	cout << "\n错误的操作符使用:\n";
	for (i = 0; scores[i] = 20; i++)         //使用了赋值运算符
		cout << "scores " << i << "是20\n";

	return 0;
}

程序执行的部分结果:

C++初探 5-1(for循环)_第19张图片

        很明显,上述程序的错误发生在这里:scores[i] = 20; 这会造成不良的后果:

  1. 因为该操作将一个非零值赋值给了数组元素,所以该表达式的值始终非零,即为 true ;
  2. 该操作实际上修改了数组元素的值;
  3. 由于循环无法终止,所以程序的运行会超出数组的范围,即发生非法访问

C-风格字符串的比较

        假设字符串数组word,如果存在以下代码:

word = "mate"

        此处需要注意:

  • 数组名是数组的地址;
  • 用引号引起的字符串常量也代表着地址。

        因此,上述代码的含义应该是比较二者的地址是否相同(而即使二者包含相同的字符,它们的地址也是不同的)。

        不过字符可以通过关系运算符进行比较(字符实际上也是整型)。

        对于这种C-风格字符串而言,应该通过库函数 strcmp( ) 进行比较:

该函数会一个一个字符地进行比较,而这种比较是依赖字符的系统编码的。总结这种比较的返回值(参考Cplusplus):

返回值x 对应情况
x < 0 第一个不匹配的字符在 str1 中的值(系统编码)比在 str2 中的
x = 0 两个字符串的内容是相等的
x > 0 第一个不匹配的字符在 str1 中的值(系统编码)比在 str2 中的

 使用例

#include
#include			//包含函数strcmp()
int main()
{
	using namespace std;
	char word[5] = "?ate";

	for (char ch = 'a';  strcmp(word, "mate"); ch++)
	{
		cout << word << endl;
		word[0] = ch;
	}
	cout << "循环结束,word = " << word << endl;

	return 0;
}

程序执行的结果是:

C++初探 5-1(for循环)_第20张图片

【分析】

        在上述循环的测试表达式中,出现了这样的语句:strcmp(word, "mate"); 按照之前介绍的方法,这条语句应该这样写:

strcmp(word, "mate") != 0;

        而程序中之所以可以那样写,是因为表达式利用了函数strcmp( )的返回值:若字符串不相等,返回值为非零(true),否则为零(false)。

        另一方面,可以对字符变量进行递增、递减运算符的使用(char类型是整型),这种操作实际上将修改存储在变量中的整数编码。

  在一些语言中,存储在不同长度的数组中的字符串彼此也不相等。但C语言并不这样比较,C-风格字符串是通过结尾的空值字符定义的,因此,即使两个字符串被存储在长度不同的数组中,也可能相同:

char a[10] = "Hello";
char b[20] = "Hello";

  上述两个数组内存储的均是5个字符+'\0'

比较string类字符串

        string类字符串的比较相对而言要简单一些,可以使用关系运算符进行比较(因为类函数重载)。

使用例

#include
#include
int main()
{
	using namespace std;
	string word = "?ate";
	for (char ch = 'a'; word != "mate"; ch++)
	{
		cout << word << endl;
		word[0] = ch;
	}
	cout << "循环结束,word = " << word << endl;

	return 0;
}

程序执行的结果和上个例子是相同的:

C++初探 5-1(for循环)_第21张图片

【分析】

        在上述循环中使用了测试表达式:word != "mate";

        string类重载运算符!=的使用条件:

  • 至少有一个操作数是string对象;
  • 另一个操作数可以是string对象,或者C-风格字符串。

  对于string对象,也可以使用数组表示法提取其中的字符。

你可能感兴趣的:(C++笔记,c++,开发语言)