问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C语言函数详解:从基础概念到递归实现

创作时间:
作者:
@小白创作中心

C语言函数详解:从基础概念到递归实现

引用
CSDN
1.
https://blog.csdn.net/m0_45447650/article/details/141224486

main 函数

一个工程中可以有多个 .c 文件,但是只能有一个 main 函数。

库函数

库函数:

  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

注意:

使用库函数,必须包含 #include 对应的头文件。

库函数及其使用方法可以在以下链接查找:C++库函数

查询工具:MSDN(Microsoft Developer Network)

C/C++官网:http://en.cppreference.com

例:字符串拷贝

#include<string.h>
int main() {
    int arr1[20] = { 0 };
    char arr2[] =  "hello world!" ;
    // 将 arr2 中的内容拷贝到 arr1 中
    // strcpy(destination, source);
    strcpy(arr1, arr2);
    printf(arr1);
    return 0;
}

自定义函数

函数的定义

定义函数:

ret_type func_name(type param , type param , ..){
    函数体
    return 返回值
}

调用函数:

type result = func_name(param1, param2);

其中

  • ret_type 表示函数的返回类型(不能省略);
  • 注意不写返回类型时,默认返回的是 int 类型
  • 如果写了返回类型,函数不返回结果,则默认返回函数的最后一条语句结果
  • 所以主函数 int main() 必须有 return 0,不写虽然不报错,但是不规范
  • func_name 表示函数名;
  • type表示数据类型如int, float;
  • param表示变量名如x, y。

形参和实参

实际参数(实参):

真实传给函数的参数,叫实参。

实参可以是:常量、变量、表达式、函数等。

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形式参数(形参):

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。

形式参数当函数调用完成之后就自动销毁了。

因此形式参数只有在函数中有效。

函数的调用

传值调用:

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

传址调用:

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

总结

函数调用部分传入的是实参, 函数定义部分的参数是形参。

当实参传递给形参的时候,形参是实参的一份临时拷贝

对形参的修改不会影像实参。

但是如果调用函数时,传入的实参是地址,那函数则可以直接对地址中的内容进行修改。

例子:交换 a 和 b 的值

// 形参定义指针变量
int Swap(int* px, int* py) {
    int temp;
    temp = *px;
    *px = *py;
    *py = temp;
}
int main() {
    int a = 10;
    int b = 20;
    printf("交换前 %d  %d\n", a, b);
    // 实参传入变量地址
    Swap(&a, &b);
    printf("交换后 %d  %d\n", a, b);
    return 0;
}

输出:

注意:由于数组传参只会传递数组的第一个元素的地址,所以在函数内部无法计算数组的长度及数组的元素个数。

函数的嵌套调用和链式访问

函数和函数之间可以根据实际的需求进行组合,也就是相互调用。

嵌套调用

int Add(int x, int y) {
    return x + y;
}
void Sub(int x, int y) {
    printf("%d", Add(x, y));
}
int main() {
    int a = 10;
    int b = 20;
    Sub(a, b);
    return 0;
}

函数可以嵌套调用,但是不能嵌套定义,即在函数内部定义函数。

如下就是嵌套定义

正确定义方式

链式访问

一个函数的返回值做为另一个函数的参数。

链式访问的前提是,函数要有返回值。

如下就是链式访问,但是为什么输出结果是4321呢?

因为 printf 的返回值是字符打印的个数。

所以第一次打印了 43 , 第二次打印了 43 的长度 2 ,第三次打印的 2 的长度 1.

函数的声明和定义

函数的声明:

告诉编译器有一个函数叫什么,参数类型是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。

函数的声明一般出现在函数的使用之前。要满足先声明后使用。

函数的声明一般要放在头文件中的。

函数的定义:

函数的定义是指函数的具体实现,交代函数的功能实现。

即,如果把函数定义在主函数后面,有的编译器解析主函数会发现未定义的函数,这时, 就要在主函数前进行函数的声明。

// 函数的声明和定义
// 函数的声明
int Add(int, int);
int main() {
    int a = 10;
    int b = 20;
    int sum = Add(a, b);
    printf("%d\n", sum);
    return 0;
}
// 函数的定义
int Add(int x, int y) {
    return x + y;
}

“函数的声明一般要放在头文件中”是什么意思呢?

新建 Add.h 文件和 Add.c 文件 , 将函数的声明放到 “.h” 文件中,将函数的定义放到 “.c” 文件中。

想在主函数所在的源文件中使用 Add 函数,只需要 包含头文件就可以(自己定义的头文件用双引号引起来, 库的头文件是尖括号)。

// 函数的声明和定义
#include "Add.h"
int main() {
    int a = 10;
    int b = 20;
    int sum = Add(a, b);
    printf("%d\n", sum);
    return 0;
}

该方法就适用于项目比较大,函数多时, 分工合作、分而治之,使项目更有条理。

使用场景

可用来生成静态库(只提供别人使用,不能修改)

项目(项目中包含函数的 .h 和 .c 文件)右键→属性→常规→配置类型 ,将原来的应用程序改为静态库,点击确定。

Ctrl+Fn+F7 编译生成静态库,如果你找不到静态库的位置,可以Ctrl+Fn+F5 报错然后显示静态库的位置。

如何使用这个静态库呢?

新项目中添加头文件和源文件(将 .h 文件和 .lib 文件放到新项目下)。此时我们手里只有含有 main函数的源文件和头文件,如何调用静态库来使用里面的函数呢?

此时,直接使用是会报错的。

需要在头文件下添加一行代码

#pragma comment(lib, "Add.lib")

然后再运行,就可以调用 静态库 .lib 文件中的函数啦。

函数递归

什么是递归?

程序调用自身的编程技巧称为递归。

递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的。

一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

只需少量的程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

例子:接收一个整型值(无符号),按顺序打印

// %d 打印有符号整数,有正负数
// %u 打印无符号整数
void func(unsigned int n) {
    if (n > 9) {
        func(n / 10);
    }
    printf("%d ", n % 10);
}
int main() {
    // 接收一个整型值(无符号),按顺序打印
    unsigned int num = 0;
    scanf("%u", &num);
    func(num);
    return 0;
}

递归的两个必要条件

存在限制条件, 当满足这个限制条件的时候,递归便不再继续。

每次递归调用之后越来越接近这个限制条件。

每一次函数的调用都会在栈区申请空间,如果递归没有终止条件,那就会造成栈溢出

如何解决栈溢出的问题呢?

使用static对象代替 nonstatic 局部对象。在递归函数设计中,可以使用static对象代替 nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

例子:在不创建临时变量的情况下,打印字符串的长度。

int my_strlen(char * str) {
    if (*str != '\0') {
        return 1 + my_strlen(str+1);
    }
    else
        return 0;
}
int main() {
    char arr[] = "hdfkjs";
    int len = my_strlen(arr);
    printf("%d\n", len);
    return 0;
}

例子:n 的阶乘

// n 的阶乘
int factorial(int n) {
    if (n > 1) {
        return n * factorial(n - 1);
    }
    else
        return 1;
}
int main() {
    int num;
    scanf("%d", &num);
    int result = factorial(num);
    printf("%d\n", result);
    return 0;
}

例子:求第n个斐波那契数

// 斐波那契
// 1 1 2 3 5 8 13 21 ..
int fib(int n) {
    if (n > 2) {
        return fib(n-1) + fib(n - 2);
    }
    else
        return 1;
}
int main() {
    int num;
    scanf("%d", &num);
    int result = fib(num);
    printf("%d\n", result);
    return 0;
}

但是这种 递归 的方法时间复杂度太高。用迭代的方式会快很多。

int fib(int n) {
    int	a = 1;
    int b = 1;
    int c = 0;
    while (n >= 3) {
        c = a + b;
        a = b;
        b = c;
        n--;
    }
    return c;
}
int main() {
    int num;
    scanf("%d", &num);
    int result = fib(num);
    printf("%d\n", result);
    return 0;
}
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号