C# C++ 笔记

第一阶段知识总结

lunix系统操作

1、基础命令

(1)cd

cd /[目录名] 打开指定文件目录

  • cd .. 返回上一级目录

  • cd - 返回并显示上一次目录

  • cd ~ 切换到当前用户的家目录

(2)pwd

pwd 查看当前所在目录路径

  • pwd -L 打印当前物理路径

(3)ls

ls 查看当前目录下的文件和目录

:部分系统文件和目录会用不同颜色显示

  • ls [目录名] : 显示目录下的文件(需要有查看目标目录的权限)

  • ls -a: 显示全部文件(包括文件名以“.”开头的隐藏文件)

  • ls -alh

  • ls -alh 查看时显示详细信息

    • d 指该内容为目录

    • x 指该内容为可执行

    • r 指可读

    • w 指可写

    • . 指当前目录

    • .. 指上一级目录

(4)touch

  • touch [文件名] 新建文件

(5)rm

  • rm [文件名] : 删除文件

    • r 强制删除

    • f 允许删除目录

(6)mv

  • mv [文件原名] [新的文件名] 重命名文件|目录

(7)mkdir

  • mkdir [目录名] 新建目录

    • mkdir -p [目录名]/[目录名]/…… 将不存在的目录全部创建

    • mkdir -v [目录名] 创建时打印目录信息

    • mkdir -m [目录名] 设置目录权限

 

(8)rmdir

  • rmdir [目录名] 删除目录(只能删除空目录)

    • rmdir /s [目录名] 删除非空目录

(9)cp

  • cp [被复制的文件名] [新的文件名] 复制文件|目录

    • cp -i [被复制的文件名] [新的文件名] 若新文件重名系统会询问是否覆盖,默认覆盖

    • cp -n [被复制的文件名] [新的文件名] 若新文件重名系统不会覆盖

    • cp -u [被复制的文件名] [新的文件名] 若新文件重名,只有被复制的文件的时间属性比重名文件新的时候才覆盖

    • cp -p [被复制的文件名] [新的文件名] 连同文件属性一起复制,默认只复制内容

(10)vi

  • vi [文件名] 编写文件

  • 按“i”键后可以开始修改内容,按之前只有delete键有效,按之后delete失效,backspace有效

编写时按“Esc”键退出编写,之后按“shift”+“:”输入命令  

  • w filename 保存

  • wq 保存并退出

  • q! 不保存强制退出

  • x 执行、保存并退出

(11)cat

  • cat [文件名] 读取文件

(12)echo

  • echo “[输入内容]” > [文件名] 清空文件并输入内容

(13)chmod

  • chmod 是 Unix 和 Linux 系统中的命令,用于更改文件或目录的权限。权限定义了哪些用户可以对文件或目录执行哪些操作。

  • chmod 命令的基本语法如下:

chmod [选项] 模式 文件名
  • 使用符号模式,你可以通过添加(+)、删除(-)或设置(=)权限来修改文件或目录的权限。权限符号可以是 r(读)、w(写)或 x(执行)。

(14)man

  • 在使用 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语言环境下的相关命令

(1)gcc

gcc [文件名] 编译C语言文件生成a.out执行文件 gcc -g [文件名] 编译C语言文件生成可调试的a.out执行文件

(2)./

./[文件名] 运行文件

:不需要空格

3、DOS通用注意点

(1)--help

  • “命令 --help”为该命令的帮助文档

(2)sudo

  • 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文件。

(2)makefile核心目的

  • makefile文件指导系统如何编辑,节省大项目局部修改后的编译时间。局部修改后之后修改处需要重新编译。

  • 如果一个文件中有makefile和Makefile文件,在命令行输入make,优先执行makefile。如果执行大写的Makefile,需要加上-f:make -f Makefile。

(3)makefile运行逻辑

(4)makefile基本语法

#[目标]:[依赖1] [依赖2] …
#	[命令1] [命令2] …
#例如
main:main.o
	gcc main.o -o main
  • 目标: 一般是指要编译的目标,也可以是一个动作

  • 依赖: 指执行当前目标所要依赖的选项。包括其他目标,某个具体文件或库等,一个目标可以有多个依赖。

  • 命令:该目标下要执行的具体命令,可以没有,也可以有多条。

(5)makefile编译流程

main:main.o myAdd.o myMinus.o myMulti.o myDiv.o
	gcc main.o myAdd.o myMinus.o myMulti.o myDiv.o -o main

# -c 生成二进制文件 -o 指定输出的文件名
myAdd.o:myAdd.c
	gcc -c myAdd.c -o myAdd.o

myMinus.o:myMinus.c
	gcc -c myMinus.c -o myMinus.o

myMulti.o:myMulti.c
	gcc -c myMulti.c -o myMulti.o

myDiv.o:myDiv.c
	gcc -c myDiv.c -o myDiv.o

main.o:main.c
	gcc -c main.c -o main.o

clean:
	@rm -rf *.o main

@表示执行但不输出这条命令

在终端输入 make 从上至下后执行文件命令 输入 make [目标] 仅执行对应命令


(6)makefile变量及文件精简过程

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 自定义常量

# 变量自定义赋值
OBJS=main.o myAdd.o myDiv.o myMinus.o myMulti.o
TARGET=main
​
# 变量取值用$()
$(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.o
    	gcc main.o myAdd.o myDiv.o myMinus.o myMulti.o -o $@
    
    myAdd.o:myAdd.c
    	gcc -c myAdd.c -o $@
    
    myMinus.o:myMinus.c
    	gcc -c myMinus.c -o $@
    
    myMulti.o:myMulti.c
    	gcc -c myMulti.c -o $@
    
    myDiv.o:myDiv.c
    	gcc -c myDiv.c -o $@
    
    main.o:main.c
    	gcc -c main.c -o $@
    
    clean:
    	@rm -rf *.o main
    6.2 $^
  • 表示所有不重复的依赖文件
  • # 针对上一张的图片,用$^替换依赖文件
    main:main.o myAdd.o myDiv.o myMinus.o myMulti.o
    	gcc $^ -o $@
    
    myAdd.o:myAdd.c
    	gcc -c $^ -o $@
    
    myMinus.o:myMinus.c
    	gcc -c $^ -o $@
    
    myMulti.o:myMulti.c
    	gcc -c $^ -o $@
    
    myDiv.o:myDiv.c
    	gcc -c $^ -o $@
    
    main.o:main.c
    	gcc -c $^ -o $@
    
    clean:
    	@rm -rf *.o main
    6.3 系统常量
  • RM:删除

  • CC:C语言编译程序

  • [常量名]=[值]赋予自定义常量值

  • $() 取自定义变量的值

  • ​编辑

6.5 makefile伪目标
  • .PHONY: [目标] 当只想执行目标命令而不希望生成目标文件时使用

  • :后有个空格

OBJS=main.o myAdd.o myDiv.o myMinus.o myMulti.o
TARGET=main

$(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: clean

clean:
	@$(RM) *.o main
6.6 模式匹配
  • %[目标]:%[依赖] 匹配目录下所有符合命令的文件,批量执行命令

 

OBJS=$(patssubst %.c, %.o, $(wildcard ./*.c))
TARGET=main

$(TARGET):$(OBJS)
    $(CC) $^ -o $@

# 模式匹配 %[目标]:%[依赖]
%.o:%.c
    $(CC) -c $^ -o $@
    
.PHONY: clean

clean:
    @$(RM) *.o main

6.7 总代码
OBJS=$(patsubst %.c, %.o, $(wildcard ./*.c))
# 变量定义赋值
TARGET=main
​
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) clean
    make -C $(A_DIR) clean
    
# wildcard : 匹配文件           (获取指定目录下所有的.c文件)
# patsubst : 模式匹配与替换    (指定目录下所有的.c文件替换成.o文件)
show:
    @echo $(wildcard ./*.c)
    @echo $(patsubst %.c, %.o, $(wildcard ./*.c))


(7)makefile动态库与静态库

   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文件

:上图myadda没有大写导致报错

  • 3、复制到没有 .c 源文件的文件夹下

  • 4、生成main.o文件

    • gcc *.c -lMyAdd -L./src_so -o main

  • 5、运行main.o文件

动态库内部makefile

  • 运行前先执行内部makefile生成动态库

  • 然后再外部调用动态库

OBJS=$(patsubst %.c, %.o, $(wildcard ./*.c))
TARGET=libMyAdd.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.cfile2.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))
TARGET=libMyDiv.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))
# 变量定义赋值
TARGET=main
​
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 区别
  • 链接方式

    • 静态库在编译时被链接到程序中,而动态库在运行时被加载到内存中。

    • 使用静态库的程序在编译时会将库的内容直接合并到最终的可执行文件中,而使用动态库的程序则会在运行时根据需要从库中加载所需的代码。

  • 更新和维护

    • 静态库,如果需要更新库中的代码,必须重新编译并重新链接所有依赖于该库的程序

    • 动态库可以独立更新,而不需要重新编译程序,这使得库的维护更加方便。动态库更适合多文件场合。

(8)Makefile的常用选项

  • -f file指定Makefile文件。默认情况下,make会在当前目录中查找名为GNUmakefile、makefile或Makefile的文件作为输入。使用-f选项,你可以指定其他名称的文件作为Makefile。

  • -v显示make工具的版本号

  • -n只输出命令,但不执行。这个选项通常用于测试Makefile,查看make会执行哪些命令,而不真正执行它们。

  • -s只执行命令,但不显示具体命令。这跟makefile中的(@+命令行)符号作用一样。这个选项在需要执行命令但不需要看到详细输出时很有用。

  • -w:显示执行前和执行后的路径。

  • -C dir:指定Makefile所在的目录。如果Makefile不在当前目录中,可以使用这个选项来指定Makefile的目录。

(9)makefile中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命令。这通常使用ifeqifneqifdefifndef等指令来实现。例如:

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命令行参数

(12)Makefile中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))
# 变量定义赋值
TARGET=mycp
​
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) clean
    make -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 b info break 显示所有断点的信息,一般按自然数排序
del [断点编号] delete 删除断点,示例:del 56 删除编号为56的断点
dis disable 禁用断点
ena enable 启用断点
p [变量] print 查询值,包括以下形式,vary,&vary,*ptr,buffer[0]
run / 执行程序,直到结束或断点
n next 执行下一条语句,会越过函数
s step 执行下一条语句,会进入函数
c continue 继续执行程序,直到遇到下一个断点
call / 直接调用函数并查看其返回值
q quit 退出 gdb 当前调试
bt backtrace 查看函数的栈调用
f frame 到指定的栈帧,配合bt使用显示当前选中的堆栈帧的详细信息包括帧编号、地址、函数名以及源代码位置
where / 显示当前线程的调用堆栈跟踪信息
ptype / 查看变量类型
thread / 切换指定线程
set br br / 将信息按标准格式输出,这样信息就显示不乱
set args / 设置程序启动命令行参数
show args / 查看设置的命令行参数
l list 显示源代码。
watch / 监视某一变量或内存地址的值是否发生变化
u until 运行程序直到退出当前循环。 快速跳过循环的剩余迭代,以便更快地到达循环之后的代码。
fi finish 执行完当前函数的剩余部分,并停止在调用该函数的地方。 示例:finish 执行完当前函数的剩余部分。
return / 结束当前调用函数并返回指定值,到上一层函数调用处
display / 每次程序停止时自动打印变量的值。 示例:display name 每次程序停止时自动打印 name 的值。
undisplay / 取消之前用 display 命令设置的自动打印。
j jump 使程序跳转到指定的位置继续执行。
dir / 重定向源码文件的位置
source / 读取并执行(加载)一个包含GDB命令的脚本文件
set / set variable=newvalue,修改变量的值

(3)调试详解

3.1 call命令
  • call func:显示地址信息,因为是函数名,指向函数地址。

  • call func( ):无参调用,显示函数的返回值

  • call add(100,200 ):有参调用,返回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系统中,你可以使用GDB(GNU调试器)来调试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 是一个在 GDB(GNU 调试器)中使用的命令,用于查看变量的类型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:进入第三个线程的栈帧

(4)wget命令(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-rate=300k http://example.com/slowdownload.zip 使用--limit-rate选项,可以限制下载速度,这里限制为300k。

    • 下载到指定目录: wget -P /path/to/directory http://example.com/file.pdf 使用-P选项,可以指定下载文件的保存目录。

4.2 redis(了解即可)
  • Redis(Remote 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的数据定期或实时保存到磁盘上,以及通过集群分片来拆分负载,实现性能的横向扩展。

(5)set args 和 show args

  • set args: 这个命令可能是用来设置某些参数或变量的。例如,在一个脚本或程序中,你可能想要设置一些输入参数或配置选项,这时可以使用 set args 来完成。

  • show args: 这个命令可能是用来显示之前设置的参数或变量的。它可以让你查看当前已经设置的参数或变量的值。

git 命令

1、将服务器更新的内容上传至gitee仓库

  • 上传gitee的流程如下

(1)git add

  • git add [文件名|目录名] 将对应的文件|目录放到暂存区


(2) git commit

  • git commit -m"[备注]" 将暂存区的内容放到对象区,并添加这次上传的备注信息

  • git commit --amend -m"[新的备注]" 修改暂存区这次上传的备注信息


(3) git push

  • git push [远程仓库名] [本地分支名] [远程分支名]

    • 将本地的指定分支推送到指定仓库的指定分支上

  • 例如 git push origin master:refs/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 的时候不会推送分支,如果一定要推送标签的话那么可以使用这个命令。

(4)git status

  • git status 工作区、暂存区内文件|目录的状态

  • $\textcolor{red}{红色}$:表示文件|目录在工作区

  • $\textcolor{green}{绿色}$:表示文件|目录在暂存区

(5) commit版本与hard指针(待完善)

(6)git 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操作

  • 同上一节,区别在于内容回退到工作区而不是暂存区

(7)git log

  • git log 是 Git 中的一个命令,用于显示仓库的提交历史(查看日志)。它提供了详细的提交信息,包括每次提交的哈希值、提交者、提交日期、提交信息以及涉及的改动文件等。

  • 以下是 git log 的一些常见用法和选项:

git log
  • 这将会显示当前分支上的所有提交,从最近的提交开始,按时间顺序逆序排列

  • 后面-3表示显示3条历史

  • 如果git log查不出来所需要的版本号,可以使用git reflog

2、git分支管理

(1) 什么是分支

  • 多个程序员分发同一程序时,由于不能直接在一个程序上进行功能的开发,所以就有了功能分支的概念。

  • 功能分支指的是专门用来开发新功能的分支,它是临时从master主分支上分叉出来的,当新功能开发且测试完毕后,最终需要合并到master主分支上,如图所示:

(2)git branch

  • git branch 查询本地所有分支

  • git branch -r 查询远程所有分支

  • git branch -a 查询本地和远程所有分支,并显示详细内容

  • git branch [分支名称] 创建分支并命名

  • git branch -d [分支名称] 删除分支

  • git branch -m [原名称] [新名称] 重命名分支


(3)git checkout

  • git checkout [分支名称] 切换分支

  • git checkout -b [分支名称] 创建并切换到分支


(4)git 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 的例子:

  1. 设置用户名

git config --global user.name "Your Name"
  • 这条命令会设置你在提交时所使用的用户名。Git 会使用这个用户名来标识你提交的代码。

  1. 设置邮箱地址

git config --global user.email "your.email@example.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:在合并或变基后,暂停提交,允许用户手动检查和修改更改。

  • 示例

  • originmaster 分支拉取并集成更改:

    • 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 --tool=memcheck --leak-check=yes --show-reachable=yes + 所跑出的文件(./a.out)

 

4-perror与exit
  • perror( const char *s );

  • 作用:将字符串参数打印至控制台,且打印程序错误信息

  • :没有成功让程序打印错误信息,但我在网上找到了C++找不到文件时的错误信息。另外,这里没有使用 exit() 函数

#include<stdio.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位),与它所指向的类型无关。

    • 动态内存管理:指针经常与动态内存分配(如使用newmalloc)一起使用,允许我们在运行时创建对象,并将指针指向这些对象的内存地址。这种灵活性是静态数组或直接在栈上分配的对象所不具备的。

    • 多态性和接口:在面向对象的编程中,指针(或引用)是实现多态性的关键。通过将基类指针指向派生类对象,我们可以实现运行时多态性,即同一接口可以有多种实现。

    • 传递大型数据结构:通过传递指针而不是整个数据结构,我们可以避免复制大型对象,从而提高性能。函数接收的是指向数据的指针,而不是数据的副本。

    • 修改外部数据:通过指针,函数可以修改调用者传递的数据,因为指针提供了对数据实际存储位置的直接访问。

(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*4=8
printf("array + 2\t\t%p\n", array + 2);
//这里在array的基础上加了两个int array[3]的字节,即2*3*4=24
printf("*array + 2\t\t%p\n", *array + 2);
//这里在array的基础上加了两个int的字节,即2*4=8

(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值