学而时习之: C语言的函数

76 阅读9分钟

C语言函数

函数是用于实现特定功能的代码块,它们使程序员能够编写模块化且可重用的代码。

C语言中的函数

函数在 C 语言中的工作原理函数语法
基本结构如下:

return_type function_name(parameter_list) {
    // 函数体
}

各部分说明:

  • 返回类型(return_type):指定函数返回值的类型。如果函数不返回值,则使用 void
  • 函数名(function_name):用于唯一标识函数的名称,命名规则与变量相同。
  • 参数列表(parameter_list):传递给函数的输入值。如果函数不需要输入,此项可留空或写为 void
  • 函数体(function body):函数被调用时执行的代码块,用花括号 { } 包围。 函数声明与定义的区别
    理解“声明”和“定义”之间的差异很重要,二者在编译器理解和使用函数时扮演不同角色。

A、函数声明
声明只向编译器提前透露函数的名称、返回类型和参数列表,不包含函数体。通常写在文件顶部或头文件中

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

B、函数定义
定义给出函数的真正实现,即当函数被调用时实际执行的代码或逻辑。

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

为什么需要声明?
如果函数在 main 函数或其他调用它的函数之后才定义,就必须在调用之前先声明。这样编译器才能识别该函数,并检查调用是否正确。 一句话:
声明告诉编译器“有个函数长这样”;
定义告诉编译器“这个函数具体做什么”。

C、C语言的函数种类
在 C 语言里,函数可大致分成两类:库函数用户自定义函数。按“是否拿输入、是否吐结果”再细分,用户自定义函数又可分成 4 种小类。

  1. 库函数
    由 C 标准库直接提供,如 printf()scanf()sqrt() 等。只要包含对应头文件(#include <stdio.h>#include <math.h> 等)即可使用。
  2. 用户自定义函数
    不同场景选不同类型,可让代码更清晰、高效。

参数传递技术

C 语言向函数传递参数主要有两种技术:

1. 值传递(Pass By Value)

做法:把实参的副本传给函数。函数内部对参数的修改只影响副本,不影响原来的实参。又称“按值调用”。

示例

#include <stdio.h>
// 值传递
void func(int val) {
    val = 123;   // 只改副本
}

int main() {
    int x = 1;
    func(x);     // 把 x 的副本传进去
    printf("%d", x);  // 输出 1
    return 0;
}

结果x 的值仍是 1,因为函数里改的是副本。
特点

  • 简单、易理解。
  • 对大数组或结构体效率低(需要整份拷贝)。
    注意:C、C++、Java 都支持这种传参方式;Java 甚至只有值传递。

2. 指针传递(Pass by Pointers)

做法:把实参的地址传给函数,函数通过指针解引用直接操作那块内存,因此函数内的修改会反映到原始变量。又称“按指针调用”。 示例

#include <stdio.h>

// 指针传递
void func(int* val) {
    *val = 123;  // 通过地址修改原变量
}

int main() {
    int x = 1;
    func(&x);    // 把 x 的地址传进去
    printf("%d", x);  // 输出 123
    return 0;
}

结果x 被改成 123。

特点

  • 能修改调用者的变量。
  • 传大对象时只传一个地址,效率高。
  • 指针操作复杂,容易出错。
    注意:不要把它跟 C++ 的“引用传递”混淆。C 语言没有真正的引用类型,我们靠“传指针”达到类似效果。

主函数

main 函数是 C 程序的入口点。它是一个用户定义的函数,程序的执行从这里开始。每个 C 程序都必须包含一个 main 函数,其返回值通常用于表示程序执行成功或失败。 C 语言中 main() 的三种写法

1. 无参数且无返回值(void main())——不推荐

这种写法不接收参数,也不返回值。虽然某些编译器支持,但不符合 C 标准,不建议使用

示例:

#include <stdio.h>

void main() {
    printf("Hello Geek!");
}

注意:
根据 C 标准,main 函数的返回类型必须是 int。即使你的编译器允许使用 void main(),也应避免使用。

2. 无参数但返回 int ——推荐

这是最常见、最标准的写法。它不接收命令行参数,但会返回一个整数值给操作系统。通常返回 0 表示成功,非零表示出错。

示例:

#include <stdio.h>

int main() {
    printf("Hello Geek!");
    return 0;
}

3. 带命令行参数

当程序需要从命令行接收参数时使用这种写法。通过 argcargv 获取参数数量和具体内容。

示例:

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

内联函数

在 C 语言中,内联函数是一种特殊类型的函数,编译器将函数调用替换为函数体的实际代码,而不是通过常规的函数调用机制来执行。这种方式可以减少函数调用的开销,从而提高性能。内联函数通常体积较小且被频繁调用,使用关键字 inline 来声明。 下面是你提供的英文内容的中文翻译:

(1)语法

要将一个函数声明为内联函数,需要在函数返回类型前加上关键字 inline

inline return_type function_name(parameters) {
    // 函数体
}

(2)内联函数的行为

inline 只是一个建议或请求,而不是强制命令,编译器可以选择忽略它。内联函数的实际行为取决于它的使用方式以及编译器如何实现内联优化。下面我们以 GCC 编译器为例进行说明,GCC 是最常用的编译器之一。

1. 未开启 GCC 优化时的内联行为

GCC 将内联函数作为优化的一部分。当你在 C 源文件中使用 inline 声明一个函数,并且没有开启任何优化选项时,会出现如下错误:

#include <stdio.h>

inline int foo() {
    return 2;
}

int main() {
    int res;
    res = foo();
    printf("%d", res);
    return 0;
}

输出错误:

/usr/bin/ld: /tmp/ccBVKkSP.o: in function `main':
solution.c:(.text+0x12): undefined reference to `foo'
collect2: error: ld returned 1 exit status

错误原因解释:

这是 GCC编译器 处理内联函数的方式所导致的。当编译器未开启优化时,GCC 不会自动生成内联函数的符号(symbol),因为它默认认为该函数会在其他翻译单元中定义。由于函数体没有被插入到调用点,也没有生成可链接的函数定义,链接器就会报错:找不到函数 foo 的定义


总结一句话:

在没有开启优化的情况下,GCC编译器 中的 inline 函数不会生成实际的函数体代码,导致链接时找不到定义,从而报错。


如果你希望即使在不开优化的情况下也能使用 inline 函数,可以:

  • 使用 static inline 替代 inline
  • 或者开启优化选项,如 -O1-O2 等。

2. 开启 GCC 优化后的内联行为

解决上述问题的第一种方法是在编译时打开 GCC 的优化开关,只要在编译命令中加上优化级别即可:

gcc solution.c -o solution -O1

任何高于 -O0 的优化级别(如 -O1-O2-O3)都会启用 GCC 的内联优化。

示例代码:

#include <stdio.h>

// C 语言中的内联函数
inline int foo() {
    return 2;
}

int main() {
    int res;

    // 内联函数调用
    res = foo();
    printf("%d", res);
    return 0;
}

程序输出:

2
3. 静态内联函数(static inline)

我们也可以在 inline 前加上关键字 static。 这样做会把函数的作用域限制在当前翻译单元内部(即“内部链接”),并强制编译器在链接阶段也把它当作可用符号处理,从而无论是否开启优化,程序都能顺利编译并运行。 当然,函数是否真的被“内联展开”,仍取决于编译器的优化级别。

示例代码:

#include <stdio.h>

// C 语言中的静态内联函数
static inline int foo() {
    return 2;
}

int main() {
    int res;

    // 内联函数调用
    res = foo();
    printf("%d", res);
    return 0;
}

运行结果:

2
4. 先声明、后定义成 inline 的情况

如果先给出函数的普通前置声明(forward declaration),再在后面把它定义为 inline,那么:

  • 该函数会被正式加入符号表;
  • 当优化级别为 -O1 及以上时,编译器仍可能把它内联展开;
  • 若优化级别为 -O0(即未开启优化),虽然不会内联,但仍能作为普通函数正常调用、链接并执行。

示例代码:

#include <stdio.h>

// 前置声明
int foo();

// 定义为内联函数
inline int foo() {
    return 2;
}

int main() {
    int res;

    // 函数调用(可能被内联,也可能正常调用)
    res = foo();
    printf("%d", res);
    return 0;
}

运行结果:

2
5. extern inline 函数

把函数声明成 extern inline 时,编译器会按“外部链接”去寻找其定义:

  • 若在其他翻译单元找到定义,则是否内联仍由优化级别决定;
  • 无论是否内联,程序都能正常编译、链接并运行,不会报错。

归根结底,能否真正内联完全取决于编译器的优化策略;其余各种写法只是“兜底”手段,用来避免链接错误。


(3)其他编译器中的 inline

  • GCC / Clanginline 只是“建议”,不开优化通常不会内联。
  • MSVC:也有 inline,但要强制内联需用 __forceinline
    各种编译器都会尽量对小型函数做内联,但具体行为受编译选项影响。

(4)inline 函数 vs 普通函数

inline 函数普通函数
inline 关键字inline 关键字
编译时尝试把调用点直接展开成代码通过正常的 call/ret 机制调用
省去调用开销,可能提速有压栈、跳转、返回等开销
过度使用会膨胀二进制体积代码只有一份,体积影响小

(5)inline 函数 vs 宏

inline 函数
有类型检查,类型安全无类型检查,易出错
可像普通函数一样调试调试困难,报错信息难懂
实参只计算一次实参可能多次求值,产生副作用
支持递归不能递归

(6)何时使用 inline 函数?

  1. 小且频繁调用的函数
    避免频繁调用开销,提升性能。
  2. 时间关键代码
    如嵌入式、中断服务程序等对时钟敏感的场合。
  3. 需要类型安全、作用域控制的场景
    相比宏,inline 函数保留类型检查与作用域规则,更安全、可维护。