在 C 语言中,库是一个包含头文件的集合,供其他程序使用。一个库因此由一个接口(在 .h 文件中定义,称为“头文件”)和一个实现(在 .c 文件中定义)组成。这个 .c 文件可能是预编译的,或者可能是程序员可以访问的。(注意:库可能会调用其他库中的函数,比如标准 C 库或数学库来完成各种任务。)

库的格式随操作系统和使用的编译器不同而有所变化。例如,在 Unix 和 Linux 操作系统中,库由一个或多个目标文件组成,这些目标文件通常是编译器(如果源代码是 C 或类似语言)或汇编器(如果源代码是汇编语言)的输出。这些目标文件然后通过 ar 程序打包成库,形成一个归档文件(归档文件是一个将多个文件存储为一个文件的程序,通常不涉及压缩)。库的文件名通常以 "lib" 开头并以 ".a" 结尾;例如,libc.a 文件包含标准 C 库,libm.a 包含数学函数,链接器会将其链接到最终程序中。其他操作系统如 Microsoft Windows 使用 .lib 扩展名表示库,.obj 扩展名表示目标文件。在 Unix 环境中,一些程序如 lexyacc 会生成可以与 liblliby 库链接的 C 代码来创建可执行文件。

我们将使用一个包含一个函数的库作为例子:该函数用于解析命令行参数。命令行参数可能是独立的:

-i

也可能是附加的:

-ioptarg

或者是分开作为独立的 argv 元素:

-i optarg

这个库除了函数外,还导出了四个声明:三个整数和一个指向可选参数的指针。如果参数没有附加可选参数,指向可选参数的指针将为 null

为了能够解析这些类型的参数,我们编写了以下的 "getopt.c" 文件:

#include <stdio.h>              /* for fprintf() and EOF */
#include <string.h>             /* for strchr() */
#include "getopt.h"             /* consistency check */

/* variables */
int opterr = 1;                 /* getopt prints errors if this is on */
int optind = 1;                 /* token pointer */
int optopt;                     /* option character passed back to user */
char *optarg;                   /* flag argument (or value) */

/* function */
/* return option character, EOF if no more or ? if problem.
	The arguments to the function:
	argc, argv - the arguments to the main() function. An argument of "--"
	stops the processing.
	opts - a string containing the valid option characters.
	an option character followed by a colon (:) indicates that
	the option has a required argument.
*/
int
getopt (int argc, char **argv, char *opts)
{
	static int sp = 1;            /* character index into current token */
	register char *cp;            /* pointer into current token */
	
	if (sp == 1)
	{
		/* check for more flag-like tokens */
		if (optind >= argc || argv[optind][0] != '-' || argv[optind][1] == '\0')
			return EOF;
		else if (strcmp (argv[optind], "--") == 0)
		{
			optind++;
			return EOF;
		}
	}
	
	optopt = argv[optind][sp];
	
	if (optopt == ':' || (cp = strchr (opts, optopt)) == NULL)
	{
		if (opterr)
			fprintf (stderr, "%s: invalid option -- '%c'\n", argv[0], optopt);
		
		/* if no characters left in this token, move to next token */
		if (argv[optind][++sp] == '\0')
		{
			optind++;
			sp = 1;
		}
		
		return '?';
	}
	
	if (*++cp == ':')
	{
		/* if a value is expected, get it */
		if (argv[optind][sp + 1] != '\0')
			/* flag value is rest of current token */
			optarg = argv[optind++] + (sp + 1);
		else if (++optind >= argc)
		{
			if (opterr)
				fprintf (stderr, "%s: option requires an argument -- '%c'\n",
							argv[0], optopt);
			sp = 1;
			return '?';
		}
		else
	                /* flag value is next token */
		        optarg = argv[optind++];
		sp = 1;
	}
	else
	{
		/* set up to look at next char in token, next time */
		if (argv[optind][++sp] == '\0')
		{
			/* no more in current token, so setup next token */
			sp = 1;
			optind++;
		}
		optarg = 0;
	}
	return optopt;
} 
/* END OF FILE */

这个代码实现了一个命令行参数解析函数 getopt,可以处理多种不同类型的命令行选项和参数,返回相应的选项字符或错误代码。

接口文件将是如下的 "getopt.h" 文件:

#ifndef GETOPT_H
	#define GETOPT_H

	/* 导出的变量 */
	extern int opterr, optind, optopt;
	extern char *optarg;

	/* 导出的函数 */
	int getopt(int, char **, char *);
#endif

/* 结束文件 */

至少,程序员需要接口文件来弄清楚如何使用一个库,尽管通常情况下,库的开发者也会编写文档来指导如何使用库。在上面的例子中,文档应该指出,提供的参数 **argv 和 *opts 都不应为 null 指针(否则为什么要使用 getopt 函数呢?)。具体来说,文档通常会说明每个参数的作用,以及在什么条件下可以预期哪些返回值。使用库的程序员通常不关心库的实现 —— 除非实现存在错误,在这种情况下,他们可能会以某种方式提出投诉。

getopts 库的实现和使用该库的程序都应该声明 #include "getopt.h",以便引用相应的接口。现在,库就“链接”到了程序中 —— 即包含 main() 函数的程序。该程序可能会引用多个接口。

在某些情况下,单纯地放置 #include "getopt.h" 可能看起来是正确的,但仍然无法正确链接。这通常意味着库没有正确安装,或者可能需要进行额外的配置。你需要查看编译器或库的文档,了解如何解决这个问题。

头文件中应该包含什么

一般来说,头文件应该包含程序中其他模块“看到”的所有声明和宏定义(预处理器的 #define)。

可能的声明:

  • structunionenum 声明
  • typedef 声明
  • 外部函数声明
  • 全局变量声明

在上面的 getopt.h 示例文件中,声明了一个函数 (getopt) 和四个全局变量(optindoptoptoptargopterr)。这些变量在头文件中使用 extern 存储类说明符声明,因为该关键字指定了“实际的”变量存储在其他地方(即 getopt.c 文件中),而不是存储在头文件中。

#ifndef GETOPT_H/#define GETOPT_H 技巧通常被称为“包含保护”。它的作用是,如果 getopt.h 文件在一个翻译单元中被多次包含,编译器只会看到该文件的内容一次。另一种方式是在头文件中使用 #pragma once,在某些编译器中可以实现相同的效果(#pragma 是一种不跨平台的通用捕获语法)。

将库链接到可执行文件

将库链接到可执行文件的过程因操作系统和使用的编译器/链接器不同而有所不同。在 Unix 中,可以使用 -L 选项指定链接的目标文件目录,并使用 -l(小写字母 l)选项指定单个库。例如,-lm 选项指定链接 libm 数学库。

参考资料

  • C FAQ: "我想知道应该在 .c 文件中放什么,应该在 .h 文件中放什么。(.h 文件到底是什么意思?)"
  • PIClist 讨论串: "在包含许多 C 文件的项目中使用全局变量"
  • "如何使用 extern 在 C 中共享变量?"
最后修改: 2025年01月12日 星期日 13:29