news 2026/10/21 16:05:22

C语言中函数的声明、定义及使用的入门教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言中函数的声明、定义及使用的入门教程

前言


初学 C 语言时,「声明」「定义」「调用」这三个词经常被混着用。有人把函数头加一个分号就叫定义,有人把头文件里的原型当成实现,还有人写了函数却忘了在调用处之前声明,靠编译器「猜」出一个返回int的版本。这些混淆在单个.c文件里往往能蒙混过关,一旦拆成多个文件、或者换一个更严格的编译器,就会变成编译错误或链接错误。


需要先明确一件事:C 语言从 C99 起就不再允许隐式函数声明,调用一个此前没有声明过的函数属于约束违反,编译器必须给出诊断;C23 进一步收紧,隐式声明和旧式(K&R)函数定义都不再是标准的一部分。所以「不声明也能用」不是语言特性,而是旧编译器留下的宽松行为。


本文按「声明—定义—使用」的顺序讲清三者的区别:声明描述接口,定义提供实体,使用则是调用方按接口发起调用。然后给出一个头文件加两个源文件的完整组织方式,最后列出初学者最常踩的坑。


一、声明:只描述接口,不产生实体


函数声明(function declaration)告诉编译器:这个名字是一个函数,返回什么类型,接受几个什么类型的参数。它不生成任何代码,也不分配存储。带参数类型的声明叫函数原型(function prototype),是 C 语言里唯一推荐的写法。


/* 适用于 C17 */
int add(int a, int b); /* 原型:两个 int 参数,返回 int */
double average(const int *data, size_t count);
void print_banner(void); /* 明确表示不接受参数 */

原型的作用是让编译器在调用点做类型检查。实参个数不对、类型无法隐式转换,编译期就能报出来。如果没有原型,编译器只能按默认实参提升规则推测,错误就留到运行期。


这里有一个经典区别:void f(void)和void f()在声明位置的含义不同。前者明确表示不接受参数;后者是旧式声明,表示「参数未知」,编译器不会对调用的实参做检查。只有在定义位置,空括号才等价于无参数。所以头文件里一律写(void),不要留空。


声明可以出现多次,只要彼此一致。同一翻译单元里重复写同一个原型是合法的;类型不一致则违反约束。声明通常放在头文件里,被多个源文件包含,这样所有调用方看到的接口是同一份。


函数指针的声明需要括号:


/* 适用于 C17 */
int (*compare)(const void *, const void *); /* 指向函数的指针 */
int *(*factory)(void); /* 返回 int* 的函数指针 */

int *compare(...)是返回指针的函数,int (*compare)(...)是指向函数的指针,两者差别只在一对括号,含义完全不同。读声明时可以按「右左法则」:从名字出发,先看右边,再看左边。


二、定义:提供函数体,全程序唯一


函数定义(function definition)包含函数体,是编译器真正生成代码的地方。一个函数在整个程序中只能有一个外部定义(inline 的情况稍后再谈)。多个源文件各自写一份同名函数的完整定义,链接阶段会报重复定义(multiple definition)。


定义与声明的区别可以用一句话概括:声明可以有很多份,定义只能有一份。定义本身也是声明,它同时提供了接口信息和实现。


链接属性由static和extern控制:




写法链接属性可见范围典型用途



int f(void) { ... }外部链接整个程序对外提供的接口

static int f(void) { ... }内部链接仅当前翻译单元文件私有的辅助函数

extern int f(void);外部链接当前翻译单元可见在头文件或源文件里引用别处的定义

inline(C99 语义)特殊见下文头文件中的小函数



static用在函数上表示内部链接,别的翻译单元链接不到它。这是隐藏实现细节的常用手段:辅助函数一律加static,既避免命名冲突,也给编译器更多内联机会。


C99 的inline语义和 C++ 不同,容易搞错。在 C 里,一个inline定义本身不提供外部定义;如果所有使用处都被内联,链接器不需要实体;一旦有调用没有被内联,就需要在某一个源文件里提供外部定义。常见做法是在头文件里写static inline(每个翻译单元各自一份内部链接的副本),简单且不易出错;或者在头文件里放inline定义,并在一个.c文件里写extern声明来生成实体。两种做法各有取舍,初学阶段用static inline最省心。


还有一类容易忽略的定义:文件作用域的int counter;是暂定定义(tentative definition)。同一翻译单元里可以出现多次,编译器把它们合并成一个定义并零初始化;如果整个翻译单元都没有带初始化的定义,最后就按一个零初始化的定义处理。这也是为什么头文件里写变量定义会导致多个源文件各自拥有一份、链接时冲突——正确做法是头文件里写extern,在一个源文件里定义。


三、使用:头文件负责声明,源文件负责实现


多文件项目的常规组织是:



  • 头文件(.h)放函数原型、类型定义、宏、extern变量声明,并用包含卫士(include guard)防止重复包含。

  • 源文件(.c)包含对应的头文件,写出函数定义。

  • 调用方包含头文件,按原型调用。


包含卫士有两种写法,#ifndef宏卫士可移植性最好,#pragma once被主流编译器支持但不属于 C 标准:


/* 适用于 C17 */
#ifndef MATHUTIL_H
#define MATHUTIL_H

#include <stddef.h> /* size_t */

int add(int a, int b);
double average(const int *data, size_t count);

#endif /* MATHUTIL_H */

源文件包含自己的头文件有一个额外好处:让编译器核对定义与声明是否一致。如果定义写错了参数类型,包含头文件后立刻就能发现,而不是等到调用方链接时才出现难以理解的错误。


调用时的规则很简单:实参个数与类型要能匹配形参,返回值可以忽略(除非函数声明了[[nodiscard]],那是 C23 的属性,C17 里没有)。数组作为参数会退化为指向首元素的指针,所以函数内部拿不到数组长度,必须额外传一个长度参数——这是初学者最常见的困惑之一。


main函数的标准形式只有两种:int main(void)和int main(int argc, char *argv[])。void main()从来不是标准写法,虽然某些编译器容忍它。main的返回值会作为进程退出码传给操作系统,省略return时 C99 起等价于return 0。


四、完整示例:三个文件的最小工程


下面的例子把接口和实现分开。头文件只放声明,mathutil.c放定义,main.c负责调用。


/* mathutil.h —— 适用于 C17 */
#ifndef MATHUTIL_H
#define MATHUTIL_H

#include <stddef.h>

int add(int a, int b);
double average(const int *data, size_t count);
void print_banner(void);

#endif /* MATHUTIL_H */

/* mathutil.c —— 适用于 C17 */
#include "mathutil.h"

#include <stdio.h>

/* 外部链接:其他翻译单元可以调用 */
int add(int a, int b) {
return a + b;
}

double average(const int *data, size_t count) {
if (data == NULL || count == 0) {
return 0.0;
}
long sum = 0;
for (size_t i = 0; i < count; ++i) {
sum += data[i];
}
return (double)sum / (double)count;
}

void print_banner(void) {
puts("mathutil demo");
}

/* 内部链接:只在本文件可见 */
static int clamp_to_zero(int value) {
return value < 0 ? 0 : value;
}

/* main.c —— 适用于 C17 */
#include "mathutil.h"

#include <stdio.h>

int main(void) {
int values[] = {3, 1, 4, 1, 5, 9, 2, 6};
size_t count = sizeof(values) / sizeof(values[0]);

print_banner();
printf("add(2, 3) = %d\n", add(2, 3));
printf("average = %.3f\n", average(values, count));

/* 传数组名等价于传首元素地址,长度必须另传 */
printf("first = %d\n", values[0]);
return 0;
}

构建时把两个.c一起交给编译器,例如cc -std=c17 -Wall -Wextra main.c mathutil.c -o demo。这里只给出命令形式,不声称任何执行结果。


clamp_to_zero加了static,即使别的文件里也有同名函数也不会冲突。average用long累加而不是int,是为了降低求和溢出的风险;即便如此,元素数量极大时仍可能溢出,工程上要按数据范围选择更宽的累加类型。sizeof(values) / sizeof(values[0])只在数组仍以数组类型存在的作用域里有效,传进函数后数组已经退化为指针,这个比值会变成指针大小除以元素大小,结果没有意义。


常见坑点


1. 调用处没有可见的声明


❌ 在main里直接调用后面才定义的函数,依赖旧编译器的隐式声明。


✅ 在头文件里写原型,调用方包含头文件;或者在同一文件里把定义或原型放在调用之前。


2.void f()当成无参数


❌ 头文件里写void f();,调用f(1, 2)编译器不报错。


✅ 明确写void f(void);,让编译器检查实参。


3. 在头文件里写函数定义(非static inline)


❌int add(int a, int b) { return a + b; }放在.h里,被两个.c包含后链接报重复定义。


✅ 头文件只放原型;需要头文件内实现时用static inline,或者按 C99 的inline规则在一个源文件里提供外部定义。


4. 在头文件里定义变量


❌int g_count = 0;放在头文件里,每个包含它的源文件都有一份。


✅ 头文件写extern int g_count;,在一个.c文件里写int g_count = 0;。


5. 数组参数里用sizeof求长度


❌void f(int arr[]) { size_t n = sizeof(arr) / sizeof(arr[0]); },得到的是指针大小之比。


✅ 增加一个size_t count参数,由调用方传入真实长度。


6. 返回局部数组的指针


❌char *make(void) { char buf[16]; return buf; },函数返回后buf已销毁。


✅ 由调用方提供缓冲区,或使用malloc分配并在文档中写明由谁释放。


7. 忘记返回值的路径


❌ 非void函数在某个分支没有return,调用方读到不确定的值。


✅ 打开-Wall -Wextra,保证每条路径都有返回值;main除外,C99 起省略等价于返回 0。


8. 用void main()


❌void main()作为入口,返回值类型不符合标准。


✅ 写int main(void)或int main(int argc, char *argv[]),并返回 0 或EXIT_SUCCESS。


总结




概念作用出现次数放在哪里



声明(原型)描述返回类型与参数,供编译器检查调用可多次头文件

定义提供函数体,生成代码全程序一次源文件

调用按原型传参并接收返回值任意次调用方源文件

static函数内部链接,仅本翻译单元可见每单元一次源文件

extern变量声明引用别处的定义可多次头文件

static inline头文件内实现,每单元一份副本每单元一次头文件



把「声明是接口、定义是实现、头文件管接口、源文件管实现」这条线立住,多文件工程就不会乱。真正需要记牢的细节只有三个:(void)不能省、数组参数会退化、头文件里不要放非 inline 的定义。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 16:01:53

knowledge-work-plugins:构建从输入到输出的知识工作插件体系

开始整理 knowledge-work-plugins 这套插件体系&#xff0c;其实是被一次尴尬的项目汇报逼出来的。当时我在给一个跨部门方案找支撑材料&#xff0c;浏览器里开着二十多个标签页&#xff0c;光看过的行业报告就有七八份&#xff0c;可等到真正要写引用出处时&#xff0c;我只记…

作者头像 李华
网站建设 2026/10/21 16:01:31

推客管理系统如何解决佣金结算与业绩归属难题?

上个月见了一个做美妆私域的朋友&#xff0c;他手里已经有四百多个推客。听起来是个不错的盘子&#xff0c;但他的原话是&#xff1a;"我现在每天一睁眼&#xff0c;就是处理推客之间因为佣金对不上、业绩归属不清在群里吵架。财务那边做结算的小姑娘已经提了两次离职。&q…

作者头像 李华
网站建设 2026/10/21 16:01:00

WebLogic反序列化漏洞CVE-2019-2725解析与加固

1. 漏洞背景与影响范围 1.1 这个漏洞为什么值得拿出来单讲 先说一个判断&#xff1a;CVE-2019-2725 是整个 WebLogic 反序列化漏洞家族里非常特殊的一个&#xff0c;它不是简单的"补丁漏了"&#xff0c;而是 Oracle 在修复上一个漏洞时留下的"绕过"产物。…

作者头像 李华
网站建设 2026/10/21 16:00:54

构建可信的房地产数据分析工作台:REA数据工程与模型复盘

最近我终于把 REA 项目的第一个可运行版本跑通了。REA 全称 Real Estate Analytics&#xff0c;一个我私底下倒腾了好几个月的房地产数据分析工具。它做的事听起来很简单&#xff1a;把公开的房产挂牌数据拉下来&#xff0c;清洗、对齐、拼起来&#xff0c;然后做价格趋势分析和…

作者头像 李华
网站建设 2026/10/21 15:58:01

Qt QSS样式表实战合集:62个开箱即用的控件美化方案

简介&#xff1a;本资源是面向Qt桌面应用开发者的一站式QSS&#xff08;Qt Style Sheets&#xff09;样式设计学习包&#xff0c;专为希望快速掌握界面美化、统一视觉风格及动态换肤能力的中初级Qt程序员打造。压缩包共1274个文件&#xff0c;涵盖1096张UI组件截图&#xff08;…

作者头像 李华
网站建设 2026/10/21 15:57:41

TCP三次握手与四次挥手:从抓包分析到参数调优的实战指南

1. 为什么面试官总爱问三次握手和四次挥手如果你面过网络相关的岗位&#xff0c;不管是后端开发、运维还是测试&#xff0c;大概率都被问过这个问题。很多人背得滚瓜烂熟&#xff0c;但真到了线上抓包排查问题的时候&#xff0c;却连一个SYN丢包都看不出来。我自己刚入行那会儿…

作者头像 李华