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


    目录

    注:

    for循环

    for循环的组成部分

    1. 表达式和语句

    2. 非表达式和语句 

    3. 修改规则

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

    修改循环更新的步长

    使用for循环访问字符串

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

    副作用和顺序点

    前缀格式和后缀格式

    递增/递减运算符和指针

    组合赋值运算符

    复合语句(语句块)

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

    关系表达式

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

    C-风格字符串的比较

    比较string类字符串


    注:

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


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

    for循环

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

    1. #include
    2. int main()
    3. {
    4. using namespace std;
    5. int i;
    6. for (i = 0; i < 5; i++)
    7. cout << "第 " << i + 1 << " 次循环。\n";
    8. cout << "循环结束。\n";
    9. return 0;
    10. }

    程序执行的结果:

    【分析】

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

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

            循环的执行逻辑如下:

    for循环的组成部分

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

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

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

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

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

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

    例子:

    1. #include
    2. int main()
    3. {
    4. using namespace std;
    5. cout << "请输入倒数开始的数字:";
    6. int limit;
    7. cin >> limit;
    8. int i = 0;
    9. for (i = limit; i; i--)
    10. {
    11. cout << "i = " << i << "\n";
    12. }
    13. cout << "现在i的值为 " << i << "\n";
    14. return 0;
    15. }

    程序执行的结果是:

            注意:循环结束时,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++而言,任何值或者任何有效值与运算符的组合都是表达式。例如

    表达式
    1010
    28 * 20560
    22 + 2749

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

    表达式注释
    x = 20

    20

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

    7

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

    例如:

    1. #include
    2. int main()
    3. {
    4. using namespace std;
    5. int x = 0;
    6. cout << "表达式 x = 100 的值是 ";
    7. cout << (x = 100) << endl;
    8. cout << "此时 x = " << x << endl << endl;
    9. cout << "表达式 x < 3 的值是 ";
    10. cout << (x < 3) << endl;
    11. cout << "表达式 x > 3 的值是 ";
    12. cout << (x > 3) << endl << endl;
    13. cout.setf(ios_base::boolalpha); //设置标记,命令cout输出true和false
    14. cout << "表达式 x < 3 的值是 ";
    15. cout << (x < 3) << endl;
    16. cout << "表达式 x > 3 的值是 ";
    17. cout << (x > 3) << endl << endl;
    18. return 0;
    19. }

    程序执行的结果是:

    【分析】

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

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

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


    2. 非表达式和语句 

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

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

    int a

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

    1. b = int a * 1000;
    2. 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循环的句法达成目的:

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

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

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

    1. #include
    2. const int ArSize = 16; //全局变量的声明
    3. int main()
    4. {
    5. long long f[ArSize];
    6. f[1] = f[0] = 1LL; //long long类型的 1
    7. for (int i = 2; i < ArSize; i++) //计算
    8. f[i] = i * f[i - 1];
    9. for (int i = 0; i < ArSize; i++) //打印
    10. std::cout << i << "! = " << f[i] << std::endl;
    11. return 0;
    12. }

    程序执行的结果是:

    【分析】

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

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

    修改循环更新的步长

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

    1. #include
    2. int main()
    3. {
    4. using std::cout; //一个using声明
    5. using std::cin;
    6. using std::endl;
    7. cout << "请输入循环的步长:";
    8. int by;
    9. cin >> by;
    10. cout << "以 " << by << " 为步长进行循环:\n";
    11. for (int i = 0; i < 100; i = i + by)
    12. cout << i << endl;
    13. return 0;
    14. }

    程序执行的结果是:

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

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

    使用for循环访问字符串

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

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

    1. #include
    2. #include
    3. int main()
    4. {
    5. using namespace std;
    6. cout << "请输入一个(英文)单词:";
    7. string word;
    8. cin >> word;
    9. cout << "按照逆序打印字符串:";
    10. for (int i = word.size() - 1; i >= 0; i--)
    11. cout << word[i];
    12. cout << "\n打印结束。\n";
    13. return 0;
    14. }

    程序执行的结果是:

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

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

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

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

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

    例子:

    1. #include
    2. int main()
    3. {
    4. using std::cout;
    5. int a = 20;
    6. int b = 20;
    7. cout << "a = " << a << ", b = " << b << "\n";
    8. cout << "a++ = " << a++ << ",++b = " << ++b << "\n";
    9. cout << "a = " << a << ", b = " << b << "\n";
    10. return 0;
    11. }

    程序执行的结果是:

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

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

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

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

    副作用和顺序点

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

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

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

    例如:

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

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

            再看下面的语句:

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

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

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

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

    前缀格式和后缀格式

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

    1. x++;
    2. ++x;
    1. for (n = 1; n > 0; --n)
    2. {
    3. //语句块
    4. }
    5. for (n = 1; n > 0; n--)
    6. {
    7. //语句块
    8. }

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

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

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

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

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

    递增/递减运算符和指针

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

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

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

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

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

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

    ---

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

    1. //接续自上述代码
    2. ++*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 作用相同

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

    1. int main()
    2. {
    3. //变量
    4. int k = 5;
    5. k += 3; //k的值变为 8
    6. //数组
    7. int* pa = new int[10]; //pa指向 pa[0] 所在位置
    8. pa[4] = 12;
    9. pa[4] += 6; //pa[4]的值变为 18
    10. //解引用
    11. *(pa + 4) += 7; //pa[4]的值变为 25
    12. //指针
    13. pa += 2; //指针pa指向 pa[2] 所在位置
    14. return 0;
    15. }
    操作符作用(L-左操作数,R-右操作数)
    *=将 L * R 赋给 L
    /=将 L / R 赋给 L
    %=将 L % R 赋给 L
    +=将 L + R 赋给 L
    -=将 L - R 赋给 L

    复合语句(语句块)

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

    1. #include
    2. int main()
    3. {
    4. using namespace std;
    5. cout << "请输入5个数字:\n";
    6. double number;
    7. double sum = 0.0;
    8. for (int i = 1; i <= 5; i++)
    9. {
    10. cout << "第" << i << "个数字:";
    11. cin >> number;
    12. sum += number;
    13. }
    14. cout << endl << "5个数字之和 = " << sum << endl;
    15. cout << "5个数字的平均值 = " << sum / 5 << endl;
    16. return 0;
    17. }

    程序执行的结果是:

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

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

    1. #include
    2. int main()
    3. {
    4. using namespace std;
    5. int x = 20;
    6. {
    7. cout << x << endl;
    8. int x = 10; //新申请的变量
    9. cout << x << endl;
    10. }
    11. cout << x << endl;
    12. return 0;
    13. }

    程序执行的结果是:

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

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

    ++j, --i

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

    int i, j;

    使用例:将字符串逆序

    1. #include
    2. #include
    3. int main()
    4. {
    5. using namespace std;
    6. cout << "请输入一个单词(英文):";
    7. string word;
    8. cin >> word;
    9. char temp;
    10. int i, j;
    11. for (j = 0, i = word.size() - 1; j < i; --i, ++j)
    12. {
    13. temp = word[i];
    14. word[i] = word[j];
    15. word[j] = temp;
    16. }
    17. cout << word << "\n程序执行完毕。\n";
    18. return 0;
    19. }

    程序执行的结果是:

    【分析】

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

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

            需要注意的是:

    1. char temp;
    2. int i, j;
    3. 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等于
    !=不等于

    使用例:

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

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

    x + 3 > y - 2;

    等价于

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

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

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

    1. #include
    2. int main()
    3. {
    4. using namespace std;
    5. int scores[10] = { 20, 20, 20, 20, 20, 19, 20, 18, 20, 20 };
    6. int i;
    7. cout << "正确的操作符使用:\n";
    8. for (i = 0; scores[i] == 20; i++) //使用了等于运算符
    9. cout << "scores " << i << "是20\n";
    10. cout << "\n错误的操作符使用:\n";
    11. for (i = 0; scores[i] = 20; i++) //使用了赋值运算符
    12. cout << "scores " << i << "是20\n";
    13. return 0;
    14. }

    程序执行的部分结果:

            很明显,上述程序的错误发生在这里: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 中的

     使用例

    1. #include
    2. #include //包含函数strcmp()
    3. int main()
    4. {
    5. using namespace std;
    6. char word[5] = "?ate";
    7. for (char ch = 'a'; strcmp(word, "mate"); ch++)
    8. {
    9. cout << word << endl;
    10. word[0] = ch;
    11. }
    12. cout << "循环结束,word = " << word << endl;
    13. return 0;
    14. }

    程序执行的结果是:

    【分析】

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

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

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

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

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

    1. char a[10] = "Hello";
    2. char b[20] = "Hello";

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

    比较string类字符串

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

    使用例

    1. #include
    2. #include
    3. int main()
    4. {
    5. using namespace std;
    6. string word = "?ate";
    7. for (char ch = 'a'; word != "mate"; ch++)
    8. {
    9. cout << word << endl;
    10. word[0] = ch;
    11. }
    12. cout << "循环结束,word = " << word << endl;
    13. return 0;
    14. }

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

    【分析】

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

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

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

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

  • 相关阅读:
    Net 高级调试之十二:垃圾回收机制以及终结器队列、对象固定
    《统计学习方法》 第4章 朴素贝叶斯法
    【每日一题】ABC301D - Bitmask | 贪心 | 简单
    APS计划排程在半导体行业的应用
    2023年【煤炭生产经营单位(安全生产管理人员)】证考试及煤炭生产经营单位(安全生产管理人员)模拟考试题库
    【RTOS训练营】I2C和UART知识和预习安排 + 晚课提问
    ElasticSearch7.3学习(六)----文档(document)内部机制详解
    Springboot毕设项目购票网站79k27(java+VUE+Mybatis+Maven+Mysql)
    【小尘送书-第八期】《小团队管理:如何轻松带出1+1>2的团队》
    shiro550复现环境搭建
  • 原文地址:https://blog.csdn.net/w_pab/article/details/127781886