C语言:函数 指针

函数、指针

一、函数

1.1 函数概述

  • 作用:提高代码的编写效率,实现对代码的重用

  • 函数使用步骤

    • 定义函数:理解为制作工具,工具只需要制作1次即可
    • 调用函数:理解为使用工具
  • 演示函数

    c 复制代码
    #include <stdio.h>
    
    // 定义函数
    void func() {
        printf("                            _ooOoo_  \n");
        printf("                           o8888888o  \n");
        printf("                           88  .  88  \n");
        printf("                           (| -_- |)  \n");
        printf("                            O\\ = /O  \n");
        printf("                        ____/`---'\\____  \n");
        printf("                      .   ' \\| |// `.  \n");
        printf("                       / \\||| : |||// \\  \n");
        printf("                     / _||||| -:- |||||- \\  \n");
        printf("                       | | \\\\\\ - /// | |  \n");
        printf("                     | \\_| ''\\---/'' | |  \n");
        printf("                      \\ .-\\__ `-` ___/-. /  \n");
        printf("                   ___`. .' /--.--\\ `. . __  \n");
        printf("                ."" '< `.___\\_<|>_/___.' >'"".  \n");
        printf("               | | : `- \\`.;`\\ _ /`;.`/ - ` : | |  \n");
        printf("                 \\ \\ `-. \\_ __\\ /__ _/ .-` / /  \n");
        printf("         ======`-.____`-.___\\_____/___.-`____.-'======  \n");
        printf("                            `=---='  \n");
        printf("  \n");
        printf("         .............................................  \n");
        printf("                  佛祖镇楼                  BUG辟易  \n");
        printf("          佛曰:  \n");
        printf("                  写字楼里写字间,写字间里程序员;  \n");
        printf("                  程序人员写程序,又拿程序换酒钱。  \n");
        printf("                  酒醒只在网上坐,酒醉还来网下眠;  \n");
        printf("                  酒醉酒醒日复日,网上网下年复年。  \n");
        printf("                  但愿老死电脑间,不愿鞠躬老板前;  \n");
        printf("                  奔驰宝马贵者趣,公交自行程序员。  \n");
        printf("                  别人笑我忒疯癫,我笑自己命太贱;  \n");
        printf("                  不见满街漂亮妹,哪个归得程序员?\n");
    }
    
    int main() {
        // 函数调用
        func();
        func();
        func();
    
        return 0;
    }

1.2 无参无返回值

在 C 语言中,无参无返回值函数是指既不需要接收参数,也不返回任何结果的函数。

  • void 关键字表示 "无类型",用于说明函数不需要返回值,且不接收参数
  • 括号中的 void 明确表示该函数不接受任何参数(可省略,但建议写上以增强可读性)
c 复制代码
#include <stdio.h>

/*
函数定义:
void 函数名() {
}

函数调用:
函数名();

需求:函数内部实现2个数相加
*/

// 函数定义
void my_add() {
    int res = 1 + 2;
    printf("res = %d\n", res);
}

int main() {
    // 函数调用
    my_add();

    my_add();

    return 0;
}

1.3 有参无返回值

  • 函数参数的作用:增加函数的灵活性

  • 可根据需求在调用函数时, 通过参数传入不同的数据

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

/*
函数定义: () 定义变量,这个变量叫形参,这个变量就是参数
void 函数名(形参1, 形参2, ......) {
}

函数调用:() 传递参数,这个参数,叫实参
函数名(实参1, 实参2, .........);

形参和实参关系:从左往右,实参和形参要一一对应传参
*/

// 函数定义
void my_add(int a, int b) {
    int res = a + b;
    printf("%d + %d = %d\n", a, b, res);
}

int main() {
    // 函数调用
    my_add(10, 20);

    return 0;
}

1.4 有参有返回值

在 C 语言中,有参有返回值函数是指既需要接收参数(输入数据),又会返回一个结果(输出数据)的函数,这类函数能实现更灵活的逻辑处理,是实际开发中最常用的函数类型之一。

  • 返回值类型:可以是任意基本数据类型(int、float、char等)或自定义类型
  • 参数列表:用逗号分隔的多个参数,每个参数需指定类型和名称(形参)
  • 返回值类型一致:return 语句后的表达式类型必须与函数声明的返回值类型一致,否则会导致编译错误或隐式类型转换。
  • 多参数顺序:传入实参的顺序必须与形参声明顺序一致,否则会导致逻辑错误。
c 复制代码
#include <stdio.h>

/*
函数定义: () 定义变量,这个变量叫形参,这个变量就是参数
类型 函数名(形参1, 形参2, ......) {

    return 内容(要和函数定义的类型匹配);
}

函数调用:() 传递参数,这个参数,叫实参
匹配的类型 接收返回值的变量 = 函数名(实参1, 实参2, .........);

形参和实参关系:从左往右,实参和形参要一一对应传参
*/

// 函数定义
int my_add(int a, int b) {
    int res = a + b;

    return res;
}

int main() {
    // 函数调用
    int temp = my_add(10, 20);
    printf("temp = %d\n", temp);

    return 0;
}
1.4.1 返回值基本语法
  • 函数返回值的作用:函数外部想使用函数内部的数据
c 复制代码
#include <stdio.h>

// 需求:函数内,判断一个形参,奇偶数,> 0 才判断, <= 0 提示参数不符合
void f1(int num){
    if (num <= 0){
        printf("%d 不符合, 必须 >= 0\n", num);
    }else{
        if (num % 2 == 0){
            printf("%d 是偶数\n", num);
        }else{
            printf("%d 是奇数\n", num);
        }
    }
}

void f2(int num) {
    if (num <= 0) {
        printf("%d 不符合, 必须 > 0\n", num);
        return; // 提前结束函数
    }

    // 能执行到这,说明 num > 0
    if (num % 2 == 0){
        printf("%d 是偶数\n", num);
    } else{
        printf("%d 是奇数\n", num);
    }
}

int main(){
    f2(-10);
    f2(5);

    return 0;
}
1.4.2 返回值注意点
  • return只能用在函数里

  • return的作用是结束函数

  • 函数内,return后面的代码不会执行

1.5 函数的声明

  • 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
  • 所谓函数声明,相当于告诉编译器,函数是有定义的,在别的地方定义,以便使编译能正常进行
  • 注意:一个函数只能被定义一次,但可以声明多次
c 复制代码
#include <stdio.h>

/*
- 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
- 所谓函数声明,相当于告诉编译器,函数是有定义的,在别的地方定义,以便使编译能正常进行
- 注意:一个函数只能被定义一次,但可以声明多次
*/

// 如果函数定义代码没有放在函数调用的前面,这时候需要先做函数的声明
// 所谓函数声明,相当于告诉编译器,函数是有定义的,在别的地方定义,以便使编译能正常进行
// 注意:一个函数只能被定义一次,但可以声明多次
int my_add(int a, int b);
int my_add(int, int); // 声明时,形参变量名,可以省略,定义时不可以
extern int my_add(int, int); // 可以加extern,加不加效果一样

int main() {
    // 函数调用
    int temp = my_add(10, 20); // 调用前,没有定义,需要声明
    printf("temp = %d\n", temp);

    return 0;
}

// 函数定义
int my_add(int a, int b) {
    int res = a + b;

    return res;
}

1.6 函数案例

  • 自定义一个函数,返回2个整数的最大值
  • 自定义一个函数,返回3个整数的最大值
c 复制代码
#include <stdio.h>

// - 自定义一个函数,返回2个整数的最大值
int get_2num_max(int a, int b) {
    return a > b ? a : b ;
}

// - 自定义一个函数,返回3个整数的最大值
int get_3num_max(int x, int y, int z) {
    // 通过 get_2num_max 求 x, y 的最大值  max1
    // 再通过 get_2num_max 求 max1 和 z 的最大值
    int max1 = get_2num_max(x, y);
    int max2 = get_2num_max(max1, z);

    return max2;
}

int main() {
    int temp;
    // 2数最大值
    temp = get_2num_max(33, 22);
    printf("temp = %d\n", temp);

    // 3数最大值
    temp = get_3num_max(8, 7, 6);
    printf("temp = %d\n", temp);

    return 0;
}

1.7 局部和全局变量

1.7.1 局部变量
  • 定义位置
    • 函数体内部、代码块{}内部、循环控制语句中,如 for(int i=0; ...)
  • 作用域(作用范围)
    • 从其定义点 开始,到其所在的代码块 ({}) 结束为止。离开了这个范围,就无法访问该变量。
  • 生命周期(存在时间)
    • 当程序执行进入其作用域时,局部变量被创建(分配内存)。
    • 当程序执行离开其作用域时,局部变量被销毁。
    • 销毁意味着其占用的内存被系统回收,用户不再有权限访问这块内存。
  • 没有初始化,值为随机数
c 复制代码
#include <stdio.h>

/*
- **定义位置**
    - 函数体内部、代码块{}内部、循环控制语句中,如 for(int i=0; ...)
- **作用域(作用范围)**
    - 从其**定义点**开始,到其所在的**代码块 ({}) 结束**为止。离开了这个范围,就无法访问该变量。
- 生命周期(存在时间)
    - 当程序执行**进入**其作用域时,局部变量被创建(分配内存)。
    - 当程序执行**离开**其作用域时,局部变量被销毁。
    - **销毁**意味着其占用的内存被系统回收,用户不再有权限访问这块内存。
- 没有初始化,值为随机数
*/

void func(int a, int b) {
    int c;
}

int main() {
    {
        int d = 10;  
    }
    // printf("d = %d\n", d); // err
    
    for (int i = 0; i < 3; i++) {

    }

    // printf("i = %d\n", i); // err

    int temp; // 没有初始化,值为随机数
    printf("temp = %d\n", temp);

    return 0;
}
1.7.2 全局变量
  • 定义位置
    • 在函数外部定义。通常位于源文件的顶部。
  • 作用域(作用范围)
    • 全局作用域 (Global Scope) 。从其定义点 开始,到整个文件结束为止。
  • 生命周期
    • 在程序启动时(通常在 main 函数执行之前)被创建和初始化。
    • 在程序结束时才被销毁。
    • 它的内存在程序的整个运行期间都持续存在
  • 没有初始化,值为0
c 复制代码
#include <stdio.h>

/*
- **定义位置**
    - 在函数**外部**定义。通常位于源文件的顶部。
- **作用域(作用范围)**
    - **全局作用域 (Global Scope)**。从其**定义点**开始,到**整个文件结束**为止。
- **生命周期**
    - 在程序**启动时**(通常在 main 函数执行之前)被创建和初始化。
    - 在程序**结束时**才被销毁。
    - 它的内存在程序的整个运行期间都**持续存在**。
- 没有初始化,值为0
*/
int g_num1; // 没有初始化,值为0

void func() {
    extern int g_num2; // 全局变量的声明,extern不要省略

    printf("g_num2 = %d\n", g_num2);
}

int g_num2 = 250; // 全局变量

int main() {
    // 调用函数
    func();

    return 0;
}

1.8 多文件编程

在 C 语言开发中,多文件编程是将程序按功能拆分到多个源文件(.c)和头文件(.h)中的组织方式,能提高代码复用性、可维护性和团队协作效率。以下是其核心概念和实现方法: 18.1 文件分类与作用 多文件编程通常包含三类文件:

  • 头文件(.h) 存放函数声明、宏定义、结构体 / 枚举类型定义、外部变量声明等。 作用:供其他文件引用,告知编译器 "这些内容的存在"。
  • 源文件(.c) 存放函数实现、全局变量定义等具体逻辑。 每个 .c 文件可对应一个功能模块(如 uart.c 处理串口通信)。
  • 主程序文件(如 main.c) 包含 main() 函数,作为程序入口,调用其他模块的功能。

18.2 核心规则 "声明与实现分离"

  • 函数声明放在 .h 中,实现放在 .c 中。 例:math.h 声明 add() 函数,math.c 实现 add() 函数。

"头文件保护"

  • 用 #ifndef 防止头文件被重复包含(避免编译错误)。

18.3 示例:实现一个简单的多文件程序 假设我们要实现一个 "计算器" 程序,拆分以下模块:

  1. 数学功能模块

math.h(头文件,声明函数)

c 复制代码
#ifndef MATH_H  // 头文件保护
#define MATH_H

// 声明加法函数
int add(int a, int b);

// 声明减法函数
int subtract(int a, int b);

#endif  // MATH_H

math.c(源文件,实现函数)

c 复制代码
#include "math.h"  // 引用自身头文件(检查声明与实现是否一致)

// 实现加法
int add(int a, int b) {
    return a + b;
}

// 实现减法
int subtract(int a, int b) {
    return a - b;
}
  1. 主程序模块 main.c(程序入口)
c 复制代码
#include <stdio.h>
#include "math.h"  // 引用math模块的头文件,使用add()和subtract()

int main() {
    int x = 10, y = 3;
    
    // 调用math模块的函数
    printf("%d + %d = %d\n", x, y, add(x, y));       // 输出:13
    printf("%d - %d = %d\n", x, y, subtract(x, y));  // 输出:7
    
    return 0;
}

18.4 编译与链接 多文件程序不能直接单个编译,需要编译所有 .c 文件并链接:

  1. 手动编译(以 GCC 为例)
c 复制代码
# 编译所有.c文件为目标文件(.o)
gcc -c math.c -o math.o    # 生成math.o
gcc -c main.c -o main.o    # 生成main.o

# 链接目标文件为可执行程序
gcc math.o main.o -o calculator

# 运行程序
./calculator
  1. 用 IDE 自动编译 在 Keil、VS Code、Dev-C++ 等 IDE 中,只需将所有 .c 和 .h 文件添加到项目中,IDE 会自动处理编译和链接。

二、指针

  • 学习一个新的类型,指针类型

2.1 基本语法

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

/*
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
*/
int main() {
    // 定义变量,变量类型为int
    int a = 10;
    // 定义变量,变量类型为指针类型,变量名为 p
    // 是变量,就可以保存数据   类型匹配    int * 类型  保存 int的地址
    int*  p = &a; // 指针变量指向谁,就把谁的地址赋值给指针变量
    printf("p = %p, &a = %p\n", p, &a);

    // 可通过 * 间接操作 a 的内存
    // p 和 *p 不是同一个内存空间
    // *p 指指针所指向的内存,就是a
    *p = 123;
    printf("a = %d\n", a);

    return 0;
}
2.1.1 指针变量的定义和使用
  • 指针也是一种数据类型,指针变量也是一种变量
  • 指针变量指向谁,就把谁的地址赋值给指针变量
c 复制代码
#include <stdio.h>

/*
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
*/
int main() {
    // 定义变量,变量类型为int
    // tom   外卖
    int a = 10;
    // a 指内存      &a 内存的地址  地址也叫指针
    printf("a = %d, &a = %p\n", a, &a); 
    // 定义变量 p,  类型为 int*   匹配类型   int * 保存  int 地址
    // 定义一个变量  保存 a 的地址 ,这个变量  地址变量   指针变量
    // 指针也是一种数据类型,指针变量也是一种变量
    int*           p = &a; // 指针变量指向谁,就把谁的地址赋值给指针变量
    printf("p = %p, &a = %p\n", p, &a);
    // 指针的意义,间接操作内存
    // p 和 *p 不是同一个内存
    // *p 指 a,  指针所执行的内存
    printf("*p = %d, a = %d\n", *p, a);

    return 0;
}
2.1.2 通过指针间接修改变量的值
  • 指针变量指向谁,就把谁的地址赋值给指针变量
  • 通过 *指针变量 间接修改变量的值
c 复制代码
#include <stdio.h>

int main() {
    int a = 10;

    // 变量p 保存 a 的地址,类型为   int * 
    int *          p = &a;  // 指针指向谁,就把谁的地址赋值给这个指针变量
    // 通过 *p 间接修改a 
    *p = 123; // *p 指p所指向的内存,就是a
    printf("a = %d\n", a);

    int b = 5;
    // &b 给p赋值
    p = &b;
    *p = 250; // *p 就是 b
    printf("a = %d, b = %d\n", a, b);

    return 0;
}
2.1.3 const修饰的指针变量
  • 语法格式
C 复制代码
int a = 10;
const int *p1 = &a;	// 等价于 int const *p1 = &a;
int * const p2 = &a;
const int * const p3 = &a;
  • a本身可以修改内容,不能通过指针间接修改a
  • 从左往右看,跳过类型,看修饰哪个字符
    • 如果是*, 说明指针指向的内存不能改变
    • 如果是指针变量,说明指针的指向不能改变,指针的值不能修改
c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int b = 1;
    const int *p1 = &a;	// 等价于 int const *p1 = &a;
    // *p1 = 123; // *p1 不能改
    // p1 = &b;  // ok

    int * const p2 = &a;
    // p2 = &b; // err
    // *p2 = 123; // ok

    const int * const p3 = &a;
    // *p3 = 123; // err
    // p3 = &b;   // err

    return 0;
}
2.1.4 指针大小
  • 使用sizeof()测量指针的大小,得到的总是:4或8
  • sizeof()测的是指针变量指向存储地址的大小
    • 在32位平台,所有的指针(地址)都是32位(4字节)
    • 在64位平台,所有的指针(地址)都是64位(8字节)
c 复制代码
#include <stdio.h>

int main() {
    char * a;
    int * b;
    double * c;
    int ** d;
    printf("%d, %d, %d, %d\n", sizeof(a), sizeof(b), sizeof(c), sizeof(d));

    return 0;
}
2.1.5 指针步长
  • 指针步长指的是通过指针进行递增或递减操作时,指针所指向的内存地址相对于当前地址的偏移量。
  • 指针的步长取决于所指向的数据类型。
    • 指针加n等于指针地址加上 n 个 sizeof(type) 的长度
    • 指针减n等于指针地址减去 n 个 sizeof(type) 的长度
c 复制代码
#include <stdio.h>

int main() {
    char a;
    char * pa = &a; // char * 指向  char
    printf("pa = %p, pa + 1 = %p\n", pa, pa + 1); // +1

    int b;
    int * pb = &b; // int * 指向  int
    printf("pb = %p, pb + 1 = %p\n", pb, pb + 1); // +4

    double c;
    double * pc = &c; // double * 指向  double
    printf("pc = %p, pc + 1 = %p\n", pc, pc + 1); // +8

    return 0;
}
2.1.6 野指针和空指针
概念 描述 危险性/安全性
野指针 指向未知或非法内存的指针。 极度危险:解引用会导致程序崩溃。
空指针 指向地址0 (NULL)的指针,明确表示不指向任何对象。 安全:可通过判断是否为NULL来避免错误。
c 复制代码
#include <stdio.h>

int main() {
    int * p; // 指针变量

    p = 0x11223344; // 内容,乱写的,内容不是系统分配的地址
    // 操作p本身的内存没有问题
    printf("p = %#x\n", p);
    // 不能操作*p, 不能操作野指针所指向的内存
    // *p = 123; // err
    printf("111111111111111111111111\n");

    // 赋值为NULL,说明指针变量p, 没有任何指向,可以用
    int * p2 = NULL;
    int a = 10;

    if (p2 == NULL) {
        p2 = &a; 
    }

    return 0;
}
2.1.7 多级指针
  • C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。
c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int *     p1 = &a;  // int * 指向  int
    int **    p2 = &p1; // int ** 指向 int *
    int ***   p3 = &p2; // int *** 指向 int **

    printf("%d, %d, %d, %d\n", ***p3, **p2, *p1, a);

    return 0;
}

2.2 指针和函数

2.2.1 函数参数传值
  • 传值是指将参数的值拷贝一份传递给函数,函数内部对该参数的修改不会影响到原来的变量
c 复制代码
#include <stdio.h>

// 通过函数交互2个实参的值
void swap(int a, int b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
    printf("函数内部:a = %d, b = %d\n", a, b);
}

int main() {
    int a = 10;
    int b = 20;
    swap(a, b); // 变量本身传递,不是&传递,值传递
    printf("函数外部:a = %d, b = %d\n", a, b);

    return 0;
}
2.2.2 函数参数传址
  • 传址是指将参数的地址传递给函数,函数内部可以通过该地址来访问原变量,并对其进行修改。
c 复制代码
#include <stdio.h>

// 通过函数交互2个实参的值
void swap(int * p1, int * p2) {
    int temp;
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
    printf("函数内部:*p1 = %d, *p2 = %d\n", *p1, *p2);
}

int main() {
    int a = 10;
    int b = 20;
    swap(&a, &b); // 变量取地址传递,&传递,地址传递
    printf("函数外部:a = %d, b = %d\n", a, b);

    return 0;
}
相关推荐
舒一笑7 分钟前
如何优雅统计知识库文件个数与子集下不同文件夹文件个数
后端·mysql·程序员
IT果果日记8 分钟前
flink+dolphinscheduler+dinky打造自动化数仓平台
大数据·后端·flink
Java技术小馆19 分钟前
InheritableThreadLoca90%开发者踩过的坑
后端·面试·github
寒士obj28 分钟前
Spring容器Bean的创建流程
java·后端·spring
数字人直播1 小时前
视频号数字人直播带货,青否数字人提供全套解决方案!
前端·javascript·后端
shark_chili2 小时前
提升Java开发效率的秘密武器:Jadx反编译工具详解
后端
武子康2 小时前
大数据-75 Kafka 高水位线 HW 与日志末端 LEO 全面解析:副本同步与消费一致性核心
大数据·后端·kafka
YANGZHAO2 小时前
Docker零基础入门:一文搞定容器化核心技能
后端·docker
字节跳跃者2 小时前
SpringBoot + MinIO + kkFile 实现文件预览,这样操作更安全!
java·后端·程序员
我是哪吒2 小时前
分布式微服务系统架构第167集:从零到能跑kafka-redis实战
后端·面试·github