C语言 函数

1.函数概念

  • 函数简单来说就是一连串语句,这些语句被 组合在一起,并被指定了一个名字。虽然“函数”这个术语来自数 学,但是C语言的函数不完全等同于数学函数。在C语言中,函数不一 定要有参数,也不一定要计算数值。
  • 函数是C程序的构建块。每个函数本质上是一个自带声明和语句的小程 序。可以利用函数把程序划分成小块,这样便于人们理解和修改程 序。由于不必重复编写要多次使用的代码,函数可以使编程不那么单 调乏味。
  • 此外,函数可以复用:一个函数最初可能是某个程序的一部 分,但可以将其用于其他程序中。
  • 到目前为止,我们的程序都只是由一个main 函数构成的。本章将学 习如何编写除main 函数以外的其他函数,并更加深入地了解main 函数本身。

2.函数的定义和调用

  • 介绍定义函数的规则之前,先来看几个简单的定义函数的程序
    • 假设我们经常需要计算两个double 类型数值的平均值。C语言库没 有“求平均值”函数,但是可以自己定义一个。
double average(double a, double b)
{
    return (a + b) / 2;
}

本例说明:

  • 返回类型:函数开始处放置的单词double,每次调用该函数时返回数据的类型。返回类型可以为任意类型,可以为整型、指针,也可以为空void
  • 形式参数(parameter):标 识符a 和标 识符b ,表示在调用average 函数时需要提供的两个数。每一个形式参数都必须有类型 (正像每个变量有类型一样),这里选择了double 作为a 和b 的类 型。(这看上去有点奇怪,但是单词double 必须出现两次,一次为 a 而另一次为b 。)
  • 函数体:每个函数都有一个用花括号括起来的执行部分,average 函数的函数体由一条return 语句构成。执行 这条语句将会使函数“返回”到调用它的地方,表达式(a+b)/2 的 值将作为函数的返回值。
  • 实际参数(argument):为了调用函数,需要写出函数名及跟随其后的实际参数 列表,例如,average(x, y) 是对average 函数的 调用。实际参数用来给函数提供信息;在此例中,函数average 需 要知道是要求哪两个数的平均值。调用average(x, y) 的效果就是 把变量x 和y 的值复制给形式参数a 和b ,然后执行average 函数 的函数体。实际参数不一定要是变量,任何正确类型的表达式都可 以,average(5.1, 8.9) 和average(x/2, y/3) 都是合法的函 数调用。
int main()
{
    double x=12.3 , y=3.14;
    double z = average(x,y);
}
/*
1.以变量x 和y 作为实际参数调用average 函数
2.把x 和y 的值复制给a 和b
3.average 函数执行自己的return 语句,返回a 和b 的平均值,并把返回结果赋给z
*/
  • 求两个给定整数的最大值
int max(int x, int y) // 该函数接收两个整型参数,并返回一个整型数据
{
    int z;
    z = x>y ? x : y;
    return z;
}
// 返回类型为int型,return 类型也为int型 

2.1 函数的定义

  • 看过上面的几个例子,相信对函数的定义有了清晰的认识

[函数定义]

  • 返回类型:函数不能返回数组,但关于返回类型没有其他限制,当函数的返回类型为 void 时,表示该函数不返回任何数据。
  • 形式参数:函数名后边有一串形式参数列表。 需要在每个形式参数的前面说 明其类型,形式参数间用逗号进行分隔。当函数的参数列表为 void 时,表示该函数不需要任何参数。
  • 函数体:函数体可以包含声明和语句,函数体内声明的变量专属于此函数,其他函数不能对这些变量进行检 查或修改
  • 返回类型:关键字 return 表示退出函数。①若函数头中规定有返回数据类型,则 return 需携带一个类型与之匹配的数据;②若函数头中规定返回类型为 void,则 return 不需携带参数。

2.2 函数的调用和声明

  • 函数调用由函数名和跟随其后的实际参数列表组成 
// x,y的值由实参一一对应初始化
int max(int x, int y)
{
    int z;
    z = x>y ? x : y;
    return z;
}

int main(void)
{
    int a = 1;
    int b = 2;
    int m;
    m = max(a, b);    
}

 

  • C语言并没有要求函数的定义必须放置在调用点之前,如果定义放在调用点之后的话,那么编译器是不知道这个函数的参数类型的,也不知道返回类型是什么。
  • 为了避免定义前调用的问题。
    • 一种方法是使每个函数的定义都出现在 其调用之前。
    • 另一种方法是在调用前声明每个函 数。函数声明 (function declaration)使得编译器可以先对函数 进行概要浏览,而函数的完整定义以后再给出。

3.实际参数

  • 复习下形式参数和实际参数:
    • 形式参数 (parameter)出现在函数定义中,它们以假名字来表示函数调用时 需要提供的值
    • 实际参数 (argument)是出现在函数调用中的表达 式
  • 注意:
    • 实参于形参的类型和个数必须一一对应
    • 形参的值由实参初始化
    • 形参与实参位于不同的内存区域,彼此独立

3.1 值传递

  • 实际参数是通过值传递的:调用函数时,计算出每个实 际参数的值并且把它赋值给相应的形式参数。在函数执行过程中,对 形式参数的改变不会影响实际参数的值,这是因为形式参数中包含的 是实际参数值的副本。
    • 考虑这么做的利与弊???

 利:

//此函数用来计算数 x 的 n 次幂

int power(int x, int n)
{
    int i, result = 1;
    for (i = 1; i <= n; i++)
        result = result * x;
        
    return result;
}
  • 因为n只是原始指数的副本 ,所以可以在函数体内修改它,因此就 不需要使用变量 i 了
int power(int x, int n)
{
    int result = 1;
    while (n > 0)
    {
        result = result * x;
        n--;    
    }        
    return result;
}

弊:

/*
假设我们需要一个函数,它把double 型的值分解成
整数部分和小数部分。
因为函数无法返回两个数,所以可以尝试把
两个变量传递给函数并且修改它们
*/
void decompose(double x, int int_part, double frac_part)
{
    int_part = (int) x; 
    frac_part = x - int_part;
}
  • 接下来调用看看会发生什么
#include <stdio.h>

void decompose(double x, int int_part, double frac_part)
{
    int_part = (int)x;
    frac_part = x - int_part;
}

int main()
{
    int i = 0;
    double d = 0;
    printf("调用函数之前: i: %d  d: %lf\n", i, d);
    decompose(3.14, i, d);
    printf("调用函数之后: i: %d  d: %lf\n", i, d);

    return 0;
}
/*
调用函数之前: i: 0  d: 0.000000
调用函数之后: i: 0  d: 0.000000
*/
  • 可见变量 i 和 d 不会因为赋值给int_part 和 frac_part 而受到影响,所以它们在函数调用前后的值是完全一样 的,那么怎么解决这种问题呢?

3.2 指针作为实参

  • 因为C语言用值进行参数传递,所以在函数调用 中用作实际参数的变量无法改变。当希望函数能够改变变量时,C语言 的这种特性就很讨厌了
  • 指针提供了此问题的解决方法:不再传递变量x 作为函数的实际参 数,而是提供&x ,即x的地址
#include <stdio.h>

void decompose(double x, int *int_part, double *frac_part)
{
    *int_part = (int)x;
    *frac_part = x - *int_part;
}
int main()
{
    int i = 0;
    double d = 0;
    printf("调用函数之前: i: %d  d: %lf\n", i, d);
    decompose(3.14, &i, &d);
    printf("调用函数之后: i: %d  d: %lf\n", i, d);

    return 0;
}
/*
调用函数之前: i: 0  d: 0.000000
调用函数之后: i: 3  d: 0.140000
*/
  • 因为 i 和 d 前有取地址运算符& ,所以decompose 函数的实际参数 是指向 i 和 d 的指针 ,而不是 i 和 d 的值

  • 用指针作为函数的实际参数实际上不新鲜,实际上我们很早就已经在 scanf 函数调用中使用过了
int i;
...
scanf("%d", &i);
  • 必须把 & 放在 i 的前面以便给scanf 函数传递指向 i 的指针,指针会告诉scanf 函数把读取的值放在哪里。如果没有 & 运算符,传递 给scanf 函数的将是 i 的值。

  • 编写一个函数,交换两个整数的值
#include <stdio.h>
#include <stdlib.h>

void swap(int *p1, int *p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main()
{
    int x = 5, y = 10;
    swap(&x, &y);
    printf("%d %d\n", x, y); // 10 5

    return 0;
}

3.3 数组作为实参

  • 数组经常被用作实际参数,当形式参数是一维数组时,可以(而 且是通常情况下)不说明数组的长度:
#include <stdio.h>

void fetch(char str[])
{
    strcpy(str, "hello world");
}

int main()
{
    char str[99];
    fetch(str);
    printf("%s\n", str); // hello world

    return 0;
}
  • 函数如何知道数组是多长呢?可惜的是,C语言没有为函数提供任何简便的方法来确定传递给它的数组的长度;如果函数需要,我们必须把长 度作为额外的参数提供出来。
int example(int a[] , int n) 
{
    ...
}

  1. 如果在定义数组时就给定了数组的大小,如 int array[len];则不管数组中初始化了多少个(显然应不大于len)元素,最后的数组中的元素个数都是 len。所以要想获得数组中真实元素的个数,在初始化数组时应注意这一点。
  2. 向子函数传递数组后,然后在子函数内部获取数组长度。先来看一个错误示例程序:
#include <stdio.h>

int getLength(int array[])
{
    int length;
    length = sizeof(array) / sizeof(array[0]);
    return length;
}
int main()
{
    int array[10] = {0};
    int length = getLength(array);
    printf("The length of array is %d\n", length);

    return 0;
}

 结果:

The length of array is 2
  • 这样得到的结果始终都是2,因为数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址
  • 在本示例中,函数名 array 传递到子函数后就完全退化为一个指针,该指针指向的是数组 array 所在的地址,即数组 array 第一个元素 array[0] 所在的地址。也就是说系统只是告诉该函数这个存储空间存有数据,但并没有告诉函数这个数据存储空间有多大
  • 例子中 sizeof(array) 的结果是指针变量 array 所占内存的字节数,具体大小与系统有关,一般在64位机器上占8个字节(32位4个字节,16位2个字节),array[0]是 int 类型,占4个字节,所以结果为2。所以要获得数组的长度最好在数组定义所在的区域内
  • 如果能够将所需的长度作为形参传递到子函数中,显然就不需要在子函数当中另行计算
#include <stdio.h>

void print_array(int *arr, int len);

int main()
{
    int array[] = {5, 2, 0, 1, 3, 1, 4};
    int length = sizeof(array) / sizeof(*array); // 计算数组中的元素个数

    print_array(array, length);
    printf("\n");

    return 0;
}

void print_array(int *arr, int len)
{
    int i = 0;

    for (i = 0; i < len; i++)
    {
        printf("%d ", arr[i]); // 5 2 0 1 3 1 4 
    }
}

4.作用域

  • 所谓作用域(Scope),就是变量的有效范围。本节知识还会在”内存管理“中提到。
  • C语言中所有的变量都有自己的作用域。决定变量作用域的是变量的定义位置。

4.1 局部变量

  • 定义在函数内部的变量称为局部变量(Local Variable)
  • 它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错
int f1(int a)
{
    int b, c; // a,b,c仅在函数f1()内有效
    int m, n;
    return a + b + c;
}

int main()
{
    int m, n; // m,n仅在函数main()内有效
    return 0;
}

注意:

  • 在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数也是一个函数,与其它函数地位平等。
  • 形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。
  • 可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。
  • 在语句块中也可定义变量,它的作用域只限于当前语句块。

4.2 全局变量

  • 在所有函数外部定义的变量称为全局变量(Global Variable),
  • 它的作用域默认是整个程序,也就是所有的源文件,如果给全局变量加上 static 关键字,它的作用域就变成了当前文件,在其它文件中就无效了
int a, b; // 全局变量
void func1()
{
}

float x, y; // 全局变量
int func2()
{
}

int main()
{
    return 0;
}
/*
a、b、x、y 都是在函数外部定义的全局变量。
C语言代码是从前往后依次执行的,由于 x、y 定义在函数 func1() 之后,所以在 func1() 内无效;
而 a、b 定义在源程序的开头,所以在 func1()、func2() 和 main() 内都有效。
*/
  • 局部变量和全局变量的综合示例:
#include <stdio.h>

int n = 10; // 全局变量

void func1()
{
    int n = 20; // 局部变量
    printf("func1 n: %d\n", n);
}

void func2(int n)
{
    printf("func2 n: %d\n", n);
}

void func3()
{
    printf("func3 n: %d\n", n);
}

int main()
{
    int n = 30; // 局部变量
    func1();
    func2(n);
    func3();
    // 代码块由{}包围
    {
        int n = 40; // 局部变量
        printf("block n: %d\n", n);
    }
    printf("main  n: %d\n", n);

    return 0;
}
/*
func1 n: 20
func2 n: 30
func3 n: 10
block n: 40
main  n: 30
*/

由上可知:

  • 当全局变量和局部变量同名时,在局部范围内全局变量被“屏蔽”,不再起作用。或者说,变量的使用遵循就近原则,如果在当前作用域中存在同名变量,就不会向更大的作用域中去寻找变量。
  • func3() 输出 10,使用的是全局变量,因为在 func3() 函数中不存在局部变量 n,所以编译器只能到函数外部,也就是全局作用域中去寻找变量 n。
  • 由{ }包围的代码块也拥有独立的作用域,printf() 使用它自己内部的变量 n,输出 40。

5.程序终止

  • main 函数返回的值是状态码,在某些操作系统中程序终止时可以检 测到状态码。 如果程序正常终止,main 函数应该返回 0;为了 表示异常终止,main 函数应该返回非 0 的值。
  • 在 main 函数中执行 return 语句是终止程序的一种方法,另一种方 法是调用 exit 函数,此函数属于,传 递给 exit 函数的实际参数和 main 函数的返回值具有相同的含义: 两者都说明程序终止时的状态。为了表示正常终止,传递 0。
exit(0);
  • 作为终止程序的方法,return 语句和exit 函数关系紧密,事实上return 表达式 exit(表达式)
  • return 语句和exit 函数之间的差异是:不管哪个函数调用 exit 函数都会导致程序终止,return 语句仅当由 main 函数调用时才会导致程序终止。一些程序员只使用 exit 函数,以便更容易定位程序 中的全部退出点。

6.递归函数

  • 递归概念:如果一个函数内部,包含了对自身的调用,则该函数称为递归函数。
  • 递归问题:
  1. 阶乘。
  2. 幂运算。
  3. 字符串翻转。

要点:

  1. 只有能被表达为递归的问题,才能用递归函数解决。
  2. 递归函数必须有一个可直接退出的条件,否则会进入无限递归。
  3. 递归函数包含两个过程,一个逐渐递进的过程,和一个逐渐回归的过程。
  • 示例:依次输出 n 个自然数。
  • 思路:先输出前面的 n-1 个自然数,再输出最后一个自然数 n 。而要输出前面的 n-1 个自然数,递归调用自身即可。
// 该函数的功能:依次输出 n 个自然数
void f(int n) 
{
    if(n < 0)          // 1,当满足此条件时,不再进行递归。
        return;
        
    f(n-1);            // 2,递归调用自己,输出前 n-1 个数
    printf("%d\n", n); // 3,输出最后一个自然数 n
}
// 幂运算。
#include <stdio.h>

int power(int a, int b) // a:底数  b:指数
{
    if (b == 0)
    {
        return 1;
    }
    if (b == 1)
    {
        return a;
    }
    else
    {
        return a * power(a, b - 1);
    }
}

int main()
{
    int i;
    i = power(2, 10);
    printf("%d\n", i);

    return 0;
}
// 利用递归函数求阶乘
#include <stdio.h>

int jie(int n)
{
    if (n == 1)
        return 1;
    return n * jie(n - 1); // 5!=5*4*3*2*1
}

int main()
{
    int i = jie(5);
    printf("i:%d\n", i);

    return 0;
}
/*
计算一个数字的每位之和(递归实现)
例如:
递归函数a(n),调用a(256),则返回2+5+6的和为13
*/
#include <stdio.h>

int sum(int n)
{
    while (n > 9)
    {
        return n % 10 + sum(n / 10);
    }
}

int main()
{

    int n = 0;
    scanf("%d", &n);
    int ret = sum(n);
    printf("%d\n", ret);

    return 0;
}
  • 递归调用时,函数的栈内存的变化如下图所示。可见,随着递归函数的层层深入,栈空间逐渐往下增长,如果递归的层次太深,很容易把栈内存耗光。
  • 层层递进时,问题的规模会随之减小,减小到可直接退出的条件时,函数开始层层回归。

7.main函数命令行参数

  • C语言中 main 函数称之为主函数,操作系统总是将 main 函数作为应用程序的开始,将 main 函数的返回值作为程序的退出状态
// main函数有时候会写出这样子
#include <stdio.h>
int main(int argc, char *argv[])
{
}
  • 我们给main函数传递两个参数,argc 和 argv。
    • argc 是 int 类型的,它表示的是命令行参数的个数。不需要用户传递,它会根据用户从命令行输入的参数个数,自动确定。
    • argv 是 char** 类型的,有时也写成 char* argv[ ] , 它的作用是存储用户从命令行传递进来的参数。它的第一个成员是用户运行的程序名字
#include <stdio.h>

int main(int argc, char **argv)
{
    printf(" argc is %d \n argc is :\n", argc);
    for (int i = 0; i < argc; i++)
    {
        printf("%s ", argv[i]);
    }

    return 0;
}
  • 通过命令行参数,程序可以根据用户的需求动态地调整其行为。例如,可以通过不同的命令行参数来指定程序的输入文件、输出文件。在编写命令行工具时,命令行参数也是很重要的。用户可以通过不同的命令行参数来指定不同的操作和选项

8.指针函数

  • 指针函数,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针

指针函数的写法

int *fun(int x,int y);
int * fun(int x,int y);
int* fun(int x,int y);

9.函数指针

  • 函数指针是指向函数的指针变量
  • 函数指针跟普通指针本质上并无区别,只是在取址和索引时,取址符和星号均可省略
#include <stdio.h>

void f(int a)
{
    printf("%d\n", a);
}

int main(int argc, char const *argv[])
{
    void (*p)(int); // 指针 p 专门用于指向类型为 void (int) 的函数
    // p = &f; // p 指向 f(取址符&可以省略)
    p = f; // p 指向 f

    // 以下三个式子是等价的:
    f(666);    // 直接调用函数 f
    (*p)(666); // 通过索引指针 p 的目标,间接调用函数 f
    p(666);    // 函数指针在索引其目标时,星号可以省略

    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

cam_______

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

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

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

打赏作者

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

抵扣说明:

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

余额充值