学习笔记 | C语言中的函数

130 阅读9分钟

函数的概念

C 语言中的函数就是一个完成某项特定的任务的一小段代码。这段代码有特殊的写法和调用方法。

在C语言中常见的两类函数:

  • 库函数:已存在,可以直接使用
  • 自定义函数:自己创建的函数

库函数

标准库和头文件

C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSI C规定了一些常用的函数的标准,被称为标准库。也就是说,C语言不去实现,只是规定标准。

那么,编译器会根据标准库来实现功能,这就是库函数。常见编译器如Linux中有gcc

库函数的使用和功能是一样的,但是函数的实现可能有所差异。

各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。

库函数相关的头文件:zh.cppreference.com/w/c/header

cplusplus.com:cplusplus.com

举例:sqrt 开平方函数

使用方法:legacy.cplusplus.com/reference/c…

     double sqrt  (double x);
      float sqrtf (float x);
long double sqrtl (long double x);

代码

#include <math.h>
int main()
{
    double r = sqrt(16.0);
    printf("%lf", r);
    return 0;
}

执行结果

image-20231124005112701

库函数文档的一般格式

  • 函数原型
  • 函数功能介绍
  • 参数和返回类型说明
  • 代码举例
  • 代码输出
  • 相关知识链接

自定义函数

库函数功能是固定的,自定义函数可以根据需求发挥程序员的想象力编写,并实现其功能。

ret_type fun_name(形式参数)
{
    
}
​
函数名 返回类型(形式参数)//函数头
{
    //函数体
}

函数返回类型只有两种:void 和 其他

void menu(void)
{
    printf("**************************\n");
    printf("**************************\n");
    printf("**************************\n");
}
  • void:什么都不返回
  • 其他:int char short ...

代入参数的函数

int Add(int x,int y)//形式上的参数,简称形参
{
    return (x + y);
}
​
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d",&a,&b);
    //计算求和
    int c = Add(a,b);//真实传递给函数Add的参数,是实际参数,简称实参
    printf("%d\n",d);
    return 0;
}
  • 形参和实参最大区别在于;对于没有调用函数时,形参不会创建内存空间。
  • 形参和实参有自己不同的地址,传参时,实参会把值传给形参。形参是实参的一份临时拷贝。

return 语句

在函数的设计中,函数中经常会出现return语句。

  • return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达,再返回表达式的结果。
  • return后边也可以什么都没有,直接写return;这种写法适合函数返回类型是void的情况。
  • return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。
  • return语句执行后,函数就彻底返回,后边的代码不再执行。
  • 如果函数中存在if等分支语句,则要保证每种情况下都有return返回,否则会出现编译错误。

看下一段代码

void test()
{
    int n = 0;
    scanf("%d", &n);
    printf("Point A\n");
    if (n == 5)
        return;
    printf("Point B\n");
}
​
int main()
{
    test();
    return 0;
}

执行结果

image-20231125234253004

并没有打印Point B,而是直接返回。

换句话说,return可以提前结束掉函数。 强调一下,break只能跳出循环,而return是跳出整个函数。

继续看代码

int test()
{
    return 3.14;
}
​
int main()
{
    int n = test();
    printf("%d\n",n);
    return 0;
}

执行结果

image-20231125235325987

继续看代码

int test()
{
    int n = 0;
    if (n == 5)
        return 1;
}
​
int main()
{
    int m = test();
    printf("%d\n",m);
    return 0;
}

执行结果

image-20231125235735305

n不等于 5 时,是没有返回值的,所以会出现如上编译问题。

数组做函数参数

比如:写一个函数对将一个整型数组的内容,全部置为-1,再写一个函数打印数组的内容。

void set_arr(int arr[],int sz)
{
    int i = 0;
    for(i=0;i<sz;i++)
    {
        arr[i] = -1;
    }
}
​
void print_arr(int arr[],int sz)
{
    int i = 0;
    for(i=0;i<sz;i++)
    {
        printf("%d ",arr[i]);
    }
}
​
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr)/sizeof(arr[0]);
    //写一个函数,将arr中的内容全部置为 -1
    set_arr(arr,sz);
    //在写一个函数,将arr中的内容打印
    print_arr(arr,sz);
    
    return 0;
}

执行结果

image-20231126005938596

  • 函数的形参和实参个数要匹配
  • 函数的实参是数组,形参也可以写成数组
  • 形参如果是一维数组,数组大小可以省略
  • 形参如果是二维数组,行可以省略,但是列不能省略(与二维数组初始化一样)
  • 数组传参,形参是不会创建新的数组
  • 形参和实参是同一个数组

二维数组传参

print_arr(int arr[3][5], int r ,int c)
{
    int i = 0;
    for (i = 0; i < r; i++)
    {
        int j = 0;
        for (j = 0; j < c; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
​
int main()
{
    int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };
    print_arr(arr, 3, 5);
​
    return 0;
}

执行结果

image-20231127224836085

  • 行可以省略,列是不能省略的;arr[3][5] 可以写成arr[][5]

函数嵌套调用和链式访问

嵌套调用

函数嵌套调用就是函数之间的互相调用,俄罗斯套娃一样。

用过Oracle的老DBA估计都经历过Oracle存储过程的各种嵌套调用。

判断每个月中有多少天

int is_leap_year(int y)
{
    if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
        return 1;
    else
        return 0;
}
​
int get_days_of_month(int y,int m)
{
    int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
    int d = days[m];
    if (is_leap_year(y) && m == 2)
    {
        d += 1;
    }
    return d;
}
​
int main()
{
    int y = 0;
    int m = 0;
    scanf("%d %d",&y,&m);
    int d = get_days_of_month(y, m);
    printf("%d\n", d);
    return 0;
}

执行结果

image-20231127231255194

链式访问

看如下代码

#include <string.h>
int main()
{
    size_t len = strlen("abc");
    printf("%zd\n", len);
    //也可以直接写成
    printf("%zd\n",strlen("abc"));//链式调用
    return 0;
}

执行结果

image-20231127233305086

在看一段代码

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

执行结果

image-20231127233832968

  • 最里层:printf打印在屏幕上的数字为43,printf本身返回值其实是打印字符的个数,也就是2
  • 第二层:printf("%d",printf("%d",2)),同理屏幕上打印2,printf函数本身返回1
  • 最外层:printf("%d",1),打印 1
  • 所以最终结果为4321

其实这里要注意的点是,printf打印到屏幕上,这是函数本身的功能,链式访问考虑的是函数本身的返回值。

函数的声明和定义

单个文件

函数的声明

伪代码:

//函数声明
int is_leap_year(int);
​
int main()
{
    is_leap_year(y);
    ...
    return 0;
}
​
//函数的定义
int is_leap_year(int)
{
    xxxxx
}

函数要先声明,后使用。

定义放在前面,编译器不会报错,那是因为定义也是一种特殊的声明

多个文件

一般情况下,函数的声明、类型的声明放在头文件中,函数的实现是放在源文件中。

main.c

#include <stdio.h>
#include "add.h"int main()
{
    int a = 10;
    int b = 20;
    scanf("%d %d",&a,&b);
    int c = Add(a,b);
    printf("%d\n",c);
    
    return 0;
}

add.h

//存放函数的声明
int Add(int x,int y);

add.c

//存放函数的实现
int Add(int x,int y)
{
    return x+y;
}

导入静态库

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

static和extern

static修饰变量

学习static和extern之前,先铺垫几个概念:

作用域:是程序设计的概念,通常来说,一段程序代码中所用到的名字并不总是有效的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

  • 局部变量的作用域是变量所在的局部范围。
  • 全局变量的作用局是整个工程。

大括号内部定义的是局部变量,大括号外部定义的就是全局变量

生命周期:指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的时间段。

  • 局部变量的生命周期是,进入作用域生命周期开始,出作用域生命周期结束
  • 全局变量的生命周期是,整个程序的生命周期。

static

  • 修饰局部变量
  • 修饰全局变量
  • 修饰函数

extern 用来声明外部符号

伪代码:

add.c

int g_val = 2023;

main.c

extern int g_val;

此时,main.c中就可以使用g_val这个全局变量了。

看如下代码:

void test()
{
    int i = 0;
    i++;
    printf("%d ",i);
}
​
int main()
{
    int i = 0;
    for(i = 0;i<5;i++)
    {
        test();
    }
    return 0;
}

执行结果

image-20231129221809992

在看一段代码

void test()
{
    static int a = 0;
    a++;
    printf("%d ",a);
}
​
int main()
{
    int i = 0;
    for(i = 0;i<5;i++)
    {
        test();
    }
    return 0;
}

执行结果

image-20231129221837319

  • int i 进入函数创建,出函数即销毁,所以打印结果是5个1
  • static修饰之后,虽然作用域没有变,但是生命周期发生了变化,即出函数时并没有对其进行销毁

生命周期边长的本质是什么?本来一个局部变量是存储在内存的栈区,但是被static修饰后存储到了静态区。全局变量也是存储在静态区。也就是说,static修饰后,生命周期与全局变量一样了。

使用建议:一个局部变量出了函数,还想保留其值时,就可以使用static来修饰。

再看一段代码:

Add.c 文件

int g_val = 2024; //定义了全局变量

main.c 文件

//声明外部符号
extern int g_val;
​
int main()
{
    printf("%d\n",g_val);
    return 0;
}
  • 全局变量默认具有外部连接属性;也就是说在其他文件只要声明,就可以使用。

注意:static 修饰全局变量后,外部链接属性变成了内部链接属性,也就是作用域变了,只能在本源文件内使用,其他源文件无法使用了。

static修饰函数

add.c

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

main.c 文件

extern int Add(int x,int y);
​
int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a,b);
    printf("%d\n",ret);
    
    return 0;
}
  • 函数默认也具有外部连接属性

同理,加上static修饰,外部连接属性变成了内部连接属性。

static int Add(int x,int y)
{
    return (x + y);
}

使用建议:一个函数指向在所在的源文件内部使用,不想背其他源文件是用,就可以使用static修饰。