C语言文件框架与运行

不知道大家有没有一种感觉,当你初学c语言运用(仅仅是运用)到熟练的时候,会明显感受到一些疑惑:这个头文件我好像从来没写过、这个多文件我好像也从来没用过、这个多文件的C语言程序又是怎么跑起来的

C语言的文件框架

没有框架

最简单的结构是什么?就是没有结构,一个单一的XX.c文件配合gcc工具编译就可以运行。

无框架示例

这里简单介绍一下gcc,gcc是GUN中的一个编译工具,在C语言中的指令(该顺序也就是c语言运行的顺序)主要有:

  1. 预处理,进行宏展开等,生成代码文件为helloworld.i
1
gcc -E helloworld.c
  1. 编译,生成汇编代码,生成代码文件为helloworld.s
1
gcc -S helloworld.c
  1. 汇编,生成机器码,生成代码文件为helloworld.o
1
gcc -c helloworld.c
  1. 链接,实际上直接链接能够运行以上所有的步骤,生成的是一个名为helloworld的可执行文件
1
gcc -o helloworld helloworld.c

在命令行中运行c语言程序的方式:

1
.\helloworld

实际上就是运行我们刚刚链接出来的可执行文件,中间的部分是我们的命名,命名为helloworld

头文件+源文件结构

我作为一个半路出家的C语言使用者(以前是使用JAVA和Python),简单使用是并不难的,但是当我想做多文件结构的时候我发现我并不会,C语言并没有像JAVA和Python一样的导入方法,但是其实是有的,就是我们在单文件编程时不怎么搭理的头文件。

头文件

首先说说头文件,头文件是什么,我们先从名字上来说:头文件,头是什么?头在哪儿?头位于人的最顶端,是人脑所在的地方负责思考获取知识。C语言中的头文件也是,头文件就是C语言main程序中的最顶端,负责获取知识(程序接口)。

那么头文件又该如何定义,细节又是如何?

我们先举个例子直观理解:

众所周知,C语言是面向过程的语言,其中函数是面向过程语言的精髓,下面我们首先定义一个函数:

1
2
3
void helloworld(){
printf("hello,World!\n");
}

众所周知,当程序员定义完用户程序后,main函数想要调用需要事先声明:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

void helloworld(); // 声明函数
int main() {
helloworld();
return 0;
}

void helloworld(){
printf("hello,World!\n");
}

现在请大家想象一个情况,如果现在我们有1万个函数要声明,那是不很麻烦,而且会有极高的耦合度,且不方便阅读。而头文件就代替了声明加函数定义的部分。

于是我们可以拆一下,分为这样的结构:

文件结构

其中helloworld.c中为

1
2
3
void helloworld(){
printf("hello,World!\n");
}

helloworld.h中为:

1
2
3
4
#ifndef EXAMPLE_HELLOWORLD_H
#define EXAMPLE_HELLOWORLD_H
void helloworld();
#endif //EXAMPLE_HELLOWORLD_H

main.c中为:

1
2
3
4
5
6
7
#include <stdio.h>
#include "helloworld.h"

int main() {
helloworld();
return 0;
}

这样就将整个文件模块化了,但是其中依旧存在一些问题没有讲清楚,比如这个头文件到底怎么写,里边的内容都有什么含义?下面我们先解释一些前置的知识,慢慢道来。

#include命令

#include命令实际上很简单,可以等同为import语句,其叫做文件包含命令,用来引入对应的头文件,其工作原理就是将头文件的内容插入到当前命令所在的位置上,连接成一整个源文件。

#include命令分为两种:

  1. 一种是#include<>,引用的是编译器的类库路径里面的头文件,其用于导入官方标准头文件

  2. 另一种是#include"",引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件,其用于导入自定义的头文件

也就是说引用系统标准库都可以,但第一种更快;引用自己定义的头文件只能用第二种。

宏定义

#define 叫做宏定义命令它也是C语言预处理命令的一种,所谓宏定义,就是用一个标识符来表示一个字符串。

宏定义的形式为:

1
#define 宏名 字符串

宏名就是一种标识符,而字符串可以是数字、表达式、if语句、函数等。

tips:

  • 宏定义仅仅是替换,并不计算

  • 宏定义的处理步骤是上面的预处理阶段,因此是否正确要等到编译阶段

  • 宏定义可以拥有定义域

    1
    2
    3
    #define PI 3.1415
    ———————作用域——————
    #undef PI
  • 习惯上,宏定义用大写

常见的预处理命令有:

命令 说明
# 空指令
#include 引入头文件
#define 定义宏
#undef 取消宏
#if 如果条件为真,则编译if内的代码
#ifdef 如果宏已经定义,则编译if内的代码
#ifundef 如果宏未定义,则编译if内的代码
#elif 如果前面的#if条件为假,当前条件为真,则编译
#endif 结束一个#if

头文件编写

现在前置知识都已经补足了,我们来说一声头文件编写的规范:

  • 建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件
  • 源文件的名字 可以不和头文件一样,但是为了好管理,一般头文件名和源文件名一样
  • 不管是标准头文件,还是自定义头文件, 都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误(重定义)

这里也就可以解释为什么头文件要有这个程序段了:

1
2
3
4
#ifndef EXAMPLE_HELLOWORLD_H
#define EXAMPLE_HELLOWORLD_H
void helloworld();
#endif //EXAMPLE_HELLOWORLD_H

一旦该头文件被重复引用,那么就会检测到有这个宏定义了已经,那么就不会再次编译这个头文件,从而避免了重定义错误。

关于gcc、make与CMake

建议参考:关于gcc、make和CMake的区别_cmake和gcc的区别_ericwzy945的博客-CSDN博客