工程开发2-(C语言)

导读:本篇文章讲解 工程开发2-(C语言),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

多模块程序

在之前的课程中,我们所有的文件操作都是单文件进行的。对于一个只完成一个特定的任务,只包含十几个函数的程序来说,单文件的组织方式还算可以接受,但是当程序越来越长,程序实现的功能越来越多,将他们全部都组织在一个文件里就会变得不那么容易让人接受了。
因此,我们需要学习如何在 C 语言中将不同功能在多个代码文件中分别实现,然后将它们看作多个模块组织在一起为同一个程序服务。

单文件编译

在这里插入图片描述
在这里插入图片描述
回忆一下,在刚开始学习 C 语言的时候,我们曾经学习过,当我们的程序只有一个main.c文件时,我们可以在命令行中通过

gcc -o program main.c

对单个代码文件进行编译,生成可执行文件program,并且通过./program运行编译生成的程序。在我们之前的课程中,小开的学习系统也帮你进行了这样的操作。

编译成目标代码文件

gcc -c -o set.o set.c

在这里插入图片描述
相比于单个文件、单一功能的程序,当程序有多个模块时,问题就开始变得复杂了。我们对每一个模块会首先编译出每个模块对应的*.o目标代码文件(relocatable object file),例如:

会将我们的一个set.c文件编译成一个set.o的目标代码文件。请注意,这里的-c表示生成目标代码文件。-o与之前单文件的时候一样,在它之后我们会写明被生成的文件的名称

链接

gcc -o program main.o set.o others.o

在这里插入图片描述
当我们完成了每一个独立模块的编译并获得它们的目标代码文件后,我们可以将我们的主程序的目标代码文件与他们链接在一起。例如:

gcc -o program main.o set.o others.o

将目标代码文件set.o和others.o与main.o在链接在一起,并且输出了 可执行文件(excutable file)program。
我们依然可以通过./program运行编译生成的程序。

头文件

在这里插入图片描述
在这里插入图片描述
当我们将一个程序写在多个文件中时,每一个文件中的变量和函数默认都是只有文件内的部分才可以访问的。但是有一些特殊的全局变量、类型定义、函数可能会需要在多个文件中被使用。
这时候,我们可以将这类的内容单独写成一个 头文件(header file) ,并且将全局变量、类型定义、函数声明写到头文件中。
对于一个文件set.c,习惯上它的头文件会被命名为set.h。在所有需要用set.h中全局变量、类型定义、声明的函数的文件中,用

#include "set.h"

将对应的头文件引入。在这里的引入头文件方式和引入系统库头文件的方式很类似,只不过这里用的是引号””而不是尖括号<>。

避免重复引入

在这里插入图片描述
在这里插入图片描述
由于头文件里也可以引入头文件,因此我们可能事实上多次引入同一个文件,比如我们引入1.h和2.h,且1.h也引入2.h,这时因为2.h被引入了两次,就有可能出现重复的声明。为了解决这个问题,我们在2.h中定义一个宏,在2.h的最开始判断这个宏是否被定义过,如果被定义过,就跳过2.h整个文件的内容。
这里我们将会用到两个新的预处理指令#ifndef xxx和#endif,它们成对出现且#ifndef在前,作用是如果这时并未已定义xxx宏,则这对#ifndef xxx, #endif
之间的内容有效。(其中xxx可以替换为任意宏名)
这样2.h可以写为类似于如下的内容:

#ifndef xxx
#define xxx
typedef enum Status { Success, Fail };
typedef struct {
    char *name;
    int age;
} People;
Status go_to_Jisuanke(People);
#endif

细心的同学已经发现,如果在程序中尚未引入2.h的位置定义了xxx宏,则#include “2.h”中的声明并不会被引入,因此我们不应该在此使用xxx这种平凡的名字。实际上,我们一般会采用一个与头文件名相关的名字来代替xxx,比如一个常用的代码风格里,这个宏的名字形式为工程名_路径名_文件名_H_。

Makefile

在前面学习多模块程序的时候,我们需要先把每个模块的代码都生成为目标代码文件,然后再将目标代码文件联编成一个可执行文件。如果每一次编译都要输入这么多命令,是不是很复杂呢?如果每次修改一点点内容就需要重新编译整个工程,是不是很浪费时间呢?
为了解决所遇到的问题,方便开发,我们使用一个叫做make的命令,它可以读取Makefile文件,并且根据Makefile中的规则描述把源文件生成为可执行的程序文件。

Makefile 中规则

在这里插入图片描述
在这里插入图片描述
最基本的Makefile中包含了一系列形式如下的规则。请注意,每一条规则的命令前,必须要有一个制表符\t。

目标: 依赖1 依赖2 ...
    命令

例如,我们可以写一条规则:

array.o: array.c array.h
   gcc -c -o array.o array.c

表示生成的文件是目标代码文件array.o,它依赖于array.c和array.h。当我们在命令行中执行make array.o时,根据这一规则,如果array.o不存在或者array.c与array.h至少之一比array.o更新,就会执行gcc -c -o array.o array.c。
我们把上述代码保存为Makefile,与array.c和array.h放在同一目录,在那个目录里执行make array.o就能看到效果。注意:Makefile里的除当前目录隐藏文件外的第一个目标会成为运行make不指定目标时的默认目标。

main: array.o main.o
    gcc -o main array.o main.o

main.o: main.c array.h
    gcc -c -o main.o main.c

array.o: array.c array.h
    gcc -c -o array.o array.c

在这里插入图片描述
在Makefile有多条规则时,如果我们希望只生成其中一个,我们可以在make命令后加上需要生成的目标的名称。例如,在这里我们可以执行make main.o、make array.o或make main。当我们执行make main时,make命令发现array.o和main.o不存在,就会根据以它们为目标的规则先生成它们。

clear 规则

在这里插入图片描述

clean:
    rm -f array.o main.o main

.PHONY: clean

clean:
    rm -f array.o main.o main

很多时候,你会需要将.o为后缀的目标代码文件和可执行的程序文件删除,完全从头进行编译。那么我们可以写一条clean规则,例如:

clean:
    rm -f array.o main.o main

rm命令表示删除文件,-f表示强制,因此rm -f array.o main.o main
按照预期,当我们执行make clean就可以删除array.o、main.o和main了。事实真的这样吗?
细心的同学已经发现,这时如果已经存在clean文件,rm命令就不会执行了。为了解决这个问题,我们通过一个特殊的方法告诉make这个名为clean的规则在clean存在的时候仍然有效。

.PHONY: clean

clean:
    rm -f array.o main.o main

.PHONY用于声明一些伪目标,伪目标与普通的目标的主要区别是伪目标不会被检查是否存在于文件系统中而默认不存在且不会应用默认规则生成它。

完整的makefile 的命令

在这里插入图片描述
在Makefile中我们还可以使用它的变量和注释。

# 井号开头的行是一个注释

# 设置 C 语言的编译器

CC = gcc

# -g 增加调试信息

# -Wall 打开大部分警告信息

CFLAGS = -g -Wall

# 整理一下 main 依赖哪些目标文件

MAINOBJS = main.o array.o

.PHONY: clean

main: $(MAINOBJS)
    $(CC) $(CFLAGS) -o main $(MAINOBJS)

array.o: array.c array.h
    $(CC) $(CFLAGS) -c -o array.o array.c

main.o: main.c array.h
    $(CC) $(CFLAGS) -c -o main.o main.c

clean:
    rm -f $(MAINOBJS) main

上面这个例子已经是一个较为完整的Makefile了。以#开头的是我们的注释,我们在这里用注释说明了我们定义的Makefile变量的用途。CC变量定义了编译器,CFLAGS变量标记了编译参数,MAINOBJS变量记录了main依赖的目标文件。定义的变量可以直接通过$(变量名)进行使用。

命令行参数

在这里插入图片描述
在这里插入图片描述
但是,实际上函数是可以有参数的。我们可以将任何过去无参数的函数替换成下面这种有参数的函数(不过考虑到我们并没有利用,不写是很正常的)。

int main(int argc, char **argv) {
    // ...
}

在这里,main函数有两个参数,第一个参数是整数型,会传入命令行参数的个数,程序运行时就可以接收到。第二个参数是char **,其中储存了用户从命令行传递进来的参数。
如果我们的程序可执行文件名为main,则在命令行中输入./main hello world我们会得到argc为 3,argv[0]为./main,argv[1]为hello,argv[2]为world。如果有更多参数也可以以此类推。
在这里插入图片描述

int main(int argc, char **argv) {
    // ...
}

我们不难发现,命令行参数默认都是空格分隔,但是如果我们希望包含空格的一个字符串作为参数,我们则需要在输入参数时用引号将其包裹起来。
如果我们的程序可执行文件名为main,则在命令行中输入./main “hello world” is my greet我们会得到argc为 5,argv[0]为./main,argv[1]为hello world,argv[2]为is,argv[3]为my,argv[4]为greet。
任何被接收到的argv参数都可以被当做正常的字符串在代码里使用。在很多程序的设计中,我们会需要根据接收到的参数来决定程序的执行方式,这时候,学会使用argc和argv就显得很重要了。在之后的课程中,你也会需要运用这一块的知识,一定要学明白喔。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76942.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!