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

发布时间:2026/10/21 16:05:34

C语言中函数的声明、定义及使用的入门教程 前言初学 C 语言时「声明」「定义」「调用」这三个词经常被混着用。有人把函数头加一个分号就叫定义有人把头文件里的原型当成实现还有人写了函数却忘了在调用处之前声明靠编译器「猜」出一个返回int的版本。这些混淆在单个.c文件里往往能蒙混过关一旦拆成多个文件、或者换一个更严格的编译器就会变成编译错误或链接错误。需要先明确一件事C 语言从 C99 起就不再允许隐式函数声明调用一个此前没有声明过的函数属于约束违反编译器必须给出诊断C23 进一步收紧隐式声明和旧式KR函数定义都不再是标准的一部分。所以「不声明也能用」不是语言特性而是旧编译器留下的宽松行为。本文按「声明—定义—使用」的顺序讲清三者的区别声明描述接口定义提供实体使用则是调用方按接口发起调用。然后给出一个头文件加两个源文件的完整组织方式最后列出初学者最常踩的坑。一、声明只描述接口不产生实体函数声明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);外部链接当前翻译单元可见在头文件或源文件里引用别处的定义inlineC99 语义特殊见下文头文件中的小函数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.hint 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.hint 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 -stdc17 -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 的定义。
延伸阅读

更多相关文章

2026/10/21 16:05:34

知识工作插件:面向认知任务的轻量级自动化单元

1. 项目概述:这不是插件,而是一套知识工作者的“思维外挂”系统“knowledge-work-plugins”这个标题乍看像某个开源仓库名,但拆开来看——knowledge(知识)、work(工作)、plugins(插件…

2026/10/21 16:00:33

文件上传绕过实战:从日志包含到RCE的完整攻击链分析

1. 从"能上传"到"能利用":这道题真正考的是什么先说结论:这道CTF题作业2最坑人的地方,恰恰在于它看起来"太简单了"。很多人一看到文件上传,脑子里立刻蹦出三个字——"马"呢?然…

2026/10/21 17:26:38

uniapp微信商城模板全解析:从架构到支付二次开发

简介:基于 uni-app 开发的微信商城小程序通用模板,适合小程序入门者、前端学习者及需要快速搭建商城原型的学生或开发者。压缩包共 490 个文件,体积仅 2.04MB,以 275 个 JS 逻辑文件、117 个 Vue 页面组件为核心,辅以 …

2026/10/21 17:26:38

RK3588与AX650N边缘AI开发板YOLOv5s实测对比与踩坑记录

做边缘计算项目这五年,手头经手的AI开发板少说也有十几块了,但真正让我愿意专门写一篇测试报告的,还是瑞芯微RK3588和爱芯元智AX650N这对组合。这俩平台的定位高度重叠,都是ARM核心加NPU的SoC方案,目标场景基本就是边缘…

2026/10/21 17:26:38

基于Spring Boot和Vue的旅游网系统:毕业设计全栈开发实战

每年到了毕业设计选题的季节,总有一批计算机专业的同学在“做什么题目”这件事上反复纠结。管理系统太普通,电商平台太烂大街,纯算法又怕做不完。如果你也处在这个阶段,我的建议很直接:考虑一个基于Spring Boot加Vue的…

2026/10/21 17:26:38

贝叶斯优化+Transformer:Matlab单变量时序预测实战指南

做了快两年时序预测的工程落地,我最大的感受是:只要还在用“固定结构 手工调参”的思路,单变量时序预测就永远像开盲盒。最近把一个设备监测项目彻底重构了一遍,核心方案就是标题里写的 BO 贝叶斯优化 Bayes-Transformer&#x…

2026/10/21 17:26:38

定积分求旋转体体积:圆盘法与壳层法的本质与实操指南

定积分算旋转体体积,是我见过最容易被学生“背公式”背废的一个知识点。大学里讲到这里,很多人记住的是∫πf(x)dx这样几个固定套路,题目一换、轴一改、函数一复杂,立刻就不会了。但说实话,这块反而是微积分里最能培养…

2026/10/21 17:21:37

Token级策略梯度:生成式模型强化学习的逐步优化指南

1. 为什么Token级策略梯度会进入视野 我第一次做生成式模型的强化学习微调时,用的是最朴素的写法:模型把整条回复生成完,奖励模型给一个总分数,然后把这个分数乘到整条序列所有token的log概率梯度上,更新一次参数。刚开…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/21 0:00:35

交通流仿真实战:IDM跟驰与MOBIL换道模型参数调优与避坑指南

简介:这份资源是面向交通工程学习者与Python开发者的微观交通流仿真项目,核心实现智能驾驶模型(IDM)下的车辆跟驰与换道行为,可用于理解速度、密度与流量之间的动态关系,适合具备Python基础、希望深入交通模…

2026/10/21 0:00:35

揭穿智能体表面顺从:用追踪代码重建任务执行确定性

1. 表面顺从不是智能体的“礼貌”,而是任务执行链路上的隐性断点最近在调试一个跨平台长周期任务调度系统时,我反复遇到一种令人困惑的现象:智能体在用户指令发出后,几乎秒级响应“已收到”“正在处理”“请稍候”,UI上…

2026/10/21 0:00:35

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑