C 基础(8) - 函数

首先,什么是函数?函数(function)是完成特定任务的独立程序代码单元。语法规则定义了函数的结 构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同,但是细节上略有不同。一 些函数执行某些动作,如printf()把数据打印到屏幕上;一些函数找出一个值供程序使用,如 strlen() 把指定字符串的长度返回给程序。一般而言,函数可以同时具备以上两种功能。

📝 函数的定义、声明与调用

要使用一个自定义函数,通常会经历定义、声明和调用三个步骤。

  1. 函数定义 (Function Definition)

    函数的定义是函数的完整实现,包含了函数执行的具体代码。其基本语法结构如下:

    复制代码
    返回值类型 函数名(参数列表) {
        // 函数体:实现具体功能的代码
        // ...
        return 返回值; // 如果返回值类型不是 void,则必须有 return 语句
    }
    • 返回值类型 : 函数执行后返回的数据类型(如 int, float, char 等)。如果函数不返回任何值,则使用 void
    • 函数名: 函数的名称,需要符合 C 语言的标识符命名规则,最好能体现函数的功能。
    • 参数列表 : 也称为形式参数(形参),是函数接收外部输入的"占位符"。可以没有参数(用 void 或留空表示),也可以有多个,多个参数之间用逗号隔开。
    • 函数体 : 由花括号 {} 包裹的代码块,包含了实现函数功能的所有语句。
    • return 语句: 用于结束函数并返回一个值。返回值的类型必须与函数声明的返回值类型一致。
  2. 函数声明 (Function Declaration)

    函数声明的作用是告诉编译器函数的名称、返回值类型和参数列表,但不包含函数体。它通常在函数被调用之前进行,可以放在主函数 main 之前,或者放在头文件(.h 文件)中。

    复制代码
    返回值类型 函数名(参数类型1, 参数类型2, ...);

    如果函数的定义写在调用它的位置之前,则可以省略声明。

  3. 函数调用 (Function Call)

    函数调用就是执行函数的过程。通过函数名和实际参数(实参)来触发函数执行

    复制代码
    函数名(实参1, 实参2, ...);

    调用时,实参的数量、类型和顺序必须与函数定义时的形参严格匹配。

💡 示例:计算两数之和

下面是一个完整的例子,展示了如何定义、声明和调用一个函数。

复制代码
#include <stdio.h>

// 函数声明:告诉编译器有一个叫 add 的函数,它接收两个 int,返回一个 int
int add(int x, int y);

int main() {
    int a = 10;
    int b = 20;
    // 函数调用:将 a 和 b 的值传递给 add 函数,并将返回值赋给 result
    int result = add(a, b);
    printf("10 + 20 = %d\n", result); // 输出:10 + 20 = 30
    return 0;
}

// 函数定义:实现 add 函数的具体功能
int add(int x, int y) {
    return x + y; // 返回 x 和 y 的和
}

🔑 核心概念

  • 形参与实参

    • 形参 (Formal Parameter):函数定义时括号内的参数,是接收数据的局部变量。
    • 实参 (Actual Parameter):函数调用时传递给函数的具体值或变量。
    • C 语言中,参数传递默认采用值传递 的方式。这意味着实参的值会被复制一份给形参。因此,在函数内部修改形参的值,不会影响到函数外部的实参。
  • 返回值

    函数通过 return 语句将一个值返回给调用者。return 语句还会立即终止函数的执行。如果函数的返回值类型是 void,则可以省略 return 语句,或使用不带值的 return; 来提前结束函数。

  • 参数传递方式

    • 传值调用 (Call by Value):传递实参的副本。函数内对形参的修改不影响实参。
    • 传址调用 (Call by Reference):通过指针传递实参的内存地址。函数可以通过解引用指针来直接修改外部变量的值。

递归

在 C 语言中,递归 (Recursion) 是一种函数直接或间接调用自身的编程技巧。它通过将复杂问题层层分解为与原问题结构相似但规模更小的子问题,直到子问题简单到可以直接求解,从而解决问题。

一个有效的递归函数必须包含两个核心部分:

  1. 基线条件 (Base Case):也称为终止条件。这是递归的"出口",当满足此条件时,函数不再调用自身,直接返回结果,防止无限递归。
  2. 递归条件 (Recursive Case):函数调用自身的部分。在这个调用中,问题规模必须向基线条件靠近,确保递归最终能够结束。

🚀 经典示例:计算 n 的阶乘

阶乘是理解递归的绝佳例子。n 的阶乘(记为 n!)可以定义为 n * (n-1)!,而 0! 等于 1。

  • 递归公式n! = n * (n-1)!
  • 基准情形0! = 1

下面是计算阶乘的递归代码实现:

复制代码
#include <stdio.h>

// 计算 n 的阶乘
int Fact(int n) {
    // 1. 基准情形:当 n 为 0 时,返回 1,递归结束
    if (n == 0) {
        return 1;
    }
    // 2. 递归情形:将问题分解为 n * (n-1)的阶乘
    // 每次调用 Fact(n-1),问题规模都在减小,更接近基准情形
    else {
        return n * Fact(n - 1);
    }
}

int main() {
    int n = 5;
    int result = Fact(n);
    printf("%d! = %d\n", n, result); // 输出: 5! = 120
    return 0;
}
执行过程解析

Fact(5) 为例,其执行过程分为两个阶段:

  1. 递推(调用)阶段:函数不断调用自身,问题规模层层缩小。

    • Fact(5) 调用 Fact(4)
    • Fact(4) 调用 Fact(3)
    • Fact(3) 调用 Fact(2)
    • Fact(2) 调用 Fact(1)
    • Fact(1) 调用 Fact(0)
  2. 回归(返回)阶段 :当达到基准情形 Fact(0) 时,开始逐层返回结果。

    • Fact(0) 返回 1
    • Fact(1) 返回 1 * 1 = 1
    • Fact(2) 返回 2 * 1 = 2
    • Fact(3) 返回 3 * 2 = 6
    • Fact(4) 返回 4 * 6 = 24
    • Fact(5) 返回 5 * 24 = 120

这个过程依赖于函数调用栈来保存每一层函数调用的状态(如局部变量、返回地址等),待递归结束后再逐层释放。

这个过程依赖于函数调用栈来保存每一层函数调用的状态(如局部变量、返回地址等),待递归结束后再逐层释放。

⚖️ 递归与迭代(循环)

递归和迭代(如 forwhile 循环)都可以用来解决重复性问题,它们各有优劣。

表格

对比维度 递归 (Recursion) 迭代 (Iteration)
优点 代码简洁、逻辑直观,特别适合解决树形结构遍历、分治算法等问题。 执行效率高,没有函数调用的开销,不占用额外的栈空间。
缺点 每次函数调用都有时间和空间开销,递归层次过深容易导致栈溢出 对于某些问题,代码不如递归简洁,逻辑可能更复杂。

使用建议:

  • 当问题本身具有递归结构(如树的遍历、汉诺塔),且递归深度不大时,优先考虑使用递归,因为它能让代码更清晰。
  • 当问题可以通过简单的循环解决,或者递归可能导致大量重复计算(如朴素的斐波那契数列实现)或过深的调用层次时,应优先使用迭代。

查找地址:&运算符

指针(pointer)是C语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。前面使用 的scanf()函数中就使用地址作为参数。概括地说,如果主调函数不使用 return 返回的值,则必须通过 地址才能修改主调函数中的值。接下来,我们将介绍带地址参数的函数。首先介绍一元&运算符的用法。

在 C 语言中,& 运算符主要有两种用途,其具体含义取决于上下文:作为取地址运算符按位与运算符

📍 作为取地址运算符 (Address-of Operator)

这是 & 作为一元运算符时的功能,也是你问题中提到的"查找地址"。它的作用是获取一个变量在内存中的地址。

  • 功能:返回其操作数(通常是一个变量)在内存中的起始地址。
  • 语法&变量名
  • 应用场景
    1. 初始化指针:将一个变量的地址赋值给一个指针变量。
    2. 函数参数传递 :在 scanf 等需要修改变量值的函数中,需要传入变量的地址。
    3. 函数间传递地址:让被调用函数能够直接修改调用函数中的变量。

示例代码:

复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int *p; // 声明一个指向整型的指针

    // 使用 & 运算符获取变量 a 的地址,并赋值给指针 p
    p = &a; 

    printf("变量 a 的值: %d\n", a);       // 输出: 10
    printf("变量 a 的地址: %p\n", &a);    // 输出 a 的内存地址
    printf("指针 p 的值: %p\n", p);       // 输出与上面相同的地址
    printf("指针 p 指向的值: %d\n", *p);  // 通过解引用运算符 * 访问地址中的值,输出: 10

    return 0;
}

🔢 作为按位与运算符 (Bitwise AND Operator)

这是 & 作为二元运算符时的功能,与"查找地址"无关。它对两个整数的二进制位进行"与"操作。

  • 功能:对两个操作数的每一个二进制位进行"与"运算。规则是:只有当两个对应的位都为 1 时,结果的该位才为 1,否则为 0。
  • 语法操作数1 & 操作数2
  • 应用场景:常用于位操作,例如判断一个数的奇偶性、将某个二进制位置 0 等。

示例代码:

复制代码
#include <stdio.h>

int main() {
    int a = 12; // 二进制: 1100
    int b = 10; // 二进制: 1010
    int result;

    // 对 a 和 b 进行按位与运算
    //   1100  (12)
    // & 1010  (10)
    // -------
    //   1000  (8)
    result = a & b;

    printf("12 & 10 = %d\n", result); // 输出: 8

    return 0;
}

⚠️ 重要区别

特性 取地址运算符 (&) 按位与运算符 (&)
操作数数量 一个 (一元运算符) 两个 (二元运算符)
功能 获取变量的内存地址 对二进制位进行"与"运算
操作数类型 必须是变量等可以取地址的对象 必须是整数类型

简单来说,当你看到 & 紧跟在一个变量前面(如 &a)时,它通常是取地址运算符;当它出现在两个数值或变量之间(如 a & b)时,它就是按位与运算符。

指针简介

指针?什么是指针?从根本上看,指针(pointer)是一个值为内存地址的变量(或数据对象)。正如 char 类型变量的值是字符,int 类型变量的值是整数,指针变量的值是地址。在C语言中,指针有许多用法。

指针是 C 语言的灵魂,也是其最核心、最强大的特性之一。它让程序员能够直接操作内存,实现高效的程序逻辑,但同时也是学习 C 语言的一大难点。

简单来说,指针就是内存地址

🏠 理解指针:一个生动的比喻

我们可以把计算机的内存想象成一栋巨大的宿舍楼:

  • 内存 :就是这栋宿舍楼
  • 内存单元(字节) :是楼里的一个个房间,每个房间都有一个唯一的编号。
  • 地址/指针 :就是房间的门牌号
  • 变量 :是住在房间里的
  • 指针变量 :是一个专门用来记录门牌号的小本子

CPU 想要找到某个数据(人),就必须知道它所在的地址(门牌号)。指针变量这个小本子,就是用来存储这个门牌号的。

📝 核心概念与语法

要掌握指针,必须理解两个核心运算符:&*

  1. 取地址运算符 &

    • 作用:获取一个变量在内存中的地址(门牌号)。
    • 语法&变量名
    • 示例&a 表示获取变量 a 的内存地址。
  2. 解引用运算符 *

    • 作用:通过一个指针(地址)去访问或修改它所指向的变量的值(通过门牌号找到房间里的人)。
    • 语法*指针变量名
    • 示例*p 表示访问指针 p 所存储的地址对应的变量的值。

💻 代码示例

下面的代码展示了如何定义、使用指针,以及 &* 的实际效果。

复制代码
#include <stdio.h>

int main() {
    int a = 100;        // 1. 定义一个整型变量 a,并赋值为 100
    int *p = &a;        // 2. 定义一个指针变量 p,并将 a 的地址 (&a) 赋给它

    printf("a 的值: %d\n", a);       // 输出: 100
    printf("a 的地址: %p\n", &a);    // 输出 a 的内存地址,例如 000000xxxxxx
    printf("p 的值: %p\n", p);       // 输出: 与上面相同的地址
    printf("p 指向的值: %d\n", *p);  // 输出: 100,*p 等价于 a

    // 3. 通过指针修改 a 的值
    *p = 200; // 这行代码等价于 a = 200;

    printf("修改后 a 的值: %d\n", a); // 输出: 200

    return 0;
}

❓ 为什么指针很重要?

指针的强大之处在于它赋予了 C 语言直接操作内存的能力,这在许多场景下至关重要:

  • 函数参数传递:C 语言默认是"值传递",函数内部修改参数不会影响外部变量。通过传递指针(地址),函数可以直接修改外部变量的值,实现"传址调用"。
  • 动态内存管理:程序可以在运行时根据需要申请和释放内存,这对于处理大小不确定的数据(如动态数组、链表)非常关键。
  • 高效处理数据结构:数组、字符串、结构体、链表、树等复杂数据结构,其底层实现都严重依赖指针。
  • 提高效率:直接通过地址访问数据,比复制整个数据块要高效得多。

⚠️ 需要警惕的"坑"

指针虽然强大,但如果使用不当,会带来严重的程序错误。

  • 野指针 :指向一个未知的、非法的或已释放的内存地址的指针。访问野指针可能导致程序崩溃或产生不可预知的结果。
    • 成因:指针未初始化、指针越界访问、或指向的局部变量在函数结束后已被销毁。
    • 规避方法 :指针在定义时最好初始化为 NULL,并避免返回局部变量的地址。
  • 空指针 NULL :一个被明确定义为不指向任何有效内存的指针。在使用指针前,检查它是否为 NULL 是一个良好的编程习惯。
相关推荐
csdn_aspnet3 小时前
C语言 (QuickSort using Random Pivoting)使用随机枢轴的快速排序
c语言·算法·排序算法
爱编码的小八嘎4 小时前
C语言完美演绎7-15
c语言
孬甭_4 小时前
揭开指针的面纱(下)
c语言
计算机安禾4 小时前
【数据结构与算法】第43篇:Trie树(前缀树/字典树)
c语言·开发语言·矩阵·排序算法·深度优先·图论·宽度优先
yashuk4 小时前
C语言入门教程:程序结构与算法举例
c语言·算法·教程·程序设计·开发过程
代码地平线4 小时前
C语言实现堆与堆排序详解:从零手写到TopK算法及时间复杂度证明
c语言·开发语言·算法
学习噢学个屁5 小时前
基于51单片机心率仪—体温心率血氧蓝牙
c语言·单片机·嵌入式硬件·51单片机
千谦阙听5 小时前
数据结构最终章:万字详解排序算法!(内部排序)
c语言·数据结构·学习·算法·排序算法
念恒123065 小时前
Linux基础开发工具(Vim篇)
linux·c语言