强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

C/C++ Linux 开发教程(GCC + CMake) / 04 — 函数与作用域

函数与作用域

1. 函数声明与定义

C 语言中,函数必须先声明后使用。声明(也叫原型)告诉编译器函数的签名,定义提供函数的实现。

// 函数声明(通常放在头文件中)
int add(int a, int b);
void print_message(const char *msg);

// 函数定义
int add(int a, int b)
{
    return a + b;
}

void print_message(const char *msg)
{
    printf("%s\n", msg);
}

💡 提示: 函数声明中的参数名可以省略,只写类型即可:int add(int, int);


2. 参数传递:值传递

C 语言中所有参数都是值传递。函数接收的是参数的副本。

#include <stdio.h>

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

int main(void)
{
    int a = 42;
    try_modify(a);
    printf("函数外 a = %d\n", a);  // a 仍然是 42
    return 0;
}

输出:

函数内 x = 100
函数外 a = 42

2.1 地址传递(通过指针)

要修改调用者的变量,需要传递地址:

#include <stdio.h>

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

int main(void)
{
    int x = 10, y = 20;
    printf("交换前: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("交换后: x=%d, y=%d\n", x, y);
    return 0;
}

输出:

交换前: x=10, y=20
交换后: x=20, y=10
方式说明能否修改原值
值传递传递变量的副本❌ 不能
地址传递传递变量的地址(指针)✅ 可以

3. 返回值

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

// 返回基本类型
double distance(double x1, double y1, double x2, double y2)
{
    double dx = x2 - x1;
    double dy = y2 - y1;
    return sqrt(dx * dx + dy * dy);
}

// 返回多个值的技巧:通过指针参数
void min_max(int arr[], int n, int *min, int *max)
{
    *min = arr[0];
    *max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

int main(void)
{
    double d = distance(0, 0, 3, 4);
    printf("距离: %.2f\n", d);

    int arr[] = {5, 2, 8, 1, 9, 3};
    int n = sizeof(arr) / sizeof(arr[0]);
    int min, max;
    min_max(arr, n, &min, &max);
    printf("min=%d, max=%d\n", min, max);

    return 0;
}

⚠️ 注意: 函数中返回局部变量的地址是未定义行为,因为局部变量在函数返回后被销毁。

// 错误示例!
int *bad_function(void)
{
    int local = 42;
    return &local;  // 危险!local 在函数返回后无效
}

4. 函数原型

函数原型是对函数的前向声明,通常放在头文件中:

math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(double a, double b);

#endif

math_utils.c

#include "math_utils.h"

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
double divide(double a, double b) { return b != 0 ? a / b : 0.0; }

main.c

#include <stdio.h>
#include "math_utils.h"

int main(void)
{
    printf("3 + 5 = %d\n", add(3, 5));
    printf("3.0 / 4.0 = %.2f\n", divide(3.0, 4.0));
    return 0;
}

编译:

gcc -Wall -std=c17 -o calc main.c math_utils.c

5. 作用域

5.1 局部变量

在函数或代码块 {} 内定义,仅在该作用域内可见:

#include <stdio.h>

int main(void)
{
    int x = 10;
    {
        int y = 20;
        printf("x=%d, y=%d\n", x, y);  // OK
    }
    // printf("%d\n", y);  // 错误!y 已经不在作用域内
    printf("x=%d\n", x);   // OK
    return 0;
}

5.2 全局变量

在所有函数外部定义,整个文件可见:

#include <stdio.h>

int counter = 0;  // 全局变量

void increment(void)
{
    counter++;
}

int main(void)
{
    increment();
    increment();
    increment();
    printf("counter = %d\n", counter);  // 3
    return 0;
}

⚠️ 注意: 全局变量增加了模块间的耦合,应尽量避免使用。用 static 限制为文件内部可见。

5.3 作用域对比

类型定义位置生命周期可见范围
局部变量函数/块内函数/块执行期间定义处到块末尾
全局变量函数外程序整个运行期定义处到文件末尾(其他文件需 extern
static 局部函数内程序整个运行期定义处到函数末尾
static 全局函数外程序整个运行期仅当前文件

6. static 变量

6.1 静态局部变量

生命周期延长到整个程序运行期,但作用域不变:

#include <stdio.h>

void counter(void)
{
    static int count = 0;  // 只初始化一次!
    count++;
    printf("调用次数: %d\n", count);
}

int main(void)
{
    counter();  // 调用次数: 1
    counter();  // 调用次数: 2
    counter();  // 调用次数: 3
    return 0;
}

💡 提示: static 局部变量常用于需要在函数调用间保持状态的场景,如计数器、缓存等。

6.2 静态全局变量

// file_a.c
static int internal_var = 42;  // 只在 file_a.c 中可见

// file_b.c
// 无法访问 internal_var

7. extern 声明

当需要在不同文件间共享全局变量时使用:

globals.h

#ifndef GLOBALS_H
#define GLOBALS_H

extern int shared_var;  // 声明(不定义)

#endif

globals.c

#include "globals.h"
int shared_var = 100;   // 定义

main.c

#include <stdio.h>
#include "globals.h"

int main(void)
{
    printf("shared_var = %d\n", shared_var);
    shared_var = 200;
    printf("shared_var = %d\n", shared_var);
    return 0;
}

编译:

gcc -Wall -std=c17 -o prog main.c globals.c

8. 递归函数

函数调用自身即为递归。每个递归函数都需要:

  1. 基本情况(终止条件)
  2. 递归情况(逐步逼近终止)
#include <stdio.h>

// 阶乘
unsigned long long factorial(int n)
{
    if (n <= 1) return 1;        // 基本情况
    return n * factorial(n - 1);  // 递归情况
}

// 斐波那契数列
long fibonacci(int n)
{
    if (n <= 0) return 0;
    if (n == 1) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main(void)
{
    for (int i = 0; i <= 10; i++) {
        printf("factorial(%d) = %llu\n", i, factorial(i));
    }

    printf("\n斐波那契数列前 15 项:\n");
    for (int i = 0; i < 15; i++) {
        printf("%ld ", fibonacci(i));
    }
    printf("\n");

    return 0;
}

⚠️ 注意: 简单的斐波那契递归时间复杂度为 O(2^n),效率极低。实际使用应改用迭代或带记忆化的递归。

8.1 递归的应用:二分查找

#include <stdio.h>

int binary_search(int arr[], int left, int right, int target)
{
    if (left > right) return -1;  // 未找到

    int mid = left + (right - left) / 2;

    if (arr[mid] == target) return mid;
    if (arr[mid] < target) return binary_search(arr, mid + 1, right, target);
    return binary_search(arr, left, mid - 1, target);
}

int main(void)
{
    int arr[] = {1, 3, 5, 7, 9, 11, 13, 15};
    int n = sizeof(arr) / sizeof(arr[0]);

    int idx = binary_search(arr, 0, n - 1, 7);
    printf("7 的位置: %d\n", idx);

    idx = binary_search(arr, 0, n - 1, 6);
    printf("6 的位置: %d\n", idx);  // -1

    return 0;
}

9. 内联函数 (inline)

inline 建议编译器将函数调用替换为函数体,减少调用开销:

#include <stdio.h>

inline int square(int x)
{
    return x * x;
}

inline int max(int a, int b)
{
    return a > b ? a : b;
}

int main(void)
{
    printf("5² = %d\n", square(5));
    printf("max(3, 7) = %d\n", max(3, 7));
    return 0;
}

⚠️ 注意: inline 仅是对编译器的建议,编译器可以选择不内联。同时,inline 函数在多个编译单元中可能导致链接错误,通常应配合 static 使用:

static inline int square(int x)
{
    return x * x;
}

10. main 函数参数

main 函数可以接收命令行参数:

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("参数个数: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }
    return 0;
}

编译运行:

gcc -Wall -o args args.c
./args hello world 42

输出:

参数个数: 4
argv[0] = "./args"
argv[1] = "hello"
argv[2] = "world"
argv[3] = "42"

10.1 解析命令行参数

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

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "用法: %s [-n 数字] [-v]\n", argv[0]);
        return 1;
    }

    int number = 0;
    int verbose = 0;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
            number = atoi(argv[++i]);
        } else if (strcmp(argv[i], "-v") == 0) {
            verbose = 1;
        }
    }

    if (verbose) {
        printf("number = %d\n", number);
    }
    printf("结果: %d\n", number * 2);

    return 0;
}

💡 提示: 对于复杂的命令行解析,可以使用 getopt() 函数(POSIX 标准)。


11. 函数设计原则

原则说明
单一职责一个函数只做一件事
命名清晰函数名应描述行为,如 calculate_average
控制长度建议不超过 50 行
避免全局状态通过参数传递数据,减少副作用
明确输入输出参数是输入,返回值和指针参数是输出
错误处理返回错误码或使用 errno

实际场景:工具函数库

// string_utils.h
#ifndef STRING_UTILS_H
#define STRING_UTILS_H

#include <stddef.h>

// 去除首尾空白
char *trim(char *str);

// 转小写
char *to_lower(char *str);

// 统计字符出现次数
size_t count_char(const char *str, char ch);

// 检查是否以指定前缀开头
int starts_with(const char *str, const char *prefix);

#endif
// string_utils.c
#include "string_utils.h"
#include <ctype.h>
#include <string.h>

char *trim(char *str)
{
    while (isspace((unsigned char)*str)) str++;
    char *end = str + strlen(str) - 1;
    while (end > str && isspace((unsigned char)*end)) *end-- = '\0';
    return str;
}

char *to_lower(char *str)
{
    for (char *p = str; *p; p++) {
        *p = (char)tolower((unsigned char)*p);
    }
    return str;
}

size_t count_char(const char *str, char ch)
{
    size_t count = 0;
    while (*str) {
        if (*str == ch) count++;
        str++;
    }
    return count;
}

int starts_with(const char *str, const char *prefix)
{
    return strncmp(str, prefix, strlen(prefix)) == 0;
}

12. 扩展阅读