文章目录
- 1. GCC
- 2. 静态库&&动态库
- 3. makefile
- 4. GDB调试
- 5. 文件IO
牛客网C++高级项目课程
1. GCC
1.1 gcc概念
-
GCC(GNU Compiler Collection,GNU编译器套件)是由GNU开发的编程语言编译器。GNU还有其他语言,例如Java、Go的编译器套件,也包括了这些语言的库
-
GCC不仅支持C,还可以区分不同的C语言标准,支持用命令行选项来控制编译器在编译源代码时应该遵循哪个C标准。例如,当使用命令行参数
-std=c99
启动 GCC 时,编译器支持 C99 标准。 -
安装命令
sudo apt install gcc g++ (版本 > 4.8.5)
- c11之后的特性,高版本才能支持
-
查看标准可以使用如下命令
gcc/g++ -v/--version
1.2 编程语言的发展
1.3 gcc工作流程
-
预处理:头文件包含、宏替换、条件编译、去除注释、添加行号。
-
typedef 和 define 的区别,
- 宏定义只是简单的字符串代换,是在预处理完成的,
- 而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能
1.4 常见指令选项
预处理:指令-E:
gcc test.c -E -o test.i
编译:指令-S
gcc test.i -S -o test.s
汇编:指令-c
gcc test.s -s -o test.o
- 注意:
-s
后生成的.o文件本身也可以是执行文件
链接:指令-o
gcc test.c -o test
指定宏:-D 不加空格直接加宏
- 主要是为了便于调试,在测试时加宏进行logger输出,上线发布时不输出
优化-On
- 例如对下列程序优化,反汇编保证安全
其他:
1.5 gcc 和 g++ 的区别
误区一: gcc 只能编译 c 代码, g++ 只能编译 c++ 代码。两者都可以,请注意:
- 后缀为 .c 的, gcc 把它当作是 C 程序,而 g++ 当作是 c++ 程序
- 后缀为 .cpp 的,两者都会认为是 C++ 程序, C++ 的语法规则更加严谨一些
- 编译阶段, g++ 会调用 gcc,对于 C++ 代码,两者是等价的,但是因为 gcc命令不能自动和 C++ 程序使用的库联接,所以通常用 g++ 来完成链接,为了统一起见,干脆编译/链接统统用 g++ 了,这就给人一种错觉,好像 cpp 程序只能用 g++ 似的
误区二: gcc 不会定义 __cplusplus 宏,而 g++ 会
- 实际上,这个宏只是标志着编译器将会把代码按 C 还是 C++ 语法来解释
- 如上所述,
- 如果后缀为 .c,并且采用 gcc 编译器,则该宏就是未定义的,
- 否则,后缀是cpp,采用gcc编译,识别C++语法,就会对宏进行定义
误区三:编译只能用 gcc,链接只能用 g++
- 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用
gcc/g++,而链接可以用 g++ 或者 gcc -lstdc++。 - gcc 命令不能自动和C++程序使用的库联接,所以通常使用 g++ 来完成联接。
但在编译阶段, g++ 会自动调用 gcc,二者等价
2. 静态库&&动态库
2.1 库的概念
- 库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
- 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
- 库文件有两种,静态库和动态库(共享库),区别是:
- 静态库在程序的链接阶段被复制到了程序中;
- 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
- 库的好处:
- 1.代码保密
- 2.方便部署和分发
2.2 静态库的制作
命名规则
- Linux : libxxx.a
- lib : 前缀(固定)
- xxx : 库的名字,自己起
- .a : 后缀(固定)
- Windows : libxxx.lib
制作
- gcc 获得 .o 文件
- 将 .o 文件打包,使用 ar 工具(archive)
ar rcs libxxx.a xxx.o xxx.o
r – 将文件插入备存文件中
c – 建立备存文件
s – 索引
代码示例
- 1.先准备源文件
- 2.通过gcc -c 指令获得.o二进制文件,不进行链接
- 3.打包成静态库
2.3 静态库的使用
- 创建工程项目结构
- 将生成的静态库放进
lib
目录下
- 编译主函数
main.c
文件- 默认加载头文件在
main.c
的当前目录中,因放到其他文件中,故要使用-I
指定头文件 - 使用
-l
指定加载的库名 - 使用
-L
指定加载库的路径
- 默认加载头文件在
#include <stdio.h>
#include "head.h"
int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
- 执行
2.4 动态库的制作
命名规则
制作
-fpic
用于编译阶段,产生的代码没有绝对地址,全部用相对地址,这正好满足了共享库的要求,共享库被加载时地址不是固定的。- 如果不加
-fpic
,那么生成的代码就会与位置有关,当进程使用该.so文件时都需要重定位,且会产生成该文件的副本,每个副本都不同,不同点取决于该文件代码段与数据段所映射内存的位置。
2.5 动态库使用
- 直接加载动态库,会有问题
2.5.1 动态库加载失败原因
(1)工作原理
- 静态库: GCC 进行链接时,会把静态库中代码打包到可执行程序中
- 动态库: GCC 进行链接时,动态库的代码不会被打包到可执行程序中只是提供一些文件信息给可执行程序
- 程序启动之后,动态库会被动态加载到内存中再进行执行
(2)ldd
查看动态库依赖关系
通过 ldd (list dynamicdependencies)
命令检查动态库依赖关系
(3)如何定位共享库文件呢?
- 当系统加载可执行代码时候,能够知道其所依赖的库的名字,
- 但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。即,动态库不能直接打包到可执行程序内存中,而是在运行的时候,通过动态载入器去指定绝对路径下寻找,再加载到内存中
- 对于
elf
格式的可执行程序,是由ld-linux.so
来完成的,它按照顺序先后搜索路径,找到库文件后将其载入内存,找不到再继续寻找其他地址,寻址顺序如下:elf
文件的DT_RPATH
段 (虚拟地址空间)——> 环境变量LD_LIBRARY_PATH
——>/etc/ld.so.cache
文件列表 ——>/lib/, /usr/lib
目录
2.5.2 解决动态库加载问题
方式1:临时修改环境变量
env
查看系统环境变量
export
配置环境变量
echo
打印配置的环境变量
- 再次查看动态库依赖关系,发现可以找到
方式2:永久配置环境变量
vim .bashrc
修改用户级配置文件
- 在配置文件中添加内容:
- 使配置文件生效
source ~/.bashrc
sudo vim /etc/profile
修改系统配置文件
方式3:修改 /etc/ld.so.cache
文件列表
方式4:将库添加到 /lib/, /usr/lib
目录下(不建议)
- 因为此目录下存放的基本是系统的库文件,如果自定义库文件名称相同,会产生冲突或覆盖原系统库文件,造生编译出现一些问题(类似于jvm的双亲委派机制的沙箱安全)
2.6 静态库与动态库的对比
2.6.1 程序编译执行过程回顾
2.6.2 静态库制作过程回顾
2.6.3 动态库制作过程回顾
- 因只有在运行时,再动态加载到内存中,内存的位置是预先不能确定的,所有加
fpic
2.6.4 静态库与动态库的优缺点
静态库优缺点
动态库优缺点
- 因为静态库与可执行文件绑定,一旦原程序修改,不仅需要重新制作动态库,还需要重新将整个程序分发、部署
- 而动态库与程序分离(程序里只有动态库名称),更新程序后,只需重新制作动态库,重新发送给对方进行替换(对方服务端的的动态库环境变量已经设定好了,运行时可以直接找到)
- 动态库分发时要和程序一块发布,静态库只需要分发程序即可(静态库已经包含在程序中)
3. makefile
3.1 概念
- 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中;
- Makefile 文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作;
- 因为 Makefile 文件就像一个 Shell 脚本一样,也可以执行操作系统的命令。
- Makefile 带来的好处就是“自动化编译” ,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。
- make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如 Delphi 的 make, Visual C++ 的 nmake, Linux 下 GNU 的 make。
示例:
Redis中的makefile
,安装redis软件时,用的就是make
命令,会编译redis
中的源文件形成可执行的redisr软件程序
3.2 makefile
文件命名和规则
命名和规则
示例
- 1.
vim makefile
创建和编译makefile
文件
make
命令进行编译执行
3.3 makefile
工作原理
工作原理:
- 命令在执行之前,需要先检查规则中的依赖是否存在
- 如果存在,执行命令
- 如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
- 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
- 如果依赖的时间比目标的时间晚,需要重新生成目标
- 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行
示例:
- 只修改
main.c
,再编译
3.4 变量
自定义变量
变量名=变量值 var=hello
预定义变量
AR : 归档维护程序的名称,默认值为 ar
CC : C 编译器的名称,默认值为 cc
CXX : C++ 编译器的名称,默认值为 g++
$@ : 获取目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件
获取变量的值
$(变量名)
# 自定义变量
str=hello
# 获取自定义变量值
$(str)
自动变量只能在规则的命令中使用
app:main.c a.c b.c
gcc -c main.c a.c b.c -o app
# 自动获取变量
app:main.c a.c b.c
$(CC) -c $^ -o $@
3.5 模式匹配
%
通配符
示例:
- 原
makefile
文件:
app:sub.o add.o mult.o div.o main.o
gcc sub.o add.o mult.o div.o main.o -o app
sub.o:sub.c
gcc -c sub.c -o sub.o
add.o:add.c
gcc -c add.c -o add.o
mult.o:mult.c
gcc -c mult.c -o mult.o
div.o:div.c
gcc -c div.c -o div.o
main.o:main.c
gcc -c main.c -o main.o
- 使用自定义变量和模式匹配后的
makefile
文件
src=sub.o add.o mult.o div.o main.o
target=app
$(target):$(src)
$(CC) $(src) -o $(target)
%.o:%.c
$(CC) -c $< -o $@
3.6 函数
函数调用的格式如下:
$(function arguments)
- 这里
function
是函数名,arguments
是该函数的参数。 - 参数和函数名之间是用空格或 Tab 隔开,如果有多个参数,它们之间用逗号隔开。这些空格和逗号不是参数值的一部分。
$(wildcard PATTERN...)
- 功能:获取指定目录下指定类型的文件列表
- 参数: PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
- 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
示例:
$(wildcard *.c ./sub/*.c)
返回值格式: a.c b.c c.c d.c e.c f.c
$(patsubst pattern,replacement,text)
- 功能:寻找
text
中符合格式pattern
的字,用replacement
替换它们。
pattern
和replacement
中可以使用通配符。 pattern
可以包括通配符%
,表示任意长度的字串。如果replacement
中也包含%
,那么,replacement
中的这个%
将是pattern
中的那个%
所代表的字串。 (可以用\
来转义,以\%
来表示真实含义的%
字符)- 返回:函数返回被替换过后的字符串
示例:
$(patsubst %.c, %.o, x.c bar.c)
返回值格式: x.o bar.o
makefile
最终编写如下
#定义变量
src=$(wildcard ./*.c) # 获取指定路径下的文件
objs=$(patsubst %.c, %.o, $(src)) # 将获取的文件名.c替换为.o文件
target=app # 生成的目标文件
$(target):$(objs)
$(CC) $(objs) -o $(target)
%.o:%.c # 通配符:依赖所有的.c文件生成.o文件
$(CC) -c $< -o $@ # $< :依赖第一个文件,$@: 目标完整文件名
.PHONY:clean # 清理文件
clean:
rm $(objs) -f
3.7 清除文件
- 当外面有
clean
文件时,作为目标会进行更新检查,由于没有依赖,clean
文件一直最新,就不会去执行 - 解决办法是使用
.PHONY:clean
定义成伪目标,不会生成clean文件,就不会去与clean
文件去对比,就会执行make clean
命令删除文件
.PHONY:clean # 定义成伪目标,不会生成clean文件
clean: # 可以不用依赖 make clean
rm $(objs) -f
4. GDB调试
4.1 GDB简介
GDB
是由GNU
软件系统社区提供的调试工具,同GCC
配套组成了一套完整的开发环境,GDB
是 Linux 和许多类 Unix 系统中的标准开发环境。- 一般来说,
GDB
主要帮助你完成下面四个方面的功能:- 启动程序,可以按照自定义的要求随心所欲的运行程序
- 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
- 当程序被停住时,可以检查此时程序中所发生的事
- 可以改变程序,将一个
BUG
产生的影响修正从而测试其他 BUG
4.2 调试前的准备工作
-
通常,在为调试而编译时,
- 我们会()关掉编译器的优化选项(
-O
), - 并打开调试选项(
-g
)。 - 另外,
-Wall
在尽量不影响程序行为的情况下选项打开所有warning
,也可以发现许多问题,避免一些不必要的BUG
。
例如:
gcc -g -Wall program.c -o program
- 我们会()关掉编译器的优化选项(
-
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb
能找到源文件。
4.3 GDB命令-启动、退出、查看代码
测试代码如下:
#include <stdio.h>
#include <stdlib.h>
int test(int a);
int main(int argc, char* argv[]) {
int a, b;
printf("argc = %d\n", argc);
if(argc < 3) {
a = 10;
b = 30;
} else {
a = atoi(argv[1]);
b = atoi(argv[2]);
}
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", a + b);
for(int i = 0; i < a; ++i) {
printf("i = %d\n", i);
// 函数调用
int res = test(i);
printf("res value: %d\n", res);
}
printf("THE END !!!\n");
return 0;
}
// 求和操作
int test(int a) {
int num = 0;
for(int i = 0; i < a; ++i) {
num += i;
}
return num;
}
启动退出
gdb 可执行程序
quit
- 生成可执行文件时,加上调试信息
-g
gdb xxx
启动调试程序
给程序设置参数/获取设置参数
set args 10 20
show args
GDB 使用帮助
help 命令
查看当前文件代码
list/l (从默认位置显示)
list/l 行号 (从指定的行显示)
list/l 函数名(从指定的函数显示)
-
list
测试 -
默认每次显示10行,输入
list
或回车键,默认继续往下显示内容 -
list 行号
测试 -
list 函数名
测试
查看非当前文件代码
list/l 文件名:行号
list/l 文件名:函数名
设置显示的行数
show list/listsize
set list/listsize 行数
4.4 GDB 命令 – 断点操作
设置断点
b/break 行号
b/break 函数名
b/break 文件名:行号
b/break 文件名:函数
查看断点
i/info b/break
删除断点
d/del/delete 断点编号
设置断点无效
dis/disable 断点编号
设置断点生效
ena/enable 断点编号
设置条件断点(一般用在循环的位置)
b/break 10 if i==5
4.5 GDB 命令 – 调试命令
运行GDB程序
start # 程序停在第一行
run # 遇到断点才停
继续运行,到下一个断点停
c/continue
向下执行一行代码(不会进入函数体)
n/next
变量操作
p/print 变量名 # 打印变量值
ptype 变量名 #打印变量类型
向下单步调试(遇到函数进入函数体)
s/step
finish #跳出函数体
自动变量操作
display 变量名 # 自动打印指定变量的值
i/info display
undisplay 编号
其它操作
set var 变量名=变量值 # 循环中用的较多
until # 跳出循环
5. 文件IO
5.1 标准 C 库 IO 函数
- C库的IO函数可以实现跨平台,是因为每个系统都有IO的API接口,会与C库的IO连接上
- C库的效率会高些,因为有缓存区
FILE源码解析
- 进入C库的_IO_FILE结构体定义
- 定义了读写指针
- 定义了文件描述符
- 定义了读写指针
- 文件描述符怎么定义的?
- 标准C库的fopen函数,会调用linux系统的open接口,系统得到fd文件标识,再赋值给C库的文件描述符,从而通过C库的文件描述符找到系统文件
linux中查看C库
- C库一般在3库
man 3 fopen
缓冲区
- 降低写磁盘次数,提高效率
- 何时刷新缓冲区-见下图
- 缓冲区数量
- 在linux系统的IO操作中没有缓冲区,为了提高实时性例如在网络通信时,需要用到linux系统的IO函数
5.2 标准 C 库 IO 和 Linux 系统 IO 的关系
5.3 虚拟地址空间
为何设计虚拟地址空间
操作系统如何管理虚拟地址
linux系统虚拟地址空间设计
5.4 文件描述符
- 文件描述符表,用数组实现,可以管理多个文件描述符,进而管理多个文件
- 每个进程的文件描述符表数组大小默认是1024,因此能够同时打开的个数是1024
5.5 Linux系统的IO函数
open
和 close
函数
man 2 open
查看api
- 2章是linux库函数,3章是标准C库函数
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open 打开文件
- 函数声明
int open(const char *pathname, int flags);
- 函数说明
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 打开一个已经存在的文件
int open(const char *pathname, int flags);
参数:
- pathname:要打开的文件路径
- flags:对文件的操作权限设置还有其他的设置,以宏的形式定义
O_RDONLY, O_WRONLY, O_RDWR 这三个设置是互斥的
返回值:返回一个新的文件描述符,如果调用失败,返回-1
errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。
#include <stdio.h>
void perror(const char *s);
作用:打印errno对应的错误描述
s参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述)
- 示例
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 打开一个文件
int fd = open("a.txt", O_RDONLY);
if(fd == -1) {
perror("open"); // 打印错误信息
}
// 读写操作
// 关闭
close(fd);
return 0;
}
open 打开并创建文件
- 函数声明
int open(const char *pathname, int flags, mode_t mode);
- 函数说明
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname:要创建的文件的路径
- flags:对文件的操作权限和其他的设置
- 必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的
- 可选项:O_CREAT 文件不存在,创建新文件
- mode:八进制的数,表示创建出的新的文件的操作权限,比如:0777
最终的权限是:mode & ~umask(普通用户:0022):最终变成 0775
0777 -> 111111111
& 0775 -> 111111101
----------------------------
111111101
按位与:0和任何数都为0
umask的作用就是抹去某些权限。
flags参数是一个int类型的数据,占4个字节,32位。
flags 32个位,每一位就是一个标志位。
- 示例
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建一个新的文件
int fd = open("create.txt", O_RDWR | O_CREAT, 0777); // flags按位或,将每位的1合并,达到同时满足多个权限的作用
if(fd == -1) {
perror("open");
}
// 关闭
close(fd);
return 0;
}
read
和 write
函数
read
函数
- 函数声明
ssize_t read(int fd, void *buf, size_t count);
- 函数解释
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:需要读取数据存放的地方,数组的地址(传出参数)
- count:指定读取数据的大小
- 存放数据时超过这个大小,就截断
- 可以不用读满这个数组大小
整体解释:从文件描述符 fd 中读取 count 字节的数据并放入从 buf 开始的缓冲区中
返回值:
- 成功:
>0: 返回实际的读取到的字节数
=0:文件已经读取完了
- 失败:-1 ,并且设置errno
write
函数
- 函数声明
ssize_t write(int fd, const void *buf, size_t count);
- 函数解释
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:要往磁盘写入的数据,数组,一个无类型的指针buf,是一个缓冲区
- count:要写的数据的实际的大小(你要写入文件的大小)
整个函数解释:向文件描述符 fd 所引用的文件中写入从 buf 开始的缓冲区中 count 字节的数据
返回值:
成功:实际写入的字节数
失败:返回-1,并设置errno
文件拷贝代码示例
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
// 1.通过open打开english.txt文件
int srcfd = open("english.txt", O_RDONLY);// 当前的相对路径,只读
if(srcfd == -1) { // 源文件描述符
perror("open");
return -1;
}
// 2.创建一个新的文件(拷贝文件)
// 对文件可写,可以创建,文件的不同用户操作权限
int destfd = open("cpy.txt", O_WRONLY | O_CREAT, 0664);
if(destfd == -1) { // 目标文件描述符
perror("open");
return -1;
}
// 3.频繁的读写操作
char buf[1024] = {0}; // 缓冲流数组大小
int len = 0;
while((len = read(srcfd, buf, sizeof(buf))) > 0) { // 读取成功,一直循环读取下去
write(destfd, buf, len); // 读到多少数据,就写多少数据
}
// 4.关闭文件-关闭文件描述符,就释放了对应的内存资源
close(destfd);
close(srcfd);
return 0;
}
lseek
函数
- 函数声明
标准C库的函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
Linux系统函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
- 函数解释
标准C库的函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
Linux系统函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
- fd:文件描述符,通过open得到的,通过这个fd操作某个文件
- offset:偏移量
- whence:
SEEK_SET
设置文件指针的偏移量:从文件开始设置的偏移量
SEEK_CUR
设置偏移量:当前位置 + 第二个参数offset的值
SEEK_END
设置偏移量:文件大小 + 第二个参数offset的值
返回值:返回文件指针的位置
作用:
1.移动文件指针到文件头-可以实现从头反复读的作用
lseek(fd, 0, SEEK_SET);
2.获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);
3.获取文件长度-找到文件某尾位置
lseek(fd, 0, SEEK_END);
4.拓展文件的长度,当前文件10b, 要扩展到110b, 增加了100个字节
lseek(fd, 100, SEEK_END)
注意:需要写一次数据
- 代码示例
- 扩展文件长度
- 作用场景:下载内容时,由于下载数据过大,如5G,下载期间,磁盘被占用,剩下空间不够5G,会下载出错,使用
lseek
,可以在下载前,就在磁盘中分配扩展5G的空间,先占用着,保证下载不会出错。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("hello.txt", O_RDWR);
if(fd == -1) {
perror("open");
return -1;
}
// 扩展文件的长度
int ret = lseek(fd, 100, SEEK_END);
if(ret == -1) {
perror("lseek");
return -1;
}
// 写入一个空数据-才能触发文件大小的扩展
write(fd, " ", 1);
// 关闭文件
close(fd);
return 0;
}
stat
和 lstat
函数
stat 函数
- 函数声明
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
- 函数解释
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
作用:获取一个文件相关的一些信息,放入 statbuf 数组中
参数:
- pathname:操作的文件的路径
- statbuf:stat结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
成功:返回0
失败:返回-1 设置errno
stat 结构体
stat 文件名
查看文件状态
stat
结构体定义
st_mode 变量
- 查找判断文件权限:因为只有一个标志位,直接与宏作
&
操作,1为true,0为false - 查找判断文件类型:是与一个掩码
&
操作,获取掩码后,再与具体的宏文件进行比对判断,如下图
- 代码测试
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct stat statbuf;
int ret = stat("a.txt", &statbuf);
if(ret == -1) {
perror("stat");
return -1;
}
printf("size: %ld\n", statbuf.st_size);
return 0;
}
lstat
函数
int lstat(const char *pathname, struct stat *statbuf);
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
成功:返回0
失败:返回-1 设置errno
- 作用:查看软链接文件信息,因为直接查看软链接文件,会直接打开链接的目标文件
案例-实现ls -l
指令功能
linux文件类型
switch(st.st_mode & S_IFMT) {
case S_IFLNK:
perms[0] = 'l'; // 链接文件
break;
case S_IFDIR:
perms[0] = 'd'; // 目录文件
break;
case S_IFREG:
perms[0] = '-'; // 普通文件
break;
case S_IFBLK:
perms[0] = 'b'; // 块设备文件
break;
case S_IFCHR:
perms[0] = 'c'; // 字符设备文件
break;
case S_IFSOCK:
perms[0] = 's'; // 套接字文件
break;
case S_IFIFO:
perms[0] = 'p'; // 管道文件
break;
default:
perms[0] = '?';
break;
}
getpwuid
函数获取文件所有者信息
// 文件所有者- getpwuid函数根据id获取结构体指针->取出名称属性
char * fileUser = getpwuid(st.st_uid)->pw_name;
// 文件所在组
char * fileGrp = getgrgid(st.st_gid)->gr_name;
ctime
时间格式转换
// 获取修改的时间-ctime:将秒数转化为本地时间格式-默认带回车换行
char * time = ctime(&st.st_mtime);
// 将最后一个回车换行符去掉
char mtime[512] = {0};
strncpy(mtime, time, strlen(time) - 1);
sprintf
格式化输出内容
// 将信息输出
char buf[1024];
sprintf(buf, "%s %d %s %s %ld %s %s", perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);
printf("%s\n", buf);
整体代码与测试
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>
// 模拟实现 ls -l 指令
// -rw-rw-r-- 1 nowcoder nowcoder 12 12月 3 15:48 a.txt
int main(int argc, char * argv[]) {
// 判断输入的参数是否正确
if(argc < 2) {
printf("%s filename\n", argv[0]);
return -1;
}
// 通过stat函数获取用户传入的文件的信息
struct stat st;
int ret = stat(argv[1], &st);
if(ret == -1) {
perror("stat");
return -1;
}
// 获取文件类型和文件权限
char perms[11] = {0}; // 用于保存文件类型和文件权限的字符串
switch(st.st_mode & S_IFMT) {
case S_IFLNK:
perms[0] = 'l'; // 链接文件
break;
case S_IFDIR:
perms[0] = 'd'; // 目录文件
break;
case S_IFREG:
perms[0] = '-'; // 普通文件
break;
case S_IFBLK:
perms[0] = 'b'; // 块设备文件
break;
case S_IFCHR:
perms[0] = 'c'; // 字符设备文件
break;
case S_IFSOCK:
perms[0] = 's'; // 套接字文件
break;
case S_IFIFO:
perms[0] = 'p'; // 管道文件
break;
default:
perms[0] = '?';
break;
}
// 判断文件的访问权限
// 文件所有者
perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-';
perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';
// 文件所在组
perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';
// 其他人
perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';
// 硬连接数
int linkNum = st.st_nlink;
// 文件所有者- getpwuid函数根据id获取结构体指针->取出名称属性
char * fileUser = getpwuid(st.st_uid)->pw_name;
// 文件所在组
char * fileGrp = getgrgid(st.st_gid)->gr_name;
// 文件大小
long int fileSize = st.st_size;
// 获取修改的时间-ctime:将秒数转化为本地时间格式-默认带回车换行
char * time = ctime(&st.st_mtime);
// 将最后一个回车换行符去掉
char mtime[512] = {0};
strncpy(mtime, time, strlen(time) - 1);
// 将信息输出
char buf[1024];
sprintf(buf, "%s %d %s %s %ld %s %s", perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);
printf("%s\n", buf);
return 0;
}
5.6 文件属性操作函数
access 函数
- 声明和解释
#include <unistd.h>
int access(const char *pathname, int mode);
作用:判断某个文件是否有某个权限,或者判断文件是否存在
参数:
- pathname: 判断的文件路径
- mode:
R_OK: 判断是否有读权限
W_OK: 判断是否有写权限
X_OK: 判断是否有执行权限
F_OK: 判断文件是否存在
返回值:成功返回0, 失败返回-1
- 测试
#include <unistd.h>
#include <stdio.h>
int main() {
int ret = access("a.txt", F_OK);
if(ret == -1) {
perror("access");
}
printf("文件存在!!!\n");
return 0;
}
chmod 函数
- 声明和解释
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
修改文件的权限
参数:
- pathname: 需要修改的文件的路径
- mode:需要修改的权限值,八进制的数
返回值:成功返回0,失败返回-1
- 代码示例
#include <sys/stat.h>
#include <stdio.h>
int main() {
int ret = chmod("a.txt", 0777);
if(ret == -1) {
perror("chmod");
return -1;
}
return 0;
}
chown 函数
- 声明和解释
int chown(const char *path, uid_t owner, gid_t group);
- 查看用户组名
vim etc/passwd
vim etc/group
truncate 函数
- 函数声明与解释
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
作用:缩减或者扩展文件的尺寸至指定的大小
参数:
- path: 需要修改的文件的路径
- length: 需要最终文件变成的大小
返回值:
成功返回0, 失败返回-1
- 测试
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
int ret = truncate("b.txt", 5);
if(ret == -1) {
perror("truncate");
return -1;
}
return 0;
}
5.7 目录操作函数
mkdir 函数
- 函数声明与解释
/*
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
作用:创建一个目录
参数:
pathname: 创建的目录的路径
mode: 权限,八进制的数
返回值:
成功返回0, 失败返回-1
*/
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
int ret = mkdir("aaa", 0777);
if(ret == -1) {
perror("mkdir");
return -1;
}
return 0;
}
rmdir 函数
int rmdir(const char *pathname);
rename 函数
/*
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
*/
#include <stdio.h>
int main() {
int ret = rename("aaa", "bbb");
if(ret == -1) {
perror("rename");
return -1;
}
return 0;
}
chdir 函数
/*
#include <unistd.h>
int chdir(const char *path);
作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
参数:
path : 需要修改的工作目录
#include <unistd.h>
char *getcwd(char *buf, size_t size);
作用:获取当前工作目录
参数:
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
返回值:
返回的指向的一块内存,这个数据就是第一个参数
*/
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
// 获取当前的工作目录
char buf[128];
getcwd(buf, sizeof(buf));
printf("当前的工作目录是:%s\n", buf); // /lesson14
// 修改工作目录
int ret = chdir("/home/nowcoder/Linux/lesson13");
if(ret == -1) {
perror("chdir");
return -1;
}
// 创建一个新的文件-更改后的工作目录下创建的文件
int fd = open("chdir.txt", O_CREAT | O_RDWR, 0664);
if(fd == -1) {
perror("open");
return -1;
}
close(fd);
// 获取当前的工作目录-更改后的工作目录
char buf1[128];
getcwd(buf1, sizeof(buf1));
printf("当前的工作目录是:%s\n", buf1); // /lesson13
return 0;
}
*getcwd 函数
- 类似于
pwd
指令
#include <unistd.h>
char *getcwd(char *buf, size_t size);
作用:获取当前工作目录
参数:
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
返回值:
返回的指向的一块内存,这个数据就是第一个参数
5.8 目录遍历函数
opendir
打开目录
// 打开一个目录
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数:
- name: 需要打开的目录的名称
返回值:
DIR * 类型,理解为目录流
错误返回NULL
readdir
读取目录
// 读取目录中的数据
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 参数:dirp是opendir返回的结果
- 返回值:
struct dirent,代表读取到的文件的信息
读取到了末尾或者失败了,返回NULL
closedir
关闭目录
// 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
案例-获取某个目录下文件个数
/*
// 打开一个目录
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数:
- name: 需要打开的目录的名称
返回值:
DIR * 类型,理解为目录流
错误返回NULL
// 读取目录中的数据
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 参数:dirp是opendir返回的结果
- 返回值:
struct dirent,代表读取到的文件的信息
读取到了末尾或者失败了,返回NULL
// 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
*/
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int getFileNum(const char * path);
// 读取某个目录下所有的普通文件的个数
int main(int argc, char * argv[]) {
if(argc < 2) {
printf("%s path\n", argv[0]);
return -1;
}
int num = getFileNum(argv[1]); // 用于递归遍历每个子目录文件
printf("普通文件的个数为:%d\n", num);
return 0;
}
// 用于获取目录下所有普通文件的个数
int getFileNum(const char * path) {
// 1.打开目录
DIR * dir = opendir(path);
// 打开目录出错-终止程序-不是递归终止
if(dir == NULL) {
perror("opendir");
exit(0);
}
struct dirent *ptr;
// 记录普通文件的个数
int total = 0;
while((ptr = readdir(dir)) != NULL) { // 递归终止:读到末尾或错误
// 获取名称
char * dname = ptr->d_name;
// 忽略掉. 和..
if(strcmp(dname, ".") == 0 || strcmp(dname, "..") == 0) {
continue;
}
// 判断是否是普通文件还是目录
if(ptr->d_type == DT_DIR) {
// 目录,需要继续读取这个目录
char newpath[256];
sprintf(newpath, "%s/%s", path, dname);// 目录拼接
total += getFileNum(newpath); // 加上递归子目录的文件个数
}
if(ptr->d_type == DT_REG) {
// 普通文件
total++;
}
}
// 关闭目录
closedir(dir);
return total;
}
5.9 文件复制函数
dup
复制文件描述符
#include <unistd.h>
int dup(int oldfd);
作用:复制一个新的文件描述符
fd=3, int fd1 = dup(fd),
fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
- 代码测试
/*
#include <unistd.h>
int dup(int oldfd);
作用:复制一个新的文件描述符
fd=3, int fd1 = dup(fd),
fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
*/
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main() {
int fd = open("a.txt", O_RDWR | O_CREAT, 0664);
int fd1 = dup(fd);
if(fd1 == -1) {
perror("dup");
return -1;
}
printf("fd : %d , fd1 : %d\n", fd, fd1);
close(fd);
char * str = "hello,world";
int ret = write(fd1, str, strlen(str));// fd文件也被写入信息
if(ret == -1) {
perror("write");
return -1;
}
close(fd1);
return 0;
}
dup2
重定向文件描述符
#include <unistd.h>
int dup2(int oldfd, int newfd);
作用:重定向文件描述符
oldfd 指向 a.txt, newfd 指向 b.txt
调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt
oldfd 必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没有做
- 代码示例
/*
#include <unistd.h>
int dup2(int oldfd, int newfd);
作用:重定向文件描述符
oldfd 指向 a.txt, newfd 指向 b.txt
调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt
oldfd 必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没有做
*/
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
// 1. 先创建两个文件,返回两个文件描述符
int fd = open("1.txt", O_RDWR | O_CREAT, 0664);
if(fd == -1) {
perror("open");
return -1;
}
int fd1 = open("2.txt", O_RDWR | O_CREAT, 0664);
if(fd1 == -1) {
perror("open");
return -1;
}
printf("fd : %d, fd1 : %d\n", fd, fd1);
// 2.将fd1 文件描述符重定向到fd指向的文件:1.txt
int fd2 = dup2(fd, fd1);
if(fd2 == -1) {
perror("dup2");
return -1;
}
// 3.通过fd1去写数据,实际操作的是1.txt,而不是2.txt
char * str = "hello, dup2";
int len = write(fd1, str, strlen(str));
if(len == -1) {
perror("write");
return -1;
}
printf("fd : %d, fd1 : %d, fd2 : %d\n", fd, fd1, fd2);
close(fd);
close(fd1);
return 0;
}
5.10 fcntl 函数
- 常见功能
int fcntl(int fd, int cmd, ... /* arg */ );
1.复制文件描述符
2.设置/获取文件的状态标志
- 函数解释
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd: 表示对文件描述符进行如何操作
- F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
int ret = fcntl(fd, F_DUPFD);
- F_GETFL : 获取指定的文件描述符文件状态flag
获取的flag和我们通过open函数传递的flag是一个东西。
- F_SETFL : 设置文件描述符文件状态flag
必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
可选性:O_APPEND, O)NONBLOCK
O_APPEND 表示追加数据
NONBLOK 设置成非阻塞
阻塞和非阻塞:描述的是函数调用的行为。
- 代码示例
/*
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd: 表示对文件描述符进行如何操作
- F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
int ret = fcntl(fd, F_DUPFD);
- F_GETFL : 获取指定的文件描述符文件状态flag
获取的flag和我们通过open函数传递的flag是一个东西。
- F_SETFL : 设置文件描述符文件状态flag
必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
可选性:O_APPEND, O)NONBLOCK
O_APPEND 表示追加数据
NONBLOK 设置成非阻塞
阻塞和非阻塞:描述的是函数调用的行为。
*/
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {
// 1.复制文件描述符
// int fd = open("1.txt", O_RDONLY);
// int ret = fcntl(fd, F_DUPFD);
// 2.修改或者获取文件状态flag
int fd = open("1.txt", O_RDWR);// 可读可写权限
if(fd == -1) {
perror("open");
return -1;
}
// 获取文件描述符状态flag
int flag = fcntl(fd, F_GETFL);
if(flag == -1) {
perror("fcntl");
return -1;
}
flag |= O_APPEND; // flag = flag | O_APPEND
// 修改文件描述符状态的flag,给flag加入O_APPEND这个标记
int ret = fcntl(fd, F_SETFL, flag);
if(ret == -1) {
perror("fcntl");
return -1;
}
char * str = "nihao";
write(fd, str, strlen(str));
close(fd);
return 0;
}