项目课程链接:牛客C++高薪求职项目–Linux高并发服务器开发
一、GCC:一个强大的编译器工具集
1.1 什么是GCC?
GCC,全称 GNU Compiler Collection(GNU 编译器套件),是由 GNU 项目开发的一套编程语言编译器。它是自由软件,遵循 GPL(GNU 通用公共许可证)协议。GCC 最初只是一个 C 语言编译器,但现在已经能够编译 C++、Java、Ada、Objective-C、Go 等多种语言。
GCC 是许多 Unix-like 系统(包括 Linux)的标准编译器,也有 Windows 版本的 GCC。GCC 是一个非常强大的工具,它不仅可以编译源代码生成可执行文件,还可以进行代码优化,甚至可以用于跨平台开发。
1.2 GCC工作流程
GCC 的工作流程可以分为四个阶段:预处理、编译、汇编和链接。
- 预处理:在这个阶段,GCC 会处理源代码(.h,.c,.cpp)中的预处理指令,如 #include、#define 等。这个阶段的结果是一个“预处理过的”源代码文件(.i)。
- 编译:在这个阶段,GCC 会将预处理过的源代码编译成汇编代码(.s)。
- 汇编:在这个阶段,GCC 会将汇编代码转换成机器代码,结果是一个或多个目标文件(.o)。
- 链接:在这个阶段,GCC 会将目标文件和需要的库链接在一起,生成可执行文件(.exe, .out)。
1.3 GCC和G++的区别
GCC 和 G++ 都是 GNU 编译器套件的一部分,但它们用于编译不同的语言。GCC 是用于编译 C 和其他语言的编译器,而 G++ 是用于编译 C++ 的编译器。
在命令行选项上,GCC 和 G++ 也有一些不同。例如,G++ 会自动链接 C++ 标准库,而 GCC 不会。因此,如果编译 C++ 程序,通常应该使用 G++。
1.4 GCC常用参数选项
GCC 有许多命令行选项,以下是一些常用的选项:
-c:只编译源代码,不进行链接,生成目标文件。
-o:指定输出文件的名字。
-g:生成调试信息,以便使用 gdb 等调试工具进行调试。
-Wall:开启所有的警告信息。
-I:指定头文件的搜索路径。
-L:指定库文件的搜索路径。
-l:链接指定的库。
例如,以下命令将源文件 main.c 编译成可执行文件 program:
gcc main.c -o program
二、静态库和动态库
2.1 什么是库?
- 在编程中,库(Library)是一组预编译的代码,这些代码被打包并可以被多个程序共享和重复使用。库通常包含一组函数和程序可以调用的程序接口。使用库可以帮助程序员节省时间,因为他们可以重复使用已经写好并经过测试的代码,而不需要从头开始编写所有的代码。
- 库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
- 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
- 库的好处:1.代码保密 2.方便部署和分发
2.2 静态库
静态库是一种包含了多个对象文件(.o 文件)的文件,它们在链接阶段被整合到最终的可执行文件中。静态库对于封装多个被多个程序共享的函数和数据非常有用。
2.2.1 命名规则
在 Linux 和 Unix 系统中,静态库的命名规则通常是 libxxx.a,其中 xxx 是库的名字。lib 是前缀,.a 是后缀。
在 Windows 系统中,静态库的命名规则通常是 libxxx.lib,其中 xxx 是库的名字。lib 是前缀,.lib 是后缀。
2.2.2 静态库的制作
制作静态库的过程通常包括两个步骤:首先使用 gcc 编译源代码文件生成对象文件,然后使用 ar 工具将多个对象文件打包成一个静态库。
以下是一个简单的示例,展示了如何从两个源代码文件 foo.c 和 bar.c 制作一个名为 libfoobar.a 的静态库:
- 使用 gcc 编译源代码文件生成对象文件:
gcc -c foo.c -o foo.o
gcc -c bar.c -o bar.o
这将生成两个对象文件 foo.o 和 bar.o。
- 使用 ar 工具将对象文件打包成静态库:
ar rcs libfoobar.a foo.o bar.o
这将生成一个名为 libfoobar.a 的静态库。
在 ar 命令中,r、c 和 s 是选项:
- r(replace):将文件插入到归档文件中。如果归档文件中已经有同名的文件,那么新文件将替换旧文件。
- c(create):创建新的归档文件。如果归档文件已经存在,那么这个选项没有效果。
- s(index):创建归档文件的索引。这可以加快链接器从归档文件中查找文件的速度。
静态库是一种非常有用的工具,它可以帮助管理和重用代码。
2.2.3 优缺点
优点:
- 独立性:静态库在编译时被包含在最终的可执行文件中,因此生成的可执行文件不依赖于系统中的其他库。这意味着你可以在没有安装相应库的系统上运行你的程序,或者在库版本不同的系统上运行你的程序,而不会出现问题。
- 性能:由于所有的代码都被包含在一个文件中,程序运行时不需要额外的加载步骤,因此可能会有更好的性能。
- 版本控制:由于库的代码被包含在可执行文件中,因此你可以确保你的程序使用的是你测试过的库版本,而不是用户系统中可能存在的不同版本的库。
缺点:
- 文件大小:静态库的代码被包含在可执行文件中,这可能会导致可执行文件的大小增加。
- 内存使用:如果有多个程序使用同一个静态库,那么每个程序都会有一份库的副本。这意味着静态库可能会使用更多的内存。
- 更新:如果库的代码需要更新,那么所有使用这个库的程序都需要重新编译和链接。这可能会使得更新库的过程变得复杂和耗时。
2.3 动态库
动态库,也被称为共享库,是一种包含了多个对象文件(.o 文件)的文件,它们在程序运行时被加载到内存中,可以被多个运行中的程序共享。动态库对于节省内存和允许在不重新编译程序的情况下更新库函数非常有用。
2.3.1 命名规则
在 Linux 和 Unix 系统中,动态库的命名规则通常是 libxxx.so,其中 xxx 是库的名字。lib 是前缀,.so 是后缀。
在 Windows 系统中,动态库的命名规则通常是 libxxx.dll,其中 xxx 是库的名字。lib 是前缀,.dll 是后缀。
2.3.2 动态库的制作
制作动态库的过程通常包括两个步骤:首先使用 gcc 编译源代码文件生成对象文件,然后使用 gcc 将多个对象文件链接成一个动态库。
以下是一个简单的示例,展示了如何从两个源代码文件 foo.c 和 bar.c 制作一个名为 libfoobar.so 的动态库:
使用 gcc 编译源代码文件生成对象文件:
gcc -c -fPIC foo.c -o foo.o
gcc -c -fPIC bar.c -o bar.o
这将生成两个对象文件 foo.o 和 bar.o。-fPIC 选项告诉 gcc 生成位置无关代码(Position Independent Code),这是创建动态库所需要的。
使用 gcc 将对象文件链接成动态库:
gcc -shared -o libfoobar.so foo.o bar.o
这将生成一个名为 libfoobar.so 的动态库。-shared 选项告诉 gcc 创建一个动态库。
动态库是一种非常有用的工具,可以帮助管理和重用代码,同时节省内存和允许动态更新库函数。
2.3.3 优缺点
优点:
- 节省内存:动态库在运行时被加载到内存中,多个程序可以共享同一个动态库的一个实例。这意味着如果有多个程序使用同一个库,那么库的代码只需要在内存中有一份副本,从而节省内存。
- 易于更新:你可以在不需要重新编译和链接使用该库的程序的情况下更新动态库。只需要替换库文件,然后重新启动使用该库的程序,新的库代码就会被使用。
- 节省磁盘空间:由于动态库不是被包含在每个可执行文件中,因此它可以减少磁盘空间的使用。
缺点:
- 依赖性:动态库在运行时被加载,因此如果系统中没有安装所需的库,或者库的版本不正确,程序可能无法运行。
- 性能:动态库需要在运行时被加载和链接,这可能会导致程序启动和运行速度稍慢。
- 版本兼容性问题:如果不同的应用程序依赖于库的不同版本,可能会出现兼容性问题。
2.4 静态库和动态库的区别
在Linux开发中,库分为静态库和动态库两种。它们都是一种文件,包含了一些预编译的代码,这些代码可以被其他程序在运行时或编译时调用。然而,静态库和动态库在使用方式和效果上有一些重要的区别。
-
静态库:静态库是一种包含了一组函数和/或变量的二进制文件,它在程序编译时被链接到目标程序中。当你使用静态库编译程序时,库中的代码会被复制到最终的可执行文件中。这意味着,如果你的程序使用了静态库,那么你的程序在没有这个库的情况下仍然可以运行,因为它已经包含了所有需要的代码。然而,这也意味着如果静态库更新了,你需要重新编译你的程序才能使用新版本的库。静态库的文件扩展名通常是 .a。
-
动态库:动态库也是一种包含了一组函数和/或变量的二进制文件,但它在程序运行时(而不是编译时)被链接到目标程序中。当你的程序使用动态库时,它不会将库的代码复制到可执行文件中,而是在运行时从库文件中加载这些代码。这意味着,如果你的程序使用了动态库,那么你的程序在没有这个库的情况下无法运行。然而,这也意味着如果动态库更新了,你的程序可以在不重新编译的情况下使用新版本的库。动态库的文件扩展名通常是 .so。
总的来说,静态库和动态库各有优缺点。静态库可以使程序更加独立,不受库文件的影响,但可能会导致程序体积较大;动态库可以使程序体积更小,易于更新,但需要确保运行环境中有相应的库文件。在实际开发中,应根据具体需求选择使用静态库还是动态库。
三、Makefile
整理完了才看到有人整理好了:
Linux编程 Makefile常用知识点总结(项目管理)
3.1 什么是 Makefile?
Makefile 是一种组织代码编译的文件,它定义了一组规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,以及如何编译这些文件。Makefile 是由 make 工具使用的,make 工具会读取 Makefile,然后根据 Makefile 中定义的规则来编译和链接源代码,生成可执行文件。
Makefile 主要包含以下几种元素:
- 目标(Target):目标通常是生成的文件名,可以是可执行文件,也可以是中间文件(如.o文件)。
- 依赖(Dependencies):依赖是目标文件需要的源文件或者其他目标文件。只有当所有的依赖都被正确生成后,目标文件才会被生成。
- 命令(Commands):命令是生成目标文件需要执行的系统命令,通常是编译命令或者链接命令。
一个简单的 Makefile 可能如下所示:
target: dependencies
commands
例如,一个编译 C++ 程序的 Makefile 可能如下所示:
hello: hello.cpp
g++ hello.cpp -o hello
在这个例子中,目标是 “hello”,依赖是 “hello.cpp”,命令是 “g++ hello.cpp -o hello”。当执行 “make” 命令时,make 工具会检查 “hello” 文件和 “hello.cpp” 文件的修改时间,如果 “hello.cpp” 文件的修改时间比 “hello” 文件的修改时间新,或者 “hello” 文件不存在,那么就会执行命令 “g++ hello.cpp -o hello” 来生成 “hello” 文件。
Makefile 可以极大地简化编译过程,特别是在大型项目中,通过 Makefile,我们可以精确地控制编译过程,避免不必要的编译,提高编译效率。
3.2 Makefile中使用变量
在 Makefile 中,变量用于存储和操作一些值,这些值可以是文件名、编译选项、编译命令等。变量可以使 Makefile 更加灵活和可维护。
- 自定义变量:您可以在 Makefile 中定义自己的变量。例如:
CFLAGS = -g -Wall
在这个例子中,CFLAGS 是一个自定义变量,它的值是 -g -Wall。您可以在 Makefile 的其他地方使用这个变量,例如:
$(CC) $(CFLAGS) main.c
- 预定义变量:Makefile 中有一些预定义的变量,这些变量有一些默认的值,但您可以根据需要修改这些值。例如:
CC = gcc
在这个例子中,CC 是一个预定义变量,它的默认值是 cc,但在这里我们将它的值修改为 gcc。
- 特殊变量:Makefile 中还有一些特殊的变量,这些变量在规则中有特殊的含义。例如:
- $@:表示规则中的目标。
- $<:表示规则中的第一个依赖。
- $^:表示规则中的所有依赖。
- 获取变量的值:您可以使用 $(变量名) 或 ${变量名} 来获取变量的值。例如:
$(CC) $(CFLAGS) main.c
在这个例子中,$(CC) 和 $(CFLAGS) 分别会被替换为 CC 和 CFLAGS 变量的值。
3.3 在 Makefile 中使用模式规则和自动变量
在模式规则中,% 是一个通配符,它可以匹配任意字符串。在依赖列表和目标中的 % 必须匹配相同的字符串。例如,规则
%.o: %.c
gcc -c $< -o $@
表示任何 .o 文件都依赖于对应的 .c 文件,如果 .c 文件有更新,那么就执行命令 gcc -c $< -o @ 来重新生成 . o 文件。在这个命令中, @ 来重新生成 .o 文件。在这个命令中, @来重新生成.o文件。在这个命令中,< 和 $@ 是自动变量,它们在规则的命令中有特殊的含义:
- $<:表示第一个依赖文件,即对应的 .c 文件。
- $@:表示目标文件,即 .o 文件。
所以,这个命令等价于 gcc -c file.c -o file.o,其中 file 是 file.c 和 file.o 中的 file。
使用模式规则和自动变量,可以使 Makefile 更加简洁和通用。例如,您的 Makefile 可以简化为:
CC = gcc
%.o: %.c
$(CC) -c $< -o $@
objects = add.o div.o sub.o mult.o main.o
all: $(objects)
在这个 Makefile 中,all 是一个伪目标,它依赖于所有的 .o 文件。当执行 make 或 make all 命令时,make 工具会检查所有的 .o 文件,如果有任何 .o 文件的依赖 .c 文件有更新,那么就重新生成这个 .o 文件。
3.4 Makefile 中的函数
3.4.1 wildcard
wildcard 函数用于获取匹配指定模式的文件列表。这个函数非常有用,特别是在处理大量文件时,可以避免手动列出所有文件。
示例:
$(wildcard *.c ./sub/*.c)
wildcard 函数会返回当前目录下所有 .c 文件,以及 ./sub/ 目录下所有 .c 文件的列表。文件名之间用空格分隔。
这个函数通常用于生成文件列表,然后将这个列表赋值给一个变量,例如:
SOURCES = $(wildcard *.c ./sub/*.c)
在这个例子中,SOURCES 变量包含了所有 .c 文件的列表。
3.4.2 patsubst
patsubst 是 Makefile 中的一个非常有用的函数,它用于进行模式替换。
示例:
$(patsubst %.c, %.o, x.c bar.c)
patsubst 函数会将 x.c bar.c 中所有匹配 %.c 模式的单词替换为 %.o。在这个模式中,% 是一个通配符,它可以匹配任意长度的字符串。在替换模式 %.o 中,% 会被替换为原模式中 % 匹配的字符串。所以,这个函数的返回值是 x.o bar.o。
四、GDB调试:Linux开发者的必备工具
4.1 什么是GDB?
GDB,全名GNU Debugger,是GNU开源组织发布的一个强大的Unix下的程序调试工具。开发人员可以使用GDB对程序进行单步调试,查看程序运行时的内存状态,甚至可以改变程序执行时的环境变量,是Linux下开发者的必备工具。
一般来说,GDB 主要帮助你完成下面四个方面的功能:
- 启动程序,可以按照自定义的要求随心所欲的运行程序
- 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
- 当程序被停住时,可以检查此时程序中所发生的事
- 可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG
4.2 准备工作
在开始使用GDB之前,我们需要确保我们的程序是以调试模式编译的。在GCC编译器中,我们可以通过添加-g参数来启用调试模式。这个选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行。但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。
另外,我们也会打开-Wall选项,这个选项会让编译器显示所有的警告信息。这可以帮助我们发现许多潜在的问题,避免一些不必要的BUG。
gcc -g -Wall myprogram.c -o myprogram
这将生成一个包含调试信息且会显示所有警告的可执行文件myprogram。
4.3 GDB 命令
整理完了才发现有人整理好了,麻了。这篇博客有代码运行结果。
linux GDB调试工具常用知识点总结(如何debug你的代码)
4.3.1 GDB 命令 – 启动、退出、查看代码
在我们已经准备好了带有调试信息的可执行文件后,就可以开始使用GDB了。首先,我们需要启动GDB并加载我们的程序。这可以通过以下命令完成:
gdb myprogram
这将启动GDB,并加载myprogram这个可执行文件。
在GDB中,我们可以使用quit命令来退出GDB:
(gdb) quit
在开始调试之前,我们可能需要给我们的程序设置一些参数。我们可以使用set args命令来设置参数,使用show args命令来查看当前的参数:
(gdb) set args arg1 arg2 arg3
(gdb) show args
在GDB中,我们可以使用list(或者缩写l)命令来查看源代码。我们可以指定要查看的行号或函数名,也可以查看其他文件的代码:
(gdb) list 10
(gdb) list myfunction
(gdb) list myfile.c:10
(gdb) list myfile.c:myfunction
我们还可以使用show listsize和set listsize命令来查看和设置每次显示的代码行数:
(gdb) show listsize
(gdb) set listsize 20
4.3.2 GDB 命令 – 断点操作
在GDB中,我们可以使用断点(breakpoints)来控制我们的程序在哪里停止执行。当程序运行到一个断点时,它会停止执行,这样我们就可以查看此时的变量值,或者单步执行代码。
我们可以使用break命令来设置断点。我们可以指定要在哪个行号或函数上设置断点,也可以指定其他文件的代码:
(gdb) break 10
(gdb) break myfunction
(gdb) break myfile.c:10
(gdb) break myfile.c:myfunction
我们可以使用info break命令来查看当前设置的所有断点:
(gdb) info break
我们可以使用delete命令来删除一个断点。我们需要指定要删除的断点的编号,这个编号可以从info break命令的输出中找到:
(gdb) delete 1
我们还可以使用disable和enable命令来使一个断点无效或生效。这些命令也需要指定断点的编号:
(gdb) disable 1
(gdb) enable 1
在某些情况下,我们可能希望只在满足某个条件时才停止执行。这时,我们可以设置条件断点。例如,我们可以设置一个断点,只有当变量i等于5时才会停止执行:
(gdb) break 10 if i==5
4.3.3 GDB 命令 – 调试命令
在设置好断点后,我们就可以开始运行我们的程序了。我们可以使用start命令来开始运行程序,程序会停在第一行。我们也可以使用run命令来开始运行程序,程序会运行到第一个断点处停止:
(gdb) start
(gdb) run
在程序停止后,我们可以使用continue命令来继续运行程序,直到遇到下一个断点:
(gdb) continue
我们可以使用next命令来执行下一行代码。如果下一行代码是一个函数调用,next命令不会进入这个函数:
(gdb) next
我们可以使用print命令来查看一个变量的值,使用ptype命令来查看一个变量的类型:
(gdb) print myvar
(gdb) ptype myvar
我们可以使用step命令来单步执行代码。如果下一行代码是一个函数调用,step命令会进入这个函数:
(gdb) step
我们可以使用finish命令来跳出当前的函数:
(gdb) finish
我们可以使用display命令来自动显示一个变量的值。每次程序停止时,GDB都会显示这个变量的值:
(gdb) display myvar
还有info display命令用于查看当前所有设置为自动显示的变量。每次程序停止时,GDB都会显示这些变量的值。这个命令不需要任何参数:
(gdb) info display
而undisplay命令用于取消自动显示一个变量。我们需要指定要取消显示的变量的编号,这个编号可以从info display命令的输出中找到:
(gdb) undisplay 1
这样,编号为1的变量就不会在每次程序停止时自动显示了。
我们可以使用set var命令来改变一个变量的值:
(gdb) set var myvar=10
我们可以使用until命令来跳出当前的循环:
(gdb) until
4.4 面试中可能会提到的问题
在面试中,你可能会被问到一些关于GDB的问题,例如:
- 如何在GDB中查看变量的值?
- 如何在GDB中改变变量的值?
- 如何在GDB中查看函数的调用栈?
对于这些问题,你可以使用print命令来查看和改变变量的值,使用backtrace命令来查看函数的调用栈。
例如,假设你有一个变量名为i,你想将变量i的值改为10,你可以使用以下命令:
(gdb) print i=10
这将会改变变量i的值,并输出新的值。
在GDB中,你可以使用backtrace命令(或者它的简写形式bt)来查看函数的调用栈。例如:
(gdb) backtrace
这将会输出当前的函数调用栈,从当前函数开始,一直到main函数。每一行代表一个函数调用,包括函数名和调用该函数的源代码行数。
五、文件IO
在编程中,我们经常需要与文件进行交互,无论是读取文件中的数据,还是将数据写入到文件中。在Linux系统中,文件IO操作是非常重要的一部分,它涉及到许多底层的概念和操作。所以接下来我将详细介绍文件IO的相关知识,包括标准C库IO函数,Linux系统IO函数,以及相关的数据结构和操作函数。
5.1 标准C库IO函数
在C语言中,我们可以使用标准库中的函数来进行文件IO操作。这些函数包括fopen、fclose、fread、fwrite、fseek等。这些函数提供了一种高级的、抽象的方式来进行文件IO操作,使得我们可以不用关心底层的细节。
例如,我们可以使用以下代码来读取一个文件中的数据:
FILE *fp = fopen("myfile.txt", "r");
if (fp == NULL) {
printf("Failed to open file\n");
return -1;
}
char buffer[1024];
size_t n = fread(buffer, 1, sizeof(buffer), fp);
if (n < sizeof(buffer)) {
if (feof(fp)) {
printf("End of file reached\n");
} else if (ferror(fp)) {
printf("Error occurred\n");
}
}
fclose(fp);
在这段代码中,我们首先使用fopen函数打开一个文件,然后使用fread函数读取文件中的数据,最后使用fclose函数关闭文件。如果在读取过程中发生了错误,我们可以使用feof和ferror函数来检查是什么原因。
5.2 FILE结构体
这些函数都是通过一个FILE结构体指针来操作文件的,这个结构体包含了文件的描述符、读写指针的位置以及IO缓冲区等信息。
5.2.1 文件描述符
- 定义:
文件描述符是一个整型值,它是Linux系统用来访问和管理文件的一种机制,是操作系统为每个打开的文件所分配的一个唯一标识。每个进程在启动时都会默认打开三个文件描述符,分别是0(标准输入(stdin))、1(标准输出(stdout))和2(标准错误(stderr))。 - 使用:
在Linux系统中,所有的输入/输出操作都可以通过文件描述符来完成。例如,我们可以使用read()和write()函数来读取或写入一个文件描述符所代表的文件。文件描述符也可以用于网络通信,例如在使用socket()函数创建一个网络连接时,会返回一个文件描述符。
在C库函数中,我们不直接使用文件描述符,而是使用FILE结构体指针。当我们使用fopen函数打开一个文件时,这个函数会返回一个FILE结构体指针,这个指针就包含了文件描述符。 - 管理:
Linux系统提供了一系列的系统调用来管理文件描述符,例如open()、close()、dup()和dup2()等。这些函数可以用来打开或关闭一个文件描述符,或者复制一个文件描述符。 - 特性:
文件描述符具有一些重要的特性,例如它们是进程级别的资源,每个进程都有自己的文件描述符表;文件描述符在父进程和子进程之间是可以共享的;文件描述符可以用来实现进程间的通信等
5.2.2 文件读写指针
文件读写指针表示了当前读写操作的位置。当我们使用fread或fwrite函数读写文件时,这个指针会自动向前移动。我们也可以使用fseek函数来手动移动这个指针,或者使用ftell函数来获取这个指针的当前位置。
5.2.3 IO缓冲区
IO缓冲区是一块内存区域,它用于暂存从文件中读取的数据或者待写入文件的数据。当我们使用fread或fwrite函数进行IO操作时,数据会先被读取到缓冲区,或者从缓冲区写入到文件。这样可以减少直接对磁盘的操作,提高IO效率。
缓冲区的大小默认为8192字节,但是我们可以使用setvbuf函数来设置缓冲区的大小。当缓冲区满时,数据会被自动刷新到磁盘。我们也可以使用fflush函数来手动刷新缓冲区。
5.2.4关闭文件
当我们完成了对文件的操作后,需要使用fclose函数来关闭文件。这个函数会刷新缓冲区,释放FILE结构体指针,以及关闭文件描述符。如果我们在main函数中打开了一个文件,但是没有使用fclose函数来关闭它,那么当main函数返回(return)或者调用exit函数时,这个文件会被自动关闭。
在C库函数中,文件的打开、读写和关闭都是通过FILE结构体指针来进行的。这个结构体封装了文件描述符、读写指针的位置以及IO缓冲区等信息,使得我们可以更方便地进行文件IO操作。
5.3 标准C库IO和Linux系统IO的关系
虽然标准C库和Linux系统IO都提供了进行文件IO操作的接口,但它们的工作方式和级别有所不同,各有其优势。
标准C库IO提供了一套高级的输入/输出接口,如fopen(), fclose(), fread(), fwrite(), fgets(), fputs()等函数。这些函数操作的是文件流(FILE*),并且提供了缓冲机制。当我们使用这些函数进行读写操作时,数据首先会被读写到用户空间的缓冲区(如buf1, buf2)。当缓冲区满或者手动调用fflush()函数时,数据才会被写入到磁盘。这种缓冲机制可以减少系统调用的次数,提高IO效率。
然而,标准C库IO实际上是基于Linux系统的IO函数实现的。也就是说,当我们调用fopen(), fread()等函数时,底层实际上是调用了Linux系统的open(), read()等函数。
Linux系统IO提供了一套更底层的接口,如open(), close(), read(), write()等函数。这些函数直接操作文件描述符(file descriptor),并且不提供用户级别的缓冲。每次调用read()或write()函数,都会直接进行系统调用,将数据读写到内核空间的缓冲区,然后再由内核将数据写入到磁盘或从磁盘读取数据。这种方式提供了更多的控制和灵活性,但也更加底层和复杂。
总的来说,标准C库IO和Linux系统IO各有其优势。标准C库IO提供了更高级别的抽象和更方便的接口,以及缓冲机制来提高IO效率。而Linux系统IO则提供了更多的控制和灵活性,允许进行更底层的操作,如非阻塞IO,多路复用等。在实际的编程中,我们可以根据需要选择使用哪种IO接口。
5.4 虚拟地址空间
在Linux系统中,每个进程都有自己的虚拟地址空间。虚拟地址空间是一个连续的地址区域,它被分为几个不同的段,包括代码段、数据段、堆、栈和文件映射区等。
当我们打开一个文件并读取数据时,数据实际上是被读取到进程的虚拟地址空间中。同样,当我们写入数据到文件时,数据实际上是从进程的虚拟地址空间中写入的。
5.5 文件描述符表
在Linux系统中,每个进程都有一个文件描述符表,这个表中存储了进程当前打开的所有文件的信息。当我们打开一个文件时,系统会在这个表中添加一个新的条目,并返回这个条目的索引,这个索引就是文件描述符。
5.6 Linux系统IO函数
Linux系统提供了一系列的IO函数,包括open、read、write、lseek和close等。
- open函数用于打开一个文件,它返回一个文件描述符。我们可以指定文件的打开模式,例如只读、只写或读写等。
- read函数用于从一个文件中读取数据,它需要一个文件描述符和一个缓冲区作为参数。它返回实际读取的字节数。
- write函数用于向一个文件中写入数据,它需要一个文件描述符和一个缓冲区作为参数。它返回实际写入的字节数。
- lseek函数用于改变文件的当前位置,它需要一个文件描述符和一个偏移量作为参数。它返回新的文件位置。
- close函数用于关闭一个文件,它需要一个文件描述符作为参数。
5.7 stat结构体
stat结构体用于存储文件的状态信息,包括文件的大小、权限、所有者、创建时间等。我们可以使用stat函数来获取一个文件的状态信息。
stat结构体定义如下:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
5.8 st_mode变量
st_mode变量是stat结构体中的一个成员,它表示文件的权限。我们可以使用一些宏来检查文件的类型和权限,例如S_ISREG、S_ISDIR、S_IRUSR、S_IWUSR等。
5.9 文件属性操作函数
Linux系统提供了一些函数来获取和修改文件的属性,例如stat、fstat、lstat、chmod、chown等。
- stat函数用于获取一个文件的状态信息,它需要一个文件名和一个stat结构体作为参数。
- fstat函数用于获取一个打开的文件的状态信息,它需要一个文件描述符和一个stat结构体作为参数。
- lstat函数类似于stat函数,但是如果文件是一个符号链接,它会返回符号链接本身的状态信息,而不是链接的目标的状态信息。
- chmod函数用于修改一个文件的权限,它需要一个文件名和一个权限模式作为参数。
- chown函数用于修改一个文件的所有者和所属的组,它需要一个文件名、一个用户ID和一个组ID作为参数。
5.10 目录操作函数
Linux系统提供了一些函数来操作目录,例如opendir、readdir、closedir等。
- opendir函数用于打开一个目录,它返回一个目录流。
- readdir函数用于读取目录流中的下一个目录项,它返回一个dirent结构体。
- closedir函数用于关闭一个目录流。
5.11 目录遍历函数
我们可以使用opendir和readdir函数来遍历一个目录中的所有文件。例如,以下代码可以打印出一个目录中的所有文件名:
DIR *dir = opendir(".");
if (dir == NULL) {
printf("Failed to open directory\n");
return -1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
printf("%s\n", entry->d_name);
}
closedir(dir);
在这段代码中,我们首先使用opendir函数打开一个目录,然后使用readdir函数读取目录中的每一个文件,最后使用closedir函数关闭目录。
5.12 dirent结构体和d_type
dirent结构体用于表示一个目录项,它包括文件的名字和类型等信息。我们可以使用readdir函数来获取一个dirent结构体。
dirent结构体定义如下:
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported
by all file system types */
char d_name[256]; /* filename */
};
d_type变量表示文件的类型,它可以是DT_REG(普通文件)、DT_DIR(目录)、DT_LNK(符号链接)等。
5.13 dup、dup2函数
dup和dup2函数用于复制一个文件描述符。它们都返回一个新的文件描述符,这个文件描述符和原来的文件描述符指向同一个文件。
- dup函数返回的文件描述符总是最小的未使用的文件描述符。例如,如果我们打开了文件描述符3和4,然后关闭了文件描述符3,那么dup(4)会返回3。
- dup2函数可以指定新的文件描述符的值。如果这个值已经被使用,那么dup2函数会先关闭这个文件描述符,然后再复制。例如,dup2(4, 3)会先关闭文件描述符3,然后将文件描述符4复制到文件描述符3。
5.14 fcntl函数
fcntl函数用于操作文件描述符。它可以用来获取和设置文件描述符的属性,例如非阻塞模式、关闭执行标志等。
例如,我们可以使用以下代码来设置一个文件描述符为非阻塞模式:
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
在这段代码中,我们首先使用F_GETFL命令获取文件描述符的当前标志,然后使用F_SETFL命令设置新的标志。