目录
注:
for循环
for循环的组成部分
1. 表达式和语句
2. 非表达式和语句
3. 修改规则
for循环的使用例 - 阶乘的计算与存储
修改循环更新的步长
使用for循环访问字符串
递增运算符(++) 和 递减运算符(--)
副作用和顺序点
前缀格式和后缀格式
递增/递减运算符和指针
组合赋值运算符
复合语句(语句块)
其他语法技巧——逗号运算符
关系表达式
赋值、比较和可能出现的错误
C-风格字符串的比较
比较string类字符串
本笔记参考:《C++ PRIMER PLUS(第6版)》
计算机除了存储数据外,还可以完成许多其他工作。而为了发挥其拥有的强大的操控能力,程序需要可执行重复操作和进行决策的工具。一般地,程序控制语句(for循环、while循环、if语句……)通过关系表达式和逻辑表达式控制其行为。
使用for循环可以令程序执行重复的任务,例如:
#include
int main()
{
using namespace std;
int i;
for (i = 0; i < 5; i++)
cout << "第 " << i + 1 << " 次循环。\n";
cout << "循环结束。\n";
return 0;
}
程序执行的结果:
【分析】
该部分使用的递增(++)运算符,作用是将操作数的值加1。
除此之外,进入该循环后,接下来执行的语句被称为 循环体 :
循环的执行逻辑如下:
由上述可知,for循环的组成部分需要完成下面这些步骤:
在C++的循环设计清晰地分出了控制部分和操作部分:
初始化表达式: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;
}
程序执行的结果是:
注意:循环结束时,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。)
对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;
}
程序执行的结果是:
【分析】
在语句 cout << (x = 100) << endl; 中,为了判断表达式 x = 100 的值,程序将100赋给了x。这种判定表达式改变了内存中的数据的值,因此,认为这种表达式是存在副作用的。(从C++的构造方式看来,赋值并不会是语句主要的作用。)
表达式可以轻易转换成语句:
(所有表达式都可以成为语句,但得到的语句不一定有编程意义。)
注意:任何表达式加上分号就可以成为语句,这是一个充分不必要条件。即
从语句中删除分号不一定能使其成为表达式,例如:
int a
但 int a 不是表达式,它没有值,也不存在类似于下面这种操作:
b = int a * 1000;
int c = for (i = 0; i < 4; i++) //同样地,for循环也不存在值
C++在C语言的基础上添加了一项特性,对for循环进行了一些调整。不同于C,C++环境下,下方的语句是合法的:
for (int i = 0; i < 5; i++)
也就是说,在C++中,可以在for循环的初始化部分进行变量的声明。
但在第2点中提到过,声明并不能算是一种表达式。曾经,为了合法化这种非法情况,C++使用了一种只能出现在for语句中的表达式——声明语句表达式。但那已经被取消了,现在,C++通过修改for循环的句法达成目的:
另一方面,这种新的语法声明的变量只会存在于for循环内部,当循环结束时,变量的生命周期也会结束。
不过较早的C++可能会把通过新语法声明的变量i判定为是在循环之前声明的,此时声明的变量在循环结束后仍可使用。
#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;
}
程序执行的结果是:
【分析】
由于 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;
}
程序执行的结果是:
当 i 的值为 108 时,循环终止。因为更新表达式可以是任意有效的表达式,所以还可以通过循环完成许多运算操作。
注意:检测不等通常比检测相等更好。比如说上述程序中,如果步长是12,那么使用条件 i == 100 就不可行,因为 i 的取值不会等于100。
使用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;
}
程序执行的结果是:
此处使用了string类存储字符串。通过将i设置为字符串的最后一个字符是索引,并使用递减(--)运算符来完成字符串的逆序打印。
在之前的程序中出现过递增和递减这两个运算符,这两个运算符都有两个变体:
这两种变体的作用是相同的,但是运算符执行的时间却有区别。
例子:
#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;
}
程序执行的结果是:
简单地说,a++ 表示先输出 a ,再执行++(递增运算符);++b 表示先执行++,再输出 b 。
尽管递增和递减运算符十分巧妙,但也会存在失去控制的情况,如:
x = 2 * x++ * (3 - ++x);
这时候,递增运算符的执行时机就会变得不明确,此时,上述语句会因为系统的不同而产生不同的结果。
完整表达式的定义:不是另一个更大表达式的子表达式。
顺序点分隔了程序,使C++在进入顺序点的下一步之前,完成对当前所有副作用的评估。
例如:
按照之前的说法,在到达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 的含义:
//接续之前的代码
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] ):
相比于之前使用的更新表达式,如: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;
}
程序执行的结果是:
如果在上述复合语句的语句块中定义一个变量,那么该变量的声明周期在离开循环时也会同时结束。这表明该变量仅在该语句块中可以被使用:
另外,如果在语句块内部申请的变量拥有和外部变量相同的变量名,那么内部新申请的变量会是当前程序使用的变量。例如:
#include
int main()
{
using namespace std;
int x = 20;
{
cout << x << endl;
int x = 10; //新申请的变量
cout << x << endl;
}
cout << x << endl;
return 0;
}
程序执行的结果是:
逗号运算符使程序能够允许将两个表达式放到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;
}
程序执行的结果是:
【分析】
程序使用了逗号运算符完成了两次初始化操作和循环的更新操作。
在上述循环的语句块中,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种关系运算符来进行数字的比较,除了数字以外,还可用于比较:
但是对于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;
}
程序执行的部分结果:
很明显,上述程序的错误发生在这里:scores[i] = 20; 这会造成不良的后果:
假设字符串数组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;
}
程序执行的结果是:
【分析】
在上述循环的测试表达式中,出现了这样的语句:strcmp(word, "mate"); 按照之前介绍的方法,这条语句应该这样写:
strcmp(word, "mate") != 0;
而程序中之所以可以那样写,是因为表达式利用了函数strcmp( )的返回值:若字符串不相等,返回值为非零(true),否则为零(false)。
另一方面,可以对字符变量进行递增、递减运算符的使用(char类型是整型),这种操作实际上将修改存储在变量中的整数编码。
在一些语言中,存储在不同长度的数组中的字符串彼此也不相等。但C语言并不这样比较,C-风格字符串是通过结尾的空值字符定义的,因此,即使两个字符串被存储在长度不同的数组中,也可能相同:
char a[10] = "Hello"; char b[20] = "Hello";
上述两个数组内存储的均是5个字符+'\0'。
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;
}
程序执行的结果和上个例子是相同的:
【分析】
在上述循环中使用了测试表达式:word != "mate";
string类重载运算符!=的使用条件:
对于string对象,也可以使用数组表示法提取其中的字符。