C语言详解(二) - 函数


1. 函数的介绍

函数是一个程序的部分代码,用来实现某些特定的功能,与主main函数分离,使程序结构模块化,代码更加清晰。
思想是高内聚低耦合。


2. 函数的种类

2.1 库函数

C语言中包含了许多种类的库函数,把一些实现特定功能的代码(如:输入输出、字符串比较、数学中的一些函数实现、申请内存等)封装成一个个函数,方便我们使用。在使用某个函数时只需要知道它在哪个库函数中,然后在自己程序的开始添加相应的库函数即可。
.h结尾的文件是头文件

输入输出库函数stdio.h
字符处理库函数ctype.h
字符串处理库函数string.h
数学库函数math.h
内存分配库函数stdlib.h
时间处理库函数time.h
布尔库函数stdbool.h
其他库函数

2.2 自定义函数

除了C语言提供的基本的库函数,我们还可以自己实现一个个函数,即自定义函数,以此来满足更具体的、专一的需求,同时也可以了解函数实现的原理,增长自身的能力。


3. 函数的定义与声明

3.1 定义

对函数的返回值类型函数名、具有的参数实现的功能进行定义。定义之后,便具有了一个可以实现一定功能的函数。
格式:

函数返回值类型  函数名(变量数据类型 变量名1...... ,变量数据类型  变量名2){
    函数功能实部分
}

函数1.png
一个具体的函数定义:

//实现两个整数的相加并以整型返回结果
int Add_sum(int a, int b){
    int ret = a + b;
    return ret;
}

函数一般都会有一个返回值,void除外。
void为返回类型意为函数没有返回值,可以在程序的末尾写上return;或者不写return;,对这个函数无影响。
void*为返回值意为,函数返回一个不指向任何类型的为"空"的指针。

一些返回值类型举例
char字符型
int整型
float单精度浮点型
double双精度浮点型
char*字符指针
int*整型指针
float*浮点型指针
double*浮点型指针

函数名的命名与变量的命名相同,由大小写字母,数字和下划线组成,且开头不能是数字。

3.2 声明

函数的定义可以放在程序的开头,但函数的定义一般会跨越多行,当有多个函数被定义时main函数前面将会变得繁杂,不利于我们写程序。函数一般满足先声明后使用

#include <stdio.h>

//函数定义 - 两个整数相加
int Add_sum(int a, int b){
    return a + b;
}

int main(){
    int a = 3;
    int b = 4;
    int sum = Add_sum(a,b);
    
    printf("%d\n",sum);
    return 0;
}

运行结果:
.png

  • 一种写法是把函数定义均放在main函数后面,但由于程序代码是从上往下依次进行,所以在main函数后面的自定义函数不能被main函数有效调用。解决方法是在main函数之前进行相应的函数声明。
  • 函数的声明一般放在程序的main函数之前,放在程序的开头部分,与函数定义不同,只需要由函数头和结尾分号组成
#include <stdio.h>
//函数声明
int Add_sum(int a, int b);

int main(){
    int a = 3;
    int b = 40;
    int sum = Add_sum(a,b);
    
    printf("%d\n",sum);
    return 0;
}
//函数定义 - 两个整数相加
int Add_sum(int a, int b){
    return a + b;
}

运行结果:
.png
另一种写法是:
把所有的函数定义都写在一个.c文件中,把所有的函数声明都写在一个.h文件中。

程序更加模块化
程序易于与他人协作


4. 函数的参数

4.1 实际参数(实参)

传递给函数的具有确定的值的参数称为实参
实参可以是常量变量函数表达式等。

4.2 形式参数(形参)

函数名后括号内定义的各种变量

函数声明时函数返回类型、函数名、函数的形参的数据类型是必需的,而形参中的变量名可有可无的。也就是说函数声明关心的是函数返回类型函数名、函数的形参的数据类型,不关心形参的变量名是什么,可以省略,但一般与函数头保持一致

4.3 实参与形参的区别

  • 在函数被调用、实参把值传递给形参时,形参才在内存中被创建,才开始有效。在被调函数执行完返回时,包括形参在内的、在被调函数内有效的所有变量被销毁释放被占用的内存空间
  • 也就是说实参传递给形参时形参占用了新的内存空间,即实参与形参具有相互独立的储存空间,形参值得改变不会对实参的值产生影响,形参是实参的一份临时拷贝

一个失败的交换两个整数的值的代码

#include <stdio.h>
//交换变量x与y的值
void swap(int x, int y);

int main(){
    int a = 0;
    int b = 0;
    scanf("%d%d",&a, &b);
    swap(a, b);
    
    printf("a = %d b = %d\n", a, b);
    return 0;
}
void swap(int x, int y){
    int t = x;
    x = y;
    y = t;
}

实际运行结果:a与b并没有交换!
.png

若想通过形参改变实参的值,需要得到实参的地址,所以需要使用类型为指针的形参来接收实参的地址,通过间接访问操作符*通过地址改变实参的值。

对交换两个数代码的改进:

#include <stdio.h>
//交换变量x与y的值
void swap(int *px, int *py);

int main(){
    int a = 0;
    int b = 0;
    scanf("%d%d",&a, &b);
    swap(a, b);
    
    printf("a = %d b = %d\n", a, b);
    return 0;
}
//函数swap的形参为两个整型指针
void swap(int *px, int *py){
    int t = *px;
    *px = *py;
    *py = t;
}

运行结果:a与b完成了交换
.png


5. 函数的调用

  • 传值调用
  • 传址调用

5.1 传值调用

实参的值传递给非指针的形参,由于实参与形参具有不同的储存空间,形参也不知道实参的地址,所以形参无法通过实参的地址影响实参的值
形参与实参相互隔绝,没有任何关系。

5.2 传址调用

传址调用实际上也是传值调用,只不过有些特殊,传递的是实参的地址的值
实参的地址传递给指针类型的形参,实参与形参也具有不用的储存空间,但是形参中存放的是实参的地址,所以可以通过储存的实参的地址来影响实参的值。
形参可以通过实参的地址访问实参,形参与实参便联系了起来。

5.3 一个例子(二分查找)

对有序数组的元素进行排序并输出

#include <stdio.h>
//函数声明
int Binary_search(int arr[], int sz, int input)int main(){
    int arr[] = {1,3,5,7,9,10,13,15,17,19};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int input = 0;
    scanf("%d", &input);
    //函数调用
    int index = Binary_search(arr, sz, input);
    if(index != -1){
        printf("找到了,下标为:%d\n",index);
    }
    else{
        printf("没找到\n");
    }
    
    return 0;
}
//函数定义
//二分查找(折半查找),找到了返回数组下标,找不到返回-1
int Binary_search(int arr[], int sz, int input){
    //数组左下标
    int left = 0;
    //数组右下标
    int right = sz - 1;
    //数组中间下标
    int middle = 0;
    while(left <= right){
        middle = left + (right - left)/2;
        //待查找input小于数组中间元素,则input只可能在数组左半边
        if(input < arr[middle]){
            right = middle - 1;
        }
        //待查找input大于数组中间元素,则input只可能在数组右半边
        else if(input > arr[middle]){
            left = middle + 1;
        }
        //待查找input等于于数组中间元素
        else{
            return middle;
        }
    }
    //左下标大于右下标还没有找到则一定找不到了
    return -1;
}

一些运行结果:
YM[BAO_M{}GXI%]EPX$3~N7.png
.png

5.4 嵌套调用

函数可以嵌套调用

#include <stdio.h>

void two_num_swap(int* px, int* py);
void arr_print(int arr[], int sz);
void compare_sort(int arr[], int sz);

int main() {
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    //求数组的长度
    int sz = sizeof(arr) / sizeof(arr[0]);

    printf("排序前:\n");
    arr_print(arr, sz);
    comment_sort(arr, sz);
    printf("排序后:\n");

    arr_print(arr, sz);
    return 0;
}
//
void compare_sort(int arr[], int sz) {
    int i = 0;
    for (i = 0; i < sz - 1; i++) {
        int j = 0;
        for (j = i + 1; j < sz; j++) {
            if (arr[i] > arr[j]) {
                //嵌套调用
                //main函数调用compare_sort函数,compare_sort函数调用two_num_swap函数
                two_num_swap(&arr[i], &arr[j]);
            }
        }
    }
    return;
}
//
void two_num_swap(int* px, int* py) {
    int t = *px;
    *px = *py;
    *py = t;
}
//
void arr_print(int arr[], int sz) {
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

运行结果:
.png

5.5 链式访问(链式调用)

一个函数的返回值作为这个函数或另一个函数的参数。
一个例子:

#include <stdio.h>

int main(){
   
    printf("%d",printf("%d",printf("%d", 1514))); 
    return 0;
}

运行结果:
6@C%TB{MN0IS`TTMSW{T4MR.png

最内层的printf打印1514,返回值为4,。
第二层的printf打印4,返回值为1。
最外层的printf打印1,返回值为1。

scanf的返回值为接受的成功输入个数。
printf返回值是其打印字符的个数,包括空白符(换行符、空格符、水平制表符、回车符)。


6. 递归

6.1 解释:

复杂的问题按照一定的方法一直分解,每次都把问题复杂度降低,最终分解成简单的问题
函数自己调用自己,满足条件时停止调用
只需要少量的代码,就可以实现复杂问题的求解。

6.2 函数递归的条件

  • 有停止递归的条件
  • 每次递归都更接近停止递归的条件

6.3 一个例子(计算字符串的个数)

#include <stdio.h>

int My_strlen(char *pstr);

int main() {
    //字符串,末尾为'\0'
    char str[] = "Hello world!";

    int ret = My_strlen(str);

    printf("%d\n", ret);
    return 0;
}
int My_strlen(char *pstr) {
    //指针pstr指向的字符不是'\0',即字符串没到末尾,字符数+1
    if (*pstr != '\0') {
        return 1 + My_strlen(pstr + 1);
    }
    //指针pstr指向的字符是'\0',即到达了字符串的末尾,字符数不变
    else {
        return 0;
    }
}

运行结果:
3SZ%4N{VJR`{9350IE1TF@H.png

6.4 递归与迭代

6.4.1 解释

循环是迭代的一种,但迭代不一定是循环
一些问题既可以用递归实现,也可以用循环实现。
相同的问题递归实现往往比循环实现会占用更多的时间和更多的内存,如求一个正整数的阶乘,斐波那契数列。
相同的问题,递归实现一般比循环代码简洁
而一些问题只能用递归实现,比如汉诺塔问题
每次函数调用都会在内存的栈上占用内存,但栈的内存是有限的,递归多次调用自身过多时可能会使栈的内存溢出,即栈溢出

6.4.2 求正整数n的阶乘n!

#include <stdio.h>

int main(){
    unsigned int n = 0;
    scanf("%u", &n);
    int ret = Factorial(n);
    
    printf("%d\n", ret);
    return 0;
}
//递归的实现
int Factorial(unsigned int n){
    if(n > 1){
        return n * Factorial(n - 1);
    }
    else{
        return 1;
    }
}

一个运行结果:
.png
循环实现:

#include <stdio.h>

int main() {
    int n = 0;
    int ret = 1;
    int i = 0;
    scanf("%d", &n);
    for (i = 1; i <= n; i++) {
        ret = ret * i;
    }

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

运行结果:
.png

在不考虑数据超出in范围的情况下,求正整数n的阶乘n!递归运行速度慢于循环。

6.4.3 求第n个斐波那契数列

#include <stdio.h>

int Fibonacci(int n);

int main(){
    int n = 0;
    scanf("%d", &n);
    int ret = Fibonacci(n);
    
    printf("%d\n", ret);
    return 0;
}
//递归实现
int Fibonacci(int n){
    if(n > 2){
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
    else{
        return 1;
    }
}

运行结果:

#include <stdio.h>

int Fibonacci(int n);

int main(){
    int n = 0;
    scanf("%d", &n);
    int ret = Fibonacci(n);
    
    printf("%d\n", ret);
    return 0;
}
//循环实现
int Fibonacci(int n){
    int a = 1;
    int b = 1;
    int c = 1;
    while(n >= 3){
        c = a + b;
        a = b;
        b = c;
        n--;
    }
    return c;
}

在不考虑数据超出in范围的情况下,求第n个斐波那契数列递归运行速度慢于循环。



END

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

re怠惰的未禾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值