全部版块 我的主页
论坛 数据科学与人工智能 IT基础
89 0
2025-11-21

一、作用域的基本概念

在编程中,作用域指的是变量或函数可以被访问的有效范围。不同的作用域决定了变量的可见性和生命周期。

以下代码展示了不同作用域中变量的使用情况:

#include<stdio.h>
int main() {
    // a 和 b 定义在 main 函数的作用域内
    // 因此在整个 main 函数中都可以使用
    int a = 1, b = 2;
    for (int i = 0; i < 3; i++) {
        // c 和 d 局部于 for 循环内部的作用域
        // 只能在该 {} 内部进行访问
        int c = 1, d = 2;
        // 由于 for 在 main 的作用域中,因此可以访问 a 和 b
        printf("a = %d, b = %d\n", a, b);
        // 同时也可以访问当前作用域内的 c 和 d
        printf("c = %d, d = %d\n", c, d);
    }
    // 离开 for 循环后仍可访问 a 和 b
    printf("a = %d, b = %d\n", a, b);
    // 下列语句将报错,因为 c 和 d 已超出其作用域
    // printf("c = %d, d = %d\n", c, d);
    return 0;
}
作用域:定义的变量,作用的区域

执行结果如下所示:

当内外层作用域存在同名变量时,内部作用域会屏蔽外部变量。例如:

#include<stdio.h>
int main() {
    // 外部作用域中的 a 和 b
    int a = 1, b = 2;
    for (int i = 0; i < 3; i++) {
        // 内部作用域重新定义了 a 和 b
        int a = 3, b = 4;
        // 此处访问的是内部变量
        printf("in for: a = %d, b = %d\n", a, b);
    }
    // 循环结束后恢复对外部变量的访问
    printf("outside : a = %d, b = %d\n", a, b);
    return 0;
}

该程序的输出结果为:

二、函数的使用与设计

1. 函数的定义与调用方式

函数是实现特定功能的代码块,其基本结构包括返回类型、函数名、参数列表和函数体。

示例代码如下:

#include <stdio.h>
#include <math.h>

// 定义一个整型返回值的函数,用于计算两数之和
int sum(int a, int b) {
    return a + b;
}

// 根据 flag 值选择对 x 执行 sqrt(x) 或 x*x 操作
// 返回值可能为浮点型,因此使用 double 类型
double function_1(int flag, int x) {
    if (flag == 1)
        return sqrt(x);
    else if (flag == 2)
        return x * x;
    else
        return 0; // 防止非法输入导致未定义行为
}

// 无返回值函数:打印 n 次 "hello world"
void printf_hello_world(int n) {
    for (int i = 0; i < n; i++) {
        printf("hello world\n");
    }
    return; // 显式结束函数
}

int main() {
    // 调用 sum 函数并输出结果
    printf("3 + 4 = %d\n", sum(3, 4));

    int flag, x;
    printf("intput flag and x\n");
    scanf("%d%d", &flag, &x);
    // 注意格式符 %lf 对应 double 类型
    printf("function_1 = %lf\n", function_1(flag, x));

    int n;
    printf("input n\n");
    scanf("%d", &n);
    printf_hello_world(n);

    return 0;
}

2. 引入函数结构的意义

为何要采用函数化编程?主要原因包括:

  • 提高代码复用性,避免重复编写相同逻辑;
  • 增强程序可读性,使主流程更清晰;
  • 便于调试与维护,功能模块独立隔离;
  • 支持分层开发,多人协作更加高效。

以数学运算为例,若需多次处理某个区间内的等差序列(起始值 start,结束值 end,公差 d),将其封装成函数即可反复调用,提升整体开发效率。

// 等差数列求和函数的封装与应用
int function_sum(int start, int end, int d) {
    int n = (end - start) / d + 1; // 计算项数
    int num = start + (n - 1) * d; // 确定末项
    int sum = (start + num) * n / 2; // 应用求和公式
    return sum;
}

int main() {
    // 示例:计算不同等差数列的和

    // 公差为1,从1加到100
    int sum1 = (1 + 100) * 100 / 2;

    // 公差为2,从1开始直到不超过100
    int n2 = (100 - 1) / 2 + 1;
    int num2 = 1 + (n2 - 1) * 2;
    int sum2 = (1 + num2) * n2 / 2;

    // 公差为17,从30到34534
    int n3 = (34534 - 30) / 17 + 1;
    int num3 = 30 + (n3 - 1) * 17;
    int sum3 = (30 + num3) * n3 / 2;

    printf("%d %d %d\n", sum1, sum2, sum3);

    // 观察发现,每次计算都需要多行重复代码
    // 因此可将该逻辑封装成函数,提升代码复用性和可读性

    // 使用封装后的函数重新计算
    printf("%d ", function_sum(1, 100, 1));
    printf("%d ", function_sum(1, 100, 2));
    printf("%d\n", function_sum(30, 34534, 17));

    return 0;
}


通过上述实现可以看出,使用函数能够有效减少冗余代码,使主函数结构更清晰、逻辑更直观。

3. 形参与实参的区别

以下代码用于演示函数调用过程中参数传递的特点: #include <stdio.h> void function_A(int a, int b) { // a 和 b 为形参 a = a + 1; b = b * 2; printf("in function_A : a = %d, b = %d\n", a, b); return; } int main() { int a = 1, b = 2; function_A(a, b); // a 和 b 作为实参传入 printf("in main : a = %d, b = %d\n", a, b); return 0; } 运行结果表明:在函数内部对形参的修改不会影响主函数中实参的原始值。这说明实参向形参传递的是值的副本,而非变量本身。

4. 函数的声明与定义

#include <stdio.h> // 函数前置声明,告知编译器函数的存在 int func_b(int x); int func_a(int x) { // 若无func_b的声明,编译器在未见其定义时会报错 if (x == 1) return func_b(x); printf("func_a : x * 2 = %d\n", x * 2); return 0; } int func_b(int x) { if (x == 2) return func_a(x); printf("func_b : x * 3 = %d\n", x * 3); return 0; } int main() { // 本程序展示了两个函数相互调用的情形: // func_a 接收1则调用func_b;否则输出x*2 // func_b 接收2则调用func_a;否则输出x*3 func_a(2); func_a(1); return 0; } 函数的“定义”描述了其具体行为,而“声明”则是提前告诉编译器该函数的签名信息(如返回类型、参数列表),以便在函数定义之前就能被调用。这对于实现函数间的交叉调用至关重要。

5. 递归函数的概念与实现

递归是一种函数调用自身的技术,常用于解决具有自相似结构的问题。关键要素包括: - 递归出口(终止条件),防止无限递归 - 每次递归应缩小问题规模 示例:计算阶乘 #include <stdio.h> int func(int n) { if (n == 1) return 1; // 递归终止条件 return n * func(n - 1); // 向下递归 } int main() { int n; while (scanf("%d", &n) != EOF) { // 支持连续输入,Ctrl+C结束 printf("!%d = %d\n", n, func(n)); } return 0; } 递归执行过程图解如下: 通过流程图可以清楚地看到函数逐层调用与返回的过程,有助于理解递归的工作机制。 练习建议:尝试编写一个递归函数来求解斐波那契数列的第n项。

斐波那契数列是一组特殊的数字序列:1,1,2,3,5,8,13,21,34,55,89……从第3项开始,每一项的值都等于前两项之和。这个规律构成了该数列的核心定义。

代码实现与递归解析

#include <stdio.h>

int func(int n) {
    if (n == 1 || n == 2) return 1;
    return func(n - 1) + func(n - 2);
}

int main() {
    int n;
    scanf("%d", &n);
    printf("func(%d) = %d\n", n, func(n));
    return 0;
}

在理解上述递归函数时,可以这样思考:当调用 func(n)(其中 n > 2)时,函数会返回 func(n-1) + func(n-2) 的结果。这一过程不断触发新的函数调用,直到参数变为1或2——这两个条件作为递归的终止出口。随后,程序开始逐层回溯,将每层的结果相加,最终得到原始调用的返回值。

为了更清晰地掌握其执行流程,建议手动绘制递归调用树。通过图形化方式展示每一层的调用与返回关系,有助于深入理解递归机制的工作原理。

欧几里得算法简介

对于两个整数 a 和 b,它们的最大公约数通常记作 gcd(a, b),即能同时整除 a 和 b 的最大正整数。例如,32 和 24 的最大公约数为 8。

该算法的核心公式为:

gcd(a, b) = gcd(b, a % b)

解释:gcd(a,b)返回值就是a和b的最大公约数

    现在需要证明: b 和 a % b的最大公约数是 a 和 b 的公约数。

    先证明b 和 a % b的最大公约数也是a 和 b 的公约数

公式证明过程

证明一:

假设
gcd(b, a % b) = c  c是b和a % b 的最大公约数
设 b = x * c  因为c为b的公约数,所以设b等于x倍c
设 a - k * b = y * c 
这个式子的推到过程
a % b是求 a / b 的余数
a / b的余数 + k倍b = a
所以 a % b = a - k * b
y * c就是y倍他们的最大公约数c
现在得到两个式子
b = x * c
a = k * b + y * c
将上面的式子带入到下面式子
a = k * x * c + y * c
a = c * (k * x + y)
最终得到
b = x * c
a = (k * x + y) * c
可以证明a和b也有c这个公约数

此部分说明了 b 与 a % b 的最大公约数同样是 a 与 b 的公约数之一。

现在来证明b和a % b的最大公约数也是a 和 b 的最大公约数

证明二:

根据上面的推理得到
b = x * c
a = (k * x + y) * c
如何去证明b和a % b的最大公约数也是a 和 b 的最大公约数
通过证明1开始的设
b = x * c
a - k * b = y * c
可以的得到
gcd(x, y) = 1
因为b 和 a % b的最大公约数就是c
所以x和y是互斥的,他们的最大公约数就是1
所以只需要证明 x 和 (k * x + y)是互斥的,也就是他们的最大公约数为1,那么c就是a和b的最大公约数
反证法:
gcd(x, k * x + y) = d
设 x = n * d
设 k * x + y = m * d
那么 y = m * d - k * x
带入x = n * d
y = d * (m - k * n)
因为gcd(x, y) = 1
那么d只能为1,如果d为2或者其他数gcd(x, y)是不可能等于1的
最终证明 b和a % b的最大公约数也是a 和 b 的最大公约数

结合“证明一”与“证明二”,可得出结论:等式 gcd(a, b) = gcd(b, a % b) 成立。

算法代码实现

#include<stdio.h>

int gcd_1(int a, int b) {
    if (b == 0) return a;
    return gcd_1(b, a % b);
}

int gcd_2(int a, int b) {
    return b ? gcd_2(b, a % b) : a;
}

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    printf("gcd_1(%d, %d) = %d\n", a, b, gcd_1(a, b));
    printf("gcd_2(%d, %d) = %d\n", a, b, gcd_1(a, b));
    return 0;
}

关于递归出口为何设置为 b == 0,可以通过一个实例来理解:

  • 设 a = 12,b = 8
  • 则有:gcd(12, 8) = gcd(8, 12 % 8) = gcd(8, 4)
  • 继续:gcd(8, 4) = gcd(4, 8 % 4) = gcd(4, 0)
  • 此时 b = 0,满足递归终止条件,返回 a = 4

由此可见,当 b 为 0 时,当前的 a 值即为原始 a 和 b 的最大公约数。需要注意的是,这里的 a 在不同递归层级中代表不同的数值,最终返回的是最深层调用中的 a 值。

此外,gcd_2 函数使用了三元运算符(? :),其逻辑与 gcd_1 完全一致,仅写法更为简洁。由于非零值视为真,零值视为假,因此可以直接以 b 作为判断条件。

扩展欧几里得算法(附加内容)

贝祖等式指出:对于任意两个整数 a 和 b,存在整数 x 和 y,使得以下等式成立:

a × x + b × y = gcd(a, b) = c

该等式的解可通过扩展欧几里得算法求出。其推导过程依赖于递归回溯的思想。

关键问题在于:已知某一步的解 x 和 y(标记为位置①),如何反推出前一步对应的 x 和 y(位置②)?由于算法最后一步已知,需逆向推导。将最后一项视为当前位置①,则倒数第二项为位置②。利用此方法,可以从末尾逐步向前计算,最终获得初始状态下的 x 和 y。

证明:
位置①
因为 a % b = a - k * b
所以可以得到等式
b * x + (a % b) * y = c
b * x + (a - k * b) * y = c
b * x + a * y - k * b * y = c
b * (x - k * y) + a * y = c
所以通过贝祖等式可以得到位置②对应的x和y
_x = y, _y = x - k * y;

根据数学推导,前一项的解可通过如下关系更新:

_x = y
_y = x - k * y

其中 k 是当前步的商(即 a / b 的整数部分)。基于此规则,可通过递归实现获取初始系数 x 和 y 的完整代码:

#include<stdio.h>

// 使用全局变量便于在整个作用域内访问
// 只要在定义之后,任何函数均可读取或修改其值
// 最终结果即为递归回溯完成后的最终取值
int x, y, _x, _y; // 分别用于存储当前层与下一层递归中的 x 和 y 值

int gcd_up(int a, int b) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    int c = gcd_up(b, a % b); // 递归调用,进入下一层

    // 根据扩展欧几里得算法的推导关系进行系数更新
    _x = y;                    // 当前层的 x 等于下一层的 y
    int k = a / b;             // 计算商 k
    _y = x - k * y;            // 利用公式计算当前层的 y

    // 回溯前更新全局变量,供上层使用
    x = _x;
    y = _y;

    return c;
}

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    int c = gcd_up(a, b);
    printf("x = %d, y = %d, gcd(%d, %d) = %d\n", x, y, a, b, c);
    return 0;
}


以下是代码执行流程的思维导图说明:
掌握该结构后,在后续熟练过程中,此类递归逻辑将能自然地在脑海中构建出来。


递归与回溯练习题解析:

本题目对初学者可能有一定难度,建议在学习完数组相关内容后再进行尝试。一旦理解此题逻辑,便基本掌握了递归与回溯的核心思想。

题目来源:海贼OJ 235


#include <stdio.h>

int n;
int num[15];  // 用于存储当前已选择的数字序列
int s = 0;    // 表示当前序列长度(栈顶指针)

int func(int x) {
    if (x > n) return 0; // 递归终止条件:起始位置超出范围

    for (int i = x; i <= n; i++) {
        num[s++] = i;  // 将当前数字加入路径

        // 打印当前子集
        for (int j = 0; j < s; j++) {
            if (j != 0) printf(" ");
            printf("%d", num[j]);
        }
        printf("\n");

        func(i + 1);   // 递归处理下一个可选起点

        // 回溯:恢复状态
        s--;
    }
    return 0;
}

int main() {
    scanf("%d", &n);
    func(1);
    return 0;
}

6. 可变参数函数的实现  


下面展示如何编写一个支持可变参数的函数,用于求多个整数中的最大值。

#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>

// 参数 n 指明后续实际传入的可变参数个数
int max_int(int n, ...) {
    va_list args;               // 定义一个指向可变参数列表的指针
    va_start(args, n);          // 初始化参数列表,从 n 之后开始读取

    int ans = INT32_MIN;        // 初始化最大值为最小可能整数值

    for (int i = 0; i < n; i++) {
        int a = va_arg(args, int); // 依次获取每个 int 类型的可变参数
        if (a > ans) ans = a;      // 更新最大值
    }

    va_end(args); // 清理参数列表(良好习惯)
    return ans;
}

int main() {
    printf("max_int(%d, %d, %d, %d) = %d\n", 2, 4, 6, 1, max_int(4, 2, 4, 6, 1));
    printf("max_int(%d, %d, %d, %d, %d) = %d\n", 10, 4, 6, 1, 88, max_int(5, 10, 4, 6, 1, 88));
    return 0;
}


7. 主函数的参数形式  


#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("argc = %d\n", argc); // 输出命令行参数总数

    // 遍历并打印所有参数内容
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }

    return 0;
}

通过运行程序可以观察到:
- `argc` 用于记录命令行输入的参数数量;
- `argv` 是一个字符串数组,保存了每一个参数的具体内容。
二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

相关推荐
栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群