跳转到内容

Lesson 05: 从 1 加到 100 CNB

练习任务

编写一个 C 程序,计算从 1 加到 100 的和,并打印结果。然后扩展:只计算偶数的和。

期望输出:

sum = 5050
sum = 2550

提示:使用 for 循环从 0 遍历到 100,用一个累加变量 sum 在每次循环中加上当前数字。累加器需要先初始化为 0,然后用 += 运算符累加。扩展任务中,用 continue 跳过奇数。思考:如果用数学公式 n*(n+1)/2 一步算出结果,和循环的方法有什么本质区别?

核心知识点

  • for 循环进阶 — 三段式的灵活性、省略各部分、逗号运算符管理多变量
  • 复合赋值运算符 +=-=*=/=%= — 完整列表与求值语义(左操作数只求值一次)
  • 累加器模式 — sum(求和)、product(累乘)、count(计数)、min/max(极值)
  • 数学单位元 — 加法单位元 0、乘法单位元 1、极值的初始化策略
  • 高斯求和公式 — n*(n+1)/2 的数学推导与证明
  • O(n) 循环 vs O(1) 公式 — 算法复杂度的本质差异
  • 整数溢出 — int 值范围、INT_MAX 边界、long long 扩展
  • 循环变量生命周期 — 退出后 i 的终值、C89 vs C99 声明差异
  • continuebreak — for/while 中的行为差异、避免死循环
  • 嵌套循环 — sum of sums、迭代次数计算
  • 循环展开 — 减少分支指令、以空间换时间的优化思想
  • 时间复杂度入门 — O(1)、O(n)、O(n²) 的直观理解
  • 代码计时 — clock() 测量循环性能
  • 有符号与无符号整数 — 溢出行为差异与陷阱
  • 编译器优化 — 常量折叠、gcc -O2 识别高斯公式
  • 循环调试 — 打印中间值观察累加过程

代码框架

sum_1_to_100.c
c
#include <stdio.h>

int main(void)
{
    int sum = /* 初始值 */;

    for (int i = /* 起始值 */; i /* 条件 */ 100; i++)
    {
        // 在这里将 i 累加到 sum
    }

    printf("sum = %d\n", sum);

    return 0;
}

填充关键思考:sum 的初始值应该是多少(0 还是 1)?i 从 0 还是 1 开始?条件用 < 还是 <=?如何用 += 累加?

TIP

先不要往下翻看参考解答。完成基本任务后,尝试扩展:能否改为只计算偶数的和?能否不修改循环体而通过修改 for 头部实现?如果求和范围变成 1 到 100000,int 还够用吗?


深度讲解

1. for 循环进阶:三段式的灵活性与高级用法

1.1 基本形式回顾

回顾 Lesson 03 中学过的 for 循环:

c
for (初始化表达式; 条件表达式; 更新表达式) {
    // 循环体
}

for 完全等价于以下 while 循环:

c
初始化表达式;
while (条件表达式) {
    // 循环体
    更新表达式;
}

关键细节:更新表达式在循环体之后执行——这意味着 continue 跳过循环体后,更新表达式仍然会执行(这与 while 中的行为不同,后文详述)。

1.2 三段式的灵活性

for 的三个表达式都是可选的

c
// 1. 省略初始化(变量已在外定义)
int i = 0;
for (; i < 10; i++) { ... }

// 2. 省略条件(无限循环,需在循环体内 break)
for (int i = 0; ; i++) {
    if (i >= 10) break;
    ...
}

// 3. 省略更新(在循环体内手动更新)
for (int i = 0; i < 10; ) {
    ...
    i++;
}

// 4. 全部省略(经典无限循环)
for (;;) {
    // 等价于 while(1) { ... }
}

NOTE

for(;;) 是 C 语言中表达无限循环的惯用写法,比 while(1) 更「地道」——因为 for 天然适合表达「无终止条件」的语义。某些编译器会对 while(1) 发出「条件恒为真」的警告,而 for(;;) 不会。

1.3 逗号运算符:在一个 for 中管理多个变量

C 语言的逗号运算符 , 允许你在 for 的初始化和更新表达式中同时操作多个变量:

for_comma.c
c
#include <stdio.h>

int main(void)
{
    // 同时管理 i 和 j:i 递增,j 递减
    for (int i = 0, j = 100; i <= 100; i++, j--)
    {
        printf("i = %3d, j = %3d, i + j = %d\n", i, j, i + j);
    }

    return 0;
}

输出片段:

i =   0, j = 100, i + j = 100
i =   1, j =  99, i + j = 100
...
i = 100, j =   0, i + j = 100

逗号运算符的语义

  • 从左到右依次求值每个子表达式
  • 整个逗号表达式的结果是最右边子表达式的值
  • i++, j-- 先执行 i++,再执行 j--,整体值是 j-- 的结果
c
int x = (1, 2, 3);    // x = 3,只有最后一个值生效
int y = (i++, i * 2); // 先 i++,再计算 i * 2 作为 y 的值

WARNING

不要混淆逗号运算符和函数参数中的逗号分隔符。f(a, b) 中的逗号是分隔符,求值顺序未定义;(a, b) 中的逗号是运算符,保证从左到右求值。

1.4 多变量累加:同时追踪 sum 和 count

for_multi_var.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;
    int count = 0;

    // i 用于遍历,count 用于计数(实际迭代次数)
    for (int i = 0; i <= 100; i++, count++)
    {
        sum += i;
    }

    printf("sum = %d, count = %d\n", sum, count);
    // 输出: sum = 5050, count = 101

    return 0;
}

注意 count = 101——因为 i 从 0 到 100 共 101 次迭代。循环变量和计数值并不总是相同。

1.5 C99 自动变量与作用域

c
for (int i = 0; i <= 100; i++) {  // C99:在 for 头部声明 i
    sum += i;
}
// 这里 i 不再可见——作用域仅限 for 循环

for 头部声明的变量称为自动变量(automatic variable),其作用域仅限于 for 循环体(包括条件表达式和更新表达式)。循环结束后,i 不再可访问。

C89/C90 的限制:旧标准不允许在 for 头部声明变量——所有变量必须在代码块开头声明。现代 C 编程通常使用 C99 或更新的标准(gcc -std=c99-std=c11)。

c
// C89 写法:变量在块开头声明
int i;
for (i = 0; i <= 100; i++) {
    sum += i;
}
// i 在此处仍然可见!

IMPORTANT

C89 中 i 的作用域延伸到 for 所在的整个代码块,而 C99 中 i 的作用域仅限于 for 循环。这是两种标准的重要差异。优先使用 C99 的 for (int i = ...) 写法——限制作用域可以避免变量名冲突。只在确实需要终值时才在 for 外声明。


2. 复合赋值运算符:从 += 到完整体系

2.1 完整列表

复合赋值运算符是「运算 + 赋值」的缩写形式:

运算符等价形式示例含义
+=a = a + bsum += i累加
-=a = a - bcount -= 1递减
*=a = a * bproduct *= i累乘
/=a = a / bhalf /= 2除后赋值
%=a = a % brem %= 10取模后赋值
&=a = a & bflags &= mask位与后赋值
|=a = a | bflags |= bit位或后赋值
^=a = a ^ bcode ^= key位异或后赋值
<<=a = a << bval <<= 2左移后赋值
>>=a = a >> bval >>= 1右移后赋值

2.2 求值语义:左操作数只求值一次

复合赋值不仅更简洁,还有一个重要的语义差异——左操作数只求值一次

c
array[get_index()] += 1;                         // get_index() 只调用一次
array[get_index()] = array[get_index()] + 1;      // get_index() 调用两次!

如果 get_index() 每次返回不同值,展开写法会写入错误的位置。复合赋值避免了这种隐患。

WARNING

这不是性能优化,而是语义保障。当左操作数含有副作用(如函数调用、自增运算)时,复合赋值与展开写法的行为可能不同。

2.3 运算符优先级与结合性

复合赋值运算符的优先级与普通赋值 = 相同——非常低:

c
a *= b + c;    // 等价于 a = a * (b + c),而不是 a = (a * b) + c
a += b << 2;   // 等价于 a = a + (b << 2),因为 << 优先级低于 +?不,<< 高于 +=

优先级从高到低:算术运算 > 移位运算 > 关系运算 > 位运算 > 逻辑运算 > 赋值运算(含复合赋值)。

结合性:所有赋值运算符都是右结合的:

c
a += b += c;   // 等价于 b += c; a += b;

TIP

尽管复合赋值运算符很简洁,嵌套使用 a += b += c 会严重降低可读性。每个表达式只用一个复合赋值是最佳实践。


3. 累加器模式:从数学单位元到编程范式

3.1 基本模式

累加器是一种经典编程模式——在循环中逐步累积计算结果:

累加器 = 初始值;           // 必须是操作的单位元
for (每个元素) {
    累加器 = 累加器 当前元素;   // 可以是 +、*& 
}
// 累加器现在包含最终结果

3.2 为什么初始值是 0——单位元的数学原理

单位元(identity element):对运算 ,满足 x ⊕ e = x 的值 e

  • 加法的单位元是 0x + 0 = x
  • 乘法的单位元是 1x × 1 = x
  • 字符串拼接的单位元是空串 ""str + "" = str
  • 位或的单位元是 0x | 0 = x
  • 位与的单位元是全 1:x & 0xFF... = x

因此:

  • 累加(求和)→ 初始值 0
  • 累乘(阶乘)→ 初始值 1
c
// 累加 → 初始值 0(加法单位元)
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;

// 累乘 → 初始值 1(乘法单位元)
int product = 1;
for (int i = 1; i <= n; i++) product *= i;

如果初始值错误

c
int sum = 1;  // 不是加法单位元
for (int i = 1; i <= 100; i++) sum += i;
// 结果:5050 + 1 = 5051(错误!多加了初始值)

3.3 本例的累加过程

迭代isum(更新前)sum += isum(更新后)
1000 + 00
2100 + 11
3211 + 23
4333 + 36
...............
10110049504950 + 1005050

3.4 其他累加器变体:count、min、max

累加器模式不仅限于数值累加,它是一个通用框架:

accumulator_patterns.c
c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    int arr[] = {42, 17, 89, 5, 73, 28, 91, 10};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 1. 求和 (sum):初始值为 0(加法单位元)
    int sum = 0;
    for (int i = 0; i < n; i++) sum += arr[i];

    // 2. 累乘 (product):初始值为 1(乘法单位元)
    long long product = 1;
    for (int i = 0; i < n; i++) product *= arr[i];

    // 3. 计数 (count):初始值为 0(加法单位元)
    int count = 0;
    for (int i = 0; i < n; i++)
        if (arr[i] > 50) count++;

    // 4. 最大值 (max):初始值为 INT_MIN 或第一个元素
    int max = INT_MIN;
    for (int i = 0; i < n; i++)
        if (arr[i] > max) max = arr[i];

    // 5. 最小值 (min):初始值为 INT_MAX 或第一个元素
    int min = INT_MAX;
    for (int i = 0; i < n; i++)
        if (arr[i] < min) min = arr[i];

    printf("sum = %d\n", sum);               // 355
    printf("count (>50) = %d\n", count);     // 3
    printf("max = %d\n", max);               // 91
    printf("min = %d\n", min);               // 5

    return 0;
}

TIP

累加器初始值的选择原则:选择操作的单位元。如果找不到单位元(如找最大值),就用第一个元素作为初始值,或者用 INT_MIN / INT_MAX(需 #include <limits.h>)。

3.5 min/max 初始化的两种策略对比

max_init_strategies.c
c
// 策略 A:用 INT_MIN(需要 <limits.h>)
int max = INT_MIN;
for (int i = 0; i < n; i++)
    if (arr[i] > max) max = arr[i];
// 优点:空数组时返回 INT_MIN(有定义)
// 缺点:需要额外的头文件,INT_MIN 不是真正的"单位元"

// 策略 B:用第一个元素
int max = arr[0];
for (int i = 1; i < n; i++)
    if (arr[i] > max) max = arr[i];
// 优点:无需额外头文件,结果永远来自数组
// 缺点:空数组时 arr[0] 越界(未定义行为)
策略适用场景风险
INT_MIN可能为空的数组<limits.h>,语义略抽象
arr[0]保证非空的数组空数组会越界

4. 高斯求和公式:数学之美与算法思维

4.1 故事与数学推导

传说高斯(Carl Friedrich Gauss)小学时,老师让全班计算 1 + 2 + 3 + ... + 100。年幼的高斯很快写出答案 5050,他的思路是:

  1 +   2 +   3 + ... +  98 +  99 + 100
+ 100 + 99 +  98 + ... +   3 +   2 +   1
-----------------------------------------
  101 + 101 + 101 + ... + 101 + 101 + 101

将原序列和反向序列相加,得到 100 对 (n+1),即 100 × 101 = 10100。因为加了两次,除以 2 得到 5050

一般化推导

sum(1..n) = 1 + 2 + 3 + ... + (n-2) + (n-1) + n

将第一项和最后一项配对:  1 + n = n + 1
将第二项和倒数第二项配对: 2 + (n-1) = n + 1
...
共有 n/2 对,每对和为 n+1

 n 为偶数时:sum = (n/2) × (n+1) = n × (n+1) / 2
 n 为奇数时:中间项为 (n+1)/2,但仍适用 n(n+1)/2

验证 n=5: 5×6/2 = 15 = 1+2+3+4+5
验证 n=100: 100×101/2 = 5050

NOTE

高斯公式对任意非负整数 n 都成立——无论 n 是奇数还是偶数。这是因为 n(n+1) 必然是偶数(两个连续整数必有一个是偶数),所以整数除法不会产生余数。

4.2 数学归纳法证明

对于 sum(1..n) = n(n+1)/2

基础步骤 (n=1):1 = 1 × 2 / 2 = 1

归纳步骤:假设公式对 k 成立,即 sum(1..k) = k(k+1)/2。 对于 k+1

sum(1..k+1) = sum(1..k) + (k+1)
            = k(k+1)/2 + (k+1)
            = (k(k+1) + 2(k+1)) / 2
            = (k+1)(k+2) / 2
            = (k+1)((k+1)+1) / 2

恰好是公式在 n=k+1 的形式。由数学归纳法,公式对所有正整数 n 成立。

4.3 O(n) 循环 vs O(1) 公式

对比两种实现方式:

gauss_comparison.c
c
#include <stdio.h>

int main(void)
{
    int n = 100;

    // 方法 1:循环累加 — O(n) 时间复杂度
    int sum_loop = 0;
    for (int i = 1; i <= n; i++)
        sum_loop += i;

    // 方法 2:数学公式 — O(1) 时间复杂度
    int sum_formula = n * (n + 1) / 2;

    printf("Loop:    sum = %d\n", sum_loop);     // 5050
    printf("Formula: sum = %d\n", sum_formula);  // 5050

    return 0;
}
维度循环累加 O(n)数学公式 O(1)
时间复杂度O(n) — 需要迭代 n 次O(1) — 一次计算
空间复杂度O(1)O(1)
可读性直观:逐步累加需知公式
适用性可累加任意序列只适用于连续整数
n=100 时101 次加法1 次乘法 + 1 次除法
n=10^9 时10 亿次加法(秒级)1 次计算(纳秒级)

IMPORTANT

这个对比揭示了计算机科学的核心原则:选择合适的算法比微观优化代码更重要。O(n) 和 O(1) 的差距在小数据量时不明显,但数据量增长时差距呈线性扩大。对入门者而言,循环累加的价值在于演示 for 和累加器的基本用法——这才是本课的重点。

4.4 公式的局限性

高斯公式虽然快,但只适用于连续整数。考虑以下情况:

gauss_limitations.c
c
#include <stdio.h>

int main(void)
{
    // 场景 1:不是从 1 开始
    // sum(m..n) = sum(1..n) - sum(1..m-1)
    //           = n(n+1)/2 - (m-1)m/2
    int m = 50, n = 100;
    int sum_range = n * (n + 1) / 2 - (m - 1) * m / 2;
    printf("Sum from %d to %d = %d\n", m, n, sum_range);

    // 场景 2:只加偶数
    // sum_even(1..n) = 2 + 4 + ... + n (n 为偶数)
    //                 = 2 × (1 + 2 + ... + n/2)
    //                 = 2 × (n/2)(n/2+1)/2 = (n/2)(n/2+1)
    n = 100;
    int sum_even = (n / 2) * (n / 2 + 1);
    printf("Sum of evens to %d = %d\n", n, sum_even);  // 2550

    // 场景 3:任意序列 [42, 17, 89, 5, ...] — 没有公式,只能循环
    return 0;
}

5. 整数溢出:当累加超出 int 的边界

5.1 int 的值范围

1 + 2 + ... + 100 = 5050,在 int 范围内安全。但如果求和范围更大呢?

int 在 32 位系统上的值范围(<limits.h> 中定义):

INT_MIN  = -2,147,483,648  (即 -2^31)
INT_MAX  =  2,147,483,647  (即 2^31 - 1)

溢出临界点

sum(1..n) = n(n+1)/2

 n = 65535: sum = 65535 × 65536 / 2 = 2,147,450,880 安全(< INT_MAX)
 n = 65536: sum = 65536 × 65537 / 2 = 2,147,516,416 安全(恰好 < INT_MAX)
 n = 65537: sum = 65537 × 65538 / 2 = 2,147,839,936 溢出!> INT_MAX)

WARNING

整数溢出是 C 语言中最隐蔽的错误之一——程序不会崩溃,只是结果错误。永远考虑你的数据范围是否在 int 的安全边界内。有符号整数溢出在 C 标准中是未定义行为(Undefined Behavior)。

5.2 有符号整数溢出 vs 无符号整数溢出

有符号和无符号整数的溢出行为有本质区别:

overflow_demo.c
c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    // 有符号整数溢出 → 未定义行为
    int a = INT_MAX;
    // int b = a + 1;  // 未定义行为!编译器可以优化掉或产生任意结果

    // 无符号整数溢出 → 定义良好的模运算(wraparound)
    unsigned int u = UINT_MAX;
    unsigned int v = u + 1;
    printf("UINT_MAX = %u\n", u);     // 4294967295
    printf("UINT_MAX + 1 = %u\n", v); // 0(回绕到 0)
    // C 标准规定:无符号整数运算结果 = 数学结果 mod (2^N)

    return 0;
}
类型溢出行为C 标准
int(有符号)未定义行为编译器可以假设不会发生,做激进优化
unsigned int(无符号)模 2^N 回绕(wraparound)定义良好,结果可预测

CAUTION

有符号整数溢出是未定义行为——编译器可以基于「溢出不会发生」的假设做优化,导致程序行为完全不可预测。使用 -ftrapv(gcc/clang)可以让程序在溢出时崩溃,帮助调试。

5.3 使用 long long 处理更大范围

int 不够用时,使用 long long(C99 引入的 64 位整数):

long_long_sum.c
c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    // int 只能安全计算到 n ≈ 65535
    // long long 可以安全计算到 n ≈ 3,037,000,499(sqrt(2 × LLONG_MAX))

    long long n = 1000000LL;  // 一百万
    long long sum = n * (n + 1) / 2;

    printf("Sum of 1 to %lld = %lld\n", n, sum);
    // 输出: Sum of 1 to 1000000 = 500000500000

    // 比较各类型的范围
    printf("INT_MAX    = %d\n", INT_MAX);          // ~2.1 × 10^9
    printf("LLONG_MAX  = %lld\n", LLONG_MAX);      // ~9.2 × 10^18

    return 0;
}
类型位宽大致范围安全求和的 n 上限
int32±2.1 × 10^9~65,535
long long64±9.2 × 10^18~3,037,000,499
unsigned long long640 ~ 1.8 × 10^19~4,294,967,295

TIP

在不确定数据范围时,优先使用 long longint64_t<stdint.h>)。多出的几个字节内存换取数值安全是值得的。但也要注意:long long 不是无限大——足够大的 n 仍然会溢出。

5.4 溢出检测技术

在累加循环中检测溢出:

overflow_detect.c
c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    int sum = 0;
    int n = 70000;  // 故意使用一个会导致溢出的 n

    for (int i = 1; i <= n; i++)
    {
        // 加法溢出检测:如果 sum > INT_MAX - i,再加 i 就会溢出
        if (sum > INT_MAX - i)
        {
            printf("Overflow detected at i = %d!\n", i);
            printf("Current sum = %d, INT_MAX = %d\n", sum, INT_MAX);
            return 1;  // 非正常退出
        }
        sum += i;
    }

    printf("sum = %d\n", sum);
    return 0;
}

原理:a + b > INT_MAX 等价于 a > INT_MAX - b。后者避免了加法溢出本身的问题。


6. 循环变量的生命周期与终值

6.1 循环退出后 i 的终值

c
int i;
for (i = 0; i <= 100; i++) {
    sum += i;
}
printf("After loop: i = %d\n", i);  // 输出 101

执行流程:

步骤i 的值操作
初始化0i = 0
迭代 10判断 0 <= 100 ✓ → 执行循环体 → i++ → i = 1
.........
最后一次迭代100判断 100 <= 100 ✓ → 执行循环体 → i++ → i = 101
退出检查101判断 101 <= 100 ✗ → 不执行循环体,退出

关键:循环退出时 i = 101。条件不满足的那次迭代,循环体不执行,但 i++ 已经在上一次迭代中执行了。

6.2 C89 vs C99:for-init 变量的作用域差异

这是两种标准的重要实际差异:

c
// C99 写法:i 作用域仅限于 for 循环
for (int i = 0; i <= 100; i++) {  // i 在此声明
    sum += i;
}
// printf("%d\n", i);  // 编译错误:i 未声明

// C89 写法:i 在 for 外部可见
int i;                           // 在 for 外声明
for (i = 0; i <= 100; i++) {
    sum += i;
}
printf("After loop: i = %d\n", i);  // OK: i = 101
标准for-init 变量作用域循环外访问默认编译器
C89/C90所在代码块✓ 可以gcc 默认 (-std=gnu89)
C99+仅 for 循环✗ 不可以gcc -std=c99 或 -std=c11

TIP

优先使用 C99 的 for (int i = ...) 写法——限制作用域可以避免变量名冲突。只在确实需要终值时才在 for 外声明。

6.3 不同循环条件的终值规律

loop_final_values.c
c
#include <stdio.h>

int main(void)
{
    int i;

    // 条件用 <=
    for (i = 0; i <= 5; i++) { }
    printf("i <= 5 → final i = %d\n", i);   // 6

    // 条件用 <
    for (i = 0; i < 5; i++) { }
    printf("i < 5  → final i = %d\n", i);   // 5

    // 条件用 >=
    for (i = 5; i >= 1; i--) { }
    printf("i >= 1 → final i = %d\n", i);   // 0

    // 条件用 >
    for (i = 5; i > 1; i--) { }
    printf("i > 1  → final i = %d\n", i);   // 1

    return 0;
}

规律:终值 = 第一个使条件为假的值。对于 i <= N 模式,终值为 N+1;对于 i < N 模式,终值为 N


7. continue 与 break:循环控制流的深入对比

7.1 continue 的工作机制

continue 跳过当前迭代的剩余部分,进入下一次迭代:

c
for (int i = 0; i <= 100; i++) {
    if (i % 2 != 0)      // 如果是奇数
        continue;         // 跳过累加,进入下一次迭代
    sum += i;             // 只有偶数才累加
}

执行 continue 后:

  1. 跳过循环体中 continue 之后的所有语句
  2. for 循环中:先执行更新表达式(i++),再判断条件
  3. while 循环中:直接跳到条件判断(不执行更新!)

7.2 continue 在 for 和 while 中的关键区别

continue_for_vs_while.c
c
#include <stdio.h>

int main(void)
{
    // for 循环:continue 后 i++ 仍然执行
    printf("for: ");
    for (int i = 0; i < 10; i++) {
        if (i == 5) continue;  // 跳到 i++,然后判断 i < 10
        printf("%d ", i);
    }
    printf("\n");
    // 输出:0 1 2 3 4 6 7 8 9(跳过了 5)

    // while 循环:continue 后 i++ 被跳过!可能死循环!
    printf("while: ");
    int i = 0;
    while (i < 10) {
        if (i == 5) { i++; continue; }  // 必须在 continue 前手动更新!
        printf("%d ", i);
        i++;
    }
    printf("\n");
    // 正确输出:0 1 2 3 4 6 7 8 9

    return 0;
}
语句forwhile
continue先执行更新表达式,再判断条件直接跳到条件判断(跳过更新!)
break立即退出循环立即退出循环

CAUTION

while 循环中使用 continue 时,必须确保更新语句在 continue 之前执行,否则可能死循环。这也是 for 循环更适合过滤迭代的原因——更新表达式不会被跳过。

7.3 break 与 continue 的对比

c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // 跳过 i=3,继续 i=4
    if (i == 7) break;     // 在 i=7 时完全退出循环
    printf("%d ", i);
}
// 输出: 0 1 2 4 5 6
语句效果适用场景
continue跳过本次迭代剩余部分过滤不需要处理的元素
break终止整个循环提前退出(找到目标、遇到错误)

7.4 扩展示例:用 continue 跳过多个条件

continue_multi_filter.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;

    for (int i = 1; i <= 100; i++)
    {
        // 跳过 3 的倍数
        if (i % 3 == 0) continue;

        // 跳过以 7 结尾的数字
        if (i % 10 == 7) continue;

        sum += i;
    }

    // 加了所有不是 3 的倍数且不以 7 结尾的数
    printf("Filtered sum = %d\n", sum);

    return 0;
}

使用 continue 的多层过滤比嵌套 if 更清晰——每个过滤条件独立一行,易于理解和修改。


8. 嵌套循环:sum of sums 与迭代次数

8.1 嵌套循环基本结构

当一个循环体内包含另一个循环时,就构成了嵌套循环。外层循环每执行一次,内层循环完整执行一轮:

nested_sum.c
c
#include <stdio.h>

int main(void)
{
    int total = 0;

    // 外层循环:i 从 1 到 3
    for (int i = 1; i <= 3; i++)
    {
        int row_sum = 0;

        // 内层循环:j 从 1 到 i
        for (int j = 1; j <= i; j++)
        {
            row_sum += j;
        }

        printf("sum(1..%d) = %d\n", i, row_sum);
        total += row_sum;
    }

    printf("Total sum of sums = %d\n", total);
    // 输出:
    // sum(1..1) = 1
    // sum(1..2) = 3
    // sum(1..3) = 6
    // Total sum of sums = 10

    return 0;
}

8.2 迭代次数分析

c
for (int i = 0; i < M; i++)     // 外层:M 次
    for (int j = 0; j < N; j++)  // 内层:每次 N 次
        do_something(i, j);
// 总迭代次数 = M × N

如果内层循环的迭代次数依赖于外层变量:

nested_iteration_count.c
c
#include <stdio.h>

int main(void)
{
    int count = 0;
    int n = 5;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            count++;

    // 总迭代次数 = 1 + 2 + 3 + 4 + 5 = n(n+1)/2
    printf("Total iterations = %d\n", count);   // 15
    printf("Formula: %d*(%d+1)/2 = %d\n", n, n, n*(n+1)/2);

    return 0;
}

这就是时间复杂度分析的雏形:内层依赖外层的嵌套循环,总迭代次数为 O(n²)。

8.3 三重循环示例:计算所有 (i,j,k) 组合的和

triple_nested.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;
    int n = 3;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            for (int k = 1; k <= n; k++)
                sum += i + j + k;

    printf("sum = %d\n", sum);
    // 迭代次数 = 3 × 3 × 3 = 27 (n³)
    // 每次加 i+j+k,总结果取决于排列组合

    return 0;
}

三重嵌套循环的迭代次数为 O(n³)——这是许多朴素算法的复杂度(如矩阵乘法的朴素实现)。


9. 时间复杂度入门:O(1)、O(n)、O(n²)

9.1 什么是时间复杂度

时间复杂度描述算法的运行时间如何随输入规模增长。它不关心具体的秒数,而是关心增长趋势(增长率)。

常见时间复杂度从快到慢:

O(1)      → 常数时间:不管输入多大,运行时间不变
O(log n)  → 对数时间:输入翻倍,运行时间增加一个常数
O(n)      → 线性时间:输入翻倍,运行时间也翻倍
O(n²)     → 平方时间:输入翻倍,运行时间变 4 倍
O(2ⁿ)     → 指数时间:输入增加 1,运行时间翻倍(极其可怕)

9.2 直观对比

nO(1)O(log n)O(n)O(n²)O(2ⁿ)
110112
101~3101001024
1001~710010,000~1.3×10³⁰
10001~1010001,000,000天文数字

9.3 从代码识别时间复杂度

complexity_examples.c
c
// O(1):只做一次计算
int sum = n * (n + 1) / 2;

// O(n):单层循环
for (int i = 0; i < n; i++)
    sum += i;

// O(n²):嵌套循环(内层依赖 n)
for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
        sum += i * j;

// O(n²/2) 仍写作 O(n²):常数因子在复杂度分析中忽略
for (int i = 0; i < n; i++)
    for (int j = i; j < n; j++)  // 平均迭代 n/2 次
        sum += i * j;
// 总迭代 ≈ n²/2,仍是 O(n²)

// 两层独立循环 → O(n) 而非 O(n²)
for (int i = 0; i < n; i++) sum += i;   // O(n)
for (int j = 0; j < n; j++) prod *= j;  // O(n)
// 总计 O(n + n) = O(2n) = O(n)

IMPORTANT

时间复杂度分析的核心原则:

  1. 忽略常数因子:O(2n) = O(n)
  2. 忽略低阶项:O(n² + n) = O(n²)
  3. 关注最内层循环的迭代次数
  4. 独立循环相加,嵌套循环相乘

10. 代码性能测量:如何计时你的循环

10.1 使用 clock() 测量

time_measurement.c
c
#include <stdio.h>
#include <time.h>

int main(void)
{
    clock_t start, end;
    double cpu_time;

    // 测量循环累加 O(n)
    start = clock();
    long long sum_loop = 0;
    for (int i = 1; i <= 100000000; i++)  // 1 亿次
        sum_loop += i;
    end = clock();
    cpu_time = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("Loop O(n):   sum = %lld, time = %.4f seconds\n",
           sum_loop, cpu_time);

    // 测量公式 O(1)
    start = clock();
    long long n = 100000000LL;
    long long sum_formula = n * (n + 1) / 2;
    end = clock();
    cpu_time = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("Formula O(1): sum = %lld, time = %.8f seconds\n",
           sum_formula, cpu_time);

    return 0;
}

典型输出(具体数值因机器而异):

Loop O(n):   sum = 5000000050000000, time = 0.1234 seconds
Formula O(1): sum = 5000000050000000, time = 0.00000012 seconds

O(n) 和 O(1) 的差距在 n = 1 亿时已经非常明显——约百万倍的差异。

10.2 clock() 的精度与局限

  • clock() 返回程序使用的 CPU 时间(不是墙上时钟时间)
  • 精度为 CLOCKS_PER_SEC(通常 1,000,000,即微秒级)
  • 太短的代码段(< 微秒)测量不准——需要重复执行取平均
  • 多核/多线程环境下,CPU 时间可能超过实际耗时

TIP

更精确的性能测量可以使用 clock_gettime(CLOCK_MONOTONIC, &ts)(POSIX)或 QueryPerformanceCounter(Windows)。但对于学习目的,clock() 已经足够。


11. while 循环版本对比

11.1 for 与 while 的等价转换

while_vs_for.c
c
#include <stdio.h>

int main(void)
{
    int sum;

    // 版本 1:for 循环
    sum = 0;
    for (int i = 0; i <= 100; i++)
        sum += i;
    printf("for:   sum = %d\n", sum);

    // 版本 2:while 循环(C99 风格——i 在外部声明)
    sum = 0;
    int i = 0;
    while (i <= 100)
    {
        sum += i;
        i++;
    }
    printf("while: sum = %d\n", sum);

    // 版本 3:while 循环(C89 兼容——i 在块开头)
    sum = 0;
    i = 0;
    while (i <= 100)
    {
        sum += i;
        i++;
    }
    printf("while: sum = %d\n", sum);

    return 0;
}

11.2 选择 for 还是 while

场景推荐原因
迭代次数已知for三要素集中,一目了然
迭代次数未知while条件判断在顶部,灵活
至少执行一次do-while后测循环,天然保证一次
过滤迭代forcontinue 不会跳过更新
无限循环for(;;)惯用写法,无警告

NOTE

for 循环在本课的场景下优于 while:迭代次数明确(0 到 100,共 101 次),三要素(初始化 int i=0、条件 i<=100、更新 i++)集中在一行,代码更紧凑。如果在循环体内使用 continuefor 也更安全——更新表达式 i++ 一定执行。


12. 边界情况:当 start > end 或涉及负数

12.1 空范围:start > end

edge_case_empty_range.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;

    // 当 start > end 时,循环体不执行
    for (int i = 100; i <= 1; i++)
        sum += i;

    printf("Empty range sum = %d\n", sum);  // 0
    // sum 保持初始值 0,这是累加器模式的正确行为

    return 0;
}

如果使用公式计算,也需要处理这种情况:

c
int sum_range(int start, int end)
{
    if (start > end) return 0;  // 空范围返回 0
    // sum(start..end) = sum(1..end) - sum(1..start-1)
    return end * (end + 1) / 2 - (start - 1) * start / 2;
}

12.2 包含负数的求和

negative_sum.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;

    // 从 -5 加到 5
    for (int i = -5; i <= 5; i++)
        sum += i;

    printf("Sum from -5 to 5 = %d\n", sum);  // 0
    // 因为 -5 + (-4) + ... + 4 + 5 = 0(对称抵消)

    // 从 -3 加到 7
    sum = 0;
    for (int i = -3; i <= 7; i++)
        sum += i;

    printf("Sum from -3 to 7 = %d\n", sum);  // 22
    // 验证: (-3)+(-2)+(-1)+0+1+2+3+4+5+6+7 = 22

    return 0;
}

对于包含负数的范围,高斯公式仍然适用:

sum(m..n) = sum(1..n) - sum(1..m-1)
          = n(n+1)/2 - (m-1)m/2

当 m 为负数时,sum(1..m-1) 会产生一个负的结果,减负等于加正——公式自动处理了符号。


13. 循环展开:以空间换时间的优化思想

13.1 什么是循环展开

循环展开(Loop Unrolling)是一种优化技术:将循环体复制多份,减少循环控制开销(比较、跳转、更新)。

loop_unrolling.c
c
#include <stdio.h>
#include <time.h>

int main(void)
{
    const int n = 100000000;
    long long sum;
    clock_t start, end;

    // 普通循环
    start = clock();
    sum = 0;
    for (int i = 1; i <= n; i++)
        sum += i;
    end = clock();
    printf("Normal:  sum = %lld, time = %.4f s\n",
           sum, ((double)(end - start)) / CLOCKS_PER_SEC);

    // 循环展开(因子 4)
    start = clock();
    sum = 0;
    int i;
    for (i = 1; i <= n - 3; i += 4)
    {
        sum += i;          // i
        sum += i + 1;      // i+1
        sum += i + 2;      // i+2
        sum += i + 3;      // i+3
    }
    // 处理剩余项(当 n 不能被 4 整除时)
    for (; i <= n; i++)
        sum += i;
    end = clock();
    printf("Unrolled: sum = %lld, time = %.4f s\n",
           sum, ((double)(end - start)) / CLOCKS_PER_SEC);

    return 0;
}

13.2 循环展开的利弊

优点缺点
减少分支指令(条件跳转)代码体积增大(更多指令)
减少循环变量更新次数指令缓存压力增大
增加指令级并行机会可读性下降
某些场景显著提升性能现代编译器通常会自动做

NOTE

在现代编译器中,-O2-O3 优化级别会自动进行循环展开。手动展开通常不必要——编译器比你更了解目标 CPU 的流水线深度和缓存特性。这里展示是为了理解编译优化原理,而非建议手写展开。

13.3 编译器比你更聪明:高斯公式的常量折叠

更有趣的是,编译器在 -O2 优化下甚至能识别高斯求和模式

c
// 如果你写:
int n = 100;
int sum = 0;
for (int i = 1; i <= n; i++) sum += i;

// gcc -O2 可能会把它优化为:
int sum = 5050;  // 直接在编译期算出了结果!

n 是编译期常量时,编译器会做常量折叠(Constant Folding)——在编译时计算 n*(n+1)/2,直接嵌入结果。这意味着你的 O(n) 循环在优化后变成了 O(1)。

如果 n 是运行时变量,编译器不会直接算出结果,但可能用 SIMD 指令(如 AVX2)进行向量化累加。


14. 有符号与无符号整数的深入对比

14.1 表示范围

signed_unsigned_range.c
c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    printf("=== signed int ===\n");
    printf("INT_MIN  = %d\n", INT_MIN);   // -2147483648
    printf("INT_MAX  = %d\n", INT_MAX);   //  2147483647
    printf("Range:   [-2^31, 2^31-1]\n");

    printf("\n=== unsigned int ===\n");
    printf("0        = %u\n", 0);
    printf("UINT_MAX = %u\n", UINT_MAX);  // 4294967295
    printf("Range:   [0, 2^32-1]\n");

    printf("\n=== size comparison ===\n");
    printf("sizeof(int)           = %zu bytes\n", sizeof(int));
    printf("sizeof(unsigned int)  = %zu bytes\n", sizeof(unsigned int));
    // 两者大小完全相同(通常 4 字节)

    return 0;
}

14.2 混合运算的类型提升陷阱

signed_unsigned_mix.c
c
#include <stdio.h>

int main(void)
{
    int a = -1;
    unsigned int b = 1;

    // 混合比较:-1 被提升为 unsigned,变成 UINT_MAX
    if (a < b)
        printf("As expected: -1 < 1\n");
    else
        printf("Surprise: -1 >= 1 (because of unsigned promotion!)\n");
    // 实际输出:Surprise: -1 >= 1

    // 原因:当 signed 和 unsigned 混合时,signed 被隐式转换为 unsigned
    // -1 的二进制表示为全 1 (0xFFFFFFFF),作为 unsigned 就是 4294967295

    // 安全做法:显式比较
    if (a < (int)b)
        printf("Fixed: -1 < 1\n");

    return 0;
}

CAUTION

有符号和无符号整数的混合运算是 C 语言中著名的陷阱。当 signedunsigned 出现在同一个表达式中时,signed 被隐式转换为 unsigned——负数会变成巨大的正数。循环条件中尤其要注意:for (unsigned i = 10; i >= 0; i--)无限循环!因为 i 永远 >= 0。

14.3 unsigned 递减的死循环陷阱

c
// 无限循环!unsigned 永远不会 < 0
for (unsigned int i = 10; i >= 0; i--)
    printf("%u\n", i);
// 当 i = 0 时,i-- 使 i 变为 UINT_MAX,仍 >= 0

// 正确写法 1:用 signed int
for (int i = 10; i >= 0; i--)
    printf("%d\n", i);

// 正确写法 2:改条件
for (unsigned int i = 10; i != UINT_MAX; i--)
    printf("%u\n", i);

WARNING

使用 unsigned 做递减循环时,条件 >= 0 永远为真——这是无限循环的经典来源。对递减循环,使用 signed int


15. 循环调试技巧:打印中间值观察累加过程

15.1 基本调试打印

在开发累加逻辑时,打印中间值是理解程序行为最直接的方式:

debug_print.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;

    printf("=== Loop Trace ===\n");
    printf("%-6s %-8s %s\n", "i", "sum_before", "sum_after");

    for (int i = 0; i <= 10; i++)  // 先小范围调试
    {
        int sum_before = sum;
        sum += i;
        printf("%-6d %-8d %d\n", i, sum_before, sum);
    }

    printf("\nFinal sum = %d\n", sum);
    return 0;
}

输出:

=== Loop Trace ===
i      sum_before sum_after
0      0          0
1      0          1
2      1          3
3      3          6
...
10     45         55

Final sum = 55

15.2 条件编译调试宏:PRINTD

回顾 Lesson 02 中接触过的条件编译技巧,在循环累加中特别有用:

debug_macro.c
c
#include <stdio.h>

// 调试宏:编译时加 -DDEBUG 启用,否则展开为空
#ifdef DEBUG
#define PRINTD(fmt, ...) printf("[DEBUG] " fmt, ##__VA_ARGS__)
#else
#define PRINTD(fmt, ...) ((void)0)
#endif

int main(void)
{
    int sum = 0;

    for (int i = 0; i <= 100; i++)
    {
        sum += i;
        PRINTD("i = %3d, sum = %d\n", i, sum);
    }

    printf("sum = %d\n", sum);
    return 0;
}

编译运行:

bash
gcc -DDEBUG debug_macro.c -o debug_macro   # 包含调试输出(打印 101 行)
gcc debug_macro.c -o debug_macro            # 不包含调试输出(只打印最终结果)

15.3 更精细的调试:打印每第 N 次迭代

对于大规模循环(如 1 到 10000),每次迭代都打印不现实:

sample_debug.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;
    int n = 10000;

    for (int i = 1; i <= n; i++)
    {
        sum += i;

        // 每 1000 次打印一次进度
        if (i % 1000 == 0)
            printf("Progress: i = %5d, partial sum = %d\n", i, sum);
    }

    printf("Final sum = %d\n", sum);
    return 0;
}

输出:

Progress: i =  1000, partial sum = 500500
Progress: i =  2000, partial sum = 2001000
...
Progress: i = 10000, partial sum = 50005000
Final sum = 50005000

TIP

调试大型循环时的采样策略:

  1. 每 N 次迭代打印一次(i % N == 0
  2. 只打印开头和结尾的几次迭代
  3. 打印关键状态变化点(如 sum 的位数变化时)
  4. 使用 #ifdef DEBUG 包裹调试代码,发布时一键关闭

参考解答

1 到 100 求和
sum_basic.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;

    for (int i = 0; i <= 100; i++)
    {
        sum += i;
    }

    printf("sum = %d\n", sum);

    return 0;
}

核心逻辑:sum 初始为 0(加法单位元),i 从 0 遍历到 100,每次 sum += i 累加。i = 0sum += 0 不改变结果,所以从 0 或 1 开始都可以——但 0 + 1 + ... + 100 = 50501 + ... + 100 = 5050 结果相同。

偶数和(使用 continue)
sum_even.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;

    for (int i = 0; i <= 100; i++)
    {
        if (i % 2 != 0)   // 如果是奇数
            continue;      // 跳过累加
        sum += i;          // 只有偶数才累加
    }

    printf("sum = %d\n", sum);

    return 0;
}

输出 sum = 2550continue 跳过奇数后,for 的更新表达式 i++ 仍然执行(这是 forwhile 的关键区别——回顾深度讲解第 7 节)。

数学公式版(O(1))
sum_formula.c
c
#include <stdio.h>

int main(void)
{
    int n = 100;
    int sum = n * (n + 1) / 2;

    printf("sum = %d\n", sum);

    return 0;
}

高斯公式:sum = n × (n+1) / 2。一次计算,无需循环。时间复杂度 O(1),比循环累加的 O(n) 快 n 倍。当 n 为编译期常量时,编译器甚至会在编译时直接算出结果(常量折叠)。

while 循环版本
sum_while.c
c
#include <stdio.h>

int main(void)
{
    int sum = 0;
    int i = 0;

    while (i <= 100)
    {
        sum += i;
        i++;
    }

    printf("sum = %d\n", sum);

    return 0;
}

while 版本与 for 版本逻辑完全等价,但三要素分散在三处(i = 0 在外部、i <= 100 在条件中、i++ 在循环体末尾)。对于迭代次数已知的场景,for 更清晰——三要素集中在一行,读者一眼就能看出循环的完整逻辑。

对照检查sum 初始值是否为 0?for 条件用的是 <= 还是 <?用 i <= 100 时结果是 5050,用 i < 100 时结果是 4950(差 100)。如果求和范围扩大到 1 到 100000,int 还安全吗?试试改用 long long


课堂讨论

  1. for 循环内部修改循环变量 i 的值,可以吗?会有什么后果?
  2. 循环退出时,i++ 是否还会执行?最终 i 的值是多少?
  3. continuewhilefor 中的行为有什么关键区别?
  4. 为什么累加器 sum 的初始值是 0?如果初始值是 1 会怎样?
  5. i <= 100 写成 i < 100,结果会差多少?
  6. 如果求和范围是 1 到 100000,使用 int 会有什么风险?如何安全处理?

讨论答案

Q1: 在 for 循环内部修改循环变量 i 的值可以吗?

语法上可以,但强烈不推荐。 修改循环变量破坏循环的可预测性。

c
for (int i = 0; i < 10; i++) {
    printf("%d\n", i);
    i += 2;  // 修改循环变量!实际每次迭代 i 增加 3
}
// 输出:0, 3, 6, 9

后果:读者看到 for (i = 0; i < 10; i++) 会预期 10 次迭代,但内部修改破坏了这一预期。更危险的是,修改方向错误可能导致死循环。

更深层的问题:如果在循环体内修改了循环变量,任何代码审查者都需要重新推导整个循环的执行轨迹——这违背了 for 循环「三要素在一行」的设计初衷。for 的设计理念是让你一眼看懂循环的执行模式,内部修改打破了这个契约。

规则:循环变量由 for 头部的更新表达式管理,不要在循环体内修改。需要跳过某些迭代时,用 continue。如果确实需要复杂的迭代模式,改用 while 并在循环体内清晰表达每一步的更新逻辑。

Q2: 循环退出时 i++ 是否还会执行?最终 i 的值是多少?

i++ 会执行到条件不满足为止。 最后一次进入循环体时 i = 100,循环体执行后 i++i = 101,判断 101 <= 100 为假,退出。最终 i = 101

c
int i;
for (i = 0; i <= 100; i++) { sum += i; }
printf("After loop: i = %d\n", i);  // 输出 101

注意:条件不满足的那次,循环体不执行,但上一次迭代中的 i++ 已经把 i 推到了 101。

通用规律:循环退出后,循环变量的值 = 第一个使条件为假的值。

  • for (i = 0; i <= N; i++) → 终值 N+1
  • for (i = 0; i < N; i++) → 终值 N
  • for (i = N; i >= 1; i--) → 终值 0

这是理解循环行为的关键——如果你在循环后还需要使用循环变量,必须清楚地知道它的终值。

Q3: continue 在 while 和 for 中的关键区别

核心区别:continuefor 中会执行更新表达式,在 while 中不会。

c
// for: continue 后 i++ 仍然执行 → 安全
for (int i = 0; i < 10; i++) {
    if (i == 5) continue;  // 跳到 i++,然后判断
    printf("%d ", i);
}
// 输出:0 1 2 3 4 6 7 8 9

// while: continue 后 i++ 被跳过 → 可能死循环
int i = 0;
while (i < 10) {
    if (i == 5) continue;  // 跳到条件判断,跳过 i++!
    printf("%d ", i);
    i++;
}
// 输出:0 1 2 3 4(然后死循环!i 永远 = 5)

为什么会这样设计? 因为 for 的更新表达式是循环结构的一部分——它不是循环体的一部分。continue 的含义是「跳过循环体剩余部分,进入下一次迭代」,在 for 中「进入下一次迭代」包含执行更新表达式。而在 while 中,更新语句在循环体内,continue 自然跳过了它。

最佳实践

  1. while 中使用 continue 时,确保更新语句在 continue 之前
  2. 或者改用 for 结构——这是 for 更适合过滤迭代的根本原因
Q4: 为什么累加器 sum 的初始值是 0?

初始值必须是加法的单位元——0。 因为 x + 0 = x,加 0 不改变结果。

如果初始值是 1:

c
int sum = 1;  // 不是加法单位元
for (int i = 1; i <= 100; i++) sum += i;
// 结果:5050 + 1 = 5051(错误!)

单位元的通用规则

运算单位元初始值示例
加法 (+)0sum = 00 + x = x
乘法 (×)1product = 11 × x = x
位或 (|)0flags = 00 | x = x
位与 (&)全 1mask = ~0~0 & x = x
找最大值INT_MINarr[0]
找最小值INT_MAXarr[0]

更深层的理解:累加器模式本质上是将「二元运算」通过「单位元」转化为「可迭代的累积计算」。初始值 = 单位元这个规则使得「空集合」的处理天然正确——如果循环体一次都不执行(如 start > end),累加器保持单位元,结果是数学上正确的「空和 = 0」。

Q5: 把 i <= 100 写成 i < 100 结果差多少?

差 100——恰好是被跳过的最后一个值。

c
// i <= 100: sum = 0+1+2+...+100 = 5050
// i < 100:  sum = 0+1+2+...+99  = 4950(少加了 100)

差值 = 5050 - 4950 = 100。这就是经典的 off-by-one(差一)错误——编程中最常见的逻辑错误之一。

为什么 off-by-one 如此常见?

人类计数从 1 开始,计算机从 0 开始。i <= 100 包含 101 个值(0 到 100),i < 100 包含 100 个值(0 到 99)。看似差 1,实则差了一整个边界值。

避免方法

  1. 用小值手动验证:求 1 + 2 + 3,用 i <= 3 得 6 ✓,用 i < 3 得 4 ✗
  2. 遵循「从 0 开始,用 <」的约定:for (int i = 0; i < N; i++) → N 次迭代,一目了然
  3. <= 时额外小心——它天然多包含一个边界值
Q6: 如果求和范围是 1 到 100000,使用 int 会有什么风险?如何安全处理?

风险分析

1 到 100000 的和 = 100000 × 100001 / 2 = 5,000,050,000

这个值约为 50 亿,已经超过 int 的最大值约 21 亿(INT_MAX = 2,147,483,647)。使用 int 计算会导致有符号整数溢出——未定义行为

c
int n = 100000;
int sum_loop = 0;          // int 不够用!
for (int i = 1; i <= n; i++)
    sum_loop += i;
// sum_loop 在某个时刻溢出,结果是未定义的

int sum_formula = n * (n + 1) / 2;
// n * (n+1) 首先溢出 int,然后再除以 2,结果错误

安全处理方案

方案 1:使用 long long(推荐)

c
long long n = 100000LL;
long long sum = n * (n + 1) / 2;
// sum = 5000050000,安全

方案 2:使用 unsigned int 避免未定义行为(但结果仍可能回绕)

c
unsigned int n = 100000;
unsigned int sum = n * (n + 1) / 2;
// 注意:n*(n+1) 作为 unsigned int 计算时可能回绕
// 最好还是用 long long

方案 3:公式运算时先提升类型

c
int n = 100000;
long long sum = (long long)n * (n + 1) / 2;
// 将 n 强制转换为 long long,后续运算在 64 位空间进行

教训:在选择数据类型时,要估计最终结果的范围,而非只看单个元素的大小。累加会放大数值——n 个元素的累加结果可能是 O(n²) 级别。


课后练习

  1. 计算平均数:用户输入 5 个数,计算并打印它们的平均数(保留两位小数)。

    知识点提示:累加器 sum + 类型转换 (double)sum / 5

  2. 找最大值与最小值:用户输入 5 个数,找出并打印最大和最小的数。

    知识点提示:维护「当前最优值」的累加器变体,初始值用 INT_MAXINT_MIN

  3. 打印月历:已知 11 月 1 日是星期四,打印 11 月份的月历。

    知识点提示%2d 格式对齐、(start + day) % 7 == 0 判断换行

  4. 挑战:奇偶分别求和:只用一层循环,分别计算 1 到 100 中所有奇数的和与偶数的和。

    知识点提示:在循环体内用 if/else 分别累加两个累加器

  5. 挑战:性能对比:用 clock() 分别测量循环累加和高斯公式计算 1 到 1 亿的和,比较耗时。

    知识点提示clock()CLOCKS_PER_SEClong long 避免溢出

练习1: 计算平均数
average.c
c
#include <stdio.h>

int main(void)
{
    int num, sum = 0;

    printf("Enter 5 numbers:\n");

    for (int i = 0; i < 5; i++)
    {
        printf("Number %d: ", i + 1);
        scanf("%d", &num);
        sum += num;
    }

    double average = (double)sum / 5;
    printf("Average = %.2f\n", average);

    return 0;
}

关键(double)sum / 5sum 强制转为 double,使除法产生浮点结果。直接用 sum / 5 会做整数除法(截断小数部分)。%.2f 保留两位小数。

练习2: 找最大值与最小值
max_min.c
c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    int num, max = INT_MIN, min = INT_MAX;

    printf("Enter 5 numbers:\n");

    for (int i = 0; i < 5; i++)
    {
        printf("Number %d: ", i + 1);
        scanf("%d", &num);

        if (num > max) max = num;
        if (num < min) min = num;
    }

    printf("Maximum = %d\n", max);
    printf("Minimum = %d\n", min);

    return 0;
}

算法:维护「当前最优值」——每次新数据到来时比较更新。这是 O(n) 时间、O(1) 空间的在线算法(不需要存储所有数据)。初始值 INT_MIN 保证任何有效输入都能更新 maxINT_MAX 保证任何有效输入都能更新 min

练习3: 打印月历
calendar.c
c
#include <stdio.h>

int main(void)
{
    int days = 30;      // 11 月有 30 天
    int start = 4;      // 1 号是星期四(0=周日, 1=周一, ..., 4=周四)

    printf("     November\n");
    printf("Su Mo Tu We Th Fr Sa\n");

    // 打印 1 号之前的空白
    for (int i = 0; i < start; i++)
        printf("   ");

    // 打印日期
    for (int day = 1; day <= days; day++)
    {
        printf("%2d ", day);

        if ((start + day) % 7 == 0)
            printf("\n");
    }

    printf("\n");
    return 0;
}

逻辑start = 4 表示前面有 4 个空位(日、一、二、三);(start + day) % 7 == 0 判断周六换行;%2d 使日期右对齐。

挑战1: 奇偶分别求和
sum_odd_even.c
c
#include <stdio.h>

int main(void)
{
    int sum_even = 0;
    int sum_odd  = 0;

    for (int i = 1; i <= 100; i++)
    {
        if (i % 2 == 0)
            sum_even += i;
        else
            sum_odd += i;
    }

    printf("Sum of even: %d\n", sum_even);  // 2550
    printf("Sum of odd:  %d\n", sum_odd);    // 2500
    printf("Total: %d\n", sum_even + sum_odd);  // 5050

    return 0;
}

一次遍历,两个累加器——时间 O(n),空间 O(1)。数学验证:2550 + 2500 = 5050 = 100×101/2 ✓。

挑战2: 性能对比 — 循环 vs 公式
benchmark.c
c
#include <stdio.h>
#include <time.h>

int main(void)
{
    long long n = 100000000LL;  // 1 亿
    long long sum;
    clock_t start, end;
    double time_loop, time_formula;

    // 循环累加 O(n)
    start = clock();
    sum = 0;
    for (long long i = 1; i <= n; i++)
        sum += i;
    end = clock();
    time_loop = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("Loop O(n):   sum = %lld, time = %.6f s\n", sum, time_loop);

    // 公式 O(1)
    start = clock();
    sum = n * (n + 1) / 2;
    end = clock();
    time_formula = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("Formula O(1): sum = %lld, time = %.9f s\n", sum, time_formula);

    // 对比
    if (time_formula > 0)
        printf("Speedup: %.0f x faster\n", time_loop / time_formula);

    return 0;
}

典型输出(具体数值因机器而异):

Loop O(n):   sum = 5000000050000000, time = 0.123456 s
Formula O(1): sum = 5000000050000000, time = 0.000000123 s
Speedup: 1003707 x faster

分析:公式比循环快约一百万倍!这直观展示了 O(1) 和 O(n) 的本质差异。注意使用 long long 避免溢出——1 到 1 亿的和约为 5 × 10^15,远超 int 范围。


参考资料

"If debugging is the process of removing bugs, then programming must be the process of putting them in." — Edsger W. Dijkstra

Released under the MIT License.