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. 递归函数
函数调用自身即为递归。每个递归函数都需要:
- 基本情况(终止条件)
- 递归情况(逐步逼近终止)
#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;
}