[C语言]函数入门

262 阅读13分钟

函数

1. 函数是什么

在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。

一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

维基百科中对函数的定义:子程序

2. C语言中函数的分类

1.库函数

2.自定义函数

2.1 库函数:

为什么会有库函数?

  1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想 把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。

  2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。

  3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。

像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员 进行软件开发。

库函数网站

C语言官网

简单的总结,C语言常用的库函数都有:

IO函数 (输入输出函数):printf scanf getchar putchar

字符串操作函数 :strcmp strlen

字符操作函数 :toupper(小写转大写)

内存操作函数 :memcpy memcmp memset

时间/日期函数 :time

数学函数 :sqrt pow(求次方)

其他库函数

举例

1.strcpy

char是函数的返回类型

strcpy是函数的名字

括号里逗号隔开的两个是函数的参数

#include<string.h>

int main()
{
    char arr1[20]={0};
    char arr2[]="hello world";
    //把arr2放到arr1中
    strcpy(arr1,arr2);
    printf("%s",arr1);//打印arr1的字符串%s--以字符串的格式打印
    
    return 0;
        
}

2.memset--内存设置

int main()
{
    char arr[] = "hello world";
    memset(arr,'x',5);//把arr这串空间的前五个字符全部设置为x
    
    printf("%s\n",arr);
    
    return 0;
}

2.2 自定义函数

自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

写一个函数可以求出两个整数的最大值

get_max(int x,int y)
{
    int z = 0;
    if(x>y)
        z = x;
    else
        z = y;
    return z;//返回z  也就是最大值
}

int main()
{
    int a = 10;
    int b = 20;
    //函数的调用
    int max = get_max(a,b);
    printf("max = %d\n",max)
    
    return 0;
}

写一个函数可以交换两个整型变量的内容

//函数返回类型的地方写成void,表示这个函数不返回任何值,也不需要返回
//在swap1在调用的时候,实参传给形参,其实形参是实参的一份临时拷贝。
//改变形参不能改变实参!!!
//传值调用
void swap1(int x,int y)//这个代码是有问题的
{
    int z = 0;
    z = y;
    y = x;
    x = z;
}

int main()
{
    int a = 10;
    int b = 20;
    //写一个函数--交换两个整型变量的值
    printf("交换前:a = %d b = %d\n", a, b);
    swap1(a,b);
    printf("交换后:a = %d b = %d\n", a, b);
    
    return 0;
}

代码出错的原因是在swap函数里面进行的交换,发现其实是和主函数没有关系的,所以在swap函数以后打印a和b的值发现没有变化。

这串代码应该它通过指针来完成

//传址调用
void swap2(int* pa,int* pb)
{
    int z = 0;
    z = *pa;
    *pa = *pb;
    *pb = z;
}


int main()
{
    int a = 10;
    int b = 20;
    //写一个函数--交换两个整型变量的值
    printf("交换前:a = %d b = %d\n", a, b);
    swap2(&a, &b);
    printf("交换后:a = %d b = %d\n", a, b);
    
    return 0;
}

什么时候需要使用到指针呢?

在函数中组若需要改变了主函数中的变量的值就需要用到指针。

3. 函数参数

3.1 实际参数(实参)

真实传给函数的参数,叫实参。也就是说在函数调用的时候传给函数的值叫做实际参数。

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

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

3.2 形式参数(形参)

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

形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。(生命周期很像局部变量)

比如在swap1在调用的时候,实参传给形参,其实形参是实参的一份临时拷贝。(形参实例化之后其实相当于实参的一份临时拷贝。)

改变形参并不能改变实参。


在上面的代码中

swap1和swap2函数中的参数数 x,y,px,py 都是形式参数。

在main函数中传给 Swap1 的a, b 和传 给 Swap2 函数的 &a , &b 是实际参数。

4. 函数调用

4.1 传值调用

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

4.2 传址调用

  • 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
  • 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

4.3 练习

  1. 打印100-200之间的素数。
//判断是不是素数
#include <math.h>
int is_prime(int n)
{
    int j = 0;
    for(j = 2; j <= sqrt(n); j++)
    {
        if(n % j == 0)
            return 0;
        
    }
    return 1; 
}

int main()
{
    int i = 0;
    int count = 0;
    for(i = 100; i<=200; i++)
    {
        //判断i是否为素数
        if (is_prime(i) == 1)
        {
            count++;//计算素数一共有多少个 
            printf("%d\n",i);//打印素数
        }
    }
    printf("count = %d\n",count);
    return 0;
}
  1. 打印一千年到两千年之间的闰年
//一个函数如果不写返回类型,默认返回int类型
int is_leap_year(int x)
{
    int j = x;
    if(j % 4 == 0 && j % 100 != 0 || j % 400 == 0)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}
//这里还有一个更加简洁的写法
int is_leap_year(int x)
{
    return (j % 4 == 0 && j % 100 != 0 || j % 400 == 0)
}

int main()
{
    int y = 0;
    for (y=1000; y<=2000; y++)
    {
        if (is_leap_year(y) == 1)
        {
            printf("%d",y);
        }
    }
    return 0;
}
  1. 写一个函数,实现一个整型有序数组的二分查找
int binary_search(int a[], int k, int s)
{
    int left = 0;
    int right = s - 1;
    //int sz = sizeof(a)/sizeof(a[0]);
    //如果在这里求的话会发现求出来sz等于1
    //下面说了传过来是是数组首字母的地址也就是指针
    //那sizeof(a)算出来就是4
    //同时sizeof(a[0])也等于4
    
    while(left <= right)
    {
         int mid = (left + right) / 2;
    if(a[mid] > k)
    {
        right = mid - 1;
    }
    else if(arr[mid] < k)
    {
        left = mid + 1;
    }
    else
    {
        return mid;
    }
    }
    return -1;//找不到了
}
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int key = 7;
    //这个数组的大小只能在这里求不能进入函数里面求
    //数组arr传参,实际上传递的不是数组本身
    //仅仅传过去了数组首元素的地址,也就是传过去的是指针
    
    int sz = sizeof(arr)/sizeof(arr[0]);  	
    //找到的就返回找到位置的下标
    //找不到就返回-1
    int ret = binary_search(arr, key, sz);
    if (-1 == ret)
    {
        printf("找不到\n");
    }
    else
    {
        printf("找到了,下标是%d\n",ret);
    }
    
    return 0;
    
}

在这里我们需要注意一件事情,那就是函数内部需要参数部分传过来某个数组的元素个数,一定是在外面求好这个元素个数然后传过去;想在函数内部求数组的元素个数是做不到的。

  1. 写一个函数,每调用一次这个函数,就会将num的值+1
void add(int*p)
{
     (*p)++;
}
int main()
{
    int num = 0;
    add(&num);
    printf("%d\n", num);
    
    add(&num);
    printf("%d\n", num);
    
    add(&num);
    printf("%d\n", num);
    return 0;
}

想在函数内部改变函数外部的某些变量的时候,需要传地址过去。

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

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

5.1 嵌套调用

#include <stdio.h>
void new_line()
{
 printf("hehe\n");
}
void three_line()
{
    int i = 0;
 for(i=0; i<3; i++)
   {
        new_line();
   }
}
int main()
{
 three_line();
 return 0;
}

函数可以嵌套调用,但是不可以嵌套定义。

5.2 链式访问

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

#include <string.h>
int main()
{
    int len = strlen("abc");
    printf("%d", len);
    
    //链式访问
    printf("%d\n",strlen("abc"));
    return 0;
}

在看一个有意思的

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));
    //发现打印的是4321
    //查到printf的返回类型是int
    //同时返回的值是打印在屏幕上的字符的个数     
    return 0;
}

6. 函数的声明和定义

6.1 函数声明

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

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

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

int main()
{
    int a = 10;
    int b = 20;
    //如果函数写在后面但是这里没有进行声明的话,编译器就会报错。因为编译器从前往后扫描并没有见过add函数
    int add(int,int);//函数声明--告知
    
    int c = add(a,b);
    printf("%d\n",c);
    
    return 0;
}

int add(int x, int y)
{
    return (x+y);
}

6.2 函数定义

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

这部分内容很多但是文字没法说清楚,建议去找个视频看一看。

函数声明和定义在工程中的具体应用

7. 函数递归

7.1 什么是递归

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

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的 一种方法它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的主要思考方式在于:把大事化小

首先一个最简单的递归

#include <stdio.h>
int main()
{
    printf("呵呵哒\n");
    main();
    
    return 0;
}

这是一种错误的示范,发现运行起来以后是一种死递归,但是这确实也是递归。

7.2 递归的两个必要条件

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。

7.2.1 练习1

接受一个整型值(无符号),按照顺序打印他的每一位。

例如:

输入:1234,输出:1 2 3 4

//思路是这样的
//首先是1234
//分解成123和4
//分解成12和3和4
//分解成1和2和3和4
void print (unsigned int n)
{
    if (n > 9)//存在限制条件,当满足这个限制条件的时候,递归便不再继续。
    {
        print(n / 10);//每次递归调用之后越来越接近这个限制条件。
    }
    printf("%d ",n % 10);
}

int main()
{
    unsigned int num = 0;//设置一个无符号的int
    scanf("%u", &num);
    //递归-自己调用自己
    print(num);//print函数可以打印参数部分数字的每一位
    return 0;
}

这串代码执行起来是这样的:

首先num等于1234,进入print函数,n就等于1234,1234>9所以if语句执行,if语句中又有print函数,但是在这个print函数中传进去的值为1234/10等于123,123>9所以if语句执行,if语句中又有print函数,这次传进去的值为123/10等于12,又发现12>9,所以又进入print函数这次的值为12/10等于1,这次进入print函数在if判断时发现不满足条件,所以直接跳过if。

开始执行printf打印1,打印1以后跳出最后这个print函数,所以上一层的if语句执行完毕,开始执行printf打印12%10等于2,从此类推到最开始print函数打印完成4。从此彻底跳出print函数,在主函数中return 0;至此主函数结束。程序运行的结果为1 2 3 4

在这里如果理解不清楚的可以画画图自己表示一下。

在这一小节我们说的是递归的必要条件,并不是充要条件,也就是说,就算是满足了这两个条件,也有可能不是一个正确的递归,但是不满足这两个条件,一定是错误的递归。

这里写一个虽然满足条件但是不正确的递归:

void test (int n)
{
    if (n < 10000)//限制条件
    {
        test (n + 1);//接近限制条件
    }
}

int main()
{
    test (1);
    return 0;
}

在递归中经常会发生这样的问题,也就是栈溢出。

原理在于:内存在使用的时候会划分为三个区域

  • 栈区:用来存放局部变量还有函数的形参(也就是一些临时的空间,一些临时的变量);
  • 堆区:动态内存分配;
  • 静态区:存放全局变量和静态变量。

在递归的时候会在栈区中开辟一块属于函数的空间,每次执行函数都会调用一块空间,但是栈区中的空间总有时候会被调取干净,栈区中没有空间了就会发生栈溢出的现象。

所以我们在写递归代码的时候需要注意:

  1. 不能死递归,都有跳出条件,每次递归逼近跳出条件;
  2. 递归层次不能太深

这里推荐一个网站程序员的知乎!名字就叫做栈溢出,但是上面都是英文,英文提问英文回答,对英语还是有些要求的。

7.2.2 练习2

编写函数不允许创建临时变量,求字符串的长度。

  • 在以前我们求字符串的长度会这样做
#include<string>
int main()
{
    char arr[] = "hehe";
    //['h']['e']['h']['e']['\0']
    //strlen在计算字符串长度的时候不会计算\0
    printf("%d\n", strlen(arr));
    //打印结果为4
}
  • 在这里我们模拟实现一个strlen函数
int my_strlen(char* str)
{
    int count = 0;
    while (*str != '\0')
    {
        count++;
        str++;
    }
    return count;
}

int main()
{
    char arr[] = "hehe";
    printf("%d\n", my_strlen(arr));
}
  • 题目要求我们不创建临时变量,我们就用递归的思想解决
//思路是这样的
//my_strlen("he");
//1+my_strlen("e");
//1+1+my_strlen("");
//1+1+0 = 2
int my_strlen(char* str)
{
    if (*str != '\0')//解引用得到指针指向的字符
    {
        return 1 + my_strlen(str+1);
        //str是地址,str加一就是下一个字符的地址
        //比如一开始是h的地址,+1之后就变成了e的地址
        //还有一个问题这里能不能写成str++呢?肯定是不行的,因为后置++是先使用后++,所以传进函数的值还是原来的值
        //所以在这里后置++是不行的,我们在写递归的时候,最好是去使用a+1这种写法
        //有两个好处:1是不会改变a的值,2.是好理解能够得到我们想要的结果。减少出错
    }
    else
    {
        return 0;
    }
}

int main()
{
    char arr[] = "hehe";
    printf("%d\n", my_strlen(arr));
}

7.3 递归与迭代

7.3.1 练习3:

求n的阶乘。(不考虑溢出)

int factorial(int n)
{
    if (n>1)
    {
        return(n*factorial(n-1));
    }
    else
    {
        return 1;
    }
}
int main()
{
    int a = 0;
    scanf("%d",&a);
    printf("%d",factorial(a));
    return 0;
}

我们在分支和循环中也学过了用循环来解决这些问题,所以其实是有一些功能既可以使用迭代的方式实现,也可以使用递归。

7.3.2 练习4

求第n个斐波那契数。(不考虑溢出)

int fib(int f)
{
    if (f > 2)
    {
        return (fib(f - 1) + fib(f - 2));
    }
    else
    {
        return 1;
    }
}

int main()
{
    int a = 0;
    scanf("%d", &a);
    
    printf("%d", fib(a));
    return 0;
}

写出来了,但是发现在计算第50个斐波那契数的时候,计算机会一直跑,特别耗费时间。

为什么呢?

我们发现fib函数在调用的过程中很多计算其实一直在重复。效率太低。

如果我们把代码修改一下:

int count = 0;//定义一个全局变量

int fib(int f)
{
    if (f == 3)//当计算3的斐波那契数的时候count+1
    {
        count++;
    }
    if (f > 2)
    {
        return (fib(f - 1) + fib(f - 2));
    }
    else
    {
        return 1;
    }
}

int main()
{
    int a = 0;
    scanf("%d", &a);

    printf("%d\n", fib(a));
    printf("count=%d\n", count);
    return 0;
}

发现在计算第40个斐波那契数的时候,第3个斐波那契数竟然被计算了三千九百多万次。

递归可以求解但是效率太低。

int fib (int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    while(n > 2)
    {
        c = a + b;
        a = b;
        b = c;
        n--;
    }
    return c;
}

int main()
{
    int a = 0;
    scanf("%d", &a);

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

发现如果是这种实现方法的话速度是非常快的,但是我们也发现在计算特别大的数的时候结果是错误的,我们先不考虑这个,错误的原因在于int存储的数字的大小限制。

提示!!!

  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。