我在尝试Exercise 8-3
K&R时遇到了困难,练习的目标是重写stdio.h
的一些函数,如fopen
、fclose
、fillbuf
和flushbuf
以下是我的源文件的组织方式:
stdio.h
:包含类型和宏定义,以及一些适用于库的函数的声明。 文件的所有内容都包含在#ifndef
#endif
行之间,如下所示:
#ifndef STDIO_H
#define STDIO_H
/* content of stdio.h */
#endif
myfunction.c
:我每个函数都有一个.c
文件,每个文件都有一个#include "stdio.h"
行来加载所有需要的类型定义。
main.c
:在我有代码来测试我的函数的地方,main.c
也有一行#include "stdio.h"
。
我的问题是:当我尝试使用gcc
编译所有文件时,我运行到错误:
multiple definition of `_iob'
在包含我的stdio.h
的每个函数文件中,(_iob
是我仅在stdio.h
中定义的变量)...为什么会这样?我认为#ifndef
行是专门防止此类错误。
更一般地说:
- 您将如何制作自己的头文件和库/函数文件并在项目中使用它们?
- 有没有办法让链接器通过包含头文件来找出我的函数的位置,就像标准函数一样?
请注意库与其头文件之间的区别。
库是二进制机器码的(集合)(带有一些额外的元数据,例如链接器的重定位指令)。
例如,在我的 Linux 系统上,动态库通常是共享对象(例如/usr/lib/x86_64-linux-gnu/libgmp.so
),尝试一些像#include "libgmp.so" //wrong
这样的预处理器指令是完全没有意义的。
但是库有一些 API。该 API 由一些文档和一些头文件提供,例如gmp.h
,你应该#include "gmp.h"
使用它的任何 C 代码(你的 C 翻译单元)。
myfunction.c:我每个函数都有一个 .c 文件
每个函数有一个文件通常很糟糕。通常可以对相关函数进行分组。例如,在您的情况下,您可能希望在同一个myopenclose.c
翻译单元中定义myfopen
和myfclose
函数(即使您不必这样做),因为这两个函数密切相关。根据经验,我更喜欢每个源文件有一行或几千行(但这确实是一个品味问题,有些人喜欢有很多小文件)。
请记住,编译器真正看到的是预处理形式的代码。考虑要求你的编译器生成该形式(例如,从foo.c
你可以通过gcc -C -E -Wall foo.c > foo.i
在我的 Linux 桌面上获得其预处理的表单foo.i
)并研究它。在您自己的文件上尝试这样做(例如,如果您有myopenclose.c
的话)。
如果您有许多小文件,编译器可能会在每个文件中包含相同的标头,并且每次都会编译这些包含的声明。顺便说一句,请注意gcc
只是一个驱动程序。将其与-v
标志一起使用。您将看到它正在运行cc1
(C 编译器本身)、as
(汇编器)、ld
(链接器)等。
我运行到错误:
"_iob"的多重定义
在包含我的 stdio.h 的每个函数文件中,(
_iob
是我只在 stdio.h 中定义的变量)。
您可能应该在stdio.h
中声明extern
_iob
全局变量,并仅在库的一个实现文件(如果相关,可能是myopenclose.c
)中定义全局_iob
。
不要混淆定义和声明(变量、函数、类型等)。花一些时间阅读 C11 标准 n1570。这些词在那里定义。根据经验,声明应该进入头.h
文件、实现中的定义(变量和函数).c
文件(当然细节要复杂得多,你经常但不总是在头文件中定义类型和struct
)。
我强烈建议使用一些Linux发行版(它对开发人员和学生非常友好)并研究一些现有的自由软件C标准库的源代码(如musl-libc,其代码非常可读)。更一般地说,研究现有自由软件项目的源代码(例如在github上)。他们会激励你。
有没有办法让链接器通过包含头文件来找出我的函数的位置,就像标准函数一样?
这显示了很多混乱(上面的问题没有任何意义)。阅读更多关于编译器(你的cc1
程序 - 由gcc
启动 - 正在将.c
文件转换为某个目标文件.o
)和链接器(你的ld
,通常由gcc
启动,正在聚合几个目标文件,处理其中的重定位,并生成ELF库或可执行文件)。预处理(例如#include
指令)在编译时由cc1
完成。链接器看不到任何头文件(它只处理对象文件或库)。
如果重写一些系统声明和函数,同时包括系统声明,则可能会发生一些冲突。
头文件(.h)包含代码(通常只有声明),你描述的机制(#ifndef STDIO_H
)是防止同一个头文件的多个包含 - 主要是因为另一个已经加载的包含文件(头)也可能包含它。这会导致与您相同的碰撞。
例如,在 C 中,您可以
-
创建一个包含您自己的声明的新头文件 + 不会与您的声明冲突的 STDIO 文件
使用 stdio 声明,并且只编写使用与 stdio 相同的结构、定义、枚举等的新函数
重写必要的声明和代码,允许您不再包含系统标头
使用其他命名约定,例如在头文件和代码中使用
my_iob
。
最后两个可能是您最好的,因为您仍然有一些来自头文件的冲突。
例如,您的代码可能不包含stdio.h
,但您包含的另一个头文件可能会间接地执行此操作...