当前位置: 首页 > news >正文

C 语言第 12 天学习笔记:函数进阶应用与变量特性解析

C语言学习笔记(第12天)

内容提要

  • 函数
  • 函数的嵌套关系
  • 函数的递归调用
  • 数组做函数参数
  • 变量的作用域
  • 变量的生命周期

函数的嵌套调用

定义

函数不允许嵌套定义,但是允许嵌套调用。

正确示例:函数嵌套调用
void a()
{
}// 函数的嵌套调用
void b()
{a();
}int main()
{printf(...);
}
错误示例:函数嵌套定义
// 函数的嵌套定义
void a()
{void b(){...}
}

嵌套调用:在被调函数内又主动去调用其他函数,这样的函数调用形式,称之为嵌套调用。

实战案例

案例1:判断素数

需求:编写一个函数,判断给定的3~100的正整数是否是素数,若是返回1,否则返回0。

分析
素数:又被称作质数,素数是大于1的自然数,除了1和它本身以外,不能被其他数整除。

代码

#include <stdio.h>// 判断是否为素数(3~100)
int is_prime(int n) {int flag = 1;for (int i = 2; i <= n/2; i++) {if (n % i == 0) {flag = 0;break;}}return flag;
}int main() {for (int i = 3; i <= 100; i++) {if (is_prime(i)) printf("%-4d", i);}return 0;
}
案例2 数组元素查找

需求:通过控制台输入一个整数,校验这个整数在一个已知数组中的位置,如果找到,返回下标,找不到,返回-1。

代码

// 在数组中查找元素位置
int find_index(int arr[], int len, int n) {int index = -1;for (int i = 0; i < len; i++) {if (arr[i] == n) {index = i;break;}}return index;
}
案例3 四数求最大值(嵌套调用)

需求:输入四个整数,找出其中最大的数,用函数嵌套来实现,要求每次只能两个数比较。

代码

#include <stdio.h>// 函数声明
int max_2(int, int);
int max_4(int, int, int, int);/**
* 求2个数中的最大值
*/
int max_2(int a, int b)
{return a > b ? a : b;
}/**
* 求4个数中的最大值
*/
int max_4(int a, int b, int c, int d)
{return max_2(a, b) > max_2(c, d) ? max_2(a, b) : max_2(c, d);
}int main(int argc, char* argv[])
{int a, b, c, d;printf("请输入4个整数:\n");scanf("%d%d%d%d", &a, &b, &c, &d);printf("%d,%d,%d,%d中最大数是%d\n", a, b, c, d, max_4(a, b, c, d));return 0;
}

函数的递归调用

定义

  • 递归调用的含义:在一个函数中,直接或者间接调用了函数本身,就称之为递归调用。本质上还是函数的嵌套调用。

    • 直接调用(推荐):a() → a();
    • 间接调用:a() → b() → a();a() → b() → ... → a();
  • 递归调用的本质:递归调用是一种循环结构,它不同于我们之前学过的whilefordo..while这样的循环结构,这些循环结构是借助于循环变量;而递归调用是利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。

注意事项

  1. 递归调用必须要有设置出口,一定要想办法终止递归(否则会产生死循环)
  2. 对终止条件的判断一定要放在函数递归之前。(先判断,在执行)
  3. 进行函数的递归调用。
  4. 函数递归的同时一定要将函数调用向出口逼近

递归的底层实现

C语言函数递归的底层实现依赖于程序调用栈(Call Stack),其核心过程是通过栈帧(Stack Frame)的创建与销毁来管理函数的多次调用。具体底层逻辑如下:

  1. 栈帧的作用:每次函数调用(包括递归调用自身)时,系统会在内存的栈区为该次调用分配一块独立的内存空间,即栈帧。栈帧用于存储:

    • 函数的参数值
    • 函数内的局部变量
    • 调用结束后返回的地址(即该次调用结束后,程序应回到的原执行位置)
  2. 递归调用的底层流程:以计算阶乘 n! 的递归函数 fac(n) 为例(fac(n)=n * fac(n-1),终止条件 fac(1) = 1):

    • 第一步:调用 fac(3) 时,系统创建栈帧1,存储参数 n=3、局部变量及返回地址(假设主函数调用处),然后执行函数体,发现需要调用 fac(2)
    • 第二步:调用 fac(2) 时,系统在栈顶创建栈帧2,存储参数 n=2 及新的返回地址(fac(3) 中等待结果的位置),暂停 fac(3) 的执行,转去执行 fac(2)
    • 第三步:同理,调用 fac(1) 时,创建栈帧3,存储 n=1 及返回地址(fac(2) 中等待结果的位置)。
    • 终止与回溯:当执行到 fac(1) 时,触发终止条件,直接返回结果 1 。此时栈帧3被销毁,程序回到栈帧2中继续执行(计算 2*1=2),栈帧2销毁后回到栈帧1(计算 3*2=6),最终栈帧1销毁,返回结果给主函数。
  3. 关键特点

    • 栈的“后进先出”特性:递归调用时,新的栈帧总是压在栈顶,而只有最顶层的栈帧(即最后一次调用)执行完毕并返回结果后,上层的栈帧才能继续执行(对应递归的“回溯”过程)。
    • 栈溢出风险:若递归次数过多(如未设置终止条件或终止条件无法触发),会导致栈帧不断创建,超出栈区内存上限,触发“栈溢出(Stack Overflow)”错误,程序崩溃。

简言之,递归的底层本质是通过栈帧的层层创建(递推)和销毁(回溯),实现函数自身的多次调用管理,而栈的特性保证了递归调用的顺序和结果传递。

案例

案例1 递归求年龄

需求:有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2个人大2岁。问第2个人,说比第1个人大2岁。最后问第1个人,他说是10岁。请问第5个人多大。
在这里插入图片描述

代码

#include <stdio.h>/**
* 定义一个函数,求年龄
* @param n 第n个人
* @return 第n个人的年龄
*/
int age(int n)
{// 创建一个变量,存储当前这个人的年龄int _age;// 出口if (n == 1) // 第1个人{_age = 10;}else if (n > 1){_age = age(n - 1) + 2; // 当前这个人的年龄 = 上一个人的年龄 + 2}return _age;
}int main(int argc, char* argv[])
{int n = 5;printf("第%d个人的年龄是%d岁!\n", n, age(n));return 0;
}
案例2 递归求阶乘

需求:求n的阶乘

分析:
在这里插入图片描述

代码

#include <stdio.h>/**
* 创建一个函数,实现n的阶乘
* @param n 阶乘上限
* @return n的阶乘结果
*/ 
size_t fac(int n)
{// 定义一个变量,用来接收阶乘结果size_t res;// 出口校验if (n <= 0){printf("n的范围不能是0及以下数字!\n");return -1;}else if (n == 1) // 出口{res = 1; // 出口设置}else {res = fac(n - 1) * n;}return res;
}int main(int argc, char* argv[])
{size_t n;printf("请输入一个整数:\n");scanf("%lu", &n);printf("%lu的阶乘结果是%lu\n", n, fac(n));return 0;
}

快速排序算法 (递归实现)

快速排序是一种高效的分治(Divide and Conquer)排序算法。它的核心思想是通过选取一个基准值(pivot),将数组划分为两个子数组:一个子数组的所有元素比基准值小,另一个子数组的所有元素比基准值大,然后递归地对子数组进行排序。

快速排序的基本步骤
  1. 选择基准值(Pivot Selection):从数组中选择一个元素作为基准值。常见的选择方式包括:

    • 第一个或最后一个元素(简单但可能效率不高,尤其在已排序或接近排序的数组中)。
    • 随机选择一个元素(减少最坏情况概率)。
    • 三数取中法(如第一个、中间、最后一个元素的中位数,提高分区均衡性)。
  2. 分区(Partition):重新排列数组,使得:

    • 所有比基准值小的元素移到基准值的左侧。
    • 所有比基准值大的元素移到基准值的右侧。
      分区完成后,基准值处于其最终排序后的正确位置。(注:分区是快速排序的关键步骤,常见的实现有Lomuto分区和Hoare分区方案。)
  3. 递归排序(Recursion):对基准值左侧的子数组和右侧的子数组递归地调用快速排序。递归的终止条件是子数组的长度为0或1(已有序)。

  4. 合并结果(Combine):由于每次分区后基准值已位于正确位置,且左右子数组通过递归排序完成,因此无需显式合并操作,整个数组自然有序。

总结:通过一个基准值(pivot)不断拆分数组,直到子数组无法再拆分(即子数组长度为1或0),此时整个数组就有序了。

代码
#include <stdio.h>/**
* 快速排序函数(递归实现)
* @param arr 待排序数组
* @param n 数组长度
*/
void QSort(int arr[], int n)
{// 出口限制,如果排序数组的大小<=1,此时数组就不再排序if (n <= 1) return;// 定义两个标记,i从左向右,j从右向左,锁定排序数组的区间int i = 0, j = n - 1;// 选择排序数组的第一个元素作为基准值(可优化为取中法)int pivot = arr[0];// 分区过程while (i < j){// 从右向左查找第一个小于等于基准值的元素(<=基准值的数据)while (i < j && arr[j] > pivot) j--;// 从左向右查找第一个大于基准值的元素(>基准值的数据)while (i < j && arr[i] <= pivot) i++;// 交换这两个元素if (i < j){int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}// 将基准值更新到正确位置(i == j)arr[0] = arr[i];arr[j] = pivot;// 切割子数组QSort(arr, i); // 左边部分QSort(arr + i + 1, n - i - 1); // 右边部分}
}int main(int argc, char* argv[])
{int arr[] = { 23,45,56,24,78,22,19 };int n = sizeof(arr) / sizeof(arr[0]);QSort(arr, n);// 函数的参数是数组,传递的是这个数组的首地址,函数中可以修改实参数据,符合这种条件的参数称之为输出型参数// 此时是可以直接遍历数组,此时的数组已经经过了修改for (int i = 0; i < n; i++){printf("%-4d", arr[i]);}printf("\n");return 0;
}

数组做函数参数

1. 传递机制

  • 地址传递:数组作为参数时,实际传递的是首地址(数组降级为指针)。
  • 特点形参和实参指向同一块内存,修改形参会影响实参。

这种传递方式称为“地址传递”(或“指针传递”),它与“值传递”的不同:

  • 值传递:传递数据的副本,修改形参不影响实参

  • 地址传递:传递数据的地址,通过形参可以修改实参。“地址传递”是逻辑上的说法,强调 传递的是地址,而不是数据本身。数据本质上还是值传递。

2. 必须传递数组长度的原因

  1. 数组形参退化为指针,无法通过sizeof获取实际长度。
  2. 防止数组下标越界访问(访问非法内存),引发未定义行为(如程序崩溃、数据损坏)。
  3. 提高函数通用性(可处理不同长度的数组)。

案例:

#include <stdio.h>/*** 定义一个函数,将数组作为参数*/
void fun(int arr[], int len) // 数组传参会被降级为指针,实际传递的是地址值
{for (int i = 0; i < len; i++) printf("%-4d",arr[i]); printf("\n");
}void main()
{int arr[] = {11,22,33,44,55};int len = sizeof(arr) / sizeof(arr[0]);fun(arr,len);
}

3. 特殊情况:字符数组

  • 字符串自带结束标志\0,无需额外传递长度:
    void print_str(char arr[]) {int i = 0;while (arr[i] != '\0') { // 以\0作为结束判断printf("%c", arr[i]);i++;}
    }
    

4. 案例:数组元素比较

需求:

有两个数组a和b,各有5个元素,将它们对应元素逐个地相比(即a[0]与b[0]比,a[1]与b[1]比……)。如果a数组中的元素大于b数组中的相应元素的数目多于b数组中元素大于a数组中相应元素的数目(例如,a[i]>b]i]6次,b[i]>a[i] 3次,其中i每次为不同的值),则认为a数组大于b数组,并分别统计出两个数组相应元素大于、等于、小于的个数。

#include <stdio.h>
// 比较两个数组对应元素的大小关系
int get_large(int x, int y) {if (x > y) return 1;if (x < y) return -1;return 0;
}int main() {int a[5] = {12,12,10,18,5};int b[5] = {111,112,110,8,5};int max=0, min=0, equal=0;for (int i=0; i<5; i++) {int res = get_large(a[i], b[i]);if (res == 1) max++;else if (res == -1) min++;else equal++;}printf("a>bb:%d次, a<b:%d次, 相等:%d次\n", max, min, equal);return 0;
}

5. 案例:自定义函数实现strcpy效果

#include <stdio.h>/*** 自定义一个字符串拷贝函数* @param source 源数组* @param dest  需要替换的字符串*/ 
void _strcpy(char source[], const char dest[])
{// 遍历源数组,以源数组的大小作为循环for (int i = 0; source[i] != '\0'; i++){// 获取dest相应位置的字符source[i] = dest[i];}
}int main(int argc,char *argv[])
{char str[] = "hello world!";printf("%s\n", str);_strcpy(str,"hi yifan!");printf("%s\n", str);_strcpy(str,"娇娇");printf("%s\n", str);return 0;
}

四、变量的作用域

引入
我们在函数设计的过程中,经常要考虑对于参数的设计,换句话说,我们需要考虑函数需要几个参数,需要什么类型的参数,但我们并没有考虑函数是否需要提供参数,如果说函数可以访问到已定义的数据,则就不需要提供函数形参。那么我们到底要不要提供函数形参,取决于什么?答案就是变量的作用域(如果函数在变量的作用域范围内,则函数可以直接访问数据,无需提供形参)

1. 定义

变量的有效范围,超出范围则无法访问。

2. 变量分类(按作用域)

类型定义位置作用域初始值
全局变量函数外部从定义处到本源文件结束默认为0(int/float)、\0(char)、NULL(指针)
局部变量函数内/形参/复合语句内所在函数或复合语句内随机值(需手动初始化)

局部变量详解

说明作用域初始值
形式参数(形参)函数作用域随机值,需要手动赋初值
函数内定义的变量函数作用域随机值,需要手动赋初值
复合语句中定义的变量块作用域随机值,需要手动赋初值
for循环表达式1定义的变量块作用域随机值,需要手动赋初值

3. 全局变量的优缺点

优点:

  1. 利用全局变量可以实现一个函数对外输出的多个结果数据。
  2. 利用全局变量可以减少函数形参的个数,从而降低内存消耗,以及因为形参传递带来的时间消耗。

缺点:

  1. 全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。
  2. 过多的全局变量会引起程序的混乱,操作程序结果错误。
  3. 降低程序的通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变量。
  4. 违反了“高内聚,低耦合”的程序设计原则。

总结:

​ 我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实参+形参的方式产生联系。

4. 同名变量处理原则

  • 就近原则:局部变量与全局变量同名时,优先使用局部变量。
    int a = 10; // 全局变量
    int main() {int a = 20; // 局部变量printf("%d", a); // 输出20(就近原则)return 0;
    }
    

五、变量的生命周期

1. 定义

变量在内存中存在的时间(从内存分配到释放)。
根据变量存在的时间不同,变量可分为静态存储方式动态存储方式
在这里插入图片描述

2. 存储类型与内存区域

语法:

变量的完整定义格式: [存储类型] 数据类型 变量列表;
  • auto
    auto存储类型只能修饰局部变量,被auto修饰的局部变量是存储在动态存储区(栈区和堆区)的。auto也是局部变量默认的存储类型。

  • static
    修饰局部变量:局部变量会被存储在静态存储区。局部变量的生命周期被延长。但是作用域不发生改变,不推荐

    修饰全局变量:全局变量的生命周期不变,但是作用域衰减,一般限制全局变量只能在本源文件内访问,其他文件不可访问。

    修饰函数:被static修饰的函数,只能被当前文件访问,其他引用该文件的文件是无法访问的,有点类似于java中private

  • extern
    外部存储类型:只能修饰全局变量,此全局变量可以被其他文件访问,相当于扩展了全局变量的作用域。

    extern修饰外部变量,往往是外部变量进行声明,声明该变量是在外部文件中定义的。起到一个标识作用。函数同理。

    demo01.c

     #include "demo01.h"int fun_a = 10;int fun1(){..}
    

    demo02.c

     #include "demo01.h"// 声明访问的外部文件的变量extern int fun_a;// 声明访问的外部文件的函数extern int fun1();int fun2();
    
  • register

    寄存器存储类型:只能修饰局部变量,用register修饰的局部变量会直接存储到CPU的寄存器中,往往将循环变量设置为寄存器存储类型(提高读的效率)

     for (register int i = 0; i < 10; i++){...}
    
面试题
static关键字的作用
  1. static修饰局部变量,延长其生命周期,但不影响局部变量的作用域。
  2. static修饰全局变量,不影响全局变量的生命周期,会限制全局变量的作用域仅限本文件内使用(私有化);
  3. static修饰函数:此函数就称为内部函数,仅限本文件内调用(私有化)。static int funa(){..}

总结

存储类型修饰对象存储区域生命周期
auto局部变量动态存储区(栈)函数调用期间
static局部变量静态存储区整个程序运行期间
static全局变量/函数静态存储区整个程序运行期间(作用域仅限本文件)
extern全局变量/函数静态存储区整个程序运行期间(作用域扩展到其他文件)
register局部变量CPU寄存器函数调用期间(提高访问速度)

六、章节作业(编程题精选)

  1. 编写一个函数,通过输入球的半径,返回球的体积;
  2. 编写一个函数,通过输入一个数字字符,返回该数字
  3. 编写一个函数,输入四个数据分别表示2个点的x,y坐标,返回两点之间的距离;
  4. 编写一个函数,通过参数输入一个整型数,返回该数各位上数字的平方和;
  5. 编写一个函数,通过参数输入x的值,计算如下的数学函数值,当 x>5时, f(x) = 4x+7;否则 f(x) = -2x+3,返回结果值
  6. 设计一个函数,用来求出多个数据的平均值;
  7. 设计一个函数,用来查找一个字符串中某个字符的位置;
  8. 设计一个函数,把一个整型数字转成对应的字符串格式
  9. 设计一个函数,统计字符串中大写字母的个数
  10. 设计两个函数,分别实现strlen,strcmp 的功能;
  11. 编写函数,用于判断输入的字符是不是个数字。是返回1,不是返回0.
  12. 设计一程序,实现一个简单的计算器。
    要求:有菜单函数 和加、减、乘、除的函数
    主函数调用这些函数实现程序的功能.要求菜单函数能够输出如下的界面

1、加法 2、减法 3、乘法 4、除法 0.退出

  1. 设计函数实现冒泡排序;

思考题【选做】

  1. 编写一个函数,将数组中的数据首尾互换
  2. 一个台阶总共有n级,如果一次可以跳1级,也可以跳2级,还可以跳3级。求总共有多少总跳法《并打印跳法》,用递归实现。

**注:**后续笔记将围绕C语言知识展开,建议每日实操时长不少于3小时

http://www.lryc.cn/news/603857.html

相关文章:

  • 每日学习笔记记录(分享更新版-凌乱)
  • imx6ull-驱动开发篇2——字符设备驱动开发步骤
  • 网络通信基础(一)
  • Redis 跨主机连接超时分析:从网络波动到架构优化
  • 使用鼠标在Canvas上绘制矩形
  • 【C++算法】80.BFS解决FloodFill算法_岛屿数量
  • 《Java 程序设计》第 9 章 - 内部类、枚举和注解
  • 实在智能Agent智能体荣登全球“Go_Global_AI_100”百强榜,中国AI走向世界!
  • STM32——HAL库
  • 什么是EasyVR shield 3?如何设置EasyVR shield 3
  • 大模型应用开发模拟面试
  • 用动态的观点看加锁
  • TCMalloc 内存分配原理简析
  • 2-verilog-基础语法
  • Coze Studio概览(三)--智能体管理
  • sqli-labs通关笔记-第24关 SQL二次注入(单引号闭合)
  • 硬件学习笔记--73 电能表新旧精度等级对应关系
  • debug redis里面的lua脚本
  • Spring Boot 防重放攻击全面指南:原理、方案与最佳实践
  • ElasticSearch 的3种数据迁移方案
  • 在Word和WPS文字中把全角数字全部改为半角
  • Vue2学习-MVVM模型
  • Spring Boot 简单接口角色授权检查实现
  • C++入门知识学习(上)
  • 嵌入式学习日志(十一)
  • css3之三维变换详说
  • SQL Server中的分页查询
  • leetcode热题——螺旋矩阵
  • 第十一天:不定方程求解
  • 镁金属接骨螺钉注册检测:骨科植入安全的科学基石