C语言梦开始的地方7:函数

文章目录

C语言梦开始的地方7:函数

上一章我们了解了C语言的数组,本章节我们开始了解C语言的函数章节。C语言的函数是什么?

数学中我们其实就见过函数的概念,比如:一次函数y=kx+b ,k和b都是常数,给一个任意的x,就得到一个y值。其实在C语言也引入函数(function)的概念,有些翻译为:子程序,子程序这种翻译更加准确一些。

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

C语言的程序其实是由无数个小的函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率。

函数

在C语言中函数分两部分:库函数和自定义函数

C语言并不提供库函数,C语言的国际标准ANSI C规定了一些常用的函数的标准,被称为标准库,那不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。这些函数就被称为库函数。比如:printfscanf这种

自定义函数则是由用户根据目前的需求自己来实现的一种函数。

库函数

上面我们了解了什么是库函数那么库函数该怎么使用呢?

库函数的使用分两部分:

  1. 调用头文件
  2. 调用所需函数

因为C标准库的命名规范其实很统一。头文件都采用功能领域的英文缩写,比如我们要求进行数学运算我们就可以去找math.h这个头文件,我们平时的输入输出就是在stdio.h这个头文件而其中的函数也是同一套命名规范。

那么要去哪里找呢?

C语言官方链接:https://zh.cppreference.com/c/header

cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/

库函数的使用

比如说我们现在想计算一个数的n次幂和一个数的平方根,我们就根据平时的了解那么就要去math头文件里面去找,幂的单词是power,平方根的单词square root。那么我只需要找英文和它最想进的进去看一下即可。

在C语言的math中 pow 是我们要的幂函数,sqrt 则是平方根

c 复制代码
#include <stdio.h>
#include <math.h>

int main()
{
    int n = 0;

    // 计算2的n次幂
    scanf("%d",&n);

    // pow函数原型
    // double pow (double base, double exponent );
    // 这是因为有可能要计算浮点数的n次幂
    printf("two to the power of n: %f\n",pow(2,n));

    int num = 0;
    // 计算num的平方根
    scanf("%d",&num);

    // sqrt函数原型
    // double sqrt( double arg ); 或
    // float sqrt( double arg ); c99起

    printf("the square root of %d: %f\n",num,sqrt(num));

    return 0;
}

自定义函数

自定义函数是程序员根据当前所需来自己实现的函数

函数的语法形式:

复制代码
ret_type fun_name(形式参数)
{

}
  • ret_type 是函数返回类型
  • fun_name 是函数名
  • 括号中放的是形式参数
  • {}括起来的是函数体

假设我们现在要实现计算器的两数相加的函数,那么首先就是命名方面我们同样规范函数内容要和函数名差不多或一模一样。

这里需要注意的是命名方式最好是英文的方式,因为计算机最早发展的地方是在美国所以最好还是使用英语命名。

我们要实现的函数主要是相加,所以我们可以根据字母意思来进行命名比如add。命名只要和他的使用或者其定义大差不差即可或者看计算机的前辈们的命名方式来命名也可以

c 复制代码
int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}

int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

    int result = Add(a,b);

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

函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。

上面只是一个例子,未来我们是根据实际需要来设计函数,函数名、参数、返回类型都是可以灵活变化的。

形参和实参

在函数使用的过程中,把函数的参数分为,实参和形参。

我们再把上述的自定义函数代码拿下来看看

c 复制代码
int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}

int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

    int result = Add(a,b);

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

实参是main函数中的a和b,形参则是我们Add函数中的num1和num2。那么它们有什么区别吗?难道只是名字上的区别吗?

传递给函数的参数a和b,称为实际参数,简称实参。实际参数就是真实传递给函数的参数。

在函数名Add 后的括号中写的num1 和 num2 ,称为形式参数,简称形参。

为什么叫形式参数呢?实际上,如果只是定义了Add 函数,而不去调用的话, Add 函数的参数num1和 num2 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在

函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。

我们在调试的时候可以观察到,num1和num2确实得到了a和b的值,但是x和y的地址和a和b的地址是不一样的,所以我们可以理解为形参是实参的一份临时拷贝。

return 语句

在函数的设计中,函数中经常会出现return语句,这里讲一下return语句使用的注意事项。

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

数组做函数参数

在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。

数组做参数需要注意一下几点:

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

还有一点是我们数组当参数时不能使用sizeof进行计算大小,因为这种计算方式仅在数组的"定义作用域"内有效 。如果将来我们把数组作为参数传给某个函数,在函数内部再用 sizeof(arr) / sizeof(arr[0])无法 正确计算出数组大小了。

所以我们在传参数是也要将数组的大小也传过去,二维数组最好是将行和列都参过去

c 复制代码
void set_arr(int arr[],int n)
{
    for(int i = 0; i < n;++i)
    {
        arr[i] = -1;
    }
}

void print_arr(int arr[],int n)
{
    for(int i = 0; i < n;++i)
    {
        printf("arr[%d]:%d ",i,arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    set_arr(arr, sz);   // 设置数组内容为-1
    print_arr(arr, sz); // 打印数组内容
    return 0;
}

嵌套调用和链式访问

嵌套调用

嵌套调用就是函数之间的互相调用,每个函数就像一个乐高零件,正是因为多个乐高的零件互相无缝的配合才能搭建出精美的乐高玩具,也正是因为函数之间有效的互相调用,最后写出来了相对大型的程序。

假设我们计算某年某月有多少天?如果要函数实现,可以设计2个函数:

  • is_leap_year():根据年份确定是否是闰年
  • get_days_of_month():调用is_leap_year确定是否是闰年后,再根据月计算这个月的天数
c 复制代码
#include <stdio.h>
#include <stdbool.h>

bool is_leap_year(int year)
{
    if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
        return true;
    else
        return false;
}

int get_days_of_month(int year, int month)
{
    int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = days[month];

    if (is_leap_year(year))
    {
        day += 1;
    }

    return day;
}

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

    int result = get_days_of_month(year, month);

    printf("%d", result);

    return 0;
}

这一段代码,完成了一个独立的功能。代码中反应了不少的函数调用:

  • main 函数调用 scanf 、printf 、get_days_of_month
  • get_days_of_month 函数调用 is_leap_year

未来的稍微大一些代码都是函数之间的嵌套调用,但是函数是不能嵌套定义的。

链式访问

所谓链式访问就是将一个函数的返回值作为另外一个函数的参数,像链条一样将函数串起来就是函数的链式访问。

比如下面这个代码:

c 复制代码
int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}

int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

    int result = Add(a,b);

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

这就是正常的使用

前面的代码完成动作写了2条语句,把如果把Add的返回值直接作为printf函数的参数呢?这样就是一个链式访问的例子了。

c 复制代码
int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}

int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

    printf("%d\n",Add(a, + b));
    return 0;
}

函数的声明和定义

单个文件

  • 定义 = 有函数体的完整实现;声明 = 只告诉编译器"函数存在"(返回类型 + 函数名 + 参数类型 + 分号,无函数体)
  • 核心规则:先声明/先定义,后使用
  • 示例:Add 定义放在 main 之后,则 main 前必须先加 int Add(int num1, int num2); 声明,否则报"隐式声明"错误/警告;若定义在 main 之前则无需声明
c 复制代码
int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

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

int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}
c 复制代码
int Add(int num1,int num2);

int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

    printf("%d\n",Add(a, + b));
    return 0;
}

int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}

所以函数的调用一定要满足,先声明后使用;

函数的定义也是一种特殊的声明,所以如果函数定义放在调用之前也是可以的。

多个文件

假设我们要实现一个自己的计算器库函数,那么我们想要其他的.c文件能访问当我们自己实现的库函数该怎么做?

  • 标准做法:新建 add.c(放定义)+ add.h(放声明),调用方法 #include "add.h"
  • 用 3 个代码块展示 add.h / add.c / test.c 的分工

为什么要这样?不能声明和定义都放在.h文件中吗?

  1. #include 本质是"文本粘贴"。预处理阶段,add.h 里的内容会被原封不动复制进每一个包含它的 .c 文件。所以头文件里的东西,是谁包含谁就有一份。

  2. 每个 .c 文件独立编译。main.c 编译成 main.obj,other.c 编译成 other.obj,最后才由链接器把它们拼成一个程序。

  3. 链接时暴露问题。如果 add.h 里直接写了 Add 的定义:

    1. main.c 包含它 → main.obj 里有一份 Add
    2. other.c 包含它 → other.obj 里也有一份 Add
    3. 链接器一看:同一个函数出现两次 → 重复定义。MSVC 报 LNK2005 ... already defined,gcc 报 multiple definition of 'Add'。
  4. 关键区别:函数声明不产生任何机器码,它只是一句"描述函数长什么样"的话,放在头文件里被包含多少次都安全;而函数定义会生成代码,多放几处就冲突了。这就是"头文件只放声明、.c 放定义"的根本原因。

    可以将其理解成这样的单个文件,声明加定义

c 复制代码
int Add(int num1,int num2);

int main()
{
    int a = 0, b = 0;

    scanf("%d %d",&a,&b);

    printf("%d\n",Add(a, + b));
    return 0;
}

int Add(int num1,int num2)
{
    int total = num1 + num2;

    return total;
}

那么实际上的多个文件就是下面这种情况,上述只是类似与多文件仅此而已

Add.h

c 复制代码
int Add(int num1,int num2);

Add.c

c 复制代码
int Add(int num1,int num2)
{
	return num1 + num2;
}

main.c

c 复制代码
#include <stdio.h>
#include "add.h"
int main()
{

    int a = 10;
    int b = 20;
    // 函数调用
    int c = Add(a, b);
    printf("%d\n", c);
    return 0;
}

有了函数声明和函数定义的理解,我们写代码就更加方便了。

相关推荐
月华路1 小时前
G1 垃圾回收:脏卡队列、记忆集与并发精化线程机制
java·开发语言
gugucoding2 小时前
47. 【Java】Java内存模型(JMM)与可见性
java·开发语言
m0_380743872 小时前
从零调用 Claude 教程
开发语言·python·node.js
起床学FPGA2 小时前
AI回答问题之后,会自动跳到最下面的问题
开发语言·javascript·ecmascript
x_x-F3 小时前
网络数据从网卡到用户态:一场基于内存所有权转移的接力赛
开发语言·网络·php
名字还没想好☜4 小时前
Java 线上内存泄漏排查实战:jmap 导堆、MAT 找 GC Roots 与四类常见泄漏
java·开发语言·jvm·内存泄漏
码匠许师傅4 小时前
【C++ 面试真题】聊聊 C++ 的多继承与虚继承
开发语言·c++·面试
我不是疯子是傻子5 小时前
Qt CAN通信周期发送抖动?实测定时器精度校准与时间戳补偿方案
开发语言·数据库·qt
IT爱学堂5 小时前
尚硅谷 - 2025年3月Java+AI大模型应用开发
java·开发语言·人工智能