关于头文件与库函数的一些问题

diaolingle 2012-10-13 07:47:40
小弟最近在恶补c,看到库函数与头文件这一段;有一些问题不解:
我们平时引入#include<...>这样一些头文件时,头文件里是一些函数原型的声明和宏之类的东西(没细看),然后我想问的是头文件怎么通过这些来调用到库函数,还有库函数存放在什么地方呢?在哪里可以看到库函数的代码?
然后就是我自己写一个头文件,我只想放一些函数原型声明进去,然后再写一些函数实现它们。之后我在写程序的时候,想引用我自己写的头文件几之前的函数,头文件可以用#include"..."引入,但是我自己写的函数头文件怎么自己找到,是要在头文件中设置一些入口信息吗,还是我自己想办法把之前的函数文件和现在写的程序链接起来,又要怎么样链接呢?在vc6.0之下怎么样链接。
...全文
310 7 打赏 收藏 转发到动态 举报
写回复
用AI写文章
7 条回复
切换为时间正序
请发表友善的回复…
发表回复
AnYidan 2012-10-14
  • 打赏
  • 举报
回复
[Quote=引用 5 楼 的回复:]
不过你说的comlier是在哪里的呢?

引用 2 楼 的回复:
标准的库函数是集成在 compiler 中的,你不用操心
自己写的要函数文件要添加到工程中
[/Quote]
不同的系统不台一样,在compiler 安装路径下找
夏天__ 2012-10-13
  • 打赏
  • 举报
回复
找本书,好好研究研究

参见:Linux C编程一站式学习


2.2. 头文件

我们继续前面关于stack.c和main.c的讨论。stack.c这个模块封装了top和stack两个变量,导出了push、pop、is_empty三个函数接口,已经设计得比较完善了。但是使用这个模块的每个程序文件都要写三个函数声明也是很麻烦的,假设又有一个foo.c也使用这个模块,main.c和foo.c中各自要写三个函数声明。重复的代码总是应该尽量避免的,以前我们通过各种办法把重复的代码提取出来,比如在第 2 节 “数组应用实例:统计随机数”讲过用宏定义避免硬编码的问题,这次有什么办法呢?答案就是可以自己写一个头文件stack.h:

/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif
这样在main.c中只需包含这个头文件就可以了,而不需要写三个函数声明:

/* main.c */
#include <stdio.h>
#include "stack.h"

int main(void)
{
push('a');
push('b');
push('c');

while(!is_empty())
putchar(pop());
putchar('\n');

return 0;
}
首先说为什么#include <stdio.h>用角括号,而#include "stack.h"用引号。对于用角括号包含的头文件,gcc首先查找-I选项指定的目录,然后查找系统的头文件目录(通常是/usr/include,在我的系统上还包括/usr/lib/gcc/i486-linux-gnu/4.3.2/include);而对于用引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。

假如三个代码文件都放在当前目录下:

$ tree
.
|-- main.c
|-- stack.c
`-- stack.h

0 directories, 3 files
则可以用gcc -c main.c编译,gcc会自动在main.c所在的目录中找到stack.h。假如把stack.h移到一个子目录下:

$ tree
.
|-- main.c
`-- stack
|-- stack.c
`-- stack.h

1 directory, 3 files
则需要用gcc -c main.c -Istack编译。用-I选项告诉gcc头文件要到子目录stack里找。

在#include预处理指示中可以使用相对路径,例如把上面的代码改成#include "stack/stack.h",那么编译时就不需要加-Istack选项了,因为gcc会自动在main.c所在的目录中查找,而头文件相对于main.c所在目录的相对路径正是stack/stack.h。

在stack.h中我们又看到两个新的预处理指示#ifndef STACK_H和#endif,意思是说,如果STACK_H这个宏没有定义过,那么从#ifndef到#endif之间的代码就包含在预处理的输出结果中,否则这一段代码就不出现在预处理的输出结果中。stack.h这个头文件的内容整个被#ifndef和#endif括起来了,如果在包含这个头文件时STACK_H这个宏已经定义过了,则相当于这个头文件里什么都没有,包含了一个空文件。这有什么用呢?假如main.c包含了两次stack.h:

...
#include "stack.h"
#include "stack.h"

int main(void)
{
...
则第一次包含stack.h时并没有定义STACK_H这个宏,因此头文件的内容包含在预处理的输出结果中:

...
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#include "stack.h"

int main(void)
{
...
其中已经定义了STACK_H这个宏,因此第二次再包含stack.h就相当于包含了一个空文件,这就避免了头文件的内容被重复包含。这种保护头文件的写法称为Header Guard,以后我们每写一个头文件都要加上Header Guard,宏定义名就用头文件名的大写形式,这是规范的做法。

那为什么需要防止重复包含呢?谁会把一个头文件包含两次呢?像上面那么明显的错误没人会犯,但有时候重复包含的错误并不是那么明显的。比如:

#include "stack.h"
#include "foo.h"
然而foo.h里又包含了bar.h,bar.h里又包含了stack.h。在规模较大的项目中头文件包含头文件的情况很常见,经常会包含四五层,这时候重复包含的问题就很难发现了。比如在我的系统头文件目录/usr/include中,errno.h包含了bits/errno.h,后者又包含了linux/errno.h,后者又包含了asm/errno.h,后者又包含了asm-generic/errno.h。

另外一个问题是,就算我是重复包含了头文件,那有什么危害么?像上面的三个函数声明,在程序中声明两次也没有问题,对于具有External Linkage的函数,声明任意多次也都代表同一个函数。重复包含头文件有以下问题:

一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。

二是如果有foo.h包含bar.h,bar.h又包含foo.h的情况,预处理器就陷入死循环了(其实编译器都会规定一个包含层数的上限)。

三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。

还有一个问题,既然要#include头文件,那我不如直接在main.c中#include "stack.c"得了。这样把stack.c和main.c合并为同一个程序文件,相当于又回到最初的例 12.1 “用堆栈实现倒序打印”了。当然这样也能编译通过,但是在一个规模较大的项目中不能这么做,假如又有一个foo.c也要使用stack.c这个模块怎么办呢?如果在foo.c里面也#include "stack.c",就相当于push、pop、is_empty这三个函数在main.c和foo.c中都有定义,那么main.c和foo.c就不能链接在一起了。如果采用包含头文件的办法,那么这三个函数只在stack.c中定义了一次,最后可以把main.c、stack.c、foo.c链接在一起。如下图所示:

图 20.2. 为什么要包含头文件而不是.c文件



同样道理,头文件中的变量和函数声明一定不能是定义。如果头文件中出现变量或函数定义,这个头文件又被多个.c文件包含,那么这些.c文件就不能链接在一起了。

2.3. 定义和声明的详细规则

以上两节关于定义和声明只介绍了最基本的规则,在写代码时掌握这些基本规则就够用了,但其实C语言关于定义和声明还有很多复杂的规则,在分析错误原因或者维护规模较大的项目时需要了解这些规则。本节的两个表格出自[Standard C]。

首先看关于函数声明的规则。

表 20.1. Storage Class关键字对函数声明的作用

Storage Class File Scope Declaration Block Scope Declaration
none
previous linkage
can define

previous linkage
cannot define

extern
previous linkage
can define

previous linkage
cannot define

static
internal linkage
can define

N/A

以前我们说“extern关键字表示这个标识符具有External Linkage”其实是不准确的,准确地说应该是Previous Linkage。Previous Linkage的定义是:这次声明的标识符具有什么样的Linkage取决于前一次声明,这前一次声明具有相同的标识符名,而且必须是文件作用域的声明,如果在程序文件中找不到前一次声明(这次声明是第一次声明),那么这个标识符具有External Linkage。例如在一个程序文件中在文件作用域两次声明同一个函数:

static int f(void); /* internal linkage */
extern int f(void); /* previous linkage */
则这里的extern修饰的标识符具有Interanl Linkage而不是External Linkage。从上表的前两行可以总结出我们先前所说的规则“函数声明加不加extern关键字都一样”。上表也说明了在文件作用域允许定义函数,在块作用域不允许定义函数,或者说函数定义不能嵌套。另外,在块作用域中不允许用static关键字声明函数。

关于变量声明的规则要复杂一些:

表 20.2. Storage Class关键字对变量声明的作用

Storage Class File Scope Declaration Block Scope Declaration
none
external linkage
static duration
static initializer
tentative definition

no linkage
automatic duration
dynamic initializer
definition

extern
previous linkage
static duration
no initializer

  • not a definition

    previous linkage
    static duration
    no initializer
    not a definition

    static
    internal linkage
    static duration
    static initializer
    tentative definition

    no linkage
    static duration
    static initializer
    definition


    上表的每个单元格里分成四行,分别描述变量的链接属性、生存期,以及这种变量如何初始化,是否算变量定义。链接属性有External Linkage、Internal Linkage、No Linkage和Previous Linkage四种情况,生存期有Static Duration和Automatic Duration两种情况,请参考本章和上一章的定义。初始化有Static Initializer和Dynamic Initializer两种情况,前者表示Initializer中只能使用常量表达式,表达式的值必须在编译时就能确定,后者表示Initializer中可以使用任意的右值表达式,表达式的值可以在运行时计算。是否算变量定义有三种情况,Definition(算变量定义)、Not a Definition(不算变量定义)和Tentative Definition(暂定的变量定义)。什么叫“暂定的变量定义”呢?一个变量声明具有文件作用域,没有Storage Class关键字修饰,或者用static关键字修饰,那么如果它有Initializer则编译器认为它就是一个变量定义,如果它没有Initializer则编译器暂定它是变量定义,如果程序文件中有这个变量的明确定义就用明确定义,如果程序文件没有这个变量的明确定义,就用这个暂定的变量定义
  • [32],这种情况下变量以0初始化。在[C99]中有一个例子:

    int i1 = 1; // definition, external linkage
    static int i2 = 2; // definition, internal linkage
    extern int i3 = 3; // definition, external linkage
    int i4; // tentative definition, external linkage
    static int i5; // tentative definition, internal linkage
    int i1; // valid tentative definition, refers to previous
    int i2; // 6.2.2 renders undefined, linkage disagreement
    int i3; // valid tentative definition, refers to previous
    int i4; // valid tentative definition, refers to previous
    int i5; // 6.2.2 renders undefined, linkage disagreement
    extern int i1; // refers to previous, whose linkage is external
    extern int i2; // refers to previous, whose linkage is internal
    extern int i3; // refers to previous, whose linkage is external
    extern int i4; // refers to previous, whose linkage is external
    extern int i5; // refers to previous, whose linkage is internal
    变量i2和i5第一次声明为Internal Linkage,第二次又声明为External Linkage,这是不允许的,编译器会报错。注意上表中标有
  • 的单元格,对于文件作用域的extern变量声明,C99是允许带Initializer的,并且认为它是一个定义,但是gcc对于这种写法会报警告,为了兼容性应避免这种写法。


diaolingle 2012-10-13
  • 打赏
  • 举报
回复
不过你说的comlier是在哪里的呢?[Quote=引用 2 楼 的回复:]
标准的库函数是集成在 compiler 中的,你不用操心
自己写的要函数文件要添加到工程中
[/Quote]
diaolingle 2012-10-13
  • 打赏
  • 举报
回复
实际操作了一下,跟预想的一样。[Quote=引用 3 楼 的回复:]

你的意思是不是我自己写一个head文件后,然后写一个***.c的函数文件,就在当前工程下,然后我再在###.c中写一个程序来调用之前的函数,我只要引入我之前的头文件就可以吗,不用另加代码引入***.c文件编译器就会自动找到我之前写的函数?引用 2 楼 的回复:

标准的库函数是集成在 compiler 中的,你不用操心
自己写的要函数文件要添加到工程中
[/Quote]
diaolingle 2012-10-13
  • 打赏
  • 举报
回复
你的意思是不是我自己写一个head文件后,然后写一个***.c的函数文件,就在当前工程下,然后我再在###.c中写一个程序来调用之前的函数,我只要引入我之前的头文件就可以吗,不用另加代码引入***.c文件编译器就会自动找到我之前写的函数?[Quote=引用 2 楼 的回复:]

标准的库函数是集成在 compiler 中的,你不用操心
自己写的要函数文件要添加到工程中
[/Quote]
AnYidan 2012-10-13
  • 打赏
  • 举报
回复
标准的库函数是集成在 compiler 中的,你不用操心
自己写的要函数文件要添加到工程中
armsword 2012-10-13
  • 打赏
  • 举报
回复
http://topic.csdn.net/u/20121013/15/c5b48e7d-b8dc-4508-a0d7-1b225b330faa.html

69,369

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧