鱼C论坛

 找回密码
 立即注册
查看: 223|回复: 3

[技术交流] 多文件编程

[复制链接]
回帖奖励 150 鱼币 回复本帖可获得 50 鱼币奖励! 每人限 1 次
发表于 2025-5-15 10:41:09 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 沉默的人e 于 2025-5-28 10:19 编辑
多文件编程

各位, 很久没写东西了. 刚刚发现, Clion 最近支持了非商业免费使用. 于是想发帖广而告之, 但是料想 Clion 并非 Pycharm, 虽然归为同一公司, 但毕竟服务语言不同, Pycharm 服务于 python 直接解释就可以. 但是 Clion 服务于 C/C++ 恐怕不能直接编译, 于是便有了使用 CMake 管理 C/C++ 项目. 搜索了一圈论坛, 并未有关于 CMake 之文. 遂写此贴以期供大家快速上手 CLion 软件.

本文的第一部分, 将会详细讲述预备知识, 即多文件编程相关知识. 使用语言以 C 语言为主. 倘若各位对此有所了解, 知道这个命令是干什么的, 为什么要同时编译 a.c 和 b.c, 跳过本文, 直接看 CMake 介绍篇.



                               
登录/注册后可看大图

I. 背景

先从我们认识的代码开始, 以下代码的工作内容是计算两个整数相加的结果.

  1. #include <stdio.h>

  2. // AddInt函数实现
  3. int AddInt(int a, int b) {
  4.     return a + b;
  5. }

  6. // 主函数
  7. int main() {
  8.     int a = 5;
  9.     int b = 3;
  10.     int c = AddInt(a, b);
  11.    
  12.     printf("The sum of %d and %d is %d\n", a, b, c);
  13.     return 0;
  14. }
复制代码

感觉还可以, 但是把一个不是很重要的东西放在前面, 有些喧宾夺主. 所以我们选择调整顺序:


  1. #include <stdio.h>

  2. // AddInt函数声明
  3. int AddInt(int a, int b);
  4. // 主函数
  5. int main() {
  6.      int a = 5;
  7.      int b = 3;
  8.      int c = AddInt(a, b);
  9.    
  10.      printf("The sum of %d and %d is %d\n", a, b, c);
  11.      return 0;
  12. }

  13. // AddInt 函数实现
  14. int AddInt(int a, int b) {
  15.      return a + b;
  16. }
复制代码

可以了, 对于目前这个只完成一两个小工作的小项目, 如此编写程序完全ok. 下面假设一个场景, 各位要编写的 c 语言程序需要自定义8个函数用来进行运算, 简化为 fa ~ fh 每个函数的工作大致相同, 是分别针对浮点数的四则运算和针对整数的四则运算功能. 现在按照上述方法, 把这些函数裂开, 声明放到开头, 定义放到末尾:


  1. #include <stdio.h>

  2. float fa(float a, float b);
  3. float fb(float a, float b);
  4. float fc(float a, float b);
  5. float fd(float a, float b);
  6. int fe(int a, int b);
  7. int ff(int a, int b);
  8. int fg(int a, int b);
  9. int fh(int a, int b);

  10. int main() {
  11.     printf("浮点运算:\n");
  12.     printf("5.5 + 3.3 = %.2f\n", fa(5.5f, 3.3f));
  13.     printf("5.5 - 3.3 = %.2f\n", fb(5.5f, 3.3f));
  14.     printf("5.5 * 3.3 = %.2f\n", fc(5.5f, 3.3f));
  15.     printf("5.5 / 3.3 = %.2f\n", fd(5.5f, 3.3f));
  16.   
  17.     printf("\n整数运算:\n");
  18.     printf("5 + 3 = %d\n", fe(5, 3));
  19.     printf("5 - 3 = %d\n", ff(5, 3));
  20.     printf("5 * 3 = %d\n", fg(5, 3));
  21.     printf("5 / 3 = %d\n", fh(5, 3));
  22.   
  23.     return 0;
  24. }

  25. float fa(float a, float b) {
  26.     return a + b;
  27. }

  28. float fb(float a, float b) {
  29.     return a - b;
  30. }

  31. float fc(float a, float b) {
  32.     return a * b;
  33. }

  34. float fd(float a, float b) {
  35.     return a / b;
  36. }

  37. int fe(int a, int b) {
  38.     return a + b;
  39. }

  40. int ff(int a, int b) {
  41.     return a - b;
  42. }

  43. int fg(int a, int b) {
  44.     return a * b;
  45. }

  46. int fh(int a, int b) {
  47.     return a / b;
  48. }

复制代码

水温尚可, 但是进一步考虑, 如果再加上其他功能, 让自定义函数数量达到20甚至30个时, 有点吃不消了. 这就是使用多文件编程的原因之一. 另一个原因也很简单, 如果想要完成某个项目, 使用单文件编程会降低工作效率. 无法保证你的工作会被人影响. 所以需要多文件编程, 一是便于项目的工作分配, 而是让项目结构更加符合逻辑. 下面正式介绍多文件编程.

II. 头文件 源代码文件

对于一个函数, 就用上例中的 fh 来说:

  1. int fh(int a, int b) {
  2.      return a / b;
  3. }
复制代码

通常将 返回值 函数名(参数列表)  部分成为函数头, 函数头末尾加了分号, 这个就成为了语句, 叫函数声明语句.

这些保存了函数声明, 结构体定义这些通常在变异过程中只要重复出现就会报错的东西等代码的文件, 被称为头文件, 毕竟大多数情况在项目中, 函数要比结构体的数量要多, 遂用函数头的"头"来代替这个文件的称呼. 头文件通常用 ".h" 或者 ".hpp" (多用于 c++) 作为后缀. 取 header file 首个单词 header 的首字母 h. 比如 stdio.h 和 stdlib.h 等, 都属于头文件.

而那些正式定义函数的代码, 也会组成另一个文件, 通常文件名上会与对应的头文件相同, 然后后缀是 .c 或 .cpp.


不是所有的头文件都一定要有对应的源代码文件, 不是所有的源代码文件都有对应的头文件, 比如main函数, 很少有人会写main 函数的函数头. main函数也就自然没有必要专门写一个头文件用来一一对应.

这样组织代码, 以那10个函数为例, 一个文件就会分裂成3个文件: 存放函数声明的 calc.h 文件, 存放函数定义的 calc.c 文件, 以及调用这些函数的主文件 main.c (个人习惯如此命名, 也有用这个项目的功能命名的, 比如叫 calculator.c 但这样会导致命名混淆, 个人不推荐).


1. main.c
  1. # include <stdio.h>
  2. int main() {
  3.     printf("浮点运算:\n");
  4.     printf("5.5 + 3.3 = %.2f\n", fa(5.5f, 3.3f));
  5.     printf("5.5 - 3.3 = %.2f\n", fb(5.5f, 3.3f));
  6.     printf("5.5 * 3.3 = %.2f\n", fc(5.5f, 3.3f));
  7.     printf("5.5 / 3.3 = %.2f\n", fd(5.5f, 3.3f));
  8.   
  9.     printf("\n整数运算:\n");
  10.     printf("5 + 3 = %d\n", fe(5, 3));
  11.     printf("5 - 3 = %d\n", ff(5, 3));
  12.     printf("5 * 3 = %d\n", fg(5, 3));
  13.     printf("5 / 3 = %d\n", fh(5, 3));
  14.   
  15.     return 0;
  16. }
复制代码
2. calc.h
  1. float fa(float a, float b);
  2. float fb(float a, float b);
  3. float fc(float a, float b);
  4. float fd(float a, float b);
  5. int fe(int a, int b);
  6. int ff(int a, int b);
  7. int fg(int a, int b);
  8. int fh(int a, int b);
复制代码
3. calc.c
  1. float fa(float a, float b) {
  2.     return a + b;
  3. }

  4. float fb(float a, float b) {
  5.     return a - b;
  6. }

  7. float fc(float a, float b) {
  8.     return a * b;
  9. }

  10. float fd(float a, float b) {
  11.     return a / b;
  12. }

  13. int fe(int a, int b) {
  14.     return a + b;
  15. }

  16. int ff(int a, int b) {
  17.     return a - b;
  18. }

  19. int fg(int a, int b) {
  20.     return a * b;
  21. }

  22. int fh(int a, int b) {
  23.     return a / b;
  24. }
复制代码

III 编译 预编译 include

接下来的工作, 就是如何编译文件:

首先, gcc 支持多文件编译, 可以使用 gcc a.c b.c main.c 的方式进行编译, 但是编译成果是一个文件, 如果你尝试编译多个含有 main 函数的文件, 想要得到多个二进制的可运行文件, 会报错.

其次, 从笔者学习 Qt 编译的教训来看, 推荐把头文件也带着编译进去, 对于本项目, 不编译头文件也可以. 但是对于某些项目, 不编译头文件会报莫名其妙的错误(对, 就是那个Qt).

将上述编写的文件内容放入gcc中编译,很开心的, 报了个错:

error2.png


                               
登录/注册后可看大图

尽管我们把 .h 文件放进去编译了, 程序依然不认识, 因为在程序中, 没有将 .h 文件和其他文件关联起来.

所以需要使用 include 操作, 将 .h 文件引入到对应的源文件和使用其中函数的程序文件中.

从网上了解, include 操作效果等价于将被引入的文件原封不动copy到当前行中. include 操作的文件对象有两种, 一种是编译器认识的几个文件地址(这个地址可以人为扩充), 从这些地方找到的文件,引入文件名通常用一对尖括号<>包裹; 另外一种, 就是在当前文件夹内的其他文件, 如果没有找到目标, 则会从前者所说的地址中寻找, 这种文件引入时通常用一对双引号""包裹. include 操作并非编译操作, 而是在编译之前就要完成的操作. 所以他不是编译时行为, 而属于预编译行为. 预编译行为的符号各位都见过, 就是 # 符号. 这就解释了如果你在程序中不写 # include <stdio.h> 你连 printf("Hello World!\n"); 都不能运行的原因.

在两个 .c 的源代码文件中的开头, 添加如下代码, 应该就可以解决这个报错的问题了.

  1. # include "calc.h"
复制代码
success.png
IV 惯例化

这个程序到目前为止, 明面上, 没有出现任何错误了, 编译器也是这么说的. 但是, 当我们添加一小段代码时, 问题又出现了.

我们通常会把常量也放到头文件中, 因为这种东西, 一般情况都是全项目通用的. 在 C++ 中, 通常使用 const 关键字, 使用常变量的方式定义常量. 当我们将这种方式应用到本文中的例子时, 编译就会报错:

error.png

所以对于 C 语言, 老老实实的用 # define 的方法进行常量定义. 不要考虑使用常变量.

视角转到 C++, 我用同样的方法写了一套代码, 分别用 calc.h calc.cpp main.cpp 来记录, 编译仍然报错, 但是使用常变量的方法是C++的官方推荐. 这又当做和解?

此处介绍两个操作, 用作条件编译行为的预编译操作, 分别是 ifndef if endif 还有一个常见的 define.

其中 ifndef xxx 是 if !defined(xxx) 的简化, 二者等效, 可能部分编译器不认识 ifndef 操作, 此处留心一下.

他们在头文件中的组合方式如下:

  1. # ifndef CALC_H
  2. # define CALC_H

  3. // your code

  4. # endif
复制代码

条件编译的这几个语句都比较简单移动, 如果没定义这个常量, 就定义一下, 然后写代码, 第二次被 include 时候, 由于已经被定义了, 其中的// yourc code  部分就被忽略了, 达成条件编译效果. 不要思考 CALC_H 的值是什么, 没有意义的, 如果想要深究, 在c++ 里面使用 cout 对象输出一下就知道了.

这种操作是所有写多文件编程的人都认可的方案, 所以我称其惯例化. 对于C语言, 我们也这么写, 尽管它不像c++一样能解决变量重复定义的问题, 为了避免其他问题, 至少为了效率提升, 在C语言的头文件中加入 ifndef 的行为也是很有必要的.

现代编译器可能支持一种新的避免重复编译的操作, 叫 # pragma once .它的优点就是避免 ifndef 中常量重名的可能. 但这种操作并没有完全普及, 没有写入到标准中, 所以现在不能替代 ifndef 操作. 某些公司(比如华为)就在规范化文档里面要求禁止使用 pragma once 行为.



本文向大家介绍了多文件编程的背景意义, 函数切分方法, 头文件的惯例书写格式等. 在项目化中, 这些行为会避免很多麻烦. 下一次将介绍 cmake 使用方法. 若本文有任何存在的错误, 还请评论区批评指正, 如果看到, 笔者会及时更改.

本文回帖, 前5位有效, 赠送鱼币50

感谢各位支持.










评分

参与人数 1荣誉 +1 鱼币 +1 收起 理由
player-none + 1 + 1

查看全部评分

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2025-6-1 15:46:18 From FishC Mobile | 显示全部楼层

回帖奖励 +50 鱼币

虽然不喜欢c++但也来支持下(手机版不支持表情)【\(O-O)/】
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-6-1 18:23:58 | 显示全部楼层

回帖奖励 +50 鱼币

对初学者很有意义
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-6-4 20:24:14 | 显示全部楼层
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-6-26 01:08

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表