C语言编程的各种源码文件

简介: 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标准库”,所有的编译器都必须支持,如何正确并熟练的使用这些标准库,可以反映出一个程序员的水平:
合格程序员:、、、
熟练程序员:、、、
优秀程序员:、、、、、、
C语言共有两套标准,也就是 ANSI C 和 C99。ANSI C 是较早的标准,各种编译器都能很好的支持,C99 是后来的标准,编译器对它的支持不尽相同,请大家阅读《C语言的三套标准:C89、C99和C11》了解更多细节。
  除了C标准库,编译器一般也会附带自己的库,以增加功能,方便用户开发,争夺市场份额。这些库中的每一个函数都在对应的头文件中声明,可以通过 #include 预处理命令导入,编译时会被合并到当前文件。
3、细说C语言头文件的路径
  我们常说,引入编译器自带的头文件(包括标准头文件)用尖括号,引入程序自定义的头文件用双引号,例如:

include //引入标准头文件

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

include "D:\abc\xyz.h"

int main()
{//代码效果参考:http://www.zidongmutanji.com/zsjx/394573.html

printf("%s已经 %d 岁了!\n", NAME, age);
return 0;

}
运行结果:
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

  现在假设程序的主模块 main.c 中需要使用 fp 变量和 printf() 函数,那么就需要同时引入 xyz.h 和 stdio.h:

include "xyz.h"

  if( (fp = fopen("demo.txt", "r")) == NULL )
  {
    printf("File open failed!\n");
  }
  //TODO:
  return 0;
  这样一来,对于 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

  这种宏保护方案使得程序员可以“任性”地引入当前模块需要的所有头文件,不用操心这些头文件中是否包含了其他的头文件。

相关文章
|
16天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
15天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
57 16
|
10天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
51 8
|
10天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
41 7
|
8天前
|
C语言 Windows
C语言课设项目之2048游戏源码
C语言课设项目之2048游戏源码,可作为课程设计项目参考,代码有详细的注释,另外编译可运行文件也已经打包,windows电脑双击即可运行效果
20 1
|
15天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
65 8
|
17天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
44 4
|
19天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
50 3
|
26天前
|
存储 编译器 C语言
如何在 C 语言中判断文件缓冲区是否需要刷新?
在C语言中,可以通过检查文件流的内部状态或使用`fflush`函数尝试刷新缓冲区来判断文件缓冲区是否需要刷新。通常,当缓冲区满、遇到换行符或显式调用`fflush`时,缓冲区会自动刷新。
|
26天前
|
存储 编译器 C语言
C语言:文件缓冲区刷新方式有几种
C语言中文件缓冲区的刷新方式主要包括三种:自动刷新(如遇到换行符或缓冲区满)、显式调用 fflush() 函数强制刷新、以及关闭文件时自动刷新。这些方法确保数据及时写入文件。