找装修公司的网站,河北省建设工程管理信息网官网,上海网站建设管理,西安网app第一阶段知识总结
lunix系统操作
1、基础命令
#xff08;1#xff09;cd
cd /[目录名] 打开指定文件目录 cd .. 返回上一级目录 cd - 返回并显示上一次目录 cd ~ 切换到当前用户的家目录 #xff08;2#xff09;pwd
pwd 查看当前所在目录路径 pwd -L 打印当前物理…第一阶段知识总结
lunix系统操作
1、基础命令
1cd
cd /[目录名] 打开指定文件目录 cd .. 返回上一级目录 cd - 返回并显示上一次目录 cd ~ 切换到当前用户的家目录 2pwd
pwd 查看当前所在目录路径 pwd -L 打印当前物理路径
3ls
ls 查看当前目录下的文件和目录 注部分系统文件和目录会用不同颜色显示 ls [目录名] 显示目录下的文件(需要有查看目标目录的权限) ls -a 显示全部文件(包括文件名以“.”开头的隐藏文件) ls -alh ls -alh 查看时显示详细信息 d 指该内容为目录 x 指该内容为可执行 r 指可读 w 指可写 . 指当前目录 .. 指上一级目录
4touch touch [文件名] 新建文件 5rm rm [文件名] 删除文件 r 强制删除 f 允许删除目录 6mv mv [文件原名] [新的文件名] 重命名文件|目录 7mkdir mkdir [目录名] 新建目录 mkdir -p [目录名]/[目录名]/…… 将不存在的目录全部创建 mkdir -v [目录名] 创建时打印目录信息 mkdir -m [目录名] 设置目录权限 8rmdir rmdir [目录名] 删除目录只能删除空目录 rmdir /s [目录名] 删除非空目录 9cp cp [被复制的文件名] [新的文件名] 复制文件|目录 cp -i [被复制的文件名] [新的文件名] 若新文件重名系统会询问是否覆盖默认覆盖 cp -n [被复制的文件名] [新的文件名] 若新文件重名系统不会覆盖 cp -u [被复制的文件名] [新的文件名] 若新文件重名只有被复制的文件的时间属性比重名文件新的时候才覆盖 cp -p [被复制的文件名] [新的文件名] 连同文件属性一起复制默认只复制内容 10vi vi [文件名] 编写文件 按“i”键后可以开始修改内容按之前只有delete键有效按之后delete失效backspace有效 编写时按“Esc”键退出编写之后按“shift”“”输入命令 w filename 保存 wq 保存并退出 q! 不保存强制退出 x 执行、保存并退出
11cat cat [文件名] 读取文件 12echo echo “[输入内容]” [文件名] 清空文件并输入内容
13chmod chmod 是 Unix 和 Linux 系统中的命令用于更改文件或目录的权限。权限定义了哪些用户可以对文件或目录执行哪些操作。 chmod 命令的基本语法如下
chmod [选项] 模式 文件名 使用符号模式你可以通过添加、删除-或设置权限来修改文件或目录的权限。权限符号可以是 r读、w写或 x执行。
14man 在使用 man 命令时可以在命令后面加上一个数字以指定查看哪个手册页面节man page section。这个数字告诉系统应该搜索哪个手册页面的部分因为一个命令或函数可能在不同的上下文中有多个手册页面。在 Unix 和类 Unix 系统中手册页一般被分成以下几个节sections General commands (通用命令)主要包含系统管理员和普通用户可以使用的命令。 例如man 1 printf 可以查看 printf 命令的手册页面。 System calls (系统调用)这些是操作系统提供的服务和功能的编程接口。 例如man 2 open 可以查看 open 系统调用的手册页面。 Library functions (库函数)包括标准 C 库和其他库的函数。 例如man 3 strlen 可以查看 strlen 函数的手册页面。 Special files (特殊文件)通常是设备文件和文件系统。 例如man 4 tty 可以查看关于 tty 特殊文件的手册页面。 File formats and conventions (文件格式和约定)包括配置文件和文件格式的描述。 例如man 5 passwd 可以查看 passwd 文件的手册页面。 Games (游戏)关于游戏的手册页面。 例如man 6 tetris 可以查看关于 tetris 游戏的手册页面。 Miscellaneous (杂项)其他的手册页面。 例如man 7 regex 可以查看关于正则表达式的手册页面。 System administration commands (系统管理命令)主要用于系统管理员的命令和工具。 例如man 8 iptables 可以查看关于 iptables 命令的手册页面。 2、C语言环境下的相关命令
1gcc
gcc [文件名] 编译C语言文件生成a.out执行文件 gcc -g [文件名] 编译C语言文件生成可调试的a.out执行文件 2./
./[文件名] 运行文件
注不需要空格 3、DOS通用注意点
1--help “命令 --help”为该命令的帮助文档 2sudo sudo [指令] 以管理员身份执行某一指令(需输入密码) sudo passwd root 修改管理员用户密码且之后带sudo的命令不需要再输入密码 su 登陆管理员账户 4、makefile
1编译步骤及原理 1、预编译 gcc -E [文件名.c] - [预处理文件名.i]编译前的一些工作生成.i文件 作用展开头文件宏替换去掉注释条件编译。 .i文件是用于c语言的.ii是用于c语言 2、编译 gcc -S [预处理文件名.i]生成对应的汇编文件生成.s文件 作用将代码转成汇编代码。 3、汇编 gcc -c [编译文件名.s]生成对应的二进制文件生成.o文件 作用将汇编代码转成机器码 4、链接 gcc [汇编文件名.o] 作用 将所有的.o文件链接成a.out文件。 2makefile核心目的 makefile文件指导系统如何编辑节省大项目局部修改后的编译时间。局部修改后之后修改处需要重新编译。 如果一个文件中有makefile和Makefile文件在命令行输入make优先执行makefile。如果执行大写的Makefile需要加上-fmake -f Makefile。
3makefile运行逻辑
4makefile基本语法 #[目标]:[依赖1] [依赖2] …
# [命令1] [命令2] …
#例如
main:main.ogcc main.o -o main 目标: 一般是指要编译的目标也可以是一个动作 依赖: 指执行当前目标所要依赖的选项。包括其他目标某个具体文件或库等一个目标可以有多个依赖。 命令:该目标下要执行的具体命令,可以没有也可以有多条。
5makefile编译流程
main:main.o myAdd.o myMinus.o myMulti.o myDiv.ogcc main.o myAdd.o myMinus.o myMulti.o myDiv.o -o main# -c 生成二进制文件 -o 指定输出的文件名
myAdd.o:myAdd.cgcc -c myAdd.c -o myAdd.omyMinus.o:myMinus.cgcc -c myMinus.c -o myMinus.omyMulti.o:myMulti.cgcc -c myMulti.c -o myMulti.omyDiv.o:myDiv.cgcc -c myDiv.c -o myDiv.omain.o:main.cgcc -c main.c -o main.oclean:rm -rf *.o main 注表示执行但不输出这条命令
在终端输入 make 从上至下后执行文件命令 输入 make [目标] 仅执行对应命令 6makefile变量及文件精简过程
6.1 $
# 针对上一张的图片用$(CC)替换gcc命令
main:main.o myAdd.o myDiv.o myMinus.o myMulti.o$(CC) $^ -o $
myAdd.o:myAdd.c$(CC) -c $^ -o $
myMinus.o:myMinus.c$(CC) -c $^ -o $
myMulti.o:myMulti.c$(CC) -c $^ -o $
myDiv.o:myDiv.c$(CC) -c $^ -o $
main.o:main.c$(CC) -c $^ -o $
# 用$(RM)替换rm命令
clean:$(RM) *.o main
6.4 自定义常量 # 变量自定义赋值
OBJSmain.o myAdd.o myDiv.o myMinus.o myMulti.o
TARGETmain
# 变量取值用$()
$(TARGET):$(OBJS)$(CC) $^ -o $
myAdd.o:myAdd.c$(CC) -c $^ -o $
myMinus.o:myMinus.c$(CC) -c $^ -o $
myMulti.o:myMulti.c$(CC) -c $^ -o $
myDiv.o:myDiv.c$(CC) -c $^ -o $
main.o:main.c$(CC) -c $^ -o $
clean:$(RM) *.o $(TARGET) 表示目标文件的完整名称。 # 针对上一小节的图片用$替换目标文件
main:main.o myAdd.o myDiv.o myMinus.o myMulti.ogcc main.o myAdd.o myDiv.o myMinus.o myMulti.o -o $myAdd.o:myAdd.cgcc -c myAdd.c -o $myMinus.o:myMinus.cgcc -c myMinus.c -o $myMulti.o:myMulti.cgcc -c myMulti.c -o $myDiv.o:myDiv.cgcc -c myDiv.c -o $main.o:main.cgcc -c main.c -o $clean:rm -rf *.o main 6.2 $^ 表示所有不重复的依赖文件 # 针对上一张的图片用$^替换依赖文件
main:main.o myAdd.o myDiv.o myMinus.o myMulti.ogcc $^ -o $myAdd.o:myAdd.cgcc -c $^ -o $myMinus.o:myMinus.cgcc -c $^ -o $myMulti.o:myMulti.cgcc -c $^ -o $myDiv.o:myDiv.cgcc -c $^ -o $main.o:main.cgcc -c $^ -o $clean:rm -rf *.o main 6.3 系统常量 RM删除 CCC语言编译程序 [常量名][值]赋予自定义常量值 $() 取自定义变量的值 编辑
6.5 makefile伪目标 .PHONY: [目标] 当只想执行目标命令而不希望生成目标文件时使用 注:后有个空格 OBJSmain.o myAdd.o myDiv.o myMinus.o myMulti.o
TARGETmain$(TARGET):$(OBJS)$(CC) $^ -o $myAdd.o:myAdd.c$(CC) -c $^ -o $myMinus.o:myMinus.c$(CC) -c $^ -o $myMulti.o:myMulti.c$(CC) -c $^ -o $myDiv.o:myDiv.c$(CC) -c $^ -o $main.o:main.c$(CC) -c $^ -o $# 伪目标(伪文件)指执行命令不生成文件
.PHONY: cleanclean:$(RM) *.o main
6.6 模式匹配 %[目标]:%[依赖] 匹配目录下所有符合命令的文件批量执行命令 OBJS$(patssubst %.c, %.o, $(wildcard ./*.c)) TARGETmain
$(TARGET):$(OBJS) $(CC) $^ -o $
# 模式匹配 %[目标]:%[依赖] %.o:%.c $(CC) -c $^ -o $ .PHONY: clean
clean: $(RM) *.o main 6.7 总代码
OBJS$(patsubst %.c, %.o, $(wildcard ./*.c))
# 变量定义赋值
TARGETmain
LDFLAGS-L./src_so -L./src_a
LIBS-lMyAdd -lMyDiv
SO_DIR./src_so
A_DIR./src_a
#变量取值用$()
$(TARGET):$(OBJS)$(CC) $^ $(LIBS) $(LDFLAGS) -o $
# 模式匹配: %目标:%依赖
%.o:%.c$(CC) -c $^ -o $
all:make -C $(SO_DIR)make -C $(A_DIR)
# 伪目标/伪文件
.PHONY: clean
clean:$(RM) $(OBJS) $(TARGET)make -C $(SO_DIR) cleanmake -C $(A_DIR) clean# wildcard : 匹配文件 (获取指定目录下所有的.c文件)
# patsubst : 模式匹配与替换 指定目录下所有的.c文件替换成.o文件
show:echo $(wildcard ./*.c)echo $(patsubst %.c, %.o, $(wildcard ./*.c)) 7makefile动态库与静态库 7.1 动态库 作用用于打包整合所有的 .c 源文件同时使用者也无法通过动态库还原代码 注windows 中是 .dll 文件linux 中是 .so 文件 1、生成 .o 二进制文件 gcc -c -fPIC myAdd.c -o myadd.o
- 2、生成动态库- gcc -shared myAdd.o -o libMyAdd.so
- **可放在一起**gcc -shared -fPIC myAdd.c -o libMyAdd.so 使用动态库 gcc -ImyAdd -L./src.so -o main libMyAdd.so由 lib 函数名 .so 组成 myAdd是函数名 src.so是动态库文件目录 提供给客户的只有libMyAdd.so和MyAdd.h文件 注上图myadd中a没有大写导致报错 3、复制到没有 .c 源文件的文件夹下 4、生成main.o文件 gcc *.c -lMyAdd -L./src_so -o main 5、运行main.o文件 动态库内部makefile 运行前先执行内部makefile生成动态库 然后再外部调用动态库
OBJS$(patsubst %.c, %.o, $(wildcard ./*.c)) TARGETlibMyAdd.so
PATHS/usr/lib/
$(TARGET):$(OBJS) $(CC) -shared -fPIC $^ -o $ cp $(TARGET) $(PATHS)
%.o:%.c $(CC) -c $^ -o $
clean: $(RM) $(OBJS) $(TARGET)
show: echo $(RM) echo $(OBJS)
7.2 静态库 以下是使用静态库的基本步骤 创建静态库 通常你会有一系列的源文件.c 或 .cpp 等这些源文件会被编译成目标文件。然后这些目标文件会被打包成一个静态库文件。在Linux中这通常是一个以 .a 为扩展名的文件。 例如假设你有两个源文件 file1.c 和 file2.c你可以这样创建静态库
gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
ar rcs libmystatic.a file1.o file2.o// rcs 换成 -r 也行 这里ar 命令用于创建静态库rcs 是其选项表示替换现有的库文件r创建库文件c并且指定库文件的索引s。 使用静态库 当你有一个或多个源文件需要使用静态库中的代码时你需要在编译和链接阶段指定这个静态库。链接器会将静态库中的目标文件与你的源文件编译出的目标文件合并生成最终的可执行文件。 例如假设你有一个源文件 main.c它调用了静态库 libmystatic.a 中的函数。你可以这样编译和链接
gcc main.c -L. -lmystatic -o myprogram 这里-L. 告诉链接器在当前目录. 表示当前目录中查找库文件-lmystatic 指定链接到 libmystatic.a 库注意链接时不需要库文件的前缀 lib 和扩展名 .a。 静态库内部makefile 运行前先执行内部makefile生成动态库 然后再外部调用动态库
OBJS$(patsubst %.c, %.o, $(wildcard ./*.c))
TARGETlibMyDiv.a
$(TARGET):$(OBJS)$(AR) -r $(TARGET) $^
# 模式匹配
%.o:%.c$(CC) -c $^ -o $
clean:$(RM) $(OBJS) $(TARGET)
7.3 动态库与静态库外部makefile 采用make -C 命令就可以执行库里面内部的makefile可以不用先在内部执行生成库文件。 先输入make all命令执行all中的代码生成库 再输入make 或者直接一条命令make all make。
OBJS$(patsubst %.c, %.o, $(wildcard ./*.c))
# 变量定义赋值
TARGETmain
LDFLAGS-L./src_so -L./src_a
LIBS-lMyAdd -lMyDiv
SO_DIR./src_so
A_DIR./src_a
#变量取值用$()
$(TARGET):$(OBJS)$(CC) -g $^ -o $
# 模式匹配: %目标:%依赖
%.o:%.c$(CC) -g -c $^ -o $
all:make -C $(SO_DIR)make -C $(A_DIR)
# 伪目标/伪文件
.PHONY: clean
clean:$(RM) $(OBJS) $(TARGET)# wildcard : 匹配文件 (获取指定目录下所有的.c文件)
# patsubst : 模式匹配与替换 指定目录下所有的.c文件替换成.o文件
show:echo $(wildcard ./*.c)echo $(patsubst %.c, %.o, $(wildcard ./*.c))
7.4 区别 链接方式 静态库在编译时被链接到程序中而动态库在运行时被加载到内存中。 使用静态库的程序在编译时会将库的内容直接合并到最终的可执行文件中而使用动态库的程序则会在运行时根据需要从库中加载所需的代码。 更新和维护 静态库如果需要更新库中的代码必须重新编译并重新链接所有依赖于该库的程序。 动态库可以独立更新而不需要重新编译程序这使得库的维护更加方便。动态库更适合多文件场合。
8Makefile的常用选项 -f file指定Makefile文件。默认情况下make会在当前目录中查找名为GNUmakefile、makefile或Makefile的文件作为输入。使用-f选项你可以指定其他名称的文件作为Makefile。 -v显示make工具的版本号。 -n只输出命令但不执行。这个选项通常用于测试Makefile查看make会执行哪些命令而不真正执行它们。 -s只执行命令但不显示具体命令。这跟makefile中的命令行符号作用一样。这个选项在需要执行命令但不需要看到详细输出时很有用。 -w显示执行前和执行后的路径。 -C dir指定Makefile所在的目录。如果Makefile不在当前目录中可以使用这个选项来指定Makefile的目录。
9makefile中shell的使用 所有在命令行输入的命令都是shell命令
9.1直接执行shell命令 在Makefile的规则中直接写shell命令并在命令前加上$(shell ...)或者反引号...来执行。例如
FILES text.txt
A$(shell ls ./)
B$(shell pwd)
C$(shell if [ ! -f $(FILE) ]; then touch $(FILE); fi;)show: echo $(A)echo $(B)echo $(C) 在这个例子中$(shell ls ./)会执行ls ./命令并将结果文件显示输出以及创建test.txt文件。然后在show规则的命令部分我们使用echo来打印这些文件名。 9.2 shell 中 -f 与 -d 指令 在Unix和Linux shell中-f 和 -d 是用于测试文件类型的条件表达式也称为测试运算符。这些通常与 if 语句或 while 循环等控制结构一起使用以根据文件的存在和类型来执行不同的操作。 -f 测试 -f 测试用于检查指定的路径是否为一个常规文件即不是目录、设备文件、符号链接等。 示例
if [ -f /path/to/file ]; then echo The path is a regular file.
else echo The path is not a regular file.
fi -d 测试 -d 测试用于检查指定的路径是否为一个目录。 示例
if [ -d /path/to/directory ]; then echo The path is a directory.
else echo The path is not a directory.
fi 在上面的示例中如果指定的路径是一个常规文件那么 -f 测试将返回真true并且会执行 then 部分的代码。如果指定的路径是一个目录那么 -d 测试将返回真并执行相应的代码。
10 makefile条件判断 Makefile支持使用条件语句来根据某些条件执行不同的shell命令。这通常使用ifeq、ifneq、ifdef、ifndef等指令来实现。例如
OS $(shell uname -s) ifeq ($(OS), Linux) CC gcc
else CC clang
endif all: $(CC) -o myprogram myprogram.c 在这个例子中我们首先使用$(shell uname -s)来获取操作系统类型并将其赋值给变量OS。然后我们使用ifeq来判断OS的值如果是Linux则使用gcc作为编译器否则使用clang。最后在all规则的命令部分我们使用选定的编译器来编译程序。
11 makefile命令行参数
12Makefile中install 源码安装不通过apt-get install安装。
12.1 功能作用 创建目录将可执行文件拷贝到指定目录(安装目录) 加全局可执行的路径 加全局的启停脚本 cp一行将生成的目标拷贝至该文件路径中 sudo一行是软链接
linux设备启动时会将这些文件启动
12.2 主要目的 Makefile中的install目标的主要目的是提供一个标准化的方式来安装编译后的程序、库、文档以及其他相关文件到用户的系统上。当开发者构建了一个软件项目后他们通常希望用户能够轻松地将其安装到他们的系统上并使其能够正常运行。install目标就是用来完成这一任务的。 具体来说install目标通常会执行以下操作 复制文件将编译后的可执行文件、库文件、头文件等复制到指定的安装目录。这些目录通常是系统级的目录如/usr/local/bin用于存放可执行文件/usr/local/lib用于存放库文件等。 设置权限确保复制的文件具有正确的权限以便用户可以正常访问和使用它们。 创建目录如果需要install目标还可以创建必要的目录结构以便将文件放置到正确的位置。 安装文档除了程序本身install目标还可能包括安装相关的文档、手册页等。 执行其他安装步骤根据项目的具体需求install目标还可以包含其他必要的安装步骤如创建配置文件、设置环境变量等。 可以将文件做成全局的比如自实现mycp命令做成全局后可以在任意位置使用mycp命令
12.3 软链接与硬链接 Linux软链接Symbolic Link是一种特殊的文件类型它可以创建一个指向另一个文件或目录的链接。软链接不是实际的文件或目录而是一个指向实际文件或目录的指针。当我们访问软链接时实际上是访问被链接的文件或目录。 软链接在Linux系统中非常常见并且被广泛应用于各种场景。其主要特点和应用包括 快速访问文件当某个文件位于深层次的目录中时可以通过创建软链接到其他位置来方便快速访问。 管理共享库在Linux系统中软链接常用于管理共享库。通过创建共享库的软链接可以实现不同版本之间的切换和共存。 创建快捷方式软链接可以被视为Linux系统中的快捷方式它允许用户为常用文件或目录创建一个指向它的链接从而方便快速访问。 创建软链接的常用方法是使用 ln 命令具体语法为 “ln -s target source” 其中 “target” 表示目标文件夹即被指向的文件夹而 “source” 表示当前目录的软连接名即源文件夹。 通过软链接指令生成的软链接文件mycp生成的文件属性为软链接 实体没有了那只是快捷方式因此在使用mycp命令会显示没有该文件 12.4 ln -sv命令 ln -sv 命令在 Linux 中用于创建符号链接软链接。这里的 -s 表示创建软链接而 -v 表示详细模式verbose即会显示创建的链接的详细信息。 具体解释如下 ln: 这是链接命令用于创建链接。 -s: 表示创建软链接符号链接。如果不加 -s那么默认创建的是硬链接。 -v: 详细模式会显示命令执行过程中的信息例如正在创建哪个链接。 例如假设你有一个文件叫做 original.txt并且你想要为它创建一个名为 link.txt 的软链接你可以使用以下命令 ln -sv original.txt link.txt 执行这条命令后你会看到类似以下的输出 link.txt - original.txt 这意味着 link.txt 现在是一个指向 original.txt 的软链接。之后如果你通过 link.txt 访问文件实际上你会访问到 original.txt。
12.5 软链接makefile操作
OBJS$(patsubst %.cpp, %.o, $(wildcard ./*.cpp))
# 变量定义赋值
TARGETmycp
LDFLAGS-L./src_so -L./src_a
LIBS-lMyAdd -lMyDiv
SO_DIR./src_so
A_DIR./src_a
PATHS/tmp/demoMain/
BIN/usr/local/bin/
#变量取值用$()
$(TARGET):$(OBJS)$(CXX) $^ -o $
# 模式匹配: %目标:%依赖
%.o:%.cpp$(CXX) -c $^ -o $
all:make -C $(SO_DIR)make -C $(A_DIR)
install:$(TARGET)if [ -d $(PATHS) ]; \then echo $(PATHS) exist; \else \mkdir $(PATHS); \cp $(TARGET) $(PATHS); \sudo ln -sv $(PATHS)$(TARGET) $(BIN); \fi
# 伪目标/伪文件
.PHONY: clean
clean:$(RM) $(OBJS) $(TARGET)make -C $(SO_DIR) cleanmake -C $(A_DIR) clean# wildcard : 匹配文件 (获取指定目录下所有的.c文件)
# patsubst : 模式匹配与替换 指定目录下所有的.c文件替换成.o文件
show:echo $(wildcard ./*.cpp)echo $(patsubst %.cpp, %.o, $(wildcard ./*.cpp))
12.6 软硬链接的区别
软链接与硬链接Hard Link有所不同。硬链接是直接指向文件的物理位置。而软链接则是指向文件名的路径如果原始文件被移动、重命名或删除软链接将会失效即所谓的“死链接”。此外软链接可以跨越不同的文件系统而硬链接只能在同一文件系统内使用。
5、Gdb调试
1基本调试步骤 1、gdb [文件名] 调试文件需事先 gcc -g .c文件 2、gdb a.out 3、run 4、bt 若没有-g就直接编译了使用gdb就会出现以下信息没有bug信息 注若报错 ‘gdb’ not found输入指令 apt-get install gdb 一直回车即可
2常用gdb调试命令
常用指令全称指令效果b [代码行数]break在第几行添加断点如果在指定文件打断点则b 指定文件 行号info binfo break显示所有断点的信息一般按自然数排序del [断点编号]delete删除断点示例del 56 删除编号为56的断点disdisable禁用断点enaenable启用断点p [变量]print查询值包括以下形式varyvary*ptrbuffer[0]run/执行程序直到结束或断点nnext执行下一条语句会越过函数sstep执行下一条语句会进入函数ccontinue继续执行程序直到遇到下一个断点call/直接调用函数并查看其返回值qquit退出 gdb 当前调试btbacktrace查看函数的栈调用fframe到指定的栈帧配合bt使用。 显示当前选中的堆栈帧的详细信息包括帧编号、地址、函数名以及源代码位置where/显示当前线程的调用堆栈跟踪信息ptype/查看变量类型thread/切换指定线程set br br/将信息按标准格式输出这样信息就显示不乱set args/设置程序启动命令行参数show args/查看设置的命令行参数llist显示源代码。watch/监视某一变量或内存地址的值是否发生变化uuntil运行程序直到退出当前循环。 快速跳过循环的剩余迭代以便更快地到达循环之后的代码。fifinish执行完当前函数的剩余部分并停止在调用该函数的地方。 示例finish 执行完当前函数的剩余部分。return/结束当前调用函数并返回指定值到上一层函数调用处display/每次程序停止时自动打印变量的值。 示例display name 每次程序停止时自动打印 name 的值。undisplay/取消之前用 display 命令设置的自动打印。jjump使程序跳转到指定的位置继续执行。dir/重定向源码文件的位置source/读取并执行(加载)一个包含GDB命令的脚本文件set/set variablenewvalue修改变量的值 3调试详解
3.1 call命令 call func显示地址信息因为是函数名指向函数地址。 call func( )无参调用显示函数的返回值 call add(100200 )有参调用返回300。 3.2 gdb attach attach命令用于将一个正在运行的进程附加到GDB调试器中以便你可以对该进程进行调试。这对于调试那些已经启动并且你希望动态地分析其行为的进程非常有用。 使用attach命令的基本语法是 gdb attach 进程ID 这里的进程ID是你想要附加的进程的ID。你可以通过ps - ef命令或者其他系统工具来获取进程ID。 一旦进程被附加到GDB你就可以使用GDB提供的各种命令来调试该进程了比如设置断点、单步执行、查看变量值等。 需要注意的是当你附加到一个进程时该进程会暂时被暂停执行直到你在GDB中继续执行它。此外如果你尝试附加到一个没有调试信息的进程比如没有编译为带调试信息的版本你可能无法查看所有的源代码和变量信息。
3.3 core文件 Core文件是Unix或Linux系统下程序崩溃时生成的内存映像文件主要用于对程序进行调试。当程序出现内存越界、bug或者由于操作系统或硬件的保护机制导致程序异常终止时或者段错误时操作系统会中止进程并将当前内存状态导出到core文件中。这个文件记录了程序崩溃时的详细状态描述程序员可以通过分析core文件来找出问题所在。 在Linux系统中你可以使用GDBGNU调试器来调试core文件。GDB提供了一系列命令 backtrace或简写为bt来查看运行栈 frame或简写为f来切换到特定的栈帧 info来查看栈帧中的变量和参数信息 print来打印变量的值等 从而定位和解决问题。 下图中将出现的段错误导入至core文件中。随后执行gdb 文件 core文件
3.4 gdb中的堆栈帧 在GDB中堆栈stack是用于存储函数调用时局部变量、返回地址等信息的一段连续的内存空间。当我们在GDB中查看堆栈信息时通常会看到一系列的堆栈帧stack frames每一个堆栈帧对应一个函数调用。这些堆栈帧按照函数调用的顺序排列最新的调用在最顶部而较早的调用则位于下方。 每个堆栈帧都包含函数名、参数和返回地址等信息。 3.5 backtrace 与 frame 命令
3.6 print 命令 和 ptype 命令 a) p 输出 b) ptype ptype 是一个在 GDBGNU 调试器中使用的命令用于查看变量的类型。ptype 命令允许你在调试过程中查看某个变量的数据类型。 使用 ptype 命令的基本语法如下 ptype 变量名 例如如果你有一个名为 my_variable 的变量并想要查看它的类型你可以在 GDB 中输入 ptype my_variable GDB 会返回该变量的类型信息。 此外ptype 命令还支持一些可选参数用于调整输出格式或提供额外的信息。例如 /r以原始数据的方式显示不会替换一些 typedef 定义。 /m查看类时不显示类的方法只显示类的成员变量。 /M与 /m 相反显示类的方法默认选项。 /t不打印类中的 typedef 数据。 /o打印结构体字段的偏移量和大小。 这些选项可以通过在 ptype 命令后附加相应的参数来使用以便根据需要调整输出。 需要注意的是ptype 命令仅在 GDB 的上下文中有效并且你需要在已经启动并加载了相应程序的 GDB 会话中使用它。此外ptype 命令只能查看当前作用域内可见的变量的类型。如果变量不在当前作用域内你可能需要切换到包含该变量的作用域例如通过进入函数或切换到特定的堆栈帧才能使用 ptype 命令查看其类型。
3.7 info 命令 和 thread 命令 3.8 jump命令
基本命令 jump命令的基本用法如下 jump location 这里的location可以是程序的行号、函数的地址或者是源代码文件名和行号的组合。例如 跳转到第100行jump 100 ----这种适合代码少的跳转当代码繁多时采用修改寄存器的方法 跳转到函数my_function的开始处jump my_function 跳转到文件myfile.c的第20行jump myfile.c:20 使用jump命令时需要注意以下几点 栈不改变jump命令不会改变当前的程序栈中的内容。这意味着如果从一个函数内部跳转到另一个函数当返回到原函数时可能会因为栈不匹配而导致错误。因此最好在同一函数内部使用jump命令。 后续执行如果jump跳转到的位置后面没有断点GDB会在执行完跳转处的代码后继续执行。如果需要暂停执行可以在跳转目标处设置断点。 小心使用jump命令可以强制改变程序的执行流程这可能导致未定义的行为或程序崩溃。因此在使用jump命令时需要谨慎并确保了解跳转的后果。 与tbreak配合使用由于jump命令执行后会立即继续执行所以经常与tbreak命令配合使用在跳转的目标位置设置一个临时断点以便调试者可以检查程序的状态。
修改寄存器pc $pc是一个特殊的变量它代表程序计数器Program Counter的当前值。程序计数器是CPU中的一个寄存器它存储了CPU将要执行的下一条指令的地址。换句话说它指向了CPU当前正在执行的代码位置。 修改$pc的值 你可以通过set命令来修改$pc的值从而改变程序执行的流程。但请注意这样做非常危险除非你确切知道你要跳转到的地址并且该地址包含有效的指令。 情景当需要停留在59行时却多打了一步在60行。 获得当前行的汇编地址代码 修改寄存器 目的获取的59行的汇编地址通过修改该行让其在59行继续运行。 3.9 display命令 3.10 source命令 在Linux中source命令用于在当前shell环境中执行指定的shell脚本文件而不是创建一个新的子shell来执行。这意味着脚本中定义的任何变量或函数都会在执行完脚本后保留在当前shell环境中。 使用source命令的基本语法如下 source /path/to/script.sh 或者你可以使用.点作为source命令的简写 . /path/to/script.sh source命令通常用于以下场景 更新环境变量加载配置文件主要用处如果你修改了某个环境变量文件如~/.bashrc或~/.bash_profile并且希望这些更改立即在当前shell会话中生效而不是在打开新的shell会话时才生效你可以使用source命令来加载这些更改。 source ~/.bashrc 执行初始化脚本某些应用程序或工具可能需要运行初始化脚本以设置环境或执行其他一次性任务。使用source可以确保这些更改在当前shell中生效。 在当前shell中运行函数和别名如果你在脚本中定义了一些函数或别名并希望它们在当前shell中可用那么使用source来执行脚本是合适的。 使用source命令而不是直接运行脚本如./script.sh的主要区别在于环境变量的持久性。直接运行脚本会在子shell中执行任何在脚本中定义的变量或更改的环境变量都不会影响父shell即你当前所在的shell。而使用source命令脚本中的变量和更改会直接影响当前shell。
3.11 save保存断点文件 提高调试的速度
其中.txt中的文件的条件也可自行修改
查看文件内容
加载文件
3.12 watch命令 watch 命令在多种场景下都非常有用比如 监视日志文件的变化以便实时查看新添加的行或错误消息。 监视系统资源使用情况如 CPU、内存或磁盘空间。 跟踪进程状态或性能数据。 请注意watch 命令会不断地执行指定的命令这可能会对系统性能产生一定的影响特别是在执行复杂或资源密集型的命令时。因此在使用 watch 命令时请确保你了解其工作原理并谨慎选择监视的命令和刷新间隔。 3.13 diff命令 diff 命令是 Linux 和类 Unix 系统中用于比较两个文件或目录的差异的实用工具。通过比较diff 命令可以显示两个文件或目录之间的不同之处以便用户可以了解它们之间的差异。 diff 命令的基本语法如下 diff [选项] 文件1 文件2 其中选项 是可选的用于调整 diff 命令的输出格式和行为而 文件1 和 文件2 是你想要比较的两个文件。 以下是一些常用的 diff 命令选项 -c 或 --context以上下文格式显示差异。这会显示文件之间的不同行以及它们前后的几行上下文有助于用户理解差异的具体位置。 -u 或 --unified以统一的格式显示差异。这种格式与上下文格式类似但显示方式稍有不同通常用于补丁文件patch files。 -r 或 --recursive递归比较目录及其子目录下的文件。当比较目录时diff 会递归地遍历目录树并比较其中的文件。 -i 或 --ignore-case忽略大小写的差异。在比较时不考虑字母的大小写。 -w 或 --ignore-all-space忽略所有空格的差异。这包括空格、制表符等空白字符。 -B 或 --ignore-blank-lines忽略空白行的差异。即不将只包含空白字符的行视为差异。 diff 命令的输出结果通常以 和 符号来表示差异。 表示文件1中的内容 表示文件2中的内容。具体的差异行会以 - 或 符号开头表示删除或添加的行。 除了上述常用选项外diff 命令还有其他一些选项如 -a将二进制文件视为文本文件进行比较、-l将结果交由 pr 程序来分页等。 图中的.bak文件是备份文件。 3.14 多线程gdb调试步骤 ps -ef | grep main查看线程号 gdb attach 线程号 info thread 查看线程数量 thread 2 表示查看第2 个线程 b 文件名行数打断点随后直接run就行 info br查看断点信息 p 参数表示想查看具体参数的细节 set pr pr设置打印好看 thread apply all bt所有线程都打印栈帧 f 3进入第三个线程的栈帧 4wget命令(redis安装)
4.1 wget命令 get是一个常用的命令行工具用于从网络上下载文件。它支持多种协议包括HTTP、HTTPS、FTP等并提供了丰富的选项和参数以满足不同的下载需求。 wget命令的基本格式如下 wget [选项] [参数] 其中选项用于指定wget的行为参数则用于指定要下载的文件或URL地址。 以下是一些常用的wget命令示例 下载单个文件例如在Downloads - Redis下载redis压缩包文件 wget https://download.redis.io/redis-stable.tar.gz 这个命令将从https://download.redis.io/redis-stable.tar.gz下载文件到当前目录。 然后开始使用tar zxvf redis-stable.tar.gz 进行解压 上面就是源码安装的过程 支持断点续传 wget -c http://example.com/largefile.iso 使用-c选项如果下载过程中连接中断wget可以从上次停止的地方继续下载。 后台下载文件 wget -b http://example.com/background.mp3 使用-b选项wget会在后台执行下载操作即使关闭终端也不会影响下载进程。 限速下载文件 wget --limit-rate300k http://example.com/slowdownload.zip 使用--limit-rate选项可以限制下载速度这里限制为300k。 下载到指定目录 wget -P /path/to/directory http://example.com/file.pdf 使用-P选项可以指定下载文件的保存目录。
4.2 redis了解即可 RedisRemote Dictionary Server即远程字典服务是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库并提供多种语言的API。以下是Redis的一些主要特性和应用 速度快由于Redis所有数据是存放在内存中的并且其源代码采用C语言编写距离底层操作系统更近执行速度相对更快。此外Redis使用单线程架构避免了多线程可能产生的竞争开销。 基于K_V的数据结构Redis提供了丰富的数据类型包括字符串、哈希、列表、集合、有序集合等并且每种数据类型都提供了丰富的操作命令。 功能相对丰富Redis对外提供了键过期的功能可以用来实现缓存。它还提供了发布订阅功能可以用来实现简单的消息系统解耦业务代码。此外Redis还支持Lua脚本提供了简单的事务功能不能rollback以及Pipeline功能客户端能够将一批命令一次性传输到Server端减少了网络开销。 扩展模块Redis提供了一个模块叫做RedisJSON它允许你在Redis中存储和查询JSON文档支持多种查询语法和索引类型。这使得Redis能够模拟MongoDB等文档数据库的功能同时由于其高性能和低延迟你可以获得更快的响应速度和更好的用户体验。 搜索与可视化Redis不仅可以存储数据还可以对数据进行搜索和分析。你可以使用RediSearch来构建自己的搜索引擎无论是针对网站内容、电商商品、社交媒体等领域都可以实现高效和灵活的搜索功能。此外你还可以使用RedisGraph来分析复杂的关系数据并利用图形界面来展示数据的结构和特征。 性能优化针对Redis可能遇到的性能问题如内存溢出和IO瓶颈可以采取一些优化措施。例如选择合适的Redis数据结构来存储数据将Redis的数据定期或实时保存到磁盘上以及通过集群分片来拆分负载实现性能的横向扩展。
5set args 和 show args set args: 这个命令可能是用来设置某些参数或变量的。例如在一个脚本或程序中你可能想要设置一些输入参数或配置选项这时可以使用 set args 来完成。 show args: 这个命令可能是用来显示之前设置的参数或变量的。它可以让你查看当前已经设置的参数或变量的值。 git 命令
1、将服务器更新的内容上传至gitee仓库 上传gitee的流程如下 1git add git add [文件名|目录名] 将对应的文件|目录放到暂存区 2 git commit git commit -m[备注] 将暂存区的内容放到对象区并添加这次上传的备注信息 git commit --amend -m[新的备注] 修改暂存区这次上传的备注信息 3 git push git push [远程仓库名] [本地分支名] [远程分支名] 将本地的指定分支推送到指定仓库的指定分支上 例如 git push origin masterrefs/for/master 即是将本地的master分支推送到远程主机origin上的对应master分支 origin 是远程主机名 注一个简单的个人理解分支和git add、git commit和git push的关系git add和commit是将所有更新的内容放进对象区然后git push 是在对象区内按分支挑选内容上传至仓库
3.1 git push origin master 如果远程分支被省略如上则表示将本地分支推送到与之存在追踪关系的远程分支通常两者同名如果该远程分支不存在则会被新建
3.2 git push origin refs/for/master 如果省略本地分支名则表示删除指定的远程分支因为这等同于推送一个空的本地分支到远程分支等同于 git push origin --delete master
3.3 git push origin 如果当前分支与远程分支存在追踪关系则本地分支和远程分支都可以省略将当前分支推送到origin主机的对应分支
3.4 git push 如果当前分支只有一个远程分支那么主机名都可以省略可以使用git branch -r 查看远程的分支名
3.5 git push 的其他命令 这几个常见的用法已足以满足我们日常开发的使用了还有几个扩展的用法如下 1 git push -u origin master 如果当前分支与多个主机存在追踪关系则可以使用 -u 参数指定一个默认主机这样后面就可以不加任何参数使用git push
- • 2 git push --all origin 当遇到这种情况就是不管是否存在对应的远程分支将本地的所有分支都推送到远程主机这时需要 -all 选项
- • 3 git push --force origin git push的时候需要本地先git pull更新到跟服务器版本一致如果本地版本库比远程服务器上的低那么一般会提示你git pull更新如果一定要提交那么可以使用这个命令。
- • 4 git push origin --tags //git push 的时候不会推送分支如果一定要推送标签的话那么可以使用这个命令。 4git status git status 工作区、暂存区内文件|目录的状态 $\textcolor{red}{红色}$表示文件|目录在工作区 $\textcolor{green}{绿色}$表示文件|目录在暂存区 5 commit版本与hard指针待完善 6git reset 6.1 git reset --hard git reset --hard [commit版本] 清空工作区和缓存区回退仓库版本 首先git log获取commit版本号 仓库版本回退 清空工作区和缓存区 注使用后一定要用git log查询版本状态 6.2 git reset --mixed git reset --mixed 将暂存区的内容回退到工作区即可用于恢复git add操作 注--mixed为默认参数即可以直接输入git reset 本地仓库版本回退
6.3 git reset --soft git reset --soft [commit版本] 将对象区的内容回退到暂存区即可用于恢复git commit操作 提交的内容回到暂存区
6.4 git reset [commit版本] git reset [commit版本] 将对象区的内容回退到工作区即可用于恢复git commit操作 同上一节区别在于内容回退到工作区而不是暂存区 7git log git log 是 Git 中的一个命令用于显示仓库的提交历史查看日志。它提供了详细的提交信息包括每次提交的哈希值、提交者、提交日期、提交信息以及涉及的改动文件等。 以下是 git log 的一些常见用法和选项
git log 这将会显示当前分支上的所有提交从最近的提交开始按时间顺序逆序排列。 后面-3表示显示3条历史 如果git log查不出来所需要的版本号可以使用git reflog
2、git分支管理
1 什么是分支 多个程序员分发同一程序时由于不能直接在一个程序上进行功能的开发所以就有了功能分支的概念。 功能分支指的是专门用来开发新功能的分支它是临时从master主分支上分叉出来的当新功能开发且测试完毕后最终需要合并到master主分支上如图所示: 2git branch git branch 查询本地所有分支 git branch -r 查询远程所有分支 git branch -a 查询本地和远程所有分支并显示详细内容 git branch [分支名称] 创建分支并命名 git branch -d [分支名称] 删除分支 git branch -m [原名称] [新名称] 重命名分支 3git checkout git checkout [分支名称] 切换分支 git checkout -b [分支名称] 创建并切换到分支 4git merge git merge [分支名称] 将当前分支与指定名称分支合并 例如 git checkout master git merge develop 将master分支和develop分支合并 3、git init git init 是一个Git命令用于初始化一个新的Git仓库。当你运行这个命令时Git会在当前目录下创建一个名为 .git 的子目录这个子目录包含了仓库中所有的元数据对象以及必要的配置文件。 以下是 git init 的基本用法 在当前目录下初始化Git仓库 如果你在某个项目目录下并希望开始使用Git来跟踪该项目的版本你可以简单地运行
git init 之后你会看到 .git 目录在当前目录下被创建。这个目录包含了Git的所有核心组件如对象数据库、引用等。 在指定目录下初始化Git仓库 你也可以使用 git init 命令来初始化一个指定目录为Git仓库。例如如果你想在名为 myproject 的目录下初始化一个新的Git仓库你可以这样做
mkdir myproject
cd myproject
git init 带有参数的 git init 虽然 git init 通常不需要任何参数但有一个 --bare 选项它用于创建一个不包含工作树的裸仓库。裸仓库主要用于存储共享项目的中央版本历史记录例如在Git服务器上。
git init --bare 使用 --bare 选项创建的仓库不包含工作目录即没有 .git 目录之外的任何文件因此你不能直接在这个仓库中进行工作。 总的来说git init 是你开始使用Git跟踪项目版本的第一步。 4、git config --global 配置信息 git config --global 是 Git 的一个命令用于设置全局的 Git 配置选项。这些选项会应用于当前用户的所有 Git 仓库。 当你运行 git config --global 命令时你实际上是在修改位于你用户主目录下的 .gitconfig 文件。这个文件存储了全局的 Git 配置信息。 这里有几个使用 git config --global 的例子 设置用户名
git config --global user.name Your Name 这条命令会设置你在提交时所使用的用户名。Git 会使用这个用户名来标识你提交的代码。 设置邮箱地址
git config --global user.email your.emailexample.com 这条命令会设置你在提交时所使用的邮箱地址。与用户名类似Git 会使用这个邮箱地址来标识你提交的代码。
5、git diff 和 git pull
5.1 git diff git diff 是一个用于在 Git 版本控制系统中比较文件差异的命令。它可以用来比较暂存区与工作区之间的差异、比较当前工作区与最后一次提交之间的差异或者比较两次提交之间的差异。通过 git diff用户可以清晰地看到代码或文件内容的更改。 下面是 git diff 的一些常用用法和选项 比较工作区与暂存区的差异 git diff 这个命令会显示工作区中所有尚未暂存的更改。它会列出工作区与暂存区即 git add 命令之前的状态之间的差异。 比较暂存区与最近一次提交的差异 git diff --cached 或者 git diff --staged 这个命令会显示暂存区即 git add 命令之后的状态与最近一次提交之间的差异。这有助于用户检查即将提交的更改。 比较两个提交之间的差异 git diff commit-hash1 commit-hash2 这个命令会显示两个指定提交之间的差异。commit-hash1 和 commit-hash2 是提交的哈希值或引用如分支名或标签名。 比较当前工作区与指定提交的差异 git diff commit-hash 这个命令会显示当前工作区与指定提交之间的差异。 使用 --word-diff 或 --color-words 选项进行更详细的比较 git diff --word-diff 或者 git diff --color-words 这些选项会以更详细的方式显示差异包括高亮显示发生变化的单词。 比较两个文件之间的差异 git diff -- file1 file2 这个命令会直接比较两个文件的内容而不需要它们处于 Git 仓库中。这可以用来比较任何两个文件而不仅仅是 Git 仓库中的文件。 其他选项 git diff 还有许多其他选项可以用来定制输出格式、忽略空白字符、限制比较范围等。例如--stat 选项可以显示一个简要的统计信息而 --name-only 选项则只列出有差异的文件名。
5.2 git pull git pull命令用于从远程仓库获取最新的更新并将其合并到当前分支。 具体来说git pull 命令做了两件事 Fetch从远程仓库下载最新的更改。这不会改变你当前的工作或任何你本地的提交但它会更新你的本地仓库以便你知道远程仓库的最新状态。 Integrate通常是通过合并merge或变基rebase操作将远程仓库的更改集成到你的本地分支。 git pull 命令也有一些有用的选项 --rebase使用变基而不是合并来集成更改。这可以保持一个线性的提交历史但也可能导致更复杂的冲突解决。 -v 或 --verbose显示更详细的输出信息。 --ff-only仅当可以通过快速前进fast-forward方式合并时才执行拉取操作。这可以防止产生一个新的合并提交。 --no-commit在合并或变基后暂停提交允许用户手动检查和修改更改。 示例 从 origin 的 master 分支拉取并集成更改 git pull origin master 从上游分支拉取并集成更改假设上游分支已经设置 git pull 使用变基从上游分支拉取并集成更改 git pull --rebase 请注意在使用 git pull 之前最好先运行 git fetch 来查看远程分支的最新状态而不立即集成这些更改。这可以帮助你更好地理解将要合并或变基哪些更改以及是否有可能出现冲突。如果你预计会出现复杂的合并或变基情况先执行 git fetch 可能会更安全。
6、git branch git branch 是 Git 中的一个命令用于列出、创建和删除仓库中的分支。分支是 Git 中的一个核心概念它允许你并行地开发多个功能或修复多个问题而不会相互干扰。 以下是 git branch 的一些常见用法
1列出所有分支
git branch
这个命令会列出当前仓库中的所有分支。当前活动的分支前面会有一个星号*标记。
2列出所有分支包括远程分支
git branch -a 使用 -a 或 --all 选项会列出所有本地分支和远程跟踪分支。远程跟踪分支通常以 remotes/origin/ 开头。 git branch -av 用于列出所有的分支并显示每个分支的最后一次提交的详细信息。具体来说 git branch 是用于列出所有分支的命令。 -a 选项表示列出所有分支包括远程跟踪分支。 -v 选项表示显示每个分支的最后一次提交的详细信息。
3创建新分支
git branch branch-name
这个命令会创建一个新的分支但是并不会切换到这个新分支。branch-name 是你想要创建的分支的名称。
4切换到新分支
git checkout branch-name
git checkout -b branch-name//创建一个新的分支并立即切换到这个分支
在较新版本的 Git 中推荐使用 git switch 命令来切换分支。这两个命令都会创建一个新的分支并立即切换到这个分支。
5删除分支
git branch -d branch-name
这个命令会删除一个已存在的本地分支。注意你不能删除当前活动的分支。
6强制删除分支
git branch -D branch-name
使用 -D 选项可以强制删除一个分支即使它包含未合并的更改。
7跟踪远程分支
如果你想创建一个本地分支来跟踪一个远程分支你可以这样做
git branch --track local-branch-name remote-branch-name
8重命名分支
Git 本身没有直接重命名分支的命令但你可以通过两步来实现首先创建一个新的分支然后将原始分支删除。
git branch -m old-branch-name new-branch-name
这个命令会重命名当前活动的分支。 7、企业gitee分支的步骤 企业级使用Gitee分支的操作步骤主要包括以下几个环节 创建和初始化本地仓库首先在本地计算机上创建并初始化一个新的Git仓库。这通常通过git init命令完成。 提交代码到本地仓库将你的代码文件添加到本地仓库并使用git add命令将其暂存。然后使用git commit命令为这次提交添加注释。 建立与远程仓库的连接如果还没有与远程仓库建立连接你需要使用git remote add origin “xxx.git”命令来添加远程仓库的URL。这里的“xxx.git”应替换为你的Gitee仓库的实际URL。 创建并切换分支使用git branch 分支名命令创建新的分支然后使用git checkout 分支名命令切换到新创建的分支。你也可以使用git switch -c 分支名命令一次性创建并切换到新分支。 在Gitee仓库中创建分支如果你希望在Gitee的在线仓库中也创建分支可以登录Gitee进入你的仓库页面点击左侧菜单栏中的“提交”在右侧页面上方选择“创建新分支”输入新分支的名字并选择基于哪个分支创建新分支。 提交分支到远程仓库当你完成本地分支的修改后可以使用git push origin 分支名命令将本地分支推送到Gitee的远程仓库。 主要分支步骤 当新分支的代码修改完成后你可能需要将这个分支合并到主分支通常是master或main分支。合并分支的常用方式是使用git merge命令。 首先切换到主分支git checkout master或git checkout main取决于你的主分支名称。 然后执行合并操作git merge 新分支名。Git会尝试将新分支的修改合并到主分支。 如果在合并过程中发生了冲突Git会提示你解决冲突。你需要手动编辑文件以解决这些冲突然后再次提交修改。 最后将合并后的主分支推送到远程仓库git push origin master或git push origin main。 gitee软件步骤 在分支页面点击贡献代码并创建Pull Request 在创建页面填写备注等信息 下拉点击创建并在右边选项选择合适的功能把合并删除提交分支也选上避免后面出现许多分支。
创建好后管理者可以查看并开始审查
然后开始合并合并的目的就是将分支的修改后的代码传至主支上master
回到工作区一定先要git pull拉一下代码更新一下 8、git merge 文件名 将其文件合并至master分支必须先切换到master分支。 9、git stash (压栈操作)
git stash 是 Git 中的一个命令用于临时保存当前工作目录和暂存区的修改以便你可以切换到其他分支进行工作然后再回来继续之前的工作。当你想要保存当前的工作进度但又不想提交一个不完整的更改时git stash 就非常有用。
以下是 git stash 的一些常见用法
1保存当前工作进度**
git stash
这个命令会将你当前工作目录和暂存区的所有修改保存起来并重置工作目录为最近一次提交的状态。所有未提交的修改都会被存储起来你可以随时应用它们。
2查看存储的工作进度列表
git stash list
这个命令会列出所有被 git stash 保存的工作进度。每个工作进度都有一个唯一的名称通常是一个哈希值你可以使用这个名字来引用特定的工作进度。
3应用存储的工作进度
git stash pop
这个命令会取出最近一次保存的工作进度并应用到当前的工作目录和暂存区。如果应用成功该工作进度会从列表中删除。如果应用时出现冲突你需要手动解决冲突后再继续。
4应用特定的工作进度
git stash apply stash{n}
这里的 stash{n} 是你想要应用的工作进度的名称。n 是一个整数表示工作进度在列表中的位置最近的保存是 stash{0}然后是 stash{1}以此类推。
5丢弃存储的工作进度
git stash drop stash{n}
这个命令会丢弃指定的工作进度从列表中删除它。如果你不指定名称它会默认丢弃最近一次保存的工作进度。
6结合 pop 和 drop
如果你想要应用一个工作进度并立即丢弃它无论是否出现冲突你可以使用
git stash pop stash{n}
或者简单地
git stash pop --index
这会同时恢复工作目录和暂存区的状态。
通过 git stash你可以轻松地管理你的工作进度避免因为需要切换到其他分支而丢失当前的修改。
7流程 当执行当前操作时需要修改其他文件的bug因此需要git stash进行保存 然后切换至需要修改的分支中bug文件进行修改 修改后的文件进行上传然后合并至master分支
解决后需要再次回到原文件分支对其进行出栈操作git stash pop
10、分支的冲突与拒绝
1分支冲突 分支冲突通常发生在尝试将两个或更多并行开发的分支合并到一个共同分支时。这些分支可能同时对同一部分代码进行了修改而Git无法自动确定应该保留哪个版本的修改。这可能导致合并冲突。 冲突可能由以下原因引起 修改同一行代码即使不同的开发者在不同的文件上进行了修改但如果同时修改了同一行的代码合并时也会发生冲突。 重命名文件或移动文件如果一个分支对文件进行了重命名或者移动而另一个分支对相应的文件进行了修改合并时就会产生冲突。 合并历史问题有时候如果两个分支有完全不同的提交历史尤其是当一个分支是另一个分支的重新创建或者重写时合并时会遇到困难。 使用了不同的换行符在不同的操作系统中换行符可能会不同。如果不同的分支使用了不同的换行符合并时可能会产生冲突。 解决冲突的方法通常包括手动编辑冲突文件解决冲突后再提交合并请求。在某些情况下也可以使用工具来帮助解决冲突。 出现冲突后不能删除冲突代码
2冲突解决方法 编辑冲突文件当Git提示合并冲突时你需要手动打开冲突文件并查看其中的特殊标记。这些标记通常包括、和它们分别表示当前分支的内容、两个分支的共同祖先的内容以及要合并的分支的内容。你需要根据实际需求决定保留哪些内容删除哪些内容。 使用工具解决冲突有些IDE或代码编辑器提供了专门的工具来帮助解决Git冲突。这些工具可以直观地显示冲突的部分并允许你通过点击或拖拽来选择保留哪些更改。 添加文件到暂存区并提交解决完冲突后你需要将修改后的文件添加到Git的暂存区并提交合并结果。这可以通过git add 文件名和git commit -m 合并描述命令来完成。注意提交时应该提供清晰的描述说明这次合并的内容和目的。 解决持续冲突如果在合并过程中遇到持续冲突即每次尝试合并都会触发相同的冲突你可能需要仔细检查代码并考虑重新设计代码结构或分工以避免未来的冲突。 将黄色部分删除。然后将共同冲突的头文件移动到上面去。 3分支拒绝 分支拒绝通常发生在尝试将代码提交到远程分支或合并其他分支到当前分支时但Git由于某些原因拒绝了这一操作。 以下是一些可能导致分支被拒绝的原因 权限不足你可能没有足够的权限将代码提交到远程分支。 分支保护规则仓库可能设置了保护规则限制了谁可以将代码提交到特定分支。 未完结任务如果当前分支有未提交的更改而你又试图合并其他分支Git会拒绝合并请求。 已被其他分支领先当你的当前分支被其他分支超过时Git会拒绝合并请求。 合并策略冲突某些情况下你可能会设置了合并策略但Git在合并时无法使用该策略从而拒绝合并请求。 解决分支拒绝的问题通常需要根据具体原因采取相应的措施如获取足够的权限、满足分支保护规则、提交或撤销当前分支的更改、更新当前分支以与其他分支保持同步或调整合并策略等。 总结被拒绝可能分支比较落后因此需要每次在master分支使用git pull拉一下代码更新。不只针对拒绝其他也适用必须要git pull拉一下代码更新
4拒绝的解决方法 权限不足 确认你是否有足够的权限进行提交或合并操作。 如果没有权限联系仓库拥有者或管理员请求相应的权限。 作为替代方案你可以在本地创建新的分支或提交更改然后请求他人合并你的更改。 分支保护规则 查看仓库的分支保护设置了解哪些分支受到保护以及保护规则是什么。 如果你的提交或合并请求违反了保护规则例如直接推送到受保护的分支你需要将更改合并到受保护分支所依赖的分支上然后再进行提交或合并请求。 存在冲突 使用git status命令查看冲突文件。 手动编辑冲突文件解决冲突部分。 使用git add命令将解决冲突后的文件标记为已解决。 继续执行合并或提交操作。 未完结的本地更改 如果你有未提交的本地更改先提交或暂存这些更改再进行合并或推送操作。 使用git stash命令可以暂时保存当前更改以便稍后恢复。 网络问题或远程仓库问题 检查你的网络连接是否正常确保能够访问远程仓库。 如果远程仓库已满或存在其他问题联系仓库管理员解决。 配置错误或仓库状态异常 检查你的Git配置是否正确包括远程仓库的URL等。 如果仓库状态异常考虑克隆一个新的仓库副本并在新的副本上工作。 使用错误的命令或参数 确保你使用的Git命令和参数是正确的。 查阅Git文档或相关教程了解正确的命令用法。 数组/指针/函数
1、数组
1特点 连续的存储空间 存储相同的数据类型。 整个代码不允许使用魔数即str[5]5是魔数应该使用宏str[BUFFER_SIZE]。
2清理脏数据 将数组里面数据全置为空 memset
memset(array, 0, sizeof(array)); bzero
bzero(array, sizeof(array));
注虽然 bzero 是一个常用的函数但在 POSIX 标准中它已经被标记为过时并推荐使用 memset 函数来替代。memset 函数提供了与 bzero 相同的功能但更为通用并且可以在非零值上进行操作。
3额外知识点 \一般来说\代表转义。 数组作为函数的参数会自动若化成指针导致调用函数时传出的数组大小不一样因此函数的参数必须加上数组大小保证传进去的数组大小跟定义的一样。 调用函数时的数组大小 函数里面的数组大小弱化成指针了 2、函数
1函数三要素 函数名定义的函数名的时候一定要准确做到见名知义。 函数参数形参 函数返回值int char等
2函数参数 函数的参数为形参 调用函数的参数为实参 解引用指获取指针或引用所指向的内存地址中的值。指针是一个变量它存储的是另一个变量的内存地址。要获取该地址中存储的实际值你需要对指针进行解引用。解引用操作通常使用星号*符号进行。 在main函数中将newsize的地址传给dedupArray函数然后后通过解引用获取该函数中的值。 3传入参数和传出参数
1、传入参数 对于整数而言没有指针的就是传入参数就是所谓的值传递。 对于字符串而言没有const限定符的是传入参数。
2、传出参数 对于整数而言有指针的就是传入参数就是所谓的地址传递。 对于字符串而言有const限定符的一定是传出参数。
3、const限定符 const修饰的是常量常量不可被修改。
4、用法 如果在关于字符串的函数参数中不需要对函数中的字符串进行修改需要加上限定符const。 以后传入指针的时候必须都要判空。 数组即指针 4函数声明 .h文件被称为头文件 下面的宏的作用是避免头文件重复包含
#ifndef _FUNC_H_
#define _FUNC_H_
#endif //_FUNC_H_
5函数的首地址 函数的首地址是指函数在内存中的起始位置即函数第一条指令的地址。在C和C等编程语言中函数名本质上就是一个指向该函数首地址的常量指针。当程序被编译后每个函数都会被分配一段连续的内存空间而这段内存空间的起始地址就是函数的首地址。 在C语言中你可以通过取地址操作符来获取一个函数的首地址并将其赋给一个函数指针变量。例如
int myFunction()
{ // 函数体
}
int (*funcPtr)() myFunction; // 将myFunction的首地址赋给funcPtr 在这个例子中funcPtr是一个函数指针变量它存储了myFunction的首地址。你可以通过这个函数指针来调用myFunction函数
int result (*funcPtr)(); // 使用函数指针调用myFunction 需要注意的是函数指针的类型必须与它所指向的函数类型相匹配。在上面的例子中funcPtr的类型是int (*)()这表示它是一个指向返回int类型且不接受任何参数的函数的指针。 在C中函数指针的概念与C语言中类似但语法上可能有些许差异。同样地函数名可以作为指向函数首地址的指针使用并且可以通过函数指针来调用函数。 3、指针
1 初始化 指针若不初始化指针就是野指针。
char *ptr NULL;
void * 是万能指针可以强转成任意指针类型。
2数组与指针 数组是存放在栈空间的 指针是指向NULL的是一段受保护的地址不能被使用因此需要在堆空间分配空间不然会出现段错误 。 分配完之后一定要判断是否为空 区别 在空间分配上数组是静态分配空间空间是物理连续的且空间利用率低。指针变量是需要动态分配内存空间内存空间在堆上分配与释放程序员管理。 数组名是指针常量一维数组名是首个元素的地址二维数组名是首个一维数组的地址。指针是-一个变量可以指向任意一块内存使用时要避免野指针的产生造成内存泄漏。 数组的访问效率高数组的访问方式等于指针的访问方式取* 数组使用容易造成数组越界指针的使用容易产生野指针造成内存泄漏。 函数传参时使用万能指针可以提高代码的通配性。数组作为形参传递时会默认退化成相应的指针。 数组只提供了一种简单的访问机制指针可以对地址直接操作来访问硬件的。 3检查内存泄露
1-内存泄漏的三个原因 野指针 malloc申请的地址没有free释放 踩内存
2-踩内存 含义需要赋值的字符串超过固定分配内存的大小造成内存泄漏。 解决方法采用安全函数strncpy( )函数。 3-内存泄漏检查
如果指针分配内存后没有进行释放则会导致内存泄漏则可以使用一下代码进行检测。
valgrind --toolmemcheck --leak-checkyes --show-reachableyes 所跑出的文件./a.out 4-perror与exit perror( const char *s ); 作用将字符串参数打印至控制台且打印程序错误信息 注没有成功让程序打印错误信息但我在网上找到了C找不到文件时的错误信息。另外这里没有使用 exit() 函数
#includestdio.h
#include errno.h
#include string.h
int main(void)
{FILE *fp;fp fopen(/home/book/test_file,r);if (NULL fp){perror(fopen error);}return 0;
}
输出结果fopen error: No such file or directory exit( int status ) 作用正常终止程序通常在 perror () 函数后使用 4解决不完整类型问题
4.1 前向声明 前向声明Forward Declaration的逻辑基于编译器如何处理标识符的解析和类型信息的获取。在编程中当我们使用某个类型比如类、结构体、枚举等时编译器需要知道这个类型的完整定义以便能够正确地进行类型检查和代码生成。然而在某些情况下我们可能希望在完全定义类型之前就引用它这时就需要使用前向声明。 前向声明的逻辑如下 提前告知编译器通过前向声明我们告诉编译器即将使用到某个类型但此时并不提供该类型的完整定义。这允许编译器在后续的代码中识别该类型的引用而不会立即报错。 占位符作用前向声明实际上是一个占位符它告诉编译器在后续代码中会找到该类型的完整定义。编译器会在后续的编译过程中查找这个完整定义以确保类型使用的正确性。 限制使用由于前向声明没有提供类型的完整信息因此我们不能使用它来创建该类型的实例或访问其非静态成员除非这些成员是之前已经声明过的指针或引用类型。我们只能声明指向该类型的指针或引用或者将该类型用作函数参数的类型。 包含头文件在使用前向声明的类型之前我们最终需要包含定义该类型的头文件以确保编译器在编译过程中能够找到该类型的完整定义。这通常发生在实现文件的开始部分或者在使用到该类型的具体细节之前。 避免循环依赖前向声明常用于解决头文件之间的循环依赖问题。通过前向声明我们可以打破头文件之间的直接包含关系从而避免循环依赖导致的编译错误。 情景在test.c里面定义一个动态数组结构体在test.h里面声明一下typedef struct ......在main.c文件中调用test.h文件然后使用结构体时出现不完整类型的错误。 解决方法将其定义成指针的形式然后在test.c文件中的调用动态数组的函数中例如初始化函数中将其定义成二级指针的形式。 4.2 将其声明成指针的本质 通过指针我们不是在直接操作一个对象或类型本身而是在操作一个指向该对象或类型的内存地址。这种间接引用的方式允许我们在不完全了解或定义某个类型的情况下就能声明和使用指向该类型的指针。 具体来说当我们声明一个指向某个类型的指针时我们实际上是在告诉编译器“我想要一个能够存储某种类型对象内存地址的变量。”编译器并不需要知道这个类型的完整定义它只需要知道这个类型存在以便为指针变量分配足够的空间来存储地址。 这种间接性有几个重要的好处 解决不完全类型问题如前所述当我们在某个类型的完整定义之前就需要引用它时可以通过声明指向该类型的指针来避免编译错误。这是因为指针的大小是固定的通常是机器字长如32位或64位与它所指向的类型无关。 动态内存管理指针经常与动态内存分配如使用new或malloc一起使用允许我们在运行时创建对象并将指针指向这些对象的内存地址。这种灵活性是静态数组或直接在栈上分配的对象所不具备的。 多态性和接口在面向对象的编程中指针或引用是实现多态性的关键。通过将基类指针指向派生类对象我们可以实现运行时多态性即同一接口可以有多种实现。 传递大型数据结构通过传递指针而不是整个数据结构我们可以避免复制大型对象从而提高性能。函数接收的是指向数据的指针而不是数据的副本。 修改外部数据通过指针函数可以修改调用者传递的数据因为指针提供了对数据实际存储位置的直接访问。
5函数指针 函数指针是一个变量它存储了一个函数的地址。你可以通过这个指针来调用这个函数。函数指针的声明通常包括函数的返回类型和参数类型。 例如假设你有一个如下定义的函数
int add(int a, int b) { return a b;
} 你可以声明一个指向这个函数的指针
c复制代码
int (*func_ptr)(int, int); 然后你可以将这个函数的地址赋给这个指针并通过这个指针来调用函数
func_ptr add; // 也可以写作 func_ptr add; 在C和C中函数名本身就是地址
int result func_ptr(3, 4); // 这将调用add函数并将结果存储在result中
6指针函数 指针函数实际上是返回一个指针的函数。它的返回类型是一个指针而不是函数本身。指针函数可以有任意数量的参数其声明方式与普通函数类似只是返回类型是一个指针。 例如以下是一个返回整数指针的函数
int* get_array() { static int arr[] {1, 2, 3, 4, 5}; return arr;
} 在这个例子中get_array是一个返回整数指针的函数。它返回了一个指向静态整数数组的指针。 总结 函数指针是指向函数的指针通过它可以调用函数。回调函数就是定义成函数指针的 指针函数是返回指针的函数其返回类型是一个指针。 理解这两个概念的关键在于区分它们的作用函数指针用于间接调用函数而指针函数则用于返回某个类型的指针。
4、二维数组
1定义 array[m][n] *(*(array m) n) 二维数组可以理解为是存储指针的一维数组里面的每一个指针都指向一个一维数组的首地址 存储多个一维数组的数据结构。 array 是一个二维数组它有3行和4列总共可以存储12个整数。你可以通过行索引和列索引来访问数组中的元素例如 array[0] [0] 访问的是第一行第一列的元素array[2] [3] 访问的是第三行第四列的元素。 二维数组在内存中是连续存储的通常按行优先或列优先的方式排列。通常使用行优先的方式即先存储第一行的所有元素然后是第二行依此类推。
int array[ROW][COLUMN];
//赋值
int value 0;
for(int idx1 0; idx1 ROW; idx1)
{for(int idx2 0; idx2 COLUMN; idx2){array[idx1][idx2] value;}
}
//两者均为 6即array[m][n] *(*(array m) n)
printf(array[1][2]\t\t%d\n, array[1][2]);
printf(*(*(array 1) 2)\t%d\n, *(*(array 1) 2));
//array[m][n] *(*(array m) n)
printf(array[1][2]\t\t%p\n, array[1][2]);
printf(*(array 1) 2\t%p\n, *(array 1) 2);
//二维数组是存储指针的一维数组里面的每一个指针都指向一个一维数组
printf(array[0][2]\t\t%p\n, array[0][2]);
//这里在array的基础上加了两个int的字节即2*48
printf(array 2\t\t%p\n, array 2);
//这里在array的基础上加了两个int array[3]的字节即2*3*424
printf(*array 2\t\t%p\n, *array 2);
//这里在array的基础上加了两个int的字节即2*48 2位置关系 5、一级指针与二级指针
1 空链表 当创建一个单链表时使用的是一级指针 定义一个指针指向结点head即创建了一个链表的头指针 BalanceBinarySearchNode *head
head-NULL; 当在空链表时的链表尾插操作中需要更改了头指针head的指向因此在函数中要使用到二级指针这里前提是头指针。
2非空链表 一段非空链表head-node-node1-node2-NULL 若想插入尾插直接将node2-newnode因此需要更改的是node2结构体的指针域的存储内容因此这时我们操作只需要node2结构体的地址即一级指针。 链表中传入二级指针的原因是我们会遇到需要更改头指针head的指向的情况。如果我们仅是在不改变头指针head的指向的情况下对链表进行操作如非空链表的尾删尾插对非首结点(FirstNode)的结点的插入/删除操作等则不需要用到二级指针.
3二级指针的例子 下面是一个使用二级指针的例子它同时展示了如何修改指针的值和访问指针所指向的值
#include stdio.h void modifyPointerAndPrint(int **pptr, int value) { // 打印当前指针所指向的值 printf(Original Value: %d\n, **pptr); // 修改指针使其指向一个新的地址这里只是作为一个例子实际上你可能不会这样做 int new_value 20; // 新的值 *pptr new_value; // 修改指针使其指向new_value的地址 // 打印修改后指针所指向的值 printf(Modified Value: %d\n, **pptr); // 注意new_value是在函数栈上分配的当函数返回时这个内存可能不再有效 // 如果你需要让指针在函数外部仍然有效你需要确保分配的内存是持久的例如使用malloc
} int main() { int x 10; int *ptr x; // 一级指针指向x printf(Before modification, ptr points to x: %d\n, *ptr); // 将ptr的地址即二级指针和x的值传递给函数 modifyPointerAndPrint(ptr, x); // 注意此时ptr已经指向了函数内部的局部变量new_value // 这个值在函数返回后可能已经无效了 printf(After modification, ptr points to new_value (might be invalid): %d\n, *ptr); // 如果你需要让ptr在函数外部仍然有效你应该在函数内部使用malloc来分配内存 // 并在适当的时候使用free来释放内存 return 0;
} 注意在上面的例子中new_value 是在 modifyPointerAndPrint 函数的栈上分配的。当函数返回时这个内存可能不再有效因此 ptr 现在指向了一个无效的内存地址。这通常不是你所想要的。如果你需要在函数外部仍然能够访问这个值你应该使用 malloc 或其他内存分配函数来在堆上分配内存并将地址赋给指针。然后在适当的时候使用 free 来释放这块内存。 如果你只是想传递一个参数的地址和值而不打算修改指针本身那么使用一级指针和值作为两个单独的参数通常更为简单和直接。
6、字符串操作
1sizeof() sizeof()测量类型的大小这里char是1个字节但是定义了32个因此为32包括空格 在这里sizeof()测得是指针的大小了char 类型的指针。 int sizeof( type ); 作用返回变量占用内存的字节数 如下 lenArray 的值为50字节没填满也算
char string1[50];
strcpy(string1, hello world);
int lenArray sizeof( string1 ); 注sizeof是一种单目运算符而非函数但就使用方面而言理解为函数也不会犯错 2strlen() strlen()是测量字符串的长度读到\0就结束不包括\0 int strlen( const char* str );
3strcpy() strcpy()字符串的拷贝将当前的字符串复制给所需要的参数包括\0但是会将之前参数的信息覆盖。 如果源字符串包括空字符的长度超过了目标字符串分配的内存大小就会发生缓冲区溢出这是一个常见的安全漏洞。 为了避免缓冲区溢出可以考虑使用 strncpy 函数它允许指定一个最大复制长度从而避免溢出。但是使用 strncpy 时需要小心处理字符串的结束标志因为如果指定的长度小于源字符串长度strncpy 不会自动在目标字符串的末尾添加空字符这可能导致未定义的行为。 4strcmp() strcmp是字符串的比较如相等则返回值为0 为直观查看strcmp的作用自己创建函数实现功能
//自己创建mystrcmp函数
int mystrcmp(const char * str1, const char * str2)
{if(str1 NULL || str2 NULL){perror(NULL);exit(1);}while(*str1 ! \0 *str2 ! \0){if(*str1 *str2){return 1;}else if(*str1 *str2){return -1;}str1;str2;}return *str1 - *str2;//如果相等则是返回0大于返回正数小于返回负数。
} 5字符串常量区 字符串是存在字符串常量区其中的数据不能被修改ptr存的是常量区的地址。因此若想获得其字符串必须使用malloc分配内存。 正确改法如下通过分配内存的数据里面存的是脏数据因此必须使用memset清除脏数据 注意后面还要用free()释放堆空间 堆空间是计算机内存中的一个重要区域用于动态分配和存储数据。通过指针来访问堆空间中的数据并需要手动管理内存的分配和释放。 7、指针数组
1字符串数组 相当于二级指针或者是二维数组 字符串数组中的行列对应联系 2字符数组、字符指针指向常量、字符指针动态分配的区别
1. 字符数组
#define BUFFER_SIZE 32
//字符数组地址在栈空间
char buffer[BUFFER_SIZE] hello world;
int size sizeof(buffer);//32等于数组字节长度
int len strlen(buffer);//11到\0的字符数
printf(size %d\tlen %d\n, size, len);
strncpy(buffer, 257jiayou, BUFFER_SIZE);
printf(buffer:%s\n, buffer);
2. 字符指针指向常量字符串
//字符指针指向字符串常量地址在全局区
char* ptr hello world;
int size sizeof(ptr);//8等于指针字节长度
int len strlen(ptr);//11到\0的字符数
printf(size %d\tlen %d\n, size, len);
//常量无法更改越界报错
strncpy(ptr, 257jiayou, len);
printf(ptr:%s\n, ptr);
3. 字符指针动态分配
//动态分配地址在堆空间
char* ptr (char*)malloc(sizeof(char) * BUFFER_SIZE);
if(ptr NULL)
{perror(malloc error);exit(-1);
}
//清楚脏数据
memset(ptr, 0, BUFFER_SIZE);
strncpy(ptr, 257jiayou, BUFFER_SIZE);
printf(ptr:%s\n, ptr);
free(ptr);
8、内存碎片
内存碎片是指在计算机内存管理过程中内存被分配和释放后形成的小块不连续的未使用内存区域的现象。这种现象会导致内存空间的利用效率降低。内存碎片主要分为两种类型外部碎片和内部碎片。
1. 外部碎片
定义 外部碎片发生在内存的分配和释放过程中内存中会留下很多小的、非连续的空闲内存块。这些小块内存块之间可能被已经分配的内存块隔开导致无法满足需要大块内存的分配请求。
示例 假设内存中有一块大的空闲区域被分成了若干个小块当一个程序需要分配一块较大的连续内存区域时这些小块的空闲内存虽然总量足够但它们之间的非连续性使得无法满足要求。
影响 难以满足大内存块的分配请求。 导致内存使用效率降低。
解决方法 内存压缩将内存中的所有活动块移动到一起以便形成一个大的连续空闲块。 内存分配策略使用更先进的内存分配算法如伙伴系统来减少碎片化。 分区分配将内存分成固定大小的区块减少外部碎片。
2. 内部碎片
定义 内部碎片发生在分配的内存块中实际使用的内存小于分配的内存大小。由于内存的分配单位可能是固定大小的块所以未被完全使用的空间会造成碎片。
示例 假设内存分配单位是64字节而一个程序只需要50字节的内存。分配了一个64字节的内存块但实际只使用了其中的50字节剩余的14字节则成为内部碎片。
影响 造成内存的浪费因为分配的内存块中有部分空间未被使用。 可能导致系统总内存使用效率降低。
解决方法 调整分配单位使用更合适的分配单位或动态调整块大小以减少内部碎片。 使用更精细的分配策略如内存池memory pool等机制来管理不同大小的内存需求。
3. 总结
内存碎片是内存管理中的一个重要问题它会影响系统的性能和内存使用效率。为了优化内存管理减少碎片的产生许多操作系统和编程语言的运行时环境会采用各种策略和算法。理解内存碎片的原因和影响有助于设计更高效的内存管理系统。 二进制
1、编码方式
反码和补码是用于表示带符号整数的一种二进制编码方式主要用于计算机系统中的算术运算。它们解决了二进制中表示负数的问题并简化了二进制减法运算。以下是原码、反码、补码和移码的概念总结
1. 原码Sign-Magnitude Form 定义原码是直接使用最高位符号位表示符号剩余位表示数值大小的二进制编码。 符号位0表示正数1表示负数。 数值位直接使用绝对值的二进制表示。 例子 5 的原码00000101 -5 的原码10000101 特点原码直观但在进行加减运算时复杂特别是负数运算。
2. 反码Ones Complement 定义反码是将原码的符号位保持不变数值部分按位取反0变11变0的编码方式。 正数反码与原码相同。 负数反码为原码的数值部分按位取反。 例子 5 的反码00000101与原码相同 -5 的反码11111010数值部分取反 特点反码的表示方式解决了符号位的处理问题但仍然存在零的两种表示形式正零和负零。
3. 补码Twos Complement 定义补码是将原码的符号位保持不变数值部分按位取反后加1的编码方式。 正数补码与原码相同。 负数补码为原码的数值部分按位取反后加1。 例子 5 的补码00000101与原码相同 -5 的补码11111011反码加1 特点补码的表示方式统一了零的表示只有一种零00000000并且简化了减法运算。补码也是计算机系统中最常用的带符号数表示方式。
4. 移码Excess-N or Offset Binary 定义移码是一种在补码基础上加上一个固定值通常是2^(n-1)的编码方式用于将所有的数值转换为非负数从而简化某些运算。 正数移码为补码加上偏移量。 负数移码也是补码加上偏移量。 例子假设偏移量为8即加8 5 的移码1101补码为0101加偏移量8 -5 的移码0011补码为1011加偏移量8 特点移码在浮点数表示中常用特别是在计算机硬件中用于标准化浮点数。
总结 原码简单直观但运算复杂。 反码通过按位取反表示负数但有两个零的问题。 补码解决了反码的问题简化了运算是现代计算机中最常用的表示方式。 移码用于浮点数表示确保所有数值为非负数。
这些编码方式在计算机系统中起着关键作用特别是在整数和浮点数运算中。
2、位运算
位运算Bitwise Operations是在二进制位bit层面进行的运算。位运算通常用于底层编程尤其是涉及硬件、网络协议、加密算法等领域。以下是常见的位运算操作及其作用 按位与 规则对应位置都为1时结果为1否则为0。 用途用于掩码操作提取特定位。 例子1010 1100 1000 按位或| 规则对应位置只要有一个为1结果为1。 用途用于设置特定位。 例子1010 | 1100 1110 按位异或^ 规则对应位置不同则为1相同则为0。 用途用于交换值、检查差异。 例子1010 ^ 1100 0110 按位取反~ 规则每个位取反1变00变1。 用途用于反转所有位。 例子~1010 0101 左移 规则将所有位向左移动指定的位数右侧补0。 用途快速乘以2的幂次。 例子1010 1 10100相当于乘以2 右移 规则将所有位向右移动指定的位数左侧补0或符号位。 用途快速除以2的幂次。 例子1010 1 0101相当于除以2
位运算广泛应用于 性能优化位运算通常比普通的算术运算更快。 网络和协议处理处理和解析二进制协议或数据包。 加密与压缩处理低级数据、提高数据操作效率。
通过位运算可以实现高效的低级数据操作尤其适合需要精确控制内存和速度的场景。
排序
1、相关知识点
1数组 数组是一种数据结构它包含一系列相同类型的元素这些元素在内存中连续存储。 数组有一个固定的大小一旦声明就不能改变。 数组有一个名字这个名字是一个常量指针指向数组的第一个元素的地址。 数组名本身不能被修改即不能让它指向别的地址。
2指针 指针是一个变量它存储的是一个内存地址这个地址指向的是另一个变量的值。 指针可以指向任何类型的变量包括数组。 指针可以被重新赋值指向别的内存地址。 指针可以进行各种算术运算比如加减操作来指向不同的内存地址。
3数组和指针的使用 当你需要固定大小的一系列同类型数据时使用数组。
例如如果你需要存储10个整数你可以定义一个大小为10的整数数组。 当你需要动态地改变数据的大小或者在函数之间传递数据时使用指针。
例如你可能需要在一个函数中创建一个数组然后在另一个函数中使用这个数组。在这种情况下你可以使用指针来传递数组的地址而不是整个数组。 在函数参数中传递大型数组时通常使用指针。
如果直接传递数组实际上传递的是整个数组的一份拷贝这可能会消耗大量的内存和处理器时间。而传递指针则只会传递一个内存地址效率更高。
4注意字符串常量区引起的段错误。 如果你想要一个可以修改的字符串你应该使用动态内存分配例如 malloc() 或 calloc()或者定义一个字符数组在栈上例如 char str[] hello world;。这样str 就会指向一块可以修改的内存区域。 字符串字面值如 hello world通常存储在程序的只读内存段中。这意味着你不能修改这些字符串的内容。 5函数传出参数和传入参数 传入参数Input Parameters 传入参数是在调用函数时传递给函数的值或变量。这些参数允许函数使用外部提供的数据来执行其任务。传入参数可以是常量、变量、表达式的结果或者是从其他函数返回的值。 用法 在函数定义时在函数名后面的括号内声明传入参数的类型和名称。 在函数调用时按照函数定义中参数的顺序和类型提供相应的值或变量。 传出参数Output Parameters 传出参数通常是通过指针或引用传递的参数允许函数修改调用者提供的变量的值。这些参数用于将结果或状态信息从函数返回给调用者。 用法 在函数定义时声明传出参数的类型和名称并使用指针或引用传递它们。 在函数调用时传递变量的地址或引用给函数以便函数可以修改该变量的值。
6 计算机内存分配区域一定要牢记 1栈向下扩展的内存。 系统栈系统自带和函数栈自己创建栈是会放满的。
栈里面的局部变量 { int a;//左括号开始右括号结束。 } 2堆也称为动态存储区。这部分内存用于动态分配向上扩展的内存。堆的空间要比栈大得多由用户自己申请并且由用户自己释放若不释放造成内存泄露
寿命是由用户自己决定的 3静态全局区静态变量和全局变量
1、全局变量能被所有的程序可见
生命周期当程序结束时被销毁(少用全局变量)
2、静态变量static修饰
修饰局部变量的特点在函数结束时不会被系统回收只在程序结束时被销毁 只初始化一次
修饰全部变量的特点只对本文可见 4文字常量区字符串常量
常量的特点不可修改 (5) 代码段 也称为文本区存储程序的二进制代码。这部分内存是只读的防止程序在运行时意外地修改其指令。 2、冒泡排序
1定义 冒泡排序Bubble Sort是一种简单的排序算法它重复地遍历要排序的数列一次比较两个元素一轮下来能将最大的数排到后面。
2bubbleSort函数排序思路 思路1按定义规则。 int bubbleSort01(int *nums, int numSize)
{
#if 0for (int idx 0; idx numSize; idx){for (int begin 1; begin numSize - idx - 1; begin)//{if (nums[begin - 1] nums[begin]){/*交换*/swapNum(nums[begin - 1], nums[begin]);}}}
#endif
for (int end numSize; end 0; end--){for (int begin 1; begin end; begin) {if (nums[begin - 1] nums[begin]){/*交换*/swapNum(nums[begin - 1], nums[begin]);}}}
return 0;
}
思路2若已经提前排序成功为避免剩余轮数继续遍历则定义一个标志位。 1 定义标志位sorted 2每开启一轮标志位为1 3内部交换完标志位为0 4如果内部没有发生交换那就说明总体已经全部排好了不需要其他的轮数再去遍历避免了内存消耗。 5没发生交换则标志位为1进入判断直接break结束循环。
/*升序*/
/*记录一下是不是排好序呢*/
int bubbleSort02(int *nums, int numSize)
{int sorted 0;//标记
for (int end numSize; end 0; end--)//轮数循环{sorted 1;for (int begin 1; begin end; begin) //一轮结束最大值就在最后{if (nums[begin - 1] nums[begin]){/*交换*/swapNum(nums[begin - 1], nums[begin]);sorted 0;}
}
/*已经排好序了*/if (sorted 1){break;}}
return 0;
}
3 全部代码
#include stdio.h
#include string.h
#define BUFFER_SIZE 100
/*函数参数传递是按值传递的形参为传出函数形参定义成指针这样就可以修改形参*/
static int swapNum(int *val1, int *val2)
{int tmpVal *val1;
*val1 *val2;
*val2 tmpVal;
}
/*升序*/
int bubbleSort01(int *nums, int numSize)
{
#if 0for (int idx 0; idx numSize; idx){for (int begin 1; begin numSize - idx - 1; begin)//{if (nums[begin - 1] nums[begin]){/*交换*/swapNum(nums[begin - 1], nums[begin]);}}}
#endif
for (int end numSize; end 0; end--){for (int begin 1; begin end; begin) {if (nums[begin - 1] nums[begin]){/*交换*/swapNum(nums[begin - 1], nums[begin]);}}}
return 0;
}
/*升序*/
/*记录一下是不是排好序呢*/
int bubbleSort02(int *nums, int numSize)
{int sorted 0;//标记
for (int end numSize; end 0; end--){sorted 1;for (int begin 1; begin end; begin) {if (nums[begin - 1] nums[begin]){/*交换*/swapNum(nums[begin - 1], nums[begin]);sorted 0;}
}
/*已经排好序了*/if (sorted 1){break;}}
return 0;
}
int printNums(int *nums, int numSize)
{for (int idx 0; idx numSize; idx){printf(%d\t, nums[idx]);}printf(\n);
}
int main()
{// int nums[BUFFER_SIZE]加上BUFFER_SIZE就是固定分配大小不加就是数据写多少分配多少。int nums[] {1, 45, 2, 34, 7, 87, 3, 9, 12, 99};
int len sizeof(nums) / sizeof(nums[0]);
bubbleSort02(nums, len);printNums(nums, len);
return 0;
} 3、选择排序
1定义 通过一次排序将待排序的数据分割成独立的两部分其中一部分的所有数据都比另一部分的所有数据都要小然后再按此方法对这两部分数据分别进行快速排序整个排序过程可以递归进行以此达到整个数据变成有序序列。 本次代码思想先定义一个POS为该数据的第一个然后遍历后面的数据并与POS比较如果遍历到最小的这将与POS发生交换此时第一个是最小的。随后POS位置向前进一个以此类推。 2selectSort的排序思路 思想1按定义中的思想进行。
int selectSort01(int *nums, int numSize)
{int mini 0;int minPos 0;for (int pos 0; pos numSize; pos){mini nums[pos];//控制posfor (int idx pos 1; idx numSize; idx){if (nums[idx] mini){mini nums[idx];minPos idx;}}/* 找到之后的最小值. */if (nums[pos] mini){swapNum((nums[pos]), (nums[minPos]));}}
return 0;
} 思想2
1增加一个数组指定范围接口。
2思想跟思想1差不多增加的接口也是为了将“找剩下的元素中最小的数”给分离出一个函数出来。
3selectSort02中是只需要控制POS往前移动即可 /*找到数组指定范围的最小值*/
static int findNumRangMinVal(int *nums, int begin, int end, int *mini, int *minPos)
{
int min nums[begin];for (int pos begin; pos end; pos){if (nums[pos] min){min nums[pos];//覆盖*minPos pos;}}/*解引用*/*mini min;return 0;
} int selectSort02(int *nums, int numSize)
{int mini 0;int minPos 0;for (int pos 0; pos numSize; pos){findNumRangMinVal(nums, pos, numSize, mini, minPos);
/* 找到之后的最小值 */if (nums[pos] mini){swapNum((nums[pos]), (nums[minPos]));}}
}
int printNums(int *nums, int numSize)
{for (int idx 0; idx numSize; idx){printf(%d\t, nums[idx]);}printf(\n);
} 3全部代码
#include stdio.h
#include string.h
static int swapNum(int *val1, int *val2)
{int tmpVal *val1;
*val1 *val2;
*val2 tmpVal;
}
/* 找到数组的最小值 */
int findNumMinVal(int *nums, int numSize, int *ppos)
{int mini nums[0];for (int pos 0; pos numSize; pos){if (nums[pos] mini){mini nums[pos];*ppos pos;}}return mini;
}
/*找到数组指定范围的最小值*/
static int findNumRangMinVal(int *nums, int begin, int end, int *mini, int *minPos)
{
int min nums[begin];for (int pos begin; pos end; pos){if (nums[pos] min){min nums[pos];*minPos pos;}}*mini min;return 0;
}
int selectSort01(int *nums, int numSize)
{int mini 0;int minPos 0;for (int pos 0; pos numSize; pos){for (int idx pos 1; idx numSize; idx){if (nums[idx] mini){mini nums[idx];minPos idx;}}/* 找到之后的最小值. */if (nums[pos] mini){swapNum((nums[pos]), (nums[minPos]));}}
return 0;
}
int selectSort02(int *nums, int numSize)
{int mini 0;int minPos 0;for (int pos 0; pos numSize; pos){findNumRangMinVal(nums, pos, numSize, mini, minPos);
/* 找到之后的最小值 */if (nums[pos] mini){swapNum((nums[pos]), (nums[minPos]));}}
}
int printNums(int *nums, int numSize)
{for (int idx 0; idx numSize; idx){printf(%d\t, nums[idx]);}printf(\n);
}
int main()
{//int pos 0;int nums[] {4, 45, 2, 34, 7, 87, 3, 9, 12, 99};
int len sizeof(nums) / sizeof(nums[0]);
selectSort02(nums, len);
printNums(nums, len);
return 0;
} 4、插入排序
1定义 通过构建有序序列对于未排序数据在已排序序列中从后向前扫描找到相应位置并插入。 本次代码实现思想先定义一个copynum。从头开始比较如果发现当前数比后一个数要大则将后一个数赋值给copynum当前数直接将后一个数覆盖。随后当前数的前面数如果比copynum要大则需要后移如果小于copynum则直接插入这个位置copynum是往前遍历的直到copynum找到最小的位置进行插入。 2insertSort的排序思路 插入排序默认第一个元素已经排好序了目的就是将第一个跟当前结点比较让后将最小的赋值给copyNum
/*插入排序*/
int insertSort(int *nums, int numSize)
{/*插入排序默认第一个元素已经排好序了*/
for (int idx 1; idx numSize; idx){int copyNum nums[idx];/*索引备份*/int preIdx idx - 1;
while (nums[preIdx] copyNum preIdx 0){/*后移*/nums[preIdx 1] nums[preIdx];//覆盖
preIdx--;}
/*nums[preIdx] copyNum || preIdx 0*/nums[preIdx 1] copyNum;}
return 0;
}
3插入排序优化 基于二分搜索
/*找到copynum插入的位置*/
static int searchInsertPos(int *nums, int numsSize, int target)
{int left 0;int right numsSize - 1;int mid 0;while (left right){mid (left right) / 2;if (nums[mid] target){left mid 1;} else{right mid - 1;}}return left;
} 4全部代码
#include stdio.h
#include string.h
static int swapNum(int *val1, int *val2)
{int tmpVal *val1;
*val1 *val2;
*val2 tmpVal;
}
/*找到copynum插入的位置*/
static int searchInsertPos(int *nums, int numsSize, int target)
{int left 0;int right numsSize - 1;int mid 0;while (left right){mid (left right) / 2;if (nums[mid] target){left mid 1;} else{right mid - 1;}}return left;
}
/*插入排序*/
int insertSort(int *nums, int numSize)
{/*插入排序默认第一个元素已经排好序了*/
int insertPos 0;for (int idx 1; idx numSize; idx){int copyNum nums[idx];int prevIdx idx - 1;
/* 找到要排序元素要插入的位置 O(logN) */insertPos searchInsertPos(nums, idx, copyNum);
#if 0while (nums[prevIdx] copyNum prevIdx 0){nums[prevIdx 1] nums[prevIdx];prevIdx--;}// nums[prevIdx] copyNum prevIdx -1nums[prevIdx 1] copyNum;
#else/* 数组迁移, 从后向前迁移. */for (int jdx idx; jdx insertPos; jdx--){nums[jdx] nums[jdx - 1];}nums[insertPos] copyNum;
#endif}
return 0;
}
int printNums(int *nums, int numSize)
{for (int idx 0; idx numSize; idx){printf(%d\t, nums[idx]);}printf(\n);
}
int main()
{// int pos 0;int nums[] {4, 45, 2, 34, 7, 87, 3, 9, 12, 99};
int len sizeof(nums) / sizeof(nums[0]);
insertSort(nums, len);
printNums(nums, len);
return 0;
}
5、快速排序
1定义 基本思想是采用分治策略 分治策略将一个复杂的问题分解为两个或多个相同或相似的子问题递归地解决这些子问题最后将子问题的解合并起来得到原问题的解。 基准元素在快速排序中选择一个元素作为基准然后将数组分为两部分一部分的元素都比基准小另一部分的元素都比基准大。
2排序思路 思想采用“分治”的思想对于一组数据选择一个基准元素pivot通常选择第一个或最后一个元素通过第一轮扫描比pivot小的元素都在pivot左边比pivot大的元素都在pivot右边再有同样的方法递归排序这两部分直到序列中所有数据均有序为止。 首先记录基准值默认定义基准值为所取范围的第一个值即 int pivot nums[begin];这个值单独出来的。 使用while()循环终止条件begin end;此循环是个大循环用来控制轮数。 内部循环1-while (begin end) 先从左往右排这是判断末尾的值。即从末尾开始即若nums[end]的值比基准值要小则将nums[end]的值赋给开头nums[begin]让后begin准备存储第二个比基准值小的值并退出循环开始执行内部循环2。 若nums[end]比基准值要大则值的位置不动并end--。开始下一轮循环直到跳出循环执行内部循环2或者begin end 结束循环。 内部循环2-while (begin end) 这是从右往左排这是判断开头的值即从起始位置若nums[begin]的值比基准值要大则将nums[begin]的值给end并且end--然后结束循环开始进去内部循环1判断end 的值。 若nums[end]的值比基准值要小则值的位置不动并begin。开始下一轮循环判断直到跳出循环执行内部循环1或者begin end 结束循环。 一轮大循环下来比基准值小的在左边比基准大的在右边但是左边和右边的还是乱的因此再次调用该函数对左边和右边再次进行快排。 总结 两个内部循环一个是判断begin的迁移一个判断判断end的迁移。 两个迁移一个是保证位置不变一个是覆盖后面end 或者begin的值实现迁移和分在基准值的两边。 3全部代码
注意需要对begin和end的初始位置进行备份因为后面几轮的排序还需要用到起始和末尾的位置。
#include stdio.h
#include stdlib.h
#include string.h
static int innerQuickSort(int *nums, int begin, int end)
{if (begin end){return 0;}
/*记录基准值*/int pivot nums[begin];
/*备份begin,end*/int tmpBegin begin;int tmpEnd end;
while (begin end){
while (begin end){/*从右往左*/if (nums[end] pivot){end--;}else{//nums[begin] nums[end];//与下面等价nums[begin] nums[end];begin;break;}}
while (begin end){/*从左往右*/if (nums[begin] pivot){nums[end--] nums[begin];break;}else{begin;}}}
/*退出这个条件 begin end*/nums[begin] pivot;//再次将begin的值给pivot
innerQuickSort(nums, tmpBegin, begin - 1);innerQuickSort(nums, begin 1, tmpEnd);
return 0;}
/*快速排序*/
int quickSort(int *nums, int numSize)
{if (nums NULL){return 0;}
innerQuickSort(nums, 0, numSize - 1);
}
int printNums(int *nums, int numSize)
{
for (int idx 0; idx numSize; idx){printf(%d\t, nums[idx]);}printf(\n);}
int main()
{// int pos 0;int nums[] {31, 47, 25, 16, 4, 35,38};
int len sizeof(nums) / sizeof(nums[0]);
quickSort(nums, len);
printNums(nums, len);
return 0;
} 6、二分搜索
1定义 二分搜索也称为二分查找是一种在有序数组中查找特定元素的搜索算法。其基本思想是通过不断地将搜索范围缩小一半来找到目标元素。 注意的是有序数组因此数组若是无序的先开始排序后在调用二分搜索。
2搜索思路 确定搜索范围首先确定有序数组的范围即左边界和右边界。 计算中间位置计算搜索范围的中间位置。通常中间位置可以通过将左边界和右边界相加然后除以2来得到。 比较中间元素将目标元素与中间位置的元素进行比较。 比较原则 如果中间值num[minIndex] 比目标值大说明目标在中间值的右边此时将end的位置更新到中间值的前一个位置即end minIndex - 1; 如果中间值num[minIndex] 比目标值小说明目标在中间值的左边此时将begin的位置更新到中间值的后一个位置即begin minIndex 1; 直到begin end就结束循环即找到该目标。
/*二分查找*/
int binarySearch(int *num, int numSize, int target)
{int begin 0;int end numSize - 1; // 不减就踩内存
int midIndex 0;
while (begin end){/*跟新中间位置*/midIndex (begin end) 1;if (num[midIndex] target){end midIndex - 1;}else if (num[midIndex] target){begin midIndex 1;}else{return midIndex;}
}return -1;
} 3全部代码 这里使用快速排序进行排序后在进行二分查找。
#include stdio.h
#include stdlib.h
/*二分查找*/
int binarySearch(int *num, int numSize, int target)
{int begin 0;int end numSize - 1; // 不减就踩内存
int midIndex 0;
while (begin end){/*跟新中间位置*/midIndex (begin end) 1;//mid (begin end) / 2 可能造成向下取整if (num[midIndex] target){end midIndex - 1;}else if (num[midIndex] target){begin midIndex 1;}else{return midIndex;}
}return -1;
}
static int innerQuickSort(int *nums, int begin, int end)
{if (begin end){return 0;}
/*记录基准值*/int pivot nums[begin];
/*备份begin,end*/int tmpBegin begin;int tmpEnd end;
while (begin end){
while (begin end){/*从右往左*/if (nums[end] pivot){end--;}else{nums[begin] nums[end];break;}}
while (begin end){/*从左往右*/if (nums[begin] pivot){nums[end--] nums[begin];break;}else{begin;}}}
/*退出这个条件 begin end*/nums[begin] pivot;
innerQuickSort(nums, tmpBegin, begin - 1);innerQuickSort(nums, begin 1, tmpEnd);
return 0;
}
/*快速排序*/
int quickSort(int *nums, int numSize)
{if (nums NULL){return 0;}
innerQuickSort(nums, 0, numSize - 1);
}
int printNums(int *nums, int numSize)
{for (int idx 0; idx numSize; idx){printf(%d\t, nums[idx]);}printf(\n);
}
int main()
{int nums[] {31, 47, 25, 16, 4, 35, 38};
int len sizeof(nums) / sizeof(nums[0]);
quickSort(nums, len);
printNums(nums, len);
int index binarySearch(nums, len, 35);printf(index1: %d\n, index);
index binarySearch(nums, len, 4);printf(index2: %d\n, index);
index binarySearch(nums, len, 47);printf(index3: %d\n, index);
index binarySearch(nums, len, 5);printf(index4: %d\n, index);
return 0;
} 数据结构
1、结构体
1结构体的使用
1.1 大小计算 结构体占所有字节的大小注意是所有字节 下面图片中大小4 4 32 40 1.2 字节对齐 默认最大的类型对齐最大类型为int型 每次都是以4个字节进行分配不满4个字节都分配4个字节。图中按理论为38由于字节对齐分配了40个字节多余出2个字节。 1.3 清除脏数据 memset的第一个参数是void *类型的因此要做取地址操作。
memset(stu, 0, sizeof(stu));
1.4 结构体的赋值 数组名是数组的首地址常量不可以更改。正确做法是使用strcpy函数对字符串进行复制。
stu.name taowanbao;//error
strcpy(stu.name,taowanbao);
2typedef的作用 typedef相当于给结构体另取一个名字。 底部为结构体新名字。 没有使用typedef之前为struct StuInfo stu; 使用typedef之后为StuInfo stu;
3结构体数组
3.1 定义 StuInfo info[SIZE];int len sizeof(info);printf(len:%d\n, len);/* 清除脏数据 */memset(info, 0, sizeof(info));
3.2 结构体数组的打印
/* 值传递 : 浪费资源 */
/* 地址传递 : 节省资源 */
int printStruct(StuInfo *pInfo, int size)
{int ret 0;if (pInfo NULL){return 0;}for (int idx 0; idx size; idx){/* 结构体指针取值使用的是 - */printf(age:%d\t, sex:%c\t, name:%s\t, height:%d\n, pInfo[idx].age, pInfo[idx].sex, pInfo[idx].name, pInfo[idx].height);}return ret;
}
4指针结构体数组 stu.address是定义了指针类型char *address一定要对其内存进行分配不然会报段错误。
StuInfo stu;memset(stu, 0, sizeof(stu));// stu.address jiangshuyancheng;stu.address (char *)malloc(sizeof(char) * BUFFER_SIZE);if (stu.address NULL){perror(malloc error);exit(-1);}strcpy(stu.address, jiangshuyancheng);
5全部代码段
#include stdio.h
#include string.h
#include stdlib.h
#define BUFFER_SIZE 32
#define SIZE 3
/* typedef 取别名 */
/* 字节对齐 */
typedef struct StuInfo
{int age; // 4char sex; // 1#if 0char name[BUFFER_SIZE]; // 32#elsechar * address;#endifchar height; // 1
} StuInfo;
#if 0
/* 值传递 : 浪费资源 */
/* 地址传递 : 节省资源 */
int printStruct(StuInfo *pInfo, int size)
{int ret 0;if (pInfo NULL){return 0;}for (int idx 0; idx size; idx){/* 结构体指针取值使用的是 - */printf(age:%d\t, sex:%c\t, name:%s\t, height:%d\n, pInfo[idx].age, pInfo[idx].sex, pInfo[idx].name, pInfo[idx].height);}return ret;
}
#endif
int main()
{
#if 0/* 结构体的基本使用 */StuInfo stu1;
int len sizeof(stu1);printf(len:%d\n, len);
len sizeof(struct StuInfo);printf(len:%d\n, len);
/* 清空脏数据 */memset(stu1, 0, sizeof(stu1));
/* 赋值 */
#if 0/* 数组名是数组的首地址, 常量不可以更改 */stu1.name zhangsan;
#endifstrcpy(stu1.name, zhangsan);stu1.age 20;stu1.height 60;stu1.sex m;
/* 写一个函数, 打印该结构体 */printStruct(stu1);
#endif
#if 0StuInfo info[SIZE];int len sizeof(info);printf(len:%d\n, len);/* 清除脏数据 */memset(info, 0, sizeof(info));
info[0].age 20;
#if 0// info[0].name lisi
#else strcpy(info[0].name, lisi);
#endifinfo[0].sex m;info[0].height 70;
info[1].age 30;
#if 0// info[0].name lisi
#else strcpy(info[1].name, zhangsan);
#endifinfo[1].sex f;info[1].height 50;info[2].age 40;
#if 0// info[0].name lisi
#else strcpy(info[2].name, wangwu);
#endifinfo[2].sex m;info[2].height 100;
#if 0for(int idx 0; idx SIZE; idx){printStruct((info[idx]));}
#elseprintStruct(info, sizeof(info) / sizeof(info[0]));
#endif
#endif
#if 1StuInfo stu;memset(stu, 0, sizeof(stu));// stu.address jiangshuyancheng;stu.address (char *)malloc(sizeof(char) * BUFFER_SIZE);if (stu.address NULL){perror(malloc error);exit(-1);}strcpy(stu.address, jiangshuyancheng);stu.age 22;stu.height 65;stu.sex m;
printf(age:%d\t, sex:%c\t, address:%s\t, height:%d\n, stu.age, stu.sex, stu.address, stu.height);
#endif
return 0;
}
2、动态数组
1. 静态数组 数组分类字符数组整形数组字符串数组结构体数组。 优点 “静态”数组的大小在编译期间就确定 内存大小已经分配确定的大小 其内存在使用结束后由计算机自动释放效率高。 静态数组的局限性1由于分配空间的大小是固定的所以当需要插入的数据大于容量时再插入会导致访问非法地址这种情况会导致栈粉碎。 静态数组的局限性2由于分配空间的大小是固定的所以当需要插入的数据远小于分配的空间时会造成内存空间的浪费。
2. 动态数组 定义 /* 动态数组结构体 */
struct DynamicArray
{/* 数据 */ELEMENTTYPE * data;/* 元素个数 */int size;/* 容量大小 */int capacity;
}; 动态数组通常使用连续的内存空间来存储元素这使得访问数组中的元素变得非常高效通常是O(1)的时间复杂度。然而当数组需要增长时则分配更大的内存块并将现有元素复制到新的内存位置。这个操作可能是昂贵的但由于它发生的频率相对较低因此动态数组仍然是一种非常高效的数据结构。 动态数组的实现通常涉及到以下关键操作 初始化为数组分配初始的内存空间。 /* 动态数组初始化 */
int dynamicArrayInit(DynamicArray *pArray, int capacity)
{int ret 0;if (pArray NULL){printf(null ptr error\n);return NULL_PTR;}
if (capacity 0){capacity DEFAULR_CAPACITY;}
/* 分配堆空间 */ pArray-data (ELEMENTTYPE *)malloc(sizeof(ELEMENTTYPE) * capacity);if (pArray-data NULL){printf(malloc error\n);return MALLOC_ERROR;}/* 清除脏数据 */memset(pArray-data, 0, sizeof(ELEMENTTYPE) * capacity);
/* 初始化的时候, 元素个数为0 */pArray-size 0;pArray-capacity capacity;
return ON_SUCCESS;
} 扩容当数组的空间不足以容纳更多元素时分配更大的内存空间并将现有元素复制到新的内存位置。 扩容操作的步骤
扩容之后开始将数据迁移。 先备份数据 tmp ptr 申请更大的容量ptr 将备份的数据放在ptr内存中 /* 静态函数: 只在本源文件中使用 */ static int expandDynamicArrayCapacity(DynamicArray *pArray) { int ret 0; // 初始化返回值为0表示成功 /* 1. 数据备份 */ // 备份当前数组的数据指针便于后续的数据迁移 ELEMENTTYPE * tmpData pArray-data; /* 2. 需要扩大的容量 */ // 计算需要扩大的新容量。这里采用1.5倍的策略pArray-capacity 1 相当于 pArray-capacity / 2 int needExpandCapacity pArray-capacity (pArray-capacity 1); // 分配新内存空间给数组并更新数组的数据指针 pArray-data (ELEMENTTYPE *)malloc(sizeof(ELEMENTTYPE) * needExpandCapacity); if (pArray-data NULL) // 判断内存分配是否成功 { return MALLOC_ERROR; // 如果分配失败返回错误代码 } /* 清除脏数据 */ // 使用 memset 函数将新分配的内存空间清零防止未初始化数据带来的潜在问题 memset(pArray-data, 0, sizeof(ELEMENTTYPE) * needExpandCapacity); /* 3. 数据迁移 */ // 将旧数据从备份指针 tmpData 迁移到新分配的数组空间中 for (int idx 0; idx pArray-size; idx) { pArray-data[idx] tmpData[idx]; } /* 4. 释放内存 */ // 释放旧的内存空间防止内存泄漏 if (tmpData ! NULL) { free(tmpData); tmpData NULL; // 释放后将指针置为 NULL避免野指针的出现 } /* 5. 更新数组的属性 */ // 更新数组的容量属性记录新的容量大小 pArray-capacity needExpandCapacity; return ret; // 返回0表示扩容操作成功 } 获取数组的元素个数 /* 获取数组的元素个数 */
int dynamicArrayGetSize(DynamicArray *pArray, int *pSize)
{if (pArray NULL){return NULL_PTR;}
if (pSize ! NULL){/* 解引用 */*pSize pArray-size;}
return ON_SUCCESS;
} 缩容如果数组的空间远远超过当前需要的空间可能会释放部分内存以节省空间。 与扩容执行相反对操作
/* 缩容 */ if (pArray-size pArray-capacity - (pArray-capacity 1)) { shrinkDynamicArrayCapacity(pArray); } for (int idx pos; idx pArray-size - 1; idx) { pArray-data[idx] pArray-data[idx 1]; }
插入 默认插到数组的默认位置结尾 /* 动态数组插入元素 (默认插到数组末尾位置) */
int dynamicArrayInsertData(DynamicArray *pArray, ELEMENTTYPE data)
{return dynamicArrayAppointPosInsertData(pArray, pArray-size, data);
} 在数组的指定位置插入一个元素。如果需要可能会涉及到扩容操作。
/* 动态数组在指定位置插入元素 */ int dynamicArrayAppointPosInsertData(DynamicArray *pArray, int pos, ELEMENTTYPE data) { int ret 0; /* 判空 */ if (pArray NULL) { printf(null ptr error\n); return NULL_PTR; } /* 判断位置是否合法 */ if (pos 0 || pos pArray-size) { return INVALID_ACCESS; } /* 空间不足预警算法是: 元素个数的1.5倍 数组容量 */ if (pArray-size (pArray-size 1) pArray-capacity) { /* 扩容 */ expandDynamicArrayCapacity(pArray); } /* 从后往前移动 */ for (int idx pArray-size; idx pos; idx--) { pArray-data[idx] pArray-data[idx - 1]; } /* 赋值 */ pArray-data[pos] data; /* 更新数组属性 */ (pArray-size); return ret; } 删除 从数组的默认位置删除元素 /* 动态数组删除元素 (默认删除数组末尾位置) */
int dynamicArrayDeleteData(DynamicArray *pArray)
{return dynamicArrayAppointPosDeleteData(pArray, pArray-size - 1);
}
//缩容
static int shrinkDynamicArrayCapacity(DynamicArray *pArray)
{/* 1. 数据备份 */ELEMENTTYPE * tmpData pArray-data;
/* 2. 开辟一块更小的空间 */int needShrinkCapacity pArray-capacity - (pArray-capacity 1);pArray-data (ELEMENTTYPE *)malloc(sizeof(ELEMENTTYPE) * needShrinkCapacity);if (pArray-data NULL){return MALLOC_ERROR;}/* 清除脏数据 */memset(pArray-data, 0, sizeof(ELEMENTTYPE) * needShrinkCapacity);
/* 3. 数据迁移 */for (int idx 0; idx pArray-size; idx){pArray-data[idx] tmpData[idx];}
/* 4. 释放内存 */if (tmpData ! NULL){free(tmpData);tmpData NULL;}
/* 5. 更新数组属性 */pArray-capacity needShrinkCapacity;
return ON_SUCCESS;
} 从数组的指定位置删除一个元素。如果需要可能会涉及到缩容操作。 /* 动态数组在指定位置删除元素 */
int dynamicArrayAppointPosDeleteData(DynamicArray *pArray, int pos)
{/* 判空 */if (pArray NULL){return NULL_PTR;}/* 判断位置合法性 */if (pos 0 || pos pArray-size){return INVALID_ACCESS;}
/* 缩容 */if (pArray-size pArray-capacity - (pArray-capacity 1)){shrinkDynamicArrayCapacity(pArray);}
for (int idx pos; idx pArray-size - 1; idx){pArray-data[idx] pArray-data[idx 1];}
/* 更新数组的属性 */(pArray-size)--;
return ON_SUCCESS;
} 访问通过索引访问数组中的元素。这是一个非常高效的操作通常是O(1)的时间复杂度。 销毁 /* 销毁动态数组 */
int dynamicArrayDestroy(DynamicArray *pArray)
{if (pArray NULL){return NULL_PTR;}
if (pArray-data ! NULL){free(pArray-data);pArray-data NULL;}
return ON_SUCCESS;
}
3、链表
链表是一种基本的数据结构用于在计算机科学中组织数据。它由一系列节点组成每个节点包含两个部分一个存储数据的字段和一个指向下一个节点的指针。链表有不同的变体包括单向链表、双向链表和循环链表。以下是链表的详细介绍以及它的优点和缺点 链表的基本概念 单向链表 (Singly Linked List) 每个节点有两个部分数据字段和指向下一个节点的指针。 第一个节点称为头节点head最后一个节点的指针为空null表示链表的结束。 双向链表 (Doubly Linked List) 每个节点有三个部分数据字段、指向下一个节点的指针和指向前一个节点的指针。 头节点的前一个指针为空尾节点的下一个指针为空。 循环链表 (Circular Linked List) 链表的最后一个节点指向第一个节点从而形成一个循环。 可以是单向或双向循环链表。 链表的优点 动态大小链表的大小可以在运行时动态调整不需要预先定义大小。与数组相比链表可以更灵活地管理内存。 插入和删除操作高效在链表中插入和删除节点的操作非常高效特别是当你已经持有对相关节点的引用时。这些操作的时间复杂度是 O(1)因为只需更新相应的指针即可。 避免内存碎片链表在内存中可以分散存储不需要连续的内存块这减少了内存碎片的风险。 链表的缺点 空间开销每个节点除了存储数据外还需要额外的指针来引用其他节点这增加了空间开销。特别是双向链表每个节点有两个指针开销更大。 随机访问困难链表不支持直接访问元素访问链表中的第 n个元素需要从头节点开始遍历时间复杂度为 O(n)。相比于数组链表在随机访问方面效率较低。 额外的操作复杂性在链表中操作节点的插入和删除可能需要更新多个指针特别是在双向链表中这增加了实现的复杂性。 缓存局部性差链表中的节点在内存中可能不是连续存储的这可能导致缓存未命中从而影响访问速度。数组则在内存中是连续存储的能够利用缓存优势。
1概念 struct LinkNode { /* 数据域 */ ELEMENTTYPE data; /* 指针域 */ struct LinkNode *next; };
struct LinkList { /* 链表的长度(结点个数) */ int size; /* 链表的头结点 (虚拟头结点) */ LinkNode * head; /* 链表的尾指针 */ LinkNode * tail; };
2链表初始化
/* 链表初始化 */
int LinkListInit(LinkList **pList)
{ /* 初始化 */LinkList *list NULL;do{list (LinkList *)malloc(sizeof(LinkList) * 1);if (list NULL){perror(malloc error);break;}/* 清除脏数据 */memset(list, 0, sizeof(LinkList) * 1);
list-head createLinkNode(0);//链表中需要创建结点if (list-head NULL){perror(malloc error);break;}/* 初始化的时候, 将尾指针指向头 */list-tail list-head;
/* 初始化链表结点个数为0. */list-size 0;
/* 解引用 */*pList list;
return ON_SUCCESS;} while(0);
/* 释放堆空间 */if (list ! NULL list-head ! NULL){free(list-head);list-head NULL;}if (list ! NULL){free(list);list NULL;}return MALLOC_ERROR;
}
3创建一个链表结点
/* 创建一个链表结点 */
static LinkNode *createLinkNode(ELEMENTTYPE data)
{LinkNode * newNode (LinkNode *)malloc(sizeof(LinkNode) * 1);if (newNode NULL){perror(malloc error);return NULL;}/* 清除脏数据 */memset(newNode, 0, sizeof(LinkNode) * 1);newNode-data data;newNode-next NULL;
return newNode;
} 4指定位置插入 虚拟头结点没有任何数据他只有指针域没有数据域 头插和尾插只需要调用该接口就行位置分别为0和size。
/* 链表任意位置插 */ int LinkListAppointPosInsert(LinkList *pList, int pos, ELEMENTTYPE data) { /* 判空 */ if (pList NULL) { return NULL_PTR; } /* 判断位置合法性 todo... */ if (pos 0 || pos pList-size) { return INVALID_ACCESS; } /* 把数据封装成结点 */ LinkNode * newNode createLinkNode(data); if (newNode NULL) { perror(malloc error); return MALLOC_ERROR; } /* head 是虚拟头结点 */ LinkNode * travelNode pList-head; int flag 0; if (pos pList-size) { travelNode pList-tail; /* 需要修改尾指针的标记 */ flag 1; } else { while (pos) { travelNode travelNode-next; pos--; } } /* 挂结点 */ newNode-next travelNode-next; // 1 travelNode-next newNode; // 2 if (flag 1) { /* 移动尾指针 */ pList-tail newNode; } /* 链表的元素个数加一 */ (pList-size); return ON_SUCCESS; }
5链表遍历
/* 链表的遍历 */
int LinkListForeach(LinkList *pList, int (*printFunc)(ELEMENTTYPE))
{/* 判空 */if (pList NULL){return NULL_PTR;}
LinkNode * travelNode pList-head-next;
while (travelNode ! NULL){
#if 0printf(val: %d\n, *(int *)(travelNode-data));
#elseprintFunc(travelNode-data);
#endif
/* 查找下一个结点 */travelNode travelNode-next;}
return ON_SUCCESS;
}
6链表的长度
/* 获取链表的长度 */
int LinkListGetSize(LinkList *pList, int *pSize)
{if (pList NULL){return NULL_PTR;}
if (pSize){*pSize pList-size;}return ON_SUCCESS;
}
7指定位置删除 /* 链表删除任意位置 */ int LinkListAppointPosDelete(LinkList *pList, int pos) { /* 判空 */ if (pList NULL) { return NULL_PTR; } /* 判断位置合法性 */ if (pos 0 || pos pList-size - 1) { return INVALID_ACCESS; } LinkNode * travelNode pList-head; int flag 0; if (pos pList-size - 1) { /* 需要移动尾指针的标记 */ flag 1; } while (pos) { travelNode travelNode-next; pos--; } /* 退出循环的条件: travelNode是我要删除结点的前一个结点 */ LinkNode * delNode travelNode-next; travelNode-next delNode-next; if (flag 1) { pList-tail travelNode; } /* 释放堆空间 */ if (delNode ! NULL) { free(delNode); delNode NULL; } /* 链表的元素个数减一 */ pList-size--; return ON_SUCCESS; }
8删除指定的值
/* 根据值获得链表的位置 */
static int LinkListAccordAppointDataGetPos(LinkList *pList, ELEMENTTYPE data, int *pos, int (*compareFunc)(ELEMENTTYPE arg1, ELEMENTTYPE arg2))
{ LinkNode * travelNode pList-head-next;
int position 0;while (travelNode ! NULL){int cmp compareFunc(data, travelNode-data);if (cmp 0){*pos position;return position;}travelNode travelNode-next;position;}*pos -1;
return -1;
}
/* 链表删除任意的值 */
int LinkListAppointDataDelete(LinkList *pList, ELEMENTTYPE data, int (*compareFunc)(ELEMENTTYPE, ELEMENTTYPE))
{if(pList NULL){return NULL_PTR;}
int pos -1;while (LinkListAccordAppointDataGetPos(pList, data, pos, compareFunc) ! -1){LinkListAppointPosDelete(pList, pos);}
return ON_SUCCESS;
} 9在链表末尾位置插入元素 尾指针的引入目的是减少时间消耗优化性能直接把尾指针放在末尾避免了指针从头的遍历至末尾 10链表头部插入元素 11主函数 代码中 LinkListInit里面参数是二级指针所以必须定义一个一级指针然后再传进一级指针的地址。 这里只给出定义
int main()
{LinkList *list NULL;LinkListInit(list);LinkListForeach(list, printData);printf(\n);return 0;
}
12其他接口
int printData(void *arg)
{int val *(int *)arg;printf(val:%d\t, val);
}
typedef struct StuInfo
{int age;char name[BUFFER_SIZE];
} StuInfo;
int printStruct(void *arg)
{int ret 0;StuInfo * stu (StuInfo *)arg;printf(age:%d,\t name:%s\n, stu-age, stu-name);
return ret;
}
int compareFunc(void *arg1, void *arg2)
{int val1 *(int *)arg1;int val2 *(int *)arg2;
return val1 - val2;
}
13链表虚拟头结点
使用了虚拟节点dummyNode它与只用真实链表头节点的写法相比有几个显著的区别 简化边界条件 虚拟头节点可以简化处理头部插入或删除的操作因为虚拟头节点使得链表头部成为一种常规的节点避免了单独处理头节点的特殊情况。 真实头节点需要特别处理链表头部的插入、删除等操作尤其是在处理链表的首节点时可能需要额外的逻辑。 代码一致性 虚拟头节点允许将所有节点处理逻辑统一化使代码更简洁和一致。例如在翻转链表或分组操作中无论处理链表的哪一部分操作方式都是一致的。 真实头节点在处理链表的边界如链表头部时通常需要分别考虑边界条件这可能导致代码复杂度增加。 避免空指针错误 虚拟头节点提供了一个始终有效的起始节点减少了因头节点为空或链表操作导致的空指针错误。 真实头节点在某些情况下如链表为空需要特别处理可能出现的空指针问题。 示例对比
使用虚拟头节点的情况
ListNode *dummyNode new ListNode(-1);
dummyNode-next head;
return dummyNode-next; 使用虚拟头节点可以在处理头部时简化逻辑例如在翻转链表时无需特别处理头节点的边界情况。
不使用虚拟头节点的情况
ListNode *prev nullptr;
ListNode *cur head;
ListNode *next nullptr;
if (cur ! nullptr cur-next ! nullptr) {// 处理头节点的特殊情况
}
return prev; 不使用虚拟头节点时处理头节点时可能需要额外的逻辑特别是在插入或删除操作中需要考虑链表的空情况或特殊位置。
14反转k组一个链表 力扣25题
1.使用虚拟头节点
ListNode* reverseKGroup(ListNode* head, int k) {// 创建虚拟头节点并将其 next 指向链表的头节点ListNode dummy;dummy.next head;ListNode* prevGroupEnd dummy; // 记录前一组翻转的尾节点
// 计算链表的总节点数ListNode* cur head;int count 0;while (cur ! nullptr) {count;cur cur-next;}
// 翻转链表中的每一组 k 个节点while (count k) {ListNode* groupStart prevGroupEnd-next; // 当前组的开始节点ListNode* prev nullptr; // 前一个节点初始为 nullptrListNode* next nullptr; // 用于暂存当前节点的下一个节点cur groupStart;
// 翻转当前组的 k 个节点for (int i 0; i k; i) {next cur-next; // 保存下一个节点cur-next prev; // 当前节点指向前一个节点完成翻转prev cur; // 更新前一个节点为当前节点cur next; // 当前节点向后移动}
// 完成当前组的翻转连接前一组的尾节点prevGroupEnd-next-next cur; // 当前组的头节点变成尾节点指向后续部分prevGroupEnd-next prev; // 前一组的尾节点指向翻转后的当前组的头节点
prevGroupEnd prevGroupEnd-next; // 更新前一组尾节点为当前组的尾节点count - k; // 减去已经处理的节点数}
// 返回虚拟头节点的 next 指向的节点作为新的头节点return dummy.next;
}
2.不使用虚拟头结点
ListNode* reverseKGroup(ListNode* head, int k) {// 计算链表中节点的总数ListNode *cur head;int count 0;while (cur ! nullptr) {count; // 统计节点总数cur cur-next;}
// 如果链表节点总数小于 k直接返回原链表if (count k) {return head;}
// 逆转前 k 个节点的链表ListNode *prev nullptr; // 前一个节点初始为 nullptrListNode *next nullptr; // 用于暂存当前节点的下一个节点cur head;
// 翻转 k 个节点for (int i 0; i k; i) {next cur-next; // 保存下一个节点cur-next prev; // 当前节点指向前一个节点完成翻转prev cur; // 更新前一个节点为当前节点cur next; // 当前节点向后移动}
// 递归处理剩余链表中的节点// 如果剩余链表存在节点即 cur 不为空继续递归处理if (cur ! nullptr) {head-next reverseKGroup(cur, k); // 将翻转后的链表连接到当前部分的尾部}
// 返回翻转后的新头节点return prev;}
}; 4、双向链表
双向链表Doubly Linked List是一种链表数据结构的变体每个节点包含两个指针一个指向前一个节点prev一个指向后一个节点next。这种结构使得在链表中进行双向遍历成为可能既可以从头到尾遍历也可以从尾到头遍历。 双向链表的基本结构 每个节点通常包含三个部分 数据域Data存储节点的数据。 前驱指针Prev指向链表中的前一个节点。 后继指针Next指向链表中的下一个节点。 双向链表的操作 创建节点 在创建节点时需要初始化 prev 和 next 指针。 插入节点 在头部插入将新节点的 next 指针指向原头节点原头节点的 prev 指针指向新节点然后更新头指针为新节点。 在尾部插入将新节点的 prev 指针指向原尾节点原尾节点的 next 指针指向新节点然后更新尾指针为新节点。 在中间插入调整前驱节点和后继节点的 next 和 prev 指针以插入新节点。 删除节点 删除头部节点将头指针更新为下一个节点并将新的头节点的 prev 指针设置为 NULL。 删除尾部节点将尾指针更新为前一个节点并将新的尾节点的 next 指针设置为 NULL。 删除中间节点调整前驱节点和后继节点的 next 和 prev 指针以删除目标节点。 遍历 正向遍历从头节点开始依次访问每个节点的 next 指针。 反向遍历从尾节点开始依次访问每个节点的 prev 指针。 双向链表优点 双向遍历可以从任意节点向前或向后遍历链表。这使得在某些操作中如双向查找或修改节点变得更加高效和灵活。 更高效的删除操作删除一个节点时无论是从头部、中间还是尾部只需要调整相邻节点的指针。相比于单向链表中的删除操作需要从头部遍历到目标节点双向链表的删除操作效率更高因为可以直接访问前一个节点。 方便的插入操作在双向链表中插入节点操作相对简单因为可以直接修改相邻节点的指针而不需要遍历链表。 容易实现某些算法双向链表使得实现一些需要双向访问的数据结构和算法变得更加简单如LRU缓存最近最少使用缓存等。 双向链表缺点 更高的内存消耗每个节点除了存储数据外还需要额外的空间来存储两个指针prev 和 next。这导致双向链表比单向链表占用更多的内存。 增加了实现复杂度在双向链表中节点的插入、删除和移动操作需要同时更新两个指针prev 和 next这增加了代码的复杂性。 维护指针的复杂性在进行节点插入和删除操作时必须非常小心地维护两个指针以避免出现指针悬挂或链表断裂的问题。
1双向链表的定义
struct DoubleLinkNode
{/* 数据域 */ELEMENTTYPE data;/* 指针域 */struct DoubleLinkNode *prev; /* prev指针指向前结点 */struct DoubleLinkNode *next; /* next指针 */
};
/* 创建一个链表结点 */
static DoubleLinkNode *createDoubleLinkNode(ELEMENTTYPE data)
{DoubleLinkNode * newNode (DoubleLinkNode *)malloc(sizeof(DoubleLinkNode) * 1);if (newNode NULL){perror(malloc error);return NULL;}/* 清除脏数据 */memset(newNode, 0, sizeof(DoubleLinkNode) * 1);newNode-data data;newNode-prev NULL;newNode-next NULL;
return newNode;
} 2在指定位置插入包括头插和尾插 当双向链表是空链表以及双向链表头插都满足如中右上角的条件 尾插时不满足其中一个条件如图中的第3个条件。 /* 链表任意位置插 */ int DoubleLinkListAppointPosInsert(DoubleLinkList *pList, int pos, ELEMENTTYPE data) { /* 判空 */ if (pList NULL) { return NULL_PTR; } /* 判断位置合法性 todo... */ if (pos 0 || pos pList-size) { return INVALID_ACCESS; } /* 把数据封装成结点 */ DoubleLinkNode * newNode createDoubleLinkNode(data); if (newNode NULL) { perror(malloc error); return MALLOC_ERROR; } /* head 是虚拟头结点 */ DoubleLinkNode * travelNode pList-head; int flag 0; if (pos pList-size) { travelNode pList-tail; /* 需要修改尾指针的标记 */ flag 1; } else { while (pos) { travelNode travelNode-next; pos--; } } /* 挂结点 */ newNode-next travelNode-next; // 1 newNode-prev travelNode; // 2 if (flag 0) { travelNode-next-prev newNode; // 3 } travelNode-next newNode; // 4 if (flag 1) { /* 移动尾指针 */ pList-tail newNode; } /* 链表的元素个数加一 */ (pList-size); return ON_SUCCESS; }
3在指定位置删除 头删与尾删也适用 /* 链表删除任意位置 */ int DoubleLinkListAppointPosDelete(DoubleLinkList *pList, int pos) { /* 判空 */ if (pList NULL) { return NULL_PTR; } /* 判断位置合法性 */ if (pos 0 || pos pList-size - 1) { return INVALID_ACCESS; } DoubleLinkNode * travelNode pList-head; DoubleLinkNode * delNode NULL; int flag 0; if (pos pList-size - 1) { /* 需要移动尾指针的标记 */ flag 1; /* 尾指针指向的结点 是要删除的结点 */ delNode pList-tail; /* 尾指针移动 */ travelNode delNode-prev; /* 手动置为NULL */ travelNode-next NULL; } else { while (pos) { travelNode travelNode-next; pos--; } /* 退出循环的条件: travelNode是我要删除结点的前一个结点 */ delNode travelNode-next; travelNode-next delNode-next; delNode-next-prev travelNode; } if (flag 1) { pList-tail travelNode; } /* 释放堆空间 */ if (delNode ! NULL) { free(delNode); delNode NULL; } /* 链表的元素个数减一 */ pList-size--; return ON_SUCCESS; }
4遍历与反向遍历 因为具有双向因此可以进行逆序遍历
/* 链表的遍历 */
int DoubleLinkListForeach(DoubleLinkList *pList, int (*printFunc)(ELEMENTTYPE))
{/* 判空 */if (pList NULL){return NULL_PTR;}
DoubleLinkNode * travelNode pList-head-next;
while (travelNode ! NULL){
#if 0printf(val: %d\n, *(int *)(travelNode-data));
#elseprintFunc(travelNode-data);
#endif
/* 查找下一个结点 */travelNode travelNode-next;}
return ON_SUCCESS;
}
/* 链表的反向遍历 */
int DoubleLinkListReverseForeach(DoubleLinkList *pList, int (*printFunc)(ELEMENTTYPE))
{/* 判空 */if (pList NULL){return NULL_PTR;}
DoubleLinkNode * travelNode pList-tail;while (travelNode ! pList-head){printFunc(travelNode-data);
travelNode travelNode-prev;}
/* 退出条件是: 碰到虚拟头结点 */return ON_SUCCESS;
}
5删除指定的值
/* 根据值 获得链表的位置 */
static int DoubleLinkListAccordAppointDataGetPos(DoubleLinkList *pList, ELEMENTTYPE data, int *pos, int (*compareFunc)(ELEMENTTYPE arg1, ELEMENTTYPE arg2))
{ DoubleLinkNode * travelNode pList-head-next;
int position 0;while (travelNode ! NULL){int cmp compareFunc(data, travelNode-data);if (cmp 0){*pos position;return position;}travelNode travelNode-next;position;}*pos -1;
return -1;
}
/* 链表删除任意的值 */
int DoubleLinkListAppointDataDelete(DoubleLinkList *pList, ELEMENTTYPE data, int (*compareFunc)(ELEMENTTYPE, ELEMENTTYPE))
{if(pList NULL){return NULL_PTR;}
int pos -1;while (DoubleLinkListAccordAppointDataGetPos(pList, data, pos, compareFunc) ! -1){DoubleLinkListAppointPosDelete(pList, pos);}
return ON_SUCCESS;
}
6获取链表的元素
/* 获取链表任意位置的元素 */
int DoubleLinkListGetAppointPositionData(DoubleLinkList *pList, int pos, ELEMENTTYPE *data)
{/* 判空 */if (pList NULL){return NULL_PTR;}
/* 判断位置的合法性 */if (pos 0 || pos pList-size - 1){return INVALID_ACCESS;}
DoubleLinkNode * travelNode pList-head-next;
/* 取最后一个元素 */if (pos pList-size - 1){travelNode pList-tail;}else{while (pos){travelNode travelNode-next;pos--;}/* 出了这个循环, travelNode到底是啥? *//* travelNode 就是我要找的结点 */}
if (data){*data travelNode-data;}
return ON_SUCCESS;
}
5、队列
1调用双向链表的接口 与双向链表有相同的地方直接调用双向链表的接口
#ifndef __COMMON_H__
#define __COMMON_H__
#define ELEMENTTYPE void *
typedef struct DoubleLinkNode DoubleLinkNode;
typedef struct DoubleLinkList DoubleLinkList;
#endif //__COMMON_H__ doubleLinkListQueue.c
#include doubleLinkListQueue.h
/* 队列初始化 */
int doubleLinkListQueueInit(DoubleLinkListQueue **queue)
{return DoubleLinkListInit(queue);
}
/* 队列入队 */
int doubleLinkListQueuePush(DoubleLinkListQueue *queue, ELEMENTTYPE data)
{return DoubleLinkListTailInsert(queue, data);
}
/* 队列出队 */
int doubleLinkListQueuePop(DoubleLinkListQueue *queue)
{return DoubleLinkListHeadDelete(queue);
}
/* 队列的队头元素 */
int doubleLinkListQueueFront(DoubleLinkListQueue *queue, ELEMENTTYPE *data)
{return DoubleLinkListGetHeadPositionData(queue, data);
}
/* 队列的队尾元素 */
int doubleLinkListQueueRear(DoubleLinkListQueue *queue, ELEMENTTYPE *data)
{return DoubleLinkListGetTailPositionData(queue, data);
}
/* 队列的元素个数 */
int doubleLinkListQueueGetSize(DoubleLinkListQueue *queue, int *pSize)
{return DoubleLinkListGetSize(queue, pSize);
}
/* 队列是否为空 */
int doubleLinkListQueueIsEmpty(DoubleLinkListQueue *queue)
{int size 0;DoubleLinkListGetSize(queue, size);
return size 0 ? 1 : 0;
}
/* 队列销毁 */
int doubleLinkListQueueDestroy(DoubleLinkListQueue *queue)
{return DoubleLinkListDestroy(queue);
}
2主函数
#include stdio.h
#include doubleLinkListQueue.h
#define BUFFER_SIZE 5
int main()
{/* 队列初始化 */DoubleLinkListQueue *queue NULL;doubleLinkListQueueInit(queue);int nums[BUFFER_SIZE] {11, 22, 33, 44, 55};/* 入队 */for (int idx 0; idx BUFFER_SIZE; idx){doubleLinkListQueuePush(queue, (void *)nums[idx]);}
int size 0;doubleLinkListQueueGetSize(queue, size);printf(size %d\n, size);
/* 队列非空 */int *frontValue NULL;while (!doubleLinkListQueueIsEmpty(queue)){doubleLinkListQueueFront(queue, (void *)frontValue);/* 出队 */doubleLinkListQueuePop(queue);
printf(frontValue:%d\n, *frontValue);}
/* 释放队列 */doubleLinkListQueueDestroy(queue);
return 0;
}
6、区别总结
1. 数组Array
数组是一个线性数据结构存储在连续的内存位置中。每个元素通过一个索引来访问索引通常从0开始。数组的特点是访问速度快通过索引可以在O(1)时间内访问但插入和删除元素的效率较低需要移动元素时间复杂度为O(n)。
优点 访问元素快。 易于实现和使用。
缺点 大小固定难以动态扩展。 插入和删除操作复杂。
2. 链表Linked List
链表是由一组节点组成的数据结构每个节点包含数据和指向下一个节点的指针。链表中的节点不需要存储在连续的内存位置因此大小可以动态扩展。
优点 动态大小易于插入和删除元素时间复杂度为O(1)。 不需要预先分配内存。
缺点 访问速度慢需要从头遍历时间复杂度为O(n)。 额外的指针存储会消耗更多内存。
3. 队列Queue
队列是一种先进先出FIFOFirst-In-First-Out的线性数据结构元素在一端插入称为队尾在另一端移除称为队头。队列的典型应用场景包括任务调度、打印队列等。
优点 遵循FIFO原则适合处理顺序相关的任务。 插入和删除操作效率高时间复杂度为O(1)。
缺点 随机访问元素的效率低时间复杂度为O(n)。
4. 栈Stack
栈是一种后进先出LIFOLast-In-First-Out的线性数据结构元素在一端插入和移除称为栈顶。栈的典型应用场景包括表达式求值、递归调用等。
优点 遵循LIFO原则适合处理逆序操作。 插入和删除操作效率高时间复杂度为O(1)。
缺点 随机访问元素的效率低时间复杂度为O(n)。
5. 二叉树Binary Tree
二叉树是一种树形数据结构每个节点最多有两个子节点称为左子节点和右子节点。二叉树广泛应用于搜索、排序等领域。
优点 结构灵活可以表示多种数据关系例如二叉搜索树、堆等。 具备递归性质适合递归算法。
缺点 复杂性较高操作较为繁琐。 不平衡的二叉树可能退化为线性结构导致性能下降。
这些数据结构各自具有不同的应用场景理解其优缺点对于选择合适的数据结构解决实际问题非常重要。
7、二叉堆binaryHeap
1基本概念
二叉堆是一种特殊的完全二叉树用于实现优先队列。它是一种用于高效地支持最小值最小堆或最大值最大堆操作的数据结构。二叉堆有很多应用尤其在实现优先队列、堆排序等算法中非常重要。
1.基本概念
二叉堆具有以下两个主要特性 堆性质Heap Property 最小堆对于二叉堆中的每个节点其值都小于或等于其子节点的值。这样根节点包含整个堆中的最小值。 最大堆对于二叉堆中的每个节点其值都大于或等于其子节点的值。这样根节点包含整个堆中的最大值。 完全二叉树 二叉堆是完全二叉树即除了最后一层之外每一层的节点都是满的且最后一层的节点都集中在左侧。
2. 二叉堆的实现
二叉堆通常使用数组来实现。对于一个节点 i其左子节点、右子节点和父节点的索引可以通过以下公式计算 左子节点2 * i 1 右子节点2 * i 2 父节点(i - 1) / 2对于 i 大于0的情况下
3. 二叉堆的操作
以下是二叉堆常见的操作
a. 插入操作 将一个新元素插入到二叉堆中通常需要以下步骤 将元素添加到堆的末尾即数组的最后一个位置。 上滤heapify-up 或 bubble-up将插入的元素与其父节点比较如果不符合堆的性质最小堆或最大堆则交换元素并继续向上比较直到堆性质恢复为止。 时间复杂度插入操作的时间复杂度为 O(log n)其中 n 是堆中元素的个数。
b. 删除最小/最大元素堆顶元素 删除堆顶元素的步骤如下 将堆顶元素最小值或最大值与堆的最后一个元素交换。 删除最后一个元素并将原堆顶元素移到堆的末尾。 下滤heapify-down 或 sift-down从堆顶开始将其与子节点比较调整位置以恢复堆性质直到堆性质恢复为止。 时间复杂度删除操作的时间复杂度为 O(log n)。
c. 获取堆顶元素 对于最小堆堆顶元素是最小值。 对于最大堆堆顶元素是最大值。 时间复杂度获取堆顶元素的时间复杂度为 O(1)。
d. 构建堆 从一个无序的数组构建一个二叉堆可以使用“建堆”算法。该算法的步骤如下 从数组的最后一个非叶子节点开始依次对每个节点执行下滤操作直到根节点。 这样做的时间复杂度为 O(n)其中 n 是数组的大小。
4. 二叉堆的优缺点
优点 高效的插入和删除操作相对于其他数据结构如平衡二叉搜索树二叉堆在插入和删除操作上表现良好。 简单的实现使用数组表示使得实现相对简单。
缺点 不支持快速的随机访问与数组不同二叉堆不支持快速的随机访问。 堆序列的访问较复杂虽然可以高效地访问最小值或最大值但堆的其他元素访问比较复杂且堆中元素的顺序不能保证。
5. 应用 优先队列通过最小堆或最大堆来实现优先队列支持插入、删除最小/最大元素等操作。 堆排序利用二叉堆进行排序将堆顶的最小或最大值与最后一个元素交换调整堆逐步得到有序数组。 图算法如 Dijkstra 算法和 Prim 算法中都用到了优先队列可以通过二叉堆来实现。
2堆的存储方式 其存储顺序采用二叉树的顺序存储 图中根结点的索引从0开始不从1开始。注意是索引不是里面的值。 本质上是优先级队列 3小顶堆与大顶堆
3.1 小顶堆 3.2 大顶堆 4堆的定义及初始化
#define ELEMENT_TYPE void *
typedef struct BinaryHeap
{ELEMENT_TYPE * data; /* 数据域 */int size; /* 堆元素个数 */int capacity; /* 容量 */
/* 回调函数必须定义成函数指针 */int (*compareFunc)(ELEMENT_TYPE arg1, ELEMENT_TYPE arg2);
} BinaryHeap;
/* 二叉堆的初始化 */
int binaryHeapInit(BinaryHeap * heap, int (*compareFunc)(ELEMENT_TYPE arg1, ELEMENT_TYPE arg2))
{if (heap NULL){return NULL_PTR;}heap-capacity DEFALUT_CAPACITY;heap-data (ELEMENT_TYPE *)malloc(sizeof(ELEMENT_TYPE) * heap-capacity);if (heap-data NULL){perror(malloc error);exit(-1);}/* 清空脏数据 */memset(heap-data, 0, sizeof(ELEMENT_TYPE) * heap-capacity);/* 初始化元素个数 */heap-size 0;
/* 比较器 */heap-compareFunc compareFunc;
return ON_SUCCESS;
}
5堆的插入
/* 二叉堆的新增 */
int binaryHeapInsert(BinaryHeap * heap, ELEMENT_TYPE data)
{if (heap NULL){return NULL_PTR;}
/* 判断容量是否满 */if (heap-size heap-capacity){expandBinaryHeapCapacity(heap);}
heap-data[(heap-size)] data;
/* 是否满足堆的特性 *//* 上浮 */floatUp(heap, heap-size);
/* 更新元素个数 */(heap-size);
return ON_SUCCESS;
}
6上浮操作 小顶堆操作 将当前结点与其父结点进行比较若父节点比当前结点大则开始交换 /* 小顶堆 */ static int floatUp(BinaryHeap * heap, int index) { /* 当前结点的值 */ ELEMENT_TYPE curIndexVal heap-data[index];
#if 0 int cmp 0; while (index 0) //index0是根结点 { /* 父结点索引 */ int parentIndex (index - 1) 1; cmp heap-compareFunc(heap-data[index], heap-data[parentIndex]); if (cmp 0) { break; } /* 交换元素的值 */ ELEMENT_TYPE tmpData heap-data[index]; heap-data[index] heap-data[parentIndex]; heap-data[parentIndex] tmpData; index parentIndex; } #else int cmp 0; while (index 0)//此方法是为了减少两者交换将当前值复制给copynum { /* 父结点索引 */ int parentIndex (index - 1) 1; cmp heap-compareFunc(curIndexVal, heap-data[parentIndex]); if (cmp 0) { break; } /* 将父结点元素值 拷贝到 当前位置 */ heap-data[index] heap-data[parentIndex]; index parentIndex; } /* 最后赋值 */ heap-data[index] curIndexVal;
#endif return ON_SUCCESS; }
7下沉操作 开始对左、右开始比较 左边比右边大则根节点选择右边开始交换 循环直到索引index size/2 /* 下沉 */ static int sinkDown(BinaryHeap * heap, int index) { ELEMENT_TYPE currentData heap-data[index]; int cmp 0; /* 第一个叶子结点的索引 非叶子结点的数量 */ /* 必须保证index位置是非叶子结点 */ int halfIndex heap-size 1; while (index halfIndex) { /* index的结点 有两种情况 */ /* 1. 有两个子结点 */ /* 2. 有一个子结点: 一定是左结点 */ /* 默认为左子结点 */ int childIndex (index 1) 1; /* 右子结点 */ int rightIndex childIndex 1; /* 选出左右子结点中 较小的值 */ if (rightIndex heap-size heap-compareFunc(heap-data[rightIndex], heap-data[childIndex]) 0) { childIndex rightIndex; } /* 比较 */ cmp heap-compareFunc(currentData, heap-data[childIndex]); if (cmp 0) { break; } /* 将子结点的值存放到当前位置 */ heap-data[index] heap-data[childIndex]; /* 更新结点index */ index childIndex; } heap-data[index] currentData; return ON_SUCCESS; }
8扩容操作
/* 静态函数: 只在本源文件中使用 */ static int expandBinaryHeapCapacity(BinaryHeap *pArray) { int ret 0; /* 1. 数据备份 */ ELEMENT_TYPE * tmpData pArray-data; /* 2. 需要扩大的容量 */ int needExpandCapacity pArray-capacity (pArray-capacity 1);//1.5倍容量 pArray-data (ELEMENT_TYPE *)malloc(sizeof(ELEMENT_TYPE) * needExpandCapacity); if (pArray-data NULL) { return MALLOC_ERROR; } /* 清除脏数据 */ memset(pArray-data, 0, sizeof(ELEMENT_TYPE) * needExpandCapacity); /* 3. 数据迁移 */ for (int idx 0; idx pArray-size; idx) { pArray-data[idx] tmpData[idx]; } /* 4. 释放内存 */ if (tmpData ! NULL) { free(tmpData); tmpData NULL; } /* 5. 更新数组的属性 */ pArray-capacity needExpandCapacity; return ret; }
9 二叉堆的删除 删除就是从顶堆开始删 删除索引1的位置让后尾部的索引19上去最后索引19所对应的值开始执行下沉操作 更新sizesize--
/* 二叉堆的删除 */ int binaryHeapDelete(BinaryHeap * heap) { if (heap NULL) { return NULL_PTR; } /* 没有元素 */ if (heap-size 0) { return INVALID_ACCESS; } /* 至少有一个元素 */ /* 覆盖 */ heap-data[0] heap-data[--(heap-size)];//原代码heap-data[heap-size - 1];(heap-size)--; /* 下沉 */ sinkDown(heap, 0); return ON_SUCCESS; }
10堆顶元素、元素个数以及销毁
/* 二叉堆 堆顶元素 */ int binaryHeapTop(BinaryHeap * heap, ELEMENT_TYPE *data) { if (heap NULL) { return NULL_PTR; } if (data) { *data heap-data[0]; } return ON_SUCCESS; }
/* 二叉堆元素个数 */ int binaryHeapGetSize(BinaryHeap * heap, int * pSize) { if (heap NULL || pSize NULL) { return NULL_PTR; } *pSize heap-size; return ON_SUCCESS; }
/* 二叉堆是否为空 */ bool binaryHeapIsEmpty(BinaryHeap * heap) { return heap-size 0; }
/* 二叉堆的销毁 */ int binaryHeapDetroy(BinaryHeap * heap) { if (heap NULL) { return NULL_PTR; } if (heap-data ! NULL) { free(heap-data); heap-data NULL; } return ON_SUCCESS; }
11主函数测试
#include stdio.h #include string.h #include binaryHeap.h
int compareFunc(void * arg1, void * arg2) { int val1 *(int *)arg1;//解引用取里面的值 int val2 *(int *)arg2; return val1 - val2;//反过来就是大顶堆 }
int main() { BinaryHeap heap; binaryHeapInit(heap, compareFunc); int nums[6] {23, 54, 7, 16, 3, 41}; for (int idx 0; idx 6; idx) { binaryHeapInsert(heap, nums[idx]); } /* 元素个数 */ int size 0; binaryHeapGetSize(heap, size); printf(size is %d\n, size); int *topVal NULL; while (!binaryHeapIsEmpty(heap)) { /* 堆顶元素 */ binaryHeapTop(heap, (void **)topVal); binaryHeapDelete(heap); printf(topVal:%d\n, *topVal); } /* 销毁 */ binaryHeapDetroy(heap); return 0; }
8、哈希表 哈希函数把复杂类型映射成整形。-----------下标 键值对--------------------目录键值 哈希表散列表 哈希冲突拉链法拉一个链表法一个数组带多个链表的结构 查找速度最快的数据结构速度O(1)。如果出现哈希冲突的时候最差会到O(n)。
7.1 哈希表概述
哈希表Hash Table是一种数据结构用于高效地存储和检索数据。它通过哈希函数将键Key映射到一个固定大小的数组中。哈希表能够在常数时间复杂度内进行插入、删除和查找操作在理想情况下。
7.2 关键组成部分 哈希函数 定义哈希函数是一个算法它将输入键转换成数组中的索引。 作用通过哈希函数将键映射到数组中的位置从而快速存取数据。 示例对于整数键 k哈希函数 h(k) 可以定义为 k % N其中 N 是数组的大小。 数组桶 定义哈希表内部维护一个数组其中的每个元素称为“桶”或“槽”。 作用用于存储通过哈希函数映射到该位置的数据。 冲突处理 定义当两个不同的键通过哈希函数映射到同一个索引时就会发生冲突。 解决方法 链式地址法Chaining每个桶维护一个链表将冲突的元素存储在链表中。 开放地址法Open Addressing当发生冲突时探测数组中的其他位置直到找到空槽或合适位置存储数据。
7.3 哈希表的操作 插入 步骤 使用哈希函数计算键的索引。 将数据存储在计算出的索引位置。 如果发生冲突则使用冲突处理方法将数据存储在相应的位置。 示例如果要插入键值对 (key10, valueA)假设哈希函数为 h(k) k % 5则计算 10 % 5 0将 (10, A) 存储在索引 0 的位置。如果索引 0 已经有数据则链式地址法会在该位置的链表中添加数据。 查找 步骤 使用哈希函数计算键的索引。 查找计算出的索引位置的桶。 如果使用链式地址法遍历链表以找到键。 如果使用开放地址法按照探测序列查找数据。 示例查找键 10计算 10 % 5 0在索引 0 的位置查找对应的值。如果有链表则遍历链表找到对应的数据。 删除 步骤 使用哈希函数计算键的索引。 查找计算出的索引位置的桶。 如果使用链式地址法从链表中删除对应的键。 如果使用开放地址法标记位置为空或已删除。 示例删除键 10计算 10 % 5 0在索引 0 的位置查找并删除对应的数据。如果有链表则从链表中删除对应节点。
7.4 哈希表的优缺点 优点 快速操作在理想情况下插入、查找和删除操作的时间复杂度是 O(1)O(1)。 动态调整许多实现允许动态调整大小保持操作效率。 缺点 内存使用为了减少冲突哈希表通常需要额外的内存。 冲突处理开销冲突处理可能会影响性能特别是当负载因子较高时。 哈希函数的设计哈希函数的选择对性能有重要影响设计不良的哈希函数可能导致大量冲突。
7.5 简单hash举例 便于直观理解 链式地址法 #include stdio.h #include stdlib.h #include string.h
#define TABLE_SIZE 10 // 哈希表的大小
// 链表节点定义 typedef struct Node { int key; int value; struct Node* next; } Node;
// 哈希表定义 typedef struct HashTable { Node* table[TABLE_SIZE]; } HashTable;
// 创建一个新的哈希表 HashTable* create_table() { HashTable* ht (HashTable*)malloc(sizeof(HashTable)); for (int i 0; i TABLE_SIZE; i) { ht-table[i] NULL; } return ht; }
// 哈希函数 int hash(int key) { return key % TABLE_SIZE; }
// 插入一个键值对 void insert(HashTable* ht, int key, int value) { int index hash(key); Node* new_node (Node*)malloc(sizeof(Node)); new_node-key key; new_node-value value; new_node-next NULL; // 如果该索引位置为空则直接插入 if (ht-table[index] NULL) { ht-table[index] new_node; } else { // 如果位置已经有链表则将新节点添加到链表的开头 new_node-next ht-table[index];//new_node指向之前冲突的值 ht-table[index] new_node;//再更新新的值形成小标-新值-旧值形成链表 } }
// 查找键对应的值 int find(HashTable* ht, int key) { int index hash(key); Node* current ht-table[index]; while (current ! NULL) { if (current-key key) { return current-value; } current current-next; } // 如果键不存在返回 -1 return -1; }
// 删除一个键值对 void delete(HashTable* ht, int key) { int index hash(key); Node* current ht-table[index]; Node* prev NULL; // 找到要删除的节点 while (current ! NULL current-key ! key) { prev current; current current-next; } // 如果找不到该键则返回 if (current NULL) { return; } // 从链表中移除节点 if (prev NULL) { // 要删除的节点是链表的第一个节点 ht-table[index] current-next; } else { prev-next current-next; } free(current); }
// 打印哈希表内容 void print_table(HashTable* ht) { for (int i 0; i TABLE_SIZE; i) { Node* current ht-table[i]; printf(Index %d: , i); while (current ! NULL) { printf((%d, %d) - , current-key, current-value); current current-next; } printf(NULL\n); } }
// 释放哈希表内存 void free_table(HashTable* ht) { for (int i 0; i TABLE_SIZE; i) { Node* current ht-table[i]; while (current ! NULL) { Node* temp current; current current-next; free(temp); } } free(ht); }
int main() { // 创建哈希表 HashTable* ht create_table(); // 插入一些键值对 insert(ht, 1, 100); insert(ht, 2, 200); insert(ht, 12, 300); // 这个会冲突到索引 2 insert(ht, 22, 400); // 这个会冲突到索引 2 // 打印哈希表内容 print_table(ht); // 查找值 printf(Value for key 2: %d\n, find(ht, 2)); // 输出: 200 printf(Value for key 12: %d\n, find(ht, 12)); // 输出: 300 printf(Value for key 22: %d\n, find(ht, 22)); // 输出: 400 printf(Value for key 3: %d\n, find(ht, 3)); // 输出: -1 (未找到) // 删除一个键值对 delete(ht, 2); printf(After deleting key 2:\n); print_table(ht); // 释放哈希表内存 free_table(ht); return 0; } 7.6 完整hash
#ifndef __HASH_TABLE_H_ #define __HASH_TABLE_H_
#include common.h
#define SLOT_CAPACITY 10
#define HASH_KEYTYPE int #define HASH_VALUETYPE int
typedef struct hashNode { HASH_KEYTYPE real_key; HASH_VALUETYPE value; } hashNode;
typedef struct hashTable { /* 哈希表的槽位数 */ int slotNums; /* 哈希表的槽位号 (分配一块连续的存储空间) 指针数组(链表头结点) */ DoubleLinkList ** slotKeyId; /* 自定义比较器 用于适配链表数据结构 */ int (*compareFunc)(ELEMENTTYPE, ELEMENTTYPE); } HashTable;
/* 哈希表的初始化 */ int hashTableInit(HashTable** pHashtable, int slotNums, int (*compareFunc)(ELEMENTTYPE, ELEMENTTYPE));
/* 哈希表 插入key, value */ int hashTableInsert(HashTable *pHashtable, HASH_KEYTYPE key, HASH_VALUETYPE value);
/* 哈希表 删除指定key. */ int hashTableDelAppointKey(HashTable *pHashtable, HASH_KEYTYPE key);
/* 哈希表 根据key获取value. */ int hashTableGetAppointKeyValue(HashTable *pHashtable, HASH_KEYTYPE key, HASH_VALUETYPE *mapValue);
/* 哈希表元素大小 */ int hashTableGetSize(HashTable *pHashtable);
/* 哈希表的销毁 */ int hashTableDestroy(HashTable *pHashtable);
#endif //__HASH_TABLE_H_
#include stdio.h #include hashtable.h #include stdlib.h #include doubleLinkList.h #include error.h #include string.h
#define DEFAULT_SLOT_NUMS 10
/* 函数前置声明 */ static int calHashValue(HashTable *pHashtable, HASH_KEYTYPE key, int *slotKeyId); static hashNode * createHashNode(HASH_KEYTYPE key, HASH_VALUETYPE value);
/* 哈希表的初始化 */ int hashTableInit(HashTable** pHashtable, int slotNums, int (*compareFunc)(ELEMENTTYPE, ELEMENTTYPE)) { /* 判空 */ if (pHashtable NULL) { return -1; } int ret 0; HashTable * hash (HashTable *)malloc(sizeof(HashTable) * 1); if (hash NULL) { perror(malloc error); return MALLOC_ERROR; } /* 清除脏数据 */ memset(hash, 0, sizeof(HashTable) * 1); /* 判断槽位号的合法性 */ if (slotNums 0) { slotNums DEFAULT_SLOT_NUMS; } hash-slotNums slotNums; /* 动态数组分配空间 */ hash-slotKeyId (DoubleLinkList **)malloc(sizeof(DoubleLinkList *) * (hash-slotNums)); if (hash-slotKeyId NULL) { perror(malloc error); return MALLOC_ERROR; } /* 清除脏数据 */ memset(hash-slotKeyId, 0, sizeof(DoubleLinkList*) * (hash-slotNums)); /* 初始化 : 每一个槽位号内部维护一个链表. */ for (int idx 0; idx hash-slotNums; idx) { /* 为哈希表的value初始化。哈希表的value是链表的虚拟头结点 */ DoubleLinkListInit((hash-slotKeyId[idx])); } /* 自定义比较函数 钩子函数 */ hash-compareFunc compareFunc; /* 指针解引用 */ *pHashtable hash; return ret; }
/* 计算外部传过来的key 转化为哈希表内部维护的slotKeyId. slotKeyIds是数组(动态数组)索引 */ static int calHashValue(HashTable *pHashtable, HASH_KEYTYPE key, int *slotKeyId) { int ret 0; if (slotKeyId) { *slotKeyId key % (pHashtable-slotNums); } return ret; } /* 新建结点 */ static hashNode * createHashNode(HASH_KEYTYPE key, HASH_VALUETYPE value) { /* 封装结点 */ hashNode * newNode (hashNode *)malloc(sizeof(hashNode) * 1); if (newNode NULL) { return NULL; } /* 清除脏数据 */ memset(newNode, 0, sizeof(hashNode) * 1); newNode-real_key key; newNode-value value; /* 返回新结点 */ return newNode; }
/* 哈希表 插入key, value */ int hashTableInsert(HashTable *pHashtable, HASH_KEYTYPE key, HASH_VALUETYPE value) { /* 判空 */ if (pHashtable NULL) { return -1; } int ret 0; /* 将外部传过来的key 转化为我哈希表对应的slotId */ int KeyId 0; calHashValue(pHashtable, key, KeyId); /* 创建哈希node */ hashNode * newNode createHashNode(key, value); if (newNode NULL) { perror(create hash node error); return MALLOC_ERROR; } /* todo: 去重... */ /* 将哈希结点插入到链表中. */ DoubleLinkListTailInsert(pHashtable-slotKeyId[KeyId], newNode); return ret; }
/* 哈希表 删除指定key. */ int hashTableDelAppointKey(HashTable *pHashtable, HASH_KEYTYPE key) { /* 判空 */ if (pHashtable NULL) { return -1; } int ret 0; /* 将外部传过来的key 转化为我哈希表对应的slotId */ int KeyId 0; calHashValue(pHashtable, key, KeyId); hashNode tmpNode; memset(tmpNode, 0, sizeof(hashNode)); tmpNode.real_key key;
#if 1 /* todo... 删除哈希结点 */ DoubleLinkNode * resNode DoubleLinkListAppointKeyValGetNode((pHashtable-slotKeyId[KeyId]), tmpNode, pHashtable-compareFunc); if (resNode NULL) { return -1; } /* 备份哈希结点 */ hashNode * delHashNode resNode-data; #endif DoubleLinkListDelAppointData(pHashtable-slotKeyId[KeyId], tmpNode, pHashtable-compareFunc); if (delHashNode) { free(delHashNode); delHashNode NULL; } return ret; }
/* 哈希表 根据key获取value. */ int hashTableGetAppointKeyValue(HashTable *pHashtable, int key, int *mapValue) { int ret 0; /* 将外部传过来的key 转化为我哈希表对应的slotId */ int KeyId 0; calHashValue(pHashtable, key, KeyId); hashNode tmpNode; tmpNode.real_key key; DoubleLinkNode * resNode DoubleLinkListAppointKeyValGetNode((pHashtable-slotKeyId[KeyId]), tmpNode, pHashtable-compareFunc); if (resNode NULL) { return -1; } hashNode * mapNode (hashNode*)resNode-data; if (mapValue) { *mapValue mapNode-value; } return ret; }
/* 哈希表元素大小 */ int hashTableGetSize(HashTable *pHashtable) { if (pHashtable NULL) { return 0; } int size 0; for (int idx 0; idx pHashtable-slotNums; idx) { size pHashtable-slotKeyId[idx]-len; } /* 哈希表的元素个数. */ return size; }
/* 哈希表的销毁 */ int hashTableDestroy(HashTable *pHashtable) { /* 自己分配的内存自己释放 */ if (pHashtable NULL) { return 0; } /* 谁开辟空间, 谁释放空间. */ /* 1. 先释放哈希表的结点 */ for (int idx 0; idx pHashtable-slotNums; idx) { DoubleLinkNode * travelLinkNode pHashtable-slotKeyId[idx]-head-next; while (travelLinkNode ! NULL) { /* 释放哈希结点 */ free(travelLinkNode-data); travelLinkNode-data NULL; /* 指针位置移动 */ travelLinkNode travelLinkNode-next; } } /* 2. 释放哈希表每个槽维的链表 */ for (int idx 0; idx pHashtable-slotNums; idx) { DoubleLinkListDestroy(pHashtable-slotKeyId[idx]); } /* 3. 释放槽位 */ if (pHashtable-slotKeyId ! NULL) { free(pHashtable-slotKeyId); pHashtable-slotKeyId NULL; } /* 4. 释放哈希表 */ if (pHashtable ! NULL) { free(pHashtable); pHashtable NULL; }
}
9、二叉树
1结点的创建包含数据域和指针域用于存储data。
数据域结点中存储数据元素的部分。
指针域结点中存储数据元素之间的链接信息即下一个结点地址的部分。 typedef struct BinarySearchNode { /* 数据域 */ ELEMENTTYPE data; /* 指针域 */ struct BinarySearchNode *left; struct BinarySearchNode *right; #if 1 struct BinarySearchNode * parent; #endif } BinarySearchNode; 2树的构建 通过自定义比较器判断树的元素结点在右边还是在左边。
typedef struct BinarySearchTree
{/* 树的根结点 */BinarySearchNode *root;/* 树的元素个数 */int size;/* 树的高度 */int height;/* 自定义比较器 */int (*compareFunc)(ELEMENTTYPE arg1, ELEMENTTYPE arg2);/* 自定义打印器 */int (*printFunc)(ELEMENTTYPE arg);
} BinarySearchTree;3树结点的创建 参数需要一个父节点和插入的节点数据
/* 创建二叉搜索树的结点 */
static BinarySearchNode * createBinarySearchTreeNode(ELEMENTTYPE data, BinarySearchNode * parent)
{BinarySearchNode *newNode (BinarySearchNode *)malloc(sizeof(BinarySearchNode) * 1);if (newNode NULL){return NULL;}newNode-data data;newNode-left NULL;newNode-right NULL;newNode-parent parent;
return newNode;
}
4树的插入 如果travelNode指向的根节点不为空就可以通过比较器确定位置位置确定好后就需要存放值了那么需要创建newNode将值存在newNode中。一定要注意创建的结点就是为了存放数据的
/* 树的插入 */
int binarySearchTreeInsert(BinarySearchTree *pTree, ELEMENTTYPE data)
{/* 判空 */if (pTree NULL){return NULL_PTR;}
/* 判断是否为空树 */
#if 0if (pTree-size 0){
}
#elseif (pTree-root NULL){pTree-root createBinarySearchTreeNode(data, NULL);if (pTree-root NULL){return MALLOC_ERROR;}/* 树的元素个数加一. */(pTree-size);return ON_SUCCESS;}
#endif
/* 程序走到这个地方, 一定不是空树 */BinarySearchNode * travelNode pTree-root;
BinarySearchNode *parentNode NULL;int cmp 0;while (travelNode ! NULL){parentNode travelNode;//将根节点赋给父节点/* 比较器 */cmp pTree-compareFunc(data, travelNode-data);//通过与父节点的比较确定结点的位置if (cmp 0){travelNode travelNode-left;}else if (cmp 0){#if 1return ON_SUCCESS;#elsetravelNode-data data;#endif}else if (cmp 0){travelNode travelNode-right;}}/* 程序执行到这里 travelNode一定为NULL. */BinarySearchNode *newNode createBinarySearchTreeNode(data, parentNode);if (newNode NULL){return MALLOC_ERROR;}
if (cmp 0){parentNode-left newNode;}else if (cmp 0){parentNode-right newNode;}
/* 树的元素个数加一. */(pTree-size);
return ON_SUCCESS;
} 5树的遍历
1.树的层序遍历 通过调用队列接口实现遍历。 * 树的层序遍历 */ /* */ int binarySearchTreeLevelOrder(BinarySearchTree *pTree) { if (pTree NULL) { return NULL_PTR; }
#if 1 if (pTree-root NULL) { return ON_SUCCESS; } #else if (pTree-size 0) { return ON_SUCCESS; } #endif DoubleLinkListQueue *queue NULL; doubleLinkListQueueInit(queue); doubleLinkListQueuePush(queue, pTree-root); BinarySearchNode * frontVal NULL; while(!doubleLinkListQueueIsEmpty(queue)) { /* 取出队头元素 */ doubleLinkListQueueFront(queue, (void **)frontVal); /* 出队 */ doubleLinkListQueuePop(queue); /* 打印器 */ pTree-printFunc(frontVal-data); /* 左子树入队 */ if (frontVal-left ! NULL) { doubleLinkListQueuePush(queue, frontVal-left); } /* 右子树入队 */ if (frontVal-right ! NULL) { doubleLinkListQueuePush(queue, frontVal-right); } } /* 释放队列 */ doubleLinkListQueueDestroy(queue); return ON_SUCCESS; }
2. 树的高度 根据层序遍历来做只需要判断当前层的结点全部出队后高度1。 解引用操作符为解引用操作符它返回指针pHeight所指的对象的值注意不是地址。通过指针找到对应的内存和内存中的数据。再通过解引用访问或修改指针指向的内存内容。
/* 树的高度 */ int binarySearchTreeGetHeight(BinarySearchTree *pTree, int *pHeight) { if (pTree NULL || pHeight NULL) { return NULL_PTR; } /* 空树 */ if (pTree-root NULL) { /* 解引用 */ *pHeight 0; return ON_SUCCESS; } /* 程序到这个地方, 根结点不为NULL. 一定有结点. */ int height 0; DoubleLinkListQueue *queue NULL; doubleLinkListQueueInit(queue); doubleLinkListQueuePush(queue, pTree-root); int levelSize 1; BinarySearchNode * frontVal NULL; while(!doubleLinkListQueueIsEmpty(queue)) { /* 取出队头元素 */ doubleLinkListQueueFront(queue, (void **)frontVal); /* 出队 */ doubleLinkListQueuePop(queue); /* 当前层的结点个数减一 */ levelSize--; /* 左子树入队 */ if (frontVal-left ! NULL) { doubleLinkListQueuePush(queue, frontVal-left); } /* 右子树入队 */ if (frontVal-right ! NULL) { doubleLinkListQueuePush(queue, frontVal-right); } if (levelSize 0) { /* 树的高度加一. */ height; /* 当前层的结点已经全部结束了 */ doubleLinkListQueueGetSize(queue, levelSize); } } /* 释放队列 */ doubleLinkListQueueDestroy(queue); /* 解引用 */ *pHeight height; return ON_SUCCESS; } 3.前序遍历 先判断根节点有根节点的先入队其次左子树再次右子树。顺序45-29-21-76-68-57-84。 定义静态函数用static修饰的函数限定在本源码文件中不能被本源码文件以外的代码文件调用。而普通的函数默认是extern的也就是说它可以被其它代码文件调用。在函数的返回类型前加上关键字static函数就被定义成为静态函数。普通 函数的定义和声明默认情况下是extern的但静态函数只是在声明他的文件当中可见。 不能被其他文件所用。定义静态函数有以下好处 1 其他文件中可以定义相同名字的函数不会发生冲突。 2 静态函数不能被其他文件所用。
/* 前序遍历 */
static int binarySearchTreeInnerPreOrder(BinarySearchTree *pTree, BinarySearchNode * travelNode)//静态函数
{if (travelNode NULL){return ON_SUCCESS;}
/* 根结点 */pTree-printFunc(travelNode-data);/* 左子树 */binarySearchTreeInnerPreOrder(pTree, travelNode-left);/* 右子树 */binarySearchTreeInnerPreOrder(pTree, travelNode-right);
return ON_SUCCESS;
}
* 树的前序遍历 */
/* 根结点, 左子树, 右子树 */
int binarySearchTreePreOrder(BinarySearchTree *pTree)
{if (pTree NULL){return NULL_PTR;}
return binarySearchTreeInnerPreOrder(pTree, pTree-root);
} 4. 中序遍历 先判断左子树有左子树的先入队其次根节点再次右子树。顺序21-29-45-57-68-76-84其特点是递增。
/* 树的中序遍历 */
static int binarySearchTreeInnerInOrder(BinarySearchTree *pTree, BinarySearchNode * travelNode)
{if (travelNode NULL){return ON_SUCCESS;}
/* 左子树 */binarySearchTreeInnerInOrder(pTree, travelNode-left);/* 根结点 */pTree-printFunc(travelNode-data);/* 右子树 */binarySearchTreeInnerInOrder(pTree, travelNode-right);
return ON_SUCCESS;
}
/* 树的中序遍历 */
/* 左子树, 根结点, 右子树 */
int binarySearchTreeInOrder(BinarySearchTree *pTree)
{if (pTree NULL){return NULL_PTR;}
return binarySearchTreeInnerInOrder(pTree, pTree-root);
}
5. 后序遍厉
左子树, 右子树, 根结点
/* 树的后序遍历 */
static int binarySearchTreeInnerPostOrder(BinarySearchTree *pTree, BinarySearchNode * travelNode)
{if (travelNode NULL){return ON_SUCCESS;}
/* 左子树 */binarySearchTreeInnerPostOrder(pTree, travelNode-left);/* 右子树 */binarySearchTreeInnerPostOrder(pTree, travelNode-right);/* 根结点 */pTree-printFunc(travelNode-data);
return ON_SUCCESS;
}
/* 树的后序遍历 */
/* 左子树, 右子树, 根结点 */
int binarySearchTreePostOrder(BinarySearchTree *pTree)
{if (pTree NULL){return NULL_PTR;}return binarySearchTreeInnerPostOrder(pTree, pTree-root);
}
6前驱结点 当前结点中序遍历(有序的)的前一个结点
1.度为2的情况 node-left这是必须存在的剩下就判断右子树。 中序遍历3-29-55-62-63-71-76-84-99
2.度为1或者0的情况 如果在父节点的右边直接返回父节点node-parent 如果结点在父节点的左边则一直往上走node-parent-parent-parent.....直到parent结点在上一个结点parent-parent的右边则返回parent-parent。 图中10没有前驱结点代码中一直放回parent直到根节点79直接返回null了。 此前驱结点代码结合上面3种情况进行了优化
3.总结 如果节点有左子树则它的前驱节点是左子树中最右边的节点即左子树中的最大值节点。 如果节点没有左子树则它的前驱节点是第一个比该节点小的祖先节点。也就是说从当前节点往上走直到遇到一个节点它是其父节点的右子节点这个父节点就是前驱节点。
/* 结点的前驱结点 */
/* 前驱结点是: 当前结点中序遍历(有序的)的前一个结点 */
static BinarySearchNode * BinarySearchTreeNodeGetPrecursor(BinarySearchNode *node)
{BinarySearchNode * travelNode NULL;if (node-left ! NULL){travelNode node-left;while (travelNode-right ! NULL){/* node-left-right-right-...... */travelNode travelNode-right;}return travelNode;}
/* 如果程序执行到这里, 说明: 左子树一定为空 *//* 只能够往上面(parent-parent-parent)找 */
travelNode node;while (travelNode-parent ! NULL travelNode travelNode-parent-left){travelNode travelNode-parent;}/* 退出这个循环: case1.travelNode-parent NULL case2.当前结点是父结点的右边 */return travelNode-parent;
}
7后继结点 当前结点中序遍历(有序的)的后一个结点 若有右子树node-right-left-left-left-left..... 若没有右子树一直往上面走直到一个parent结-left点有left就结束。 /* 结点的后继结点 */ /* 后继结点是: 当前结点中序遍历(有序的)的后一个结点 */ static BinarySearchNode * BinarySearchTreeNodeGetSuccessor(BinarySearchNode *node) { BinarySearchNode * travelNode NULL; if (node-right ! NULL) { travelNode node-right; while (travelNode-left ! NULL) { travelNode travelNode-left; } return travelNode; } /* 程序执行到这个地方, 说明: 右子树一定为空 */ /* 只能够往上面走(parent-parent-parent...)找 */ travelNode node; while (travelNode-parent ! NULL travelNode travelNode-parent-right) { travelNode travelNode-parent; } /* 退出循环条件: case1.travelNode-parent NULL case 2. travelNode node-parent-left */ return travelNode-parent; }
1.总结 如果节点有右子树则它的后继节点是右子树中最左边的节点即右子树中的最小值节点。 如果节点没有右子树则它的后继节点是第一个比该节点大的祖先节点。也就是说从当前节点往上走直到遇到一个节点它是其父节点的左子节点这个父节点就是后继节点。
2.例子
考虑如下二叉搜索树
markdownCopy code 20/ \10 30/ \ \5 15 40/ \12 50 前驱节点 节点 15 的前驱节点是 12因为 12 是 15 左子树中最大的节点。 节点 30 的前驱节点是 20因为 20 是 30 第一个比它小的祖先节点。 后继节点 节点 15 的后继节点是 20因为 20 是第一个比 15 大的祖先节点。 节点 12 的后继节点是 15因为 15 是 12 右子树中最小的节点。 前驱节点是中序遍历中的前一个节点可以通过检查左子树或向上寻找父节点来找到。 后继节点是中序遍历中的后一个节点可以通过检查右子树或向上寻找父节点来找到。
8树的销毁 通过上述遍历方式获取每个结点通过free()进行释放之后将树也free()释放一下。
/* 树的销毁 */
int binarySearchTreeDestroy(BinarySearchTree *pTree)
{/* 只需要遍历到所有的结点 释放 */
#if 1if (pTree NULL){return NULL_PTR;}
if (pTree-root NULL){return ON_SUCCESS;}
DoubleLinkListQueue *queue NULL;doubleLinkListQueueInit(queue);
/* 根结点入队 */doubleLinkListQueuePush(queue, pTree-root);
BinarySearchNode * frontVal NULL;while(!doubleLinkListQueueIsEmpty(queue)){/* 取出队头元素 */doubleLinkListQueueFront(queue, (void **)frontVal);/* 出队 */doubleLinkListQueuePop(queue);
/* 左子树入队 */if (frontVal-left ! NULL){doubleLinkListQueuePush(queue, frontVal-left);}
/* 右子树入队 */if (frontVal-right ! NULL){doubleLinkListQueuePush(queue, frontVal-right);}
/* 释放结点 */if (frontVal ! NULL){free(frontVal);frontVal NULL;}}/* 释放队列 */doubleLinkListQueueDestroy(queue);
/* 释放树 */if (pTree ! NULL){free(pTree);pTree NULL;}
return ON_SUCCESS;
#else/* 使用中序遍历的方式去释放结点信息 */
#endif
}
9树的删除 分为三种情况都是通过找前驱结点然后将前驱结点的值赋值给当前需要删除的结点最后将前驱结点置空并释放。
1. 总体情况 2. 为0的情况
3. 度为1的情况 /* 树是否存在指定元素 */ int binarySearchTreeIsContainVal(BinarySearchTree *pTree, ELEMENTTYPE data) { if (pTree NULL) { return NULL_PTR; } return baseAppointValGetBSTreeNode(pTree, data) NULL ? 0 : 1; }
/*通过指定的值获取对应的结点*/ static BinarySearchNode * baseAppointValGetBSTreeNode(BinarySearchTree *pTree, ELEMENTTYPE data) { BinarySearchNode * travelNode pTree-root; int cmp 0; while (travelNode ! NULL) { cmp pTree-compareFunc(data, travelNode-data); if (cmp 0) { travelNode travelNode-left; } else if (cmp 0) { return travelNode; } else if (cmp 0) { travelNode travelNode-right; } } /* 退出循环: travelNode NULL */ return travelNode; } //删除指定结点 static int binarySearchTreeDeleteNode(BinarySearchTree *pTree, BinarySearchNode * delNode) { int ret 0; if (delNode NULL) { return ret; } /* 度为2 */ if (BinarySearchTreeNodeHasTwoChildrens(delNode)) { /* 获取当前结点的前驱结点 */ BinarySearchNode * preNode BinarySearchTreeNodeGetPrecursor(delNode); /* 前驱结点的值 赋值到 度为2的结点 */ delNode-data preNode-data; delNode preNode; } /* 程序到这个地方要删除的结点要么是度为1 要么是度为0. */ /* 度为1 */ BinarySearchNode * childNode delNode-left ! NULL ? delNode-left : delNode-right; BinarySearchNode * freeNode NULL; if (childNode ! NULL) { /* 度为1 */ childNode-parent delNode-parent; if (delNode-parent NULL) { /* 度为1 且是根结点 */ pTree-root childNode; freeNode delNode; } else { if (delNode delNode-parent-left) { delNode-parent-left childNode; } else if (delNode delNode-parent-right) { delNode-parent-right childNode; } freeNode delNode; } } else { if (delNode-parent NULL) { /* 度为0 根结点 */ freeNode delNode; /* 根结点置为NULL. */ pTree-root NULL; } else { /* 度为0 */ if (delNode delNode-parent-left) { delNode-parent-left NULL; } else if (delNode delNode-parent-right) { delNode-parent-right NULL; } freeNode delNode; } } /* 释放堆空间 */ if (freeNode ! NULL) { free(freeNode); freeNode NULL; } /* 树的元素个数减一. */ (pTree-size)--; return ret; }
/* 树的删除 */ int binarySearchTreeDelete(BinarySearchTree *pTree, ELEMENTTYPE data) { if (pTree NULL) { return NULL_PTR; }
#if 0 BinarySearchNode * delNode baseAppointValGetBSTreeNode(pTree, data); binarySearchTreeDeleteNode(pTree, delNode); #else binarySearchTreeDeleteNode(pTree, baseAppointValGetBSTreeNode(pTree, data)); #endif return ON_SUCCESS; }
/* 树的元素个数 */ int binarySearchTreeGetSize(BinarySearchTree *pTree, int *pSize) { if (pTree NULL || pSize NULL) { return NULL_PTR; } *pSize pTree-size; return ON_SUCCESS; }
10主函数
#include stdio.h
#include binarySearchTree.h
#define BUFFER_SIZE 6
/* 比较器 */
int comparFuncBasic(void *arg1, void *arg2)
{ int val1 *(int *)arg1; int val2 *(int *)arg2;
return val1 - val2;
}
/* 打印器 */
int printFuncBasic(void *arg)
{ int ret 0; int val *(int *)arg;
printf(val:%d\t, val); return ret;
}
int main()
{ BinarySearchTree * tree NULL; binarySearchTreeInit(tree, comparFuncBasic, printFuncBasic);
/* 17 6 23 48 5 11 */ int nums[BUFFER_SIZE] {17, 6, 23, 48, 5, 11}; for (int idx 0; idx BUFFER_SIZE; idx) { binarySearchTreeInsert(tree, (void *)nums[idx]);//(void *)nums[idx]就是data }
int size 0; binarySearchTreeGetSize(tree, size); printf(size:%d\n, size);
int height 0; binarySearchTreeGetHeight(tree, height); printf(height:%d\n, height);
/* 层序遍历 */ binarySearchTreeLevelOrder(tree); printf(\n);
} 11常见的数据逻辑结构
12树知识点 树的特性插入树的元素一定是具有可比较性的。 假设插入一组数据12 2 34 23 5 6 45 8。
1如果按照顺序表的排列需要查一个数据就需要从头开始遍历直到查到所需要的数据如果数据太大更浪费资源。所以引入树。
2树的排列有规律的先插入一个进去作为根结点如12接着插入 22比12小插入左边接着3434比12大就放在12的右边以此类推。因此查找的时候就能节省时间。 树的定义 树(tree)是由n(n≥0)个结点组成的有限集合T。n0的树称为空树对n0的树有: 1仅有一个特殊的结点称为根(root)结点根结点没有前驱结点 2当n1时除根结点外其余的结点分为m(m0)个互不相交的有限集合T1,T2…Tm其中每个集合Ti本身又是一棵树称之为根的子树 subtree。 注树的定义具有递归性即“树中还有树”。仅有一个根结点的树是最小树。
13空与非空链表
1. 空链表 当创建一个单链表时使用的是一级指针 定义一个指针指向结点head即创建了一个链表的头指针, BalanceBinarySearchNode *head
head-NULL; 当在空链表时的链表尾插操作中需要更改了头指针head的指向因此在函数中要使用到二级指针这里前提是头指针。
2.非空链表 一段非空链表head-node-node1-node2-NULL 若想插入尾插直接将node2-newnode因此需要更改的是node2结构体的指针域的存储内容因此这时我们操作只需要node2结构体的地址即一级指针。 链表中传入二级指针的原因是我们会遇到需要更改头指针head的指向的情况。如果我们仅是在不改变头指针head的指向的情况下对链表进行操作如非空链表的尾删尾插对非首结点(FirstNode)的结点的插入/删除操作等则不需要用到二级指针.
10、平衡二叉树AVL树
定义也称为AVL树是一种特殊的二叉树结构其定义要求每个节点的左子树和右子树的高度差不超过1即平衡因子为-101这三种情况|平衡因子| ≤ 1就平衡。
作用这种约束条件保证了平衡二叉树的高度相对较低使得在最坏情况下的查找、插入和删除操作的时间复杂度为O(log n)其中n是树中节点的个数。需要注意的是平衡二叉树并不要求所有节点的左子树和右子树的高度完全相等只要它们的高度差不超过1即可。这样可以通过适当地调整树的结构来保持平衡。
1.平衡二叉树的实现
1结构增加的接口
依据二叉树的代码构架衍生出平衡二叉树的结构增加了以下接口 获取AVL树的结点平衡因子 判断AVL树是否搜索树结点是否平衡 更新高度如果结点平衡就更新高度。 当前结点是父结点的左子树 当前结点是父结点的右子树 获取当前结点中较高的子结点 插入结点需要做出的调整 删除结点需要做出的调整 调整平衡 左旋 右旋 统一旋转操作由于左旋和右旋存在相同的旋转操作为避免冗余代码另起接口。 删除结点接口
2理论逻辑 平衡判断需要对判断的第一个不平衡结点进行旋转处理来达到平衡的目的然后以此类推直到整个树达到平衡再更新树的高度。 新增的结点一定通过比较后放在最底下所以一定是叶子结点新增的结点只会改变跟他有关系的结点平衡以及往上衍生node-parent-parent-parent.....都有可能不平衡。 更新高度比较该结点的左子树和右子树结点个数取出最长的子树并加1就是该结点的高度。树的高度初始化默认为height 1每增加一个叶子结点高度1。
/* 创建二叉搜索树的结点 */ static BalanceBinarySearchNode *createBalanceBinarySearchTreeNode(ELEMENTTYPE data, BalanceBinarySearchNode *parent) { BalanceBinarySearchNode *newNode (BalanceBinarySearchNode *)malloc(sizeof(BalanceBinarySearchNode) * 1); if (newNode NULL) { return NULL; } newNode-data data; newNode-left NULL; newNode-right NULL; newNode-parent parent; /*树的高度初始化默认为1*/ newNode-height 1; return newNode; } 旋转逻辑定义三个结点一个是当前结点grand一个是parent结点当前grand结点的最高子结点一个是parent结点的当前最高子节点child结点。通过结点的更替完成旋转。 不同的旋转三个结点的位置关系不一样。 /**右旋/BalanceBinarySearchNode *parent grand-left;BalanceBinarySearchNode *child parent-right;/**左旋/BalanceBinarySearchNode *parent grand-right;BalanceBinarySearchNode *child parent-left; 左旋结点的定义其中图画错了child结点是200. 右旋结点的定义 树的结点的删除需要删除85找到85的前驱结点80再将85覆盖然后判断平不平衡即可。 2.AVL树的代码接口实现
1获取AVL树的结点平衡因子
/*平衡二叉搜索树结点平衡因子*/
static int BalanceBinarySearchNodeFactor(BalanceBinarySearchNode *node)
{/*左子树高度*/int leftHeight node-left NULL ? 0 : node-left-height;/*右子树高度*/int rightHeight node-right NULL ? 0 : node-right-height;return leftHeight - rightHeight;
} 2判断AVL树是否搜索树结点是否平衡
/*判断平衡二叉搜索树结点是否平衡*/
static int BalancebinarySearchTreeNodeIsBalanced(BalanceBinarySearchNode *node)
{return abs(BalanceBinarySearchNodeFactor(node)) 1;
} 3更新高度
/*取两者之间最大的数*/
static int tmpMax(int val1, int val2)
{return val1 - val2 0 ? val1 : val2;
}
/*更新高度*/
static int BalancebinarySearchTreeNodeUpdateHeight(BalanceBinarySearchNode *node)
{/*左子树高度*/int leftHeight node-left NULL ? 0 : node-left-height;/*右子树高度*/int rightHeight node-right NULL ? 0 : node-right-height;
/*直接更新高度*/node-height 1 tmpMax(leftHeight, rightHeight);return ON_SUCCESS;
}
注这里的高度是指结点的高度两个结点的之间高度或者三个结点之间的高度平衡因子的那块需要用到在二叉树中定义的高度为整个树的高度两者概念不一样。 4当前结点是父结点的左子树
/*当前结点是父结点的左子树*/
static int BalancebinarySearchTreeNodeIsLeft(BalanceBinarySearchNode *node)
{
/*是根结点这行代码直接在return里面实现*/
return (node-parent ! NULL) (node node-parent-left);
}
5当前结点是父结点的右子树
/*当前结点是父结点的右子树*/
static int BalancebinarySearchTreeNodeIsRight(BalanceBinarySearchNode *node)
{
/*是根结点*/return (node-parent ! NULL) (node node-parent-right);
} 6获取当前结点中较高的子结点 此代码是判断RR LL RL LR的一个接口目的是判断左右子树哪边高若左子树高则是L右子树高是R。然后返回该子结点。
/*获取当前结点中较高的子结点*/
static BalanceBinarySearchNode *BalancebinarySearchTreeGetTallerNode(BalanceBinarySearchNode *node)
{/*左子树高度*/int leftHeight node-left NULL ? 0 : node-left-height;/*右子树高度*/int rightHeight node-right NULL ? 0 : node-right-height;if (leftHeight rightHeight){return node-left;}else if (leftHeight rightHeight){return node-right;}else{// leftHeight rightHeightif (BalancebinarySearchTreeNodeIsLeft(node)){return node-left;}else{return node-right;}}
} 7插入结点需要做出的调整 插入结点后可能造成树的失衡因此还需要判断若平衡就更新高度若不平衡就调用调整平衡函数进行平衡
/*插入完数据后需要做出调整*/
static int insertNodeAfter(BinarySearchTree *pTree, BalanceBinarySearchNode *node)
{int ret 0;
while ((node node-parent) ! NULL){/*node是什么 父结点 祖父结点 祖先结点*//*程序执行到这个地方说明不止一个结点*/if (BalancebinarySearchTreeNodeIsBalanced(node)){/*如果平衡就更新高度*/BalancebinarySearchTreeNodeUpdateHeight(node);}else{/*如果不平衡且node是最低的不平衡结点*/BalancebinarySearchTreeNodeAdjustBalance(pTree, node);
/*直接break*/break;}}
} 8删除结点需要做出的调整 同理删除结点后可能导致失衡也需要调用平衡函数。
/*删除结点需要做出的调整*/
/*node是要删除的点*/
static int removeNodeAfter(BinarySearchTree *pTree, BalanceBinarySearchNode *node)
{while ((node node-parent) ! NULL){/*程序执行到这个地方说明不止一个结点*/if (BalancebinarySearchTreeNodeIsBalanced(node)){/*如果该点平衡更新高度*/BalancebinarySearchTreeNodeUpdateHeight(node);}else{/*如果不平衡且node是最低的不平衡结点*/BalancebinarySearchTreeNodeAdjustBalance(pTree, node);
}}
} 9调整平衡
/*该点不平衡且node最低点的不平衡结点就调整平衡*/
static int BalancebinarySearchTreeNodeAdjustBalance(BinarySearchTree *pTree, BalanceBinarySearchNode *node)
{/*这里需要判断出不平衡的类型LL RR LR RL*/BalanceBinarySearchNode *parent BalancebinarySearchTreeGetTallerNode(node);BalanceBinarySearchNode *child BalancebinarySearchTreeGetTallerNode(parent);
if (BalancebinarySearchTreeNodeIsLeft(parent)){/*L?*/if (BalancebinarySearchTreeNodeIsLeft(parent)){/*LL*/BalancebinarySearchTreeNodeRotateRight(pTree, node);}else{/*LR: 先左旋再右旋*/BalancebinarySearchTreeNodeRotateLeft(pTree, parent);
BalancebinarySearchTreeNodeRotateRight(pTree, node);}}else{/*R?*/if (BalancebinarySearchTreeNodeIsLeft(child)){/*RL先右旋再左旋*/BalancebinarySearchTreeNodeRotateRight(pTree, parent);
BalancebinarySearchTreeNodeRotateLeft(pTree, node);}else{/*RR*/BalancebinarySearchTreeNodeRotateLeft(pTree, node);}}
} 10左旋
/*左旋:RR*/
static int BalancebinarySearchTreeNodeRotateLeft(BinarySearchTree *pTree, BalanceBinarySearchNode *grand)
{BalanceBinarySearchNode *parent grand-right;BalanceBinarySearchNode *child parent-left;grand-right child; // 1parent-left grand; // 2/*统一旋转操作*/BalancebinarySearchTreeNodeAfterRotate(pTree, grand, parent, child);return ON_SUCCESS;} 11右旋
/*右旋:LL*/
static int BalancebinarySearchTreeNodeRotateRight(BinarySearchTree *pTree, BalanceBinarySearchNode *grand)
{BalanceBinarySearchNode *parent grand-left;BalanceBinarySearchNode *child parent-right;grand-left child; // 1parent-right grand; // 2/*统一旋转操作*/BalancebinarySearchTreeNodeAfterRotate(pTree, grand, parent, child);return ON_SUCCESS;
} 12统一旋转操作
/*统一旋转操作*/
static int BalancebinarySearchTreeNodeAfterRotate(BinarySearchTree *pTree, BalanceBinarySearchNode *grand, BalanceBinarySearchNode *parent, BalanceBinarySearchNode *child)
{
parent-parent grand-parent; // 3
if (BalancebinarySearchTreeNodeIsLeft(grand)){grand-parent-left parent; // 4}else if (BalancebinarySearchTreeNodeIsRight(grand)){grand-parent-right parent; // 4}else{/*根结点*/pTree-root parent;}
grand-parent parent; // 5
if (child ! NULL){child-parent grand; // 6}
/*调整高度从低往高*/BalancebinarySearchTreeNodeUpdateHeight(grand);BalancebinarySearchTreeNodeUpdateHeight(parent);
return ON_SUCCESS;
} 13删除结点接口
/*删除结点接口*/
static int BalancebinarySearchTreeDeleteNode(BinarySearchTree *pTree, BalanceBinarySearchNode *delNode)
{int ret 0;if (delNode NULL){return ret;}
/* 度为2 */if (BalanceBinarySearchTreeNodeHasTwoChildrens(delNode)){/* 获取当前结点的前驱结点 */BalanceBinarySearchNode *preNode BinarySearchTreeNodeGetPrecursor(delNode);/* 前驱结点的值 赋值到 度为2的结点 */delNode-data preNode-data;
delNode preNode;}/* 程序到这个地方要删除的结点要么是度为1 要么是度为0. */
/* 度为1 */BalanceBinarySearchNode *childNode delNode-left ! NULL ? delNode-left : delNode-right;
BalanceBinarySearchNode *freeNode NULL;if (childNode ! NULL){/* 度为1 */childNode-parent delNode-parent;
if (delNode-parent NULL){/* 度为1 且是根结点 */pTree-root childNode;
freeNode delNode;
/*删除的结点*/removeNodeAfter(pTree, freeNode);}else{if (BalancebinarySearchTreeNodeIsLeft(delNode)){delNode-parent-left childNode;}else if (delNode delNode-parent-right){delNode-parent-right childNode;}freeNode delNode;
/*删除的结点*/removeNodeAfter(pTree, freeNode);}}else{if (delNode-parent NULL){/* 度为0 根结点 */freeNode delNode;
/*删除的结点*/removeNodeAfter(pTree, freeNode);
/* 根结点置为NULL. */pTree-root NULL;}else{/* 度为0 */if (BalancebinarySearchTreeNodeIsLeft(delNode)){delNode-parent-left NULL;}else if (BalancebinarySearchTreeNodeIsRight(delNode)){delNode-parent-right NULL;}freeNode delNode;
/*删除的结点*/removeNodeAfter(pTree, freeNode);}}
/* 释放堆空间 */if (freeNode ! NULL){free(freeNode);freeNode NULL;}
/* 树的元素个数减一. */(pTree-size)--;
return ret;
} 3.判断不平衡的类型 定义node 是当前结点parent是当前结点node的最高子结点child是parent结点的最高子结点。注意这里的三个结点跟旋转接口里面定义的结点规则不一样。 LR型判断规则先判断parent结点是否在左边若是这是L进入L型再次判断child是否在左边若不是则是LR型则需要对parent先左旋当前结点node 再右旋。 RL型判断规则先判断parent结点是否在右边若是这是R进入R型再次判断child是否在左边若是则是RL型则需要对parent先右旋当前结点node 再左旋。
/*该点不平衡且node最低点的不平衡结点就调整平衡*/
static int BalancebinarySearchTreeNodeAdjustBalance(BinarySearchTree *pTree, BalanceBinarySearchNode *node)
{/*这里需要判断出不平衡的类型LL RR LR RL*/BalanceBinarySearchNode *parent BalancebinarySearchTreeGetTallerNode(node);BalanceBinarySearchNode *child BalancebinarySearchTreeGetTallerNode(parent);
if (BalancebinarySearchTreeNodeIsLeft(parent)){/*L?*/if (BalancebinarySearchTreeNodeIsLeft(child)){/*LL*/BalancebinarySearchTreeNodeRotateRight(pTree, node);}else{/*LR: 先左旋再右旋*/BalancebinarySearchTreeNodeRotateLeft(pTree, parent);
BalancebinarySearchTreeNodeRotateRight(pTree, node);}}else{/*R?*/if (BalancebinarySearchTreeNodeIsLeft(child)){/*RL先右旋再左旋*/BalancebinarySearchTreeNodeRotateRight(pTree, parent);
BalancebinarySearchTreeNodeRotateLeft(pTree, node);}else{/*RR*/BalancebinarySearchTreeNodeRotateLeft(pTree, node);}}
}
4.树的删除
1树的删除的两种情况 调整高度 判断平衡 2通过指定的值获取对应的结点 通过找到要删除的结点的前驱结点通过前驱结点覆盖掉该结点然后再判断平不平衡。
/*通过指定的值获取对应的结点*/
static BalanceBinarySearchNode *baseAppointValGetBSTreeNode(BinarySearchTree *pTree, ELEMENTTYPE data)
{BalanceBinarySearchNode *travelNode pTree-root;int cmp 0;while (travelNode ! NULL){cmp pTree-compareFunc(data, travelNode-data);if (cmp 0){travelNode travelNode-left;}else if (cmp 0){return travelNode;}else if (cmp 0){travelNode travelNode-right;}}/* 退出循环: travelNode NULL */return travelNode;
}
/* 树的删除*/
int BalancebinarySearchTreeDelete(BinarySearchTree *pTree, ELEMENTTYPE data)
{if (pTree NULL){return NULL_PTR;}
#if 0BalanceBinarySearchNode * delNode baseAppointValGetBSTreeNode(pTree, data);BalancebinarySearchTreeDeleteNode(pTree, delNode);
#elseBalancebinarySearchTreeDeleteNode(pTree, baseAppointValGetBSTreeNode(pTree, data));
#endifreturn ON_SUCCESS;
}
5.主函数
#include stdio.h
#include balanceBinarySearchTree.h
#define BUFFER_SIZE 6
/* 比较器 */
int comparFuncBasic(void *arg1, void *arg2)
{int val1 *(int *)arg1;int val2 *(int *)arg2;
return val1 - val2;
}
/* 打印器 */
int printFuncBasic(void *arg)
{int ret 0;int val *(int *)arg;
printf(val:%d\t, val);return ret;
}
int main()
{BinarySearchTree * tree NULL;BalancebinarySearchTreeInit(tree, comparFuncBasic, printFuncBasic);
/* 17 6 23 48 5 11 */int nums[BUFFER_SIZE] {17, 6, 23, 48, 5, 11};for (int idx 0; idx BUFFER_SIZE; idx){BalancebinarySearchTreeInsert(tree, (void *)nums[idx]);//(void *)nums[idx]就是data}
int size 0;BalancebinarySearchTreeGetSize(tree, size);printf(size:%d\n, size);
int height 0;BalancebinarySearchTreeGetHeight(tree, height);printf(height:%d\n, height);
/* 层序遍历 */BalancebinarySearchTreeLevelOrder(tree);printf(\n);
}
11、DFS DFS全称为 Depth-First Search即 深度优先搜索是一种用于遍历或搜索树或图数据结构的算法。与广度优先搜索BFS不同DFS 优先深入每一个可能的分支路径直到无法继续为止然后回溯并探索其他路径。
1DFS 的基本思想 DFS 的基本思想是从一个起始节点开始沿着一个路径不断深入直到到达一个无法继续的节点即叶子节点或死胡同然后回溯到最近的分叉点继续探索未访问的分支。这一过程会递归进行直到所有节点都被访问为止。
2DFS 的实现 DFS 可以使用两种主要方法实现 递归利用函数调用栈来处理节点的访问。 显式栈手动维护一个栈结构来模拟递归过程。
1. 递归实现 DFS
递归是实现 DFS 的一种直观方法代码如下
#include stdio.h
#include stdlib.h
#include stdbool.h
#define MAX_NODES 100
typedef struct Node {int vertex;struct Node* next;
} Node;
typedef struct Graph {int numVertices;Node** adjLists;bool* visited;
} Graph;
Node* createNode(int vertex) {Node* newNode malloc(sizeof(Node));newNode-vertex vertex;newNode-next NULL;return newNode;
}
Graph* createGraph(int vertices) {Graph* graph malloc(sizeof(Graph));graph-numVertices vertices;graph-adjLists malloc(vertices * sizeof(Node*));graph-visited malloc(vertices * sizeof(bool));
for (int i 0; i vertices; i) {graph-adjLists[i] NULL;graph-visited[i] false;}
return graph;
}
void addEdge(Graph* graph, int src, int dest) {// 从 src 到 dest 添加边Node* newNode createNode(dest);newNode-next graph-adjLists[src];graph-adjLists[src] newNode;
// 如果是无向图添加反向边newNode createNode(src);newNode-next graph-adjLists[dest];graph-adjLists[dest] newNode;
}
void DFS(Graph* graph, int vertex) {graph-visited[vertex] true;printf(Visited %d\n, vertex);
Node* adjList graph-adjLists[vertex];Node* temp adjList;
while (temp ! NULL) {int connectedVertex temp-vertex;
if (graph-visited[connectedVertex] false) {DFS(graph, connectedVertex);}temp temp-next;}
}
int main() {Graph* graph createGraph(6);
addEdge(graph, 0, 1);addEdge(graph, 0, 2);addEdge(graph, 1, 3);addEdge(graph, 1, 4);addEdge(graph, 2, 5);addEdge(graph, 4, 5);
DFS(graph, 0);
// 释放内存for (int i 0; i graph-numVertices; i) {Node* temp graph-adjLists[i];while (temp) {Node* toFree temp;temp temp-next;free(toFree);}}free(graph-adjLists);free(graph-visited);free(graph);
return 0;
}
2. 显式栈实现 DFS
如果不使用递归也可以使用栈来实现 DFS
#include stdio.h
#include stdlib.h
#include stdbool.h
#define MAX_NODES 100
typedef struct Node {int vertex;struct Node* next;
} Node;
typedef struct Graph {int numVertices;Node** adjLists;bool* visited;
} Graph;
typedef struct Stack {int items[MAX_NODES];int top;
} Stack;
Node* createNode(int vertex) {Node* newNode malloc(sizeof(Node));newNode-vertex vertex;newNode-next NULL;return newNode;
}
Graph* createGraph(int vertices) {Graph* graph malloc(sizeof(Graph));graph-numVertices vertices;graph-adjLists malloc(vertices * sizeof(Node*));graph-visited malloc(vertices * sizeof(bool));
for (int i 0; i vertices; i) {graph-adjLists[i] NULL;graph-visited[i] false;}
return graph;
}
void addEdge(Graph* graph, int src, int dest) {Node* newNode createNode(dest);newNode-next graph-adjLists[src];graph-adjLists[src] newNode;
newNode createNode(src);newNode-next graph-adjLists[dest];graph-adjLists[dest] newNode;
}
Stack* createStack() {Stack* stack malloc(sizeof(Stack));stack-top -1;return stack;
}
void push(Stack* stack, int value) {if (stack-top MAX_NODES - 1) {printf(Stack overflow\n);} else {stack-items[stack-top] value;}
}
int pop(Stack* stack) {if (stack-top -1) {printf(Stack underflow\n);return -1;} else {return stack-items[stack-top--];}
}
bool isStackEmpty(Stack* stack) {return stack-top -1;
}
void DFS(Graph* graph, int startVertex) {Stack* stack createStack();graph-visited[startVertex] true;push(stack, startVertex);
while (!isStackEmpty(stack)) {int vertex pop(stack);printf(Visited %d\n, vertex);
Node* temp graph-adjLists[vertex];while (temp) {int adjVertex temp-vertex;if (!graph-visited[adjVertex]) {graph-visited[adjVertex] true;push(stack, adjVertex);}temp temp-next;}}// 释放栈的内存free(stack);
}
int main() {Graph* graph createGraph(6);
addEdge(graph, 0, 1);addEdge(graph, 0, 2);addEdge(graph, 1, 3);addEdge(graph, 1, 4);addEdge(graph, 2, 5);addEdge(graph, 4, 5);
DFS(graph, 0);
// 释放内存for (int i 0; i graph-numVertices; {Node* temp graph-adjLists[i];while (temp) {Node* toFree temp;temp temp-next;free(toFree);}}free(graph-adjLists);free(graph-visited);free(graph);
return 0;
}
3DFS 的应用
DFS 广泛应用于各种算法和问题中例如 路径查找在迷宫、棋盘或网络中查找路径。 连通分量检测用于检测图中的连通分量即在无向图中所有节点都相互连接的部分。 拓扑排序在有向无环图DAG中确定节点的线性顺序。 检测环路在图中检测是否存在循环。 解决谜题如数独、八皇后问题等利用 DFS 进行回溯。
4DFS 的时间和空间复杂度 时间复杂度O(V E)其中 V 是节点数E 是边数。每个节点和边都可能被访问一次。 空间复杂度O(V) 主要用于存储递归栈或显式栈以及访问标记。
12、BFS 广度优先搜索BFSBreadth-First Search是一种用于遍历或搜索树或图数据结构的算法。与深度优先搜索DFS不同BFS 是按层次逐层地访问节点先访问与起始节点距离最近的节点再访问更远的节点。
1BFS 的基本思想 BFS 从一个起始节点开始将该节点标记为已访问并将其放入队列中。然后反复执行以下步骤 从队列的前端取出一个节点。 访问该节点的所有未访问的邻居并将这些邻居节点依次放入队列中。 重复以上步骤直到队列为空。
2BFS 的实现 在 C 语言中实现 BFS 时通常使用队列来存储当前层次的节点。以下是 BFS 的实现代码。
#include stdio.h
#include stdlib.h
#include stdbool.h
#define MAX_NODES 100 // 队列的最大节点数
// 节点结构体用于表示邻接表中的单个节点
typedef struct Node {int vertex; // 节点编号struct Node* next; // 指向下一个相邻节点的指针
} Node;
// 图结构体用于表示整个图
typedef struct Graph {int numVertices; // 图中的节点数量Node** adjLists; // 邻接表用于存储每个节点的相邻节点bool* visited; // 访问标记数组用于标记节点是否已被访问
} Graph;
// 队列结构体用于实现BFS中的队列操作
typedef struct Queue {int items[MAX_NODES]; // 队列数组用于存储队列中的节点int front; // 队列头部索引int rear; // 队列尾部索引
} Queue;
// 创建新节点
Node* createNode(int vertex) {Node* newNode malloc(sizeof(Node)); // 为新节点分配内存newNode-vertex vertex; // 设置节点编号newNode-next NULL; // 初始化下一个节点指针为 NULLreturn newNode; // 返回新节点的指针
}
// 创建新图
Graph* createGraph(int vertices) {Graph* graph malloc(sizeof(Graph)); // 为图结构体分配内存graph-numVertices vertices; // 设置图中的节点数量graph-adjLists malloc(vertices * sizeof(Node*)); // 为邻接表分配内存graph-visited malloc(vertices * sizeof(bool)); // 为访问标记数组分配内存
// 初始化邻接表和访问标记数组for (int i 0; i vertices; i) {graph-adjLists[i] NULL; // 初始化每个节点的邻接表为空graph-visited[i] false; // 初始化每个节点的访问标记为未访问}
return graph; // 返回新图的指针
}
// 添加边到图中
void addEdge(Graph* graph, int src, int dest) {// 从 src 到 dest 添加边Node* newNode createNode(dest); // 创建新节点表示 destnewNode-next graph-adjLists[src]; // 将新节点插入 src 的邻接表graph-adjLists[src] newNode; // 更新邻接表
// 如果是无向图还需添加从 dest 到 src 的边newNode createNode(src); // 创建新节点表示 srcnewNode-next graph-adjLists[dest]; // 将新节点插入 dest 的邻接表graph-adjLists[dest] newNode; // 更新邻接表
}
// 创建新队列
Queue* createQueue() {Queue* queue malloc(sizeof(Queue)); // 为队列结构体分配内存queue-front -1; // 初始化队列头部索引queue-rear -1; // 初始化队列尾部索引return queue; // 返回新队列的指针
}
// 检查队列是否为空
bool isQueueEmpty(Queue* queue) {return queue-rear -1; // 如果队列尾部索引为 -1则队列为空
}
// 将元素加入队列
void enqueue(Queue* queue, int value) {if (queue-rear MAX_NODES - 1) {printf(Queue is full\n); // 如果队列满了输出提示return;}if (queue-front -1) {queue-front 0; // 如果队列是空的设置头部索引为 0}queue-items[queue-rear] value; // 将元素加入队列并更新尾部索引
}
// 从队列中移除元素
int dequeue(Queue* queue) {if (isQueueEmpty(queue)) {printf(Queue is empty\n); // 如果队列为空输出提示return -1;}int item queue-items[queue-front]; // 获取队列头部的元素queue-front; // 更新头部索引if (queue-front queue-rear) { // 如果头部超过尾部队列已空queue-front queue-rear -1; // 重置队列}return item; // 返回移除的元素
}
// 广度优先搜索算法
void BFS(Graph* graph, int startVertex) {Queue* queue createQueue(); // 创建队列用于BFS
graph-visited[startVertex] true; // 标记起始节点为已访问enqueue(queue, startVertex); // 将起始节点加入队列
while (!isQueueEmpty(queue)) { // 当队列不为空时继续搜索int currentVertex dequeue(queue); // 从队列中取出节点printf(Visited %d\n, currentVertex); // 输出访问的节点
Node* temp graph-adjLists[currentVertex]; // 获取当前节点的邻接表
// 遍历邻接表中的所有邻居节点while (temp) {int adjVertex temp-vertex; // 获取邻居节点
// 如果邻居节点未被访问if (!graph-visited[adjVertex]) {graph-visited[adjVertex] true; // 标记邻居节点为已访问enqueue(queue, adjVertex); // 将邻居节点加入队列}temp temp-next; // 移动到下一个邻居节点}}// 释放队列的内存free(queue);
}
int main() {Graph* graph createGraph(6); // 创建一个包含6个节点的图
// 添加边到图中addEdge(graph, 0, 1);addEdge(graph, 0, 2);addEdge(graph, 1, 3);addEdge(graph, 1, 4);addEdge(graph, 2, 5);addEdge(graph, 4, 5);
BFS(graph, 0); // 从节点0开始进行BFS
// 释放内存for (int i 0; i graph-numVertices; i) {Node* temp graph-adjLists[i];while (temp) {Node* toFree temp; // 释放邻接表中的节点内存temp temp-next;free(toFree);}}free(graph-adjLists); // 释放邻接表的内存free(graph-visited); // 释放访问标记数组的内存free(graph); // 释放图结构体的内存
return 0;
}
13、红黑树
红黑树是一种自平衡二叉查找树在插入、删除和查找操作的最坏情况下都能保持O(log n)的时间复杂度。红黑树被广泛应用于许多计算机系统中如Linux内核中的调度器、STL中的map和set等。
1.红黑树的特点
红黑树是一种特殊的二叉查找树BST它在每个节点上增加了一个存储位用来表示节点的颜色可以是红色或黑色。红黑树通过对树的颜色及结构进行调整保证树的大致平衡从而在最坏情况下基本操作插入、删除、查找的时间复杂度都是O(log n)。
红黑树必须满足以下性质 节点是红色或黑色这条性质定义了节点的颜色属性。 根节点是黑色红黑树的根节点必须是黑色的。这保证了红黑树的基础结构。 所有叶子节点NULL或NIL节点都是黑色红黑树中的每个叶子节点都是黑色并且这些叶子节点是NULL或NIL节点即不存储数据。 红色节点的两个子节点必须都是黑色的即不能有连续的红色节点这条性质确保了没有两个连续的红色节点在树的路径上出现。 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点这条性质保证了树的平衡使得树的最长路径不会超过最短路径的两倍。 2.红黑树的操作
a. 插入操作
当插入一个新节点时初始时它被标记为红色避免违反性质5。然后可能会出现一些性质被破坏需要进行调整。调整操作通常包括 变色改变节点的颜色可能由红色变为黑色或者由黑色变为红色。 左旋或右旋通过旋转来调整树的结构恢复红黑树的性质。
插入后的调整步骤包括 如果新节点的父节点是黑色不需要任何调整红黑树的性质依然保持。 如果新节点的父节点是红色则违反了性质4这时候需要通过变色、旋转等操作恢复红黑树的性质。
b. 删除操作
删除节点比插入要复杂得多因为删除一个节点可能会破坏红黑树的多条性质。删除后的调整步骤通常包括 替代节点变色如果删除节点是黑色会引入额外的黑色破坏性质5。通过变色来平衡树。 双重黑色通过将某个子节点标记为“额外的黑色”并通过旋转和变色来调整树的结构。
调整完成后红黑树的所有性质将重新被维护。
3.红黑树的旋转操作
旋转操作是红黑树最核心的操作之一分为左旋和右旋。旋转用于调整树的结构以维持平衡。
a. 左旋
左旋的目的是让树的右子树上升为当前节点的父节点当前节点成为其左子树的一部分。
// 左旋示意图
// 旋转前
// x
// \
// y
// /
// z
//
// 旋转后
// y
// /
// x
// \
// z
b. 右旋
右旋的目的是让树的左子树上升为当前节点的父节点当前节点成为其右子树的一部分。
cppCopy code// 右旋示意图
// 旋转前
// y
// /
// x
// \
// z
//
// 旋转后
// x
// \
// y
// \
// z
旋转操作会改变树的结构但不会改变树中各节点的顺序关系。
4.红黑树的优势 平衡性红黑树是一种自平衡树保证了最坏情况下操作的时间复杂度为O(log n)。 插入、删除效率高相比其他自平衡二叉树如AVL树红黑树的插入和删除操作效率更高。 广泛应用红黑树由于其高效和稳定的性能被广泛应用于标准库中的关联容器如C STL中的map和set。
5.简单的代码示例
下面是一个简化的红黑树实现。这个实现包括插入操作并维护红黑树的平衡性。为了简洁起见删除操作未包括在内。
#include stdio.h
#include stdlib.h
// 定义节点颜色
typedef enum { RED, BLACK } Color;
// 定义红黑树节点结构
typedef struct RBTreeNode {int data; // 数据域Color color; // 节点颜色struct RBTreeNode* left; // 左子节点struct RBTreeNode* right; // 右子节点struct RBTreeNode* parent; // 父节点
} RBTreeNode;
// 定义红黑树结构
typedef struct {RBTreeNode* root; // 根节点RBTreeNode* nil; // 哨兵节点表示空节点所有叶子节点
} RBTree;
// 创建一个新的红黑树节点
RBTreeNode* createNode(int data, Color color, RBTreeNode* nil) {RBTreeNode* node (RBTreeNode*)malloc(sizeof(RBTreeNode));node-data data;node-color color;node-left nil;node-right nil;node-parent nil;return node;
}
// 创建一个空的红黑树
RBTree* createTree() {RBTree* tree (RBTree*)malloc(sizeof(RBTree));// 初始化哨兵节点所有空节点用这个节点表示tree-nil (RBTreeNode*)malloc(sizeof(RBTreeNode));tree-nil-color BLACK;tree-root tree-nil;return tree;
}
// 左旋操作
void leftRotate(RBTree* tree, RBTreeNode* x) {RBTreeNode* y x-right; // y 是 x 的右子节点x-right y-left; // 将 y 的左子树转为 x 的右子树if (y-left ! tree-nil) {y-left-parent x;}y-parent x-parent; // y 的父节点设置为 x 的父节点if (x-parent tree-nil) {tree-root y; // 如果 x 是根节点则 y 变为根节点} else if (x x-parent-left) {x-parent-left y; // 如果 x 是左子节点则 y 变为左子节点} else {x-parent-right y; // 如果 x 是右子节点则 y 变为右子节点}y-left x; // x 变为 y 的左子节点x-parent y;
}
// 右旋操作
void rightRotate(RBTree* tree, RBTreeNode* y) {RBTreeNode* x y-left; // x 是 y 的左子节点y-left x-right; // 将 x 的右子树转为 y 的左子树if (x-right ! tree-nil) {x-right-parent y;}x-parent y-parent; // x 的父节点设置为 y 的父节点if (y-parent tree-nil) {tree-root x; // 如果 y 是根节点则 x 变为根节点} else if (y y-parent-right) {y-parent-right x; // 如果 y 是右子节点则 x 变为右子节点} else {y-parent-left x; // 如果 y 是左子节点则 x 变为左子节点}x-right y; // y 变为 x 的右子节点y-parent x;
}
// 修复红黑树以保持性质
void insertFixup(RBTree* tree, RBTreeNode* z) {while (z-parent-color RED) {if (z-parent z-parent-parent-left) {RBTreeNode* y z-parent-parent-right; // y 是 z 的叔叔节点if (y-color RED) { // 情况1叔叔是红色z-parent-color BLACK; // 父节点变黑y-color BLACK; // 叔叔变黑z-parent-parent-color RED; // 祖父节点变红z z-parent-parent; // z 上升两层} else {if (z z-parent-right) { // 情况2z 是右子节点z z-parent;leftRotate(tree, z); // 左旋}z-parent-color BLACK; // 情况3z 是左子节点z-parent-parent-color RED;rightRotate(tree, z-parent-parent); // 右旋}} else {RBTreeNode* y z-parent-parent-left; // y 是 z 的叔叔节点if (y-color RED) { // 情况1叔叔是红色z-parent-color BLACK;y-color BLACK;z-parent-parent-color RED;z z-parent-parent;} else {if (z z-parent-left) { // 情况2z 是左子节点z z-parent;rightRotate(tree, z); // 右旋}z-parent-color BLACK; // 情况3z 是右子节点z-parent-parent-color RED;leftRotate(tree, z-parent-parent); // 左旋}}}tree-root-color BLACK; // 根节点始终为黑色
}
// 插入新节点到红黑树中
void insert(RBTree* tree, int data) {RBTreeNode* z createNode(data, RED, tree-nil); // 新节点初始为红色RBTreeNode* y tree-nil;RBTreeNode* x tree-root;
while (x ! tree-nil) { // 找到插入位置y x;if (z-data x-data) {x x-left;} else {x x-right;}}
z-parent y;if (y tree-nil) {tree-root z; // 如果树是空的新节点为根节点} else if (z-data y-data) {y-left z; // 新节点作为左子节点插入} else {y-right z; // 新节点作为右子节点插入}
insertFixup(tree, z); // 修复红黑树的性质
}
// 中序遍历红黑树
void inorderTraversal(RBTree* tree, RBTreeNode* node) {if (node ! tree-nil) {inorderTraversal(tree, node-left);printf(%d (%s) , node-data, node-color RED ? R : B);inorderTraversal(tree, node-right);}
}
// 销毁红黑树并释放内存
void destroyTree(RBTree* tree, RBTreeNode* node) {if (node ! tree-nil) {destroyTree(tree, node-left);destroyTree(tree, node-right);free(node);}
}
int main() {RBTree* tree createTree();
// 插入节点insert(tree, 10);insert(tree, 20);insert(tree, 30);insert(tree, 15);insert(tree, 25);
// 中序遍历并打印红黑树inorderTraversal(tree, tree-root);printf(\n);
// 销毁红黑树destroyTree(tree, tree-root);free(tree-nil);free(tree);
return 0;
} 代码解析 节点颜色RED表示红色BLACK表示黑色。每个节点有一个颜色属性用于维持红黑树的平衡。 哨兵节点nil节点用于表示所有叶子节点它们统一为黑色。这简化了红黑树的实现使得插入和旋转操作不必处理NULL指针。 旋转操作包括leftRotate和rightRotate用于在树结构需要调整时改变节点的位置。 插入操作insert函数插入新节点到红黑树中并调用insertFixup函数修复可能被破坏的红黑树性质。 遍历和销毁inorderTraversal进行中序遍历destroyTree递归销毁树并释放内存。
文件IO
1、系统调用和库函数的区别
系统调用System Call和库函数Library Function是操作系统和编程语言中常见的概念但它们有不同的作用和层次。下面是它们的主要区别
1. 定义 系统调用系统调用是操作系统提供的接口用于让用户程序直接与操作系统内核进行交互。它允许程序执行特权操作如文件操作、进程控制、内存管理等。系统调用通常需要从用户模式切换到内核模式以执行这些操作。 库函数库函数是编程语言提供的预定义函数封装了常见的功能如字符串处理、数学运算、文件操作等。库函数通常由标准库提供运行在用户模式下不需要直接与操作系统内核交互。 开发过程中需要注意资源和效率问题 2. 执行层次 系统调用执行时需要进入内核模式这意味着它涉及从用户模式到内核模式的切换这个过程比较耗时。 库函数通常在用户模式下执行不需要进入内核模式因此执行速度比系统调用快。
3. 功能和作用 系统调用提供底层操作系统服务例如文件读写、进程管理、网络通信等。这些操作往往是必须通过系统调用才能完成的因为它们涉及操作系统的核心资源。 库函数提供对常见任务的高层次封装例如字符串操作、数学计算等。库函数可能内部使用系统调用但它们对程序员隐藏了底层的复杂性。
4. 使用方式 系统调用一般直接通过操作系统的API调用需要较多的参数和特权操作。例如在Linux中使用write()系统调用来写文件。 库函数通过编程语言的标准库调用更易于使用。例如C语言中的printf()函数是一个库函数用于输出数据。
5. 性能 系统调用由于涉及用户模式和内核模式的切换开销较大因此性能可能不如库函数。 库函数通常运行在用户模式下开销较小执行速度更快。
6. 安全性 系统调用由于直接与操作系统内核交互系统调用通常需要更多的安全检查以防止程序对系统资源的误用或恶意利用。 库函数一般没有直接接触操作系统内核因此相对更安全但某些库函数可能会间接调用系统调用。
2、文件属性
1文件属性解读 2添加和删除权限 chmod 使用chmod实现添加和删除操作比如chmod -x就是删除文件的可执行操作。 3、文件描述符 文件描述符的0、1、2分别表示标准输入流、标准输出流和标准错误流。具体来说 0代表标准输入流通常对应着键盘的设备文件可以理解为键盘输入。在编程中可以使用这个描述符来读取从键盘输入的数据。 1代表标准输出流通常对应着显示器的设备文件用于将程序运行的结果输出到屏幕上。通过这个描述符程序可以将需要展示给用户的信息发送到终端。 2代表标准错误流也是输出到终端但它用于输出错误信息。当程序运行出错时错误信息会通过这个描述符发送到终端以便用户能够及时发现并处理错误。 在Linux系统中每个进程都会默认打开这三个文件描述符。这些描述符在编程中非常重要因为它们提供了进程与外部环境如键盘、显示器等进行交互的接口。通过操作这些描述符程序可以实现数据的输入和输出功能。 4、文件IO
1open函数 函数定义 flag打开文件的行为标记 文件权限解读 rwx 111 1×2^2 1×2^1 1×2^0 7 ---------因此对应的r-4w-2, x-1。 因此属性0644 rw--r--r-- test.txt文件添加0644属性之后的属性如下 文件打开个数 代码段
#include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h
#define BUFFER_SIZE 32
int main() { const char *name ./test.txt; int fd1 open(name, O_RDWR | O_CREAT | O_APPEND, 0644);//O_APPEND:追加不写这个那么之前的的数据就会被覆盖 if (fd1 -1) { perror(open error); exit(-1); } printf(fd1: %d\n, fd1); char buffer[BUFFER_SIZE] 257 NIHAO; int fd2 open(name, O_RDWR | O_CREAT, 0644); if (fd2 -1) { perror(open error); exit(-1); } printf(fd2: %d\n, fd2); int fd3 open(name, O_RDWR | O_CREAT, 0644); if (fd3 -1) { perror(open error); exit(-1); } printf(fd3: %d\n, fd3); int fd4 open(name, O_RDWR | O_CREAT, 0644); if (fd4 -1) { perror(open error); exit(-1); } printf(fd4: %d\n, fd4); printf(hello wworld\n); return 0; }
2close函数 函数定义 close()函数作用 如果打开的文件1不使用就需要关闭所打开的文件1并且如果又新建一个文件2那么此文件2的位置就会放在刚刚关闭原文件1的位置。 节省资源提高效率。 代码段 /*关闭文件描述符 资源回收*/close(fd3);
int fd5 open(name, O_RDWR | O_CREAT, 0644);//会替换fd3,因为fd3被关了资源回收if (fd5 -1){perror(open error);exit(-1);}printf(fd5: %d\n, fd5);
3write函数 函数定义 这里的buf是传入参数。 代码块 O_APPEND: 追加不写这个那么之前的的写入的数据就会被覆盖
#include stdio.h
#include fcntl.h
#include errno.h
#include stdlib.h
#include unistd.h
#include string.h
#include sys/types.h
#include sys/stat.h
#define BUFFER_SIZE 32
int main()
{const char *name ./test.txt;
int fd1 open(name, O_RDWR | O_CREAT | O_APPEND, 0644);//O_APPEND:追加不写这个那么之前的的数据就会被覆盖if (fd1 -1){perror(open error);exit(-1);}printf(fd1: %d\n, fd1);
char buffer[BUFFER_SIZE] 257 NIHAO;
write(fd1, buffer, strlen(buffer) 1);/*必须回收资源*/close(fd1);
printf(hello wworld\n);
return 0;
} 4read函数 函数定义 这里的buf是传出参数 传出去的是实际的字节个数 buf就是缓冲区没有缓冲区就定义一个 函数作用 read 函数不会添加字符串终止符\0。因此在使用 read 读取的字符串时你需要确保你知道读取了多少字节并在合适的位置添加字符串终止符。 读取字节的个数记得sizeof(buffer) - 1因为最后一个是\0
#include stdio.h
#include fcntl.h
#include errno.h
#include stdlib.h
#include unistd.h
#include string.h
#define BUFFER_SIZE 32
int main()
{const char *name ./test.txt;
int fd1 open(name, O_RDWR | O_CREAT | O_APPEND, 0644);//O_APPEND:追加不写这个那么之前的的数据就会被覆盖if (fd1 -1){perror(open error);exit(-1);}printf(fd1: %d\n, fd1);
char buffer[BUFFER_SIZE] {0};
int readBytes read(fd1, buffer, sizeof(buffer) - 1);
printf(readBytes:%d, \t buffer:%s\n, readBytes, buffer);/*完整打印出全部字符*/for (int idx 0; idx sizeof(buffer); idx){printf(%c , buffer[idx]);}printf(\n);
/*必须回收资源*/close(fd1);
return 0;
} read读取\0问题 当你使用read函数读取文件内容到缓冲区时如果文件内容中的某个位置恰好是\0那么read函数会将它当作字符串的结束后续的内容将不会被当作字符串的一部分来处理。 如果你想要读取整个文件内容包括其中的\0字符你需要以字节为单位来处理而不是以字符串为单位。这意味着你不能简单地将读取的内容当作C风格的字符串即以\0终止的字符数组来处理。 请注意在打印每个字节时使用的是%c格式说明符它会按照字符的原始值进行打印而不管它是否是空字符。如果我们使用%s格式说明符那么它会在遇到第一个\0时停止打印因此\0后面部分将不会被输出。 因此上面程序若想完整打印出字符串必须使用循环打印每个字符。 /*完整打印出全部字符*/for (int idx 0; idx sizeof(buffer); idx){printf(%c , buffer[idx]);}printf(\n);
4.1 循环read 思想 同循环实现直到没有读取到字符就结束循环即readBytes 0 readBytes是实际读取的字节数若为0则说明上一次就已经读取过了没有字符需要读取直接跳出循环这是一种特殊情况。 每循环一次若读取超过的BUFFER_SIZE - 1数据时就直接跳过判断直接打印BUFFER_SIZE - 1长度的数据如果只读取比BUFFER_SIZE - 1小的位置buffer是则在该位置添加\0结束符再执行打印。 然后继续循环直到读取不到字节为止 开始读取31下一个循环再读取31个字节最后将剩余的读取出来。 read 函数从文件 test.txt 中读取最多 BUFFER_SIZE- 1 个字节并将它们存储在 buffer 中。然后我们在读取的字节序列的末尾添加一个字符串终止符\0这样 buffer 就成了一个合法的 C 字符串。最后我们打印出读取的字节数和字符串内容。 代码块 #include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #define BUFFER_SIZE 32 int main() { const char *name ./test.txt; int fd1 open(name, O_RDWR | O_CREAT | O_APPEND, 0644); // O_APPEND:追加不写这个那么之前的的数据就会被覆盖 if (fd1 -1) { perror(open error); exit(-1); } printf(fd1: %d\n, fd1); char buffer[BUFFER_SIZE] {0}; int readBytes 0; while (1) { readBytes read(fd1, buffer, BUFFER_SIZE - 1);减去一个字节用于存储字符串终止符 if (readBytes 0) { /*上一次就已经读取完毕*/ break; } else { if (readBytes BUFFER_SIZE - 1) { buffer[readBytes] \0; } printf(readBytes:%d, \t buffer:%s\n, readBytes, buffer); } } printf(readBytes:%d, \t buffer:%s\n, readBytes, buffer); /*必须回收资源*/ close(fd1); return 0; }
5命令行参数 含义 argc是一个整数表示命令行参数的数量包括程序名称argv是一个指向字符指针数组的指针每个指针指向一个命令行参数。通过argv数组程序可以访问每个参数的值。 命令行输入三个参数指针对应三个参数 代码段 #include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #define BUFFER_SIZE 32 int main(int argc, const char *argv[]) { printf(argc: %d\n, argc); for(int idx 0; idx argc; idx) { printf(argv[%d]: %s\n, idx, argv[idx]); } return 0; } 6自实现cp函数 通过命令行参数和文件读写操作实现cp函数功能 ./mycp 文件1 文件2三个命令行参数所以若不等3这是错误 编辑 读多少写入多少 md5sum判断文件是否一模一样文件标识如果修改某一文件的内容则就会不一样 代码段
#include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #define BUFFER_SIZE 32
int main(int argc, const char *argv[]) { if(argc ! 3) { printf(error); return -1; } const char *srcName argv[1]; const char *dstName argv[2]; int srcfd open (srcName, O_RDONLY ); if(srcfd -1) { perror(srcname open error); return -1; } int dstfd open (dstName, O_RDONLY | O_CREAT, 0644); if(dstfd -1) { perror(dstname open error); close(dstfd); return -1; } char buffer[BUFFER_SIZE] {0}; int readBytes 0; while(1) { readBytes read(srcfd, buffer, BUFFER_SIZE - 1); if(readBytes 0) { perror(read error); break; } else if(readBytes 0) { break; } else if(readBytes 0) { write(dstfd, buffer, readBytes); } } /*回收资源*/ close(srcfd); close(dstfd); return 0; }
7lseek函数 函数定义 函数用法
#include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h
#define BUFFER_SIZE 5
int main() { const char *name ./test.txt; int fd1 open(name, O_RDWR); if (fd1 -1) { perror(open error); exit(-1); } printf(fd1: %d\n, fd1); char buffer[BUFFER_SIZE] {0}; int readBytes read(fd1, buffer, sizeof(buffer) - 1); printf(readBytes:%d\t buffer:%s\n, readBytes, buffer); // int offest lseek(fd1, 0, SEEK_END);//可以计算文件的大小 // printf(offest:%d\n, offest); int offest1 lseek(fd1, -1, SEEK_CUR);//左移一个 char ch 0; read(fd1, ch, 1); printf(ch: %c\n, ch); /*必须回收资源*/ close(fd1); return 0; }
8stat函数 函数定义 buf是返回的文件信息 文件类型需要使用下面宏指令去判断然后输出0则是这个类型若为1则不是。 函数作用 用来获取文件的状态信息比如在配置文件中如果修改了配置文件相应的也要修改其他文件但是判断该配置文件有没有修改就需要用stat函数去判断。 判断修改时间等。 代码段 time_t的使用必须使用ctime()函数打印不然会出现总共多少秒而不是年月日。 #include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h #include sys/stat.h #include sys/types.h #include time.h
#define BUFFER_SIZE 32
int main() { const char *name ./test.txt; struct stat bufferStat; memset(bufferStat, 0, sizeof(bufferStat)); stat(name, bufferStat); /*文件大小*/ printf(size: %ld\n, bufferStat.st_size); /*文件类型: 普通文件*/ printf(mode: %d\n, S_ISDIR(bufferStat.st_mode)); /*时间打印,最后一次访问时间*/ localtime(bufferStat.st_atime); printf(st_atime:%s \n, ctime(bufferStat.st_atime)); /*时间打印最后一次修改时间更改内容*/ localtime(bufferStat.st_mtime); printf(st_mtime:%s \n, ctime(bufferStat.st_mtime)); /*时间打印 最后一次被更改时间更改属性*/ localtime(bufferStat.st_ctime); printf(st_ctime: %s\n, ctime(bufferStat.st_ctime)); return 0; }
9access函数 函数定义 函数用法 大多数用来测试文件是否存在 F_OK 代码段
#include stdio.h
#include fcntl.h
#include errno.h
#include stdlib.h
#include unistd.h
#include string.h#define BUFFER_SIZE 32int main()
{const char *name ./test.txt;/*判断文件是否存在*/int ret access(name, F_OK);printf(ret:%d\n, ret);/*判断文件是否有读权限*/ret access(name, R_OK);/*判断文件是否写权限*/ret access(name, W_OK);/*判断文件是否有执行权限*/ret access(name, X_OK);printf(ret:%d\n, ret);return 0;
}10DIR DIR 结构体通常是由 dirent.h 或 direct.h 头文件取决于你的编译器和平台定义的但它是一个不透明的结构体意味着其内部成员并不直接暴露给程序员。你通常使用一组与 DIR 结构体相关的函数来操作它如 opendir(), readdir(), closedir() 等。 然而为了帮助你理解其概念我们可以假设一个简化的 DIR 结构体可能如下所示注意这不是实际的定义只是为了说明概念
// 这是一个简化的 DIR 结构体示例实际定义可能因平台或库而异
typedef struct DIR { // 当前目录的句柄或标识符 void* handle; // 指向当前读取到的目录项的指针 struct dirent* current_entry; // 其他可能的内部状态信息如读取位置、错误代码等 // ...
} DIR; // dirent 结构体通常用于表示目录中的一个项文件或子目录
typedef struct dirent { // 文件名或目录名的长度不包括终止的空字符 ino_t d_ino; // inode number在某些系统上可能不存在 off_t d_off; // 偏移量到下一个 dirent 的位置在某些系统上可能不存在 unsigned short d_reclen; // 此 dirent 结构体的长度 unsigned char d_type; // 文件类型如 DT_REG, DT_DIR 等 char d_name[256]; // 文件名包括空字符
} dirent;
10.1 定义 DIR结构体是一个在C语言中与目录操作相关的数据结构它用于保存当前正在被读取的目录的有关信息。 DIR结构体类似于FILE结构体是一个内部结构通常在使用readdir等函数时用到。 在这个结构体中各个成员的含义如下 __fd指向与目录流相关的文件描述符的指针。 __data指向存储目录条目的缓冲区的指针。 entry_data一个标志指示data是否包含有效的目录条目数据。 __ptr指向当前正在处理的目录条目的指针。 entry_ptr一个内部索引用于跟踪data缓冲区中的当前条目。 allocation分配给data缓冲区的总字节数。 size当前data缓冲区中有效数据的字节数。 __lock用于同步访问目录流的锁。 DIR结构体通常与readdir函数一起使用readdir函数用于从已打开的目录流中读取下一个目录条目并将其存储在dirent结构体中。这样通过连续调用readdir函数并传递DIR结构体作为参数可以遍历整个目录并获取每个文件和子目录的信息。 readdir 函数是一个在C语言中用于读取目录内容的函数。它通常与 DIR 结构体一起使用DIR 结构体是通过 opendir 函数打开的目录流的抽象表示。 10.2 用法及步骤
include stdio.h #include stdlib.h #include sys/types.h #include sys/stat.h #include fcntl.h #include errno.h #include unistd.h #include string.h #include dirent.h
#define BUFFER_SIZE 32
int main() { const char *name /home/TAO/operation-system/FileIO/testDIR/music; char buffer[BUFFER_SIZE] {0}; getcwd(buffer, BUFFER_SIZE - 1); printf(buffer:%s\n, buffer); DIR *dir opendir(name); if (dir NULL) { perror(opendir error); return -1; } /*文件打开成功*/ struct dirent *musicdir readdir(dir); while ((musicdir readdir(dir)) ! NULL) { if (musicdir-d_type ! DT_DIR) { printf(d_type:%d d_name:%s\n, musicdir-d_type, musicdir-d_name); } } /*关闭文件夹*/ closedir(dir); return 0; }
11getcwd函数 12truncate函数 函数定义 通常它指的是调整文件大小的操作。在C语言的文件I/O中truncate 函数用于更改已存在文件的大小。 代码段 #include stdio.h #include fcntl.h #include errno.h #include stdlib.h #include unistd.h #include string.h #includesys/types.h
#define BUFFER_SIZE 32
int main() { const char *name ./test.txt; int ret truncate(name, 3); printf(ret:%d\n, ret); return 0; }
13strdup函数 strdup 是 C 语言中的一个标准库函数通常用于动态分配内存并复制一个给定的字符串到新分配的内存中。这个函数在字符串的末尾会自动添加一个空字符 \0 以标记字符串的结束。 以下是 strdup 函数的详细说明 函数原型 #include string.h
char *strdup(const char *str); 功能 strdup 函数接受一个指向以 \0 结尾的字符串的指针 str 作为参数即要复制的字符串。 它动态地分配足够的内存使用 malloc 或其等价物来存储复制品包括额外的终止空字符。 然后它将源字符串的内容包括终止空字符复制到新分配的内存中。 最后它返回一个指向新分配的内存的指针该内存现在包含与源字符串相同的字符串。 使用注意事项 使用完 strdup 分配的内存后必须使用 free 函数来释放它以避免内存泄漏。 strdup 的参数不能是 NULL。如果传递了 NULL 指针函数的行为是未定义的并且可能会导致程序崩溃或错误。 由于 strdup 使用了 malloc 或其等价物来分配内存因此如果系统内存不足它可能会失败并返回 NULL。在调用 strdup 后应检查返回值是否为 NULL以确保内存分配成功。 示例
#include stdio.h
#include stdlib.h
#include string.h int main() { const char *original Hello, world!; char *copy strdup(original); if (copy NULL) { fprintf(stderr, Memory allocation failed\n); return 1; } printf(Original: %s\n, original); printf(Copy: %s\n, copy); free(copy); // Remember to free the allocated memory return 0;
} 5、文件输入与输出
1fopen函数 函数定义 用于在文件系统中打开一个文件并返回一个文件指针以便后续的文件操作如读取、写入等。 文件打开模式 r以只读方式打开文件。文件必须存在。 r以读写方式打开文件。文件必须存在。
- **w以只写方式打开文件。如果文件存在则清空文件内容如果文件不存在则创建新文件。**
- **w以读写方式打开文件。如果文件存在则清空文件内容如果文件不存在则创建新文件。**
- **a以追加方式打开文件用于写入。如果文件存在数据会被添加到文件末尾如果文件不存在则创建新文件。**
- **a以读写方式打开文件用于追加。如果文件存在数据会被添加到文件末尾如果文件不存在则创建新文件。读取操作会从文件开头开始。**
- x以只写方式创建新文件。如果文件已存在则打开文件失败。
- x以读写方式创建新文件。如果文件已存在则打开文件失败。
- b与上述模式结合使用表示以二进制模式打开文件。例如rb 表示以二进制模式只读方式打开文件。
- t与上述模式结合使用表示以文本模式打开文件。这是默认模式通常可以省略。例如rt 表示以文本模式只读方式打开文件。 需要注意的是当使用 w、w、x 或 x 模式打开文件时如果文件已存在其内容将被清空。如果希望保留原有文件内容并追加新数据应该使用 a 或 a 模式。 代码段
#include stdio.h
int main()
{
const char *name test.txt;FILE *fp fopen(name, w);if (fp NULL){perror(fopen error\n);return -1;}
char buffer[] hello world\n;fwrite(buffer, 1, sizeof(buffer), fp);
/*关闭文件*/fclose(fp);
return 0;
}
6、open与fopen的区别 在C语言中open和fopen都是用于打开文件的函数但它们属于不同的库并且在使用方式和功能上有所区别。 所属库 open函数是Unix/Linux系统调用的一部分通常定义在fcntl.h或sys/types.h和sys/stat.h中。 fopen函数是C标准库中的函数定义在stdio.h中。 使用方式 open函数通常用于低级文件操作它返回一个文件描述符一个小的非负整数用于后续的文件操作。例如int fd open(filename.txt, O_RDONLY); fopen函数返回一个FILE指针这是一个指向FILE结构体的指针该结构体包含了用于文件操作的缓冲区和其他信息。例如FILE *fp fopen(filename.txt, r); 错误处理 当open失败时它返回-1并设置全局变量errno以指示错误。你可以使用perror或strerror来获取错误描述。 当fopen失败时它返回NULL。你可以使用ferror和clearerr来处理错误。 模式 open函数使用标志如O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC等来指定打开文件的模式。 fopen函数使用字符串如r, w, a, r等来指定打开文件的模式。 功能 open是一个更底层的文件操作函数通常用于系统编程或需要与操作系统进行更直接交互的情况。 fopen提供了更高级别的文件操作包括缓冲和格式化输入/输出。 关闭文件 对于open打开的文件使用close函数来关闭。 对于fopen打开的文件使用fclose函数来关闭。 总的来说open和fopen都用于打开文件但fopen提供了更高级别的接口适合大多数常规的文件操作而open则提供了更底层的接口适合需要更多控制和与操作系统直接交互的情况。在大多数情况下开发者会倾向于使用fopen因为它提供了更易于使用的接口和更多的功能。
7、fprintf 和 fflush fprintf 和 fflush 是 C 语言中标准库提供的两个函数用于文件操作和缓冲区管理。
1fprintf 函数 fprintf 函数用于将格式化的输出写入一个指定的文件流。它的原型如下 int fprintf(FILE *stream, const char *format, ...); stream一个指向 FILE 对象的指针它指定了 fprintf 函数写入的文件流。这可以是标准输出 stdout、标准错误 stderr或者由 fopen、freopen 或 fdopen 函数打开的文件流。 format一个格式字符串它包含了将被插入到输出中的文本以及格式说明符。 ...根据格式字符串中的格式说明符这里需要提供相应的参数。 fprintf 函数返回写入的字符数不包括终止的空字符如果发生错误则返回负值。
2fflush 函数 fflush 函数用于刷新一个流。它的原型如下 int fflush(FILE *stream); stream一个指向 FILE 对象的指针它指定了需要刷新的流。如果 stream 是 NULL则 fflush 会刷新所有打开的输出流。 fflush 函数用于确保所有挂起的输出都被实际写入到其目标设备。这在你想要立即看到输出时很有用例如在打印调试信息或进度更新时。对于输入流fflush 的行为是未定义的。 fflush 函数成功时返回 0失败时返回 EOF。
3示例 下面是一个简单的示例它展示了如何使用 fprintf 和 fflush
#include stdio.h int main() { FILE *file fopen(example.txt, w); if (file NULL) { perror(Error opening file); return 1; } fprintf(file, Hello, World!\n); fflush(file); // 确保数据被写入文件 // 更多的文件操作... fclose(file); // 关闭文件 return 0;
}
在这个例子中fprintf 用于将字符串 Hello, World!\n 写入文件 example.txt。fflush 函数用于确保这个字符串立即被写入文件而不是等待缓冲区满或者文件被关闭。最后使用 fclose 关闭文件。 8、日志调试 改系统文件时一定要备份一份避免改坏了之后没有替换的文件。 日志调试在软件开发和运维过程中起到了重要的作用它主要用于记录程序运行过程中的详细信息帮助开发人员和运维人员定位问题、排查逻辑错误以及了解程序运行流程。以下是日志调试的主要作用和使用方法 一、日志调试的主要作用 打印调试日志可以记录程序运行的流程包括变量或某一段逻辑的执行情况这有助于排查逻辑问题。 问题定位当程序出现异常或故障时通过查看日志记录的信息可以快速定位问题所在方便后期解决。 用户行为日志记录用户的操作行为用于大数据分析比如监控、风控、推荐等。这种日志通常用于其他团队的分析使用因此需要按照约定的格式来记录。 根因分析在关键地方记录日志有助于定位问题的根源避免互相推脱责任。 二、日志调试的使用方法 设定日志级别根据需求设定不同的日志级别如DEBUG、INFO等。DEBUG级别最详细主要用于开发阶段的调试INFO级别通常用来记录系统运行的关键事件或正常流程信息。 设计好日志语句需要输出的日志数量是一个简约与信息量的权衡。日志中的每个记录应标记其在源代码里的位置、执行它的线程如果可用的话、时间精度并且通常还应包含一些额外的有效信息如变量的值、剩余内存大小、数据对象的数量等。 使用日志框架如log4j等可以控制日志信息输送的目的地如控制台、文件、GUI组件定义每一条日志信息的级别以及配置日志的输出格式。 定期查看和分析日志定期查看和分析日志可以帮助发现潜在的问题和优化程序的性能。 编写一个日志调试文件通常涉及以下几个步骤 选择或实现日志库你可以使用现有的日志库比如log4c或者自己实现一个简单的日志系统。 定义日志级别例如DEBUG、INFO、WARN、ERROR等。 实现日志输出函数这些函数负责将日志信息写入文件或控制台。 在代码中插入日志语句在代码的关键位置插入日志语句以便在运行时记录信息。
#include stdio.h
#include time.h
#include unistd.h
#include fcntl.h
#include error.h/* 调试标记是否存在 */
int g_debug 0;/* 文件指针 */
FILE *g_logfp NULL;#define LOGPR(fmt, args...) \do \{ \if (g_debug) \{ \time_t now; \struct tm *ptm NULL; \now time(NULL); \ptm localtime(now); \fprintf(g_logfp, [file:(%s), func(%s), line(%d), time(%s)]: fmt, \__FILE__, __FUNCTION__, __LINE__, asctime(ptm), ##args); \fflush(g_logfp); \} \}while(0)/* 日志 : 就是文件 */
/* 打开日志文件 /*/
void log_init()
{ time_t now;/* 避免野指针 */struct tm *ptm NULL;
#define DEBUG_FLAG ./my_debug.flag/* access函数 成功返回0 */if (access(DEBUG_FLAG, F_OK) 0){g_debug 1;}if (!g_debug){return;}#define DEBUG_FILE /var/log/test_main.logif ((g_logfp fopen(DEBUG_FILE, a)) NULL){perror(fopen error);return;}now time(NULL);ptm localtime(now);LOGPR(log init done.);LOGPR(%s, asctime(ptm));return;
}/* 关闭文件 */
void log_close()
{if (g_logfp){fclose(g_logfp);g_logfp NULL;}
}int main()
{/* 启动日志程序 */log_init();int count 50;while(count--){LOGPR(count %d\n, count);sleep(2);}/* 关闭文件 */log_close();return 0;
}操作系统
1、进程
1top top 可以查看进程状态 同Windows任务管理器 2进程 进程是在运行过程的程序如果该程序结束就不存在进程因此使用代码演示时采用sleep()函数将程序休眠一段时间在终止以便查看进程。 3ps - ef | grep 命令 ps - ef 查看进程 grep 是过滤如果想查看某一文件的进程使用该命令查看 grep -v是忽视 4PID与PPID 5kill kill -9 是一个Linux命令用于强制终止一个进程。当你在终端中输入 kill -9 PID 并按下回车后系统会发送一个强制终止信号SIGKILL给指定进程的进程IDPID以立即终止该进程。 注意使用 kill -9 命令会立即终止目标进程而不给进程任何机会保存数据或执行清理操作。这是一种非常强硬的终止方式应该谨慎使用。通常情况下应该首先尝试使用 kill PID 命令发送默认的终止信号SIGTERM给进程让进程有机会优雅地退出。只有在无法正常终止进程或者需要立即停止一个非响应的进程时才考虑使用 kill -9。 6后台运行 加上符号就是后台运行 当你使用 ./main 命令来在后台运行一个程序例如一个名为 main 的可执行文件该命令会立即返回命令行的控制权给你而 main 程序会在后台开始执行。由于它是在后台运行的因此你不会在终端中直接看到该程序的输出除非你明确地将其重定向到某个文件或终端。 7进程的状态 运行态和就绪态都是CPU执行 8ps命令 ps -auxf 进程状态信息 9进程号和相关函数 每个文件进程都对应的一个进程号其进程号的维护在proc文件里 10free 与 free -h -h是人性化的意思free -h 可以将显示内存占了具体大小。 其他命令加上-h也会显示人性化的内容。 11getpid函数 与 getppid函数 前者是子进程号pid后者是父进程号ppid 所有进程的老大是祖先进程init()进程 12进程退出函数 13代码段
#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h
/*什么是程序 什么叫代码 什么叫进程*/ /*什么情况下会导致CPU飙升死循环*/ int main() { int fd open(./makefile, O_RDONLY); pid_t pid getpid(); printf(pid is %d\n, pid); pid_t ppid getppid(); printf(ppid is %d\n, ppid); int count 20; while (count--) { sleep(1); } /*休眠让出CPU资源*/ // sleep(1); return 0; }
2、多进程的创建
1fork函数 注意它的返回值成功后在父进程中返回子进程的PID 在子进程中返回0。失败时在第1段中返回-1父进程时不会创建任何子进程并且正确设置了errno。 fork函数一旦执行进程就会创建而且父进程和子进程都是独立的内存空间因此能够同时进行。 2父子进程关系 使用fork(函数得到的子进程是父进程的一个复制品它从父进程处继承了整个进程的地址空间包括进程上下文进程执行活动全过程的静态描述、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。 子进程所独有的只有它的进程号计时器等(只有小量信息)。因此使用fork(函数的代价是很大的。 简单来说一个进程调用fork()函数后系统先给新的进程分配资源例如存储数据和代码的空间。然后把原来的进程的有值都复制到新的新进程中只有少数值与原来的进程的值不同。相当于克隆了一个自己。 实际上更准确来说Linux的fork0使用是通过写时拷贝(copy- on-write)实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间而是让父子进程共享同一个地址空间。 只用在需要写入的时候才会复制地址空间从而使各个进行拥有各自的地址空间。也就是说资源的复制是在需要写入的时候才会进行在此之前只有以只读方式共享。 注意: fork之后父子进程共享文件fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表引用计数增加共享文件文件偏移指针。 3代码段
#include stdio.h
#include sys/types.h
#include unistd.h
/** * 多进程并发处理 * 1、优点干活快 ------效率 * 2、缺点消耗资源 -------资源 * 3、压力测试性能瓶颈 ** */
int main()
{
#if 1
//守护进程子进程和父进程同时进行 pid_t pid fork(); //vfork()让父进程先进行
if (pid 0) { perror(fork error\n); return -1; } else if (pid 0) { usleep(1000); //1000us 1 ms 0.001s /*子进程和父进程执行顺序是随机的谁先打印由操作系统打印*/ printf(child process\n); printf(child: pid is %d\t parent: ppid is %d\n, getpid(), getppid());
while (1) { sleep(1); } } else { printf(pid:%d\n, pid); printf(parent process\n); /*子进程和父进程执行顺序是随机的谁先打印由操作系统打印*/ printf(parent :pid is %d\t parent :ppid is %d\n, getpid(), getppid());
while (1) { sleep(1); } }
#endif
#if 0 //父进程结束导致孤儿进程 pid_t pid fork();
if (pid 0) { perror(fork error\n); return -1; } else if (pid 0) { printf(child process\n); printf(child: pid is %d\t parent: ppid is %d\n, getpid(), getppid());
while (1) { sleep(1); } } else { printf(parent process\n); printf(parent :pid is %d\t parent :ppid is %d\n, getpid(), getppid());
} /*这种情况是父进程结束了子进程没人来回收孤儿进程会交给系统管理自动回收*/
#elseif 0
// 子进程结束导致僵尸进程 pid_t pid fork();
if (pid 0) { perror(fork error\n); return -1; } else if (pid 0) { printf(child process\n); printf(child: pid is %d\t parent: ppid is %d\n, getpid(), getppid()); } else { printf(parent process\n); printf(parent :pid is %d\t parent :ppid is %d\n, getpid(), getppid());
while (1) { sleep(1); } } /*总结子进程结束父进程没有回收他的资源就会导致子进程就是僵尸进程危害资源依旧被占用没有释放*/
return 0;
}
#endif
3、独立空间
1局部变量 局部变量中子进程是拿不到父进程中的数据因为是独立的空间。 2全局变量 与上面同理 3通信 可以通过文件读写操作让子进程读取到父进程的值将父进程的数据写入文件子进程读取文件。
4代码段
#include stdio.h
#include fcntl.h
#include errno.h
#include stdlib.h
#include unistd.h
#include string.h
/*全局变量*/
int g_data 666;
int main()
{ /*局部变量*/ int val 100;
#if 0 /*创建进程*/ pid_t pid fork(); if (pid 0) { perror(fork error); } else if (pid 0) { sleep(1);//这样就能保证父进程先执行
/*子进程*/ printf(child: val:%d\n, val); printf(child: g_data:%d\n, g_data);
/*将文件数据读取*/ int fd open(fork.txt, O_RDONLY); if (fd -1) { perror(open error); exit(-1); }
int data 0; int readBytes read(fd, (void *)data, sizeof(int)); if (readBytes 0) { } else if(readBytes 0) {
} else { printf(g_data: %d\n, data); }
printf(readbytes: %d\n, readBytes);
/*必须回收资源*/ close(fd);
if(g_data 1000) {
} } else { /*父进程*/ printf(parent: val:%d\n, val); printf(parent: g_data:%d\n, g_data);
val 666; printf(parent: val:%d\n, val);
g_data 222; printf(parent: g_data:%d\n, g_data);
/*将数据写入文件*/ int fd open(fork.txt, O_WRONLY | O_CREAT, 0644); if (fd -1) { perror(open error); exit(-1); }
int writeBytes write(fd, g_data, sizeof(int)); if (writeBytes -1) { perror(write error); return -1; }
printf(writebytes: %d\n, writeBytes);
/*必须回收资源*/ close(fd); }
sleep(3);
#endif
// 这个开一个fd太麻烦了拿建议用上面的方法。 /*将数据写入文件*/ int fd open(fork.txt, O_RDWR | O_CREAT, 0644); if (fd -1) { perror(open error); exit(-1); }
/*创建进程*/ pid_t pid fork(); if (pid 0) { perror(fork error); } else if (pid 0) { sleep(1); // 这样就能保证父进程先执行
/*子进程*/ printf(child: val:%d\n, val); printf(child: g_data:%d\n, g_data);
/*将文件数据读取*/ lseek(fd, 0, SEEK_SET); // 文件指针复原到开头 int data 0; int readBytes read(fd, (void *)data, sizeof(int)); if (readBytes 0) { perror(read error); return -1; } else if (readBytes 0) { printf(readBytes 0); } else { printf(g_data: %d\n, data); }
printf(readbytes: %d\n, readBytes);
if (g_data 1000) { } } else { /*父进程*/ printf(parent: val:%d\n, val); printf(parent: g_data:%d\n, g_data);
val 666; printf(parent: val:%d\n, val);
g_data 222; printf(parent: g_data:%d\n, g_data);
/*写入文件*/ int writeBytes write(fd, g_data, sizeof(int)); if (writeBytes -1) { perror(write error); return -1; }
printf(writebytes: %d\n, writeBytes); } sleep(10);
/*关闭文件描述符*/ close(fd);
sleep(3);
return 0;
}
4、等待子进程进程退出函数
1概述 在每个进程退出的时候内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息 这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。 wait)和waitpid()函数的功能一样区别在于wait()函数会阻塞waitpid(可以设置不阻塞waitpid)还可 以指定等待哪个子进程结束。 注意:一次wait或waitpid调用只能清理一个 子进程清理多个子进程应使用循环。
2wait函数 注意wait函数是回收已经结束的子进程的资源如果父进程比子进程先结束那么父进程中的wait()会启用导致子进程阻塞了。 3阻塞函数
3.1 阻塞函数 程序卡在这边在等条件满足在等子进程退出 3.1.1 阻塞的原因 当没有数据时read就会阻塞。 read在等条件是否有数据可读通知就是条件。是通过内核通知的有数据需要让read去读 strace -p 查看进程是否堵塞 查看进程有没有堵塞下面是堵塞时的状态 strace -p 进程号 3.2 解决进程阻塞问题
fcntl函数 步骤 flag | O_NONBLOCK; 是C语言中的一个位运算表达式用于设置flag变量的某个位。具体来说它设置了flag变量中与O_NONBLOCK对应的位。 这里的关键操作是|这是一个复合赋值运算符表示按位或然后赋值。O_NONBLOCK是一个宏通常在Unix-like系统的头文件中定义用于文件或套接字操作以设置非阻塞模式。 flag | O_NONBLOCK;这行代码的作用是确保flag变量中对应于O_NONBLOCK的位被设置为1通常用于将文件描述符或套接字设置为非阻塞模式。
/* 获取文件描述符当前属性 */ int flag fcntl(pipefd[0], F_GETFL); /* 设置成非阻塞 */ flag | O_NONBLOCK; fcntl(pipefd[0], F_SETFL, flag);
4waitpid函数 5代码段
#include stdio.h #include sys/types.h #include unistd.h #include sys/wait.h
//int g_status 100;//全局
int main() { // 守护进程子进程和父进程同时进行 pid_t pid fork(); // vfork()让父进程先进行 if (pid 0) { perror(fork error\n); return -1; } else if (pid 0) { usleep(1000); // 1000us 1 ms 0.001s /*子进程和父进程执行顺序是随机的谁先打印由操作系统打印*/ printf(child process\n); printf(child: pid is %d\t parent: ppid is %d\n, getpid(), getppid()); int count 3; while (count--) { sleep(1); } int g_status 1;//局部 _exit(g_status); } else { printf(pid:%d\n, pid); printf(parent process\n); /*子进程和父进程执行顺序是随机的谁先打印由操作系统打印*/ printf(child :pid is %d\t parent :ppid is %d\n, getpid(), getppid()); /*阻塞函数程序卡在这边在等条件满足在等子进程退出*/ #if 0 int status 0; wait(status); printf(status: %d\n, WEXITSTATUS(status)); #else int status 0; waitpid(pid, status, 0); printf(status: %d\n, WEXITSTATUS(status)); #endif } /*子进程退出但是没有回收子进程的资源*/ printf(hello world); return 0; }
5、僵尸进程与孤儿进程 6、IPC
0进程间通信 因为进程具有独立的地址空间所以必须有通信机制。IPC 进程间通信Inter-Process Communication, IPC是指不同进程之间传输数据或同步执行的机制。在操作系统中有多种方式实现进程间通信以下是几种常见的方法 管道Pipe 匿名管道一种半双工的通信方式常用于具有亲缘关系的父子进程间通信。由pipe()系统调用创建具有固定的读写方向。 命名管道FIFO允许无关的进程间进行通信通过文件系统路径来命名通过mkfifo()创建。 消息队列Message Queues 允许一个或多个进程通过消息进行通信的一种机制。消息队列独立于发送和接收进程存在常用于解耦和异步通信。 共享内存Shared Memory 允许多个进程访问同一块物理内存进程可以直接读写共享内存效率高但需要额外的同步机制来避免数据一致性问题。 信号量Semaphores 用于进程间同步的计数器可以用来保护共享资源防止进程之间的竞态条件。 套接字Socket 一种用于不同主机或同一主机上不同进程间通信的机制基于网络协议如TCP/IP或本地域协议如Unix域套接字实现。 1. 消息队列Message Queues 操作方式 创建消息队列使用msgget()系统调用创建消息队列。需要指定一个key和一组标志来定义队列的属性。 int msgget(key_t key, int msgflg); 发送消息使用msgsnd()系统调用向消息队列发送消息。需要指定消息队列标识符、消息结构体指针、消息长度和消息类型。 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 接收消息使用msgrcv()系统调用从消息队列接收消息。需要指定消息队列标识符、消息结构体指针、消息长度和消息类型。 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); 删除消息队列使用msgctl()系统调用删除消息队列。 int msgctl(int msqid, int cmd, struct msqid_ds *buf); 2. 共享内存Shared Memory 操作方式 创建或获取共享内存使用shmget()系统调用创建或获取一个共享内存段。需要指定key、大小和权限等参数。 int shmget(key_t key, size_t size, int shmflg); 映射共享内存使用shmat()系统调用将共享内存段连接到当前进程的地址空间中。 void *shmat(int shmid, const void *shmaddr, int shmflg); 操作共享内存通过指针直接读写共享内存数据操作类似于普通的内存操作。 解除映射使用shmdt()系统调用解除共享内存段与当前进程的连接。 int shmdt(const void *shmaddr); 删除共享内存使用shmctl()系统调用删除共享内存段。 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 3. 信号量Semaphores 操作方式 创建或获取信号量集使用semget()系统调用创建或获取一个信号量集。需要指定key、信号量数量和权限等参数。 int semget(key_t key, int nsems, int semflg); 初始化信号量使用semctl()系统调用初始化信号量集中的每个信号量。 int semctl(int semid, int semnum, int cmd, ...); 常见的初始化命令包括设置信号量的初始值和获取当前值等。 操作信号量使用semop()系统调用对信号量进行操作如增加V操作或减少P操作信号量的值。 int semop(int semid, struct sembuf *sops, size_t nsops); 删除信号量使用semctl()系统调用删除信号量集。 1管道-无名管道
1.1 无名管道 概述 管道也叫无名管道它是UNIX系统IPC(进程间通信)的最古老形式所有的UNIX系统都支持这种通信机制。 管道有如下特点: 半双工数据在同一时刻只能在一个方向上流动。 数据只能从管道的一端写入 从另一端读出。 写入管道中的数据遵循先入先出的规则。 管道所传送的数据是无格式的这要求管道的读出方与写入方必须事先约定好数据的格式如多少字节算一个消息等。 管道不是普通的文件不属于某个文件系统其只存在于内存中。 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。 从管道读数据是一次性操作数据一旦被读走 它就从管道中被抛弃释放空间以便写更多的数据。 管道没有名字只能在具有公共祖先的进程(父进程与子进程或者两个兄弟进程具有亲缘关系)之间使用。 对于管道特点的理解我们可以类比现实生活中管子管子的一端塞东西管子的另一端取东西。 管道是一种特殊类型的文件在应用层体现为两个打开的文件描述符。 半双工 类似独木桥原理 1.2 pipe函数 pipefd[0]是读管道 pipefd[1]是写管道 1.3 代码块 #include stdio.h #include unistd.h #include sys/types.h #include unistd.h #include stdlib.h #include sys/wait.h #include string.h #include fcntl.h #include errno.h #include error.h
/* 为什么要有进程间通信? 因为进程具有独立的地址空间, 所以必须要有通信机制. IPC */
#define PIPE_SIZE 2 #define BUFFER_SIZE 32
int main() { int pipefd[PIPE_SIZE] {0}; /* 创建管道 */ int ret pipe(pipefd); if (ret -1) { perror(pipe error); exit(-1); } /* 创建进程 */ pid_t pid fork(); if (pid 0) { perror(fork error); exit(-1); } else if (pid 0) { sleep(2); /* 子进程读 */ /* pipefd[0]: 读 */ /* 将写端关闭 */ close(pipefd[1]); /* 获取文件描述符当前属性 */ int flag fcntl(pipefd[0], F_GETFL); /* 设置成非阻塞 */ flag | O_NONBLOCK; fcntl(pipefd[0], F_SETFL, flag); char buffer[BUFFER_SIZE] {0}; int readBytes read(pipefd[0], buffer, sizeof(buffer) - 1); if (readBytes 0) { if (errno EAGAIN) { printf(heiheihei\n); } else { perror(read error); } } else if (readBytes 0) { printf(readBytes 0\n); } else { printf(child --- readBytes:%d,\tbuffer:%s\n, readBytes, buffer); } /* 关闭读端 */ close(pipefd[0]); } else { /* 父进程写 */ /* pipefd[1]: 写 */ /* 将读端关闭 */ close(pipefd[0]); char * str hello world; write(pipefd[1], str, strlen(str) 1); /* 回收子进程资源 */ wait(NULL); /* 关闭写端 */ close(pipefd[1]); } return 0; }
2管道-有名管道
2.1 无名与有名的区别 无名管道不是普通的文件不属于某个文件系统其只存在于内存中。 有名以FIFO的文件形式存在于文件系统中 2.2 mkfifo函数创建有名管道 2.3 有名管道读写操作 2.4 有名管道的通信 有名管道通信可以在不具备亲属进程之间 在具有亲属关系进程通信 通过fork函数创建进程使其具备亲属关系。
#include stdio.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include errno.h
#include error.h
#include stdlib.h
#include unistd.h
#include string.h
#include wait.h
#define BUFFER_SIZE 5
int main()
{/* 创建命名管道 */const char * fileName ./fifoInfo;int ret mkfifo(fileName, 0644);if (ret -1){if (errno ! EEXIST){perror(mkfifo error);exit(-1);}}/* 创建进程 */pid_t pid fork();if (pid 0){
}else if (pid 0){printf(child process\n);/* 打开管道文件 *//* 必选项: 只读 / 只写 / 可读可写可选项: { 追加 创建 清空 }*/int fd open(fileName, O_RDWR);if (fd -1){perror(open error);}
/* 设置非阻塞 */int flag fcntl(fd, F_GETFL)flag | O_NONBLOCK;fcntl(fd, F_SETFL, flag);
char buffer[BUFFER_SIZE] { 0 };int readBytes 0;while (1){readBytes read(fd, buffer, BUFFER_SIZE - 1);if (readBytes 0){if (errno EAGAIN){break;}else{perror(read error);close(fd);exit(-1);} }else if (readBytes 0){printf(readBytes 0);break;}else{printf(readBytes %d\t, buffer:%s\n, readBytes, buffer);}sleep(2);}/* 关闭文件描述符 */close(fd);}else{printf(parent process\n);/* 打开管道文件 *//* 必选项: 只读 / 只写 / 可读可写可选项: { 追加 创建 清空 }*/int fd open(fileName, O_RDWR);if (fd -1){perror(open error);}
char * buf hello world;int cnt 3;
int writeBytes 0;while (cnt--){writeBytes write(fd, buf, strlen(buf) 1);if (writeBytes 0){perror(write error);}else if (writeBytes 0){printf(buffer overflow...\n);}else{printf(writeBytes %d\n, writeBytes);}sleep(1);}
/* 关闭文件描述符, 回收资源. */close(fd);
/* 回收子进程资源 */wait(NULL);}
return 0;
}
在不具有亲属关系进程**
- 分别建立读写库通过将文件至于统一的路径分别进行写与读 头文件#include stdio.h#include sys/types.h#include sys/stat.h#include fcntl.h#include errno.h#include error.h#include stdlib.h#include unistd.h#include string.h#include wait.h write
#define BUFFER_SIZE 5
int main()
{const char *fileName ../fifoInfo;//使用上一目录文件/*判断文件存不存在*/int ret1 access(fileName, F_OK);if (ret1 -1){perror(file not exist\n);}/* 创建命名管道 */int ret mkfifo(fileName, 0644);if (ret -1){if (errno ! EEXIST){perror(mkfifo error);exit(-1);}}/* 打开管道文件 *//* 必选项: 只读 / 只写 / 可读可写可选项: { 追加 创建 清空 }*/int fd open(fileName, O_RDWR);if (fd -1){perror(open error);}char *buf hello world;int cnt 3;int writeBytes 0;while (cnt--){writeBytes write(fd, buf, strlen(buf) 1);if (writeBytes 0){perror(write error);}else if (writeBytes 0){printf(buffer overflow...\n);}else{printf(writeBytes %d\n, writeBytes);}sleep(4);}/* 关闭文件描述符, 回收资源. */close(fd);return 0;
}
read
#define BUFFER_SIZE 5
int main()
{const char *fileName ../fifoInfo; // 读上一目录文件/*判断文件存不存在*/int ret1 access(fileName, F_OK);if (ret1 -1){perror(file not exist\n);}/* 创建命名管道 */int ret mkfifo(fileName, 0644);if (ret -1){if (errno ! EEXIST){perror(mkfifo error);exit(-1);}}/* 打开管道文件 *//* 必选项: 只读 / 只写 / 可读可写可选项: { 追加 创建 清空 }*/int fd open(fileName, O_RDWR);if (fd -1){perror(open error);}/* 设置非阻塞 */int flag fcntl(fd, F_GETFL);flag | O_NONBLOCK;fcntl(fd, F_SETFL, flag);char buffer[BUFFER_SIZE] {0};int readBytes 0;while (1){readBytes read(fd, buffer, BUFFER_SIZE - 1);if (readBytes 0){if (errno EAGAIN){break;}else{perror(read error);close(fd);exit(-1);}}else if (readBytes 0){printf(readBytes 0);break;}else{printf(readBytes %d\t, buffer:%s\n, readBytes, buffer);}sleep(2);}/* 关闭文件描述符 */close(fd);return 0;
}
3共享存储映射 3.1 概述 存储映射I/O (Memory mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。 于是当从缓冲区中取数据就相当于读文件中的相应字节。于此类似将数据存入缓冲区则相应的字节就自动写入文件。这样就可在不适用read和write函数的情况下使用地址(指针)完成I/O操作。 共享内存可以说是最有用的进程间通信方式也是最快的IPC形式因为进程可以直接读写内有而不需要任何数据的拷贝。
3.2 mmap函数存储映射函数 关于mmap函数的使用总结: 1)第一个参数写成NULL 2)第二个参数要映射的文件大小 0 3)第三个参数: PROT_READ 、PROT_WRITE 4)第四个参数: MAP_ SHARED或者MAP_ PRIVATE 5)第五个参数:打开的文件对应的文件描述符 6)第六个参数: 4k的整数倍通常为0
3.3 munmap函数 3.3 注意事项 创建映射区的过程中隐含着一次对映射文件的读操作。 当MAP_SHARED时要求映射区的权限应文件打开的权限(出于对映射区的保护)。而MAP _PRIVATE则无所谓因为mmap中的权限是对内存的限制。 映射区的释放与文件关闭无关。只要映射建立成功文件可以立即关闭。 特别注意当映射文件大小为0时不能创建映射区。所以用于映射的文件必须要有实际大小。mmap使用时常常会出现总线错误通常是由于共享文件存储空间大小引起的。 munmap传入的地址一定是munmap的返回地址。坚决杜绝指针 操作。 如果文件偏移量必须为4K的整数倍。 mmap创建映射区出错概率非常高一定要检查返回值确保映射区建立成功再进行后续操作。
3.4 代码段
#include stdio.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include sys/mman.h
#include stdlib.h
#include time.h
#include string.h
#include sys/wait.h
#include unistd.hint main()
{/*获取文件大小*/char *name ./tongxing.txt;int fd open(name, O_RDWR);if (fd -1){perror(open error);exit(-1);}/*获取文件大小*/off_t len lseek(fd, 0, SEEK_END);printf(len: %ld\n, len);/*映射:一个文件映射到内存addr指向此内存*/void *addr mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (addr MAP_FAILED){perror(mmap error);exit(-1);}/*关闭文件*/close(fd);/*创建进程*/pid_t pid fork();if (pid 0){perror(fork error);exit(-1);}else if (pid 0){/*子进程*//*读数据*/usleep(1000);printf(buffer: %s \n, (char *)addr);}else{/*父进程*//*写数据*/char *str hello world;strncpy((char *)addr, str, strlen(str) 1);/*回收子进程资源*/wait(NULL);}printf(addr: %p\n, addr);/*解除映射*/munmap(addr, len);return 0;
} 4信号
4.1 特点 简单 不能携带大量信息 满足某个特设条件才发送
4.2 概述 信号可以直接进行用户空间进程和内核空间进程的交互内核进程可以利用它来通知用户空间进程发生了哪些系统事件。 一个完整的信号周期包括三个部分信号的产生信号在进程中的注册信号在进程中的注销执行信号处理函数。
4.3 信号的编号 可以通过kill -l查看相应的信号
重要的几个信号 4.4 信号的默认动作 这里特别强调了 9) SIGKILL 和 19) SIGSTOP信号不允许忽略和捕捉只能执行默认动作。甚至不能将其设置为
4.5 信号产生函数
4.5.1 kill函数 其中getpid() 获取当前的进程号。
kill(getpid(), sig); 例子在父进程中杀死子进程 在父进程中pid返回的是子进程中的pid值即为0。 int pipefd[PIPE_SIZE] {0};pipe(pipefd);pid_t pid fork();if (pid 0){}else if (pid 0){/*子进程*/while (1){printf(i am child\n);} }else{sleep(1);/*父进程*/int ret1 kill(pid, SIGKILL);if (ret1 -1){perror(kill error);exit(-1);}/*回收资源*/wait(NULL);}return 0;
4.5.2 raise函数 参数为指定信号
#include stdio.h
#include string.h
#include sys/types.h
#include sys/wait.h
#include unistd.h
#includesignal.h
#includestdlib.hint main()
{int count 15;while(count--){if(count 5){raise(SIGKILL);}printf(hello world\n);sleep(1);}return 0;
}
运行结果
4.5.3 abort函数 无参数返回终止信号
int main()
{int count 5;while(count--){if(count 2){/*给自己发送终止信号*/abort();}printf(hello world\n);sleep(1);}return 0;
}
5信号捕捉
5.1 信号处理方式 一个进程收到一个信号的时候可以用如下方法进行处理: 执行系统默认动作 对大多数信号来说系统默认动作是用来终止该进程。 忽略此信号(丢弃) 接收到此信号后没有任何动作。 执行自定义信号处理函数(捕获) 用用户定义的信号处理函数处理该信号。 [注意] : SIGKILL和SIGSTOP不能更改信号的处理方式因为它们向用户提供了一种使进程终止的可靠方法。
5.2 signal函数 signal函数是注册信号 kill是发送信号 举例 要求1:不管我客户端做任何事情服务器都不能宕机 要求2: CtrlC 回收资源 不允许交由操作系统 因为 CtrlC是系统回收资源为确保不宕机崩了因此可以使用信号接收CtrlC然后实施回收操作。 通过注册一个信号比如SIGIN该信号接收Ctrl C的指令。 因此按Ctrl C则返回SIGIN信号传进signal函数执行钩子函数。 代码段
int g_fd 0;/*钩子函数要求函数命名一定要有Handler*/
void sigHandler(int num)
{printf(tao wanbao num:%d\n, num);close(g_fd);exit(0);
}
int main()
{// signal(SIGALRM, sigHandler);//自定义动作// signal(SIGALRM, SIG_IGN);//忽略该信号signal(SIGINT, sigHandler); // 自定义动作const char *name ./test.txt;g_fd open(name, O_RDWR | O_CREAT, 0644);if (g_fd -1){perror(open error);exit(-1);}char buffer[BUFFER_SIZE] {0};strcpy(buffer, hello world\n);int count 10;int writeBytes 0;while (count--){writeBytes write(g_fd, buffer, strlen(buffer) 1);printf(writeBytes: %d\n, writeBytes);if(writeBytes 0){perror(write error);exit(-1);}sleep(1);}close(g_fd);return 0;
}
5.3 信号捕捉的原理 5.4 SIGCHLD信号
5.4.1 SIGCHLD信号产生条件 这个信号wait()也在使用这样wait才能回收子进程的资源。 5.4.2 如何避免僵戶进程 最简单的方法父进程通过wait()和waitpid()等函数等待子进程结束但是这会导致父进程挂起。 如果父进程要处理的事情很多不能够挂起通过signal() 函数人为处理信号SIGCHLD只要有子进程退出自动调用指定好的回调函数因为子进程结束后父进程会收到该信号SIGCHLD可以在其回调函数里调用wait()或waitpid()回收。 7、线程
1线程概念 在许多经典的操作系统教科书中总是把进程定义为程序的执行实例它并不执行什么只是维护应用程序所需的各种资源而线程则是真正的执行实体。 所以线程是轻量级的进程(LWP: light weight process)在Linux环境下线程的本质仍是进程。 为了让进程完成一定的工作 进程必须至少包含一个线程。 进程直观点说保存在硬盘上的程序运行以后会在内存空间里形成一个独立的内存体 这个内存体有自己的地址空间有自己的堆上级挂靠单位是操作系统。操作系统会以进程为单位分配系统资源所以我们也说进程是CPU分配资源的最小单位。 线程存在与进程当中(进程可以认为是线程的容器)是操作系统任务调度执行的最小单位。说通俗点线程就是干活的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体是CPU调度和分派的基本单位它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源只拥有一点在运行中必不可少的资源(如程序计数器-组寄存器和栈)但是它可与同属一个 进程的其他的线程共享进程所拥有的全部资源。 如果说进程是一个资源管家负责从主人那里要资源的话那么线程就是干活的苦力。一个管家必须完成一项工作就需要最少一个苦力也就是说一个进程最少包含一个线程也可以包含多个线程。苦力要干活就需要依托于管家所以说一个线程必须属于某一个进程。 进程有自己的地址空间线程使用进程的地址空间也就是说进程里的资源线程都是有权访问的比如说堆啊栈啊静态存储区什么的。
总结线程是依托进程而存在线程的创建也是基于进程的如果只是单独的进程该进程又被称为主进程/主线程。
2线程的特点 类Unix系统中早期是没有线程”概念的80年代才引入借助进程机制实现出了线程的概念。 因此在这类系统中进程和线程关系密切: 线程是轻量级进程(light-weight process)也有PCB 创建线程使用的底层函数和进程一样都是clone。 从内核里看进程和线程是一样的都有各自不同的PCB。 进程可以蜕变成线程。 在linux下线程是最小的执行单位进程是最小的分配资源单位。
3线程常用操作
3.1 线程号 就像每个进程都有一个进程号一样每个线程也有一个线程号。进程号在整个系统中是唯一的但线程号不同线程号只在它所属的进程环境中有效。 进程号用pid_t数据类型表示是一个非负整数。线程号则用pthread_t数据类型来表示线程标识符IDLinux 使用无符号长整数表示。 有的系统在实现pthread _t的时候用一个结构体来表示所以在可移植的操作系统实现不能把它做为整数处理。
3.1.2 pthread_self函数 获取线程号返回的是%ld 3.2 线程的创建
3.2.1 phread_create函数 编译时需要加库-lpthread 第三个参数为函数的首地址可不是回调函数。 看进程的信息是使用ps -ef 看线程的信息是使用gbd attach 进程号
#include pthread.h
#include stdio.h
#include sys/types.h
#include unistd.h
#includestdlib.hvoid *thread_func()
{printf(new thread:%ld\n, pthread_self());//子线程
}int main()
{pthread_t tid ;int ret pthread_create(tid, NULL, thread_func, NULL);if(ret ! 0){perror(pthread_create error\n);exit(-1);}printf(main thread:%ld\n, pthread_self());//主线程sleep(1);return 0;
}
3.3 线程的资源回收
3.3.1 pthread_join函数 回收的是子线程的资源同wait()差不多。 若主线程结束子线程还没有结束就阻塞。 void *thread_func()
{printf(new thread:%ld\n, pthread_self());sleep(1);/*线程结束*//*方式1*/static int retStatus -45;//static修饰的变量也会等程序结束才会清空该地址/*方式二*///int *retStatus (int *)malloc(sizeof(int)* 1);*//retStatus -45;pthread_exit(retStatus);
}
int main()
{pthread_t tid ;int ret pthread_create(tid, NULL, thread_func, NULL);if(ret ! 0){perror(pthread_create error\n);exit(-1);}printf(tid:%ld\n, tid);printf(main thread:%ld\n, pthread_self());/*阻塞等待回收子线程的资源*/int *retStatus NULL;pthread_join(tid, (void *)retStatus);printf(retStatus: %d\n, *retStatus);sleep(1);return 0;
} 3.4 线程退出
在进程中我们可以调用exit函数或_exit函数来结束进程在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。 线程从执行函数中返回。 线程调用pthread_exit退出线程。 线程可以被同一进程中的其它线程取消。
3.4.1 pthread_exit函数 3.5 线程取消 注意线程的取消并不是实时的而又一定的延时。需要等待线程到达某个取消点(检查点)。类似于玩游戏存档必须到达指定的场所(存档点如:客栈、仓库、城里等)才能存储进度。 杀死线程也不是立刻就能完成必须要到达取消点。 取消点是线程检查是否被取消并按请求进行动作的一个位置。通常是一些系统调用creat open pauseclose read, write.... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。可粗略认为一个系统调用(进入内核)即为一个取消点。
3.5.1 pthread_cancel函数
void *thread_func()
{
printf(new thread:%ld\n, pthread_self()); while(1)//让其死循环 { printf(1 am thread...\n); sleep(1); } /*线程结束*/ /*方式1*/ static int retStatus -45;//static修饰的变量也会等程序结束才会清空该地址
/*方式二*/ //int *retStatus (int *)malloc(sizeof(int)* 1); //retStatus -45;
pthread_exit(retStatus);
}
int main()
{ pthread_t tid ;
int ret pthread_create(tid, NULL, thread_func, NULL); if(ret ! 0) { perror(pthread_create error\n); exit(-1); } sleep(5);
/*杀死线程*/ printf(tid:%ld\n, tid); pthread_cancel(tid);//杀死死循环子进程
printf(main thread:%ld\n, pthread_self()); /*阻塞等待回收子线程的资源*/ int *retStatus NULL; pthread_join(tid, (void *)retStatus); printf(retStatus: %d\n, *retStatus);
return 0;
}
3.6 线程分离 一般情况下 线程终止后其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态 这样的线程一旦终止就立刻回收它占用的所有资源而不保留终止状态。 不能对一个已经处于detach状态的线程调用pthread_join 这样的调用将返回EINVAL错误。也就是说如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
3.6.1 ptherad_detach函数
void *thread_func(void *arg)
{pthread_detach(pthread_self());printf(new thread:%ld\n, pthread_self());while(1){printf(1 am thread...\n);sleep(1);}static int retStatus -45;//static修饰的变量也会等程序结束才会清空该地址pthread_exit(retStatus);}int main()
{pthread_t tid ;int ret pthread_create(tid, NULL, thread_func, NULL);if(ret ! 0){perror(pthread_create error\n);exit(-1);}/*这边延时操作就是为了让子线程分离*/sleep(1);/*阻塞等待回收子线程的资源*/int *retStatus NULL;ret pthread_join(tid, (void *)retStatus);printf(ret: %d\n, ret);if(ret EINVAL)//接收EINVAL错误{printf(detached...\n);}int cnt 5;while(cnt--){printf(main thread\n);sleep(1);}if(retStatus){/*解引用*/printf(retStatus:%d\n, *retStatus);}return 0;
}
3.6.2 结论 创建一个线程后就需要将其分离。
4进程与线程的区别
8、线程同步 线程同步是确保使用共同资源的线程即定义为线程相关组的线程以串行方式执行各自的代码这意味着在一段时间内仅允许单个线程执行从而消除多个线程对共享资源的并发访问冲突。简而言之线程同步就是协同步调按预定的先后次序进行运行。当有一个线程在对内存进行操作时其他线程都不可以对这个内存地址进行操作直到该线程完成操作。 线程资源是共享的。 线程不共享的只有各自栈空间其他全部共享。 栈空间是在各自的子线程的函数中定义的参数这是不共享的。 1资源竞争 资源共享一定会引发竞争 2互斥锁Mutex 因为共享会存在竞争但是的需要一定的秩序才能保证资源的分配与运行效率最大化 在多任务操作系统中同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于在公司部门里我在使用着打印机打印东西的同时(还没有打印完)别人刚好也在此刻使用打印机打印东西如果不做任何处理的话打印出来的东西肯定是错乱的。 而在线程里也有这么一把锁互斥锁(mutex) 也叫互斥量互斥锁是一种简单的加锁的方法来控制对共享资源的访问互斥锁只有两种状态即加锁( lock )和解锁( unlock )。 互斥锁的操作流程如下 1、 在访问共享资源后临界区域前对互斥锁进行加锁。 2 、在访问完成后释放互斥锁导上的锁。 3 、对互斥锁进行加锁后任何其他试图再次对互斥锁加锁的线程将会被阻塞直到锁被释放。 互斥锁的数据类型是: pthread_mutex_t 死锁多个线程竞争资源造成了僵局
2.1 pthread_mutex_init函数 静态初始化给锁结构体里面的参数赋值为0 先上锁打印完在解锁 2.2 pthread_mutex_lock函数 当一个函数被一个进程调用后肯定得阻止其他进程调用则开始对这个函数内部上锁阻止其他进程进来直至解锁。 2.3 pthread_mutex_unlock函数
2.4 pthread_mutex_destroy函数 2.5 死锁
解决方法 死锁预防 通过破坏死锁的四个必要条件之一避免死锁的发生。例如可以通过一次性分配所有需要的资源来破坏“占有并等待条件”或 者允许资源被强制释放来破坏“不可剥夺条件”。 死锁避免 通过对资源分配进行动态检查确保系统不会进入死锁状态。例如银行家算法是经典的死锁避免算法它通过模拟资源分配的结果来决定是否允许资源分配从而避免系统进入死锁状态。 死锁检测和恢复 允许死锁发生但通过监控系统检测死锁。一旦检测到死锁系统会采取措施恢复例如强制终止某些进程或回收资源来打破死锁。 死锁忽略 这是最简单但也最冒险的策略系统不采取任何措施防止或检测死锁而是依赖用户或系统管理员来处理死锁。这种策略在某些系统中采用如 UNIX 和 Windows认为死锁发生概率较低不值得为此付出额外的代价。
/*** brief 尝试锁定互斥锁** 尝试在给定的时间范围内锁定指定的互斥锁。如果互斥锁已经被其他线程锁定则该函数将等待指定的秒数* 并尝试重新锁定互斥锁直到达到指定的尝试次数或成功锁定互斥锁。** param m 指向互斥锁的指针* param seconds 等待的秒数* param count 尝试锁定的次数** return 如果成功锁定互斥锁则返回true否则返回false*/
bool MutexTryLock(Mutex * m, int seconds, int count)
{while(count ! 0){//上锁成功if(pthread_mutex_trylock(m-mutex) 0){return true;}else{//等待seconds秒sleep(seconds);count--;}}return false;
}2.6 活锁
活锁Livelock是多线程编程中一种特殊的并发问题它与死锁Deadlock类似但有着不同的表现形式。活锁指的是多个线程或进程在响应对方的动作时不断地改变自己的状态从而使得这些线程或进程都无法继续向前执行。虽然系统在运行但实际上没有线程能够继续完成任务。
活锁的特点 状态不断变化 在活锁的情况下线程不会停滞不前如死锁中的等待而是不断地进行一些动作但这些动作并不能使它们接近完成任务的目标。也就是说线程虽然是“活着”的但却在原地打转。 没有线程被阻塞 与死锁不同活锁中的线程都不是被阻塞的状态它们能够继续运行但由于互相干扰或不断地改变状态导致没有线程能够取得进展。 可能自我解除 在某些情况下活锁可能会自动解除。如果系统中的线程能够随机地选择不同的行动路径活锁可能在一定时间内自行解决。
活锁的示例
一个典型的活锁例子是两个线程试图进入对方正在试图离开的区域。例如 线程A和线程B都试图避免碰撞于是A后退一步B也后退一步。接着A再前进一步B也前进一步。 由于两者都在试图避免冲突结果它们会不断地移动而无法进入预期的区域从而陷入了活锁。
处理活锁的方法 随机化 引入随机性让线程在重试之前等待一个随机时间段以避免所有线程都在同一时间采取相同的行动。 优先级机制 为线程分配不同的优先级让优先级高的线程先行以避免相互等待的循环。 限制重试次数 限制线程重试某一操作的次数超过次数后强制线程采取其他策略或进入等待状态。
活锁是一种类似于死锁的问题但线程并未被阻塞而是陷入了无效的重复动作中。理解和预防活锁对于构建高效且健壮的并发系统非常重要。通过引入随机性、优先级或限制重试次数等策略可以有效地避免或解决活锁问题。
2.7 代码段 fflush(stdout); 的常见用途包括 确保在程序崩溃或异常终止之前所有待输出的内容都被写入到目标设备。 在某些交互式应用中需要立即看到输出结果而不是等待缓冲区满或程序结束。 当输出被重定向到文件或其他非交互式设备时确保数据及时写入。 fflush(stdout); 确保 字符, 立即输出到屏幕
#include stdio.h
#include pthread.h
#include stdlib.h
#include unistd.h/* 面包 */
int g_data 10;/* 锁 */
pthread_mutex_t g_mutex;
pthread_cont_t g_cond;#if 0
void * thread_func1(void *arg)
{while (g_data 0){/* 客气一下 */usleep(10);g_data--;printf(thread1... gdata:%d\n, g_data);}
}void * thread_func2(void *arg)
{while (g_data 0){/* 假装客气一下 */usleep(5);g_data--;printf(thread2... gdata:%d\n, g_data);}
}void * thread_func3(void *arg)
{while (g_data 0){/* 假装客气一下 */usleep(7);g_data--;printf(thread3... gdata:%d\n, g_data);}
}
#endif// 打印机公共资源
void printer(char *str)
{pthread_mutex_lock(g_mutex);while (*str ! \0){putchar(*str);//使用 putchar 函数时你可以直接传递一个字符常量或者一个字符变量的值fflush(stdout);str;sleep(1);}pthread_mutex_unlock(g_mutex);printf(\n);
}// 线程一
void *thread_func1(void *arg)
{char *str hello;printer(str); //打印
}// 线程二
void *thread_func2(void *arg)
{char *str world;printer(str); //打印
}int main()
{/* 初始化锁 */pthread_mutex_init(g_mutex, NULL);pthread_t tid1, tid2;int ret pthread_create(tid1, NULL, thread_func1, NULL);if (ret -1){printf(pthread_create error\n);exit(-1);} ret pthread_create(tid2, NULL, thread_func2, NULL);if (ret -1){printf(pthread_create error\n);exit(-1);} while(1){sleep(1);}pthread_mutex_destroy(g_mutex);return 0;
} 9、条件变量
1条件变量概述 当同时有多个进程抢夺一个资源时其中多个进程的执行顺序是根据内部的调度算法去执行谁的执行速度快谁就去先占领资源但是同时慢的线程根本占领不了一直被执行速度快的线程给占用因此引入条件变量。 与互斥锁不同条件变量是用来等待而不是用来上锁的条件变量本身不是锁条件变量用来自动阻塞一个线程直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。 条件变量的两个动作: 条件不满阻塞线程 当条件满足通知阻塞的线程开始工作 条件变量的类型: pthread_cond_t。
2 pthread_cond_init函数
3 pthread_cond_wait函数 该函数不消耗任何的CPU时间 pthread_cond_wait 是一个 POSIX 线程pthreads库中的函数用于线程同步。这个函数允许一个线程等待某个条件变量的满足。它通常与互斥锁mutex一起使用以确保对共享数据的正确访问。 需要注意的是调用 pthread_cond_wait 的线程必须已经锁定了 mutex 指定的互斥锁。在调用 pthread_cond_wait 后线程会释放这个互斥锁并进入等待状态。当线程被唤醒时它会重新获取这个互斥锁。 另外还需要注意的是条件变量和互斥锁必须在使用前进行初始化且在不再需要时进行销毁。 该函数启动时会阻塞住等收到参数中的信号在解除阻塞和解锁随后再获取互斥锁 4 pthread_cond_destroy函数 5 pthread_cond_signal函数 6pthread_cond_broadcast函数 pthread_cond_broadcast 函数是 POSIX 线程库中的一个用于条件变量的函数。它用于唤醒所有等待在特定条件变量上的线程。这在需要多个线程响应某个事件时非常有用。 函数原型 #include pthread.h
int pthread_cond_broadcast(pthread_cond_t *cond); 参数 cond: 指向要广播的条件变量的指针。 返回值 成功时返回 0。 失败时返回错误码。
7消费者和生产者消费条件变量模型 线程同步典型的案例即为生产者消费者模型而借助条件变量来实现这一模型是比较常见的一种方法。 假定有两个线程一个模拟生产者行为一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚)生产向其中添加产品消费者从中消费掉产品。
/* 面包 */
int g_data 10;/* 锁 资源 */
pthread_mutex_t g_mutex;
/* 条件变量 资源 */
pthread_cond_t g_freePlate;
pthread_cond_t g_enoughPie;/* 链表 */
typedef struct Node
{int data;struct Node * next;
}Node;
/* 链表头结点 */
Node * head NULL;
int g_pieCnt 0;
/* 常量 */
const int maxPlates 8;/* 生产者 : 做饼 */
void * thread_produce(void *arg)
{while (1){pthread_mutex_lock(g_mutex);/* 当前饼的数量 已经等于盘子的数量 */while (g_pieCnt maxPlates){pthread_cond_wait(g_freePlate, g_mutex);}/* 程序到这个地方 : 有空闲的盘子了 *//* 创建一个链表结点 (饼) */Node * newPie malloc(sizeof(Node) * 1);newPie-data rand() % 999 1; // 1 ~ 999/* 头插 */newPie-next head;head newPie;printf(producer make %d pie\n, newPie-data);/* 当前已经有的饼加一. */g_pieCnt;pthread_mutex_unlock(g_mutex);pthread_cond_signal(g_enoughPie);/* 休息1S */sleep(1);}
}/* 消费者 : 吃饼 */
void * thread_consume(void *arg)
{while (1){pthread_mutex_lock(g_mutex);while (g_pieCnt 0){pthread_cond_wait(g_enoughPie, g_mutex);}/* 程序到这个地方说明: 有饼吃 */Node * pie head;head head-next;printf(tid:%ld,\tconsumer eat %d pie\n, pthread_self(), pie-data);/* 释放内存 */if (pie ! NULL){free(pie);pie NULL;}/* 饼减一. */g_pieCnt--;pthread_mutex_unlock(g_mutex);pthread_cond_signal(g_freePlate);}
}int main()
{/* 初始化锁 */pthread_mutex_init(g_mutex, NULL);/* 初始化条件变量 */pthread_cond_init(g_freePlate, NULL);pthread_cond_init(g_enoughPie, NULL);pthread_t tid1, tid2, tid3;int ret pthread_create(tid1, NULL, thread_produce, NULL);if (ret -1){printf(pthread_create error\n);exit(-1);} ret pthread_create(tid2, NULL, thread_consume, NULL);if (ret -1){printf(pthread_create error\n);exit(-1);} ret pthread_create(tid3, NULL, thread_consume, NULL);if (ret -1){printf(pthread_create error\n);exit(-1);} while(1){sleep(1);}return 0;
} 10、进/线程高并发
在高并发环境下线程和进程的使用都涉及到如何高效地管理资源、保证性能并避免潜在的问题。让我们先区分一下线程和进程的概念然后讨论它们在高并发中的应用和可能出现的问题。
1. 线程和进程的区别 进程 进程是操作系统中资源分配的基本单位。每个进程都有自己独立的内存空间、文件描述符等资源。 进程之间的通信较为复杂需要使用进程间通信IPC机制如管道、信号、共享内存等。 进程切换上下文切换开销较大因为需要保存和恢复所有进程的状态。 线程 线程是进程中的一个执行单元线程共享进程的内存空间和资源但有自己独立的栈、寄存器等。 线程之间通信相对简单因为它们共享内存可以直接读写共享数据。 线程切换开销比进程小但仍然存在上下文切换的开销。
2. 线程和进程的高并发
高并发下的线程使用 多线程并发多个线程在同一个进程中并行执行任务适用于I/O密集型和需要频繁数据共享的任务。 优点线程切换开销小资源共享方便。 常见问题 线程安全多个线程同时读写共享数据时容易出现竞态条件Race Condition导致数据不一致或崩溃。需要使用锁如互斥锁、读写锁来同步线程但锁的使用不当会引起死锁、饥饿、活锁等问题。 线程上下文切换线程切换频繁时会增加系统开销降低性能。 线程的上下文切换指的是操作系统将 CPU 从一个线程切换到另一个线程的过程。这是多任务操作系统用来实现并发执行的一个关键机制。在上下文切换期间操作系统会保存当前正在执行的线程的状态称为“上下文”然后加载并恢复另一个线程的状态使其能够继续执行。
高并发下的进程使用 多进程并发多个进程并行执行任务各进程独立运行适用于CPU密集型任务或需要严格隔离的任务。 优点进程隔离性好一个进程的崩溃不会影响其他进程。 常见问题 进程间通信复杂由于进程间不共享内存数据交换需要通过IPC机制增加了开发复杂度和延迟。 进程上下文切换开销大频繁的进程切换会导致性能下降尤其是在高并发环境下。
3. 高并发中常见问题 资源竞争线程或进程可能会争夺系统资源如CPU、内存、文件句柄导致性能下降甚至系统崩溃。 死锁多个线程或进程因资源竞争进入一种相互等待的状态导致程序无法继续执行。 上下文切换开销频繁的上下文切换会增加系统负担降低整体性能。 内存泄漏不当的资源管理会导致内存泄漏长时间运行后可能耗尽系统内存。 过度线程或进程创建如果高并发环境下创建过多的线程或进程系统可能会耗尽资源反而导致性能下降。
4. 解决策略 使用线程池或进程池限制线程或进程的数量避免系统资源被耗尽。 优化锁机制使用细粒度锁或无锁数据结构减少锁的争用和死锁的风险。 异步与事件驱动模型对于I/O密集型任务使用异步或事件驱动的架构减少线程/进程的上下文切换。 计算机网络
1、网络基础及概念 1IPInternet Protocol
1.1 概念 3.2 数据链路层 - 封装成帧 3.3 数据链路层 - 透明传输 3.4 数据链路层 - 差错检验 3.5 Ethernet V2 帧的格式 3.6 交换机 3.7 网卡 4三层网络层 4.1 网络层首部 - 版本、首部长度、区分服务 4.2 网络层首部 - 总长度 4.3 网络层首部 - 标识、标志 4.4 ping 4.5 网络层首部 - 片偏移 4.6 网络层首部 - 生存时间 4.7 网络层首部 - 协议、首部校验和 一层-物理层 双绞线光纤等 二层-数据链路层 交换机 网卡MAC地址 三层-网络层 IP地址 四层-传输层 TCP/UDP 五层-应用层 OSI 七层网络模型和 Linux 四层网络模型是用于描述和管理网络协议的不同层次的模型但它们关注的层次和细节有所不同 OSI 七层模型 目的是提供一个详细的网络通信分层架构帮助理解和设计网络协议。 层次 物理层传输原始比特流。 数据链路层提供节点到节点的可靠传输。 网络层路由和转发数据包。 传输层提供端到端的通信服务。 会话层管理会话控制。 表示层数据格式转换。 应用层网络应用和服务接口。 Linux 四层模型 目的是描述操作系统内核处理网络通信的实际过程更贴近实际实现。 层次 网络接口层Network Interface Layer处理数据链路层和物理层的功能。 网络层Network Layer处理 IP 协议等网络层功能。 传输层Transport Layer处理 TCP 和 UDP 协议。 应用层Application Layer处理高层协议和应用的接口。 IP地址分为两部分 主机位 网络位 其中IP 位与 子网掩码 主机位 主要区别 OSI 模型 是理论上的模型用于教育和理解网络协议的层次化设计。 Linux 四层模型 更关注于操作系统内核中实际的网络协议栈实现更简化直接对应实际的协议处理流程。 本质是一个整形数用于表示计算机在网络中的地址。IP协议版本有两个IPv4和IPv6. IPv4Internet Protocol version4 使用一个32位的整形数描述一个IP地址。 也可以使用一个点分十进制字符串描述这个IP地址 192.168.247.135. 分成了4份, 每份1字节。最小值为0最大值为 255。 那么0.0.0.0是最小的IP地址255.255.255.255是最大的IP地址。 按照IPv4协议计算可以使用的IP地址共有 $2^{32}$个。 IPv6Internet Protocol version6 使用一个128位的整形数描述一个IP地址16个字节 也可以使用一个字符串描述这个IP地址2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b分成了8份每份2字节。每一部分以16进制的方式表示 按照IPv6协议计算可以使用的IP地址共有 $2^{128}$ 个 查看IP地址命令: linux系统 ifconfig windows系统 ipconfig 1.2 ip分类 IP地址根据其范围和用途分为五类A类、B类、C类、D类和E类。每一类具有不同的网络规模和用途。以下是对每一类的详细介绍 A类 IP 地址 范围1.0.0.0 到 126.0.0.0 子网掩码255.0.0.0 网络号位数8位 主机号位数24位 最大网络数量128个其中0和127为保留地址 每个网络的最大主机数量16,777,214个 特点 A类地址用于非常大的网络例如跨国公司的内部网络。 网络号的第一个字节用于定义网络剩下的三个字节用于定义主机。 B类 IP 地址 范围128.0.0.0 到 191.255.0.0 子网掩码255.255.0.0 网络号位数16位 主机号位数16位 最大网络数量16,384个 每个网络的最大主机数量65,534个 特点 B类地址用于中等规模的网络例如大学和大公司的网络。 网络号由前两个字节定义后两个字节用于主机。 C类 IP 地址 范围192.0.0.0 到 223.255.255.0 子网掩码255.255.255.0 网络号位数24位 主机号位数8位 最大网络数量2,097,152个 每个网络的最大主机数量254个 特点 C类地址用于小型网络例如小型企业的内部网络。 网络号由前三个字节定义最后一个字节用于主机。 D类 IP 地址 范围224.0.0.0 到 239.255.255.255 用途多播 特点 D类地址用于多播数据同时发送到多个计算机。 没有子网掩码。 E类 IP 地址 范围240.0.0.0 到 255.255.255.255 用途研究和实验 特点 E类地址保留用于实验和未来用途。 不用于常规网络通信。 特殊IP地址 127.0.0.0到127.255.255.255用于回环测试本地环回地址例如127.0.0.1用于测试网络接口。 0.0.0.0用于指示未知地址或默认路由。 255.255.255.255用于广播到本地网络。 总结 IP地址分类帮助网络管理员根据规模和需求选择合适的地址块并在网络设计中有效地管理IP地址空间。使用CIDR无类别域间路由可以进一步灵活地分配IP地址超越传统分类限制。 1.3 网络掩码 网络掩码Subnet Mask是一个32位的地址用于区分IP地址的网络部分和主机部分。它帮助确定一个IP地址属于哪个子网并在IP网络中用于划分子网、控制流量和管理地址空间。 网络掩码的基本概念 IP地址 IP地址是一个32位的二进制数通常表示为四个十进制数每个数用点分隔例如192.168.1.1。 IP地址分为两个部分网络部分和主机部分。 子网掩码 子网掩码也是一个32位的二进制数与IP地址结合使用。 它的网络部分用连续的1表示主机部分用连续的0表示例如255.255.255.0。 CIDR表示法 子网掩码可以用CIDR无类别域间路由表示法表示即在IP地址后面加斜杠和一个数字该数字表示网络部分的位数例如192.168.1.1/24。 网络掩码的作用 确定子网 子网掩码确定网络的范围和划分子网的大小。 根据子网掩码IP地址的网络部分用于识别子网主机部分用于识别子网中的设备。 流量路由 路由器使用子网掩码来确定数据包的目的地是本地子网还是需要转发到其他网络。 路由器比较目的IP地址和子网掩码以决定数据包的转发路径。 IP地址分配 网络管理员使用子网掩码来优化IP地址的分配减少浪费并提高网络效率。 子网掩码示例 标准子网掩码 /24255.255.255.0 网络部分24位主机部分8位 常用于小型局域网最多支持254个主机 /16255.255.0.0 网络部分16位主机部分16位 常用于中型网络最多支持65,534个主机 /8255.0.0.0 网络部分8位主机部分24位 常用于大型网络最多支持16,777,214个主机 计算示例 IP地址192.168.1.10 子网掩码255.255.255.0 (/24) 网络地址计算 IP地址和子网掩码按位与运算 结果为网络地址192.168.1.0 广播地址计算 网络地址主机部分全为1 结果为广播地址192.168.1.255 子网划分与优化 子网划分 根据网络规模和需求将网络划分为多个子网。 选择合适的子网掩码以确保地址空间利用效率最大化。 子网优化 使用变长子网掩码VLSM以灵活地分配不同大小的子网。 减少地址浪费提高地址使用效率。 总结 网络掩码在IP网络中扮演关键角色它定义了网络结构、管理IP地址分配并协助路由器进行数据包转发。理解子网掩码的功能和应用有助于网络设计和维护。 1.4 ifconfig显示的信息 ifconfig 是一个在 Unix 和类 Unix 系统如 Linux中用于配置网络接口的命令。当你执行 ifconfig 时它会显示关于系统上所有活动网络接口的信息。这些信息通常包括 接口名称例如 eth0、wlan0、lo 等分别代表以太网接口、无线接口和本地回环接口。 接口状态接口是否是 UP 或 DOWN。如果接口是 UP则表示它已启用并可以发送和接收数据。 IP 地址分配给接口的 IPv4 地址。 广播地址用于在本地网络上进行广播的 IPv4 地址。 子网掩码用于确定哪些 IP 地址位于同一本地网络上的掩码。 硬件地址或 MAC 地址网络接口的物理地址用于在网络上进行唯一标识。 MTU最大传输单元接口可以处理的最大数据包大小。 RX packets接口接收的数据包数量。 TX packets接口发送的数据包数量。 RX errors 和 TX errors接口在接收和发送数据时遇到的错误数量。 RX dropped 和 TX dropped接口由于各种原因如缓冲区溢出而丢弃的数据包数量。 RX overruns 和 TX overruns由于接收或发送队列溢出而丢弃的数据包数量。 碰撞在以太网接口上发生的碰撞次数如果有的话。 中断网络接口使用的硬件中断号如果适用。 2MAC地址 MAC地址每张网卡设备都会一个或者多个网口。每个网口都会有单独且唯一的。 只有后3bytes是自己分配的。 3二层数据链路层 3.1 概念 最大传输单元mtu ARP协议用于获取对方的MAC地址。 request请求报文采用的是广播(目的MAC地址FF:FF:FF:FF:FF:FF). response回复报文采用的是单播回复。 ARPAddress Resolution Protocol地址解析协议是用来将IP地址解析为MAC地址的协议。主机或三层网络设备上会维护一 张ARP表用于存储IP地址和MAC地址的映射关系一般ARP表项包括动态ARP表项和静态ARP表项。 查看arp表的命令(command)是arp -a 删除arp表的命令(command)是arp -d 交换机里面存储的是来自各个PC端的MAC地址。 交换机的基本工作原理 学习MAC地址 当交换机接收到来自某个端口的数据帧时它会检查该帧的源MAC地址和端口号并将其记录在自己的MAC地址表或称为转发表中。 这样交换机可以知道哪些MAC地址是连接到哪些端口上的。 构建转发表 交换机会持续更新其转发表以反映网络设备的连接情况。 如果交换机接收到一个数据帧其目的MAC地址尚未在转发表中记录交换机将会广播该帧到所有端口除了接收到该帧的端口这称为“泛洪”。 数据帧转发 交换机会根据转发表来决定如何处理数据帧。 如果目的MAC地址在表中有记录交换机会直接将数据帧转发到对应的端口。 如果没有记录例如第一次通信交换机会将帧发送到所有其他端口泛洪。 过滤和隔离冲突域 交换机能够隔离冲突域即每个端口都是一个独立的冲突域从而减少网络冲突。 交换机通过只将数据帧发送到目的设备所在的端口从而减少不必要的网络流量。 全双工通信 大多数现代交换机支持全双工模式即同时进行数据的发送和接收提高了网络带宽和效率。 作用抓包 源IP地址 目标IP地址 4.8 路由器 路由是指在计算机网络中确定数据包传输路径的过程。路由通常由专门的网络设备称为路由器Router来执行。路由器负责将数据从源设备传送到目标设备跨越一个或多个网络。 路由的基本概念 路由器 路由器是连接多个网络的设备它根据目标IP地址将数据包转发到合适的网络路径。 路由器在网络层OSI模型的第三层工作能够管理和转发IP数据包。 路由表 路由器维护一个路由表其中包含网络路径的信息用于决定数据包应该转发到哪个下一跳Next Hop设备。 路由表条目通常包括目的网络、下一跳地址、网络掩码和接口信息。 路由协议 路由协议用于在路由器之间交换网络可达性信息并动态更新路由表。 常见的路由协议包括RIPRouting Information Protocol、OSPFOpen Shortest Path First、BGPBorder Gateway Protocol等。 路由的基本过程 数据包接收 路由器从一个接口接收到数据包并检查其目标IP地址。 查找路由表 路由器查找路由表以确定数据包的最佳传输路径。 路由器选择具有最长匹配前缀的路由条目。 转发数据包 路由器根据路由表的信息将数据包转发到下一跳设备。 如果数据包到达目标网络路由器将其发送到最终目的设备。 动态更新 路由器使用路由协议与其他路由器交换信息更新路由表以反映网络拓扑的变化。 路由的类型 静态路由 管理员手动配置路由表条目适用于简单和小型网络。 优点是配置简单、无协议开销但缺点是不具备自动适应网络变化的能力。 动态路由 使用路由协议自动学习和更新路由信息。 适用于大型和复杂网络能够自动调整路由以适应网络拓扑变化。 路由协议的分类 距离矢量路由协议 基于路由跳数或距离来选择路径如RIP。 优点是简单缺点是在大型网络中可能导致路由不稳定。 链路状态路由协议 每个路由器都有完整的网络拓扑信息如OSPF。 优点是快速收敛适合大型网络但需要更多的计算和内存资源。 路径矢量路由协议 使用路径属性来决定最佳路径如BGP。 适用于互联网级别的路由能够处理复杂的网络策略和路由选择。 路由的应用场景 企业网络通过路由器连接不同的分支网络实现数据通信。 互联网ISP使用路由器和BGP协议连接全球网络。 数据中心内部和外部网络之间的数据交换。 路由器的转发过程 路由器首先在路由表中查找判明是否知道如何将分组信息发送到下一个站点路由器或主机如果路由器不知道通常将该分组丢弃否则就根据路由表的相应表项将分组发送到下一个站点。 路由表就是图中的iptables 路由器必须具备的基本条件包括 两个或两个以上的接口 协议至少实现到网络层 至少支持两种以上的子网协议 具有存储、转发、寻径功能 一组路由协议。 4.9 交换机和路由的区别 关键区别 功能 交换机主要用于设备之间的通信负责在同一网络或子网内传递数据。 路由器负责不同网络之间的数据传输和路由选择。 工作方式 交换机使用MAC地址进行数据转发。 路由器使用IP地址和路由表进行数据转发。 应用场景 交换机适用于局域网内部设备的快速连接。 路由器适用于连接不同的网络例如将家庭网络连接到互联网。 配置和管理 交换机通常不需要复杂的配置。 路由器需要配置IP地址、路由协议等参数并支持更复杂的网络管理功能。
4.10 广播地址
在IP网络中广播地址用于将数据包发送到同一网络中的所有主机。广播是一种通信方式它允许网络上的所有设备接收到发送的数据包而不需要逐个指定每个目标设备的IP地址。 广播地址的作用 网络发现和管理 广播地址常用于网络发现和管理任务。例如当一个设备加入网络时它可能会通过广播地址发送一个请求询问网络中的其他设备是否存在或可以提供特定服务如DHCP请求。 服务发现 一些网络服务如文件共享和打印服务会使用广播地址来宣传它们的存在让网络上的其他设备能够找到它们。 ARP地址解析协议 在以太网中ARP协议用于将IP地址映射到MAC地址。为了找到特定IP地址对应的MAC地址设备会发送一个ARP请求到广播地址以便网络上的所有设备都能看到这个请求并进行回应。 广播地址的计算 在IPv4中广播地址是通过将网络地址中的主机部分全部设置为1来计算的。例如 假设网络地址为 192.168.1.0/24这表示子网掩码是 255.255.255.0。 在这个网络中广播地址是 192.168.1.255。这是因为在 192.168.1.0 网络地址中将主机部分即最后一个字节全设置为1即 255。 在IPv6中广播已被弃用取而代之的是组播Multicast。IPv6使用组播地址来实现类似的功能但并不使用传统的广播地址。 广播的局限性 网络拥堵广播会增加网络流量因为所有广播数据包都会被网络中的所有设备接收到。这可能导致网络拥堵和性能下降尤其是在大型网络中。 安全隐患广播可能会被用于攻击或恶意活动如网络嗅探和伪装攻击。
5四层传输层 资源就是带宽
5.1 UDP-数据格式 5.2 UDP-检验和 5.3 端口(port) 端口的作用是定位到主机上的某一个进程通过这个端口进程就可以接受到对应的网络数据了。 比如: 在电脑上运行了微信和QQ, 小明通过客户端给我的的微信发消息, 电脑上的微信就收到了消息, 为什么? 因为运行在电脑上的微信和QQ都绑定了不同的端口。通过IP地址可以定位到某一台主机通过端口就可以定位到主机上的某一个进程通过指定的IP和端口发送数据的时候对端就能接受到数据了。 端口也是一个整形数 unsigned short 一个16位整形数有效端口的取值范围是0 ~ 65535(0 ~ 2^16-1) 提问计算机中所有的进程都需要关联一个端口吗一个端口可以被重复使用吗? 不需要如果这个进程不需要网络通信那么这个进程就不需要绑定端口的 一个端口只能给某一个进程使用多个进程不能同时使用同一个端口 IP是区分主机端口是区分软件和进程等。 netstat -h可以解决任何的网络问题 5.4 TCP-数据格式 5.5 TCP-检验和 5.6 TCP-标志位 5.7 TCP-序号、确认号、窗口 5.8 TCP-要点
5.8.1 可靠传输 三次握手 用超时重传的机制保证可靠传输