-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 323 KB
/
index.json
1
[{"categories":["C++"],"content":"A星算法简介\rA*(A星)算法是一种基于格子(Grid)的寻路算法,用于从众多路径中,寻找一条从起点到终点代价最小的路径。基于格子也就是说会把地图看作是由 w*h 个格子组成的,因此寻得的路径也就是由一连串相邻的格子所组成的路径。 既然是要寻找代价最小的路径,那么遍历节点时,就不能盲目地遍历,而是挑那些 「最有可能代价很小」 的节点来运算。如何获得这种代价很小的节点呢?这就需要使用启发函数来计算每个节点的代价了(在A*中启发函数也被称为 估价函数 )。 一条路径的代价无非就是路径长度(不带权重时),可是当寻找路径时,该如何计算代价呢?那就是将代价分为两个方面: 已知代价。 即从起点到当前节点的路径长度。 未来可能会有的代价。 用 “猜” 的,假设当前节点到终点之间没有障碍,代价是多少。 那么我们就可以得到估价函数为:f(n) = g(n) + h(n),其中g(n)表示起点到格子n的代价,h(n)表示格子n到终点的代价,f(n)表示格子n的代价。 由于在EDA中,是使用曼哈顿距离来表示两点之间的距离,所以本文采用曼哈顿距离来计算代价。 知道启发函数是怎么回事之后我们就可以开始遍历节点了。从起点开始,计算其四周节点的代价,并把那些节点放到一个 「池子」 里面去,之后每次都从池子里面捞出来代价最小的节点,继续计算其周围节点的代价。 如果遍历到目标节点,或者池子空了之后,寻路就结束了。那么如何获得路径呢?我们从遍历的过程可以看出,节点加入到池子之前,我们知道这些加入到池子中的节点是来自于当前节点的,所以在加入池子之前将那些节点的父节点设为当前节点,等寻路到目标节点之后我们逐个节点遍历其父节点,就可以遍历完最短路径。 对于算法,有以下几点需要注意: 计算当前节点的周围节点的代价值时,如果需计算的节点原先就已经算过代价值,且当前算出来的代价值比原先的小,说明这个节点从当前节点过去路径会更短一些,应更新其父节点和代价值。 我们每次都是从「池子」中捞出代价最小的节点,所以用小根堆来实现「池子」是十分高效的。 综上所述,可以画出 A* 算法流程图: ","date":"2024-09-18","objectID":"/posts/1d723ce/:1:0","tags":["Astar"],"title":"Astar算法","uri":"/posts/1d723ce/"},{"categories":["C++"],"content":"A星算法的实现\r作者采用 C++ 来实现 A* 算法,代码已上传至此仓库 ","date":"2024-09-18","objectID":"/posts/1d723ce/:2:0","tags":["Astar"],"title":"Astar算法","uri":"/posts/1d723ce/"},{"categories":["Linux"],"content":"软件安装与更新\r在Linux下,一般用包管理器来安装软件,它能自动化地完成软件的安装、更新、配置和移除功能,优雅而快速。 软件包管理器的一个重要组成部分是软件仓库。软件仓库是收藏了互联网上可用软件包(应用程序)的图书馆,里面往往包含了数万个可供下载 和安装的可用软件包。 有了软件仓库,我们不需要手动下载大量的软件包再通过包管理器安装。只需要知道软件在软件仓库中的名称,即可让包管理器从网络中抓取到相应的软件包到本地,自动进行安装。 但是与应用商店相比,使用包管理器安装需要预先知道所需软件在软件仓库中的对应包名,和应用商店相比无法进行模糊搜索(不过你也可以在包管理器官网上进行查找包名,再通过包管理器安装)。 作者使用的是Ubuntu系统,Ubuntu下默认的包管理器为apt,所有后面的讲解以apt为例。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:1:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"使用包管理器系统安装\r搜索\r安装前,先在浏览器中搜索所需的软件,查找包名,再使用 apt search package_name 命令搜索软件仓库,查看对应的包名是否在软件仓库中(这步可跳过,只要知道要安装的软件包名即可)。 安装\r找到要安装的软件的包名后,使用命令 apt install package_name 进行安装。 PS: 如果提示当前用户无安装软件权限,则在命令前加上 sudo,即 sudo apt install package_name,输入此命令后会要求用户输入密码(输入的密码不会在终端显示)。 官方软件源镜像\r通过 apt 安装的软件都来源于相对应的软件源,每个 Linux 发行版一般都带有官方的软件源,在官方的软件源中已经包含了丰富的软件,apt 的软件源列表在 /etc/apt/sources.list 下。 Ubuntu 官方源位于国外,往往会有速度与延迟上的限制,可以通过修改官方源为其镜像实现更快的下载速度。镜像缓存了官方源中的软件列表,与官方源基本一致。 修改官方源为镜像步骤 本例以修改官方源为 USTC Mirror 为例。注意:在操作前请做好备份。 一般情况下,/etc/apt/sources.list 下的官方源地址为 http://archive.ubuntu.com/ ,我们只需要将其替换为 http://mirrors.ustc.edu.cn 即可。 如果你使用 Ubuntu 图形安装器安装,默认的源地址通常不是 http://archive.ubuntu.com/ , 而是 http://\u003ccountry-code\u003e.archive.ubuntu.com/ubuntu/ ,如 http://cn.archive.ubuntu.com/ubuntu/,同样也将其替换为 http://mirrors.ustc.edu.cn 即可。 可以使用如下命令: $ sudo sed -i 's|//.*archive.ubuntu.com|//mirrors.ustc.edu.cn|g' /etc/apt/sources.list 当然也可以直接使用 vim、nano 等文本编辑器进行修改。 第三方软件源\r有时候,由于种种原因,官方软件源中并没有我们需要的软件,但是第三方软件提供商可以提供自己的软件源。在将第三方软件源添加到 /etc/apt/sources.list 中之后,就可以获取到第三方提供的软件列表,再通过 apt install package_name 安装我们需要的第三方软件。一般可以在需要的第三方软件官网找到对应的配置说明。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:1:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"使用包管理器手动安装\r在一些情况下,软件仓库中并没有加入我们所需要的软件。除了添加第三方软件源,从源安装外,有时候还可以直接下载安装软件供应商打包好的 deb、rpm 等二进制包。 安装软件包需要相应的软件包管理器,deb 格式的软件包对应的是 dpkg。 相对于 apt 而言,dpkg 会更加底层,apt 是一个包管理器的前端,并不直接执行软件包的安装工作,是交由 dpkg 完成。dpkg 反馈的依赖信息则会告知 apt 还需要安装的其他软件包,并从软件仓库中获取到相应的软件包进行安装,从而完成依赖管理问题。 直接通过 dpkg 安装 deb 并不会安装需要的依赖,只会报告出相应的依赖缺失了。 所以,推荐使用apt来安装dev文件,命令如下: sudo apt install devFileName.dev 如果使用dpkg -i 安装软件,导致出现依赖包问题,可以使用命令: sudo apt -f install 来帮助修复依赖管理 ","date":"2024-08-15","objectID":"/posts/3d623e4/:1:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"更新软件列表与更新软件\r在计算机本地,系统会维护一个包列表,在这个列表里面,包含了软件信息以及软件包的依赖关系,在执行 apt install 命令时,会从这个列表中读取出想要安装的软件信息,包括下载地址、软件版本、依赖的包,同时 apt 会对依赖的包递归执行如上操作,直到不再有新的依赖包。如上得到的所有包,将会是在 apt install some-package 时安装的。 使用命令:sudo apt update 获取新的软件版本、软件依赖关系。在更新好包列表后,再使用命令 sudo apt upgrade 将安装的软件进行更新升级。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:1:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"文件和目录操作\r在 Linux 在进行操作文件与目录是使用 Linux 最基础的一个技能。不像在 Windows 和 macOS 下有图形化界面,拖拽文件即可完成文件的移动,很容易管理文件与目录,Linux 的命令行操作虽然繁琐一些,但一旦上手,就可以通过命令与参数的组合完成通过图形化界面难以实现或者无法实现的功能。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"查看文件内容\rcat命令\r使用该命令会一次性将整个文件内容打印到终端,适用于看内容不多的文件,cat 命令用法如下: 查看 file.txt 的全部内容 cat file.txt 查看 file1.txt 与 file2.txt 连接后的全部内容 cat file1.txt file2.txt less命令\r该命令与cat命令的区别在于,此命令一次只会显示一页,且支持向前/后滚动、搜索等功能,适用于查看大型文件。用法如下: less FILE 在查看文件时,可以使用以下快捷键: 按键 效果 d/u 向下/上滚动半页 f/b or Page Down/Page Up 向下/上滚动一页 g/G 跳转到文件开头/结尾 j/Down 向下移动一行 k/Up 向上移动一行 /PATTERN 在文件中搜索内容PATTERN n/N 跳转到下一个/上一个找到的搜索内容 q 退出 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"编辑文件内容\rNano 是在很多机器上自带的命令行文本编辑器,相比于 vim 和 emacs 来说,对新手更加友好,不需要提前记忆复杂的键位。 命令:nano file.txt 使用 nano 编辑 file.txt 文件,如果没有则创建。 Nano 启动后,用户可以直接开始输入需要的内容,使用方向键移动光标。在终端最下方是 nano 的快捷键,^ 代表需要按下 Ctrl 键(例如,^X 就是需要同时按下 Ctrl + X)。在编辑完成后,按下 Ctrl + X,确认是否保存后即可。 当然也可以使用 Vi/Vim 来编辑文件内容。 Vim 被誉为「编辑器之神」,但是其陡峭的学习曲线也让人望而却步。因为不一定所有的机器上都有 nano,但是可以肯定几乎所有的机器上都会安装 vi(vim 的前身),所以了解如何使用 vim,恐怕是一件难以避免的事情。所幸,vim 的基础操作并不算难。图形界面下也可以安装 gvim 获得图形界面。 在打开 vim 后,默认进入的是普通模式,按下 i 就进入编辑模式,进入编辑模式后就可以随意编辑文件了。在这两个模式中,都可以使用键盘方向键移动光标。在编辑完成后,按下 Esc 回到普通模式,然后输入 :wq 就可以保存文件并退出;如果不想保存文件直接退出,则输入 :q! 即可。 以上的简单教学已经可以帮助你正常操作 vim 了,vim 也自带 vimtutor 教学程序(非常好!!!墙裂推荐,直接在命令行输入vimtutor即可启动程序),可以帮助你快速掌握 vim 的基本操作。熟练使用 vim 有助于提高编辑文本时的工作效率。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"复制文件和目录\r命令格式为:cp [opt1] [opt2] ... [optn] source dest source 表示要复制的文件或目录的路径 dest 表示目标文件或目录的路径 [opt] 表示可选参数 常用参数如下: 参数 含义 -r, -R, --recursive 递归复制,常用于复制目录 -f, --force 覆盖目标地址同名文件 -u, --update 仅当源文件比目标文件新才进行复制 -l, --link 创建硬链接 -s, --symbolic-link 创建软链接 硬链接和软链接\r简单而言,一个文件的硬链接和软链接都指向文件自身,但是在底层有不同的实现。\r需要先了解一个概念:inode。在许多“类 Unix 文件系统”中,inode 用来描述文件系统的对象,如文件和目录。inode 记录了文件系统对象的属性和磁盘块的位置。可以被视为保存在磁盘中的文件的索引(英文:index node)。 关于 inode 的进一步讲解可以参考这篇文章。 硬链接与源文件有着相同的 inode,都指向磁盘中的同一个位置。删除其中一个,并不影响另一个。 软链接与源文件的 inode 不同。软链接保存了源文件的路径,在访问软链接的时候,访问的路径被替换为源文件的路径,因此访问软链接也等同于访问源文件。但是如果删除了源文件,软链接所保存的路径也就无效了,软链接因此也是无效的。 ln命令也可以用来硬链接和软链接 ln -s file symlink # 创建指向文件 file 的软链接 symlink ln file hardlink # 创建指向文件 file 的硬链接 hardlink 下面是一些cp命令的使用示例: 将 file.txt 文件复制到当前目录下的 dir/ 下 cp file1.txt dir/ 将目录 dir1/ 及其所有内容递归地复制到当前目录下的 dir2/ 中 cp -r dir1/ dir2/ 将文件 file1.txt 复制为 file2.txt,并且在复制之前询问是否覆盖已存在的 file2.txt cp -i file1.txt file2.txt 将文件 file1.txt 复制为 file2.txt,并且强制覆盖已存在的 file2.txt,而不提示 cp -f file1.txt file2.txt 递归地复制当前目录下的 dir1/ 及其所有内容到当前目录下的 dir2/,并且保留源文件的权限、所有权和时间戳信息 cp -rp dir1/ dir2/ 将多个文件一同复制到当前目录下的dir2/中 cp file1.txt file2.txt file3.txt dir2/ ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"移动文件和目录\r命令格式为:mv [opt1] [opt2]... source dest mv与cp 的使用方式类似(source文件也可以有多个),效果就是 Windows 下的剪切,即mv命令默认是递归的。 常用参数: 参数 含义 -f 覆盖目标地址同名文件 -u 仅当源文件比目标文件新才进行移动 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:4","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"删除文件和目录\r命令格式:rm [opt1] [opt2] ... FILE ... 常用参数: 参数 含义 -f 不需要提示,直接删除文件,即使文件属性为只读 -i 删除前逐一询问是否删除 -r 递归删除目录下的文件。删除除空目录外的目录必须加此参数 rm file1.txt # 删除当前目录下的 file1.txt 文件 rm -r test/ # 删除当前目录下的 test 目录 rm -rf test1/ test2/ file1.txt #删除当前目录下的 test1、test2目录 和 file1.txt 文件 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:5","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"创建目录\r命令格式:mkdir [opt1] ... DirName ... 常用参数: 参数 含义 -p 如果中间目录不存在,则创建;如果要创建的目录已经存在,则不报错 mkdir test1 #在当前目录下创建目录test1 mkdir test1 test2 #在当前目录下创建目录test1和test2 mkdir -p test1/test2/test3/ #在当前目录下创建路径 test1/test2/test3 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:6","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"创建文件\r命令格式:touch file1 ... touch file1.txt test.md #在当前目录下创建 file1.txt 和 test.md 文件 为什么名字是touch而不是create呢?\rtouch 工具实际上的功能是修改文件的访问时间(access time, atime)和修改时间(modification time, mtime),可以当作是摸(touch)了一下文件,使得它的访问与修改时间发生了变化。当文件不存在时,touch 会创建新文件,所以创建文件也就成为了 touch 最常见的用途。\r使用 stat filename 命令可以查看文件的属性信息。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:7","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"搜索文件和目录\r命令格式:find PATH [opt1] [opt2] ... PATH 用于指定查找路径。如果不指定,则默认在当前目录下进行查找,即 find . 等价于 find 常用参数: 参数 含义 -name '*.ext' 查找后缀为ext的文件,*是任意匹配符 -type d 指定查找的文件类型为目录 -size +1M 查找文件大小大于1M的文件,+表示大于这个大小,-表示小于这个大小 -or 或运算符,表示它前后两个条件满足一个即可 find -name 'report.pdf' #在当前目录下查找 report.pdf 文件 find / -size +1G #在全盘查找大于1G的文件 find ~/ -name 'node_modules' -type d #在用于目录下查找名为node_modules的目录 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:8","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"模式匹配\r许多现代的 shell 都支持一定程度的模式匹配。举个例子,bash 的匹配模式被称为 glob,支持的操作如下: 模式 匹配的字串 * 任意字串 foo* 以foo开头的字串 *x* 含有x的字串 ? 一个任意字符 a?b acb、a0b等,但不能是accb这种,?只能表示一个任意字符 *.[ch] 以 .c 或 .h 结尾的字串 和上面提到的命令结合,可以显著提高操作效率。例如: rm *.tar.gz #删除当前目录下所有以 .tar.gz 结尾的压缩文件 mv *.[ch] /path #将当前目录及子目录下所有以 .c 和 .h 结尾的文件移动到 /path 下 PS: 使用通配符前请再三确认输入无误,否则可能出现严重的后果(建议结合ChatGPT来使用)。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:9","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"使用tar操作文档、压缩文件\r经常,我们希望将许多文件打包然后发送给其他人,这时候就会用到 tar 命令,作为一个存档工具,它可以将许多文件打包为一个存档文件。 通常,可以使用其自带的 gzip 或 bzip2 算法进行压缩,生成压缩文件:tar [opt1] [opt2] ... FILE ... 常用选项: 选项 含义 -A 将一个存档文件中的内容追加到另一个存档文件中 -r, --append 将一些文件追加到一个存档文件中 -c, --create 从一些文件创建存档文件 -t, --list 列出一个存档文件的内容 -x, --extract, --get 从存档文件中提取出文件 -f, --file=ARCHIVE 使用指定的存档文件 -C, --directory=DIR 指定输出的目录 添加压缩选项可以使用压缩算法进行创建压缩文件或者解压压缩文件: 选项 含义 -z, --gzip, --gunzip, --ungzip 使用 gzip 算法处理存档文件 -j, --bzip2 使用 bzip2 算法处理存档文件 -J, --xz 使用 xz 算法处理存档文件 下面是一些tar 的使用示例: tar -c -f target.tar file1 file2 file3 # 将 file1、file2、file3 打包为 target.tar tar -x -f target.tar -C test/ # 将 target.tar 中的文件提取到 test 目录中 tar -cz -f target.tar.gz file1 file2 file3 # 将 file1、file2、file3 打包,并使用 gzip 算法压缩,得到压缩文件 target.tar.gz tar -xz -f target.tar.gz -C test/ # 将压缩文件 target.tar.gz 解压到 test 目录中 tar -Af archive.tar archive1.tar archive2.tar archive3.tar # 将 archive1.tar、archive2.tar、archive3.tar 三个存档文件中的文件追到 archive.tar 中 tar -t -f target.tar # 列出 target.tar 存档文件中的内容 tar -tv -f target.tar # 列出 target.tar 存档文件中的文件的详细信息 与大部分 Linux 命令相同,tar 命令允许将多个单字母(使用单个 - 符号的)选项组合为一个参数,便于用户输入。例如,以下命令是等价的: tar -c -z -v -f target.tar test/ tar -czvf target.tar test/ tar -f target.tar -czv test/ 存档文件的后缀名\r后缀名并不能决定文件类型,但后缀名通常用于帮助人们辨认这个文件的可能文件类型,从而选择合适的打开方法。\r在第一个例子中,创建得到的文件名为 target.tar,后缀名为 tar,表示这是一个没有进行压缩的存档文件。 在第二个例子中,创建得到的文件名为 target.tar.gz。将 tar.gz 整体视为后缀名,可以判断出,为经过 gzip 算法压缩(gz)的存档文件(tar)。可知在提取文件时,需要添加 -z 选项使其经过 gzip 算法处理后再进行正常 tar 文件的提取。 同样地,通过不同压缩算法得到的文件应该有不同的后缀名,以便于选择正确的参数。如经过 xz 算法处理得到的存档文件,其后缀名最好选择 tar.xz,这样可以知道为了提取其中的文件,应该添加 --xz 选项。 为什么使用 tar 创建压缩包需要两次处理?\rtar 名字来源于英文 tape archive,原先被用来向只能顺序写入的磁带写入数据。tar 格式本身所做的事情非常简单:把所有文件(包括它们的“元数据”,包含了文件权限、时间戳等信息)放在一起,打包成一个文件。注意,这中间没有压缩的过程。\r为了得到更小的打包文件,方便存储和网络传输,就需要使用一些压缩算法,缩小 tar 文件的大小。这就是 tar 处理它自己的打包文件的逻辑。在 Windows 下的一部分压缩软件中,为了获取压缩后的 tar 打包文件的内容,用户需要手动先把被压缩的 tar 包解压出来,然后再提取 tar 包中的文件。 在 Linux 上的 tar 一般只支持 gzip、bzip、xz 和 lzip 几种压缩算法。如果需要解压 Windows 上更为常见的 7z、zip 和 rar 等,则需要寻求替代软件,这里推荐使用 unar 软件。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:2:10","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"进程\r简单而不太严谨地来说,进程就是正在运行的程序:当我们启动一个程序的时候,操作系统会从硬盘中读取程序文件,将程序内容加载入内存中,之后 CPU 就会执行这个程序。 进程是现代操作系统中必不可少的概念。在 Windows 中,我们可以使用「任务管理器」查看系统运行中的进程;Linux 中同样也有进程的概念。下面简单介绍 Linux 中的进程。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:3:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"查看当前运行的进程\r使用htop软件\rhtop 可以简单方便查看当前运行的所有进程,以及系统 CPU、内存占用情况与系统负载等信息,直接在命令行输入 htop 即可启动。 使用鼠标与键盘都可以操作 htop。Htop 界面的最下方是一些选项,使用鼠标点击或按键盘的 F1 至 F10 功能键可以选择这些功能,常用的功能例如搜索进程(F3, Search)、过滤进程(F4, Filter,使得界面中只有满足条件的进程)、切换树形结构/列表显示(F5, Tree/List)等等。 使用 ps 命令\rps(process status)是常用的输出进程状态的工具。直接在命令行输入 ps 命令即可启动。 ps 命令仅会显示本终端中运行的相关进程,如果需要显示所有进程,对应的命令为 ps aux。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:3:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"进程标识符\r首先,有区分才有管理。进程标识符(PID,Process Identifier)是一个数字,是进程的唯一标识。在 htop 中,最左侧一列即为 PID。当用户想挂起、继续或终止进程时可以使用 PID 作为索引。 在 htop 中,直接单击绿色条内的 PID 栏,可以将进程顺序按照 PID 升序排列,再次点击为降序排列,同理可应用于其他列。 Linux进程启动顺序\r按照 PID 排序时,我们可以观察系统启动的过程。Linux 系统内核从引导程序接手控制权后,开始内核初始化,随后变为 init_task,初始化自己的 PID 为 0。随后创建出 1 号进程(init 程序,目前一般为 systemd)衍生出用户空间的所有进程,创建 2 号进程 kthreadd 衍生出所有内核线程。随后 0 号进程成为 idle 进程,1 号、2 号并非特意预留,而是产生进程的自然顺序使然。\r由于 kthreadd 运行于内核空间,故需按大写 K(Shift + k)键显示内核进程后才能看到。然而无论如何也不可能在 htop 中看到 0 号进程本体,只能发现 1 号和 2 号进程的 PPID 是 0。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:3:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"进程优先级与状态\r我们平时使用操作系统的时候,可能同时会开启浏览器、聊天软件、音乐播放器、文本编辑器……前面提到它们都是进程,但是单个 CPU 核心一次只能执行一个进程。为了让这些软件看起来 「同时」 在执行,操作系统需要用非常快的速度将计算资源在这些进程之间切换,这也就引入了进程优先级和进程状态的概念。 优先级与 nice 值\r有了进程,谁先运行?谁给一点时间就够了,谁要占用大部分 CPU 时间?这又是如何决定的?这些问题之中体现着 「优先权」 的概念。如果说上面所介绍的那些进程属性描述了进程的控制信息,那么 「优先权」 则反映操作系统调度进程的手段。 在 htop 的显示中有两个与优先级有关的值: Priority(PRI) 和 nice(NI)。以下主要介绍用户层使用的 nice 值。 Nice 值越高代表一个进程对其它进程越 “nice”(友好),对应的优先级也就更低。Nice 值最高为 19,最低为 -20。通常,我们运行的程序的 nice 值为 0。我们可以打开 htop 观察与调整每个进程的 nice 值。 用户可以使用 nice 命令在运行程序时指定优先级,而 renice 命令则可以重新指定优先级。当然,若想调低 nice 值,还需要 sudo(毕竟不能随便就把自己的优先级设置得更高,不然对其他的用户不公平)。 nice -n 10 vim # 以 10 为 nice 值运行 vim renice -n 10 -p 12345 # 设置 PID 为 12345 的进程的 nice 值为 10 PRI值\r如果你在 htop 中测试调整进程的 nice 值,可能会发现一个公式:「PRI = nice + 20」。这对于普通进程是成立的——普通进程的 PRI 会被映射到一个「非负」整数。\r但在正常运行的 Linux 系统中,我们可能会发现有些进程的 PRI 值是 RT,或者是负数。这表明对应的进程有更高的实时性要求(例如内核进程、音频相关进程等),采用了与普通进程不同的调度策略,优先级也相应更高。 进程状态\r配合上面的进程调度策略,我们可以粗略地将进程分为三类: 一类是正在运行的程序,即处于运行态(running) 一类是可以运行但正在排队等待的程序,即处于就绪态(ready) 最后一类是正在等待其他资源(例如网络或者磁盘等)而无法立刻开始运行的程序,即处于阻塞态(blocked)。 调度时操作系统轮流选择可以运行的程序运行,构成就绪态与运行态循环。运行中的程序如果需要其他资源,则进入阻塞态;阻塞态中的程序如果得到了相应的资源,则进入就绪态。 在实际的 Linux 系统中,进程的状态分类要稍微复杂一些。在 htop 中,按下 H 键到帮助页,可以看到对进程状态的如下描述: Status: R: running; S: sleeping; T: traced/stopped; Z: zombie; D: disk sleep 其中 R 状态对应上文的运行和就绪态(即表明该程序可以运行),S 和 D 状态对应上文阻塞态。 需要注意的是,S 对应的 sleeping 又称 interruptible sleep,字面意思是「可以被中断」;而 D 对应的 disk sleep 又称 uninterruptible sleep,不可被中断,一般是因为阻塞在磁盘读写操作上。 Zombie 是僵尸进程,该状态下进程已经结束,只是仍然占用一个 PID,保存一个返回值。而 traced/stopped 状态正是下文使用 Ctrl + Z 导致的挂起状态(大写 T),或者是在使用 gdb 等调试(Debug)工具进行跟踪时的状态(小写 t)。 进程状态表: 状态 缩写表示 说明 Running R 正在运行/可以立刻运行 Sleeping S 可以被中断的睡眠 Disk Sleep D 不可以被中断的睡眠 Traced / Stopped T 被跟踪/被挂起的进程 Zombie Z 僵尸进程 ","date":"2024-08-15","objectID":"/posts/3d623e4/:3:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"用户进程控制\r要想控制进程,首先要与进程对话,那么必然需要了解进程间通信机制。由于 「进程之间不共享自己的内存空间」,也就无法直接发送信息,必须要操作系统帮忙,于是信号机制就产生了。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:4:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"信号\r信号是 Unix 系列系统中进程之间相互通信的一种机制。发送信号的 Linux 命令叫作 kill。被称作 “kill” 的原因是:早期信号的作用就是关闭(杀死)进程。 信号列表可以使用命令 man 7 signal 查看。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:4:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"前后台切换\r上面的图片中,出现了 fg,bg 和 Ctrl + Z,涉及到的正是 shell 中前后台的概念。 一个程序运行在前台指的是: 当前终端窗口会不能接受其他指令,直到当前程序暂停或运行结束 关闭当前终端窗口时,该程序也会被关闭 一个程序运行在后台指的是: 程序运行在后台,当前终端窗口可以接受其他指令 关闭当前终端窗口时,该程序也会被关闭 PS: 无论程序运行在前台还是后台,如果有输出到终端的程序指令,如cout,cerr等,都会在运行的终端上输出 默认情况下,在 shell 中运行的命令都在前台运行,如果需要在后台运行程序,需要在最后加上 \u0026 符号: ./matmul \u0026 # 例子:将耗时的计算放在后台运行同时进行其他操作 ps PID TTY TIME CMD 1720 pts/0 00:00:00 bash 1861 pts/0 00:00:06 matmul 1862 pts/0 00:00:00 ps # 使用 ps 命令,可以发现 matmul 程序在后台运行,同时我们仍然可以操作 shell 而如果需要将前台程序切换到后台,则需使用命令 「Ctrl + Z」 发送 SIGTSTP 信号使进程挂起(即将程序暂停运行并放入后台),控制权还给 shell。再使用 「jobs」 命令,查看当前 shell 上所有相关进程。 此时屏幕输出如下所示,刚才挂起的进程 代号为 2(不是pid),状态为 stopped,命令为 ping localhost。 任务前的代号在 fg,bg,乃至 kill 命令中发挥作用。使用时需要在前面加 %。 bg %2 #表示让代号为2的任务在后台继续运行 fg %2 #表示让代号为2的任务在前台继续运行 也许你注意到了编号后面跟着 + 和 - 号: [1] - running ./signal handle [2] + suspended ping localhost 这里的加号标记了 fg 和 bg 命令默认作用到的任务为 2,所以这里 bg %2 也可以直接简化为 bg。减号表示如果加号标记的进程退出了,它就会成为加号标记进程。我们也可以用 %+ 和 %- 指代这两个任务。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:4:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"终止进程\r正如上所述,许多信号都会引发进程的终结,然而标准的终止进程信号是 SIGTERM,意味着一个进程的自然死亡。 使用 htop 发送信号\r启动 htop 后,使用 「空格」 标记要发送信号的进程(被标记的进程会变色),再按下 K 键,在左侧提示栏中选择要发送的信号,按下回车发送信号。 使用 kill 命令发送信号\r命令格式:kill -signal PID -signal 参数为信号的名称或编号。如果不使用此参数,将默认使用 15(SIGTERM) 作为信号参数 使用 kill -l 可查看所有信号 $ kill -l # 显示所有信号名称 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX ","date":"2024-08-15","objectID":"/posts/3d623e4/:4:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"脱离终端\r前面说过,无论是前台还是后台运行的程序,只要关闭 shell 窗口(即终端),这些程序都会停止运行。这是因为终端一旦被关闭会向其中每个进程发送 SIGHUP(Signal hangup)信号,而 SIGHUP 的默认动作即退出程序运行。 经常使用 SSH 连接到远程服务器执行任务的人都知道,shell 中执行的程序在 SSH 断开之后会被关闭(SSH 断开视为关闭终端)。如果想要程序在关闭shell窗口或SSH断开后仍然能继续运行,则可以在命令前加上 nohup 命令。该命令字面含义就是「不要被 SIGHUP 影响」。 例如:nohup ping 101.ustclug.org \u0026 ","date":"2024-08-15","objectID":"/posts/3d623e4/:4:4","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"命令行多终端方式\r一个终端(硬件概念)只有一套鼠标键盘,只能有一个 shell 主持一个 session,那如果我在 SSH 连接的时候有几个程序需要同时交互的话,只有一个前台进程很不方便。而且上面说过如果 SSH 网络断开,视为终端被关闭,也就意味着前后台一起收到 SIGHUP 一起退出,好不容易设置好的临时变量什么的还得重设。 开启多个 SSH 连接似乎可以解决这个问题。但是如果程序既需要交互,又想保证不因意外断线而停止程序,就是 nohup 也帮不了。 这时 tmux 的出现,解决了会话保持与窗口复用的问题。关于 tmux 更详细的介绍,可以参见这篇博客。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:4:5","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"服务与例行性任务\r说到 「服务」,我们可能会想到服务器,上面运行着各式各样的网络服务。但是这里的「服务」不止于此,系统的正常运行也需要关键服务的支撑。在 Windows 中,我们可以从任务管理器一窥 Windows 中的「服务」;Linux 也有服务的概念,下面将作介绍。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:5:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"守护进程\r服务的特征,意味着服务进程必须独立于用户的登录,不能随用户的退出而被终止。根据前面的讲解,只有启动时脱离会话才能避免因为终端的关闭而消失。而这类一直默默工作于后台的进程被称为守护进程 (daemon)。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:5:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"管理服务\r目前绝大多数 Linux 发行版的 init 方案都是 systemd,其管理系统服务的命令是 systemctl。 要管理服务,首先我们要清楚系统内有哪些服务。可以通过 systemctl status 命令一览系统服务运行情况。 ~ systemctl status ● zyz-virtual-machine State: running Jobs: 0 queued Failed: 0 units Since: Thu 2024-04-18 11:00:49 CST; 3h 22min ago CGroup: / ├─user.slice │ └─user-1000.slice │ ├─user@1000.service │ │ ├─gsd-xsettings.service │ │ │ └─1869 /usr/libexec/gsd-xsettings │ │ ├─gvfs-goa-volume-monitor.service │ │ │ └─1731 /usr/libexec/gvfs-goa-volume-monitor │ │ ├─gsd-power.service │ │ │ └─1815 /usr/libexec/gsd-power │ │ ├─xdg-permission-store.service │ │ │ └─1697 /usr/libexec/xdg-permission-store │ │ ├─xdg-document-portal.service │ │ │ └─1982 /usr/libexec/xdg-document-portal │ │ ├─xdg-desktop-portal.service │ │ │ └─2138 /usr/libexec/xdg-desktop-portal │ │ ├─gsd-sound.service │ │ │ └─1856 /usr/libexec/gsd-sound (以下省略) 上面命令所列出的是系统中正在运行的服务,若想了解全部服务内容,可以运行 systemctl list-units 来查看。该命令将显示所有 systemd 管理的单元,同时右面还会附上一句注释来表明该服务的任务。 Unit并不是Service,以上的叙述仅仅是为了方便。Systemd 中的 unit 可以是服务(Service)、设备(Device)、挂载点(Mount)、定时器(Timer)……有关 systemd unit 的详细介绍,可见 systemd.unit(5) 至于服务的启动,终止,重载配置等命令可交付 tldr 介绍。 $ tldr systemctl systemctl Control the systemd system and service manager. - List failed units: # 列出运行失败的服务 systemctl --failed - Start/Stop/Restart/Reload a service: # 开启/关闭/重启/重载服务。Reload 代表重载配置文件而不重启进程。 systemctl start/stop/restart/reload {{unit}} - Show the status of a unit: # 显示服务状态 systemctl status {{unit}} - Enable/Disable a unit to be started on bootup: # 设置(Enable)/取消(Disable)服务开机自启 systemctl enable/disable {{unit}} - Mask/Unmask a unit, prevent it to be started on bootup: # 阻止/取消阻止服务被 enable systemctl mask/unmask {{unit}} - Reload systemd, scanning for new or changed units: # 重载 systemd,需要在创建或修改服务文件后执行 systemctl daemon-reload ","date":"2024-08-15","objectID":"/posts/3d623e4/:5:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"自定义服务\r如果我想将一个基于 Web 的应用(如基于 Web 的 Python 交互应用)作为局域网内 Web 服务,以便于在其他设备上访问。那么如何将其注册为 systemd 服务呢? 其实只需要编写一个简单的 .service 文件即可。 编写 .service 文件并运行(以 Jupyter Notebook 为例) Jupyter Notebook 是基于浏览器的交互式编程平台,在数据科学领域非常常用。 首先使用文本编辑器在 /etc/systemd/system 目录下创建一个名为 jupyter.service 的文件。并填入以下内容后保存: [Unit] Description=Jupyter Notebook # 该服务的简要描述 [Service] PIDFile=/run/jupyter.pid # 用来存放 PID 的文件 ExecStart=/usr/local/bin/jupyter-notebook --allow-root # 使用绝对路径标明的命令及命令行参数 WorkingDirectory=/root # 服务启动时的工作目录 Restart=always # 重启模式,这里是无论因何退出都重启 RestartSec=10 # 退出后多少秒重启 [Install] WantedBy=multi-user.target # 依赖目标,这里指进入多用户模式后再启动该服务 然后运行 systemctl daemon-reload,就可以使用 systemctl 命令来管理这个服务了,例如: systemctl start jupyter systemctl stop jupyter systemctl enable jupyter # enable 表示标记服务的开机自动启动 systemctl disable jupyter # 取消自启 可以参考系统中其他 service 文件,以及 systemd.service(5) 手册页编写配置文件 ","date":"2024-08-15","objectID":"/posts/3d623e4/:5:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"例行性服务\r所谓例行性任务,是指基于时间的一次或多次周期性定时任务。在 Linux 中,实现定时任务工作的程序主要有 at 和 crontab,它们无一例外都作为系统服务存在。 at命令\rat 命令负责单次计划任务,当前许多发行版中,并没有预装该命令,需要使用命令 sudo apt install at 进行安装。 随后使用 tldr 查看该命令使用方法: $ tldr at at Execute commands once at a later time. Service atd (or atrun) should be running for the actual executions. - Execute commands from standard input in 5 minutes (press Ctrl + D when done): at now + 5 minutes - Execute a command from standard input at 10:00 AM today: echo \"{{./make_db_backup.sh}}\" | at 1000 - Execute commands from a given file next Tuesday: at -f {{path/to/file}} 9:30 PM Tue 所以该命令的基本用法示例如下: $ at now + 1min \u003e echo \"hello\" \u003e \u003cEOT\u003e (按下 Ctrl + D) job 3 at Sat Apr 18 16:16:00 2020 # 任务编号与任务开始时间 一分钟后……为什么没有打印出字符串呢?其实是因为 at 默认将标准输出(stdout)和标准错误(stderr)的内容以邮件形式发送给用户。使用编辑器查看 /var/mail/$USER 就可以看到输出了(需要本地安装 mail 相关的服务)。 设置完任务之后,我们需要管理任务,我们可以用 at -l 列出任务,at -r \u003c编号\u003e 删除任务。它们分别是 atq 和 atrm 命令的别名。 crontab命令\rcron 命令负责周期性的任务设置,与 at 略有不同的是,cron 的配置大多通过配置文件实现。 大多数系统应该已经预装了 crontab,首先查看 crontab 的用法: $ crontab --help crontab: invalid option -- '-' # 出现这两行字很正常,许多命令(如 ssh)没有专用的 help crontab: usage error: unrecognized option # 输入「错误」的选项时,便会出现简单的使用说明。 usage: crontab [-u user] file crontab [ -u user ] [ -i ] { -e | -l | -r } (default operation is replace, per 1003.2) -e (edit user's crontab) -l (list user's crontab) -r (delete user's crontab) -i (prompt before deleting user's crontab) 可以看到基本命令即对指定用户的例行任务进行显示、编辑、删除。如果任何参数都不添加运行 crontab,将从标准输入(stdin)读入设置内容,并覆盖之前的配置。所以如果想以后在现有配置基础上添加,应当在家目录中创建专用文件存储,或者使用 crontab -e 来对本用户任务进行编辑。 crontab 的配置格式很简单,对于配置文件的每一行,前半段为时间,后半段为 shell 执行命令。其中前半段的时间配置格式为: # 分 时 日 月 星期 | 命令 # 下面是几个示例 * * * * * echo \"hello\" \u003e\u003e ~/count # 每分钟输出 hello 到家目录下 count 文件 0,15,30,45 0-6 * JAN SUN command # 每年一月份的每个星期日半夜 0 点到早晨 6 点每 15 分钟随便做点什么 5 3 * * * curl 'http://ip.42.pl/raw' | mail -s \"ip today\" xxx@xxx.com # 每天凌晨 3 点 05 分将查询到的公网 ip 发送到自己的邮箱 (假设半夜 3 点重新拨号) 如果这里解释得尚不清楚,可以访问 此网站,该网站可以将配置文件中的时间字段翻译为日常所能理解的时间表示。 另外,systemd 的 timer 单元也可以实现定时任务,配置文件的格式与上面的 service 文件一致,具体可以参考 Systemd 定时器教程进行配置。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:5:4","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"用户与用户组、文件权限、文件系统层次结构\r","date":"2024-08-15","objectID":"/posts/3d623e4/:6:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"用户与用户组\r为何需要用户\r早期的操作系统没有用户的概念(如 MS-DOS),或者有「用户」的概念,但是几乎不区分用户的权限(如 Windows 9x)。而现在,这不管对于服务器,还是个人用户来说,都是无法接受的。 在服务器环境中,「用户」的概念是明确的:服务器的管理员可以为不同的使用者创建用户,分配不同的权限,保障系统的正常运行;也可以为网络服务创建用户(此时,用户就不再是一个有血有肉的人),通过权限限制,以减小服务被攻击时对系统安全的破坏。 而对于个人用户来说,他们的设备不会有第二个人在使用。此时,现代操作系统一般区分使用者的用户与「系统用户」,并且划分权限,以尽可能保证系统的完整性不会因为用户的误操作或恶意程序而遭到破坏。 Linux下的用户简介\r你可以查看 /etc/passwd 文件,来得到系统中用户的配置信息。 以下是一个例子: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin (中间内容省略) sshd:x:110:65534::/run/sshd:/usr/sbin/nologin ustc:x:1000:1000:ustc:/home/ustc:/bin/bash lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false mysql:x:111:116:MySQL Server,,,:/nonexistent:/bin/false 在此文件中,每一行都代表一个用户,每行中用户信息由冒号 : 隔开,存储着包括用户名、用户编号 (UID, User ID)、家目录位置等信息。更多介绍,可以通过 man 5 passwd 查阅。 可以关注到,除了你自己以外,还有一个特殊的用户:root,和一大堆你素未相识的名字。下面将会进行介绍。 在 Unix 最初的时候,passwd 文件存储了用户密码的哈希。但是,这个文件是所有用户都可以读取的。为了不让用户的密码哈希被任意获取,进而导致用户的密码被暴力破解,现在一般把密码存在别的地方。对于 Linux 来说,密码哈希信息存储在 /etc/shadow 里面,只有根用户可以访问与修改。 根用户 根用户在 Linux 操作系统中拥有最高的权限,可以对系统做任何操作(包括删除所有系统文件这一类极端危险的操作)。root 用户的用户数据存储在 /root 下。 在使用 sudo 的时候,输入自己的密码并验证正确之后,sudo 就会以 root 用户的身份,执行后面我们希望执行的命令。而使用 apt 安装的软件存储在了系统的目录下,所以必须要以 root 用户的身份安装,这就是我们平时需要 sudo 来安装软件的原因。 谨慎使用 sudo 来执行命令\r我们知道,root 用户可以对系统做极其危险的操作。当使用 root 权限执行命令时(如使用 sudo),一定要小心、谨慎,理解命令的含义之后再按下回车。请不要复制网络上所谓的「Linux 优化命令」等,以 root 权限执行,否则可能会带来灾难性的后果。 以下是一些会对系统带来毁灭性破坏的例子。 再重复一遍,不要执行下面的命令! rm -rf /(删除系统中的所有可以删除的文件,包括被挂载的其他分区。即使不以 root 权限执行,也可以删掉自己的所有文件。) mkfs.ext4 /dev/sda(将系统的第一块硬盘直接格式化为 ext4 文件系统。这会破坏其上所有的文件。) dd if=/dev/urandom of=/dev/sda(对系统的第一块硬盘直接写入伪随机数。这会破坏其上所有的文件,并且找回文件的可能性降低。) :(){ :|: \u0026 };:(被称为「Fork 炸弹」,会消耗系统所有的资源。在未对进程资源作限制的情况下,只能通过重启系统解决,所有未保存的数据会丢失。) 系统用户 除了你、root 和其他在用你的电脑/服务器的人(如果有)以外,剩下还有很多用户,如 nobody, www-data 等。它们由系统或相关程序创建,用于执行服务等系统任务。不要随意删除这些用户,以免系统运行出现问题。 一般地,在 Linux 中,系统用户的 UID 有一个指定范围,而这段范围在各个发行版中可能不同。如 Debian 使用了 100-999, 60000-64999 等区间分配给系统用户。 此外,由于系统用户的特殊性,它们一般默认禁止使用密码登录。 普通用户 普通用户可以登录系统,并只能对自己的家目录下的文件进行操作。所有普通用户的家目录都在 /home/ 下,位于 /home/username/ 的位置,其中 username 是用户名。 普通用户无法直接修改系统配置,也无法为系统环境安装或卸载软件。 切换用户\rsudo 命令 该命令可以让你以「另一个用户的身份」来执行指定的命令。命令格式如下: sudo -u 用户名 命令 不加参数,则表示以 root 用户身份执行指定命令。 su 命令 该命令用于直接切换用户,命令格式如下: su 用户名 不加用户名参数,则表示切换到 root 用户。 在读完上面这句话之后,你可能会尝试切换到 root,但是却失败了。这是因为,如 Ubuntu 等 Linux 发行版默认禁止了 root 用户的密码登录,只允许通过 sudo 提高权限。但是,我们可以用 sudo 运行 su,来得到一个为 root 用户权限的 shell。 $ sudo su Password: (没错,是我自己的密码) # id uid=0(root) gid=0(root) groups=0(root) # exit $ 「sudo su」 和 「sudo su -」 的区别\rsudo su: 该命令切换当前用户到 root 用户,并启动一个新的 shell。它不会改变环境变量,当前用户的环境变量将被保留。 例如,如果当前用户是 user1,则执行 sudo su 后,用户会切换到 root 用户,但当前工作目录和环境变量等都保持为 user1 的设置。 sudo su -: 该命令也用于切换当前用户到 root 用户,并启动一个新的 shell。但是,它会模拟登录 root 用户,因此会加载 root 用户的环境变量和配置文件(例如 .bashrc、.profile 等),并且当前工作目录会切换到 root 用户的家目录。 与 sudo su 不同,执行 sudo su - 后,用户切换到 root 用户后会拥有 root 用户的完整环境,包括 PATH、umask 等设置。 因此,如果需要以 root 用户身份登录到系统,并且希望获取完整的 root 用户环境和配置文件,可以使用 sudo su - 命令。如果您只是需要执行一些 root 权限下的命令,并且不需要修改环境变量,可以使用 sudo su 命令。 用户组简介\r用户组是用户的集合。通过用户组机制,可以为一批用户设置权限。可以使用 groups 命令,查看自己所属的用户组。 ➜ 桌面 groups zyz adm cdrom sudo dip plugdev lpadmin lxd sambashare 可以看到,用户 zyz 从属于多个用户组,包括一个与其名字相同的用户组。一般在用户创建的时候,都会创建与它名字相同的用户组。 对于普通用户来说,用户组机制会在配置部分软件时使用到。如在使用 Docker 时,可以把自己加入 docker 用户组,从而不需要使用 root 权限,也可以访问它的接口。 将某个用户加入某个组的命令为:sudo usermod -a -G groupname username 同样,用户组和用户一样,也有编号:GID (Group ID)。 命令行的用户配置操作\rpasswd命令 使用命令 passwd UserName 修改 UserName 用户的密码。如果没有输入用户名,则修改自己的密码。 adduser命令 adduser 是 Debian 及其衍生发行版的专属命令,它可以用来向系统添加用户、添加组,以及将用户加入组。 sudo adduser 用户名 ,即可添加新用户 sudo adduser --group 组名 ,即可添加新用户组 sudo adduser 用户名 组名 ,即可将指定用户添加到指定用户组 ","date":"2024-08-15","objectID":"/posts/3d623e4/:6:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"文件权限\r在 Linux 中,每个文件和目录都有自己的权限。可以使用 ls -l 查看当前目录中文件的详细信息。 $ ls -l total 8 -rwxrw-r-- 1 ustc ustc 40 Feb 3 22:37 a_file drwxrwxr-x 2 ustc ustc 4096 Feb 3 22:38 a_folder ······ 第一列的字符串从左到右意义分别是: 文件类型(一位) 文件所属用户的权限(三位) 文件所属用户组的权限(三位) 其他人的权限(三位) 对于每个权限,第一位 r 代表读取 (Read),第二位 w 代表写入 (Write),第三位 x 代表执行 (Execute),- 代表没有对应的权限。 第三、四列为文件所属用户和用户组。 例如,上面的文件 a_file 为普通文件 (-),所属用户权限为 rwx,所属用户组权限为 rw-,其他人的权限为 r--,文件所属用户和用户组均为 ustc。 文件类型对应的字符有: 普通文件: -,表示常规文件,存储文本、二进制数据或其他内容的常规文件类型。 目录: d,表示目录,用于组织文件和其他目录的容器。 符号链接: l,表示符号链接(软链接),是指向另一个文件或目录的引用。 设备文件: 字符设备文件: c,表示字符设备文件,用于表示字符设备(如键盘、鼠标等)。 块设备文件: b,表示块设备文件,用于表示块设备(如硬盘、闪存驱动器等)。 管道: p,表示管道(命名管道或 FIFO),用于在进程之间进行通信的特殊文件类型。 套接字: s,表示套接字,用于在网络上进行进程间通信的特殊文件类型。 执行权限的意义\r读取和写入权限是很容易理解的。但是执行权限是什么意思?对于一个文件来说,拥有执行权限,它就可以被操作系统作为程序代码执行。如果某个程序文件没有执行权限,你仍然可以查看这个程序文件本身,修改它的内容,但是无法执行它。\r而对于目录来说,拥有执行权限,你就可以访问这个目录下的文件的内容。以下是一个例子: $ ls -l total 8 -rwxrw-r-- 1 ustc ustc 40 Feb 3 22:37 a_file drw-rw-r-- 2 ustc ustc 4096 Feb 3 22:38 a_folder $ (与上面不同,我们去掉了 a_folder 的执行权限) $ cd a_folder -bash: cd: a_folder/: Permission denied $ (失败了,这说明,如果没有执行权限,我们无法进入这个目录) $ ls a_folder ls: cannot access 'a_folder/test': Permission denied test $ (列出了这个目录中的文件列表,但是因为没有执行权限,我们没有办法访问到里面的文件 test) $ cat a_folder/test cat: a_folder/test: Permission denied $ cp a_folder/test test cp: cannot stat 'a_folder/test': Permission denied $ mv a_folder/test a_folder/test2 mv: failed to access 'a_folder/test2': Permission denied $ touch a_folder/test2 touch: cannot touch 'a_folder/test2': Permission denied $ rm a_folder/test rm: cannot remove 'a_folder/test': Permission denied $ (可以看到,即使我们有写入权限,在此目录中进行添加、删除、重命名的操作仍然是不行的) 为了更好地理解目录权限的含义,可以把目录视为一个「文件」来看待,这个文件包含了目录中下一层的文件列表—— 「读取」 对应读取文件列表的权限,「写入」 对应修改文件列表(添加、删除、重命名文件)的权限,「执行」 对应实际去访问列表中文件、以及使用 cd 切换当前目录到此目录的权限。当没有执行权无法访问文件时,就无法修改文件。 使用 chmod 命令可修改文件/目录权限。命令格式如下: chmod [who] [operator] [permissions] FileName who:指定权限变更对象。可以是以下之一: u:文件拥有者(user) g:文件所属组(group) o:其他用户(others) a:所有用户(all,也是默认参数) operator:指定权限变更操作。可以是以下之一: +:添加权限 -:移除权限 =:设置权限 permissions:指定要添加、移除或设置的权限。可以是以下之一: r:读权限 w:写权限 x:执行权限 fileName:指定要更改权限的文件或目录。 当我们执行某个文件时,出现提示 Permission denied ,大多数情况下,说明这个文件没有执行权,可以使用 chmod 加上。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:6:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"文件系统层次结构\rLinux 下文件系统的结构和 Windows 的很不一样。在 Windows 中,分区以盘符的形式来标识(如「C 盘」、「D 盘」),各个分区的分界线是很明确的。在系统所在的分区(一般为 C 盘)中,存储着程序文件 (Program Files),系统运行需要的文件 (Windows),用户文件 (Users) 等。这种组织形式源于 DOS 和早期的 Windows,并一直传承下来。 而 UNIX 系列采用了一种不一样的思路组织文件:整个系统的文件都从 /(根目录)开始,像一棵树一样,类似于下图。 其他的分区以挂载 (mount) 的形式「挂」在了这棵树上,如图中的 /mnt/windows_disk/。 那么在根目录下的这些目录各自有什么含义呢?这就由文件系统层次结构标准 (FHS, Filesystem Hierarchy Standard) 来定义了。这个标准定义了 Linux 发行版的标准目录结构。大部分的 Linux 发行版遵循此标准,或由此标准做了细小的调整。以下进行一个简要的介绍,也可以在官网查看标准的具体内容。 当然,实际情况不一定会和以下介绍的内容完全一致。可以使用 man hier 和 man file-hierarchy 查看你的系统中关于文件系统层次结构的文档。 文档。 目录 功能 /bin 存储必须的程序文件,对所有用户都可用 /boot 存储在启动系统时需要的文件 /dev 存储设备文件(设备文件就是计算机设备抽象成文件的形式) /etc 存储系统和程序的配置文件 /home 用户的家目录。存储用户自己的信息。 /lib 存放系统运行必须的程序库文件 /media 和 /mnt 这两个目录都用于挂载其他的文件系统。/media 用于可移除的文件系统(如光盘),而 /mnt 用于临时使用 /opt 存放额外的程序包。一般将一些大型的、商业的应用程序放置在这个目录 /root root 用户的家目录 /run 系统运行时的数据。在每次启动时,里面的数据都会被删除 /sbin 存储用于系统管理,以及仅允许 root 用户使用的程序。如 fsck(文件系统修复程序)、reboot(重启系统)等 /srv 存储网络服务的数据 /tmp 临时目录,所有用户都可使用 /usr 大多数软件都会安装在此处。其下有一些目录与 / 下的结构相似,如:/usr/bin 、/usr/lib 等 /usr/include 存储系统通用的 C 头文件。当然,里面会有你非常熟悉的头文件,如 stdio.h /usr/local 存储系统管理员自己安装的程序,这些文件不受系统的软件管理机制(如 apt)控制。/usr/local 里面的层次结构和 /usr 相似 /usr/share 存储程序的数据文件(如 man 文档、GUI 程序使用的图片等) /var 存储会发生变化的程序相关文件 ","date":"2024-08-15","objectID":"/posts/3d623e4/:6:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"网络、文本处理工具\r","date":"2024-08-15","objectID":"/posts/3d623e4/:7:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"I/O重定向与管道\r重定向\r一般情况下命令从标准输入(stdin)读取输入,并输出到标准输出(stdout),默认情况下两者都是你的终端。使用重定向可以让命令从文件读取输入/输出到文件。 重定向输出使用 \u003e 或 \u003e\u003e 符号,下面是以 echo 为例的重定向输出: $ echo \"Hello Linux!\" \u003e output_file # 将输出写入到文件(覆盖原有内容) $ cat output_file Hello Linux! $ echo \"rewrite it\" \u003e output_file $ cat output_file # 可以看到原来的 Hello Linux! 被覆盖了。 rewrite it $ echo \"append it\" \u003e\u003e output_file # 将输出追加到文件(不会覆盖原有内容) $ cat output_file rewrite it append it 无论是 \u003e 还是 \u003e\u003e,当输出文件不存在时都会创建该文件。 重定向输入使用符号 \u003c: command \u003c inputfile command \u003c inputfile \u003e outputfile 除了 stdin 和 stdout 还有标准错误(stderr),他们的编号分别是 0、1、2(这些数字都是文件描述符)。stderr 可以用 2\u003e 重定向(注意数字 2 和 \u003e 之间没有空格)。 使用 2\u003e\u00261 可以将 stderr 合并到 stdout。 \u0026 符号用于表示后面跟的是一个文件描述符,如果不使用 \u0026 符号则表示 1 是一个文件名 管道\r管道(pipe),使用操作符 |,作用为将符号左边的命令的 stdout 作为符号右边的命令的 stdin。管道不会处理 stderr。 管道是类 UNIX 操作系统中非常强大的工具。通过管道,我们可以将实现各类小功能的程序拼接起来干大事。 $ ls / | grep bin # 筛选 ls / 输出中所有包含 bin 字符串的行 bin sbin ","date":"2024-08-15","objectID":"/posts/3d623e4/:7:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"网络下载\r在 Windows 下,很多人下载文件时会使用「迅雷」、「IDM」之类的软件来实现下载。那么在 Linux 环境下呢?在终端下,没有可视化软件提供点击下载。即使有桌面环境,有 Firefox 可以很方便地下载文件,硬件资源也会被很多不必要的服务浪费。通过以下内容讲述的 wget和 curl 工具,我们可以 Linux 上进行轻量的下载活动。 使用 wget\rwget 是强力方便的下载工具,可以通过 HTTP 和 FTP 协议从因特网中检索并获取文件。 命令为:wget [opt1]... [url1]... 常用的选项: 选项 含义 -i, –input-file=文件 下载本地或外部文件中的 URL -O, –output-document=文件 将输出写入文件 -b, –background 在后台运行 wget -d, –debug 调试模式,打印出 wget 运行时的调试信息 例如批量下载 filelist.txt 中的链接:wget -i filelist.txt 使用 curl\rcurl 是一个利用 URL 语法在命令行下工作的文件传输工具,其中 c 意为 client。虽然 cURL 和 Wget 基础功能有诸多重叠,如下载。但 cURL 由于可自定义各种请求参数,所以在模拟 web 请求方面更擅长;wget 由于支持 FTP 协议和递归遍历,所以在下载文件方面更擅长。 curl 的使用与 wget 相似,使用 curl -h 可详细了解其用法 常用的选项: 选项 含义 -o 把远程下载的数据保存到文件中,需要指定文件名 -O 把远程下载的数据保存到文件中,直接使用 URL 中默认的文件名 -I 只展示响应头内容 ","date":"2024-08-15","objectID":"/posts/3d623e4/:7:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"文本处理\r在进行文本处理时,我们有一些常见的需求: 获取文本的行数、字数 比较两段文本的不同之处 查看文本的开头几行和最后几行 在文本中查找字符串 在文本中替换字符串 下面介绍如何在 shell 中做到这些事情。 文本统计\rwc 是文本统计的常用工具,它可以输出文本的行数、单词数与字符(字节)数。 $ wc file 427 2768 20131 file PS: wc 无法准确统计中文文本 文本比较\rdiff 工具用于比较两个文件的不同,并列出差异。 diff file1.txt file2.txt #比较两个文件 diff -r dir1 dir2 #比较两个目录 diff -w file1.txt file2.txt #忽略空白字符(空格、制表符)引起的差异 diff -u file1.txt file2.txt #显示差异处的三个上下文行,在中间显示差异行 diff -i file1.txt file2.txt #忽略大小写 文本开头与结尾\rhead 和 tail 分别用来显示开头和结尾指定数量的文字。 以 head 为例,这里给出共同的用法: 不加参数的时候默认显示前 10 行 -n \u003cNUM\u003e 指定行数,可简化为 \u003cNUM\u003e -c \u003cNUM\u003e 指定字节数 head file # 显示 file 前 10 行 head -n 25 file # 显示 file 前 25 行 head -25 file # 显示 file 前 25 行 head -c 20 file # 显示 file 前 20 个字符 tail -10 file # 显示 file 最后 10 行 除此以外,tail 还有一个非常实用的参数 -f:当文件末尾内容增长时,持续输出末尾增加的内容。这个参数常用于「动态显示」 log 文件的更新(试一试tail -f /var/log/syslog)。 文本查找\rgrep 命令可以查找文本中的字符串: grep 'hello' file # 查找文件 file 中包含 hello 的行 ls | grep 'file' # 查找当前目录下文件名包含 file 的文件 grep -i 'Systemd' file # 查找文件 file 中包含 Systemd 的行(忽略大小写) grep -R 'hello' . # 递归查找当前目录下内容包含 hello 的文件 注: grep事实上是非常强大的查找工具,将在介绍正则表达式语法之后进一步介绍 grep。 文本替换\rsed 命令可以替换文本中的字符串:sed [opt]... 'command' file 常用参数: -e \u003ccommand\u003e:指定多个编辑命令。 -n:禁止默认输出,只输出经过编辑的行。 -i:直接编辑文件,而不是将结果输出到标准输出。 -r:使用扩展正则表达式(需要 E 选项)。 -E:使用扩展正则表达式。 常用命令: s/regexp/replacement/:替换匹配到的文本。 d:删除匹配到的行。 p:打印匹配到的行。 i\\:在指定行之前插入文本。 a\\:在指定行之后追加文本。 示例: 替换文件中的文本: sed 's/old_text/new_text/' file.txt 删除文件中匹配到的行: sed '/pattern/d' file.txt 插入文本到指定行之前: sed '/pattern/i\\inserted_text' file.txt 使用多个编辑命令: sed -e 's/old_text/new_text/' -e '/pattern/d' file.txt 使用扩展正则表达式进行匹配和替换: sed -E 's/regexp/replacement/' file.txt 注:sed 事实上也是非常强大的查找工具,将在后面进一步介绍 ","date":"2024-08-15","objectID":"/posts/3d623e4/:7:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"Shell高级文本处理与正则表达式\r","date":"2024-08-15","objectID":"/posts/3d623e4/:8:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"其他shell文本处理工具\rsort\rsort 用于文本的行排序。默认排序方式是升序,按每行的字典序排序。 一些基本用法: r 降序(从大到小)排序 u 去除重复行 o [file] 指定输出文件 n 用于数值排序,否则“15”会排在“2”前 $ echo -e \"snake\\nfox\\nfish\\ncat\\nfish\\ndog\" \u003e animals $ sort animals cat dog fish fish fox snake $ sort -r animals snake fox fish fish dog cat $ sort -u animals cat dog fish fox snake $ sort -u animals -o animals $ cat animals cat dog fish fox snake $ echo -e \"1\\n2\\n15\\n3\\n4\" \u003e numbers #-e参数是允许解释转义字符 $ sort numbers 1 15 2 3 4 $ sort -n numbers 1 2 3 4 15 uniq\runiq 也可以用来排除重复的行,但是仅对连续的重复行生效。 通常会和 sort 一起使用: sort animals | uniq 只是去重排序明明可以用 sort -u ,uniq 工具是否多余了呢?实际上 uniq 还有其他用途。 uniq -d 可以用于仅输出重复行: sort animals | uniq -d uniq -c 可以用于统计各行重复次数: sort animals | uniq -c ","date":"2024-08-15","objectID":"/posts/3d623e4/:8:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"正则表达式\r正则表达式(regular expression)描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。 特殊字符\r特殊字符 描述 [] 方括号表达式,表示匹配的字符集合,例如 [0-9]、[abcde] () 标记子表达式起止位置 * 匹配前面的子表达式零或多次 + 匹配前面的子表达式一或多次 ? 匹配前面的子表达式零或一次 \\ 转义字符,除了常用转义外,还有:\\b 匹配单词边界;\\B 匹配非单词边界等 . 匹配除 \\n(换行)外的任意单个字符 {} 标记限定符表达式的起止。例如 {n} 表示匹配前一子表达式 n 次;{n,} 匹配至少 n 次;{n,m} 匹配 n 至 m 次 | 表明前后两项二选一 $ 匹配字符串的结尾 ^ 匹配字符串的开头,在方括号表达式中表示不接受该方括号表达式中的字符集合 以上特殊字符,若是想要匹配特殊字符本身,需要在之前加上转义字符 \\。 简单示例\r匹配正整数: [1-9][0-9]* 匹配仅由 26 个英文字母组成的字符串: ^[A-Za-z]+$ 匹配 Chapter 1-99 或 Section 1-99: ^(Chapter|Section) [1-9][0-9]{0,1}$ 匹配“ter”结尾的单词: ter\\b ","date":"2024-08-15","objectID":"/posts/3d623e4/:8:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"常用 Shell 文本处理工具(正则)\rgrep\rgrep 全称 Global Regular Expression Print,是一个强大的文本搜索工具,可以在一个或多个文件中搜索指定 pattern 并显示相关行。 grep 默认使用 BRE,要使用 ERE 可以使用 grep -E 或 egrep。 命令格式:grep [option] pattern file 一些用法: n:显示匹配到内容的行号 v:显示不被匹配到的行 i:忽略字符大小写 ls /bin | grep -n \"^man$\" # 搜索内容仅含 man 的行,并且显示行号 ls /bin | grep -v \"[a-z]\\|[0-9]\" # 搜索不含小写字母和数字的行 ls /bin | grep -iv \"[A-Z]\\|[0-9]\" # 搜索不含字母和数字的行 sed\rsed 全称 Stream EDitor,即流编辑器,可以方便地对文件的内容进行逐行处理。 sed 默认使用 BRE,要使用 ERE 可以 sed -E。 命令格式: sed [OPTIONS] 'command' file(s) sed [OPTIONS] -f scriptfile file(s) 此处的 command 和 scriptfile 中的命令均指的是 sed 命令。 常见 sed 命令: s 替换 d 删除 c 选定行改成新文本 a 当前行下插入文本 i 当前行上插入文本 $ echo -e \"seD\\nIS\\ngOod\" \u003e sed_demo $ cat sed_demo seD IS gOod $ sed \"2d\" sed_demo # 不显示第二行 seD gOod $ sed \"s/[a-z]/~/g\" sed_demo # 替换所有小写字母为 ~ ~~D IS ~O~~ $ sed \"3cpErfeCt\" sed_demo # 选定第三行,改成 pErfeCt seD IS pErfeCt awk\rawk 是一种用于处理文本的编程语言工具,名字来源于三个作者的首字母。相比 sed,awk 可以在逐行处理的基础上,针对列进行处理。默认的列分隔符号是空格,其他分隔符可以自行指定。 awk 使用 ERE。 命令格式:awk [options] 'pattern {action}' [file] awk 逐行处理文本,对符合的 patthern 执行 action。需要注意的是,awk 使用单引号时可以直接用 $,使用双引号则要用 \\$。 一些示例: $ cat awk_demo Beth 4.00 0 Dan 3.75 0 kathy 4.00 10 Mark 5.00 20 Mary 5.50 22 Susie 4.25 18 $ # 选择第三列值大于 0 的行,对每一行输出第一列的值和第二第三列的乘积 $ awk '$3 \u003e0 { print $1, $2 * $3 }' awk_demo kathy 40 Mark 100 Mary 121 Susie 76.5 示例中 $1,$2,$3 分别指代本行的第 1、2、3 列。特别地,$0 指代本行。 awk 语言是「图灵完全」的,这意味着理论上它可以做到和其他语言一样的事情。这里我们不仅可以对每行进行操作,还可以定义变量,将前面处理的状态保存下来,以下是一个求和的例子: $ awk 'BEGIN { sum = 0 } { sum += $2 * $3 } END { print sum }' awk_demo 337.5 关于 awk,有一本知名的书籍《The AWK Programming Language》(中文翻译) ","date":"2024-08-15","objectID":"/posts/3d623e4/:8:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"Ubuntu下推荐的软件\r","date":"2024-08-15","objectID":"/posts/3d623e4/:9:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"Zsh\r使用 Linux 系统时,不可避免接触终端命令行操作,但是默认的终端黑底白字。有什么办法可以既美化终端,又提高工作效率呢?下面介绍一个美化终端的方法,更换 Shell。 在此之前,使用命令:echo $SHELL 来检查目前我们正在使用的是什么 Shell,Ubuntu默认使用 Bash。在这里推荐一个更加强大的 Shell 工具——Z shell(Zsh)。 Zsh使用步骤如下: 使用命令 sudo apt install zsh 来安装 zsh 使用命令 chsh -s /bin/zsh 将zsh设置为默认 shell 重启后打开终端就会发现 shell 变为了 zsh Zsh 虽然好用,但直接用起来还是比较麻烦,不过幸运的是,已经有大神给我们配置好了一个很棒的框架:oh-my-zsh,专门为 Zsh 打造,使用以下命令即可安装: sh -c \"$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)\" 安装好oh-my-zsh就可以配置插件一些好用的插件。在 ~/.zshrc 文件中,中间靠下有plugins这句话,这里可以看到我们目前启用的插件,在里面输入已安装且想启用的插件名,插件之间用空格间隔。 git 无需配置,默认已开启,使我们可以方便的使用git命令的缩写 zsh-syntax-highlighting 高亮语法,输入正确语法会显示绿色,错误的会显示红色,使得我们无需运行该命令即可知道此命令语法是否正确。使用以下命令安装: git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting zsh-autosuggestions 自动补全,只需输入部分命令即可根据之前输入过的命令提示,按右键即可补全,使用以下命令安装: git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions zsh的一些使用技巧\r在zsh中按一次tab键就会列出所有的选项和帮助说明,连按两次tab键就可以开始在补全的列表直接选择参数(按tab键或方向键移动),回车选中参数。 在当前目录下输入 .. 或 ... ,或直接输入当前目录名都可以跳转,甚至不再需要输入 cd 命令了。 在你知道路径的情况下,比如 /usr/local/bin 你可以输入cd /u/l/b 然后按tab补全,实现快速输入 输入 d,即可列出你在这个会话里访问的目录列表,输入列表前的序号,即可直接跳转。 在 .zshrc 中添加 setopt HIST_IGNORE_DUPS 可以消除重复记录,也可以利用sort -t \";\" -k 2 -u ~/.zsh_history | sort -o ~/.zsh_history手动清除 ","date":"2024-08-15","objectID":"/posts/3d623e4/:9:1","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"unar 在 Linux 上的 tar 一般只支持 gzip、bzip、xz 和 lzip 几种压缩算法,如果需要解压 Windows 上更为常见的 7z、zip 和 rar 等,则需要寻求替代软件,推荐使用 unar 软件。 Ubuntu 上直接使用 apt 安装即可:sudo apt install unar 安装完成后会得到两个命令:unar 和 lsar,分别用来解压存档文件以及浏览存档文件内容。 lsar 命令详解: lsar archive.zip # 浏览存档文件内容 lsar -l archive.zip # 查看详细信息 lsar -L archive.zip # 查看特别详细的信息 unar 命令详解: # 直接将文件解压到当前目录下 unar test.zip # -o 参数指定解压结果保存的位置 unar test.zip -o /home/dir/ # -e 参数指定编码 unar -e GBK test.zip # -p 参数指定解压密码 unar -p 123456 test.zip # 解决压缩包乱码问题 # 1.指定查看时的编码方式,如果能够正常显示,则直接使用该编码方式解压 lsar -e GB18030 test.zip # 2. 解压 unar -e GB18030 test.zip ","date":"2024-08-15","objectID":"/posts/3d623e4/:9:2","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"htop htop 是 Linux 系统中的一个互动的进程查看器,与 Linux 传统的 top 相比, htop 更加人性化。 它可让用户交互式操作,支持颜色主题, 可横向或纵向滚动浏览进程列表,并支持鼠标操作。 使用命令 sudo apt install htop 安装,直接在命令行输入 htop 即可启动软件,htop使用教程。 PS: F10按键被窗口占用时,可在 窗口设置→配置文件首选项→取消启用菜单快捷键,即可将F10从终端窗口解绑。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:9:3","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"silversearcher-ag\rsilversearcher-ag(通常称为 ag)是一个代码搜索工具,它旨在比传统的 grep 更快、更高效。ag 擅长在大型代码库中快速进行搜索,它的速度优势主要得益于并行搜索以及自动忽略.git和其他版本控制系统文件夹和文件。 安装命令为:sudo apt install silversearcher-ag ag 的基本语法非常简单:ag [options] \u003csearch-pattern\u003e [path] path可选,默认为当前目录 常用选项\r-i:忽略大小写。 -v:只打印不匹配的行。 -w:仅匹配整个单词。 -A NUM:打印匹配行之后 NUM 行。 -B NUM:打印匹配行之前 NUM 行。 -C NUM:打印匹配行前后各 NUM 行。 -l:只打印包含匹配项的文件名。 -L:只打印不包含匹配项的文件名。 -g:只打印匹配 PATTERN 的文件名。 -G:只从满足正则表达式的文件中搜索。 -u:搜索所有文件,忽略 .gitignore 等忽略文件。 -z:搜索压缩文件的内容。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:9:4","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"tmux tmux是一款优秀的终端复用软件,拥有丝滑分屏、保护现场、会话共享等主要功能。一个 tmux 会话可以包括多个窗口,一个窗口又可以包括多个面板,窗口下的面板,都处于同一界面下,这些面板适合运行相关性高的任务,以便同时观察到它们的运行情况。 tmux 由会话(session),窗口(window),面板(pane)组织起每个 shell 的输入框。会话用于区分不同的工作;窗口是会话中以显示屏为单位的不同的页;而面板则是一个窗口上被白线分割的不同区域。熟练掌握会话,窗口,面板之间的切换,可以极大提高使用效率。 安装命令为:sudo apt install tmux 新建一个 tmux 会话\rtmux # 新建一个无名称的会话 tmux new -s demo # 新建一个名称为demo的会话 PS: 为了便于管理,建议指定会话名称 关闭会话\r会话的使命完成后,一定要关闭,命令如下: tmux kill-session -t demo #关闭demo会话 tmux kill-server #关闭全部会话 查看所有会话\rtmux ls 断开当前会话\r会话中操作了一段时间,我希望断开会话同时下次还能接着用,怎么做?此时可以使用detach命令。 tmux detach 进入之前的会话\r断开会话后,想要接着上次留下的现场继续工作,就要使用到tmux的attach命令了,语法为tmux attach-session -t session-name,可简写为tmux a -t session-name 或 tmux a。通常我们使用如下两种方式之一即可: tmux a #默认进入第一个会话 tmux a -t demo #进入名称为demo的会话 常用快捷键\rtmux的所有指令,都包含同一个前缀,默认为Ctrl+b,输入完前缀过后,控制台激活,命令按键才能生效。即按快捷键之前需要先按完 Ctrl+b 键激活控制台。 快捷键(需先按 Ctrl+B) 功能 % 左右分屏 \" 上下分屏 ↑ ↓ ← → 焦点切换为上、下、左、右侧pane,正在交互的pane被绿色框选中。 d (detach) 从tmux中脱离,回到命令行界面 z (zoom) 将pane暂时全屏,再按一次恢复原状 c 新建窗口 , 为窗口命名 s 列出所有 session 定制 tmux\r说实在的,tmux 默认的快捷键的确有些苦手,比如 Ctrl + B 这个对手指相当不友好的长距快捷键就应当改进。而且你可能会想,横竖分屏居然需要 % 和 \",为什么不使用更为直观的 - 和 | 呢?如果要对这些特性进行修改,可以在家目录下创建配置文件 .tmux.conf 达到所需目的。我的配置文件内容如下: set -g prefix C-a # 设置前缀按键 Ctrl + A。 unbind C-b # 取消 Ctrl + B 快捷键。 bind C-a send-prefix # 第二次按下 Ctrl + A 为向 shell 发送 Ctrl + A。 # (Shell 中 Ctrl + A 表示光标移动到最前端)。 set -g mouse on # 启动鼠标操作模式,随后可以鼠标拖动边界进行面板大小调整。 unbind -n MouseDrag1Pane unbind -Tcopy-mode MouseDrag1Pane unbind '\"' # 使用 - 代表横向分割。 bind - splitw -v -c '#{pane_current_path}' # -v 代表新建的面板使用全部的宽度,效果即为横向分割(或者说,切割得到的新的面板在竖直方向 (vertical) 排列)。 unbind % # 使用 \\ 代表纵向分割(因为我不想按 Shift)。 bind \\\\ splitw -h -c '#{pane_current_path}' # -h 则代表新建的面板使用全部的高度,效果即为纵向分割(切割得到的新的面板在水平方向 (horizontal) 排列)。 setw -g mode-keys vi # 设置 copy-mode 快捷键模式为 vi。 以 . 开头的文件为隐藏文件,需要使用 ls -a 命令查看。所以保存之后你可能不会直接在图形界面看到,不用担心。 保存后,使用 tmux source ~/.tmux.conf 重新载入配置,或者 tmux kill-server 后重启 tmux。 关于 tmux 更详细的介绍,可以参见这篇博客。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:9:5","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"gparted\rgparted 是一款功能强大的开源磁盘分区工具,广泛用于 Linux 发行版。它支持多种文件系统和存储设备,提供直观的图形界面来帮助用户创建、移动、调整大小、格式化和删除磁盘分区。 安装命令为:sudo apt install gparted,输入命令 sudo gparted 即可进入图形化页面。 使用 gparted 调整分区大小\r打开 gparted。在图形界面中,你会看到当前磁盘的分区布局。 选择需要调整大小的分区,右键点击并选择 “Resize/Move”。 拖动分区边界来调整大小,或者在对话框中输入新的分区大小。 调整后,点击 “Resize/Move” 应用更改。 最后,点击工具栏上的 “Apply” 按钮执行所有挂起的操作 。 创建新分区\r在 gparted 中选择未分配的空间。 右键点击并选择 “New”。 在弹出的对话框中设置分区大小、文件系统类型、分区名称和标签。 点击 “Add” 创建新分区。 同样,不要忘记点击 “Apply” 来应用更改 。 删除分区\r选择要删除的分区。 右键点击并选择 “Delete”。 确认删除操作。 点击 “Apply” 执行删除。 注意事项\r在对磁盘分区进行操作之前,建议备份重要数据,以防数据丢失。 在使用 gparted 进行分区操作时,确保待操作的分区没有挂载,否则可能无法进行操作。 如果使用的是虚拟机,扩展虚拟硬盘的大小需要先在虚拟机设置中扩大硬盘大小。 ","date":"2024-08-15","objectID":"/posts/3d623e4/:9:6","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["Linux"],"content":"参考资料\rhttps://101.lug.ustc.edu.cn/ ","date":"2024-08-15","objectID":"/posts/3d623e4/:10:0","tags":["笔记"],"title":"Linux学习笔记","uri":"/posts/3d623e4/"},{"categories":["VsCode"],"content":"Better C++ Syntax\r该插件主要作用是提供 C++ 语法高亮 ","date":"2024-08-12","objectID":"/posts/c0ef113/:1:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Bookmarks\r该插件主要作用是允许用户在代码中添加书签,以便快速跳转到这些特定位置。这个插件对于需要在大型项目中导航和跟踪重要代码段的开发者来说非常有用。 该插件常用快捷键为: 添加书签:Ctrl+Alt+K 删除书签:将光标放在带有书签的行上,然后使用 Ctrl+Alt+J 切换书签:Ctrl+Alt+Q可以在书签之间切换,如果当前行有书签,则会删除它。 跳转到下一书签:Ctrl+Alt+L 跳转到上一书签:Shift+Ctrl+Alt+L ","date":"2024-08-12","objectID":"/posts/c0ef113/:2:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"C/C++\r该插件主要作用是提供了一系列的工具和功能,例如代码分析功能、代码格式化功能、代码提示等。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:3:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"CMake\r该插件主要作用是CMake语法高亮、CMake代码自动补全。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:4:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"CMake Tools\r该插件主要作用是提供各种CMake编译相关的小工具,包括在底部状态栏显示一些快捷工具。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:5:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"cmake-format\r该插件的主要作用是格式化 CMakeLists.txt 文件,使其保持一致和可读性。安装此插件前,需要先安装cmake-format工具。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:6:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Code Runner\r该插件的主要作用是运行选定的代码片段。 该插件常用快捷键为: 运行选定代码 Ctrl+Alt+N 或 直接点击右上角的三角形 或 点击右键菜单中的 Run Code 停止正在运行的代码 Ctrl+Alt+M 或 点击右键菜单中的 Stop Run Code 插件详细说明地址 ","date":"2024-08-12","objectID":"/posts/c0ef113/:7:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Diff\r该插件的主要作用是比较两个文件的不同之处,直接在资源管理窗口中选择两个要比较的文件,将会直接显示比较结果。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:8:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Error Lens\r该插件的主要作用是将代码中存在的问题突出显示(包括错误、警告和语法问题),它不仅在代码行尾显示问题,而且会在整行进行高亮,使得诊断信息更加明显。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:9:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"GitLens\r该插件的主要作用是可以更方便地在VsCode中进行Git相关操作,该插件的使用可看此视频了解。 PS: 建议能够熟练使用Git命令后再使用此插件,Git的学习可看此教程。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:10:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"GitHub Copilot\r该插件的主要作用是辅助编码。该插件是GitHub的AI编码工具,能根据注释、函数名、函数参数编写代码。该插件的详细介绍可看此文章。 PS: 该插件需要进行GitHub学生认证才能免费使用,可参考此教程。 ","date":"2024-08-12","objectID":"/posts/c0ef113/:11:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Include Autocomplete\r该插件的主要作用是提供编写 C++ #include 语句时自动补全功能 ","date":"2024-08-12","objectID":"/posts/c0ef113/:12:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Markdown All in One 和 Markdown Preview Enhanced\r这两个插件都是用于帮助编写Markdown文件 ","date":"2024-08-12","objectID":"/posts/c0ef113/:13:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"SVG Viewer\r该插件的主要作用是在VsCode中编辑和预览 SVG 文件 ","date":"2024-08-12","objectID":"/posts/c0ef113/:14:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Todo Tree\r该插件的主要作用是用于展示工作区中所有的待办事项,点击对应事项后可以跳转到对应位置 主要有以下几种注释格式: 该插件的配置代码如下,直接粘贴进 setting.json 文件中即可。使用 Ctrl + , 命令,再点击右上角的打开设置,即可打开 setting.json 文件。 \"todo-tree.tree.showScanModeButton\": false, \"todo-tree.filtering.excludeGlobs\": [\"**/node_modules\", \"*.xml\", \"*.XML\"], \"todo-tree.filtering.ignoreGitSubmodules\": true, \"todohighlight.keywords\": [ ], \"todo-tree.tree.showCountsInTree\": true, \"todohighlight.keywordsPattern\": \"TODO:|FIXME:|NOTE:|\\\\(([^)]+)\\\\)\", \"todohighlight.defaultStyle\": { }, \"todohighlight.isEnable\": false, \"todo-tree.highlights.customHighlight\": { \"BUG\": { \"icon\": \"bug\", \"foreground\": \"#F56C6C\", \"type\": \"line\" }, \"FIXME\": { \"icon\": \"flame\", \"foreground\": \"#FF9800\", \"type\":\"line\" }, \"TODO\":{ \"foreground\": \"#FFEB38\", \"type\":\"line\" }, \"NOTE\":{ \"icon\": \"note\", \"foreground\": \"#67C23A\", \"type\":\"line\" }, \"INFO\":{ \"icon\": \"info\", \"foreground\": \"#909399\", \"type\":\"line\" }, \"TAG\":{ \"icon\": \"tag\", \"foreground\": \"#409EFF\", \"type\":\"line\" }, \"HACK\":{ \"icon\": \"versions\", \"foreground\": \"#E040FB\", \"type\":\"line\" }, \"XXX\":{ \"icon\": \"unverified\", \"foreground\": \"#E91E63\", \"type\":\"line\" } }, \"todo-tree.general.tags\": [ \"BUG\", \"HACK\", \"FIXME\", \"TODO\", \"INFO\", \"NOTE\", \"TAG\", \"XXX\" ], \"todo-tree.general.statusBar\": \"total\", ","date":"2024-08-12","objectID":"/posts/c0ef113/:15:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Doxygen Documentation Generator\r该插件的主要作用是生成Doxygen能够读取的注释风格,配合Doxygen软件使用,可自动生成代码的说明文档。 默认使用方法为:在要生成注释的位置输入 /**后直接回车即可。 PS: 一般使用默认格式即可,如果要修改生成注释风格模板可直接在设置(Ctrl + ,即可打开)➡扩展➡Doxygen Documentation Generator 里自行调式 ","date":"2024-08-12","objectID":"/posts/c0ef113/:16:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["VsCode"],"content":"Remote-SSH\r该插件的主要作用是允许通过 SSH 连接到远程服务器,并在 VSCode 环境中无缝地进行远程开发。 相关学习资料: Remote-SSH的使用 Ubuntu下安装ssh服务 Remote-SSH的使用(视频) ","date":"2024-08-12","objectID":"/posts/c0ef113/:17:0","tags":["vscode插件"],"title":"VsCode推荐插件","uri":"/posts/c0ef113/"},{"categories":["C++"],"content":"C++学习笔记","date":"2024-08-07","objectID":"/posts/3b2b064/","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"前言\r由于笔者具备一定的C语言基础,所以在本文中对于C语言的语法不再会进行赘述,主要专注与C++语法。为了更好的学习C++,笔者找了一些题目来进行练习,所有题目与题解均可在此仓库找到。 本学习笔记适用于 「具备一定C语言基础,想要使用C++来编写代码」 的读者。 本文阅读指南: 在看过示例代码后,一定要编写一个自己的示例代码 或 自己动手重写一遍。 标红的注意内容一定要仔细看!!! 一定要做上面仓库中的题!!!一定要做上面仓库中的题!!!一定要做上面仓库中的题!!! 好的命名能够提高代码的可读性,本文从类章节开始采用以下命名规范: 局部变量名单词之间使用下划线隔开; 类的变量成员用下划线作为前缀如 _file_name; 类的函数名使用驼峰类型;如doSomething(); 类的成员存取使用 如get_file_name() set_file_name(); 类名是PASCAL风格,即首字母大写 如MyClass; 常量用k作为前缀后面是PASCAL风格如 kFileName; 全局变量用g作为前缀后面是PASCAL风格如 gFileName; 宏定义全大写,中间用下划线隔开 FILE_NAME。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:1:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"从C到C++\r本章节主要介绍一些C++的比较重要特性,例如如何申请和释放内存空间、bool和string类型、命名空间等。这些新特性可以给我们编程提供遍历,提高开发效率。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"布尔类型(bool)\r在C语言中,没有\"真\"与\"假\"的数据类型,我们通常使用一个整形变量的值来表示真假,其值为 1 表示真,为 0 表示假。以下为一个例子: int IsOddNum(int n) //判断一个数是否是奇数 { int flag; if(n % 2 == 0) flag = 0; else flag = 1; return flag; } 然而,这种做法有几个缺点: 可读性差: 使用整数来表示布尔值可能会让代码的可读性降低,因为读者需要记住0和非0值的含义。 存在类型安全问题: 整数可以进行算术运算,这可能导致意外的类型转换和错误。 语义不明确: 整数类型的使用没有明确表达出变量的布尔语义。 所以,C++提供了 bool 类型来表示真假,该类型的变量只有 true 和 false 两种取值。那么上面的例子则可改为: bool IsOddNum(int n) { bool flag; if(n % 2 == 0) flag = true; else flag = false; return flag; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"输入输出\r在C++中,使用输入输出时,需先包含头文件 iostream。使用 cin从标准输入设备(通常是键盘)读取数据,使用 cout 向标准输出设备(通常是屏幕)发送数据,使用 cerr向标准错误设备(通常是屏幕)发送错误信息。 cin要配合 \u003e\u003e运算符使用,cout和cerr要配合\u003c\u003c运算符使用。 cerr 默认情况下是无缓冲的,这意味着发送到 cerr 的输出会立即显示在标准错误输出(通常是控制台),在程序发生严重错误时,也能保证错误信息的及时显示。而 cout 的输出会被缓存直到缓冲区满或者遇到 endl 时才刷新输出。 PS:cin、cout、cerr 都是C++的内置对象,不是 C++ 中的关键字,其本质是函数调用,采用运算符重载来实现的(后面会讲解)。 一个简单示例如下: #include\u003ciostream\u003e using namespace std; //使用标准命名空间 int main() { int n; float a, b, c; cout \u003c\u003c \"Please enter an integer\" \u003c\u003c endl; //endl 等价于C语言中的 \\n 符号 cin \u003e\u003e n; cout \u003c\u003c \"The integer you entered is \" \u003c\u003c n \u003c\u003c endl; cout \u003c\u003c \"Please enter three floating point numbers\" \u003c\u003c endl; cin \u003e\u003e a \u003e\u003e b \u003e\u003e c; cout \u003c\u003c \"The three floating point numbers you entered are \" \u003c\u003c a \u003c\u003c \",\"\u003c\u003c b \u003c\u003c \",\" \u003c\u003c c \u003c\u003c endl; return 0; } cin 可以连续的从键盘读取数据,以空格、制表符、换行符作为分隔符(按下的回车键会被转换为换行符存入缓冲区),当 cin 遇到这些分隔符时,它会停止为当前变量读取数据。 cin停止读入数据的几种情况\r遇到空白字符: 默认情况下,cin 使用空白字符(如空格、制表符 \\t、换行符 \\n)作为字段分隔符。当 cin 遇到这些字符时,它会停止为当前变量读入数据。 输入与类型不匹配: 当尝试将输入的字符串转换为变量类型失败时,cin 会停止向该变量读入数据。例如,如果输入包含非数字字符而程序试图将其读入一个整数变量,cin 将停止并设置错误标志。 达到输入流的末尾: 如果输入来源(如键盘输入或文件)已经结束(EOF),cin 将停止读入数据。 手动清空输入缓冲区: 通过调用 cin.ignore() 方法,可以忽略输入缓冲区中的字符直到遇到指定的分隔符或者达到忽略的字符数上限。 使用 std::getline: 如果使用 std::getline(std::cin, str) 来读取一行文本,cin 会在遇到换行符之前读取所有字符,并将它们存储在提供的字符串变量中。遇到换行符后,cin 停止读入并丢弃换行符。 设置 cin 的错误状态: 如果 cin 遇到一个它无法解析的输入(例如,输入的数据类型不匹配),它会设置错误状态。如果错误状态被设置,cin 将停止进一步的输入操作,直到错误状态被清除。 流的同步操作: 在某些情况下,如果 cin 与 cout 同步(cin.tie() 返回 \u0026cout),cout 的刷新操作(如使用 std::endl 或 cout.flush())可能会影响 cin 的行为。 外部因素: 例如,如果从文件中读取,文件的实际结束或读取操作被外部程序或操作系统中断。 cerr的使用方法与cout一致,只不过其通常用于输出错误信息。想要更详细的学习这三个对象,可浏览此网站。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"命名空间\rC++语言引入命名空间这一概念主要是为了避免命名冲突,其关键字为 namespace。 例如,两个不同的库可能都有名为 log 的函数,如果不加以区分,就会产生冲突。命名空间允许每个库将 log 函数放在自己的命名空间中,如 myLib::log 和 yourLib::log,这样就可以同时使用这两个函数而不会发生冲突。代码如下: // myLib.h #ifndef MYLIB_H #define MYLIB_H namespace myLib { void log(const std::string\u0026 message); } #endif // MYLIB_H // myLib.cpp namespace myLib { void log(const std::string\u0026 message) { // 实现 myLib 库的日志功能 std::cout \u003c\u003c \"MyLib: \" \u003c\u003c message \u003c\u003c std::endl; } } /* ---------------------------------------分割线--------------------------------------- */ // yourLib.h #ifndef YOURLIB_H #define YOURLIB_H namespace yourLib { void log(const std::string\u0026 message); } #endif // YOURLIB_H // yourLib.cpp namespace yourLib { void log(const std::string\u0026 message) { // 实现 yourLib 库的日志功能 std::cout \u003c\u003c \"YourLib: \" \u003c\u003c message \u003c\u003c std::endl; } } 当你想要在主程序或其他代码中使用这两个库的 log 函数时,你可以按照 命名空间名称::Name 的格式来引用它们: #include \"myLib.h\" #include \"yourLib.h\" int main() { myLib::log(\"This is a log message from myLib.\"); yourLib::log(\"This is a log message from yourLib.\"); return 0; } 除了 使用域解析符::,还可以使用 using namespace 命名空间名称 的方式来引用它们: #include \"myLib.h\" #include \"yourLib.h\" using namespace myLib; int main() { log(\"This is a log message from myLib.\"); //使用的是myLib的log函数 yourLib::log(\"This is a log message from yourLib.\"); return 0; } PS: 在头文件中,应避免使用 using namespace XXX,因为头文件会被其他文件所包含,这就会导致XXX命名空间被多个文件包含。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"引用\r引用可以看做是「被引用对象的存储空间的别名」,在声明引用时,必须同时对其进行初始化。引用的声明方法和示例如下: // 格式为:类型 \u0026变量名 = 被引用对象; #include\u003ciostream\u003e using namespace std; int main() { int a = 7; int \u0026b = a; cout \u003c\u003c \"a value is: \" \u003c\u003c a \u003c\u003c endl; cout \u003c\u003c \"b value is: \" \u003c\u003c b \u003c\u003c endl; cout \u003c\u003c \"a addr = \" \u003c\u003c \u0026a \u003c\u003c \", b addr = \" \u003c\u003c \u0026b \u003c\u003c \".\" \u003c\u003c endl; return 0; } /** 运行结果如下: * a value is: 7 * b value is: 7 * a addr = 0x61fe14, b addr = 0x61fe14. */ 从这段程序中我们可以看出,变量 a 和 b 的地址相同,即地址为 0x61fe14 的存储空间拥有两个名字:a 和 b。变量 a 和 b 均可访问和修改该存储空间的存储的值。如果不想让引用变量修改值,可使用 const 关键字。 函数引用参数\r引用变量经常被用作函数的参数(尤其是参数为较大的结构体、对象时),这使得可以快速传递参数且能在被调用的函数中修改调用函数中的变量(与指针效果类似)。示例如下: #include\u003ciostream\u003e using namespace std; void swap(int \u0026a, int \u0026b); int main() { int num1 = 10; int num2 = 20; cout \u003c\u003c num1 \u003c\u003c \" \"\u003c\u003c num2 \u003c\u003c endl; swap(num1, num2); cout \u003c\u003c num1 \u003c\u003c \" \" \u003c\u003c num2 \u003c\u003c endl; return 0; } void swap(int \u0026a, int \u0026b) { int temp = a; a = b; b = temp; } 函数引用返回值\r函数的返回值也可以是引用。普通的传值返回,是将运算结果拷贝到一个临时存储空间,再从该临时存储空间拷贝给对应变量;当我们将函数返回值声明为引用时,会直接将运算结果拷贝给对应变量,不经过临时存储空间。 但需注意函数返回的引用不能是函数体内的临时变量。因为函数运行完,其申请的存储空间就会被销毁,这时我们还未进行数据的拷贝,赋值操作就不能正确执行。 #include\u003ciostream\u003e using namespace std; int \u0026 valplus1(int \u0026a); int \u0026 valplus2(int c); int main() { int num1 = 10; int num2 = 7; int num3; num3 = valplus1(num1); // 能够正确赋值,且不经过临时存储空间 cout\u003c\u003c num1 \u003c\u003c \" \" \u003c\u003c num3 \u003c\u003cendl; num3 = valplus2(num2); // 不能正确赋值,因为拷贝数据前存储空间已经被销毁 cout\u003c\u003c num2 \u003c\u003c \" \" \u003c\u003c num3 \u003c\u003cendl; return 0; } int \u0026 valplus1(int \u0026n) { n += 5; return n; } int \u0026 valplus2(int n) { int t = n + 5; return t; } 何时使用引用参数的一些指导原则\r如果参数是数组或基本数据类型,则使用指针 如果参数是类对象或结构变量,则使用引用 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"常量与只读变量\r常量在学习C语言时,就已接触,这里再进行简单的回顾一下,常量(Constant) 是指那些「在编译期间就能确定的值,且在运行期间这个确定的值不会发生变化」。 42; // 这是 int 型 42u; // 这是 unsigned int 型 42U; // 这也是 unsigned int 型 42l; // 这是 long 型 42L; // 这也是 long 型 42ll; // 这是 long long 型 42ul; // 这是 unsigned long 型 42ull; // 这是 unsigned long long 型 3.14f; // 这是 float 型 1e7L; // 这是 long double 型 'A'; // 这是 char 型 \"abc\"; // 这是 string 型 true; // 这是 bool 型 1 + 1; // 这也是常量 // 注:字面量不包括分号;,此处加上分号只为演示作用。 // 字面量(Literal)是指在 C++ 代码中,它的写法能直接体现它所表达的值的常量。 只读变量指的是「一旦被初始化赋值之后,其值就不能被更改的变量」。如何声明并定义一个变量为只读的?很简单,只需在类型说明符前加上 const 关键词修饰即可。比如: const int a = 37; 这样 a 就成为了一个 int 类型的,只读的变量。从此以后 a 的值不能发生变化,如以下行为都会导致编译错误: cin \u003e\u003e a; // 编译错误:无法向 a 中输入,因为 a 无法发生变化 a = 56; // 编译错误:无法为 a 赋值 PS: 只读变量必须在定义时就完成初始化。 常量与只读变量的关系\r你可能已经注意到,只读变量和常量都有一个共同的特点,就是“无法在运行期间更改它的值”。那么能否说只读变量就是常量呢? 答案是否定的。请看下面这个例子: #include \u003ciostream\u003e using namespace std; int main() { int a{0}; cin \u003e\u003e a; const int b{a}; // b = 42; // 编译错误 } 第 8 行声明了一个只读变量 b ,因此不能在第 9 行通过赋值更改它的值。但是你也注意到,程序在编译期间是无法得知 b 的值是多少的。因为 b 是用 a 初始化的,但是 a 的值则是在第 7 行由输入提供的。所以 b 的值只能在运行期间确定,无法在编译期间得知; b 不满足常量的定义。 上面这个例子表示,并非所有的只读变量都是常量。那么什么时候只读变量可以是常量呢?条件也很简单:只有使用常量作为初始化值初始化的只读变量才是常量。比如: #include \u003ciostream\u003e using namespace std; int main() { const int a{42}; const int b{a}; const int c{a + b}; // 以上三个只读变量均是常量 } a 是由 42 初始化的, 42 是常量,a的值在编译时期就能确定,所以 a 就是一个常量。同理,只读变量 b 由 a 初始化, a 已经是一个常量了,那么 b 因而也是一个常量。再来看 c , a 和 b 已经是常量了,那么由常量组成的表达式也是常量;故 a + b 也是常量。所以只读变量 c 也是常量。 你会发现判断一个只读变量是否是常量这件事情并不容易,尤其在更大的程序里。是不是常量这件事情有时候会显得很重要(比如将来会学的数组长度,以及模板泛型编程的时候常量与否也很关键)。因此 C++ 提供了一个关键字用于常量: constexpr 。 当 constexpr 出现在声明语句的时候,指明这个声明引入的变量是一个常量。如果不是常量的话,会导致编译错误。例如: #include \u003ciostream\u003e using namespace std; int main() { constexpr int a{42}; // a 是常量(当然常量必然是只读的。) int b; // b 既不是只读变量,也不是常量 const int c{b}; // c 不是常量,但它是只读变量 // constexpr int d{b}; // 编译错误,因为要求 d 是常量,但 d 未用常量初始化 constexpr int e{a}; // OK, e 是常量,由常量初始化 } 对于变量来说,关键字 constexpr 蕴含了 const 。(因为常量必然是只读的:运行时值不会发生更改。)因此在需要常量的场合,建议用 constexpr 代替 const ,避免意料之外的错误。 顶层 const 和 底层 const\r顶层const: 作用于变量或指针本身的const,即变量或指针的值不能修改。 底层const: 作用于指针指向的内容或引用的对象的const,通过指针或引用不能修改那个对象。 对于引用来说,只存在底层const,因为引用本身的值在初始化就定了,并不能改变,所以顶层const对于引用来说没有意义。 // 这里的 const 是底层 const,指针所指向的内容(int类型的值)不能通过 ptr 修改 void func(const int *ptr) { // *ptr = 100; // 错误: 不能通过 const 指针修改值 } // x 是对一个 const int 的引用,即底层 const void print(const int \u0026x) { // x = 100; // 错误: 不能修改 const 引用绑定的对象 } // 这里的 const 是顶层 const,指针的值(一个存储int类型的空间地址)不能修改 void func(int * const ptr) { ...... } //顶层const,a的值不能被修改 void func(const int a) { ...... } //左边的const为底层const,右边的const为顶层const void func(const int * const ptr) { ...... } ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"函数与const\r上一小节介绍了const搭配变量的使用,其实const也能配合函数来使用,主要有三种用法: // 1. const参数,防止对应实参被修改 void Func(const int \u0026a, const int *b, int c) // { a = 1; // 错误,不能修改 a 的值 *b = 2; // 错误,不能修改 *b 的值,但可修改 b 的值 } // 2. const成员函数(即类中的函数),表明该函数不会修改任何成员变量 // class是定义类的关键字,public和private是定义访问权限的关键字,后面都会详细讲解 class MyClass { public: void set_value(int value) { _value = value; } // get_value()函数不会修改任何成员变量,如果修改了会报错。 int get_value() const { return _value; } /** * 使用 const 修饰的成员函数就一定要确保其不会修改成员变量。所以,如果一个 const 成员函数调用了另一个 * 成员函数,也要确保调用的成员函数也是被 const 修饰的。 * 即const成员函数只能调用其他const成员函数,否则会报错。 * 例如,如果 get_value() 函数不是const成员函数,那么 show() 是不能调用它的,强行调用是不能通过编译的。 */ void show() const { cout \u003c\u003c get_value() \u003c\u003c endl; } private: int _value; }; // 3.const返回值,防止调用者通过返回的引用或指针修改原始对象。 const MyClass\u0026 getConstObject() const { return obj; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"默认参数\r默认参数指的是当调用函数时某些实参被省略,形参自动使用的一个值。直接看例子: #include \u003ciostream\u003e using namespace std; void Func(int a, int b, int c = 1) // Func函数的形参c就有默认参数1 { cout \u003c\u003c a \u003c\u003c \" \" \u003c\u003c b \u003c\u003c \" \" \u003c\u003c c \u003c\u003c endl; } int main() { Func(1, 2, 3); // 正确调用,输出 1 2 3 Func(1, 2); // 正确调用,输出 1 2 1 return 0; } 注意: 当某个参数需要指定默认值时,其右边的参数都需要指定默认参数 // 错误定义,如果b要定义默认参数,则c也要定义默认参数 int Func(int a, int b = 1, int c) { ...... return a; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:7","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"函数重载\r函数重载也叫函数多态,指的是在同一作用域内有多个同名的函数,它们完成类似的工作,但使用不同的参数列表。 函数重载的关键是函数参数列表——也称为函数特征标。如果两个函数的参数数量、参数类型、参数顺序都相同,则它们的特征标相同,在同一作用域中是不被允许的。 例如,可以定义一组原型如下的 print() 函数: #include \u003ciostream\u003e using namespace std; // 以下 print 函数形成重载 void print(const char *str, int width); // 声明 #1 void print(double d, int width); // 声明 #2 void print(long l, int width); // 声明 #3 void print(int i, int width); // 声明 #4 void print(const char *str); // 声明 #5 int main() { print(\"Pancakes\", 15); // 调用第1个print print(1999.0, 10); // 调用第2个print print(1999L, 15); // 调用第3个print print(1999, 12); // 调用第4个print print(\"syrup\"); // 调用第5个print return 0; } // 5个print函数的具体实现省略 .... ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:8","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"基于范围的 for 循环\r该语法是C++11 引入的一种新的 for 循环语法,它提供了一种简洁且易于阅读的方式来遍历 数组、容器(如 std::vector、std::list 等)和其他可迭代对象。 直接看以下示例就可明白如何使用: // 遍历数组 int array[] = {1, 2, 3, 4, 5}; for (int num : array) { std::cout \u003c\u003c num \u003c\u003c \" \"; } // 输出:1 2 3 4 5 /* ---------------------------------分割线--------------------------------- */ // 遍历 std::vector #include \u003cvector\u003e #include \u003cstring\u003e #include \u003ciostream\u003e std::vector\u003cstd::string\u003e vec = {\"Apple\", \"Banana\", \"Cherry\"}; for (const std::string\u0026 fruit : vec) { std::cout \u003c\u003c fruit \u003c\u003c \" \"; } // 输出:Apple Banana Cherry /* ---------------------------------分割线--------------------------------- */ // 遍历字符串 std::string str = \"Hello\"; for (char c : str) { std::cout \u003c\u003c c \u003c\u003c \" \"; } // 输出:H e l l o /* ---------------------------------分割线--------------------------------- */ // 修改数组中的值 int array[] = {1, 2, 3, 4, 5}; for (int \u0026num : array) { num++; } // 数组中的值变为:2 3 4 5 6 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:9","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"内联函数\r内联函数是C++为了 提高程序运行速度 所做的一项改进,内联函数会直接在调用处将调用的语句替换为函数体,而不必像常规函数一样,需要先跳转到函数地址,执行完函数后再跳转回调用处。如下图所示: 在程序设计过程中,我们通常会将一些 频繁被调用的短小函数 声明为内联函数。使用 inline 关键字来实现,但这只是建议编译器将该函数定义为内联函数,编译器不一定会接受此建议,它可能认为该函数过大或注意到函数调用了自己,因此不将其作为内联函数。 为了使得 inline 声明内联函数有效,我们必须将其与函数体放在一起才行,否则是不能成功将函数声明内联函数的,如下例所示: inline void swap(int \u0026a, int \u0026b); // 将 inline 放在函数声明处不会起作用 void swap(int \u0026a, int \u0026b) { int temp = a; a = b; b = temp; } /* ---------------------------------分割线--------------------------------- */ void swap(int \u0026a, int \u0026b); inline void swap(int \u0026a, int \u0026b) // 成功将 swap 函数声明为内联函数 { int temp = a; a = b; b = temp; } PS: 学完这一小节我们应该了解到:应该将那些频繁使用且短小(一般少于10行)的函数声明为内联函数(虽然编译器不一定会将其认定为内联函数),且应将 inline 放在函数体前才会起作用。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:10","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"内存空间的申请\r在 C语言中,动态分配和释放内存的函数是 malloc、calloc 和 free,而在 C++语言中,通常使用new、new[]、delete 和 delete[] 操作符来动态地分配内存和释放内存。 new、new[]、delete 和 delete[] 均是C++中的关键字,而非函数!!! new 用于动态分配单个空间,new [] 用于动态分配数组空间。delete 用于释放分配的单个空间,delete[] 用于释放分配的数组空间。 int *p = new int; // 申请了一个int类型的空间,等价于 malloc(sizeof(int)); int *A = new int[10]; // 申请了一个数组空间,大小为10,用于存在int类型的值,等价于 malloc(sizeof(int) * 10); delete p; // 释放单个的空间 delete[] A; //释放数组空间 为了避免内存泄露,new 和 delete、new[] 和 delete[] 操作符应该成对出现,并且不要将这些操作符与 C语言中动态分配内存和释放内存的几个函数一起混用。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:11","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"命令行处理技术\r在C++中,命令行处理是一项基本但非常重要的技术,特别是开发命令行工具和应用程序时。处理命令行参数可以通过标准的 main 函数参数或更高级的库来实现。下面介绍一种比较常用的方法: 使用main函数的参数 C++程序的入口点是 main 函数,它可以接受两个参数: int main(int argc, char* argv[]) argc(argument count):表示命令行参数的数量,包括命令本身。 argv(argument vector):一个字符指针数组,包含命令行参数的实际值。 例如,对于命令 wc report1 report2 report3, argc为4,argv[0]为wc,argv[1]为report1,依次类推。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:12","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"强制类型转换(选读)\r在 C++中有四个关键字用于强制类型转换: static_cast、const_cast、reinterpret_cast 和 dynamic_cast。它们相比C语言中的传统转换方式(使用括号表示的类型转换)提供了更为精细和安全的类型转换机制。 static_cast\r该关键字用于相关类型之间进行转换,如整型与浮点型,指针类型等。它执行的是编译时类型检查,不会做运行时的类型检查。它的工作原理是在编译时,编译器利用已知的类型信息来进行类型兼容性检查并执行转换。所以如果类型之间的转换是不安全的或不允许的,编译器在编译时会给出错误。使用方法如下: static_cast\u003c想要转换为的类型\u003e(变量或表达式) // 基本数据类型转换 int i = 10; float f = static_cast\u003cfloat\u003e(i); // 将int转换为float /* ---------------------------------分割线--------------------------------- */ // 指针类型转换 // 对于基本数据类型的指针 static_cast 不能在两个「具体类型」的指针之间进行转换,只能 // 将一个指针转换为 void * 类型,或将 void * 类型的指针转换为其原本的数据类型 int i = 5; void* ptr = static_cast\u003cvoid *\u003e(\u0026i); // 将 int * 类型的指针转换为 void * 类型 int* intPtr = static_cast\u003cint*\u003e(ptr); // void指针转换回int指针 // float *floatPtr = static_cast\u003cfloat*\u003e(\u0026i) // 这种转换是不被允许的 class Base {}; class Derived : public Base {}; Derived d; Base* basePtr = static_cast\u003cBase*\u003e(\u0026d); // 派生类指针转换为基类指针 /* ---------------------------------分割线--------------------------------- */ // 类型向上的安全类型转换。 // 了解即可,后面会介绍类的相关知识 class Base { public: virtual ~Base() {} }; class Derived : public Base { public: void func() { // ... } }; void function(Base\u0026 baseRef) { Derived\u0026 derivedRef = static_cast\u003cDerived\u0026\u003e(baseRef); //将派生类 Derived 类型的引用转换为基类Base类型的引用 derivedRef.func(); // 使用 Derived 类型的引用调用 Derived 的成员函数 } PS: 一定要注意 static_cast 是不能用于两个具体类型指针之间的转换的,因为它们指向的数据类型在内存中的存储方式和大小不同,直接转换指针类型可能会导致未定义行为。 dynamic_cast\r在这里认识该关键字即可,后面学完类的知识再仔细理解。 该关键字主要用于处理含有继承关系的类之间的安全向下转型,即从基类指针或引用转换为派生类指针或引用。它在运行时检查转换的安全性,所以具有较高的性能开销。其利用了 C++ 的多态性和运行时类型识别(RTTI)机制来确保转换的安全性。dynamic_cast 只能用于含有虚函数的类或其派生类,因为RTTI 需要虚函数表来确定对象的动态类型。 class Base { public: virtual ~Base() {} // 虚析构函数,确保类是多态的 }; class Derived : public Base { // 派生类内容 }; int main() { Derived d; Base* b = \u0026d; // 向下转型尝试 Derived* d2 = dynamic_cast\u003cDerived*\u003e(b); if (d2) { // 转换成功,d2 是一个 Derived 类型的指针 } else { // 转换失败,这在本例中不会发生,因为 b 确实指向 Derived 对象 } // 错误的向下转型尝试 Base b2; Derived* d3 = dynamic_cast\u003cDerived*\u003e(\u0026b2); if (!d3) { // 转换失败,b2 是 Base 类型的实例,而不是 Derived } return 0; } const_cast\r该关键字用于去除指向常数对象的指针或引用的常量性。 PS: 这里只会简单的介绍一下,了解该关键字的作用即可。因为使用 const_cast 去掉指针或引用的常量性并且去修改原始变量的数值,是一种非常不好的行为,应该在程序中避免这种情况。 #include\u003ciostream\u003e using namespace std; int main() { const int a = 10; const int * p = \u0026a; int *q; q = const_cast\u003cint *\u003e(p); *q = 20; //fine cout \u003c\u003c a \u003c\u003c \" \" \u003c\u003c *p \u003c\u003c \" \" \u003c\u003c *q \u003c\u003c endl; cout \u003c\u003c \u0026a \u003c\u003c \" \" \u003c\u003c p \u003c\u003c \" \" \u003c\u003c q \u003c\u003cendl; return 0; } 输出结果如下: 10 20 20 002CFAF4 002CFAF4 002CFAF4 查看运行结果,问题来了,指针 p 和指针 q 都是指向 a 变量的,且经过调试发现 002CFAF4 地址内的值确实由 10 被修改成了 20,这是怎么一回事呢?为什么 a 的值打印出来还是 10 呢? 其实这是一件好事,我们要庆幸 a 变量最终的值没有变成 20!变量 a 一开始就被声明为一个常量变量,不管后面的程序怎么处理,它就是一个常量,就是不会变化的。试想一下如果这个变量 a 最终变成了 20 会有什么后果呢?对于这些简短的程序而言,如果最后 a 变成了 20,我们会一眼看出是 q 指针修改了,但是一旦一个项目工程非常庞大的时候,在程序某个地方出现了一个 q 这样的指针,它可以修改常量 a,这是一件很可怕的事情的,可以说是一个程序的漏洞,毕竟将变量 a 声明为常量就是不希望修改它,如果后面能修改,这就太恐怖了。 reinterpret_cast\r该关键字可以在几乎任何类型的指针之间进行转换,甚至可以在指针和足够大的整数类型之间进行转换。这种转换不会尝试保留对象的值,而是简单地重新解释位模式,很容易导致错误的结果。由于这种转换的不安全性,只有在确实必要且你非常清楚自己在做什么的情况下,才应该使用它。 在 C++中,该关键字主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。 // 将整型指针转换为双精度浮点型指针 int *a = new int; double *d = reinterpret_cast\u003cdouble *\u003e(a); int i = 100; int* p = \u0026i; cout \u003c\u003c \"p value: \" \u003c\u003c p \u003c\u003c endl; // 将整数地址转换为整数 intptr_t address_as_int = reinterpret_cast\u003cintptr_t\u003e(p); cout \u003c\u003c \"address_as_int: \" \u003c\u003c address_as_int \u003c\u003c endl ; // 将整数重新转换回指针 int* p2 = reinterpret_cast\u003cint*\u003e(address_as_int); cout \u003c\u003c \"p2 value: \" \u003c\u003c p2 \u003c\u003c endl; ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:13","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"异常处理(选读)\r异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了(因为其很难实现),不再建议使用。所以,下面的内容了解一下即可(不看也行)。 在 C++ 中,一个函数能够检测出异常并且将异常返回,这种机制称为抛出异常。当抛出异常后,函数调用者捕获到该异常,并对该异常进行处理,我们称之为异常捕获。 C++ 新增 throw 关键字用于抛出异常,新增 catch 关键字用于捕获异常,新增 try 关键字尝试捕获异常。通常将可能会出现异常的语句放在try{ }程序块中,而将异常处理语句置于catch{ }语句块中。其基本语法如下: try { //可能抛出异常的语句 } catch (异常类型1) { //异常类型1的处理程序 } catch (异常类型2) { //异常类型2的处理程序 } // …… catch (异常类型n) { //异常类型n的处理程序 } 由 try 程序块捕获 throw 抛出的异常,然后依据异常类型运行对应 catch 程序块中的异常处理程。catch 程序块顺序可以是任意的,不过均需要放在 try 程序块之后。一个具体示例如下: #include\u003ciostream\u003e using namespace std; enum index{underflow, overflow}; int array_index(int *A, int n, int index); int main() { int *A = new int[10]; for(int i=0; i \u003c 10; i++) A[i] = i; try { cout\u003c\u003carray_index(A,10,5)\u003c\u003cendl; cout\u003c\u003carray_index(A,10,-1)\u003c\u003cendl; cout\u003c\u003carray_index(A,10,15)\u003c\u003cendl; } catch(index e) { if(e == underflow) { cout\u003c\u003c\"index underflow!\"\u003c\u003cendl; exit(-1); } if(e == overflow) { cout\u003c\u003c\"index overflow!\"\u003c\u003cendl; exit(-1); } } return 0; } int array_index(int *A, int n, int index) { if(index \u003c 0) throw underflow; // 抛出下溢出异常 if(index \u003e n-1) throw overflow; // 抛出上溢出异常 return A[index]; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:2:14","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"String类型\rC++增强了对字符串的支持,除了可以使用 C 语言风格的字符串,还可以使用 string类 处理字符串,且后者处理起字符串来更加方便。使用 string 数据类型需要包含头文件 \u003cstring\u003e。 PS: string类型的变量本质是一个对象,其包含的字符串不存在C语言中的字符串结束符 \\0,因为其内部通过维护一个长度计数器来获取字符串的长度,并不依赖 \\0。 string类型变量可直接调用类中的 size() 或 length() 函数来获取字符串长度。 string s = \"mystring\"; int len = s.length(); // len 的值为 8 ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"string类型变量定义和转换\rstring类型变量常用的定义方法有: #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { char c_char_array[] = \"Hello, World!\"; // 只定义不初始化,编译器会使用默认值(空字符串)进行赋值 string s1; // 定义时直接进行初始化 string s2 = \"mystring\"; // 定义时进行复制初始化,s3的内容和s2一样 string s3 = s2; // 定义时使用指定字符和大小来初始化,s4 的内容为 sssssss string s4 (7, 's'); // 定义时使用字符数组来进行初始化,s5的内容为 Hello, World! string s5(c_char_array); return 0; } 虽然 C++ 提供了 string 类型来替代 C 语言中的 char* 字符串,但程序设计过程中还是不可避免地会碰到用 char* 字符串的地方。为此,string类提供了一个转换函数 c_str(),该函数会返回一个只读的字符指针(const char *),指向的内容与string对象包含的字符串相同,且以 \\0 结尾。 #include \u003ciostream\u003e #include \u003cstring\u003e int main() { std::string str = \"Hello, World!\"; const char* cstr = str.c_str(); // 进行转换 std::cout \u003c\u003c cstr \u003c\u003c std::endl; // 输出: Hello, World! return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"string类型变量输入输出\r在 C++ 中,对于C风格的字符串有三种输入方法;而对于 string 对象,只有两种输入方式: // C风格的字符串 char info[100]; cin \u003e\u003e info; //第一种 cin.getline(info, 100); // 第二种,读取一行,会抛弃最后的 '\\n' cin.get(info, 100); // 第三种,读取一行,保留最后的 '\\n' /* ---------------------------------分割线--------------------------------- */ // string对象 string str; cin \u003e\u003e str; //第一种,读取一个单词,即遇到 空格或换行符就结束读取 getline(cin, str); //第二种,读取一行,即遇到 '\\n' 结束读取 string对象和字符数组均使用 cout 进行输出。 #include\u003ciostream\u003e #include\u003cstring\u003e using namespace std; int main() { string mystr; getline(cin, mystr); cout \u003c\u003c mystr \u003c\u003c endl; //进行输出 return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"string类型字符串的连接\r对于 string 类型变量,我们可以直接用 + 或者 += 符号进行字符串的连接(利用了操作符重载,后面会学习)。 用“+”风格字符串进行字符串连接时,操作符左右两边既可以都是 string 字符串,也可以是一个 string 字符串和一个 C 风格的字符串,还可以是一个 string 字符串和一个 char 字符。而用“+=”风格字符串进行字符串连接时,操作符右边既可以是一个 string 字符串,也可以是一个 C 风格字符串或一个 char 字符。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1, s2, s3; s1 = \"first\"; s2 = \"second\"; s3 = s1 + s2; cout\u003c\u003c s3 \u003c\u003cendl; // 输出:firstsecond s2 += s1; cout\u003c\u003c s2 \u003c\u003cendl; // 输出:secondfirst s1 += \"third\"; cout\u003c\u003c s1 \u003c\u003cendl; //输出:firstthird s1 += 'a'; cout\u003c\u003c s1 \u003c\u003cendl; //输出:firstthirda return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"修改字符串\r和字符数组一样,string 字符串也可以按照下标逐一访问每一个字符,起始下标仍是 0 。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s = \"1234567890\"; for(int i = 0; i \u003c s.length(); i++) // 遍历字符串中的每个字符 { cout\u003c\u003c s[i] \u003c\u003c\" \"; } // for循环的最终输出结果为 1 2 3 4 5 6 7 8 9 0 cout \u003c\u003c endl; // 输出换行符 s[7] = '9'; cout \u003c\u003c s \u003c\u003c endl; // 输出结果为1234567990 return 0; } 除了能逐个访问字符串中每个字符外,string类还提供了一些成员函数方便我们操作 string 类型变量。 erase()成员函数\r该函数用于删除字符串中的子字符串。有两个参数,第一个参数为子字符串的起始下标,第二个参数为子字符串的长度(不指定此参数,则默认子串为从起始位置开始一直到最后一个字符)。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1, s2, s3; s1 = s2 = s3 = \"1234567890\"; s2.erase(5); s3.erase(5, 3); cout \u003c\u003c s1 \u003c\u003c endl; // 输出 1234567890 cout \u003c\u003c s2 \u003c\u003c endl; // 输出 12345 cout \u003c\u003c s3 \u003c\u003c endl; // 输出 1234590 return 0; } insert()成员函数\r该函数用于在字符串指定位置插入另一个字符串。有两个参数,第一个参数表示插入位置的下标,第二个参数为要插入的字符串(可以是string 或 字符数组)。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { char carry[] = \"bbb\"; string s1, s2; s1 = \"1234567890\"; s2 = \"aaa\"; cout \u003c\u003c s1 \u003c\u003c endl; // 输出 1234567890 s1.insert(5, s2); cout \u003c\u003c s1 \u003c\u003c endl; // 输出 12345aaa67890 s1.insert(5, carry); cout \u003c\u003c s1 \u003c\u003c endl; // 12345bbbaaa67890 return 0; } replace()成员函数\r该函数用于使用指定的字符串来替换一个指定的子字符串。有三个参数,第一个参数为被替换的子字符串的起始下标,第二个参数为被替换的子字符串长度,第三个参数为用来替换的字符串(可以是 string 或 字符数组)。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1, s2, s3; s1 = s2 = \"1234567890\"; s3 = \"aaa\"; cout \u003c\u003c s1 \u003c\u003c endl; // 输出 1234567890 s1.replace(5, 4, s3); cout \u003c\u003c s1 \u003c\u003c endl; // 输出 12345aaa0 cout \u003c\u003c s2 \u003c\u003c endl; // 输出 1234567890 s2.replace(5, 2, \"aaa\"); cout\u003c\u003c s2 \u003c\u003cendl; // 输出 12345aaa890 return 0; } swap()成员函数\r该函数用于交换两个 string 类型变量的值。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1 = \"string\"; string s2 = \"aaaaaa\"; s1.swap(s2); // 互换值 cout \u003c\u003c s1 \u003c\u003c endl; // 输出 aaaaaa cout \u003c\u003c s2 \u003c\u003c endl; // 输出 string return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"获取子字符串\r使用成员函数 substr() 来获取string字符串中的一个子字符串。有两个参数,第一个参数为子字符串的起始下标,第二个参数为子字符串的长度(不指定此参数,则默认子字符串为从起始下标的字符开始一直到最后一个字符)。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1 = \"first second third\"; string s2, s3; s2 = s1.substr(6, 6); s3 = s1.substr(6); cout \u003c\u003c s2 \u003c\u003c endl; // 输出second cout \u003c\u003c s3 \u003c\u003c endl; // 输出second third return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"查找字符串\r使用成员函数 find() 在一个字符串中查找指定字符串。有两个参数,第一个参数为要查找的字符串,第二个参数为查找的起始位置(不指定此参数,则默认从0开始,即从字符串首开始查找)。 找到了会返回起始下标,没找到会返回一个 std::string::npos (这是一个静态成员常量,表示没有找到子字符串的占位符值)。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1 = \"first second third\"; string s2 = \"second\"; int index1 = s1.find(s2, 6); int index2 = s1.find(s2, 7); // 输出 Found at index : 6 if(index1 != std::string::npos) cout\u003c\u003c\"Found at index : \"\u003c\u003c index1 \u003c\u003cendl; else cout\u003c\u003c\"Not found\"\u003c\u003cendl; // 输出 Not found if(index2 != std::string::npos) cout\u003c\u003c\"Found at index : \"\u003c\u003c index2 \u003c\u003cendl; else cout\u003c\u003c\"Not found\"\u003c\u003cendl; return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"字符串的比较\r==、!=、\u003c=、\u003e=、\u003c 和 \u003e 操作符都可以用于进行 string 类型字符串的比较(是从第一个字符开始逐个比较的),这些操作符两边都可以是 string 字符串,也可以一边是 string 字符串另一边是字符串数组。 #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; int main() { string s1 = \"secondsecondthird\"; string s2 = \"secondthird\"; if( s1 == s2 ) cout \u003c\u003c \" == \" \u003c\u003c endl; if( s1 != s2 ) cout \u003c\u003c \" != \" \u003c\u003c endl; if( s1 \u003c s2 ) cout \u003c\u003c \" \u003c \" \u003c\u003c endl; if( s1 \u003e s2 ) cout \u003c\u003c \" \u003e \" \u003c\u003c endl; return 0; } // 最终输出 != 和 \u003c ","date":"2024-08-07","objectID":"/posts/3b2b064/:3:7","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类和对象\rC++是一门 面向对象 的编程语言,学习 C++,必须要搞清楚类和对象的概念。 类是C++面向对象编程的实现方式,类可以看做是结构体的升级版,它既可以包含变量,也可以包含函数。C++中的对象,可以理解为通过类定义出来的变量。和结构体一样,类是我们自定义的一种数据类型,而对象就是类这种数据类型的一个变量。一个类可以创建多个对象,每个对象都是类的一个具体实例,拥有类中的变量和函数。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类和对象的定义\r定义一个类与定义一个结构体的语法相似,只不过将 struct 关键字换位了 class 关键字。定义完后就可以像使用int那样来使用 Student定义对象、数组、指针。示例如下: class Student { // 在这里定义成员变量和成员函数 }; int main() { Student xiao_ming; // 定义了一个 Student类型的对象 xiao_ming Student all_student[1000]; // 定义了一个Student类型的数组,大小为1000 Student *stuptr; // 定义了一个Student类型的指针 return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类的成员变量和成员函数\r前面说了类不仅拥有成员变量,还有成员函数。下面是一个Student类的定义: class Student { // 定义对应的成员变量 string _name; int _id_num; int _age; char _sex; // 定义对应的成员函数 void set_age(int age); int get_age(); }; 你可能发现了在定义类的时候我们并没有定义成员函数,只是对其进行了声明,这是因为有两种方法可以给出成员函数的定义: 在定义类,对成员函数进行声明时就给出定义,这种方法被称为内联定义(即编译器会自动在函数前加上 inline 关键字)。这种定义方法直接在头文件(.h)中写函数定义。 在类外部给出函数定义,即在对应的.cpp文件中给出函数定义(需要使用与解析符 ::)。 PS1: 推荐使用第二种方法给出函数定义,即在头文件中给出类的定义(只对成员函数进行声明),在对应的 .cpp 文件中给出函数定义。只有那些一两行就可以搞定的成员函数才使用内联定义的方法。 PS2: 其实不只成员函数,构造函数、析构函数等也推荐使用第二种方式给出定义(除非函数定义特别短)。 说了那么多,你可能有点迷糊,那就来看看具体示例吧: class Student { // 定义对应的成员变量 string _name; int _id_num; int _age; char _sex; // 使用内联定义 void set_age(int age) { _age = age; } int get_age() const { return _age; } //这里的const表示该函数只会读取变量值,不会修改变量值,如果修改了变量值编译器会报错。 }; /* ---------------------------------分割线--------------------------------- */ // 在头文件 example.h 中给出类定义 class Student { // 定义对应的成员变量 string _name; int _id_num; int _age; char _sex; // 只声明成员函数 void set_age(int age); int get_age() const; }; // 在对应的 example.cpp 文件给出成员函数的定义 void Student::set_age(int age) // 需要使用 类名+域解析符:: 告诉编译器这是哪个类的成员函数 { _age = age; } int Student::get_age() const { return _age; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"成员访问符\r通过前面的知识,我们已经学会了如何编写一个类、如何定义一个类对象、如何定义一个类对象指针等,那么我们该如何访问类成员呢? 和结构体一样,类对象通过 . 符号访问成员,类对象指针通过-\u003e符号访问成员。示例如下: #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; class Student { string _name; int _id_num; int _age; char _sex; public: void set_age(int age) { _age = age; } int get_age() const { return _age; } }; int main() { Student xiao_ming; Student *xiao_li_ptr = new Student; xiao_ming.set_age(20); cout \u003c\u003c xiao_ming.get_age() \u003c\u003c endl; xiao_li_ptr-\u003eset_age(22); cout \u003c\u003c xiao_li_ptr-\u003eget_age() \u003c\u003c endl; return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类成员的访问权限以及类的封装\r在上一小节中,你可能注意到了我在定义类时使用了 public,这是一个关键字,表示该关键字下的类成员是具有 公开的 访问权限。这一节就来详细讲解。 在C++中,通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的。通俗来说,就是使用这三个关键字来限制成员的访问方式,以实现类的封装完成,完成数据隐藏。这三个访问权限由高到低依次为: public → protected → private。 在这里主要讲解 public 和 private 关键字,protected 将会在继承和派生那一章进行详细讲解。 你主要需要记住的是: 类对象和类对象指针只能访问 public 成员 private成员只能被该类的其他成员访问 将尽可能多的成员设置为私有,以隐藏类的内部实现细节,只提供必要的公共接口(即函数)供外部访问。 若某个私有成员需要被派生类访问,则将其设置为 protected 成员 示例如下: #include \u003ciostream\u003e using namespace std; //类的声明 class Student { private: // 私有成员,只能被该类的其他成员访问 string _name; int _age; float _score; public: // 公有成员 void set_name(const string \u0026name); void set_age(const int age); void set_score(const float score); void showInfo() const; }; // 成员函数的定义 void Student::set_name(const string \u0026name) { _name = name; } void Student::set_age(const int age) { _age = age; } void Student::set_score(const float score) { _score = score; } void Student::showInfo() const { cout \u003c\u003c _name \u003c\u003c \"的年龄是\" \u003c\u003c _age \u003c\u003c \",成绩是\" \u003c\u003c _score \u003c\u003c endl; } int main() { Student stu; stu.set_name(\"小明\"); stu.set_age(15); stu.set_score(92.5f); stu.showInfo(); Student *pstu = new Student; pstu -\u003e set_name(\"李华\"); pstu -\u003e set_age(16); pstu -\u003e set_score(96); pstu -\u003e showInfo(); return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类(class)和结构体(struct)的区别\r在C语言中,struct 是只能定义成员变量,而不能定义成员函数的。而在 C++ 中,struct 与 class 相似,既可以定义成员变量,又可以定义成员函数。 struct 和 class的区别有: 当不指定成员的访问权限时,struct中的成员默认是 public 属性,而class中的成员默认是 private 属性。 class 继承默认是 private 继承,而 struct 继承默认是 public 继承 class 可以使用模板,而 struct 不能 在编写C++代码时,强烈建议使用 class 来定义类,而使用 struct 来定义结构体,这样做语义更加明确。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"通过引用来传递和返回类对象\r在讲引用时,我们就说了推荐使用引用 来传递类对象,这是因为采用传值的方式需要经历对象间的拷贝操作,一定程度上会降低程序运行的效率。当然也可以使用指针来传递,但是使用引用更加简练直观。 示例如下: #include \u003ciostream\u003e using namespace std; class Student { private: string _name; int _age; float _score; public: void set_name(const string \u0026name); void set_age(const int age); void set_score(const float score); string get_name()const {return _name;} int get_age()const {return _age;} float get_score()const {return _score;} }; void Student::set_name(const string \u0026name) { _name = name; } void Student::set_age(const int age) { _age = age; } void Student::set_score(const float score) { _score = score; } const Student\u0026 show(const Student \u0026mstu) // 使用引用来传递和返回对象 { cout \u003c\u003c mstu.get_name() \u003c\u003c \"的年龄是\" \u003c\u003c mstu.get_age() \u003c\u003c \",成绩是\" \u003c\u003c mstu.get_score() \u003c\u003c endl; return mstu; } int main() { Student stu; stu.set_name(\"小明\"); stu.set_age(15); stu.set_score(92.5f); const Student \u0026t_stu = show(stu); cout \u003c\u003c t_stu.get_name() \u003c\u003c endl; // 输出小明 return 0; } PS: 虽然前面已经说过,但是还是需要提醒的是 不要返回临时变量和局部变量的引用!!! ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"构造函数及其调用\r在前面的例子中,我们为每个成员变量都编写了初始化函数(set_name、set_age等),但是,这很麻烦,有没有什么简单的方法可以来完成初始化呢?当然有,那就是使用构造函数!有了它就不用编写类似于set_name、set_age的函数了,偷懒星人狂喜(*^▽^)/ 构造函数是类中一种特殊的成员函数,其特殊之处有三点: 构造函数的函数名必须与类名相同 构造函数无返回值 创建类对象的时候,构造函数会被自动调用,而无需我们主动调用 构造函数的作用就是初始化对象,并处理对象创建时需要处理的其它事务。 所以,前面的代码我们就可以简化为: #include \u003ciostream\u003e using namespace std; class Student { private: string _name; int _age; float _score; public: Student(const string \u0026name, int age, float score); string get_name()const {return _name;} int get_age()const {return _age;} float get_score()const {return _score;} void showInfo() const; }; Student::Student(const string \u0026name, int age, float score) { _name = name; _age = age; _score = score; } void Student::showInfo() const { cout \u003c\u003c _name \u003c\u003c \"的年龄是\" \u003c\u003c _age \u003c\u003c \",成绩是\" \u003c\u003c _score \u003c\u003c endl; } int main() { Student stu(\"小明\", 15, 92.5f); // 隐式调用对应的构造函数完成初始化 // Student stu1; // 该代码是错误的,因为没有匹配的构造函数 stu.showInfo(); return 0; } 需要注意的是,对于编写了构造函数的类,编译器就不会为其生成一个访问权限为public的无参的默认构造函数(该函数的函数体为空),所以上面注释的代码是错误的(没有匹配的构造函数)。如果要使用无参的构造函数就需要自己手动编写了。 既然有隐式调用构造函数,那么肯定就有显示调用构造函数的方法,但是并不推荐。 显示调用为:Student stu = Student(\"小明\", 15, 92.5f); PS: 构造函数既然是函数,那它就可以重载和使用默认参数。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:7","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"初始化列表\r在前面的例子中,我们是在类外部给出构造函数的定义来进行成员变量的初始化,还是太麻烦了(要写的代码太多了!),还有更简单的方法吗? 直接在声明构造函数时给出定义进行初始化?No,这样的初始化代码一点不简洁不优雅。这时 初始化列表 就出场了!直接看例子: #include \u003ciostream\u003e using namespace std; class Student { private: string _name; int _age; float _score; public: // 使用初始化列表来进行成员变量的初始化 Student(const string \u0026name, int age, float score):_name(name), _age(age), _score(score){} string get_name()const {return _name;} int get_age()const {return _age;} float get_score()const {return _score;} void showInfo() const; }; void Student::showInfo() const { cout \u003c\u003c _name \u003c\u003c \"的年龄是\" \u003c\u003c _age \u003c\u003c \",成绩是\" \u003c\u003c _score \u003c\u003c endl; } int main() { Student stu(\"小明\", 15, 92.5f); stu.showInfo(); return 0; } 如果构造函数只进行初始化操作,那么墙裂推荐使用初始化列表。如果构造函数除了初始化成员变量,还需要进行其他操作,操作较短可直接写在函数体内,操作较长那还是建议不使用初始化列表在类外部编写构造函数的定义。 注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。 class Student { private: string _name; int _age; float _score; public: // 初始化顺序只跟变量声明顺序有关,这里的初始化顺序为 _name, _age, _score Student(const string \u0026name, int age, float score): _score(score), _age(age), _name(name) {} ...... }; PS:const成员变量只能使用初始化列表来进行初始化! ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:8","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"转型构造函数\r构造函数一般划分为两类:默认构造函数(不带参数)和带参构造函数。在带参构造函数中有两种比较常见的构造函数:转型构造函数和复制构造函数。在这一节先介绍转型构造函数。 转型构造函数用于 「隐式类型转换,隐式地将其他类型的对象转换为该类的对象」。当转型构造函数只有一个参数时,当这个参数为int类型,则可用于将int类型的对象转变为该类对象;当这个参数为char *类型,则可用于将char *类型的对象转变为该类对象。 这里举一个参数的转型构造函数只是方便讲解,转型构造函数当然可以有多个参数,通过学习下面的例子自己就能举一反三。 直接看一个简单的例子: #include \u003ciostream\u003e using namespace std; class Age { public: Age(int a):_age(a){} // 定义了一个转型构造函数,用于隐式类型转换 private : int _age; }; void func(Age a) { cout \u003c\u003c \"The function is called.\" \u003c\u003c endl; } int main() { int num = 7; func(num); return 0; } 明明func函数的参数是一个Age类型的变量,为什么我们传入一个int类型的变量也可以成功调用该函数呢? 这是因为执行代码 func(num); 时,编译器发现形参与实参类型不匹配且在形参的类定义中发现了一个用于将int类型对象转换为Age类型对象的转型构造函数,这时编译器就会自动调用该转型构造函数将int类型对象转换为Age类型对象,然后再使用转换出的Age类型对象去调用func函数。 之所以说是隐式的,是因为这个转型过程完全由编译器完成,无需程序设计人员来显示的转换。 隐式类型转换给我们带来了一定的便利,但更可能会给我们设计的程序带来一些难以觉察的细微错误。有时候我们希望关闭掉这种隐式类型转换,这时就需使用 explicit 关键字。修改上面的例子: #include \u003ciostream\u003e using namespace std; class Age { public: explicit Age(int a):_age(a){} // 使用 explicit 关键字,禁止此构造函数用于隐式类型转换 private : int _age; }; void func(Age a) { cout \u003c\u003c \"The function is called.\" \u003c\u003c endl; } int main() { int num = 7; func(num); // 编译时,此代码就会报错了,因为实参与形参类型不匹配且无法转换 return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:9","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"复制(拷贝)构造函数\r复制构造函数也被称为拷贝构造函数,顾名思义就是 「创建一个现有对象的副本」。标准的复制构造函数格式如下: class MyClass { public: // 标准复制构造函数 MyClass(const MyClass\u0026 other) // 使用 const 防止修改原始对象,也可去掉const,但是引用符号是万万不可去掉的 { // ...初始化代码... } }; C++11标准引入了一种特殊的复制构造函数,称为“完美转发”构造函数,它可以有多个参数,并且这些参数可以通过模板参数完美转发给其他构造函数或函数。但一般标准复制构造函数就能满足需求了。如有需要,请自行学习“完美转发”构造函数。 复制构造函数的参数必须是该类对象的引用。这是因为如果不使用引用传递,那就是通过值传递,而值传递需要通过复制的方式创建一个临时对象,再将该临时对象传递给复制构造函数。而临时对象的复制构造函数也是值传递,这又需要创建一个临时对象2来传递,依次类推,会创建无限多的临时对象,导致爆栈。 你可能觉点抽象,没关系,直接看例子: class Student { public: Student(const Student s) // 创建了一个参数不是引用的复制构造函数 { ...... } private: ...... }; int main() { Student a; // 创建了一个Student类的对象a Student b(a); // 创建了一个新的对象b,用a来进行初始化 /** * 由于Student类的复制构造函数是值传递的,所以会创建一个临时对象(假设为 temp1)并使用a来进行初始化,再 * 将该临时对象传递给b的复制构造函数,即 Student temp1(a); * 而创建temp1对象又需要创建一个临时对象 temp2 来传递值给其复制构造函数,即 Student temp2(a); * 这样就会无限递归下去,直到爆栈 */ return 0; } 当你没有编写复制构造函数时,编译器会自动生成一个复制构造函数,生成的复制构造函数只能进行浅复制。 浅复制与深复制的定义\r浅复制是复制对象的数据成员的值。如果数据成员包括指向动态分配内存的指针,浅复制不会复制内存本身,而是只复制指针的值。这意味着原始对象和复制对象的指针成员将指向相同的内存地址 深复制不仅复制非指针数据成员的值,还会复制指针指向的内存空间,即申请一块新的内存空间,在该内存空间存储与原来的内存空间一样的值,并将新内存空间的地址赋值给副本对象对应的指针成员。这意味着原始对象和复制对象的指针成员将指向不同的内存地址,每个对象都有自己的独立拷贝 简单来说,浅复制和深复制的差别就在于指针类型成员的处理,浅复制只会复制初始对象的指针成员存储的地址值,不会开辟新空间;而深复制则会开辟新的内存空间,将初始对象的指针成员指向的内存空间中存储的值复制到新内存空间中。 既然编译器会自动生成复制构造函数,那么什么时候才需要我们自己手写呢? 当类中有动态资源需要进行深拷贝 默认的复制构造函数可能会导致资源管理问题 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:10","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"析构函数\r在创建对象的时系统会自动调用构造函数来进行初始化,在对象需要被销毁时系统同样会自动调用一个函数来释放相关资源(例如申请的内存空间等),这个函数被称之为析构函数。 析构函数也是一个成员函数,与普通成员函数相比,有如下特征: 无返回值 不接受任何参数 函数名必须为 ~类名 不能重载,一个类有且仅有一个析构函数 必须是 public 访问权限 示例如下: class Array { public: Array(): length(0), num(nullptr){} // nullptr 表示空指针 Array(int * A, int n); Array(const Array \u0026a); void setIndexValue(int value, int index); int * getArray() const; int get_length() const {return length;} ~Array(); // 定义析构函数 private: int length; int *num; }; // 这里只需关注析构函数,其他函数就不写了 Array::~Array() { if(num != NULL) delete[] num; // 释放申请的内存空间 cout\u003c\u003c\"destructor\"\u003c\u003cendl; } 析构函数的调用顺序与构造函数的调用顺序相反,即先调用构造函数的对象后调用析构函数,后调用构造函数的对象先调用析构函数。 PS: 虽然编译器会生成默认析构函数,但还是推荐自己编写析构函数,确保资源的正确释放。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:11","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"常量指针this\r在C++中,this 指针是一个特殊的指针,它在每个非静态成员函数中都隐含地存在。this 指针存储的是调用成员函数的对象的地址。使用 this 指针可以访问调用对象的成员变量和成员函数,这在处理对象内部数据或实现链式调用时特别有用。 PS: 静态成员函数是没有 this 指针的,学了后续章节 类与 static 关键字 就可明白。 #include \u003ciostream\u003e using namespace std; class Box { public: Box(int length, int width): _length(length), _width(width) {} // 使用初始化列表直接初始化成员变量 void set_length(int length) { this-\u003e_length = length; // 使用this指针访问成员变量,这只是举例,实际上直接写 _length = length; 即可 } // 返回 *this 允许链式调用 Box\u0026 set_width(int width) { this-\u003e_width = width; return *this; } void display() const { std::cout \u003c\u003c \"Length: \" \u003c\u003c _length \u003c\u003c \", Width: \" \u003c\u003c _width \u003c\u003c std::endl; } private: int _width; int _length; }; int main() { Box box(20, 10); box.display(); // 输出 Length: 20, Width: 10 box.set_width(20).set_length(10); // 链式调用 box.display(); // 输出 Length: 10, Width: 20 return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:12","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类与 new 和 delete 关键字\r当我们需要为类对象动态分配内存空间和释放内存空间时,应该使用 C++语言提供的 new、new[]、delete、delete [] 关键字,不要使用 C语言提供的 malloc()、free() 函数。 这是因为 new、new[] 在申请到内存空间后会调用类的构造函数,而malloc()不会;同理,delete、delete [] 在释放内存空间前会先调用类的析构函数释放相关资源,而free()不会。 #include\u003ciostream\u003e using namespace std; class test { public: test(int num = 1):_num(num){cout \u003c\u003c _num \u003c\u003c \" Constructor\" \u003c\u003c endl;} ~test(){cout \u003c\u003c _num \u003c\u003c \" Destructor\" \u003c\u003c endl;} private: int _num; }; int main() { test * t0 = new test(0); // 输出:0 Constructor test * t1 = new test[3]; // 输出3次:1 Constructor test * t2 = (test *)malloc(sizeof(test)); // 不会有输出 delete t0; // 输出:0 Destructor delete[] t1; // 输出3次:1 Destructor free(t2); // 不会有输出 return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:13","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类与 const 关键字\r在前面我们已经详介绍过const关键字了,const当然也可以和成员变量和成员函数结合使用(和普通变量、普通函数结合使用的方法一样),除此之外,类对象也可以结合const关键字(即在定义类对象时在类名前加上const关键字)。 需要注意的是:const成员变量只能使用初始化列表来进行初始化;const成员函数可以访问任何成员变量,但只能访问const成员函数;const对象只能访问const成员函数。 PS: 对于const成员函数,编译器会使用const来修饰其this指针,确保其不会修改实列对象的状态(即改变属于对象的成员变量)。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:14","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类与 mutable 关键字(选读)\rmutable的含义是“可变的,易变的”,与常量关键词const的含义相反。那么什么时候才需要使用该关键词呢?什么时候才需要将一个成员变量声明为可变的呢? 没错!就是需要在const成员函数中被修改的成员变量!当一个成员函数被const修饰时,说明该函数不会去修改成员变量的值。然而,在某些情况下,我们需要在const成员函数中修改某个与类对象关系不大的变量。比如,统计某个const成员函数调用次数,这时就需要将存储调用次数的变量声明为 mutable。 class Counter { private: mutable int callCount; // 将其声明为可变的 public: Counter() : callCount(0) {} void someConstMethod() const { ++callCount; // 可以在 const 函数中被修改 // 执行其他逻辑 } int getCallCount() const { return callCount; } }; ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:15","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类与 static 关键字\r静态成员变量\r到目前为止,我们设计的类的所有成员变量都属于该类对象,并不属于类。例如,我们前面设计的Student类 class Student { private: string _name; int _age; float _score; public: // 使用初始化列表来进行成员变量的初始化 Student(const string \u0026name, int age, float score):_name(name), _age(age), _score(score){} string get_name()const {return _name;} int get_age()const {return _age;} float get_score()const {return _score;} void showInfo() const; }; 每个Student对象都有自己的存储空间,用来存储自己的_name、_age、_score变量,你修改 a._age 值并不影响 b._age 值。 可有时我们希望在多个对象之间共享数据,对象a改变了某个数据后对象b可以检测到。这时就可以使用静态成员变量来实现数据共享,即在变量类型前使用 static 关键字修饰。 能实现数据共享的原因在于 「静态成员变量属于类,不属于该类对象」,即使创建多个对象,也只为静态成员变量分配一个内存空间,所有对象都使用这个内存空间的数据。当某个对象修改了其值,也会影响到其他对象。 class Student { public: Student(){_count++;} ~Student(){_count--;} private: static int _count; //其它成员变量 }; 在上面的简单例子中,我们定义了一个_count静态成员变量用来统计学生人数。你可能发现了,在定义_count时我们并没有进行初始化,这是因为 「静态成员变量只能在类外部进行初始化」,具体格式为: 类型 类名::变量名 = 值 那么就将上面的例子完成初始化吧: class Student { public: Student(){_count++;} ~Student(){_count--;} private: static int _count; //其它成员变量 }; int Student::_count = 0; // 不能再加上 static 注意: static 成员变量的内存既不是在定义类时分配,也不是在创建对象时分配,而是在初始化时分配。反过来说,没有在类外进行初始化的 static 成员变量是不能使用的。且静态成员变量是属于类的,那么在初始化后就可使用 类名::静态成员变量名 的方式对访问权限为 public的静态成员变量 进行访问。 静态成员变量的内存空间在哪?\r静态成员变量的内存空间和普通静态变量、全局变量一样,都是在内存的全局数据区进行分配,在该静态成员变量进行初始化时分配,到程序结束时才释放。 静态成员函数\r既然可以声明静态成员变量,那么可不可以声明静态成员函数呢?当然可以!同样的在函数返回值前加上static关键字即可声明一个静态成员函数。 在前面我们说:非静态成员变量是属于对象的,而静态成员变量是属于类的。那么由此可知静态成员函数也是属于类的,那非静态成员函数呢? 没错!非静态成员函数也是「属于类」的! 非静态成员函数和静态成员函数的根本区别在于:「非静态成员函数有this指针,可以访问类中任意成员;而静态成员函数没有this指针,只能访问类的静态成员(静态成员变量和静态成员函数)」。 那么就来把上面的Student类完善一下吧, #include \u003ciostream\u003e using namespace std; class Student { public: Student(char *name, int age, float score); void show() const; public: //声明静态成员函数 static int getTotal(); static float getPoints(); private: static int _total; //总人数 static float _points; //总成绩 private: char *_name; int _age; float _score; }; // 静态成员变量初始化 int Student::_total = 0; float Student::_points = 0.0; Student::Student(char *name, int age, float score): _name(name), _age(age), _score(score) { _total++; _points += score; } void Student::show() const { cout \u003c\u003c _name \u003c\u003c \"的年龄是\" \u003c\u003c _age \u003c\u003c \",成绩是\" \u003c\u003c _score \u003c\u003c endl; } //定义静态成员函数 int Student::getTotal() { return _total; } float Student::getPoints() { return _points; } int main() { (new Student(\"小明\", 15, 90.6)) -\u003e show(); (new Student(\"李磊\", 16, 80.5)) -\u003e show(); (new Student(\"张华\", 16, 99.0)) -\u003e show(); (new Student(\"王康\", 14, 60.8)) -\u003e show(); int total = Student::getTotal(); float points = Student::getPoints(); cout \u003c\u003c \"当前共有\" \u003c\u003c total \u003c\u003c \"名学生,总成绩是\" \u003c\u003c points \u003c\u003c \",平均分是\" \u003c\u003c points/total \u003c\u003c endl; return 0; } 既然非静态成员函数和静态成员函数都属于类,那为什么还要把一个成员函数声明为静态的呢?前面已经说了,静态成员函数只能访问静态成员变量,当一个成员函数只会去访问静态成员变量时,我们就可将其声明为静态成员函数,这样可以更加明确该函数的功能,使语义更加清晰。 注意: 静态成员函数不能被const修饰,在前面的学习中,我们已经知道对于const成员函数,编译器会使用const来修饰其this指针,确保其不会修改实列对象的状态。但静态成员函数并没this指针,本就不会修改实列对象的状态,自然就不能使用const修饰。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:16","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"友元函数和友元类\r我们在前面说只有该类的成员函数才能访问该类的private成员,只有该类的成员函数及其派生类的成员函数可以访问该类的protected成员。现在,我们来介绍一种例外情况——友元(friend)。「通过friend关键字可以使得其他类的成员函数以及全局范围内的函数访问当前类的private和protected成员」。 友元函数\r当该类外的函数(指其他类的成员函数和全局范围内的函数)需要访问该类的private和protected成员时,就可在该类中将此函数声明为友元函数。 #include \u003ciostream\u003e using namespace std; class Student{ public: Student(char *name, int age, float score); public: friend void show(const Student \u0026stu); // 将全局范围内的函数 show()声明为友元函数 private: char *_name; int _age; float _score; }; Student::Student(char *name, int age, float score): _name(name), _age(age), _score(score){ } void show(const Student \u0026stu) { cout \u003c\u003c stu._name \u003c\u003c \"的年龄是 \" \u003c\u003c stu._age \u003c\u003c \",成绩是 \" \u003c\u003c stu._score \u003c\u003c endl; } int main() { Student stu(\"小明\", 15, 90.6); show(stu); // 调用友元函数 return 0; } 注意: 友元函数毕竟不是该类的函数,在友元函数中不能直接访问该类成员,必须 「借助该类对象或该类对象指针」。友元函数的在该类中的声明位置没有要求,但是推荐在 public 下声明。 友元类\r不仅可以将一个函数声明为一个类的 “朋友”,还可以将整个类声明为另一个类的 “朋友”,这就是友元类。友元类中的所有成员函数都是另一个类的友元函数。 例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。 // 提前声明Address类,告诉编译器存在Address类 class Address; //声明Student类 class Student{ public: Student(char *name, int age, float score); public: void show(Address *addr); // 在这里还未进行Address类的声明,所以需要在前面进行提前声明存在Address类 private: char *_name; int _age; float _score; }; //声明Address类 class Address { public: Address(char *province, char *city, char *district); public: //将Student类声明为Address类的友元类,Student类的成员函数均可访问该类的成员 friend class Student; private: char *_province; char *_city; char *_district; }; 注意: 除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。且友元关系是不能传递的。比如类B是类A的友元类,类C是类B的友元类,但类C不是类A的友元。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:4:17","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"继承和派生\r","date":"2024-08-07","objectID":"/posts/3b2b064/:5:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"继承的概念与语法\r在C++中继承是一个很简单很直观的概念,描述的是 「类与类之间的关系」。与现实世界中的继承类似,例如儿子继承父亲的财产。B继承于A可以理解为「B类获取了A类的成员变量和成员函数」。 在C++中继承和派生是同一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。 被继承的类称为父类或基类,继承的类称为子类或派生类。通常“子类”和“父类”放在一起称呼,“基类”和“派生类”放在一起称呼。在上面的例子中,你把A类称为父类的话,就得把B类称为子类(当然也可以将其称为派生类,但是不协调)。 派生类除了天然拥有基类的成员,还可以定义自己的新成员,以增强类的功能。 常见的继承使用场景有: 创建的新类与某个已有类很相似,只是多出若干成员,可以让新类继承于该已有类。 当需要创建多个类时,这些类拥有很多相似成员,可以将这些相似成员提取出来,定义为一个基类,然后从该基类派生出这些类。 基类的定义方法与普通类一样,派生类的定义语法为: class 派生类类名: 继承方式 基类名 { ...... }; 说了那么多,直接来看例子: #include\u003ciostream\u003e using namespace std; //定义一个基类 Pelple class People { public: void set_name(char *name); void set_age(int age); char *get_name() const; int get_age() const; private: char *_name; int _age; }; void People::set_name(char *name){ _name = name; } void People::set_age(int age){ _age = age; } char* People::get_name()const { return _name; } int People::get_age()const { return _age;} //派生类 Student,采用public继承方式,后面会详细讲解继承方式 class Student: public People { public: void set_score(float score); float get_score() const; private: float _score; }; void Student::set_score(float score){ _score = score; } float Student::get_score() const { return _score; } int main(){ Student stu; // 继承了People类的对应成员 stu.set_name(\"小明\"); stu.set_age(16); stu.set_score(95.5f); cout \u003c\u003c stu.get_name() \u003c\u003c \"的年龄是 \" \u003c\u003c stu.get_age() \u003c\u003c \",成绩是 \" \u003c\u003c stu.get_score() \u003c\u003c endl; return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"继承方式\r派生类继承基类的继承方式有三种:public、protected、private。没错,又是它们三兄弟!在没有指定继承方式时,编译器默认继承方式为private。 这三种继承方式有什么区别呢?不同的继承方式会影响基类成员在派生类中的访问权限。 public继承方式 基类中的public成员在派生类中仍然是public访问权限 基类中的protected成员在派生类中仍然是protected访问权限 protected继承方式 基类中的public成员在派生类中是protected访问权限 基类中的protected成员在派生类中仍然是protected访问权限 private继承方式 基类中的public成员在派生类中是private访问权限 基类中的protected成员在派生类中是private访问权限 你可能发现了,继承方式是用来 「指定基类成员在派生类中的最高访问权限的」。例如,当继承方式为protected时,那么基类成员在派生类中的最高访问权限也为protected,高于protected的会降级为protected,但低于的不会升级。 为什么没有谈论基类的private成员呢?因为我们在前面说过了一个类的private成员只能被该类的其他成员访问。所以,基类的private成员虽然被派生类所继承(仍然是private访问权限),且占用派生类对象的内存,但它在派生类中是不可见的,派生类只能通过基类的成员函数去访问它。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。 注意:由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public继承方式。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"修改基类成员在派生类中的访问权限\r根据实际情况确定好类的继承方式后,是不是基类成员在派生类中的访问权限就无法修改了呢?并不是,使用 using关键字可以修改基类成员在派生类中的访问权限,在派生类中 对应访问权限下进行以下声明: using 基类名::成员名; 注意:函数成员也只需要写函数名即可,不必加上()符号。且using只能改变基类的 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类的 private 成员在派生类中是不可见的。 #include\u003ciostream\u003e using namespace std; // 基类People class People { public: void show(); protected: char *_name; int _age; }; void People::show() { cout \u003c\u003c _name \u003c\u003c \"的年龄是\" \u003c\u003c _age \u003c\u003c endl; } // 派生类Student class Student : public People { public: void learning(); public: using People::_name; //将protected改为public using People::_age; //将protected改为public float _score; private: using People::show; //将public改为private }; void Student::learning() { cout \u003c\u003c \"我是\" \u003c\u003c _name \u003c\u003c \",今年\" \u003c\u003c _age \u003c\u003c \"岁,这次考了\" \u003c\u003c _score \u003c\u003c \"分!\" \u003c\u003c endl; } int main() { Student stu; stu._name = \"小明\"; stu._age = 16; stu._score = 99.5f; //stu.show(); //编译错误,因为在派生类中show()函数被声明为私有成员了 stu.learning(); return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"继承时的名字遮蔽问题\r如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员时,使用的是派生类自己的成员,而不是从基类继承过来的成员。如果要使用从基类继承过来的成员,就得采用 基类名::成员名 的方式。 注意:派生类的成员函数与基类的成员函数不构成重载(作用域不同),所以只要同名就会造成遮蔽。 #include\u003ciostream\u003e using namespace std; class People { public: People(char *name, int age): _name(name), _age(age){} void show(); protected: char *_name; int _age; }; void People::show() { cout \u003c\u003c \"嗨,大家好,我叫\" \u003c\u003c _name \u003c\u003c \",今年\" \u003c\u003c _age \u003c\u003c \"岁\" \u003c\u003c endl; } class Student: public People { public: // 别急,下小节会解释为什么派生类的构造函数这样写 Student(char *name, int age, float score):People(name, age), _score(score){} public: void show(); //遮蔽基类的show() private: float _score; }; void Student::show() { cout \u003c\u003c _name \u003c\u003c \"的年龄是\" \u003c\u003c _age \u003c\u003c \",成绩是\" \u003c\u003c _score \u003c\u003c endl; } int main() { Student stu(\"小明\", 16, 90.5); // 调用的是派生类新增的成员函数,而不是从基类继承的 stu.show(); // 调用的是从基类继承来的成员函数 stu.People::show(); return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"基类和派生类的构造函数\r经过前面的学习,你肯定以为派生类能够继承基类的全部成员函数吧!No,No,No,这里能被继承的成员函数仅限于那些普通成员函数,「构造函数(包括复制构造函数、转型构造函数等)、析构函数是不能被继承的」。 不能被继承是因为这些函数与类是紧紧相关的,例如,如果基类的构造函数被派生类继承了,但该构造函数与派生类的类名并不相同,它并不能成为派生类的构造函数,更不可能成为派生类的普通函数。所以,它并不能被继承。 派生类既然继承了基类的成员变量,那么这些变量的初始化工作也应由派生类的构造函数完成,但是大部分基类的成员变量都是private访问权限的,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 在上一节的示例代码中,有这样一行 Student(char *name, int age, float score):People(name, age), _score(score){} People(name, age)就是在调用基类的构造函数,并将name和age作为实参传给基类的构造函数。 也可以将基类构造函数的调用放在参数初始化列表后: Student(char *name, int age, float score): _score(score), People(name, age){} 不管顺序如何,「派生类的构造函数都会先调用基类的构造函数再执行其他代码」。 注意:只能在派生类构造函数的头部调用基类的构造函数,而不能在派生类构造函数的函数体内调用。因为基类构造函数不会被继承,不能当做普通的成员函数来调用。 从上面的学习可以得出一个结论:派生类总是先调用基类的构造函数,再调用派生类的构造函数。即先执行基类的构造函数,再执行派生类的构造函数。 那么继承关系有多层时,构造函数的执行顺序是怎样的呢?「构造函数的执行顺序是按照继承的层次自顶向下」。 例如:C继承于B,B继承于A,那么构造函数的执行顺序为 A的构造函数→B的构造函数→C的构造函数。 直接基类和间接基类的定义\r顾名思义,一个类直接继承的类被称为直接基类(简称基类),间接继承的类被称为间接基类。在上面的例子中,B是C的直接基类,A是C的间接基类。\r注意:派生类只能且必须调用直接基类的构造函数。要理解这句话必须明白两点:1. 如果你没有在派生类的构造函数头部调用基类的构造函数,那么编译器就会自动调用基类的默认构造函数,如果基类不存在默认构造函数,则编译时会报错。2. 通过第一点可知,直接基类会调用它的基类的构造函数,如果派生类再调用其间接基类的构造函数,那么就会重复调用,造成资源的浪费,所以派生类禁止调用间接基类的构造函数(如上面的C类不能调用A类的构造函数)。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"基类和派生类的析构函数\r和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。 析构函数的执行顺序与构造函数的执行顺序是相反的:「先执行派生类的析构函数,再执行基类的析构函数」。 #include \u003ciostream\u003e using namespace std; class A { public: A(){cout\u003c\u003c\"A constructor\"\u003c\u003cendl;} ~A(){cout\u003c\u003c\"A destructor\"\u003c\u003cendl;} }; class B: public A { public: B(){cout\u003c\u003c\"B constructor\"\u003c\u003cendl;} ~B(){cout\u003c\u003c\"B destructor\"\u003c\u003cendl;} }; class C: public B { public: C(){cout\u003c\u003c\"C constructor\"\u003c\u003cendl;} ~C(){cout\u003c\u003c\"C destructor\"\u003c\u003cendl;} }; int main() { C test; return 0; } 输出结果为: A constructor B constructor C constructor C destructor B destructor A destructor ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"多继承\r在前面的例子中,派生类都只有一个基类,称为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类。 多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用。使用多继承时一定要确保代码逻辑正确。 多继承的语法很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D: class D: public A, private B, protected C { ...... } D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。 多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为: // 这里的声明顺序决定了后面基类构造函数的调用顺序 class D: public A, private B, protected C { D(形参列表):A(实参列表),B(实参列表),C(实参列表){ /*其他操作*/ } ...... } 注意:基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。 使用多继承时,当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:7","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"虚继承和虚基类\r在多继承时很容易产生命名冲突问题,如果我们很小心地将所有类中的成员变量及成员函数都命名为不同的名字时,命名冲突依然有可能发生,比如非常经典的菱形继承结构。 所谓菱形继承,举个例子,类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A → B → D 这条路,另一份来自 A → C → D 这一条路。 class A { public: void set_x(int a){x = a;} int get_x(){return x;} private: int x; }; class B: public A { public: void set_y(int a){y = a;} int get_y(){return y;} private: int y; }; class C: public A { public: void set_z(int a){z = a;} int get_z(){return z;} private: int z; }; class D: public B, public C { //...... }; 上面这个例子即为典型的菱形继承结构,类 A 中的成员变量及成员函数继承到类 D 中均会产生两份,这样的命名冲突非常的棘手,通过域解析操作符已经无法分清这些成员分别来自谁了。为此,C++ 提供了虚继承这一方式来解决这种情况下的命名冲突问题。虚继承只需要在中间类(在这个例子中是B、C类)的继承属性前加上 virtual 关键字,该关键字后面的类被称为虚基类。 class A { public: void set_x(int a){x = a;} int get_x(){return x;} private: int x; }; class B: virtual public A { public: void set_y(int a){y = a;} int get_y(){return y;} private: int y; }; class C: virtual public A { public: void set_z(int a){z = a;} int get_z(){return z;} private: int z; }; class D: public B, public C { //...... }; 在本例中,B 和 C 都以虚继承的方式继承虚基类 A,如此操作之后,类 D 只会得到一份来自类 A 的数据,这样就解决了刚刚棘手的命名冲突问题。 注意:在这一小节和上一小节中,我们只是简单的介绍了一下多继承和命名冲突时的解决办法,但也可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序的编写、调试和维护工作都会变得更加困难,因此不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:8","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"向上转型和向下转型\r在 C/C++ 中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量。 类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义。「将派生类赋值给基类称为向上转型(Upcasting),将基类赋值给派生类称为向下转型(Downcasting)」。 向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。所以,在实际项目中向下转型的使用频率远远小于向上转型,因此,我们在这里只详细介绍向上转型,向下转型请自己查阅资料学习。 将派生类对象赋值给基类对象\r直接看例子: #include \u003ciostream\u003e using namespace std; //基类 class A { public: A(int a):_a(a){}; public: void display(); public: int _a; }; void A::display() { cout \u003c\u003c \"Class A: a=\" \u003c\u003c _a \u003c\u003c endl; } //派生类 class B: public A{ public: B(int a, int b):A(a), _b(b){}; public: void display(); public: int _b; }; void B::display() { cout \u003c\u003c \"Class B: a=\" \u003c\u003c _a \u003c\u003c \", b=\" \u003c\u003c _b \u003c\u003c endl; } int main(){ A a(10); B b(66, 99); cout \u003c\u003c \"赋值前\" \u003c\u003c endl; a.display(); b.display(); cout\u003c\u003c\"------------------------\"\u003c\u003cendl; a = b; cout \u003c\u003c \"赋值后\" \u003c\u003c endl; a.display(); b.display(); return 0; } /* 运行结果为: 赋值前 Class A: a=10 Class B: a=66, b=99 ------------------------ 赋值后 Class A: a=66 Class B: a=66, b=99 */ 在本例中我们分别定义了基类对象a和派生类对象b,a的内存空间只存储了_a的值,b的内存空间存储了_a和_b的值(通过对前面学习可知对象的存储空间只存放非静态成员变量)。 在赋值操作a = b后,a的内存空间中是否也会存在_b的值了呢?并没有,因为在a的内存空间的大小在定义时就已经确定好了,不会再更改了,a的内存空间只能存放_a的值,所以在进行赋值操作a = b时,会将b的内存空间中的_a的值赋值给a的内存空间中的_a。即派生类对象赋值给基类对象时,会将派生类独有的成员变量值抛弃,只将派生类继承而来的成员变量值赋值给基类对象(因为基类对象只能存放这些值)。 有人会想:为什么赋值后a调用的display()函数仍然是A类的?这是因为赋值操作并不会改变a的类型,a仍然是A类对象,所以它调用的肯定是A类的display()函数。 将派生类指针赋值给基类指针\r修改上面的示例代码: #include \u003ciostream\u003e using namespace std; class A { public: A(int a):_a(a){}; void display(); int _a; }; void A::display() { cout \u003c\u003c \"Class A: a=\" \u003c\u003c _a \u003c\u003c endl; } class B { public: B(int b):_b(b){}; void display(); int _b; }; void B::display() { cout \u003c\u003c \"Class B: b=\" \u003c\u003c _b \u003c\u003c endl; } class C: public A, public B { public: C(int a, int b, int c):A(a), B(b), _c(c){}; void display(); int _c; }; void C::display() { cout \u003c\u003c \"Class C: a=\" \u003c\u003c _a \u003c\u003c \", b=\" \u003c\u003c _b \u003c\u003c \", c=\" \u003c\u003c _c \u003c\u003c endl; } int main(){ A *a = new A(3); B *b = new B(7); C *c = new C(10, 11, 12); cout \u003c\u003c \"赋值前\" \u003c\u003c endl; a-\u003edisplay(); b-\u003edisplay(); c-\u003edisplay(); cout\u003c\u003c\"------------------------\"\u003c\u003cendl; a = c; b = c; cout \u003c\u003c \"赋值后\" \u003c\u003c endl; a-\u003edisplay(); b-\u003edisplay(); c-\u003edisplay(); cout \u003c\u003c \"a的地址为\" \u003c\u003c a \u003c\u003c endl; cout \u003c\u003c \"b的地址为\" \u003c\u003c b \u003c\u003c endl; cout \u003c\u003c \"c的地址为\" \u003c\u003c c \u003c\u003c endl; return 0; } /* 运行结果如下: 赋值前 Class A: a=3 Class B: b=7 Class C: a=10, b=11, c=12 ------------------------ 赋值后 Class A: a=10 Class B: b=11 Class C: a=10, b=11, c=12 a的地址为0x6b1aa0 b的地址为0x6b1aa4 c的地址为0x6b1aa0 */ 该例子是一个多继承的例子,C类分别继承于A类和B类,看完这个例子后,你可能会有两个疑问:为什么赋值后A类和B类指针调用的还是自己的display()函数?为什么赋值操作后只有a和c存储的地址值相同,b的值要大于a、c的值?让我们依次来思考解决这两个问题。 1.为什么赋值后A类和C类指针调用的还是自己的display()函数? 对于这个问题,首先要明白成员函数是属于类的而不是类对象,类对象空间并不存储成员函数。在进行 a = c; b = c; 操作后,a和b的类型并没有改变,只是指向了派生类C的对象而已。所以,执行a-\u003edisplay(); b-\u003edisplay();操作时,编译器会去对应的A类和B类中找到display()来执行。a,b指向C类对象,调用display()时传入的this肯定是指向C类对象的,所以最终在display()中使用的是C类对象存储的成员变量值。 简单概括就是:编译器通过指针类型来访问成员函数(虚函数除外,后面会讲),通过指针指向的对象来访问成员变量 2.为什么赋值操作后只有a和c存储的地址值相同,b的值要大于a、c的值? 这是因为在 C++ 中,当一个类通过多重继承从多个基类派生时,派生类对象的内存空间中包含了所有基类的成员变量。每个基类的成员在内存空间中占据不同的位置。为了 「使基类指针能够正确指向其对应的基类成员变量,编译器在进行指针转换时需要进行地址调整」。 本例的c的内存空间如下图所示: 执行a = c;操作时,因为A类成员变量的起始位置就是0x6b1aa0,无需调整,所以 a的值与c的值一样;执行b = c;操作时,编译器发现B类成员变量的起始位置并不是0x6b1aa0,所以会将0x6b1aa0加上对应偏移量调整为0x6b1aa4后再赋值给b。所以,b的值大于a、c的值。 派生类存储基类成员的顺序与声明继承关系时的顺序一致。在下面的例子中就会先存储_b再存储_a,使得c和b的值相同,而a的值大于它们两的值。 class C: public B, public A { public: C(int a, int b, int c):A(a), B(b), _c(c){}; void display(); int _c; }; 将派生类引用赋值给基类引用\r引用在本质上是通过指针的方式实现的,既然基类的指针可以指向派生类的对象,那么我们就有理由推断:基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的。 修改前面的代码,验证推断正确: #include \u003ciostream\u003e using namespace std; class A { public: A(int a):_a(a){}; void display(); int _a; }; void A::display() { cout \u003c\u003c \"Class A: a=\" \u003c\u003c _a \u003c\u003c endl; } class B { public: B(int b):_b(b){}; void display(); int _b; }; void B::display() { cout \u003c\u003c \"Class B: b=\" \u003c\u003c _b \u003c\u003c endl; } class C: public A, public B { public: C(int a, int b, int c):A(a), B(b), _c(c){}; void display(); int _c; }; void C::display() { cout \u003c\u003c \"Class C: a=\" \u003c\u003c _a \u003c\u003c \", b=\" \u003c\u003c _b \u003c\u003c \", c=\" \u003c\u003c _c \u003c\u003c endl; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:5:9","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"多态与虚函数\r面向对象程序设计语言有 「封装、继承和多态」 三种机制,这三种机制能够有效提高程序的可读性、可重用性和可扩展性。前面已经学习过封装、继承了,我们将在这一节介绍多态。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"多态的简单介绍\r“多态\"按字面来说就是「多种形态」,指的是同一名字的事物有多种形态,可以完成不同的功能。 多态可以分为编译时的多态和运行时的多态。前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关,是本章要讲述的内容,在后面提及的多态都是指运行时的多态。 那多态有什么用呢?在向上转型的示例中,我们发现将派生类的对象指针赋值给基类对象的指针后,基类对象的指针只能使用派生类的成员变量,而不能使用派生类的成员函数。这并不符合人们的思维习惯,因为我们在直观上认为:如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数。而多态就可以实现这样的效果,使得基类指针指向派生类对象时调用的是派生类的成员函数。 要想形成多态必须具备以下三个条件: 存在继承关系 继承关系中必须有函数签名相同的虚函数 存在基类类型的指针或引用,通过该指针或引用调用虚函数 下一小节将会详细介绍为什么必须具备这三个条件才能形参多态。 多态的使用场景是什么呢?来看下面这个例子,我们假设你正在玩一款军事游戏,敌人突然发动了地面战争,于是你命令陆军、空军及其所有现役装备进入作战状态。具体的代码如下所示: #include \u003ciostream\u003e using namespace std; //军队 class Troops { public: virtual void fight(){ cout\u003c\u003c\"Strike back!\"\u003c\u003cendl; } }; //陆军 class Army: public Troops { public: void fight(){ cout\u003c\u003c\"--Army is fighting!\"\u003c\u003cendl; } }; //99A主战坦克 class _99A: public Army { public: void fight(){ cout\u003c\u003c\"----99A(Tank) is fighting!\"\u003c\u003cendl; } }; //武直10武装直升机 class WZ_10: public Army { public: void fight(){ cout\u003c\u003c\"----WZ-10(Helicopter) is fighting!\"\u003c\u003cendl; } }; //长剑10巡航导弹 class CJ_10: public Army { public: void fight(){ cout\u003c\u003c\"----CJ-10(Missile) is fighting!\"\u003c\u003cendl; } }; //空军 class AirForce: public Troops { public: void fight(){ cout\u003c\u003c\"--AirForce is fighting!\"\u003c\u003cendl; } }; //J-20隐形歼击机 class J_20: public AirForce { public: void fight(){ cout\u003c\u003c\"----J-20(Fighter Plane) is fighting!\"\u003c\u003cendl; } }; //CH5无人机 class CH_5: public AirForce { public: void fight(){ cout\u003c\u003c\"----CH-5(UAV) is fighting!\"\u003c\u003cendl; } }; //轰6K轰炸机 class H_6K: public AirForce { public: void fight(){ cout\u003c\u003c\"----H-6K(Bomber) is fighting!\"\u003c\u003cendl; } }; int main(){ Troops *p = new Troops; p -\u003efight(); //陆军 p = new Army; p -\u003efight(); p = new _99A; p -\u003e fight(); p = new WZ_10; p -\u003e fight(); p = new CJ_10; p -\u003e fight(); //空军 p = new AirForce; p -\u003e fight(); p = new J_20; p -\u003e fight(); p = new CH_5; p -\u003e fight(); p = new H_6K; p -\u003e fight(); return 0; } 这个例子中的派生类比较多,如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱;而有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数。 从这个例子中也可以发现,对于具有复杂继承关系的大中型程序,多态可以增加其灵活性,让代码更具有表现力。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"虚函数\r虚函数指的就是虚成员函数(构造函数和静态成员函数除外),因为普通函数(非成员函数)是无法声明为虚函数的。而普通函数无法声明为虚函数的原因是 「虚函数的作用就是形成多态」,而形成多态的第一个条件就是存在继承关系,普通函数很明显是不存在继承关系的,所以无法声明为虚函数。 只需在声明成员函数时在函数的返回值前使用 virtual 关键字,就可将一个成员函数声明为虚函数。跟静态成员函数一样,在类外定义虚函数时无需再加上virtual关键字。虚函数被派生类继承后仍然是虚函数,当你需要重写继承的虚函数时,只能重写其函数体,而不能修改其函数签名(即返回值、函数名、参数列表)。且建议在该虚函数的参数列表后使用 override 关键字,告诉编译器此函数是重写的虚函数,以便编译器在编译时检查重写是否出错。如果某个基类不允许派生类重写某个虚函数时,使用关键字 final 来实现。 直接来看例子: #include\u003ciostream\u003e using namespace std; class Base { public: virtual void display(); // 将此函数声明为虚函数 }; class Derived: public Base { public: void display() override; //使用 override 关键字告诉编译器此函数是重写基类的虚函数 }; void Base::display() { cout \u003c\u003c \"I'm Base class!\" \u003c\u003c endl; } void Derived::display() { cout \u003c\u003c \"I'm Derived class!\" \u003c\u003c endl; } int main() { Base *Bp = new Base; Bp-\u003edisplay(); Derived *Dp = new Derived; Dp-\u003edisplay(); cout \u003c\u003c \"赋值后:\" \u003c\u003c endl; Bp = Dp; Bp-\u003edisplay(); return 0; } /* 运行结果: I'm Base class! I'm Derived class! 赋值后: I'm Derived class! */ 使用多态后,当我们将派生类指针赋值给基类指针后,可以发现该基类指针也可以调用派生类的成员函数了! 在这里再总结一下:虚函数的唯一作用就是构成多态,而使用多态的目的就是使得基类指针(引用)能够访问派生类的成员函数。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"虚函数表vtable\r在最开始时将派生类指针赋值给基类指针后,基类指针并不能访问派生类的成员函数,而使用虚函数后为什么就可以了呢? 这是因为使用了虚函数后,在创建对象时会额外地增加一个虚函数表和一个虚函数表指针,当通过基类指针调用虚函数时,程序会在运行时通过虚函数表指针找到该对象的虚函数表,在表中找到对应的虚函数的地址来调用该虚函数。 虚函数表简称 vtable,其本质就是一个数组,该数组存储了该类每一个虚函数的入口地址。虚函数表与对象是分开存储的,为了将虚函数表和对象关联起来,编译器会在对象中添加一个指针(该指针放在内存空间起始位置),该指针指向该对象的虚函数表的起始位置。 我们将通过下面这个例子来进行详细讲解: #include \u003ciostream\u003e #include \u003cstring\u003e using namespace std; class People { public: People(const string name, const int age): _name(name), _age(age){} virtual void display() const; virtual void studying(); protected: string _name; int _age; }; void People::display() const { cout \u003c\u003c \"Class People:\" \u003c\u003c _name \u003c\u003c \"今年\" \u003c\u003c _age \u003c\u003c \"岁了。\" \u003c\u003c endl; } void People::studying() { cout \u003c\u003c \"Class People:我正在学习,请不要打扰我!!!\" \u003c\u003c endl; } class Student: public People { public: Student(const string name, const int age, const float score):People(name, age), _score(score){} public: void display() const override; virtual void examing(); protected: float _score; }; void Student::display() const { cout \u003c\u003c \"Class Student:\" \u003c\u003c _name \u003c\u003c \"今年\" \u003c\u003c _age \u003c\u003c \"岁了,考了\" \u003c\u003c _score \u003c\u003c \"分。\" \u003c\u003c endl; } void Student::examing() { cout \u003c\u003c \"Class Student:\" \u003c\u003c _name \u003c\u003c \"正在考试,请不要打扰Ta啊!\" \u003c\u003c endl; } class Senior: public Student { public: Senior(const string name, const int age, const float score, const bool hasJob): Student(name, age, score), _hasJob(hasJob){} void display() const override; virtual void partying(); private: bool _hasJob; }; void Senior::display() const { if(_hasJob) { cout \u003c\u003c \"Class Senior:\" \u003c\u003c _name \u003c\u003c \"以\" \u003c\u003c _score \u003c\u003c \"的成绩从大学毕业了,并找到了工作,Ta今年\" \u003c\u003c _age \u003c\u003c \"岁。\" \u003c\u003c endl; } else { cout \u003c\u003c \"Class Senior:\" \u003c\u003c _name \u003c\u003c \"以\" \u003c\u003c _score \u003c\u003c \"的成绩从大学毕业了,但没找到工作,Ta今年\" \u003c\u003c _age \u003c\u003c \"岁。\" \u003c\u003c endl; } } void Senior::partying() { cout \u003c\u003c \"Class Senior:快毕业了,大家正在吃散伙饭...\" \u003c\u003c endl; } int main(){ People *p = new People(\"李一\", 29); p -\u003e display(); p = new Student(\"李二\", 16, 84.5); p -\u003e display(); p = new Senior(\"李三\", 24, 99.9, true); p -\u003e display(); return 0; } /* 运行结果: Class People:李一今年29岁了。 Class Student:李二今年16岁了,考了84.5分。 Class Senior:李三以99.9的成绩从大学毕业了,并找到了工作,Ta今年24岁。 */ 上面这个例子的各个类的对象的内存空间如下图所示: 仔细观察虚函数表,可以发现基类的虚函数在vtable中的位置是固定的,不会随着继承层次的增加而改变,如果派生类重写了基类的虚函数,那么将使用派生类的虚函数替换基类对应的虚函数,且派生类新增的虚函数会添加到虚函数表的底部。 当通过指针调用虚函数时,会先根据指针找到 vptr,再根据 vfptr 找到虚函数的入口地址。以虚函数 display() 为例,它在 vtable 中的索引为0,通过 p -\u003e display(); 调用时,编译器内部会发生类似下面的转换: ( *( *(p+0) + 0 ) )(p); 0是vptr在对象内存空间中的偏移,p+0表示vptr的地址,*(p+0)表示vptr的值,即虚函数表的起始地址。display()在虚函数表中的下标为0,所以( *(p+0) + 0 )就是display()的地址,*( *(p+0) + 0 )(p)就是对display()的调用,这里的p就是调用display()时使用的实参,它会赋值给this指针。 那么对应p -\u003e studying();就会被转换为 ( *( *(p+0) + 1 ) )(p); 注意: 别忘了p是People对象的指针,它只能调用display()和studying()函数,虚函数只是使得它能调用派生类对应的成员函数。对于有需要的函数才将其设置为虚函数,其他函数请不要设置为虚函数,这会降低程序运行效率。 以上是针对单继承进行的讲解。当存在多继承时,虚函数表的结构就会变得复杂,尤其是有虚继承时,还会增加虚基类表,更加让人抓狂,这里我们就不分析了,有兴趣的读者可以自行研究。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"虚析构函数\r构造函数是不能被声明为虚函数的,因为调用构造函数时虚函数表尚不存在,此时无法通过查询虚函数表来找到要调用的函数是哪个。 析构函数的作用为在销毁对象时进行清理工作,此时虚函数表早已存在,所以可以声明为虚函数,而且有时候必须要声明为虚函数。 直接看例子: #include\u003ciostream\u003e using namespace std; class Base { public: Base(); ~Base(); private: int * a; }; Base::Base() { cout \u003c\u003c \"Base constructor!\" \u003c\u003c endl; a = new int[10]; } Base::~Base() { cout \u003c\u003c \"Base destructor!\" \u003c\u003c endl; delete[] a; } class Derived: public Base { public: Derived(); // 这里没有显示调用基类构造函数,编译器会自动调用基类的默认构造函数 ~Derived(); private: int * b; }; Derived::Derived() { cout \u003c\u003c \"Derived constructor!\" \u003c\u003c endl; b = new int[1000]; } Derived::~Derived() { cout \u003c\u003c \"Derived destructor!\" \u003c\u003c endl; delete[] b; } int main() { Base *bp = new Derived; delete bp; cout \u003c\u003c \"-------------------------\" \u003c\u003c endl; Derived *dp = new Derived; delete dp; return 0; } /* 运行结果: Base constructor! Derived constructor! Base destructor! ------------------------- Base constructor! Derived constructor! Derived destructor! Base destructor! */ 在本例中,bp、dp分别表示基类指针和派生类指针,它们都指向派生类对象。最后使用delete bp只调用了基类的析构函数,没有调用派生类的析构函数,导致派生类申请的1000个int空间未被释放,造成了内存泄漏;使用delete dp则正确的先调用了派生类的析构函数,再调用基类的析构函数,申请的内存空间都正确释放掉了。 1)为什么delete bp不会调用派生类的析构函数? 因为这里的析构函数不是虚函数,通过指针访问非虚函数时,编译器会根据指针类型来确定要调用的函数。所以,无论bp指向谁,调用的始终都是基类的析构函数。 2)为什么delete dp能够调用基类的析构函数? 在前面我们已经说了,在派生类的析构函数中编译器会自动调用基类的析构函数,且调用顺序为派生类的析构函数→基类的析构函数。 哪该怎么让基类指针指向派生类对象时,执行delete bp可以调用派生类的析构函数呢?没错,就是将基类的析构函数声明为虚函数,这样派生类的析构函数也会自动成为虚函数。这个时候编译器就会根据指针的指向来调用析构函数啦!修改上面的示例,将基类析构函数声明为虚函数: class Base { public: Base(); virtual ~Base(); // 只修改了这里 private: int * a; }; /* 运行结果: Base constructor! Derived constructor! Derived destructor! Base destructor! ------------------------- Base constructor! Derived constructor! Derived destructor! Base destructor! */ 在实际开发中,自己定义析构函数就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"纯虚函数和抽象类\r在C++中,可以将虚函数声明为纯虚函数,语法为: virtual 返回值 函数名(参数列表) = 0; 纯虚函数指的就是没有函数体的虚函数,包含纯虚函数的类称为抽象类。之所以说它抽象,是因为它无法实例化。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。 抽象类通常是作为基类,让派生类去实现纯虚函数,派生类必须实现纯虚函数才能被实例化。一般将虚函数声明为纯虚函数的情况有: 该函数并不是基类所需要的,但派生类必须要实现 基类无法实现该函数的功能,需要派生来实现 ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"typeid运算符\r在多态环境下,如果你需要在运行时检查某个对象的实际类型,可以使用 typeid 来实现。这在处理复杂的继承层次或实现特定逻辑时可能有用。 if (typeid(*basePtr) == typeid(Derived)) { // Perform some operation specific to Derived type } ","date":"2024-08-07","objectID":"/posts/3b2b064/:6:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"运算符重载\r","date":"2024-08-07","objectID":"/posts/3b2b064/:7:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"运算符重载基础教程\r运算符重载也被称为操作符重载,和函数重载一样,运算符重载的目的是 「使得同一个运算符具有多种功能」。运算符重载是一种优雅的对象操作技术,通过重载运算符,可以让自定义类型的使用更加自然和符合直觉。 实际上,我们已经在不知不觉中使用了运算符重载。例如,+号可以对不同类型(int、float 等)的数据进行加法操作;\u003c\u003c 既是位移运算符,又可以配合 cout 向控制台输出数据。C++ 本身已经对这些运算符进行了重载。C++ 也允许程序员自己重载运算符,这给我们带来了很大的便利。 运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数。所以,运算符重载的本质就是函数重载。 运算符重载的语法为: 返回值类型 operator运算符(参数列表) { ...... } 运算符重载的基本规则为: 只能重载已经存在的运算符,不能创造新的运算符(比如 @)。 至少有一个操作数必须是用户定义的类型(防止重载基本数据类型的运算符)。 不能违法运算符原来的语法规则。例如%本来有两个操作数,不能重载为只有一个。 不能修改运算符优先级。 不能重载以下运算符:::(作用域解析)、. *(指向成员的指针)、sizeof(对象或类型的大小)、typeid(对象或类型的信息)、?:(三目运算符) 运算符重载的方式: 运算符可以通过成员函数或非成员函数(通常是友元函数)来重载。一般情况下,选择任意一种方式都行,但单目运算符最好重载为成员函数,双目运算符最好重载为友元函数。 但这些运算符只能通过成员函数来重载: =、()、[]、→。 例如重载了 + 操作符时, 对于 a + b 不同的重载方法等价的结果不同 重载为成员函数:a + b 等价于 a.operator+(b) 重载为友元函数:a + b 等价于 operator+(a, b) 推荐双目运算符重载为友元函数的原因\r对于一个时间类的对象a来说 a * 3.7 和 3.7 * a 是等价的,但是如果将 * 重载为该类的成员函数, 3.7 * a 是无法正常处理的。 下面我们将举一些重载运算符的示例,其他运算符如:new、delete、()等也可以重载,有需要时请自行了解。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:7:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"重载数学运算符\r四则运算符(+、-、、/、+=、-=、=、/=)和关系运算符(\u003e、\u003c、\u003c=、\u003e=、==、!=)都是数学运算符,它们在实际开发中非常常见,被重载的几率也很高,并且有着相似的重载格式。本节以复数类 Complex 为例对它们进行重载,重在演示运算符重载的语法以及规范。 复数能够进行完整的四则运算,但不能进行完整的关系运算:我们只能判断两个复数是否相等,但不能比较它们的大小,所以在该类中不能对 \u003e、\u003c、\u003c=、\u003e= 进行重载。下面是具体的代码: #include \u003ciostream\u003e #include \u003ccmath\u003e using namespace std; class Complex{ public: Complex(double real = 0.0, double imag = 0.0): _real(real), _imag(imag){ } public: // 运算符重载 // 以友元函数的形式重载 friend Complex operator+(const Complex \u0026c1, const Complex \u0026c2); friend Complex operator-(const Complex \u0026c1, const Complex \u0026c2); friend Complex operator*(const Complex \u0026c1, const Complex \u0026c2); friend Complex operator/(const Complex \u0026c1, const Complex \u0026c2); friend bool operator==(const Complex \u0026c1, const Complex \u0026c2); friend bool operator!=(const Complex \u0026c1, const Complex \u0026c2); // 以成员函数的形式重载 Complex \u0026 operator+=(const Complex \u0026c); Complex \u0026 operator-=(const Complex \u0026c); Complex \u0026 operator*=(const Complex \u0026c); Complex \u0026 operator/=(const Complex \u0026c); public: //成员函数 double get_real() const{ return _real; } double get_imag() const{ return _imag; } private: double _real; // 实部 double _imag; // 虚部 }; Complex operator+(const Complex \u0026c1, const Complex \u0026c2) { Complex c; c._real = c1._real + c2._real; c._imag = c1._imag + c2._imag; return c; } Complex operator-(const Complex \u0026c1, const Complex \u0026c2) { Complex c; c._real = c1._real - c2._real; c._imag = c1._imag - c2._imag; return c; } // (a+bi) * (c+di) = (ac-bd) + (bc+ad)i Complex operator*(const Complex \u0026c1, const Complex \u0026c2) { Complex c; c._real = c1._real * c2._real - c1._imag * c2._imag; c._imag = c1._imag * c2._real + c1._real * c2._imag; return c; } // (a+bi) / (c+di) = [(ac+bd) / (c²+d²)] + [(bc-ad) / (c²+d²)]i Complex operator/(const Complex \u0026c1, const Complex \u0026c2) { Complex c; c._real = (c1._real * c2._real + c1._imag * c2._imag) / (pow(c2._real, 2) + pow(c2._imag, 2)); c._imag = (c1._imag * c2._real - c1._real * c2._imag) / (pow(c2._real, 2) + pow(c2._imag, 2)); return c; } bool operator==(const Complex \u0026c1, const Complex \u0026c2) { if( c1._real == c2._real \u0026\u0026 c1._imag == c2._imag ) { return true; } else { return false; } } bool operator!=(const Complex \u0026c1, const Complex \u0026c2) { if( c1._real != c2._real || c1._imag != c2._imag ) { return true; } else { return false; } } Complex \u0026 Complex::operator+=(const Complex \u0026c) { this-\u003e_real += c._real; this-\u003e_imag += c._imag; return *this; } Complex \u0026 Complex::operator-=(const Complex \u0026c) { this-\u003e_real -= c._real; this-\u003e_imag -= c._imag; return *this; } Complex \u0026 Complex::operator*=(const Complex \u0026c) { this-\u003e_real = this-\u003e_real * c._real - this-\u003e_imag * c._imag; this-\u003e_imag = this-\u003e_imag * c._real + this-\u003e_real * c._imag; return *this; } Complex \u0026 Complex::operator/=(const Complex \u0026c) { this-\u003e_real = (this-\u003e_real*c._real + this-\u003e_imag*c._imag) / (pow(c._real, 2) + pow(c._imag, 2)); this-\u003e_imag = (this-\u003e_imag*c._real - this-\u003e_real*c._imag) / (pow(c._real, 2) + pow(c._imag, 2)); return *this; } int main() { Complex c1(25, 35); Complex c2(10, 20); Complex c3(1, 2); Complex c4(4, 9); Complex c5(34, 6); Complex c6(80, 90); Complex c7 = c1 + c2; Complex c8 = c1 - c2; Complex c9 = c1 * c2; Complex c10 = c1 / c2; cout \u003c\u003c \"c7 = \" \u003c\u003c c7.get_real() \u003c\u003c \" + \" \u003c\u003c c7.get_imag() \u003c\u003c \"i\" \u003c\u003c endl; cout \u003c\u003c \"c8 = \" \u003c\u003c c8.get_real() \u003c\u003c \" + \" \u003c\u003c c8.get_imag() \u003c\u003c \"i\" \u003c\u003cendl; cout \u003c\u003c \"c9 = \" \u003c\u003c c9.get_real() \u003c\u003c \" + \" \u003c\u003c c9.get_imag() \u003c\u003c \"i\" \u003c\u003cendl; cout \u003c\u003c \"c10 = \"\u003c\u003c c10.get_real() \u003c\u003c \" + \" \u003c\u003c c10.get_imag() \u003c\u003c \"i\" \u003c\u003cendl; c3 += c1; c4 -= c2; c5 *= c2; c6 /= c2; cout \u003c\u003c \"c3 = \" \u003c\u003c c3.get_real() \u003c\u003c \" + \" \u003c\u003c c3.get_imag() \u003c\u003c \"i\" \u003c\u003c endl; cout \u003c\u003c \"c4 = \" \u003c\u003c c4.get_real() \u003c\u003c \" + \" \u003c\u003c c4.get_imag() \u003c\u003c \"i\" \u003c\u003c endl; cout \u003c\u003c \"c5 = \" \u003c\u003c c5.get_real() \u003c\u003c \" + \" \u003c\u003c c5.get_imag() \u003c\u003c \"i\" \u003c\u003c endl; cout \u003c\u003c \"c6 = \" \u003c\u003c c6.get_real() \u003c\u003c \" + \" \u003c\u003c c6.get_imag() \u003c\u003c \"i\" \u003c\u003c endl; if(c1 == c2) { cout \u003c\u003c \"c1 == c2\" \u003c\u003c endl; } if(c1 != c2) { cout \u003c\u003c \"c1 != c2\" \u003c\u003c endl; } return 0; } /* 运行结果: c7 = 35 + 55i c8 = 15 + 15i c9 = -450 + 850i c10 = 1.9 + -0.3i c3 = 26 + 37i c4 = -6 + -11i ","date":"2024-08-07","objectID":"/posts/3b2b064/:7:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"重载 \u003c\u003c 和 \u003e\u003e\r在C++中,标准库本身已经对左移运算符\u003c\u003c和右移运算符\u003e\u003e分别进行了重载,使其能够用于不同数据的输入输出,但是输入输出的对象只能是 C++ 内置的数据类型(例如 bool、int、double 等)和标准库所包含的类类型(例如 string、complex、ofstream、ifstream 等)。如果我们自己定义了一种新的数据类型,需要用输入输出运算符去处理,那么就必须对它们进行重载。 cout 是 ostream 类的对象,cin 是 istream 类的对象,要想达到这个目标,就必须以友元函数的形式重载\u003c\u003c和\u003e\u003e,否则就要修改标准库中的类,这显然不是我们所期望的。 在刚刚的Complex类添加以下两个成员函数: class Complex { ...... friend istream \u0026 operator\u003e\u003e(istream \u0026 in, complex \u0026 A) // 返回引用是以便可以进行连续读入操作,如 cin \u003e\u003e a \u003e\u003e b; { in \u003e\u003e A.real \u003e\u003e A.imag; return in; } friend ostream \u0026 operator\u003c\u003c(ostream \u0026 out, complex \u0026 A) // 同样返回引用是以便进行连续输出操作 { out \u003c\u003c A.real \u003c\u003c\" + \"\u003c\u003c A.imag \u003c\u003c\" i \"; return out; } }; ","date":"2024-08-07","objectID":"/posts/3b2b064/:7:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"重载[]\rC++ 规定,下标运算符[ ]必须以成员函数的形式进行重载。该重载函数在类中的声明格式如下: 返回值类型 \u0026 operator[ ] (参数); 或者: const 返回值类型 \u0026 operator[ ] (参数) const; 在实际开发中,我们应该同时提供这两种以上两种形式,这样做是为了适应const对象,因为const对象只能调用const成员函数。 注意: 当你需要自定义类的下标访问行为时,才重载[],否则无需重载该运算符。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:7:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"重载++和--\r自增++和自减--都是一元运算符,它的前置形式(类似++i)和后置形式(类似i++)都可以被重载。前置形式的重载参数列表为空,且返回对象的引用;后置形成的重载参数列表为一个int变量(一般命名为dummy,无实际作用,只是为了区分前置版本)、且返回操作前的对象副本。 如下所示: #include \u003ciostream\u003e #include \u003ccmath\u003e using namespace std; class Counter { public: Counter(int value) : _count(value) {} // 自增前置版本 Counter\u0026 operator++() { _count++; return *this; } // 自增后置版本 Counter operator++(int dummy) { Counter temp = *this; // 保存当前状态 _count++; return temp; // 返回增加前的副本 } // 自减前置版本 Counter\u0026 operator--() { _count--; return *this; } // 自减后置版本 Counter operator--(int dummy) { Counter temp = *this; // 保存当前状态 _count--; return temp; // 返回减少前的副本 } friend ostream\u0026 operator\u003c\u003c (ostream \u0026out, Counter \u0026c) { out \u003c\u003c c._count; } private: int _count; }; int main() { Counter c(1); cout \u003c\u003c \"start c =\" \u003c\u003c c \u003c\u003c endl; ++c;// 调用前置版本 c++;// 调用后置版本 --c; c--; cout \u003c\u003c \"end c =\" \u003c\u003c c \u003c\u003c endl; return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:7:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"模板\r泛型程序设计是一种算法在实现时不指定具体要操作的数据类型的程序设计方法。所谓“泛型”,指的是 「算法只要实现一遍,就能适用于多种数据类型」。泛型程序设计方法的优势在于能够减少重复代码的编写。在C++中泛型程序设计是使用模板来实现的,模板又分为函数模板和类模板。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:8:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"函数模板\r当我们需要编写多个参数类型不同但完成的工作都相同的函数时,可以使用函数模板。函数模板本身是一个蓝图,并不是函数,是编译器用来产生拥有具体类型参数的函数的模板(蓝图、设计图)。 比如,当我们想要编写 swap() 函数用于交换两个变量的值时,就需要为 int、double、char等变量类型都编写对应的 swap() 函数,这样效率是低下的、难以维护且不优雅,这时就可以使用函数模板来实现这一功能。 定义一个函数模板的基本语法如下: template \u003ctypename T1, typename T2, ......\u003e void functionName(T1 parameter1, T2 parameter2, ......) { // 函数体 } template 是用于定义模板的关键字。 \u003ctypename T1, typename T2\u003e 定义模板参数T1和T2,跟函数参数一样(T1,T2是推荐的参数名,是可修改的)。typename可以使用class来替换,但不推荐。 T1、T2 均是模板参数类型,在编写函数时使用,用于取代具体的数据类型。 例如,一个交换两个同类型变量的值的函数模板可以写为: template \u003ctypename T\u003e void swap(T \u0026a, T \u0026b) { T temp = a; a = b; b = temp; } 请注意: 模板函数(指由函数模板生成的函数)与模板函数、模板函数与常规函数均能实现重载,请看示例代码: #include \u003ciostream\u003e // 常规函数 void func(int x) { std::cout \u003c\u003c \"Non-template function called with int: \" \u003c\u003c x \u003c\u003c std::endl; } // 函数模板1 template\u003ctypename T\u003e void func(T x) { std::cout \u003c\u003c \"Template function1 called with value: \" \u003c\u003c x \u003c\u003c std::endl; } // 函数模板2 template\u003ctypename T\u003e void func(T x, int n) { std::cout \u003c\u003c \"Template function2 called with value: \" \u003c\u003c x \u003c\u003c std::endl; } int main() { func(10); // 调用常规函数 func(10.5); // 调用由函数模板1实例化的模板函数 func(10.3, 7); // 调用由函数模板2实例化的模板函数 } 有时对于某种特定类型,我们不想使用通用的函数模板,而是想要有些特定的操作时,可以使用显示具体化来完成此要求。比如在上述例子中,对于两个结构体 job 变量a, b(该结构体包含 姓名、薪资、职位 成员),我们只想交换两个变量的薪资和职位的值,这时就可以采用显示具体化来实现,代码如下: typedef struct job { string name; double salary; string position; }job; template \u003ctypename T\u003e void Swap(T \u0026a, T \u0026b) { T temp = a; a = b; b = temp; } //显示具体化 template \u003c\u003e void Swap\u003cjob\u003e(job \u0026j1, job \u0026j2) { string position_temp = j1.position; double salary_temp = j1.salary; j1.position = j2.position; j1.salary = j2.salary; j2.position = position_temp; j2.salary = salary_temp; } int main() { job a, b; ······ Swap(a, b); //调用的是显示具体化创建的函数,只交换这两个变量的薪资和职位的值 } ","date":"2024-08-07","objectID":"/posts/3b2b064/:8:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"类模板\rC++ 除了支持函数模板,还支持类模板。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。 声明类模板的语法为: template \u003ctypename T1, typename T2, ...\u003e class 类名 { ... }; 一旦声明了类模板,就可以将类型参数用于成员变量和成员函数了。换句话说,就可以把T1、T2、…当作int、double等来用。 假如我们现在要定义一个类来表示坐标,要求坐标的数据类型可以是整数、小数和字符串,这个时候就可以使用类模板: template\u003ctypename T1, typename T2\u003e class Point { public: Point(T1 x, T2 y): _x(x), _y(y){ } public: T1 get_x() const; //获取x坐标 void set_x(T1 x); //设置x坐标 T2 get_y() const; //获取y坐标 void set_y(T2 y); //设置y坐标 private: T1 _x; //x坐标 T2 _y; //y坐标 }; x 坐标和 y 坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了。 上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍需带上模板头,格式为: template\u003ctypename T1, typename T2, ...\u003e 返回值类型 类名\u003c类型参数1 , 类型参数2, ...\u003e::函数名(形参列表) { ...... } 下面就对Point类的成员函数进行定义: template\u003ctypename T1, typename T2\u003e //模板头 T1 Point\u003cT1, T2\u003e::get_x() const { return _x; } template\u003ctypename T1, typename T2\u003e void Point\u003cT1, T2\u003e::set_x(T1 x) { _x = x; } template\u003ctypename T1, typename T2\u003e T2 Point\u003cT1, T2\u003e::get_y() const { return _y; } template\u003ctypename T1, typename T2\u003e void Point\u003cT1, T2\u003e::set_y(T2 y) { _y = y; } 完成了类模板的编写就可以使用其来创建对象,将上面的代码综合起来再加上创建对象的代码就可以得到完整的代码: #include \u003ciostream\u003e using namespace std; template\u003ctypename T1, typename T2\u003e class Point { public: Point(T1 x, T2 y): _x(x), _y(y){ } public: T1 get_x() const; //获取x坐标 void set_x(T1 x); //设置x坐标 T2 get_y() const; //获取y坐标 void set_y(T2 y); //设置y坐标 private: T1 _x; //x坐标 T2 _y; //y坐标 }; template\u003ctypename T1, typename T2\u003e //模板头 T1 Point\u003cT1, T2\u003e::get_x() const { return _x; } template\u003ctypename T1, typename T2\u003e void Point\u003cT1, T2\u003e::set_x(T1 x) { _x = x; } template\u003ctypename T1, typename T2\u003e T2 Point\u003cT1, T2\u003e::get_y() const { return _y; } template\u003ctypename T1, typename T2\u003e void Point\u003cT1, T2\u003e::set_y(T2 y) { _y = y; } int main() { // 类模板实例化时必须指定具体的数据类型 Point\u003cint, int\u003e p1(10, 20); cout \u003c\u003c \"x=\" \u003c\u003c p1.get_x() \u003c\u003c \", y=\" \u003c\u003c p1.get_y() \u003c\u003c endl; Point\u003cint, char*\u003e p2(10, \"东经180度\"); cout \u003c\u003c \"x=\" \u003c\u003c p2.get_x() \u003c\u003c \", y=\" \u003c\u003c p2.get_y() \u003c\u003c endl; // 实例化对象指针时,赋值号两边都要指明具体的数据类型,且要保持一致。 Point\u003cchar*, char*\u003e *p3 = new Point\u003cchar*, char*\u003e(\"东经180度\", \"北纬210度\"); cout \u003c\u003c \"x=\" \u003c\u003c p3-\u003eget_x() \u003c\u003c \", y=\" \u003c\u003c p3-\u003eget_y() \u003c\u003c endl; return 0; } /* 运行结果: x=10, y=20 x=10, y=东经180度 x=东经180度, y=北纬210度 */ 注意:类模板实例化对象时必须指定具体的数据类型;实例化对象指针时必须在赋值号两边都指定具体的数据类型,具体请参考示例代码。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:8:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"decltype关键字(C++11)\r当我们在编写模板时,可能会遇到不知道该定义什么类型的变量的情况,如下所示: template \u003ctypename T1, typename T2\u003e void func(T1 x, T2, y) { ... xpy = x + y; // 这里的 xpy该定义为什么类型呢? ... } 这里的 xpy 在定义时应该定义为什么类型呢?由于无法预先知道x,y的类型,所以 xpy 的类型也就无法确定。这时,就可以使用关键字 decltype 来解决此问题,其用法如下: decltype(expression) var_name = expression; 编译器会根据 expression 的运算结果正确推导出 var_name 的类型,所以上述代码可以改为: decltype(x + y) xpy = x + y; 我们对于模板的介绍就到此为止了,虽然模板还有很多内容,但在我们实际编程中,上面的模板知识大多数情况下都是够用的了。对模板感兴趣的可以自行查阅相关资料进行学习。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:8:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"STL(标准模板库)\rSTL(Standard Template Library)标准模板库是 C++ 标准库中的一部分,标准模板库为 C++ 提供了完善的数据结构及算法。 STL 标准模板库包括三部分:容器、算法和迭代器。容器是对象的集合,STL 的容器有:vector、stack、queue、deque、list、set 和 map 等。STL 算法是对容器进行处理,比如排序、合并等操作。迭代器则是访问容器的一种机制。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"容器\r容器用于存放数据的类模板。可变长数组、大根堆等数据结构在 STL 中都被实现为容器。使用容器时,即将容器类模板实例化为容器类时,会指明容器中存放的元素是什么类型的。 容器中可以存放基本类型的变量,也可以存放对象。对象或基本类型的变量被插入容器中时,实际插入的是对象或变量的一个复制品。 STL 中的许多算法(即函数模板),如排序、查找等算法,在执行过程中会对容器中的元素进行比较。这些算法在比较元素是否相等时通常用运算符进行,比较大小通常用 \u003c 运算符进行,因此,被放入容器的对象所属的类最好重载 ==和\u003c 运算符,以使得两个对象用==和\u003c进行比较是有定义的。 容器可分为顺序容器和关联容器两种,同类型的容器对象可以使用\u003c、\u003c=、\u003e、\u003e=、==、!=进行比较。 顺序容器\r顺序容器的特点为:存储的元素在容器中的位置与元素值无关,元素在插入时可以指定插入位置。 常用的顺序容器只有三种:动态数组 vector、双端队列 deque、双向链表 list。 关联容器\r关联容器的特点为:默认情况下,存储的元素按照关键字从小到大进行排序(使用\u003c运算符来进行比较大小),插入时不能指定插入位置。因为是排好序的,所以关联容器在查找时具有非常好的性能。 常用的关联容器有:set、multiset、map、multimap。关联容器内的元素是排好序的。 除了以上两类容器外,STL 还在两类容器的基础上屏蔽一部分功能,突出或增加另一部分功能,实现了三种容器适配器:栈 stack、队列 queue、优先级队列 priority_queue。 为称呼方便起见,本教程后面将容器和容器适配器统称为容器。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"迭代器\r使用“迭代器(iterator)”是访问容器元素的通用方法。迭代器就是一个变量,指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。 迭代器按照定义方式可以分为以下四种: 正向迭代器:容器类名::iterator 迭代器名; 常量正向迭代器:容器类名::const_iterator 迭代器名; 反向迭代器:容器类名::reverse_iterator 迭代器名; 常量反向迭代器:容器类名::const_reverse_iterator 迭代器名; 注意: 所有容器(不包括容器适配器)都能定义正向迭代器和常量正向迭代器,但不是所有容器都能定义反向迭代器和常量反向迭代器。 *迭代器名 就可访问迭代器指向的元素,通过非常量迭代器还能修改其指向的元素。 迭代器都可以进行++操作,正向迭代器++,就会指向后一个元素;反向迭代器++就会指向前一个元素。 直接来看例子吧: #include \u003ciostream\u003e #include \u003cvector\u003e using namespace std; int main() { vector\u003cint\u003e v; // 定义一个动态数组v for (int n = 0; n \u003c 5; ++n) v.push_back(n); //push_back成员函数用于在容器尾部添加一个元素 vector\u003cint\u003e::iterator i; //定义正向迭代器 for (i = v.begin(); i != v.end(); ++i) //用迭代器遍历容器 { cout \u003c\u003c *i \u003c\u003c \" \"; //*i 就是迭代器i指向的元素 (*i) *= 2; // 每个元素变为原来的2倍 } cout \u003c\u003c endl; // 用反向迭代器遍历容器 for (vector\u003cint\u003e::reverse_iterator j = v.rbegin(); j != v.rend(); ++j) cout \u003c\u003c *j \u003c\u003c \" \"; return 0; } /* 运行结果: 0 1 2 3 4 8 6 4 2 0 */ 迭代器按照功能强弱分为输入、输出、正向、双向、随机访问五种,这里是介绍常用的三种(假设p为对应类型的迭代器)。 1)正向迭代器:支持++p、p++、*p操作。此外,两个正向迭代器可以互相赋值,还可以使用 == 和!=来进行比较。 2)双向迭代器:除了支持正向迭代器的全部功能外,还支持--p和p--操作。 3)随机访问迭代器:除了支持双向迭代器的全部功能外,还支持以下操作: p += i; 使得 p 往后移动 i 个元素 p -= i; 使得 p 往前移动 i 个元素 p + i; 返回 p 后面第 i 个元素的迭代器 p - i; 返回 p 前面第 i 个元素的迭代器 p[i]; 返回 p 后面第 i 个元素的引用 下表为各个容器的迭代器功能: 容器 迭代器功能 vector 随机访问 string 随机访问 deque 随机访问 list 双向 set/multiset 双向 map/multimap 双向 stack 不支持迭代器 queue 不支持迭代器 priority_queue 不支持迭代器 注意: 容器适配器是没有迭代器的!!! ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"STL算法\rSTL 提供能在各种容器中通用的算法(大约有70种),如插入、删除、查找、排序等。算法就是函数模板。算法通过迭代器来操纵容器中的元素。 许多算法操作的是容器上的一个区间(也可以是整个容器),因此需要两个参数,一个是区间起点元素的迭代器,另一个是区间终点元素的后面一个元素的迭代器。例如,排序和查找算法都需要这两个参数来指明待排序或待查找的区间。 有的算法返回一个迭代器。例如,find 算法在容器中查找一个元素,并返回一个指向该元素的迭代器。 有的算法会改变其所作用的容器。例如: copy:将一个容器的内容复制到另一个容器。 remove:在容器中删除一个元素。 random_shuffle:随机打乱容器中的元素。 fill:用某个值填充容器。 有的算法不会改变其所作用的容器。例如: find:在容器中查找元素。 count_if:统计容器中符合某种条件的元素的个数。 算法可以处理容器,也可以处理普通的数组。 STL 中的大部分常用算法都在头文件 algorithm 中定义。此外,头文件 numeric 中也有一些算法。 algorithm下常用的函数: max(x, y) min(x, y) abs(x) //x必须为int类型 fabs(x) //x可为浮点型 swap(x, y) reverse(it1, it2) //反转区间[it1, it2) 内的元素 fill(a, a + len, 0) //将数组a的元素全部赋值为0,和memset功能一样 sort(a, a + x, cmp) //cmp可选 //在[first, last)范围内寻找第一个值大于等于 val的元素的位置,如果是数组返回指针,容器返回迭代器 lower_bound(first, last, val) //在[first, last)范围内寻找第一个值大于val的元素的位置,如果是数组返回指针,容器返回迭代器 upper_bound(first, last, val) next_permutation(a, a + len) // 给出一个序列在全排列中的下一个序列,可以搭配 do while 来使用 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"STL中的大、小和相等概念\rSTL 中关联容器内部的元素是排序的。STL 中的许多算法也涉及排序、查找。这些容器和算法都需要对元素进行比较,有的比较是否相等,有的比较元素大小。 在 STL 中,默认情况下,比较大小是通过**\u003c**运算符进行的,和\u003e运算符无关。在STL中提到“大”、“小”的概念时,以下三个说法是等价的: x 比 y 小。 表达式x\u003cy为真。 y 比 x 大。 一定要注意,y比x大意味着x\u003cy为真,而不是y\u003ex为真。y\u003ex的结果如何并不重要,甚至y\u003ex是没定义的都没有关系。 在 STL 中,x和y相等也往往不等价于x==y为真。对于在未排序的区间上进行的算法,如顺序查找算法 find,查找过程中比较两个元素是否相等用的是==运算符;但是对于在排好序的区间上进行查找、合并等操作的算法(如折半查找算法 binary_search,关联容器自身的成员函数 find)来说,x和y相等是与x\u003cy和y\u003cx同时为假等价的,与==运算符无关。看上去x\u003cy和y\u003cx同时为假就应该和x==y为真等价,其实不然。例如下面的 class A: class A { int v; public: bool operator\u003c (const A \u0026 a)const {return false;} }; 可以看到,对任意两个类 A 的对象 x、y,x\u003cy和y\u003cx都是为假的。也就是说,对 STL 的关联容器和许多算法来说,任意两个类 A 的对象都是相等的,这与==运算符的行为无关。 综上所述,使用 STL 中的关联容器和许多算法时,往往需要对\u003c运算符进行适当的重载,使得这些容器和算法可以用\u003c运算符对所操作的元素进行比较。最好将\u003c运算符重载为友元函数,因为在重载为成员函数时,在有些编译器上会出错(由其 STL 源代码的写法导致)。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"vector\rvector 是顺序容器的一种,是可变长的动态数组,支持随机访问迭代器,所有 STL 算法都能对 vector 进行操作。要使用 vector,需要包含头文件 vector。 注意:vector数组是在堆空间开的。而普通数组如果是在局部区域内开(比如函数体内),则是在栈空间开的,栈空间较小。所以,如果在局部区域开的普通数组不易过大(不能超过$10^6$),而对vector数组则没有限制。 vector的定义如下: // 定义一个vector,初始化为空 vector\u003ctypename\u003e name; //typename为容器内元素类型,name为容器名称 // 定义一个vector,初始化有 n 个元素 vector\u003ctypename\u003e name(n); // 定义一个vector,初始化有 n 个元素,每个元素值均为 val vector\u003ctypename\u003e name(n, val); // 使用其他迭代器来初始化 vector vector\u003ctypename\u003e name(frist, last); // 和数组一样的初始化方式 vector\u003cint\u003e numbers1 = {1, 2, 3, 4, 5}; vector\u003cint\u003e numbers2{1, 2, 3, 4, 5}; // 拷贝初始化 vector\u003cint\u003e a(n, 0); vector\u003cint\u003e b(a); vector\u003cint\u003e c = a; // 容器元素类型如果为容器时,定义如下 vector\u003c vector\u003ctypename\u003e \u003e name; //类似二维数组,只不过是可变长的,\u003e\u003e之间需要有空格 vector常用成员函数(vi为数组名称): 代码 算法复杂度 返回值类型 含义 vi.front() O(1) 引用 返回容器中的第一个数据 vi.back() O(1) 引用 返回容器中的最后一个数据 vi.at(idx) - 引用 返回vi[idx], 会进行边界检查,如果越界会报错,比直接使用[]更好一些,常在项目中使用 vi.size() O(1) - 返回实际数据个数(unsigned类型) vi.begin() O(1) 迭代器 返回首元素的迭代器 vi.end() O(1) 迭代器 返回最后一个元素后一个位置的迭代器 vi.empty() O(1) bool 判断是否为空,为空返回真,反之返回假 vi.reserve(sz) - - 为数组提前分配sz的内存大小,即改变了capacity的大小,主要是为了防止在push_back过程中多次的内存拷贝 vi.assign(beg, end) - - 将另外一个容器[x.begin(),x.end())里的内容拷贝到vi中 vi.assign(n, val) - - 将n个val值拷贝到vi数组中,这会清除掉容器中以前的内容,vi数组的size将变为n,capacity不会改变 vi.pop_back() O(1) - 删除最后一个数据 vi.push_back(element) O(1) - 在尾部加一个数据 vi.emplace_back(ele) O(1) - 在数组中加入一个数据,和push_back功能基本一样,在某些情况下比它效率更高,支持传入多个构造参数 vi.clear() O(N) - 清除容器中的所有元素 vi.resize(n, v) - - 改变数组大小为n。如果n小于以前的大小,则保留前n个元素n,v参数不起作用;如果n大于以前的大小,则新增n-pre个元素,新增元素值为v,不指定v时默认为0 vi.insert(pos, x) O(N) - 向迭代器pos指向的位置插入元素x vi.erase(first, end) O(N) - 删除 [first, end) 的所有元素 访问vector数组中的元素有三种方法: 和普通数组一样,使用下标来访问 使用迭代器 使用基于范围的for循环(前面介绍过) ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"list\rlist 是顺序容器的一种。list 是一个双向链表,使用 list 需要包含头文件 list。 list 的构造函数和许多成员函数的用法都与 vector 类似,此处不再列举。除了顺序容器都有的成员函数外,list 容器还独有如下表 所示的成员函数(此表不包含全部成员函数,且有些函数的参数较为复杂,表中只列出函数名)。 成员函数或成员函数模板 作用 void push_front(const T \u0026val) 将 val 插入链表最前面 void pop_front() 删除链表最前面的元素 void sort() 将链表从小到大排序 void remove(const T \u0026val) 删除和 val 相等的元素 remove_if 删除符合某种条件的元素 void unique() 删除所有和前一个元素相等的元素 void merge(list\u003cT\u003e \u0026x) 将链表 x 合并进来并清空 x。要求链表自身和 x 都是有序的 void splice(iterator i, list\u003cT\u003e \u0026x, iterator first, iterator last) 在位置 i 前面插入链表 x 中的区间 [first,last),并在链表 x 中删除该区间。链表自身和链表 x 可以是同一个链表,只要 i 不在 [first,last) 中即可 STL 中的算法 sort 可以用来对 vector 和 deque 排序,它需要随机访问迭代器的支持。因为 list 不支持随机访问迭代器,所以不能用算法 sort 对 list 容器排序。因此,list 容器引入了 sort 成员函数以完成排序。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"deque\rdeque 也是顺序容器的一种,同时也是一个可变长数组。要使用 deque,需要包含头文件 deque。所有适用于 vector 的操作都适用于 deque。 deque 和 vector 有很多类似的地方。在 deque 中,随机存取任何元素都能在常数时间内完成(但慢于vector)。它相比于 vector 的优点是,vector 在头部删除或添加元素的速度很慢,在尾部添加元素的性能较好,而 deque 在头尾增删元素都具有较好的性能(大多数情况下都能在常数时间内完成)。它有两种 vector 没有的成员函数: void push_front (const T \u0026 val); //将 val 插入容器的头部 void pop_front(); //删除容器头部的元素 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:7","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"pair类模板\r在学习关联容器之前,首先要了解 STL 中的 pair 类模板,因为关联容器的一些成员函数的返回值是 pair 对象,而且 map 和 multimap 容器中的元素都是 pair 对象。 pair实例化出来的类都有两个成员变量,一个是 first, 一个是 second。pair的主要作用就是将两个元素绑定成一个,pair 的定义方式如下所示: pair\u003ctypename1, typename2\u003e p; pair\u003cstring, int\u003e p; // 调用无参构造函数 p.first = \"ABC\"; p.second = 3; // 也可以定义时就赋值 pair\u003cstring, int\u003e p(\"ABC\", 3); // 调用对应的构造函数 注意:pair类型的变量可以进行比较,先比较first,如果first相同,则比较second ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:8","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"set\rset为集合,是一个内部自动有序(默认递增排序,可修改排序规则,请自行了解)且元素不重复的容器。可用set来实现元素的去重。使用需要包含头文件set。 「不能直接修改 set 容器中元素的值」。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。因此,如果要修改 set 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素。 set常用成员函数: st.insert(x); //插入元素x st.find(x); //查找元素x,查找成功返回该元素的迭代器,查找失败返回st.end()的值 st.erase(it); //删除迭代器it指向的元素 st.erase(x); //删除元素x st.erase(it_first, it_end); //删除区间[it_first~it_end]的元素 st.size(); //获取元素个数 st.clear(); //删除全部元素 除了set以外,还有: multiset:元素可以重复,且元素有序 unordered_set :元素无序且只能出现一次 unordered_multiset : 元素无序可以出现多次 但实际使用频率较低,就不介绍了,有需要的请自行了解 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:9","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"map\rmap 是关联容器的一种,map容器主要用以映射,比如将一个string映射为一个int。map 的每个元素都分为关键字和值两部分,容器中的元素是按关键字排序的 (默认递增排序,可修改排序规则,请自行了解),并且不允许有多个元素的关键字相同(即key是唯一的)。使用需要包含头文件map。 map容器的定义: map\u003ckey, value\u003e mp; //mp为将key类型的变量映射为value类型的变量 map\u003cstring, int\u003e mp; map\u003cset\u003cint\u003e, string\u003e mp; map容器内元素的访问: (1)通过下标,即key访问 mp[key] (2)通过迭代器,迭代器的定义和其他容器一样,map容器内的每个元素都包括key和value,故 it→first 访问key,it→second 访问value,it为迭代器 map常用函数: mp.find(key) //返回键为key的元素的迭代器,如果不存在则返回 mp.end() mp.erase(key) //删除键为key的元素 mp.erase(first, end) //删除区间[first,end)的元素 mp.size() mp.clear() ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:10","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"stack\rstack即栈容器(其实是容器适配器,没有迭代器)。使用需要包含头文件stack。 定义方式为:stack\u003ctypename\u003e stackname; 常用成员函数: st.push(x) //将x入栈 st.pop() //弹出栈顶元素 st.top() //获取栈顶元素 st.empty() st.size() ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:11","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"queue\rqueue即队列容器(其实是容器适配器,没有迭代器)。使用需要包含头文件queue。 定义方式为:queue\u003ctypename\u003e qname; 常用成员函数: q.push(x) //将x入队 q.pop() //出队 q.front() //获取队首元素 q.back() //获取队尾元素 q.empty() q.size() ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:12","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"priority_queue\r优先队列,底层采用堆来实现的。队列元素顺序并不是按照入队顺序排列,而是按照优先级进行排列。队首元素为优先级最高的元素。 优先队列与队列的不同之处在于:优先队列不存在 front()和back()函数,只能通过 top() 函数来访问队首(堆顶)元素。使用top()函数前一定要先判断队列是否为空。 元素优先级设置: 1)基本数据类型的优先级设置 优先队列对基本数据类型的优先级设置一般是 越大的优先级越高,基本数据类型的优先级设置如下: //采用默认优先级设定的优先队列 priority_queue\u003ctype\u003e pq; //自定义优先级的优先队列 priority_queue\u003ctype, vector\u003ctype\u003e, less\u003ctype\u003e \u003e pq; priority_queue\u003ctype, vector\u003ctype\u003e, greater\u003ctype\u003e \u003e pq; 第一个参数为队列元素类型,第二个参数为承载堆的容器,第三个参数为第一个参数的比较类,less\u003ctype\u003e 表示type越大优先级越高,greater\u003ctype\u003e表示type越小优先级越高。 //该优先队列表示数字越小的优先级越高 priority_queue\u003cint, vector\u003cint\u003e, greate\u003cint\u003e \u003e pri_q; 2)结构体的优先级设置 使用频率较低,有需要请自行了解 ","date":"2024-08-07","objectID":"/posts/3b2b064/:9:13","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"智能指针模板类\r使用指针时,一定要 new 与 delete 成对出现,但有时我们会忘记 delete 又或者 在 delete 前就出现了异常转而去执行对于的 catch 块去了。 如果指针是对象就好,这样就可以在其过期时,自动调用析构函数来释放其指向的空间。这正是智能指针模板类 auto_ptr、 unique_ptr、 shared_ptr 背后的思想,只需要将申请到的空间地址赋值给其对象就行,其对象的析构函数会自动释放对应空间,不必手动 delete。 使用这些智能指针需要包含头文件 memory。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:10:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"unique_ptr\rstd::unique_ptr 是一种 独占所有权 的智能指针。它确保指针能独占一个申请的内存空间,即该指针不能将自己的值赋值给其他指针,当该 unique_ptr 指针被销毁时,它管理的空间也会被释放。 特点: 独占所有权:不能复制,只能移动。 轻量级:没有额外的引用计数开销。 主要操作: 创建和初始化:使用 std::make_unique 创建。 移动所有权:使用 std::move 将所有权转移给另一个 unique_ptr。 示例: #include \u003ciostream\u003e #include \u003cmemory\u003e class MyClass { public: MyClass() { std::cout \u003c\u003c \"MyClass constructor\" \u003c\u003c std::endl; } ~MyClass() { std::cout \u003c\u003c \"MyClass destructor\" \u003c\u003c std::endl; } void sayHello() const { std::cout \u003c\u003c \"Hello from MyClass\" \u003c\u003c std::endl; } }; int main() { std::unique_ptr\u003cMyClass\u003e ptr1 = std::make_unique\u003cMyClass\u003e(); ptr1-\u003esayHello(); // std::unique_ptr\u003cMyClass\u003e ptr2 = ptr1; // 错误:unique_ptr 不能复制 std::unique_ptr\u003cMyClass\u003e ptr2 = std::move(ptr1); // 移动所有权 if (!ptr1) { std::cout \u003c\u003c \"ptr1 is now empty\" \u003c\u003c std::endl; } ptr2-\u003esayHello(); return 0; } 注意: make_unique模板函数在C++14及以后的标准中才有。如果要在C++11及以前的标准中使用,需要自己实现。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:10:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"shared_ptr\rstd::shared_ptr 是一种 共享所有权 的智能指针。多个 shared_ptr 可以 「共享同一个空间」,并使用 引用计数 来管理空间的生命周期。当最后一个 shared_ptr 指针被销毁时,管理的空间才会被释放。 特点: 共享所有权:多个 shared_ptr 可以指向同一个空间。 引用计数:内部维护引用计数,控制空间的生命周期。 主要操作: 创建和初始化:使用 std::make_shared 创建。 拷贝和赋值:可以拷贝和赋值,增加引用计数。 重置和释放:reset() 方法用于释放对象或更换对象。 获取原始指针:get() 方法来获取原始指针,通过这种方法不会增加引用计数。 查看对象的引用计数:use_count() 方法来查看一个对象的引用计数。 示例: #include \u003ciostream\u003e #include \u003cmemory\u003e class MyClass { public: MyClass() { std::cout \u003c\u003c \"MyClass constructor\" \u003c\u003c std::endl; } ~MyClass() { std::cout \u003c\u003c \"MyClass destructor\" \u003c\u003c std::endl; } void sayHello() const { std::cout \u003c\u003c \"Hello from MyClass\" \u003c\u003c std::endl; } }; int main() { std::shared_ptr\u003cMyClass\u003e ptr1 = std::make_shared\u003cMyClass\u003e(); std::shared_ptr\u003cMyClass\u003e ptr2 = ptr1; // 共享所有权 MyClass *temp = ptr1.get(); // 获取原始指针,这不会增加引用计数 std::cout \u003c\u003c \"-------------------------------------------------\" \u003c\u003c std::endl; // 输出当前引用计数,在此为 2,但 temp,ptr1,ptr2都指向同一片内存空间 std::cout \u003c\u003c \"Reference count: \" \u003c\u003c ptr1.use_count() \u003c\u003c std::endl; std::cout \u003c\u003c \"ptr1: \" \u003c\u003c ptr1 \u003c\u003c \", \" \u003c\u003c \"ptr2: \" \u003c\u003c ptr2 \u003c\u003c \", \" \u003c\u003c \"temp: \" \u003c\u003c temp \u003c\u003c std::endl; std::cout \u003c\u003c \"-------------------------------------------------\" \u003c\u003c std::endl; ptr1-\u003esayHello(); ptr2-\u003esayHello(); temp-\u003esayHello(); std::cout \u003c\u003c \"-------------------------------------------------\" \u003c\u003c std::endl; ptr1.reset(); // 释放 ptr1 的所有权 // 引用计数为 1 std::cout \u003c\u003c \"Reference count after reset: \" \u003c\u003c ptr2.use_count() \u003c\u003c std::endl; ptr2-\u003esayHello(); // 不用 delete temp,因为这三个指针指向同一个空间,该空间已经被智能指针释放掉了 return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:10:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"weak_ptr\rstd::weak_ptr 是一种弱引用智能指针,它不参与对象的引用计数管理,不能直接访问对象。它的主要作用是解决 std::shared_ptr 的循环引用问题。 std::shared_pt 的循环引用问题 考虑以下情况,存在两个类A,B;A类里面有一个指向B类对象的shared_ptr指针,B类里面有一个指向A类对象的shared_ptr指针(后面简称指针)。现在通过一个指向A类对象的指针,让该对象中的指针指向B类对象;再让一个指向B类对象的指针,让该对象中的指针指向A类对象。此时,就构成了循环引用,无法自动释放这两个空间,因为引用无法归零。 #include \u003ciostream\u003e #include \u003cmemory\u003e class B; // 前向声明 class A { public: std::shared_ptr\u003cB\u003e b_ptr; ~A() { std::cout \u003c\u003c \"A destroyed\" \u003c\u003c std::endl; } }; class B { public: std::shared_ptr\u003cA\u003e a_ptr; ~B() { std::cout \u003c\u003c \"B destroyed\" \u003c\u003c std::endl; } }; int main() { auto a = std::make_shared\u003cA\u003e(); // auto是C++11新增的关键字,可以自动推断变量的类型 auto b = std::make_shared\u003cB\u003e(); a-\u003eb_ptr = b; b-\u003ea_ptr = a; // 此时 a 和 b 离开作用域,但不会调用析构函数,因为存在循环引用 return 0; } 为了避免这种情况出现,就应将 类中的指针定义为 weak_ptr 类型 的指针 特点: 弱引用:不增加引用计数,不影响对象的生命周期。 转换:需要转换为 std::shared_ptr 才能访问对象。 主要操作: 创建和初始化:通过 std::shared_ptr 创建。 锁定和访问:使用 lock 方法转换为 std::shared_ptr。 示例: #include \u003ciostream\u003e #include \u003cmemory\u003e class MyClass { public: MyClass() { std::cout \u003c\u003c \"MyClass constructor\" \u003c\u003c std::endl; } ~MyClass() { std::cout \u003c\u003c \"MyClass destructor\" \u003c\u003c std::endl; } void sayHello() const { std::cout \u003c\u003c \"Hello from MyClass\" \u003c\u003c std::endl; } }; int main() { std::shared_ptr\u003cMyClass\u003e sp1 = std::make_shared\u003cMyClass\u003e(); std::weak_ptr\u003cMyClass\u003e wp = sp1; // 创建弱引用 std::shared_ptr\u003cMyClass\u003e sp2 = wp.lock(); // 转换为 shared_ptr后赋值 sp2-\u003esayHello(); std::cout \u003c\u003c \"Reference count: \" \u003c\u003c sp2.use_count() \u003c\u003c std::endl; sp1.reset(); // 释放 sp1 的所有权 sp2.reset(); // 释放 sp2 的所有权,此时引用计数为0,该空间被释放掉 // 在空间被释放后,尝试转换,转换失败会返回一个空的 shared_ptr std::shared_ptr\u003cMyClass\u003e sp3 = wp.lock(); if(sp3 == nullptr) std::cout \u003c\u003c \"Conversion failed\" \u003c\u003c std::endl; return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:10:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"文件操作\r","date":"2024-08-07","objectID":"/posts/3b2b064/:11:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"什么是文件?\r内存中的数据在计算机关机后就会消失。要长久保存数据,就要使用硬盘、光盘、U 盘等设备。而为了便于数据的管理和检索,引入了“文件”的概念,即 「文件是计算机系统中用于存储和组织数据的一种基本形式」。 一个文档、一段视频、一个可执行程序,都可以被保存为一个文件,并赋予一个文件名。操作系统以文件为单位管理磁盘中的数据。 如果不对文件进行分类,当有成千上万个文件放在一起时,使用起来就会非常不便,因此又引入了树形目录(目录也叫文件夹)的机制,可以把文件放在不同的文件夹中,文件夹中还可以嵌套文件夹,这就便于用户对文件进行管理和使用,正如 Windows 的资源管理器呈现的那样。 通常,按文件的功能来分,文件可分为文本文件、视频文件、音频文件、图像文件、可执行文件等多种类别。但从数据存储的角度来说,所有文件本质上都是一样的,都是一个 0、1 比特串。不同的文件呈现出不同的形态(有的是文本,有的是视频等等)是因为文件的创建者和解释者(使用文件的软件)约定好了文件格式。 所谓“格式”,就是对文件中每一部分的内容代表什么含义的一种约定。例如,常见的纯文本文件(也叫文本文件,扩展名通常是“.txt”),指的是能够在 Windows 的“记事本”程序中打开,并且能看出是一段有意义的文字的文件。文本文件的格式可以用一句话来描述:文件中的每个字节都是一个可见字符的 ASCII 码。 除了纯文本文件外,图像、视频、可执行文件等一般被称作“二进制文件”。二进制文件如果用“记事本”程序打开,看到的是一片乱码。 所谓“文本文件”和“二进制文件”,只是约定俗成的、从用户角度出发进行的分类,并不是计算机科学的分类。因为从计算机科学的角度来看,所有的文件都是由二进制位组成的,都是二进制文件。文本文件和其他二进制文件只是格式不同而已。 实际上,只要规定好格式,而且不怕浪费空间,用文本文件一样可以表示图像、声音、视频甚至可执行程序。简单地说,如果约定用字符 ‘1’、‘2’、…、‘7’ 表示七个音符,那么由这些字符组成的文本文件就可以被遵从该约定的音乐软件演奏成一首曲子。 以 256 色图像为例,可以用 0~255 这 256 个数代表 256 种颜色,那么每个像素就可以用一个数来表示。再约定文件开始的两个数代表图像的宽度和高度(以像素为单位),则以下文本文件就可以表示一幅宽度为 7 像素、高度为 3 像素的 256 色图像: 7 3 37 0 38 129 4 154 0 73 3 227 40 0 0 1 17 173 127 20 0 0 2 这个“文本图像”文件的格式可以描述为:第一行的两个数分别代表水平方向的像素数目和垂直方向的像素数目,此后每行代表图像的一行像素,一行中的每个数对应于一个像素,表示其颜色。理解这一格式的图像处理软件就可以把上述文本文件呈现为一幅图像。视频是由每秒 24 幅图像组成的,因此用文本文件也可以表示视频。 但用文本文件来表示图像是非常低效的方法,浪费了太多的空间。文件中大量的空格是一种浪费。另外,常常要用 2 个甚至 3 个字符来表示一个像素,也造成大量浪费,因为用一个字节就足以表示 0~255 这 256 个数。因此,可以约定一个更节省空间的格式来表示一个 256 色的图像,此种文件格式的描述如下:文件中的第 0 和 1 个字节是整数 n,代表图像的宽度(2 字节的 n 的取值范围是 0~65 535,说明图像最多只能是 65 535 个像素宽),第 2 和 3 个字节代表图像的高度。接下来,每 n 个字节表示图像的一行像素,其中每个字节对应于一个像素的颜色。 用这种格式存储 256 色图像,比用上面的文本格式存储图像能够大大节省空间。在“记事本”程序中打开它,看到的就会是乱码,这个图像文件也就是所谓的“二进制文件”。 真正的图像文件、音频文件、视频文件的格式都比较复杂,有的还经过了压缩,但只要文件的制作软件和解读软件(如图像查看软件,音频、视频播放软件)遵循相同的格式约定,用户就可以在文件解读软件中看到文件的内容。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:1","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"文件流类\rC++ 标准类库中有三个类可以用于文件操作,它们统称为文件流类。这三个类是: ifstream:用于从文件中读取数据。 ofstream:用于向文件中写人数据。 fstream:既可用于从文件中读取数据,又可用于 向文件中写人数据。 使用这三个类时,程序中需要包含 fstream 头文件。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:2","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"打开文件和关闭文件\r在对文件进行读写操作前需要先打开文件。打开文件的目的有以下两个: 建立起指定文件和文件流对象的关联,以后要对文件进行操作时,就可以通过与之关联的流对象来进行。 指明文件的使用方式。使用方式有只读、只写、既读又写、在文件末尾添加数据、以文本方式使用、以二进制方式使用等多种。 打开文件可以通过以下两种方式进行: 调用流对象的 open 成员函数打开文件。 定义文件流对象时,通过构造函数打开文件。 注意: 对文件操作结束后,一定要调用流对象的 close() 成员函数来关闭文件!!!且打开后应调用流对象的 is_open() 成员函数来判断是否成功打开文件。 无论是哪种打开方式,都需要使用两个参数:①以字符串的形式给出的要打开的文件路径。②文件打开模式 文件打开模式如下表所示: 模式标记 适用对象 作用 ios::in ifstream fstream 打开文件用于读取数据。如果文件不存在,则打开出错。 ios::out ofstream fstream 打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存在,则打开时清除原来的内容。 ios::app ofstream fstream 打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。 ios::ate ifstream 打开一个已有的文件,并将文件读指针指向文件末尾。如果文件不存在,则打开出错。 ios::trunc ofstream 单独使用时与ios::out的效果相同。该模式主要应用于组合使用的情况,用于清晰表达程序员的意图,即明确表示打开文件时清空其内容 ios::binary ifstream ofstream fstream 以二进制方式打开文件。若不指定此模式,则以文本模式打开。所有文件都可以二进制的形式打开。 ios::in | ios::out fstream 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 打开模式的值是可以通过 | 符号来组合使用的,例如:ios::in | ios::out | ios::trunc 表示以读写方式打开指定文件,且打开文件时清空其内容。 #include \u003ciostream\u003e #include \u003cfstream\u003e int main() { // 以构造函数的形式打开 output.txt 文件,省略了第二个参数,因为有默认值 ios::out std::ofstream fout(\"output.txt\"); // 以调用成员函数 open() 的方式打开 example.txt文件 ,省略了第二个参数,因为有默认值 ios::in std::ifstream fin; fin.open(\"example.txt\"); // 判断是否成功打开了文件 if ( !fout.is_open() ) { std::cerr \u003c\u003c \"fout Error opening file!\" \u003c\u003c std::endl; return 1; } if ( !fin.is_open() ) { std::cerr \u003c\u003c \"fin Error opening file!\" \u003c\u003c std::endl; return 1; } ...... // 对文件进行的一系列操作 // 对文件操作完成后关闭文件。 fout.close(); fin.close(); return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:3","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"文本打开方式和二进制打开方式的区别\r在 UNIX/Linux 平台中,用文本方式或二进制方式打开文件没有任何区别。 在 UNIX/Linux 平台中,文本文件以\\n(ASCII 码为 0x0a)作为换行符号;而在 Windows 平台中,文本文件以连在一起的\\r\\n(\\r的 ASCII 码是 0x0d)作为换行符号。 在 Windows 平台中,如果以文本方式打开文件,当读取文件时,系统会将文件中所有的\\r\\n转换成一个字符\\n,如果文件中有连续的两个字节是 0x0d0a,则系统会丢弃前面的 0x0d 这个字节,只读入 0x0a。当写入文件时,系统会将\\n转换成\\r\\n写入。 也就是说,如果要写入的内容中有字节为 0x0a,则在写人该字节前,系统会自动先写入一个 0x0d。因此,如果用文本方式打开二进制文件进行读写,读写的内容就可能和文件的内容有出入。 因此,用二进制方式打开文件总是最保险的。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:4","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"文本文件的读取和写入\r使用文件流对象打开文件后,该文件流对象就成为了一个输入流或输出流。就可以像使用 cin、cout 那样来使用对象对文件进行读写操作。 #include \u003ciostream\u003e #include \u003cfstream\u003e int main() { std::ifstream fin(\"in.txt\"); std::ofstream fout(\"output.txt\"); string mstr; if (!fin.is_open()) { std::cerr \u003c\u003c \"in Error opening file!\" \u003c\u003c std::endl; return 1; } if (!fout.is_open()) { std::cerr \u003c\u003c \"out Error opening file!\" \u003c\u003c std::endl; return 1; } fin \u003e\u003e mstr; // 从 in.txt 读取一个字符串存储到mstr中 fout \u003c\u003c mstr \u003c\u003c endl; // 将 mstr的内容和换行符输入到 output.txt 中 fin.close(); fout.close(); return 0; } ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:5","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"二进制文件的读取和写入\r二进制文件比文本文件更加容易检索、且存储相同大小的内容时二进制文件占用的空间更小。 对二进制文件的读写只能使用文件流类的 read() 和 write() 成员函数。 write() 的原型为: ostream \u0026 write(char* buffer, int count); 该成员函数用于将buffer 中的前 count 个字节写入到文件中去,从文件写指针指向的位置开始写。返回值是调用该函数的对象的引用。 读指针和写指针 文件中通常有读指针(read pointer)和写指针(write pointer),它们分别用于跟踪文件内容的读取和写入位置。 读指针: 当文件以读取模式打开时,读指针指向文件的当前读取位置。随着数据的读取,读指针会相应地向前移动。如果到达文件末尾,读指针会保持在文件末尾的位置。 写指针: 当文件以写入模式打开时,写指针指向文件的当前写入位置。写入数据时,写指针会根据写入的数据量向前移动。如果文件是新创建的或被清空的,写指针通常从文件的开始位置写入;如果是追加模式,写指针会移动到文件的末尾。 在可以同时进行读写操作的文件流(如 fstream)中,读指针和写指针可以同时存在。它们可以独立操作,分别用于读取和写入数据。 直接看示例: #include \u003ciostream\u003e #include \u003cfstream\u003e using namespace std; class CStudent { public: char szName[20]; int age; }; int main() { CStudent s; ofstream outFile(\"students.dat\", ios::out | ios::binary); // 以二进制的形式打开文件 while (cin \u003e\u003e s.szName \u003e\u003e s.age) outFile.write((char *)\u0026s, sizeof(s)); // 将内容输入到 students.dat 中 outFile.close(); // 关闭文件 return 0; } 如果用记事本程序打开该文件,则会呈现乱码。 read()的原型如下: istream \u0026 read(char* buffer, int count); 该成员函数的作用是从文件的读指针指向的位置开始读取 count 个字节的内容到 buffer 中。返回值是调用该函数的对象的引用。 如果想要知道最终读取的字节数,可以在调用read()函数后调用 gcount()。 将上面的students.dat文件中的内容读取出来并输出到控制台。 #include \u003ciostream\u003e #include \u003cfstream\u003e using namespace std; class CStudent { public: char szName[20]; int age; }; int main() { CStudent s; ifstream inFile(\"students.dat\",ios::in|ios::binary); //二进制读方式打开 if(!inFile.is_open()) { cout \u003c\u003c \"error\" \u003c\u003cendl; return 0; } while(inFile.read((char *)\u0026s, sizeof(s))) // 每次读取一个同学的信息 { int readedBytes = inFile.gcount(); // 获取刚才读取的字节数 cout \u003c\u003c s.szName \u003c\u003c \" \" \u003c\u003c s.age \u003c\u003c endl; // 输出到控制台 } inFile.close(); // 关闭文件 return 0; } 如果每次只想读取或写入一个字节,那么可以使用get()和put()成员函数。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:6","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"移动或获取文件读写指针\r在读写文件时,有时希望直接跳到文件中的某处开始读写,这就需要先将文件的读写指针指向该处,然后再进行读写。 ifstream 类和 fstream 类有 seekg 成员函数,可以设置文件读指针的位置; ofstream 类和 fstream 类有 seekp 成员函数,可以设置文件写指针的位置。 所谓“位置”,就是指 「距离文件开头有多少个字节」。文件开头的位置是 0。 这两个函数的原型如下: ostream \u0026 seekp (int offset, int mode); istream \u0026 seekg (int offset, int mode); mode 代表文件读写指针的设置模式,有以下三种选项: ios::beg:让文件读指针(或写指针)指向从文件开始向后的 offset 字节处。offset 等于 0 即代表文件开头。在此情况下,offset 只能是非负数。 ios::cur:在此情况下,offset 为负数则表示将读指针(或写指针)从当前位置朝文件开头方向移动 offset 字节,为正数则表示将读指针(或写指针)从当前位置朝文件尾部移动 offset字节,为 0 则不移动。 ios::end:让文件读指针(或写指针)指向从文件结尾往前的 |offset|(offset 的绝对值)字节处。在此情况下,offset 只能是 0 或者负数。 此外,我们还可以得到当前读写指针的具体位置: ifstream 类和 fstream 类还有 tellg 成员函数,能够返回文件读指针的位置 ofstream 类和 fstream 类还有 tellp 成员函数,能够返回文件写指针的位置。 返回值为当前指针距离文件头的字节数,即偏移量。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:11:7","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"现代C++教程\r一般学完上面的知识就够用了。如果你想学习更新的特性,比如C++14后的新特性,可去此网站进行学习。 ","date":"2024-08-07","objectID":"/posts/3b2b064/:12:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["C++"],"content":"参考资料\rhttps://www.54benniao.com/view/6396.html https://learn-cpp.guyutongxue.site/ch01/assignment_and_if.html https://docs.oldtimes.me/c.biancheng.net/view/2264.html https://www.rowlet.info/post/7#1.%20static_cast Kimi ai ChatGPT C++ Primer(中文版,第5版) ","date":"2024-08-07","objectID":"/posts/3b2b064/:13:0","tags":["笔记"],"title":"C++学习笔记","uri":"/posts/3b2b064/"},{"categories":["iEDA"],"content":"脚本创建\r在单独运行点工具前必须配置对应的环境变量,在iEDA目录/iEDA/scripts/design/sky130_gcd下使用命令touch创建一个.sh脚本,内容如下: #!/bin/bash CURRENT_DIR=\"$(pwd)\" export CONFIG_DIR=\"$CURRENT_DIR/iEDA_config\" export RESULT_DIR=\"$CURRENT_DIR/result/my_test_result\" export TCL_SCRIPT_DIR=\"$CURRENT_DIR/script\" export NETLIST_FILE=\"$CURRENT_DIR/result/verilog/gcd.v\" export FOUNDRY_DIR=\"$CURRENT_DIR/../../foundry/sky130\" export SPEF_FILE=\"$CURRENT_DIR/../../foundry/sky130/spef/gcd.spef\" export SDC_FILE=\"$CURRENT_DIR/../../foundry/sky130/sdc/gcd.sdc\" export DESIGN_TOP=\"gcd\" export DIE_AREA=\"0.0 0.0 149.96 150.128\" export CORE_AREA=\"9.996 10.08 139.964 140.048\" echo \"CONFIG_DIR set to: $CONFIG_DIR\" echo \"RESULT_DIR set to: $RESULT_DIR\" echo \"TCL_SCRIPT_DIR set to: $TCL_SCRIPT_DIR\" echo \"NETLIST_FILE set to: $NETLIST_FILE\" echo \"FOUNDRY_DIR set to: $FOUNDRY_DIR\" echo \"SPEF_FILE set to: $SPEF_FILE\" echo \"SDC_FILE set to: $SDC_FILE\" echo \"DESIGN_TOP set to: $DESIGN_TOP\" echo \"DIE_AREA set to: $DIE_AREA\" echo \"CORE_AREA set to: $CORE_AREA\" ","date":"2024-08-02","objectID":"/posts/3b24a9e/:1:0","tags":["点工具","教程"],"title":"iEDA点工具运行前环境变量设置","uri":"/posts/3b24a9e/"},{"categories":["iEDA"],"content":"点工具运行\r运行点工具前,在当前窗口使用命令 source 脚本名.sh 来设置环境变量,然后按照iEDA的用户手册来运行点工具即可 PS: 设置的环境变量只在当前窗口有效,即在新窗口运行点工具前需要再次运行脚本来设置环境变量,如果想要使得变量在全部窗口有效请自行谷歌。 ","date":"2024-08-02","objectID":"/posts/3b24a9e/:2:0","tags":["点工具","教程"],"title":"iEDA点工具运行前环境变量设置","uri":"/posts/3b24a9e/"},{"categories":["iEDA"],"content":"参考资料\rhttps://github.com/OSCC-Project/iEDA/blob/master/docs/user_guide/iEDA_user_guide.md ","date":"2024-08-02","objectID":"/posts/3b24a9e/:3:0","tags":["点工具","教程"],"title":"iEDA点工具运行前环境变量设置","uri":"/posts/3b24a9e/"},{"categories":["算法笔记"],"content":"使用动态规划解决零钱兑换问题","date":"2024-07-29","objectID":"/posts/bd6092d/","tags":["动态规划"],"title":"322. 零钱兑换","uri":"/posts/bd6092d/"},{"categories":["算法笔记"],"content":"题目描述(力扣): 什么是最优子结构 最优子结构指的是,问题的最优解包含子问题的最优解。反过来说就是,可以通过子问题的最优解,推导出问题的最优解。要符合「最优子结构」,子问题间必须互相独立。 比如说,假设你考试,每门科目的成绩都是互相独立的。你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。 得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,「每门科目考到最高」这些子问题是互相独立,互不干扰的。 但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,不能同时达到满分,数学分数高,语文分数就会降低,反之亦然。 这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为「每门科目考到最高」的子问题并不独立,语文数学成绩户互相影响,无法同时最优,所以最优子结构被破坏。 思路: 题目说明了每种硬币数量无限,所以该问题具有最优子结构,可以用动态规划来解决。确定好用动态规划来解决后,先确定dp数组的意义,这里dp[i]存储的值就是总金额为i的最优解。接下来最重要的事情就是找到状态转移方程,dp[0] = 0,当amount大于0时,我们只需遍历给出的coins数组找到 dp[amount - coin_value] + 1 中的最小值即可(避免越界,需要先判断coin_value是否小于amount),加一是因为要加上一个面值为coin_value的硬币。 代码: class Solution { public: int coinChange(vector\u003cint\u003e\u0026 coins, int amount) { vector\u003cint\u003e dp(amount + 1, INT_MAX - 1); //初始化dp数组,初始值为INT_MAX - 1是因为后面有dp[i - value] + 1操作,需要避免整形溢出 dp[0] = 0; //已知的最优解 for(int i = 1; i \u003c= amount; i++) { for(int value : coins) //遍历每种硬币 { if(i - value \u003c 0) continue; //进行判断是为了避免越界 dp[i] = min(dp[i], dp[i - value] + 1); //找到总金额为 i 时的最优解 } } return dp[amount] == (INT_MAX - 1) ? -1: dp[amount]; } }; ","date":"2024-07-29","objectID":"/posts/bd6092d/:0:0","tags":["动态规划"],"title":"322. 零钱兑换","uri":"/posts/bd6092d/"},{"categories":["算法笔记"],"content":"使用后序遍历计算树的直径","date":"2024-07-25","objectID":"/posts/b92556f/","tags":["二叉树","后序遍历"],"title":"543. 二叉树的直径","uri":"/posts/b92556f/"},{"categories":["算法笔记"],"content":"题目描述(力扣): 思路: 首先理解二叉树的直径指的是二叉树中任意两个节点之间的路径长度的最大值(最长直径不一定经过根节点)。想要找到二叉树的直径,就需要遍历每个节点,依次计算每个节点的左右子树深度之和,其中的最大值就是二叉树的直径。因为二叉树的直径大多数情况下就是根节点下的左右子树深度之和,但存在直径不经过根节点的情况,所以需要遍历每个节点,计算以该节点为根的二叉树的直径,然后取计算结果中的最大值。根据前面的分析,我们需要计算左右子树深度之和,这就需要左右子树的信息,所以选择后序遍历。 代码: /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: int diameterOfBinaryTree(TreeNode* root) { maxDepth(root); return maxDia; } int maxDepth(TreeNode* root) { if(root == nullptr) return 0; int lefth_h = maxDepth(root-\u003eleft); int right_h = maxDepth(root-\u003eright); int Dia = lefth_h + right_h; //计算当前二叉树的直径 maxDia = max(maxDia, Dia); return lefth_h \u003e right_h? lefth_h + 1: right_h + 1; //返回子树高度 } private: int maxDia = -1; //用于存储二叉树直径 }; ","date":"2024-07-25","objectID":"/posts/b92556f/:0:0","tags":["二叉树","后序遍历"],"title":"543. 二叉树的直径","uri":"/posts/b92556f/"},{"categories":["算法笔记"],"content":"使用双指针进行单链表的分解","date":"2024-07-22","objectID":"/posts/6fc9142/","tags":["双指针","链表"],"title":"86. 分隔链表","uri":"/posts/6fc9142/"},{"categories":["算法笔记"],"content":"题目描述(力扣): 思路: 创造两个指针,分别指向大于x的链表和小于x的链表,然后依次遍历初始链表的每个节点进行判断将其添加到对应的新链表中,最后将两个链接进行连接后返回。 代码: /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* partition(ListNode* head, int x) { ListNode *h1 = new ListNode(-1); //指向小于x的节点组成的链表的头节点 ListNode *h2 = new ListNode(-1); //指向大于x的节点组成的链表的头节点 ListNode *t1, *t2; //分别指向各自链表的尾结点 t1 = h1; t2 = h2; while(head != nullptr) //遍历每个节点,与x进行比较 { if(head-\u003eval \u003c x) { t1-\u003enext = head; t1 = head; } else { t2-\u003enext = head; t2 = head; } head = head-\u003enext; } t1-\u003enext = h2-\u003enext; //将两个链表进行连接 t2-\u003enext = nullptr; return h1-\u003enext; } }; ","date":"2024-07-22","objectID":"/posts/6fc9142/:0:0","tags":["双指针","链表"],"title":"86. 分隔链表","uri":"/posts/6fc9142/"},{"categories":["Markdown"],"content":"Markdown常用的一些语法","date":"2024-07-21","objectID":"/posts/d43ba28/","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"代码\r","date":"2024-07-21","objectID":"/posts/d43ba28/:1:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"行内代码\r格式:ˋ代码内容ˋ ","date":"2024-07-21","objectID":"/posts/d43ba28/:1:1","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"块内代码\r格式: ˋˋˋmarkdown Sample text here ... ˋˋˋ ","date":"2024-07-21","objectID":"/posts/d43ba28/:1:2","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"标题\r格式: # 一级标题 ## 二级标题 ... ###### 六级标题 ","date":"2024-07-21","objectID":"/posts/d43ba28/:2:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"水平线\r格式: 水平线有三种实现方式: ___:三个连续的下划线 ---:三个连续的破折号 ***:三个连续的星号 ","date":"2024-07-21","objectID":"/posts/d43ba28/:3:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"换行\r","date":"2024-07-21","objectID":"/posts/d43ba28/:4:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"分行\r格式:两个空格 + 回车 或 使用\u003cbr\u003e ","date":"2024-07-21","objectID":"/posts/d43ba28/:4:1","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"分段\r格式:两个回车 ","date":"2024-07-21","objectID":"/posts/d43ba28/:4:2","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"强调\r","date":"2024-07-21","objectID":"/posts/d43ba28/:5:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"粗体\r格式:**加粗的内容** 也可以选择需要加粗的内容后使用快捷键Ctrl + B ","date":"2024-07-21","objectID":"/posts/d43ba28/:5:1","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"斜体\r格式:*倾斜的内容* 或 _倾斜的内容_ 也可以选择需要加粗的内容后使用快捷键Ctrl + I ","date":"2024-07-21","objectID":"/posts/d43ba28/:5:2","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"删除线\r格式:~~倾斜的内容~~ ","date":"2024-07-21","objectID":"/posts/d43ba28/:5:3","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"组合使用\r_**加粗和斜体**_ ~~**删除线和加粗**~~ ~~_删除线和斜体_~~ ~~_**加粗,斜体和删除线**_~~ ","date":"2024-07-21","objectID":"/posts/d43ba28/:5:4","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"引用\r格式:\u003e + 空格 + 引用的内容 例如: \u003e **Fusion Drive** combines a hard drive with a flash storage (solid-state drive) and presents it as a single logical volume with the space of both drives combined. ","date":"2024-07-21","objectID":"/posts/d43ba28/:6:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"列表\r","date":"2024-07-21","objectID":"/posts/d43ba28/:7:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"无序列表\r格式: 无序列表有三种实现方式 * 一项内容 - 一项内容 + 一项内容 例如: * Lorem ipsum dolor sit amet * Consectetur adipiscing elit * Integer molestie lorem at massa * Facilisis in pretium nisl aliquet * Nulla volutpat aliquam velit * Phasellus iaculis neque * Purus sodales ultricies * Vestibulum laoreet porttitor sem * Ac tristique libero volutpat at * Faucibus porta lacus fringilla vel * Aenean sit amet erat nunc * Eget porttitor lorem ","date":"2024-07-21","objectID":"/posts/d43ba28/:7:1","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"有序列表\r格式:1. + 空格 备注:如果对每一项都使用1. ,Markdown会自动为每一项编号 ","date":"2024-07-21","objectID":"/posts/d43ba28/:7:2","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"任务列表\r格式:- [] 这是内容 或 - [x] 这是内容 效果: Write the press release Update the website Contact the media ","date":"2024-07-21","objectID":"/posts/d43ba28/:7:3","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"表格 格式: | Option | Description | | ------ | ----------- | | data | path to data files to supply the data that will be passed into templates. | | engine | engine to be used for processing templates. Handlebars is the default. | | ext | extension to be used for dest files. | 效果: Option Description data path to data files to supply the data that will be passed into templates. engine engine to be used for processing templates. Handlebars is the default. ext extension to be used for dest files. 备注:竖线无需垂直对齐,------:表示这一列的内容右对齐,:------表示这一列的内容左对齐,:------:表示这一列的内容居中对齐 ","date":"2024-07-21","objectID":"/posts/d43ba28/:8:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"链接\r","date":"2024-07-21","objectID":"/posts/d43ba28/:9:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"常用链接方式\r格式: \u003chttps://assemble.io\u003e \u003ccontact@revolunet.com\u003e [Assemble](https://assemble.io) 推荐此方式进行链接 效果: https://assemble.io contact@revolunet.com Assemble ","date":"2024-07-21","objectID":"/posts/d43ba28/:9:1","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"定位标记\r定位标记使你可以跳至同一页面上的指定锚点。例如,每个章节。格式: [跳转到Chapter 1](#chapter-1) [跳转到Chapter 2](#chapter-2) [跳转到Chapter 3](#chapter-3) 将跳转到这些地方: ## Chapter 1 \u003ca id=\"chapter-1\"\u003e\u003c/a\u003e Content for chapter one. ## Chapter 2 \u003ca id=\"chapter-2\"\u003e\u003c/a\u003e Content for chapter one. ## Chapter 3 \u003ca id=\"chapter-3\"\u003e\u003c/a\u003e Content for chapter one. 例如:点击跳转到Chapter 1将跳转到表格章节,只需要使用上面的语法后,在# 表格后面加上代码\u003ca id=\"chapter-1\"\u003e\u003c/a\u003e即可 ","date":"2024-07-21","objectID":"/posts/d43ba28/:9:2","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"图片\r格式:![鼠标移动到图片上显示的描述](图片路径) ","date":"2024-07-21","objectID":"/posts/d43ba28/:10:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Markdown"],"content":"脚注\r脚注使你可以添加注释和参考,而不会使文档正文混乱。 当你创建脚注时,会在添加脚注引用的位置出现带有链接的上标编号。 读者可以单击链接以跳至页面底部的脚注内容。 格式: 这是一个数字脚注 [^1] 这是一个带标签的脚注 [^label] 下面的是点击上面的脚注后跳转显示的内容 [^1]: 这是一个数字脚注 [^label]: 这是一个带标签的脚注 效果: 巴拉巴拉巴拉巴拉巴拉 1 备注: 要创建脚注引用,请在方括号中添加插入符号和标识符 [^1]。 标识符可以是数字或单词,但不能包含空格或制表符。 标识符仅将脚注引用与脚注本身相关联。在脚注输出中,脚注按顺序编号。 这是参考内容或注释 ↩︎ ","date":"2024-07-21","objectID":"/posts/d43ba28/:11:0","tags":["Markdown"],"title":"Markdown常用语法","uri":"/posts/d43ba28/"},{"categories":["Git"],"content":"Git的基础知识","date":"2024-07-21","objectID":"/posts/a538cc4/","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Git概述\rGit 是一款分布式的代码版本控制工具,Linux 之父 Linus 嫌弃当时主流的中心式的版本控制工具太难用还要花钱,就自己花两周时间开发出了 Git 的主体程序,一个月后就开始用Git来维护 Linux 的版本(给大佬跪了)。 常见的版本控制工具有:分布式版本控制工具和集中式版本控制工具 分布式版本控制工具: 工作原理: 分布式版本控制工具不依赖于单一的中央服务器,每个开发者都拥有完整的代码库的副本。开发者在本地工作副本上进行修改,并在本地进行提交、分支、合并等操作。开发者可以选择与其他开发者直接交换修改的内容,也可以将修改推送到共享的远程仓库中。 特点: 每个开发者都拥有完整的代码库的副本,可以在本地进行版本控制 具有更好的分支和合并功能,能够轻松地处理复杂的开发工作流程 典型的分布式版本控制工具有Git和Mercurial等 集中式版本控制工具: 工作原理: 集中式版本管理工具使用单一的中央服务器来存储所有版本的代码库。开发者在本地工作副本上进行修改,然后将修改后的内容提交到中央服务器。其他开发者通过从中央服务器检出代码库来获取最新的代码,并将他们的修改提交到同一个中央服务器上。 特点: 中央服务器是唯一的源头,所有的代码修改都需要提交到中央服务器 开发者在没有网络连接的情况下无法进行版本控制操作 典型的集中式版本管理工具包括Subversion(SVN)和Perforce等 Git的工作机制如下图所示: 工作区:开发人员在本地存放项目文件(代码)的地方 暂存区:是一个缓冲区域,用于临时存放即将提交到本地库的修改,开发者通过 git add 命令将工作区中的修改添加到暂存区,只有添加到暂存区的文件该会被提交到本地库中去 本地库:存放项目完整历史记录和版本信息的地方,开发者通过 git commit 命令将暂存区的内容提交到本地库,使用该命令后暂存区就会清空 远程库:用于存放项目的中央代码库(如GitHub),位于云端或其他服务器上。团队成员可以通过 git clone 操作从远程仓库克隆一个完整的 Git 仓库到本地。克隆操作会复制远程仓库的所有历史记录、分支、标签等信息,并在本地创建一个相同的仓库副本。通过 git push 用于将本地库推送到远程库, 通过 git pull 从远程库拉取最新的代码到本地库 ","date":"2024-07-21","objectID":"/posts/a538cc4/:1:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Git安装\r","date":"2024-07-21","objectID":"/posts/a538cc4/:2:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Windows下安装\r先确定自己的电脑是64位操作系统还是32位的(按Win键,设置→系统→关于),然后去Git官网下载对应的安装包,根据提示进行安装(详细安装过程请自行Google)。 注意: 安装路径中不要包含中文! ","date":"2024-07-21","objectID":"/posts/a538cc4/:2:1","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Ubuntu下安装\r由于本人是Ubuntu下使用Git的,所以这里的步骤会写的详细点。 输入命令 sudo apt-get install git进行安装,输入 git version 检查是否安装成功 输入命令 ssh -T git@github.com 检查是否可以连接到GitHub,如果看到 则说明能够连接。 安装SSH keys(一定要在 ~/.ssh 目录下操作) 第一步:检查是否已经具有ssh keys,如果具有,则直接进行第三步 ========================================================== cd ~/.ssh ls ========================================================== 如果存在文件 id_rsa 和 id_rsa.pub 则具有ssh keys 第二步:备份并移除已经存在的ssh keys ========================================================== mkdir key_backup cp id_rsa* key_backup rm id_rsa* ========================================================== 第三步:执行以下命令 ========================================================== ssh-keygen -t rsa -C \"你的github邮箱\" ========================================================== 运行时会要求输入文件名,就输入id_rsa即可 接着会要求你输入两次密码,该密码用于push时的密码,不想设置密码可直接回车,建议设置 安装完毕后,再次输入ls查看是否存在文件id_rsa 和 id_rsa.pub 输入命令 cat id_rsa.pub 查看 id_rsa.pub 中的内容,将其内容复制到GitHub账户的SSH keys中(头像→Settings→SSH and GPG keys→ New SSH key) 再次输入命令ssh -T git@github.com (在~/.ssh目录下),如果显示Hi 你的用户名! You’ve successfully authenticated, but GitHub does not provide shell access. 则添加成功(如果在安装SSH keys时设置了密码,则该步骤会要求输出此密码) 注意: 若报错sign_and_send_pubkey: signing failed: agent refused operation ,则输入命令eval \"$(ssh-agent -s)\" 和 ssh-add 安装好Git后,需要配置Git全局环境,输入以下命令: git config --global user.name \"你的GitHub用户名\" git config --global user.email \"你的GitHub邮箱地址\" 输入命令 git config --global --list 查看是否设置成功 这里设置的环境的作用是区分不同操作者身份。用户的签名信息可以在每一个版本的提交信息中看到,以此确认本次提交是谁做的。 注: 此用户签名与远程库的账号没有任何关系 ","date":"2024-07-21","objectID":"/posts/a538cc4/:2:2","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Git常用命令\r","date":"2024-07-21","objectID":"/posts/a538cc4/:3:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"获取Git仓库\r通常有两种获取 Git 项目仓库的方式(两种方式都需要先进入到项目目录): 将尚未进行版本控制的本地目录转换为 Git 仓库 输入命令 git init ,完成Git仓库初始化操作,会得到一个隐藏目录 .git 从其它服务器克隆一个已存在的 Git 仓库 输入命令 git clone url (url为仓库地址,点击仓库页面的code按钮即可看到),这样就能得到一个与远程仓库一样的本地仓库(仓库名也一样)。 如果想要自定义本地库的名字,则可以修改命令为 git clone url 你要设定的本地仓库名 注: 当你克隆一个远程仓库时,本地仓库通常只会创建一个默认的主分支(一般为 master 分支,也有可能是 main 分支),其他分支会以远程分支的形式存在于本地仓库中。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:1","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"记录每次更新到仓库\rGit仓库下的每一个文件只有两种状态:已跟踪 或 未跟踪。已跟踪指的是已经被纳入版本控制的文件,即是Git已经知道的文件。 可用 git status 命令查看哪些文件处于什么状态,如果在克隆仓库后立即使用此命令,会看到类似的输出: On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working directory clean 这说明当前所在分支为 master,所有已跟踪的文件在上次提交后都未被修改,且当前目录下没有出现未跟踪的文件。如果此时在当前项目下创建一个新的 README 文件,再次使用 git status 命令,会看到以下输出: On branch master Your branch is up-to-date with 'origin/master'. Untracked files: (use \"git add \u003cfile\u003e...\" to include in what will be committed) README nothing added to commit but untracked files present (use \"git add\" to track) 在输出中可以看到存在未跟踪的文件 README,想要跟踪此文件则使用命令 git add README ,再次输入git status 命令,会发现 README 文件已被跟踪,并处于暂存状态: On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use \"git restore --staged \u003cfile\u003e...\" to unstage) new file: README 也可使用命令 git add . 将当前目录下的所有文件以及子目录下的所有文件添加到暂存区,即命令git add 的作用是将文件存添加到暂存区(已跟踪的文件发生变化也需要使用此命令将其最新版本添加到暂存区),暂存区存放的文件是你执行git add 命令时的文件版本。 注意: 如果你不想某些文件(如日志文件,编译过程中的临时文件)被追踪且也不希望被Git提醒,则可以创建一个名为.gitignore的txt文件,列出要忽略的文件的模式。例如: # 忽略所有的 .a 文件 *.a # 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件 !lib.a # 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO /TODO # 忽略任何目录下名为 build 的文件夹 build/ # 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt doc/*.txt # 忽略 doc/ 目录及其所有子目录下的 .pdf 文件 doc/**/*.pdf GitHub有一个十分详细的针对数十种项目及语言的 .gitignore 文件列表,点击此处即可查看。 当要被追踪的文件和修改后的文件都已经添加到暂存区时,就可使用命令 git commit -m \"CommitInfo\" 或 git commit 将暂存区的文件提交到本地库。 两者的区别是前者是在命令行书写简单的提交信息,后者会启动文本编辑器来书写复杂的提交信息,写好提交说明信息是非常重要的,可以帮助团队成员更好地理解和维护代码。我个人觉得这篇如何写好 Commit Message 的博客非常值得一读(中文版)。对于简单提交信息的书写则可以参照以下格式: 格式:\u003ctype\u003e(\u003cscope\u003e): \u003csubject\u003e type表示提交类型,scope表示涉及的文件(可用 * 来表示多个文件),subject描述此次涉及的修改,常用type类型如下: Type 说明 备注 feat 提交新功能 常用 fix 修复Bug 常用 docs 修改文档 style 修改格式,例如格式化代码,空格,拼写错误等 refactor 重构代码,没有添加新功能也没有修复bug test 添加或修改测试用例 perf 代码性能调优 chore 修改构建工具、构建流程、更新依赖库、文档生成逻辑 示例:fix(ngRepeat): fix trackBy function being invoked with incorrect scope ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:2","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"撤销修改\r当修改了工作区某个文件的内容且保持了,想要丢弃该修改。 命令:git checkout -- FileName 当修改了工作区某个文件,且添加到暂存区,想要丢弃该修改。 依次执行命令:a. git reset HEAD FileName b.git checkout -- FileName PS: 只执行命令a 则是从暂存区丢弃该修改(即从暂存区删除此文件) 当修改了工作区某个文件,不仅添加到了暂存区,还提交到了本地库,想要丢弃该修改,则需要进行版本回退。 命令:git reset --hard VersionNum PS: 版本号可通过 git reflog 命令查看 ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:3","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"比较差异\r使用命令 git diff 来比较文件之间的差异,以下是此命令的常见用法: git diff 这会显示工作区中未暂存的更改与暂存区中的内容之间的差异 git diff --cached 这会显示暂存区中的更改与最新提交(HEAD)之间的差异 git diff HEAD 这会显示工作区中的未暂存更改与最新提交(HEAD)之间的差异 PS: 若不想比较全部文件的差异,以上三种命令均可在末尾指定文件名 git diff branch1 branch2 -- FileName 比较不同分支中的同一文件的不同之处 ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:4","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"文件重命名\r想要在Git中对文件改名,则使用命令 git mv file_from file_to ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:5","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"查看提交历史\r查看提交历史的常用命令有两个:git log 和 git reflog,后一个命令只显示简略的版本信息(常用于版本的穿梭)。 git log 命令常用参数如下: 常用的命令为:git log --graph --oneline 版本穿梭命令为:git reset --hard VersionNum ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:6","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"删除文件\r想要删除某个文件 命令:git rm FileName PS: 此命令等价于依次执行 rm FileName 和 git add FileName 命令 想要删除被修改,但未添加到暂存区的文件,或已经在暂存区的文件。 命令:git rm -f FileName 想要某文件不再被Git跟踪(工作区中仍然存在该文件,只是不再被纳入版本管理) 命令:git rm --cached FileName 想要恢复删除文件 命令:git checkout -- FileName 此命令其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以\"一键还原\" ","date":"2024-07-21","objectID":"/posts/a538cc4/:3:7","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Git分支\r分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。 如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN! 分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。 现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:4:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"创建、合并、删除分支\r一开始的时候,主分支(master 或 main)是一条线,Git用master 或 main(创建仓库时确定主线指针名)指针指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点: 每次提交新的内容到本地库,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。 当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上: 本质上每创建一个分支就是创建一个与分支同名的指针,让其指向当前分支的最新提交;HEAD则是指向当前所在分支的指针,例如当前在dev分支,则HEAD指向dev指针,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变: 假如我们在dev上的工作完成了,就可以把dev合并到master上。最简单的方法,就是直接把master指向dev的当前提交(这种合并方式被称为 快进模式 或 快速合并),就完成了合并: 合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支: 合并的本质就是将目标分支的改动添加到当前分支 分支常用命令 查看本地所有分支:git branch ,当前分支前会有一个 * 号 查看远程所有分支:git branch -r,使用 git branch -a 可查看本地和远程的所有分支 查看本地分支和远程分支的映射关系:git branch -vv 设置当前分支的上游分支(即设置该分支映射到远程库的哪个分支):git branch -u REMOTE_BRANCH_NAME 创建分支:git branch branch_name 切换分支:git switch branch_name 创建并切换分支:git switch -c branch_name 或 git checkout -b branch_name 合并某分支到当前分支(采用快速合并方式,删除分支后会丢失分支信息):git merge branch_name 合并某分支到当前分支(禁用快速合并方式,不会丢失分支信息):git merge --no-ff -m \"Title\" branchName 删除已经合并的分支:git branch -d branch_name 删除未合并的分支:git branch -D branch_name 通常,合并分支时,如果可能,Git会用 Fast forward 模式,但这种模式在删除分支后会丢掉分支信息。 如果不想在删除分支后丢失分支信息,可以强制禁用Fast forward模式,即 git merge --no-ff -m \"Title\" branchName ,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。 当使用Fast forward模式合并时,merge后将像这样: 禁用Fast forward模式合并时,merge后将像这样: ","date":"2024-07-21","objectID":"/posts/a538cc4/:4:1","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"解决冲突\r人生不如意之事十之八九,合并分支往往也不是一帆风顺的。 准备新的 feature1 分支,继续新分支开发: $ git switch -c feature1 Switched to anew branch 'feature1' 修改 readme.txt 最后一行,改为: Creating anew branch is quick AND simple. 在 feature1 分支上提交修改到本地库: $ git add readme.txt $ git commit -m \"AND simple\" [feature1 14096d0]AND simple 1 file changed, 1 insertion(+), 1 deletion(-) 切换到 master 分支: $ git switch master Switched to branch 'master' Your branch is ahead of 'origin/master' by 1commit. (use \"git push\"to publish yourlocal commits) 在 master 分支上把 readme.txt 文件的最后一行改为: Creating anew branch is quick \u0026 simple. 提交: $ git add readme.txt $ git commit -m \"\u0026 simple\" [master 5dc6824] \u0026 simple 1 file changed, 1 insertion(+), 1 deletion(-) 现在,master 分支和 feature1 分支各自都分别有新的提交,变成了这样: 这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突。 在上面这种情况时,使用 git merge feature1 命令,Git就会告诉我们readme.txt文件存在冲突,必须手动解决冲突后再提交。使用命令 git diff branch1 branch2 -- FileName 来比较不同分支中的同一文件的不同之处。将当前分支中的readme.txt文件的内容修改为 Creating a new branch is quick and simple. 再提交。 现在,master分支和feature1分支变成了下图所示: 当你解决完冲突后,将修改后的文件添加到暂存区时Git就会知道冲突已解决,会自动执行合并操作,无需再次输入合并命令 使用命令 git log --graph --oneline 也可以看到分支的合并情况 ","date":"2024-07-21","objectID":"/posts/a538cc4/:4:2","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Bug分支\r软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。 假设有如下情景:你当前正在dev分支进行工作,且进行的工作因未完成还未提交,这时出现了一个代号为 333 的bug任务需要修复。 修复此bug的流程如下: 使用命令 git stash 把当前工作现场储藏起来(会保存当前工作目录中的所有更改,并将工作目录恢复到最后一次提交的状态),等处理完bug后恢复现场继续工作。 确定要在哪个分支上修复bug。假定需要在 master 分支上修复,就从 master 分支创建临时分支 bug-333。 在 bug-333 分支上修复bug。 切换回 master 分支进行合并操作,并删除 bug-333 分支。 切换回 dev 分支,使用命令 git stash list 来查看存储列表,再使用 git stash pop StashName 来恢复对应的工作现场,并在列表中删除此工作现场。 由于dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?有木有更简单的方法?有! 同样的bug,要在dev上修复,我们只需要把 4c805e2 fix bug 333 这个提交所做的修改 “复制” 到dev分支。 注意: 我们只想复制 4c805e2 fix bug 101 这个提交所做的修改,并不是把整个master分支merge过来。 Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支: git cherry-pick commit-hash 在上面的例子中,完整命令为 git cherry-pick 4c805e2 ,执行该命名前,要确定所在分支为dev 既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过仍然需要 git stash 命令保存现场,才能从dev分支切换到master分支。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:4:3","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"Rebase\rRebase被称为 变基 操作,其作用为使日志看起来更加简洁明了,缺点是会打乱时间线。 merge操作是将两个分支的最新commit合并后进行提交,形成新的commit;而 git rebase 提取操作有点像 git cherry-pick 一样,执行rebase后依次将当前(执行rebase时所在分支)的提交cherry-pick到目标分支(待rebase的分支)上,然后将在原始分支(执行rebase时所在分支)上的已提交的commit删除。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:4:4","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"分支管理策略\r在实际开发中,我们应该按照几个基本原则进行分支管理: 首先,main(或master)分支应该是非常稳定的,也就是仅用来发布新版本。develop和hotfix均从main分支分出,分别用于集成测试和修复出现的bug。feature分支从develop分支分出,用于开发新功能,开发完成后合并到develop分支。有条件的可以细化不同的测试环境,例如,用text分支来进行功能测试,通过测试后由text分支合并到release分支进行用户验收测试,测试通过后再合并到main分支。 所以,团队合作的分支看起来就像这样: ","date":"2024-07-21","objectID":"/posts/a538cc4/:4:5","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"标签管理\r发布一个版本时,我们通常先在版本库中打一个 标签(tag),这样,就 唯一 确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。 Git的标签虽然是版本库的快照,但其实它就是 指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。 Git已经有commit号,为什么还需引入tag?见以下场景: “请把上周一的那个版本打包发布,commit号是6a5819e…” “一串乱七八糟的数字不好找!” 如果换一个办法: “请把上周一的那个版本打包发布,版本号是v1.2” “好的,按照tag v1.2查找commit就行!” 所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。 可以理解为 tag 就是域名,commit号 就是IP ","date":"2024-07-21","objectID":"/posts/a538cc4/:5:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"创建标签\r在Git中打标签非常简单,首先,切换到需要打标签的分支上: $ git branch * dev master $ git checkout master Switched to branch 'master' 然后,使用命令 git tag tag_name 就可以创建一个名为tag_name的新标签,使用命令 git tag 即可查看所有标签,通过 git show tag_name 可查看标签的详细信息。 注意: 标签默认是打在当前分支的最新提交上,如果想要给以前的commit打标签,则需要先通过git reflog获得对应的commit_id,然后使用命令 git tag tag_name commit_id 标签是按字母来排序的! 标签总是与commit_id相挂钩的! ","date":"2024-07-21","objectID":"/posts/a538cc4/:5:1","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"删除标签\r如果标签打错了,也可以用命令 git tag -d tag_name 删除 因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。 如果要推送某个标签到远程,使用命令: git push origin \u003ctagname\u003e 或者,一次性将全部标签推送到远程库,使用命令: git push origin --tags 如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除: $ git tag -d v0.9 Deleted tag 'v0.9' (was f52c633) 然后,从远程删除。删除命令也是push,但是格式如下: $ git push origin :refs/tags/v0.9 To github.com:michaelliao/learngit.git - [deleted] v0.9 最后,登陆GitHub查看看看是否真的从远程库中删除了此标签。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:5:2","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"与远程库相关的操作\r使用分布式版本控制系统时,是有一台电脑充当服务器的角色。这样,当我们有了修改时直接把修改提交到服务器的仓库里。其他人若是想获取这次修改,直接从服务器仓库中拉取即可。 而GitHub就扮演着这个服务器仓库的角色。你需要先注册一个GitHub账号,配置好你电脑的SSH Key,这样才能将你使用的电脑与你的GitHub账号绑定起来(如何配置SSH)。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:6:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"与远程库建立连接\r与远程库建立连接有两种方式: 使用 git clone 命令。 当你克隆某个远程库到本地后,Git会自动创建一个名为 origin 的远程库引用(如果已经存在此引用,则新值会覆盖旧值),也可以指定Git创建的远程库引用名,命令: git clone \u003c仓库URL\u003e --origin \u003c自定义远程仓库名\u003e 在本地仓库下运行命令:git remote add 远程库引用名 remote-url 如果不指定远程库引用名,Git会使用默认的origin。如果想要更改远程库的别名,则使用命令 git remote rename old-name new-name ","date":"2024-07-21","objectID":"/posts/a538cc4/:6:1","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"查看远程库信息\r如果想要查看现在和哪些仓库建立的连接,可以使用命令:git remote 或 git remote -v,后一个命令可以查看详细的远程库信息,比如远程库的地址(没有push权限则不能看到)。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:6:2","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"与远程库断开连接\r使用命令:git remote rm 远程库引用名 ,即可断开与此远程库的连接 ","date":"2024-07-21","objectID":"/posts/a538cc4/:6:3","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"抓取或拉取远程库分支\r当远程库有了新的提交(更新),则需要将这些更新取回本地,这时就要用到命令: git fetch 远程库引用名 将远程库的全部更新抓取到本地,如果只想要抓取某个分支的更新,则使用命令: git fetch 远程库引用名 分支名 对于抓取的更新,在本机上需要用 远程库引用名/分支名 的形式读取。比如origin主机的master分支,就要用 origin/master 读取。可用前面提到的命令来查看远程分支。在确定合并不会发生冲突后,就使用命令 git merge 在本地分支上合并远程分支。 也可使用命令:git pull 拉取远程库的某个分支并与本地的指定分支合并。即 git pull = git fetch + git merge,其完整格式为: git pull 远程库引用名 远程分支名:本地分支名 如果远程分支是与当前分支合并,则可省略冒号及其后面的内容: git pull 远程库引用名 远程分支名 在某些场合,Git会自动在本地分支与远程分支之间,建立一种追踪关系(tracking,也叫上下游关系)。比如,在 git clone 的时候,所有本地分支默认与远程主机的同名分支,建立追踪关系,也就是说,本地的master分支自动 “追踪” origin/master分支。也可使用前面的命令来手动建立追踪关系。如果当前分支与远程存在追踪关系,在拉取时就可省略远程分支名。 git pull origin 上面命令表示,本地的当前分支自动与远程库对应的追踪分支进行合并。如果合并需要采用rebase模式,可以使用--rebase选项: git pull --rebase origin 如果远程主机删除了某个分支,默认情况下,git pull 不会在拉取远程分支的时候,删除对应的本地分支。这是为了防止,由于其他人操作了远程主机,导致git pull不知不觉删除了本地分支。 但是,你可以改变这个行为,加上参数 -p 就会在本地删除远程已经删除的分支。 $ git pull -p # 等同于下面的命令 $ git fetch --prune origin # 或下面这个命令 $ git fetch -p ","date":"2024-07-21","objectID":"/posts/a538cc4/:6:4","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"推送到远程库\rgit push命令用于将本地分支的更新,推送到远程主机。它的格式与git pull命令相仿。 git push 远程库引用名 本地分支名:远程分支名 如果省略远程分支名,则表示将指定的本地分支推送到与之存在 “追踪关系” 的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。 $ git push origin master 上面命令表示,将本地的master分支推送到origin主机的master分支。如果后者不存在,则会被新建。 如果省略本地分支名和远程分支名,则表示将当前所在分支推送到其上游远程分支,如果该远程分支不存在,则会被新建。 $ git push origin 如果只省略本地分支名,则表示删除指定的远程分支。 $ git push origin :master # 等同于 $ git push origin --delete master 如果你想将本地的所有分支都推送到远程主机,不管是否存在对应的远程分支,这时需要使用--all选项。 git push --all origin 推送时,如果远程库的版本比本地库的版本新,则推送时Git会报错,要求先在本地做 git pull 合并差异,然后再推送到远程主机。 ","date":"2024-07-21","objectID":"/posts/a538cc4/:6:5","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"},{"categories":["Git"],"content":"参考资料\rhttps://blog.51cto.com/u_15242250/2856081 https://www.bilibili.com/video/BV1vy4y1s7k6/?p=8\u0026spm_id_from=pageDriver\u0026vd_source=744dd2bfd43a3b6a0d6a04beeeb1f108 https://git-scm.com/book/en/v2 https://www.liaoxuefeng.com/wiki/896043488029600 https://www.cnblogs.com/linj7/p/14377278.html https://www.ruanyifeng.com/blog/2014/06/git_remote.html ","date":"2024-07-21","objectID":"/posts/a538cc4/:7:0","tags":["Git","笔记"],"title":"Git学习笔记","uri":"/posts/a538cc4/"}]