3、C编程的各种源码⽂件
1、C语⾔模块化编程中的头⽂件
实际开发中⼀般是将函数和变量的声明放到头⽂件,再在当前源⽂件中 #include 进来。如果变量的值是固定的,最好使⽤宏来代替。
.c和.h⽂件都是源⽂件,除了后缀不⼀样便于区分外和管理外,其他的都是相同的,在.c中编写的代码同样也可以写在.h中,包括函数定义、变量定义、预处理等。
但是,.h 和 .c 在项⽬中承担的⾓⾊不⼀样:.c ⽂件主要负责实现,也就是定义函数和变量;.h ⽂件主要负责声明(包括变量声明和函数声明)、宏定义、类型定义等。这些不是C语法规定的内容,⽽是约定成俗的规范,或者说是长期形成的事实标准。
根据这份规范,头⽂件可以包含如下的内容:
可以声明函数,但不可以定义函数。
可以声明变量,但不可以定义变量。
可以定义宏,包括带参的宏和不带参的宏。
结构体的定义、⾃定义数据类型⼀般也放在头⽂件中。
在项⽬开发中,我们可以将⼀组相关的变量和函数定义在⼀个 .c ⽂件中,并⽤⼀个同名的 .h ⽂件(头⽂件)进⾏声明,其他模块如果需要使⽤某个变量或函数,那么引⼊这个头⽂件就可以。
这样做的另外⼀个好处是可以保护版权,我们在发布相关模块之前,可以将它们都编译成⽬标⽂件,或者打包成静态库,只要向⽤户提供头⽂件,⽤户就可以将这些模块链接到⾃⼰的程序中。
2、C语⾔标准库以及标准头⽂件
源⽂件通过编译可以⽣成⽬标⽂件(例如 GCC 下的 .o 和 Visual Studio 下的 .obj),并提供⼀个头⽂件向外暴露接⼝,除了保护版权,还可以将散乱的⽂件打包,便于发布和使⽤。
实际上我们⼀般不直接向⽤户提供⽬标⽂件,⽽是将多个相关的⽬标⽂件打包成⼀个静态链接库(Static Link Library),例如 Linux 下的 .a 和 Windows 下的 .lib。
打包静态库的过程很容易理解,就是将多个⽬标⽂件捆绑在⼀起形成⼀个新的⽂件,然后再加上⼀些索引,⽅便链接器到,这和压缩⽂件的过程⾮常类似。
C语⾔在发布的时候已经将标准库打包到了静态库,并提供了相应的头⽂件,例如 stdio.h、stdlib.h、string.h 等。
Linux ⼀般将静态库和头⽂件放在/lib和/user/lib⽬录下,C语⾔标准库的名字是libc.a,⼤家可以通过locate命令来查它的路径:
$ locate libc.a
/usr/lib/x86_64-redhat-linux6E/lib64/libc.a
$ locate stdio.h
/usr/include/stdio.h
/usr/include/bits/stdio.h
/usr/include/c++/4.8.2/tr1/stdio.h
/usr/lib/x86_64-redhat-linux6E/include/stdio.h
/usr/lib/x86_64-redhat-linux6E/include/bits/stdio.h
在 Windows 下,标准库由 IDE 携带,如果你使⽤的是 Visual Studio,那么在安装⽬录下的\VC\include⽂件夹中会看到很多头⽂件,包括我们常⽤的stdio.h、stdlib.h 等;在\VC\lib⽂件夹中有很多 .
lib ⽂件,这就是链接器要⽤到的静态库。
⼤家也可以在当前⼯程的属性⾯板(在⼯程名处单击⿏标右键选择“属性”)中查看路径:
ANSI C 标准共定义了 15 个头⽂件,称为“C标准库”,所有的编译器都必须⽀持,如何正确并熟练的使⽤这些标准库,可以反映出⼀个程序员的⽔平:
合格程序员:<stdio.h>、<ctype.h>、<stdlib.h>、<string.h>
熟练程序员:<assert.h>、<limits.h>、<stddef.h>、<time.h>
优秀程序员:<float.h>、<math.h>、<error.h>、<locale.h>、<setjmp.h>、<signal.h>、<stdarg.h>
C语⾔共有两套标准,也就是 ANSI C 和 C99。ANSI C 是较早的标准,各种编译器都能很好的⽀持,C99 是后来的标准,编译器对它的⽀持不尽相同,请⼤家阅读《》了解更多细节。
除了C标准库,编译器⼀般也会附带⾃⼰的库,以增加功能,⽅便⽤户开发,争夺市场份额。这些库中的每⼀个函数都在对应的头⽂件中声明,可以通过#include 预处理命令导⼊,编译时会被合并到当前⽂件。
3、细说C语⾔头⽂件的路径
我们常说,引⼊编译器⾃带的头⽂件(包括标准头⽂件)⽤尖括号,引⼊程序⾃定义的头⽂件⽤双引号,例如:
#include <stdio.h> //引⼊标准头⽂件
#include "myFile.h"//引⼊⾃定义的头⽂件
使⽤尖括号< >,编译器会到系统路径下查头⽂件;⽽使⽤双引号" ",编译器⾸先在当前⽬录下查头⽂件,如果没有到,再到系统路径下查。也就是说,使⽤双引号⽐使⽤尖括号多了⼀个查路径,它的功能更为强⼤,我们完全可以使⽤双引号来包含标准头⽂件,例如:
#include "stdio.h"
#include "stdlib.h"
那么,这⾥所说的“系统路径”和“当前路径”是什么意思呢?
3.1、绝对路径和相对路径
理论上讲,我们可以将头⽂件放在磁盘上的任意位置,只要带路径包含进来就可以。以 Windows 为例,在 D 盘下创建⼀个⾃定义的⽂件夹,名字为abc,它⾥⾯有⼀个头⽂件叫做xyz.h,那么在程序开头使⽤#include "D:\\abc\xyz.h"就能够引⼊该头⽂件。
现在不妨假设 xyz.h 中有⼀个宏定义和⼀个变量:
#define NAME "C语⾔中⽂⽹"
int age = 5;
我们不⿎励在头⽂件中定义变量,否则多次引⼊后会出现重复定义错误,这⾥仅是⼀个演⽰案例,并不规范。
下⾯的代码会输出头⽂件中的宏和变量:
#include<stdio.h>
#include "D:\\abc\xyz.h"
int main()
{
printf("%s已经 %d 岁了!\n", NAME, age);
return0;
}
运⾏结果:
C语⾔中⽂⽹已经 5 岁了!
(1)绝对路径
像D:\\abc\xyz.h这种从盘符开始、完整地描述⽂件位置的路径就是绝对路径(Absolute Path)。
绝对路径从⽂件系统的“根部”开始查⽂件:
1) 在 Windows 下,根部就是 C、D、E 这样的盘符,例如D:\\a.h、E:\images\123.jpg、E:/videos/me.mp4、D://abc/xyz.h等,分隔符可以是正斜杠/也可以是反斜
杠\,盘符后⾯的斜杠可以有⼀个也可以有两个。
2) Linux 没有盘符,根部就是/,例如/home/xxx/abc.h、/user/include/module.h等,分隔符只能是正斜杠/,⽐ Windows 简洁很多。
为了增强代码的可移植性,引⼊头⽂件时请尽量使⽤正斜杠/。
(2)相对路径
相对路径(relative path)是从当前⽬录(⽂件夹)开始查⽂件;当前⽬录是指需要引⼊头⽂件的源⽂件所在的⽬录,这也是本⽂开头提到的“当前路径”。
以 Windows 为例,假设在E:/cDemo/中有源⽂件 main.c 和头⽂件 xyz.h,那么在 main.c 中使⽤#include "./xyz.h"语句就可以引⼊ xyz.h,其中./表⽰当前⽬录,也即E:/cDemo/。
如果将 xyz.h 移动到E:/cDemo/include/(main.c 所在⽬录的下级⽬录),那么包含语句就应该修改为#include "./include/xyz.h";对于 main.c 来说,此时的“当前⽬录”依然是E:/cDemo/。
如果将 xyz.h 移动到E:/(main.c 所在⽬录的上级⽬录),那么包含语句就应该修改为#include "./../xyz.h",其中../表⽰上级⽬录。./../xyz.h的意思是,在当前⽬录的上级⽬录中查 xyz.h ⽂件。
如果将 xyz.h 移动到E:/include⽬录,那么包含语句就应该修改为#include "./../include/xyz.h"。
需要注意的是,我们可以将./省略,此时默认从当前⽬录开始查,例如#include "xyz.h"、#include "include/xyz.h"、#include "../xyz.h"、#include "../include/xyz.h"。
上⾯介绍的相对路径的写法同样适⽤于 Linux,请⼤家亲⾃测试,这⾥不再赘述。
在实际开发中,我们都是将头⽂件放在当前⼯程⽬录下,⾮常建议⼤家使⽤相对路径,这样即使后来改变了⼯程所在⽬录,也⽆需修改包含语句,因为源⽂件的相对位置没有改变。
3.2、系统路径
Windows 下的C语⾔标准库由 IDE ⾃⼰携带,Linux 下的C语⾔标准库⼀般在固定的路径下,总起来说,标准库不在⼯程⽬录下,要使⽤绝对路径才能引⼊头⽂件,这样每次切换平台或者 IDE 都要修改包含路径,⾮常不⽅便。
为了让头⽂件更加具有实践意义,Windows 下的 IDE 都可以为静态库和头⽂件设置默认⽬录。以 Visual Studio 为例,在当前⼯程名处单击⿏标右键,选择“属性”,在弹出的对话框中就可以看到已经设置好的路径,如下图所⽰:
这些已经设置好的路径就是本⽂开头提到的“系统路径”。
当使⽤相对路径的⽅式引⼊头⽂件时,如果使⽤< >,那么“相对”的就是系统路径,也就是说,编译器会直接在这些系统路径下查头⽂件;如果使⽤" ",那么⾸先“相对”的是当前路径,然后“相对”的才是系统路径,也就是说,编译器⾸先在当前路径下查头⽂件,不到的话才会继续在系统路径下查。
⽽使⽤绝对路径的⽅式引⼊头⽂件时,< >和" "没有任何区别,因为头⽂件路径已经写死了(从根部开始查),不需要“相对”任何路径。
总起来说,相对路径要有“相对”的⽬标,这个⽬标可以是当前路径,也可以是系统路径,< >和" "决定了到底相对哪个⽬标。
4、防⽌C语⾔头⽂件被重复包含
头⽂件包含命令 #include 的效果与直接复制粘贴头⽂件内容的效果是⼀样的,预处理器实际上也是这样做的,它会读取头⽂件的内容,然后输出到
#include 命令所在的位置。
头⽂件包含是⼀个递归(循环)的过程,如果被包含的头⽂件中还包含了其他的头⽂件,预处理器会
继续将它们也包含进来;这个过程会⼀直持续下去,直到不再包含任何头⽂件,这与递归的过程颇为相似。
递归包含会导致⼀个问题,就是重复引⼊同⼀个源⽂件。例如在某个⾃定义头⽂件 xyz.h 中声明了⼀个 FILE 类型的指针,以使得所有的模块都能使⽤它,如下所⽰:
extern FILE *fp;
FILE 是在 stdio.h 中⾃定义的⼀个类型(本质上是⼀个结构体),要想使⽤它,必须包含 stdio.h,因此 xyz.h 中完整的代码应该是这样的:
#include <stdio.h>
extern FILE *fp;
现在假设程序的主模块 main.c 中需要使⽤ fp 变量和 printf() 函数,那么就需要同时引⼊ xyz.h 和 stdio.h:
#include <stdio.h>
#include "xyz.h"
int main()
{
if( (fp = fopen("", "r")) == NULL )
程序员和编程员的区别 {
printf("File open failed!\n");
}
//TODO:
return0;
}
这样⼀来,对于 main.c 这个模块,stdio.h 就被包含了两次。stdio.h 中除了有函数声明,还有宏定义、类型定义、结构体定义等,它们都会出现两次,如果不做任何处理,不仅会出现重复定义错误,⽽且不符合编程规范。
有⼈说,既然已经知道 xyz.h 中包含了 stdio.h,那么在 main.c 中不再包含 stdio.h 不就可以了吗?是的,确实如此,这样做就不会出现任何问题!
现在我们不妨换⼀种场景,假设 xyz1.h 中定义了类型 RYPE1,xyz2.h 中定义了类型 TYPE2,并且它们都包含了 stdio.h,如果主模块需要同时使⽤
TYPE1 和 TYPE2,就必须将 xyz1.h 和 xyz2.h 都包含进来,这样也会导致 stdio.h 被重复包含,并且⽆法回避,上⾯的⽅案解决不了问题。
实际上,头⽂件的交叉包含是⾮常普遍的现象,不仅我们⾃⼰创建的头⽂件是这样,标准头⽂件也是如此。例如,标准头⽂件 limits.h 中定义了⼀些与数据类型相关的宏(最⼤值、最⼩值、⼀个字节所包含的⽐特位等),stdlib.h 就包含了它。
我们必须到⼀种⾏之有效的⽅案,使得头⽂件可以被包含多次,但效果与只包含⼀次相同。
在实际开发中,我们往往使⽤宏保护来解决这个问题。例如,在 xyz.h 中可以添加如下的宏定义:
#ifndef _XYZ_H
#define _XYZ_H
/* 头⽂件内容 */
#endif
第⼀次包含头⽂件,会定义宏 _XYZ_H,并执⾏“头⽂件内容”部分的代码;第⼆次包含时因为已经定义了宏 _XYZ_H,不会重复执⾏“头⽂件内容”部分的代码。也就是说,头⽂件只在第⼀次包含时起作⽤,再次包含⽆效。
标准头⽂件也是这样做的,例如在 Visual Studio 2010 中,stdio.h 就有如下的宏定义:
#ifndef _INC_STDIO
#define _INC_STDIO
/* 头⽂件内容 */
#endif
这种宏保护⽅案使得程序员可以“任性”地引⼊当前模块需要的所有头⽂件,不⽤操⼼这些头⽂件中是否包含了其他的头⽂件。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论