13 重忆C 之 工程开发命令
pwd
返回了根目录
这时候看到系统返回了一个 /,这个 / 被我们称为系统的 根目录(root
),这个位置也就是我们现在在系统中的位置。
但是,我们要开展工作的位置的路径为:
/home/user/project
所以,我们要学会如何切换我们所在的位置:
输入 cd 命令,并在 cd 命令后加上空格,在此后输入开展工作的位置的路径:
cd /home/user/project
这就是一个层级化路径,其实其可以通过
cd home
cd user
cd project
逐次到达。
下载输入 ls -l。
这里就有个old目录。
mv main.c old //mv [选项] 源文件或目录 目标文件或目录
再使用移动操作:
移动文件再创建新文件,通过命令
touch main.c
创建新文件
这时候可以看见:
去old
目录下把key
删除
更多相关命令的可附加参数及使用意义,可以通过 man [空格] [命令名]
的方式进行进行进一步的查询(查询后退出只需敲击键盘上的 q 即可)。
多模块程序
之前的课中,所有文件操作都是单文件进行。对于一个只完成一个特定的任务,只包含十几个函数的程序来说,单文件的组织方式还算可以接受,但是当程序越来越长,程序实现的功能越来越多,将他们全部都组织在一个文件里就会变得不那么容易让人接受了。
因此,我们需要学习如何在 C 语言中将不同功能在多个代码文件中分别实现,然后将它们看作多个模块组织在一起为同一个程序服务。
关于gcc命令
原地址:gcc命令中参数c和o混合使用的详解、弄清gcc test.c 与 gcc -c test.c 的差别
gcc命令使用GNU推出的基于C/C++的编译器,是开放源代码领域应用最广泛的编译器,具有功能强大,编译代码支持性能优化等特点。现在很多程序员都应用GCC,怎样才能更好的应用GCC。目前,GCC可以用来编译C/C++、FORTRAN、JAVA、OBJC、ADA等语言的程序,可根据需要选择安装支持的语言。
语法
gcc(选项)(参数)
选项
-o:指定生成的输出文件;
-E:仅执行编译预处理;
-S:将C代码转换为汇编代码;
-wall:显示警告信息;
-c:仅执行编译操作,不进行连接操作。
参数
C源文件:指定C语言源代码文件。
实例
常用编译命令选项
假设源程序文件名为test.c
无选项编译链接
gcc test.c
将test.c
预处理、汇编、编译并链接形成可执行文件。
这里未指定输出文件,默认输出为a.out
。
选项 -o
gcc test.c -o test
将test.c
预处理、汇编、编译并链接形成可执行文件test
。
-o
选项用来指定输出文件的文件名。
选项 -E
gcc -E test.c -o test.i
将test.c预处理输出test.i文件。
选项 -S
gcc -S test.i
将预处理输出文件test.i汇编成test.s文件。
选项 -c
gcc -c test.s
将汇编输出文件test.s编译输出test.o文件。
无选项链接
gcc test.o -o test
将编译输出文件test.o链接成最终可执行文件test。
选项 -O
gcc -O1 test.c -o test
使用编译优化级别1编译程序。级别为1~3,级别越大优化效果越好,但编译时间越长。
多源文件的编译方法
如果有多个源文件,基本上有两种编译方法:
假设有两个源文件为test.c
和testfun.c
- 多个文件一起编译
gcc testfun.c test.c -o test
将testfun.c
和test.c
分别编译后链接成test可执行文件。
- 分别编译各个源文件,之后对编译后输出的目标文件链接。
gcc -c testfun.c
将testfun.c编译成testfun.o
gcc -c test.c
将test.c编译成test.o
gcc -o testfun.o test.o -o test
将testfun.o和test.o链接成test
以上两种方法相比较,第一中方法编译时需要所有文件重新编译,而第二种方法可以只重新编译修改的文件,未修改的文件不用重新编译。
再来复习一下:
gcc -c a.c 编译成目标文件a.o
gcc -o a a.o 生成执行文件a.exe
gcc a.c 生成执行文件a.exe
gcc -o a -c a.c 编译成目标文件a
gcc -o a a.c 生成执行文件a.exe
在a.c中引用test.c中的一个函数后:
gcc -c test.c 编译成目标文件test.o
gcc -c a.c 编译成目标文件a.o
gcc -o a test.o a.o 生成执行文件a.exe
gcc -o a test.o a.c 生成执行文件a.exe
gcc -o a test.c a.c 生成执行文件a.exe
gcc -o a test.o a.c 生成执行文件a.exe
总结:只要参数中有-c,总是生成目标文件;只要参数中无-c而只有-o,则总是生成执行文件。
在刚开始学习 C 语言的时候,我们曾经学习过,当我们的程序只有一个main.c
文件时,我们可以在命令行中通过
gcc -o program main.c
对单个代码文件进行编译,生成可执行文件program
,并且通过./program
运行编译生成的程序。在我们之前的课程中,计蒜客的学习系统也帮你进行了这样的操作。
相比于单个文件、单一功能的程序,当程序有多个模块时,问题就开始变得复杂了。我们对每一个模块会首先编译出每个模块对应的*.o目标代码文件(relocatable object file),例如:
gcc -c -o set.o set.c
会将我们的一个set.c
文件编译成一个set.o
的目标代码文件。请注意,这里的-c表示生成目标代码文件。-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_
。
总结的几点
- 某一代码中定义的函数如果需要被其他代码文件所使用,应该将函数的声明放入头文件,并在其他代码文件中引入这一头文件。
- 并不需要把每个函数单独写成一个模块,还是应该根据功能的划分和实现去决定怎么抽出模块。
- 可以只有多个
.c
的文件,也并不一定非要都拆出.h
文件。 -
#include
可以被用于引入系统库头文件也可以被用于引入自己实现的头文件。 - 只不过在引入系统库头文件时,我们往往会使用尖括号
<>
,而在引入自己实现的头文件时一般用引号""
。 - 用
gcc
时,-o
之后写的是生成可执行文件的名称。-c
的参数的使用会帮我们得到一个对象文件。
//-c和-o都是gcc编译器的可选参数
//-c表示只编译(compile)源文件但不链接,会把.c或.cc的c源程序编译成目标文件,一般是.o文件。
//-o用于指定输出(out)文件名。不用-o的话,一般会在当前文件夹下生成默认的a.out文件作为可执行程序。
//例如
gcc -c test.c //将生成test.o的目标文件
gcc -o app test.c //将生成可执行程序app
gcc -c a.c -o a.o //表示把源文件a.c编译成指定文件名a.o的中间目标文件(其实在这里,你把-o a.o省掉,效果是一样的,因为中间文件默认与源文件同名,只是后缀变化)。
Makefile
在前面学习多模块程序的时候,我们需要先把每个模块的代码都生成为目标代码文件,然后再将目标代码文件联编成一个可执行文件。如果每一次编译都要输入这么多命令,是不是很复杂呢?如果每次修改一点点内容就需要重新编译整个工程,是不是很浪费时间呢?
为了解决所遇到的问题,方便开发,我们使用一个叫做make
的命令,它可以读取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
不存在,就会根据以它们为目标的规则先生成它们。
很多时候,会需要将.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中我们还可以使用它的变量和注释。
# 井号开头的行是一个注释
# 设置 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
依赖的目标文件。定义的变量可以直接通过$(变量名)
进行使用。
总结
- 一个 Makefile 可以包含多个规则,我们既可以每次在make后说明执行哪个功能,也可以通过定义的all来执行一系列的规则。
- 在用gcc编译时加上-Wall会显示错误信息,Wall是用于显示大部分警告信息的,编译错误信息默认就会显示。
- Makefile其实描述了一系列转为对象文件、联编的过程,不使用make也是可以完成的。
- Makefile中的变量是用$()的方式来用哒。
Makefile体验
(1)→ ~/project ls -l
total 16
-rw-r--r-- 1 user user 304 Sep 15 16:46 array.c
-rw-r--r-- 1 user user 87 Sep 15 16:46 array.h
-rw-r--r-- 1 user user 297 Sep 15 16:46 main.c
-rw-r--r-- 1 user user 0 Sep 15 16:46 main.h
-rw-r--r-- 1 user user 419 Sep 15 16:46 Makefile
→ ~/project
→ ~/project cat 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
→ ~/project
(2)
→ ~/project make
gcc -g -Wall -c -o main.o main.c
gcc -g -Wall -c -o array.o array.c
gcc -g -Wall -o main main.o array.o
→ ~/project
(3)
→ ~/project ./main
1 2 3 4 5 6 7 8 9 0
数组元素和为: 45
数组元素平均值为: 4.5
→ ~/project
(4)
→ ~/project make clean
rm -f main.o array.o main
(5)
→ ~/project
→ ~/project ls -l
total 16
-rw-r--r-- 1 user user 304 Sep 15 16:46 array.c
命令行参数
之前,main
函数一般都没参数,对应在运行时,一般就直接输入可执行的程序文件名(例如./main
)。
但实际上main函数可以有参数。我们可以将任何过去无参数的main函数替换成下面这种有参数的main函数(不过考虑到我们并没有利用,不写是很正常的)。
int main(int argc, char **argv) {
// ...
}
在这里,main
函数有两个参数,第一个参数是整数型,会传入命令行参数的个数,程序运行时就可以接收到。第二个参数是char **
,其中储存了用户从命令行传递进来的参数。
如果我们的程序可执行文件名为main
,则在命令行中输入./main hello world
我们会得到argc
为3
,argv[0]
为./main
,argv[1]
为hello
,argv[2]
为world
。如果有更多参数也可以以此类推。
命令行参数默认都是空格分隔,但是如果我们希望包含空格的一个字符串作为参数,我们则需要在输入参数时用引号将其包裹起来。
如果我们的程序可执行文件名为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
就显得很重要了。在之后的课程中,你也会需要运用这一块的知识,一定要学明白喔。
一些总结
- 命令行读入的参数是从命令行键入的可执行程序路径开始计算。
- 在main函数中用于接收命令行参数的函数参数中,第一个是命令行参数的个数。
- 在int main(int argc, char **argv)中,argc就固定为 2 了,它取到的应该是命令行中键入的参数个数。
命令行参数
命令行参数是怎么获取和使用的?
请先输入 cat main.c 看一下我们当前所在目录下的 main.c 文件。
image.png看到,在这个 main.c 的文件中,我们的 main 函数获取了命令行参数的个数 argc(整数型)和一系列参数(字符指针的指针,可以用访问数组的形式访问)。
这个程序将预期输出命令行参数的数量,并且将每一个参数逐一列出。
接下来让我们 make 一下,完成对这个程序的编译。
完成了 make
,就让我们把它运行起来吧。请输入 ./main
并运行起来这个程序,并在之后随意输上一些空格分隔开的字符串,例如:
./main I feel better after this
我们程序中的argc
接受到的参数一共是几个,它们分别对应了我们在终端中输入的哪一部分的内容呢。
文件操作
之前课程中,我们学习、设计的所有程序都是从标准输入进行读取、向标准输出进行写出的,操作系统为我们准备好了标准输入、标准输出的界面。在这节课中,我们将要学习如何从文件中进行读取、如何向文件进行写入。
在读文件的时候我们需要先有一个可以让我们访问到文件的 文件指针(file pointer),它是一个FILE
类型的指针。
我们可以通过下面的方式声明一个文件指针。
FILE *fp;
这时候,如果我们希望对一个文件进行操作,我们需要先使用
fp = fopen(文件路径, 访问模式);
将文件指针和文件关联起来,其中第一个参数是一个字符串,对应了我们希望访问的文件路径。第二个参数是访问模式,它可以是表示只读模式的"r"
,也可以是表示只写模式的"w"
,还可以是在文件末尾追加的"a"
。
当我们将文件指针和文件关联起来后,我们就可以通过fgetc(fp)
;获得当前指针之后位置的一个字符了,每获得一个字符,指针会向后移动一个字符(如果到达文件尾部则会返回EOF
)。
我们这时通过fputc('c', fp);
的方式将字符'c'
写入到fp
关联的文件内了。
了解到这些信息后,我们就可以实现将一个文件复制到另一个文件内的函数了,例如:
void filecopy(FILE *in_fp, FILE *out_fp) {
char ch;
while ((ch = fgetc(in_fp)) != EOF) {
fputc(ch, out_fp);
}
}
这个函数接收的两个参数都是文件指针。这个函数会通过一个可读模式的文件指针逐字符地读取,并且通过一个可写模式的文件指针逐字符地将所有字符写出,从而起到复制文件内容的作用。
你需要注意,在给文件指针进行命名的时候,要避开 stdin、stdout 和 stderr 这三个名称。因为这三个名称其实已经用于标准输入、标准输出、标准错误的文件指针。
你可能会问了,那我们看到的 stdin
、stdout
和 stderr
的这三个文件指针可以直接使用吗?回答是肯定的。
我们是通过 fgetc(stdin);
获得来自标准输入的字符,也可以通过fputc(ch, stdout);
或 fputc(ch, stderr);
将变量 ch
中的字符输出到标准输出或标准错误中的。
除了fgetc和fputc之外,我们还可以使用fscanf和fprintf函数。这两个函数都很像我们已经很熟悉的scanf和printf函数,只是不过,scanf和printf 可以被看作 fscanf和fprintf 的特例。
我们使用 fscanf 从文件指针in_fp进行读取时,可以写成:
fscanf(in_fp, "%c", &a);
而如果我们写
fscanf(stdin, "%c", &a);
这将完全与下面直接使用 scanf 的方式等价。
scanf("%c", &a);
类似地,我们使用fprintf向文件指针out_fp进行写出时,可以写成:
fprintf(out_fp, "%c", a);
而如果我们写
fprintf(stdout, "%c", a);
这将完全与下面直接使用 printf
的方式等价。
printf("%c", a);
在使用文件并且确定不再继续使用后,我们要通过下面所示的方式将文件指针fp与文件的关联断开。你可以将它视为和fopen
相反的一个操作。
fclose(fp);
如果你不在程序中使用fclose
,程序正常结束时,程序会为所有打开的文件调用fclose
。
stdin
、stdout
其实也是被打开的文件指针,如果你觉得用不到的话,其实也是可以使用fclose
将他们关闭掉的。你可以自己试一试,关闭 stdin
、stdout
会对我们以前写过的程序带来什么样的影响呢?