编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

      • README
      • 基础语法
      • 数据类型
      • 运算符
      • 循环和选择
      • 输入输出
      • 函数
        • 6.1 概述和定义
          • 6.1.1 函数概念
          • 6.1.2 函数的定义
          • 6.1.3 函数声明
          • 6.1.4 综合案例与思考
          • 6.2.1 调用函数语法
          • 6.2.2 理解栈帧
          • 6.2.3 综合案例与思考
          • 6.3.1 参数
          • 6.3.2 默认参数
          • 6.3.3 传值和传址
          • 6.3.4 综合案例与思考
          • 6.4.1 void返回
          • 6.4.2 return返回
        • 6.5 函数分类
        • 6.6 函数与数组
          • 6.6.1 数组作为函数参数
          • 6.6.2 多维数组作为函数参数
          • 6.6.3 综合案例与思考
          • 6.7.1 字符串安全函数
          • 1.1 `strcpy_s`
          • 1.2 `strcat_s`
          • 1.3 `strncpy_s`
          • 6.7.2 内存操作安全函数
          • 3.1 `memcpy_s`
          • 3.2 `memset_s`
          • 6.7.3 文件操作安全函数
          • 4.1 `fopen_s`
          • 6.7.4 输入输出安全函数
          • 2.1 `gets_s`
          • 2.2 `scanf_s`
        • 6.8 标准库函数
        • 6.9 函数注意事项
          • 6.9.1 综合案例与思考
      • 指针
      • 数组和容器
      • 类和内存
      • 流与文件
      • 结构体
      • 线程和锁
      • 预处理器
      • 高级数据
    • 综合案例

    • 专栏博客

    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 入门教程
杨充
2025-07-21
目录

函数

# 06.函数

# 目录介绍

  • 6.1 概述和定义
    • 6.1.1 函数概念
    • 6.1.2 函数的定义
    • 6.1.3 函数声明
    • 6.1.4 综合案例与思考
  • 6.2 函数调用
    • 6.2.1 调用函数语法
    • 6.2.2 理解栈帧
    • 6.2.3 综合案例与思考
  • 6.3 函数参数
    • 6.3.1 参数
    • 6.3.2 默认参数
    • 6.3.3 传值和传址
    • 6.3.4 综合案例与思考
  • 6.4 函数返回值
    • 6.4.1 void返回
    • 6.4.2 return返回
  • 6.5 函数分类
  • 6.6 函数与数组
    • 6.6.1 数组作为函数参数
    • 6.6.2 多维数组作为函数参数
    • 6.6.3 综合案例与思考
  • 6.7 安全函数
    • 6.7.1 字符串安全函数
    • 6.7.2 内存操作安全函数
    • 6.7.3 文件操作安全函数
    • 6.7.4 输入输出安全函数
  • 6.8 标准库函数
  • 6.9 函数注意事项
    • 6.9.1 综合案例与思考

# 6.1 概述和定义

在 C 语言中,函数是程序的基本构建块,用于将代码组织成可重用的模块。函数可以接收输入参数,执行特定任务,并返回结果。通过使用函数,可以提高代码的可读性、可维护性和复用性。

# 6.1.1 函数概念

函数的基本概念

  • 函数定义:实现函数功能的代码块。
  • 函数声明:告诉编译器函数的名称、返回类型和参数列表。
  • 函数调用:执行函数中的代码。

# 6.1.2 函数的定义

语法

返回类型 函数名(参数列表) {
    // 函数体
    return 返回值; // 如果返回类型不是 void
}
1
2
3
4

示例

int add(int a, int b) {
    return a + b;
}
1
2
3

# 6.1.3 函数声明

函数声明(也称为函数原型)告诉编译器函数的名称、返回类型和参数列表。

语法

返回类型 函数名(参数列表);
1

示例

int add(int a, int b);
1

原理说明:函数声明(原型)的作用是让编译器在调用函数之前就知道函数的签名信息。C语言采用单遍编译模型,编译器从上到下处理源文件。如果没有提前声明,编译器遇到未知函数时会隐式假设其返回 int,这在C99之后已被废弃。函数原型使编译器能够在编译期检查参数类型和数量是否匹配,是类型安全的重要保障。

# 6.1.4 综合案例与思考

综合案例:函数定义、声明与使用的完整流程

#include <stdio.h>

// 函数声明(原型)——告诉编译器函数签名
int factorial(int n);
double power(double base, int exp);
void print_separator(int length);

int main() {
    // 案例1:阶乘函数
    for (int i = 0; i <= 10; i++) {
        printf("%2d! = %d\n", i, factorial(i));
    }
    
    // 案例2:幂运算
    print_separator(30);
    printf("2^10 = %.0f\n", power(2.0, 10));
    printf("3.14^2 = %.4f\n", power(3.14, 2));
    
    // 案例3:函数的嵌套调用
    print_separator(30);
    // 组合数 C(n,k) = n! / (k! * (n-k)!)
    int n = 10, k = 3;
    int combination = factorial(n) / (factorial(k) * factorial(n - k));
    printf("C(%d,%d) = %d\n", n, k, combination);
    
    return 0;
}

// 函数定义
int factorial(int n) {
    if (n <= 1) return 1;
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; i++) {
        result *= base;
    }
    return result;
}

void print_separator(int length) {
    for (int i = 0; i < length; i++) {
        printf("-");
    }
    printf("\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

原理说明:函数是C语言程序的基本构建块,每个C程序至少有一个函数(main)。函数的三要素是:返回类型、函数名、参数列表。编译器处理函数时会生成一个符号表条目,链接器通过符号表将函数调用与函数定义关联起来。将声明放在文件头部、定义放在 main 之后是一种常见的代码组织方式,可以让 main 函数更早出现,便于阅读。

思考题:

  1. 如果不写函数声明,直接在 main 下方定义函数,会发生什么?C89和C99对此行为有何不同?
  2. 函数声明中参数名可以省略(如 int add(int, int);),为什么允许这样做?什么时候需要保留参数名?
  3. 递归版本的 factorial 和循环版本相比,各有什么优缺点?

# 6.2.1 调用函数语法

通过函数名和参数列表调用函数。

语法

函数名(参数列表);
1

示例

int result = add(3, 5);
1

# 6.2.2 理解栈帧

当函数被调用时,系统会在 stack 空间上申请一块内存,用来给函数调用提供空间。存储 形参 和 局部变量(定义在函数内部的变量)。

函数调用结束时,这块内存空间,会被自动释放 (消失) 。

原理说明:栈帧(Stack Frame)是函数调用的核心机制。每次函数调用时,系统会在栈上创建一个栈帧,包含:返回地址(调用完成后跳转回哪里)、函数参数(按照调用约定压栈)、局部变量、保存的寄存器值。函数返回时,栈帧被弹出,栈指针恢复到调用前的位置。这就是为什么局部变量在函数返回后就"消失"了——它们所在的内存已被回收。栈的大小是有限的(通常几MB),递归调用过深会导致栈溢出(Stack Overflow)。

# 6.2.3 综合案例与思考

综合案例:理解函数调用过程和栈帧

#include <stdio.h>

void func_c() {
    int c = 30;
    printf("  func_c: c的地址 = %p\n", (void *)&c);
    printf("  func_c: 栈帧最深处\n");
}

void func_b() {
    int b = 20;
    printf(" func_b: b的地址 = %p\n", (void *)&b);
    func_c();
    printf(" func_b: func_c返回后继续执行\n");
}

void func_a() {
    int a = 10;
    printf("func_a: a的地址 = %p\n", (void *)&a);
    func_b();
    printf("func_a: func_b返回后继续执行\n");
}

// 递归演示栈帧增长
void recursive(int depth) {
    int local_var = depth;
    printf("递归深度 %d: local_var地址 = %p\n", depth, (void *)&local_var);
    if (depth < 5) {
        recursive(depth + 1);
    }
}

int main() {
    printf("=== 函数调用链演示 ===\n");
    int m = 0;
    printf("main: m的地址 = %p\n\n", (void *)&m);
    func_a();
    
    printf("\n=== 递归栈帧演示 ===\n");
    recursive(1);
    // 观察地址:每深入一层,地址向低地址方向增长(栈向下生长)
    
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

原理说明:通过观察局部变量的地址可以直观看到栈的增长方向——在大多数平台上,栈从高地址向低地址增长。每次函数调用都会消耗一定的栈空间,因此递归深度受栈大小限制。通过 ulimit -s 可查看/修改栈大小(Linux系统)。

思考题:

  1. 运行上面的程序,观察变量地址的变化规律,能得出栈增长方向的结论吗?
  2. 如果一个函数的局部数组很大(如 int arr[1000000]),可能会发生什么?如何解决?
  3. 函数调用有开销(保存/恢复寄存器、压栈弹栈),inline 关键字如何优化这个问题?

# 6.3.1 参数

  • 形式参数:函数定义中的参数。
  • 实际参数:函数调用时传递的参数。

示例

int add(int a, int b) { // a 和 b 是形式参数
    return a + b;
}

int result = add(3, 5); // 3 和 5 是实际参数
1
2
3
4
5

# 6.3.2 默认参数

C语言不支持默认参数(这是C++的特性)。但可以通过以下技巧模拟:

方法1:使用宏定义

#define print_msg(msg, times) _print_msg(msg, times)
#define print_msg_default(msg) _print_msg(msg, 1)

void _print_msg(const char *msg, int times) {
    for (int i = 0; i < times; i++) {
        printf("%s\n", msg);
    }
}
1
2
3
4
5
6
7
8

方法2:使用特殊值标记

void draw_circle(int x, int y, int radius) {
    if (radius <= 0) radius = 10;  // 默认半径为10
    printf("画圆: (%d,%d) 半径=%d\n", x, y, radius);
}
1
2
3
4

# 6.3.3 传值和传址

C语言中函数参数默认是值传递,即函数接收的是参数的副本,修改副本不影响原值。要修改原值,需要传递指针(传址)。

值传递示例

void try_modify(int x) {
    x = 100;  // 只修改了副本
    printf("函数内 x = %d\n", x);
}

int main() {
    int a = 10;
    try_modify(a);
    printf("函数外 a = %d\n", a);  // a仍然是10
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11

传址(指针)示例

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(&x, &y);
    printf("x=%d, y=%d\n", x, y);  // x=20, y=10
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 6.3.4 综合案例与思考

综合案例:传值与传址的深入理解

#include <stdio.h>
#include <stdlib.h>

// 值传递:无法修改外部变量
void bad_alloc(int *p) {
    p = (int *)malloc(sizeof(int));  // 只修改了指针的副本!
    if (p) *p = 42;
}

// 传址:通过二级指针修改指针本身
void good_alloc(int **pp) {
    *pp = (int *)malloc(sizeof(int));
    if (*pp) **pp = 42;
}

// 通过返回值传递结果
int *return_alloc() {
    int *p = (int *)malloc(sizeof(int));
    if (p) *p = 42;
    return p;
}

// 数组作为参数:天然是传址
void double_array(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;  // 直接修改原数组
    }
}

int main() {
    // 案例1:错误的动态分配
    int *p1 = NULL;
    bad_alloc(p1);
    printf("bad_alloc后: p1 = %p (仍为NULL!)\n", (void *)p1);
    
    // 案例2:正确的动态分配
    int *p2 = NULL;
    good_alloc(&p2);
    printf("good_alloc后: p2 = %p, *p2 = %d\n", (void *)p2, *p2);
    free(p2);
    
    // 案例3:通过返回值
    int *p3 = return_alloc();
    printf("return_alloc后: *p3 = %d\n", *p3);
    free(p3);
    
    // 案例4:数组天然传址
    int arr[] = {1, 2, 3, 4, 5};
    printf("\n原数组: ");
    for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
    double_array(arr, 5);
    printf("\n翻倍后: ");
    for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
    printf("\n");
    
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

原理说明:C语言只有值传递这一种参数传递方式。所谓"传址"其实是传递了指针的值(即地址的副本)。要修改指针本身(而非指针指向的内容),需要传递指针的指针(二级指针)。数组作为参数时会退化为指针,所以看起来像传址——函数内可以直接修改原数组。这是C语言中最容易混淆的概念之一。

思考题:

  1. 为什么 bad_alloc 函数无法正确分配内存给外部指针?画出函数调用时内存的变化过程。
  2. 结构体作为函数参数是值传递还是传址?传递大结构体时有什么性能问题?如何优化?
  3. const int *p 和 int *const p 作为函数参数时各有什么含义?
  • 使用 return 语句返回函数的结果。
  • 如果返回类型是 void,则不需要 return 语句。

# 6.4.1 void返回

void printHello() {
    printf("Hello, World!\n");
}
1
2
3

# 6.4.2 return返回

int add(int a, int b) {
    return a + b;
}
1
2
3

# 6.5 函数分类

(1) 无参数无返回值函数

void printHello() {
    printf("Hello, World!\n");
}
1
2
3

(2) 有参数无返回值函数

void printSum(int a, int b) {
    printf("Sum: %d\n", a + b);
}
1
2
3

(3) 无参数有返回值函数

int getRandomNumber() {
    return rand();
}
1
2
3

(4) 有参数有返回值函数

int add(int a, int b) {
    return a + b;
}
1
2
3

# 6.6 函数与数组

# 6.6.1 数组作为函数参数

数组可以作为函数参数传递,实际传递的是数组的首地址。

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

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 6.6.2 多维数组作为函数参数

void printMatrix(int mat[3][3], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}
1
2
3
4
5
6
7
8

# 6.6.3 综合案例与思考

综合案例:函数与数组的综合运用

#include <stdio.h>

// 数组求和
int array_sum(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

// 数组求平均值
double array_avg(int arr[], int size) {
    return (double)array_sum(arr, size) / size;
}

// 查找最大值及其索引
int array_max_index(int arr[], int size) {
    int max_idx = 0;
    for (int i = 1; i < size; i++) {
        if (arr[i] > arr[max_idx]) {
            max_idx = i;
        }
    }
    return max_idx;
}

// 数组反转(原地操作)
void array_reverse(int arr[], int size) {
    for (int i = 0; i < size / 2; i++) {
        int temp = arr[i];
        arr[i] = arr[size - 1 - i];
        arr[size - 1 - i] = temp;
    }
}

// 打印数组
void array_print(const char *label, int arr[], int size) {
    printf("%s: ", label);
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int scores[] = {85, 92, 78, 96, 88, 73, 91};
    int n = sizeof(scores) / sizeof(scores[0]);
    
    array_print("成绩", scores, n);
    printf("总分: %d\n", array_sum(scores, n));
    printf("平均分: %.1f\n", array_avg(scores, n));
    
    int max_i = array_max_index(scores, n);
    printf("最高分: %d (索引=%d)\n", scores[max_i], max_i);
    
    array_reverse(scores, n);
    array_print("反转后", scores, n);
    
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

原理说明:数组作为函数参数时,会退化为指向首元素的指针,因此函数内部无法通过 sizeof 获得数组长度,必须额外传递 size 参数。使用 const 修饰数组参数(如 const int arr[])可以防止函数意外修改数组内容。将常用的数组操作封装为函数是良好的编程习惯,可以提高代码复用性。

思考题:

  1. 为什么数组作为函数参数时,sizeof(arr) 返回的是指针大小而非数组大小?
  2. 如何在函数中"创建"一个数组并返回给调用者?直接返回局部数组有什么问题?
  3. void func(int arr[10]) 中的 10 有实际约束作用吗?编译器会检查吗?

C 语言的安全函数通过引入边界检查机制,可以有效避免缓冲区溢出等安全问题。常见的函数包括:

  • 字符串操作:strcpy_s、strcat_s、strncpy_s。
  • 输入输出:gets_s、scanf_s。
  • 内存操作:memcpy_s、memset_s。
  • 文件操作:fopen_s。

# 6.7.1 字符串安全函数

# 1.1 strcpy_s

  • 功能:安全地复制字符串。
  • 原型:
    errno_t strcpy_s(char *dest, rsize_t dest_size, const char *src);
    
    1
  • 参数:
    • dest:目标字符串。
    • dest_size:目标字符串的缓冲区大小。
    • src:源字符串。
  • 返回值:成功返回 0,失败返回非 0 值。
  • 示例:
    char dest[10];
    strcpy_s(dest, sizeof(dest), "Hello");
    
    1
    2

# 1.2 strcat_s

  • 功能:安全地连接字符串。
  • 原型:
    errno_t strcat_s(char *dest, rsize_t dest_size, const char *src);
    
    1
  • 参数:
    • dest:目标字符串。
    • dest_size:目标字符串的缓冲区大小。
    • src:源字符串。
  • 返回值:成功返回 0,失败返回非 0 值。
  • 示例:
    char dest[10] = "Hello";
    strcat_s(dest, sizeof(dest), "World");
    
    1
    2

# 1.3 strncpy_s

  • 功能:安全地复制指定长度的字符串。
  • 原型:
    errno_t strncpy_s(char *dest, rsize_t dest_size, const char *src, rsize_t count);
    
    1
  • 参数:
    • dest:目标字符串。
    • dest_size:目标字符串的缓冲区大小。
    • src:源字符串。
    • count:要复制的最大字符数。
  • 返回值:成功返回 0,失败返回非 0 值。
  • 示例:
    char dest[10];
    strncpy_s(dest, sizeof(dest), "HelloWorld", 5);
    
    1
    2

# 6.7.2 内存操作安全函数

# 3.1 memcpy_s

  • 功能:安全地复制内存块。
  • 原型:
    errno_t memcpy_s(void *dest, rsize_t dest_size, const void *src, rsize_t count);
    
    1
  • 参数:
    • dest:目标内存块。
    • dest_size:目标内存块的大小。
    • src:源内存块。
    • count:要复制的字节数。
  • 返回值:成功返回 0,失败返回非 0 值。
  • 示例:
    char dest[10];
    char src[] = "Hello";
    memcpy_s(dest, sizeof(dest), src, sizeof(src));
    
    1
    2
    3

# 3.2 memset_s

  • 功能:安全地设置内存块的值。
  • 原型:
    errno_t memset_s(void *dest, rsize_t dest_size, int value, rsize_t count);
    
    1
  • 参数:
    • dest:目标内存块。
    • dest_size:目标内存块的大小。
    • value:要设置的值。
    • count:要设置的字节数。
  • 返回值:成功返回 0,失败返回非 0 值。
  • 示例:
    char buffer[10];
    memset_s(buffer, sizeof(buffer), 0, sizeof(buffer));
    
    1
    2

# 6.7.3 文件操作安全函数

# 4.1 fopen_s

  • 功能:安全地打开文件。
  • 原型:
    errno_t fopen_s(FILE **file, const char *filename, const char *mode);
    
    1
  • 参数:
    • file:指向文件指针的指针。
    • filename:文件名。
    • mode:打开模式(如 "r"、"w" 等)。
  • 返回值:成功返回 0,失败返回非 0 值。
  • 示例:
    FILE *file;
    fopen_s(&file, "test.txt", "r");
    
    1
    2

# 6.7.4 输入输出安全函数

# 2.1 gets_s

  • 功能:安全地从标准输入读取字符串。
  • 原型:
    char *gets_s(char *buffer, rsize_t size);
    
    1
  • 参数:
    • buffer:存储输入字符串的缓冲区。
    • size:缓冲区的大小。
  • 返回值:成功返回 buffer,失败返回 NULL。
  • 示例:
    char buffer[10];
    gets_s(buffer, sizeof(buffer));
    
    1
    2

# 2.2 scanf_s

  • 功能:安全地从标准输入读取格式化数据。
  • 原型:
    int scanf_s(const char *format, ...);
    
    1
  • 参数:
    • format:格式化字符串。
    • ...:可变参数列表。
  • 返回值:成功返回读取的项数,失败返回 EOF。
  • 示例:
    char buffer[10];
    scanf_s("%9s", buffer, sizeof(buffer));
    
    1
    2

# 6.8 标准库函数

C 语言提供了丰富的标准库函数,如:

  • 输入输出:printf、scanf
  • 字符串操作:strcpy、strlen
  • 数学函数:sqrt、pow
  • 内存管理:malloc、free

示例

#include <stdio.h>
#include <string.h>
#include <math.h>

int main() {
    char str[50];
    strcpy(str, "Hello, World!");
    printf("%s\n", str);

    double result = sqrt(16.0);
    printf("Square root: %f\n", result);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.9 函数注意事项

  1. 函数声明:

    • 如果函数定义在调用之后,需要先声明函数。
  2. 参数传递:

    • C 语言中,参数是按值传递的(指针除外)。
  3. 返回值:

    • 如果函数有返回值,必须使用 return 语句。
  4. 递归:

    • 递归函数必须有终止条件,否则会导致栈溢出。
  5. 函数命名:

    • 函数名应具有描述性,遵循命名规范。

# 6.9.1 综合案例与思考

综合案例:函数的综合应用——简易计算器

#include <stdio.h>

// 函数声明
double add(double a, double b);
double subtract(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);

// 函数指针类型定义
typedef double (*operation)(double, double);

// 获取操作函数
operation get_operation(char op) {
    switch (op) {
        case '+': return add;
        case '-': return subtract;
        case '*': return multiply;
        case '/': return divide;
        default: return NULL;
    }
}

int main() {
    // 测试数据
    double a = 10.0, b = 3.0;
    char operators[] = {'+', '-', '*', '/'};
    
    printf("=== 简易计算器 ===\n");
    for (int i = 0; i < 4; i++) {
        operation op = get_operation(operators[i]);
        if (op) {
            printf("%.1f %c %.1f = %.2f\n", a, operators[i], b, op(a, b));
        }
    }
    
    // 函数指针数组
    printf("\n=== 使用函数指针数组 ===\n");
    operation ops[] = {add, subtract, multiply, divide};
    const char *names[] = {"加法", "减法", "乘法", "除法"};
    for (int i = 0; i < 4; i++) {
        printf("%s: %.2f\n", names[i], ops[i](a, b));
    }
    
    return 0;
}

double add(double a, double b) { return a + b; }
double subtract(double a, double b) { return a - b; }
double multiply(double a, double b) { return a * b; }
double divide(double a, double b) {
    if (b == 0) {
        printf("错误: 除数不能为0!\n");
        return 0;
    }
    return a / b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

原理说明:函数指针是C语言实现回调机制和策略模式的核心手段。函数名本身就是指向函数代码起始地址的指针。typedef 可以简化函数指针的类型声明。函数指针数组常用于实现命令分发、事件处理等场景,比大量的 if-else 或 switch-case 更优雅、更易扩展。标准库中的 qsort、bsearch 都使用函数指针作为参数。

思考题:

  1. 函数指针和普通指针有什么区别?sizeof 一个函数指针的结果是什么?
  2. qsort 函数要求传入一个比较函数指针,如何用它对整数数组和字符串数组分别进行排序?
  3. static 函数有什么特点?在多文件项目中如何合理使用 static 限制函数的可见性?
上次更新: 2026/06/10, 11:13:41
输入输出
指针

← 输入输出 指针→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式