├── .gitignore ├── env_setup ├── img │ ├── compile_hello.png │ ├── env.png │ ├── env2.png │ ├── env3.png │ ├── gcc.png │ ├── gcc_version.png │ ├── marp.png │ ├── marp_view_export.png │ ├── open_ps.png │ ├── powershell.png │ └── powershell_search.png ├── main.md └── vscode-config-files.md ├── r1 ├── img │ ├── bell_labs.png │ ├── cpp23.png │ ├── exit_-1.png │ ├── principia_mathematica.png │ ├── rtfm.jpg │ ├── the_cpp_pl.jpg │ └── unicode.png └── r1.md ├── r10 ├── img │ └── templeRun.jpg └── r10.md ├── r11 ├── demo │ ├── message │ │ └── message.hpp │ └── sum │ │ ├── a.cpp │ │ ├── sum.cpp │ │ └── sum.hpp └── r11.md ├── r12 ├── demo │ ├── a.cpp │ ├── expr.hpp │ └── use.cpp └── r12.md ├── r13 ├── check_result.csv └── r13.md ├── r14 ├── img │ ├── deque.png │ ├── forward_list.png │ ├── iostream_inheritance.png │ ├── list.png │ ├── range-begin-end.svg │ └── vector.png └── r14.md ├── r15 └── r15.md ├── r2 ├── img │ ├── doge.jpg │ └── scopes.png └── r2.md ├── r3 ├── img │ ├── mdarray │ │ ├── mdarray.png │ │ └── mdarray.tex │ └── ptradd │ │ ├── ptradd.png │ │ └── ptradd.tex └── r3.md ├── r4 ├── img │ ├── malloc2d │ │ ├── malloc2d.png │ │ ├── malloc2d.tex │ │ └── malloc2d.xdv │ └── translations │ │ ├── translations.png │ │ ├── translations.tex │ │ └── translations.xdv └── r4.md ├── r5 ├── img │ ├── yangge.png │ └── yanglp.jpg └── r5.md ├── r6 └── r6.md ├── r7 └── r7.md ├── r8 ├── check.csv ├── img │ └── tle.png └── r8.md └── r9 └── r9.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.pdf 3 | tmp 4 | *.mp4 5 | *.aux 6 | *.log 7 | *.zip 8 | *.tar -------------------------------------------------------------------------------- /env_setup/img/compile_hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/compile_hello.png -------------------------------------------------------------------------------- /env_setup/img/env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/env.png -------------------------------------------------------------------------------- /env_setup/img/env2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/env2.png -------------------------------------------------------------------------------- /env_setup/img/env3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/env3.png -------------------------------------------------------------------------------- /env_setup/img/gcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/gcc.png -------------------------------------------------------------------------------- /env_setup/img/gcc_version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/gcc_version.png -------------------------------------------------------------------------------- /env_setup/img/marp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/marp.png -------------------------------------------------------------------------------- /env_setup/img/marp_view_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/marp_view_export.png -------------------------------------------------------------------------------- /env_setup/img/open_ps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/open_ps.png -------------------------------------------------------------------------------- /env_setup/img/powershell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/powershell.png -------------------------------------------------------------------------------- /env_setup/img/powershell_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/env_setup/img/powershell_search.png -------------------------------------------------------------------------------- /env_setup/main.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | --- 4 | 5 | # 搭环境 6 | 7 | GKxx 8 | 9 | --- 10 | 11 | # 前置知识 12 | 13 | --- 14 | 15 | ## 编辑器、编译器、IDE 16 | 17 | - 编辑器:任何可以编辑文本的软件,例如记事本、Word、WPS 文字、手机备忘录 18 | 19 | --- 20 | 21 | ## 编辑器、编译器、IDE 22 | 23 | - 编辑器:任何可以编辑文本的软件,例如记事本、Word、WPS 文字、手机备忘录 24 | - 但我们需要的是**代码编辑器**,尤其是**现代代码编辑器**: 25 | - `Notepad++`, `Visual Studio Code`, `Vim`, `Sublime Text`, ... 26 | 27 | --- 28 | 29 | ## 编辑器、编译器、IDE 30 | 31 | - 编辑器:任何可以编辑文本的软件,例如记事本、Word、WPS 文字、手机备忘录 32 | - 但我们需要的是**代码编辑器**,尤其是**现代代码编辑器**: 33 | - `Notepad++`, `Visual Studio Code`, `Vim`, `Sublime Text`, ... 34 | - 编译器:用来将高级语言的代码“翻译”成计算机真正能执行的代码 35 | - 比较流行的C/C++编译器:`GCC`(GNU), `Clang`(LLVM), `MSVC`(Microsoft), `ICC`(Intel), ... 36 | 37 | --- 38 | 39 | ## 编辑器、编译器、IDE 40 | 41 | - 编辑器:任何可以编辑文本的软件,例如记事本、Word、WPS 文字、手机备忘录 42 | - 但我们需要的是**代码编辑器**,尤其是**现代代码编辑器**: 43 | - `Notepad++`, `Visual Studio Code`, `Vim`, `Sublime Text`, ... 44 | - 编译器:用来将高级语言的代码“翻译”成计算机真正能执行的代码 45 | - 比较流行的C/C++编译器:`GCC`(GNU), `Clang`(LLVM), `MSVC`(Microsoft), `ICC`(Intel), ... 46 | - IDE:**I**ntegrated **D**evelopment **E**nvironment(集成开发环境):代码编辑器 + 编译器 + 调试器 + 项目管理工具 + ...... 47 | 48 | --- 49 | 50 | ## 编辑器、编译器、IDE 51 | 52 | - 代码编辑器:`Visual Studio Code`, `Vim`, `Sublime Text`, ... 53 | - 编译器:用来将高级语言的代码“翻译”成计算机真正能执行的代码 54 | - IDE:**I**ntegrated **D**evelopment **E**nvironment(集成开发环境):代码编辑器 + 编译器 + 调试器 + 项目管理工具 + ...... 55 | - 好的IDE有各种工具,例如 linter, profiler, 版本管理等等 56 | - **宇宙最强IDE**:Microsoft Visual Studio 57 | - 常见的IDE:VS, CLion, PyCharm, ... 58 | 59 | --- 60 | 61 | ## 编辑器、编译器、IDE 62 | 63 | - 代码编辑器:`Visual Studio Code`, `Vim`, `Sublime Text`, ... 64 | - 编译器:用来将高级语言的代码“翻译”成计算机真正能执行的代码 65 | - IDE:**I**ntegrated **D**evelopment **E**nvironment(集成开发环境):代码编辑器 + 编译器 + 调试器 + 项目管理工具 + ...... 66 | - 我们实际需要的是一个 IDE 67 | - 你可以直接使用像 CLion 或 VS 这样的狠货,也可以用编辑器 + 编译器 + ... 自己搭一套。 68 | 69 | --- 70 | 71 | # 安装编译器 72 | 73 | GCC (MinGW) 和 Clang(可选,Winlibs 自带) 74 | 75 | --- 76 | 77 | ## Linux(以 Ubuntu 为例) 78 | 79 | - `sudo apt install gcc g++` 或者 `sudo apt install build-essential` 即可安装默认版本的 `gcc` 和 `g++`,在 Ubuntu 20.04 上安装的版本是 9。 80 | - 添加 PPA源:`sudo apt-add-repository ppa:ubuntu-toolchain-r/test` 之后,`sudo apt install gcc-11 g++-11` 即可安装 11。在 Ubuntu 22.04 上还可以装 12。 81 | - `wget https://apt.llvm.org/llvm.sh` 82 | `sudo chmod +x llvm.sh` 83 | `sudo ./llvm.sh 15` 84 | 安装 `clang-15`(即目前的最新版 `clang`) 85 | 86 | --- 87 | 88 | ## Windows 89 | 90 | 无法直接安装 `GCC`,我们需要 `MinGW`:**Min**imalist **G**NU for **W**indows. 91 | - 实际上我们需要 `MinGW-w64`。`MinGW` 停止维护很久了。 92 | 93 | [Winlibs](https://winlibs.com/) 是个好东西 94 | - 在 **Download** 下选择 **UCRT runtime** 中标有 **(LATEST)** 的那个 release 里的 Win64 Zip archive(建议选带有 LLVM/Clang 的) 95 | 96 | #### 我不识字版: 97 | 98 | [GCC 12.2.0 + Clang 15.0.7 + MinGW-w64 10.0.0 (UCRT) release 64, Win64 Zip](https://github.com/brechtsanders/winlibs_mingw/releases/download/12.2.0-15.0.7-10.0.0-ucrt-r4/winlibs-x86_64-posix-seh-gcc-12.2.0-llvm-15.0.7-mingw-w64ucrt-10.0.0-r4.zip) 99 | 100 | --- 101 | 102 | ## Windows 103 | 104 | 解压后将 `mingw64` 这个文件夹放在 C 盘或 D 盘,**路径最好简单一点**,例如 `C:\mingw64` 或者 `D:\mingw64`。 105 | 106 | --- 107 | 108 | ## Windows 环境变量 109 | 110 | 按 `Win` 键,输入 `env`,即可搜出这个选项 111 | 112 | ![](img/env.png) 113 | 114 | --- 115 | 116 | ## Windows 环境变量 117 | 118 | 编辑 `Path` 119 | 120 | ![](img/env2.png) 121 | 122 | --- 123 | 124 | ## Windows 环境变量 125 | 126 | 新建一项,输入 `mingw64\bin` 所在的位置,最好把它移到最上面。 127 | 128 | --- 129 | 130 | ![](img/env3.png) 131 | 132 | --- 133 | 134 | # 终端(命令行,命令提示符) 135 | 136 | Terminal / Shell / Command Line / Command Prompt 137 | 138 | --- 139 | 140 | ## Windows PowerShell 141 | 142 | 按 `Win`+`r` 输入 `powershell`,或者按 `Win` 输入 `powershell` 搜索,打开 Windows PowerShell 143 | 144 | ![](img/powershell_search.png) 145 | 146 | --- 147 | 148 | ## Windows PowerShell 149 | 150 | ![](img/powershell.png) 151 | 152 | --- 153 | 154 | ## 在终端输入命令 155 | 156 | Windows PowerShell 常见命令(不一定全都掌握,但 `cd` 必须会): 157 | 158 | - `ls` 列出当前工作目录(Current Working Directory, CWD)下的文件 159 | - 默认情况下打开 PowerShell,工作目录是`C:\Users\Username` 160 | - `cd somePath` 将工作目录切换到 `somePath`,可以是相对或绝对路径 161 | - `cd ..` 切换到上一级目录 162 | - `cd 'D:\Program Files'` 切换到 `D:\Program Files`。加引号是因为有空格 163 | - `mkdir CS100` 在当前工作目录下创建名为 `CS100` 的文件夹 164 | - `rmdir CS100` 删除这个文件夹 165 | - `del file` 删除名为 `file` 的文件 166 | 167 | (我差不多只会这些了,因为我平常都用 Linux ...) 168 | 169 | --- 170 | 171 | ## 在终端输入命令 172 | 173 | 输入 `gcc`,PowerShell 会试图在 `Path` 环境变量所包含的路径中寻找名为 `gcc.exe` 的可执行文件,它实际上位于 `mingw64\bin` 174 | 175 | 如果编译器的安装及 `Path` 设置正确,应当看到如下输出 176 | 177 | ![](img/gcc.png) 178 | 179 | --- 180 | 181 | ## 在终端输入命令 182 | 183 | 要查看编译器的版本:给出 `--version` 参数 184 | 185 | ![](img/gcc_version.png) 186 | 187 | `gcc` 和 `clang` 是 C 编译器,`g++` 和 `clang++` 是 C++ 编译器 188 | 189 | --- 190 | 191 | ## 编译运行一个 Hello World 程序 192 | 193 | 假设你的 CS100 课程的文件夹位于 `D:\courses\CS100` 194 | 195 | - 在 `D:\courses\CS100\tmp` 创建 `hello.c`,用记事本打开,输入如下代码 196 | 197 | ```c 198 | #include 199 | int main() { 200 | puts("Hello world"); 201 | return 0; 202 | } 203 | ``` 204 | 205 | - 打开 PowerShell 并切换到 `D:\courses\CS100\tmp`,输入如下命令 206 | 207 | ``` 208 | gcc hello.c -o hello.exe 209 | ``` 210 | 211 | 将会得到可执行文件 `hello.exe`。该文件名由紧跟在 `-o` 之后的字符串指定。 212 | 213 | --- 214 | 215 | ## 编译运行一个 Hello World 程序 216 | 217 | 在 PowerShell 里输入 `.\hello` 即可运行 `hello.exe`。 218 | 219 | - 当然也可以双击运行,但你可能会看到它一闪而过。 220 | 221 | ![](img/compile_hello.png) 222 | 223 | --- 224 | 225 | ## 编译运行一个 Hello World 程序 226 | 227 | 也可以用资源管理器打开 `D:\courses\CS100\tmp` 后,在地址栏输入 `powershell`,这时打开的 PowerShell 的工作目录就是这个文件夹。 228 | 229 | ![](img/open_ps.png) 230 | 231 | --- 232 | 233 | ## 向编译器传递参数 234 | 235 | ``` 236 | gcc hello.c -o hello.exe 237 | ``` 238 | 239 | 跟在 `gcc` 之后的各项是传递给 `gcc.exe` 的**参数**。 240 | 241 | - `hello.c` 是它需要编译的文件名 242 | - 如果它不在当前工作目录下,需要指明路径,例如 `..\..\CS101\pa1\a.c` 243 | - `-o hello.exe` 指明生成的可执行文件的名字,也可以带有路径 244 | - Windows 下通常以 `.exe` 为后缀,Linux / Mac OS 下通常无后缀名 245 | - 还可以有其它参数,例如 `-std=c17` 指明采用 2017 年发布的语言标准 C17,`-lm` 链接数学库,`--save-temps` 保留编译过程中产生的临时文件。 246 | - 以上参数的顺序无所谓 247 | 248 | --- 249 | 250 | # Visual Studio Code 251 | 252 | 编写、编译、运行 C/C++ 代码 253 | 254 | --- 255 | 256 | ## VSCode 257 | 258 | VSCode 是一个现代的**代码编辑器** 259 | - 它不能帮你**编译**程序 260 | - “为什么我的代码在 VSCode 上跑出来是这样...?” 261 | - 但它有大量好用的插件,可以帮你一键调用编译器、方便地调试代码、版本管理、在远程/容器里开发,甚至上QQ、逛知乎、听网易云、炒股...... 262 | 263 | --- 264 | 265 | ## 下载、安装 VSCode 266 | 267 | - 去[官网](https://code.visualstudio.com) 268 | - 挑个好位置安装,例如 `D:\Program Files\Microsoft VS Code`,最好别挤在 C 盘里,**更不要装在桌面**! 269 | 270 | --- 271 | 272 | ## 配置 VSCode 273 | 274 | VSCode 有 $N+1$ 套配置: 275 | - 每个工作区/文件夹可以有自己的配置,以 `json` 文件的形式存放在 `.vscode` 目录下。(`.` 开头的文件/文件夹可能会被隐藏, `查看` - `显示` - `隐藏的项目`) 276 | - 另外有一套全局配置,也是 `json` 文件的形式,存在于某个特定的位置。 277 | - 某一项设置如果在当前工作区/文件夹的配置中出现了,就采用这里指定的值,否则采用全局配置中的值。 278 | 279 | --- 280 | 281 | ## 安装插件(扩展,extension): 282 | 283 | - Code Runner:提供一个按钮,运行一个特定的指令,可以一键编译运行 284 | - C/C++:提供 C/C++ 代码高亮、静态分析等功能 285 | - vscode-icons:更好的文件图标显示(安装后记得 enable) 286 | - One Dark Pro 和 GitHub Theme:个人比较喜欢的颜色主题 287 | - GlassIt-VSC:按 `Ctrl`+`Alt`+`z` 变透明,按 `Ctrl`+`Alt`+`c` 变回来 288 | - Office Viewer(Markdown Editor):我用它打开 pdf 和图片 289 | - Chinese (Simplified) Language Pack for Visual Studio Code? 290 | 291 | **以上插件已经足够,不建议安装很多 C/C++ 相关的插件(例如 Clang Command Adapter、C/C++ Runner 等),否则配置起来很困难** 292 | 293 | 个人不建议安装 VSCode 推荐的 C/C++ Extension Pack。 294 | 295 | --- 296 | 297 | ## 全局配置 298 | 299 | 按 `Ctrl`+`,`,或者左下角的齿轮 - `Settings` 300 | 301 | - Code-runner: Save File Before Run 设为 true 302 | - Code-runner: Run In Terminal 设为 true 303 | - Code-runner: Ignore Selection 设为 true 304 | - Editor: Format On Save 设为 true 305 | - Editor: Format On Type 设为 true 306 | - Editor: Accept Suggestion On Enter 个人建议设为 off,避免和换行冲突(按 `Tab` 仍然可以接受补全) 307 | 308 | --- 309 | 310 | ## 工作区配置 311 | 312 | 1. 为这门课程创建文件夹,例如 `D:\courses\CS100` 313 | 2. `File` - `Open Folder...`(快捷键是 `Ctrl`+`k`+`Ctrl`+`o`)打开 `CS100` 文件夹。 314 | 315 | - 以后每一次都要先打开 VSCode,再打开文件夹,再编辑文件;而不是直接右击文件 - 用 VSCode 打开。否则工作区的配置无法起效。 316 | 3. 创建 `D:\courses\CS100\.vscode` 文件夹,创建文件 `.vscode\settings.json` 和 `.vscode\c_cpp_properties.json`,去[这里](https://www.luogu.com.cn/paste/scc7i5yq)复制这两个配置文件的内容。 317 | - 链接是 https://www.luogu.com.cn/paste/scc7i5yq 318 | - 注意阅读“注意事项”。 319 | 320 | --- 321 | 322 | ## Code Runner 的配置解释 323 | 324 | 打开 `hello.c`,可以看到右上角有一个播放按钮,这个按钮来自于 Code Runner。 325 | 326 | - 点击这个按钮(快捷键 `Ctrl`+`Alt`+`n`)时,Code Runner 会根据当前文件的语言在 `code-runner.executorMap` 中执行对应的指令。 327 | - 以 `"c"` 为例: 328 | - `cd $dir` 是切换到当前工作目录。`$dir` 是 Code Runner 提供的一个变量。 329 | - `gcc '$fileName' -o '$fileNameWithoutExt.exe' -std=c17 -Wall -Wpedantic -Wextra` 是编译指令。`-std=c17` 指定语言标准为 C17,`-Wall -Wpedantic -Wextra` 开启了更多 warning。 330 | - `echo 'compilation ends.'` 是让它在编译完毕后吱一声。 331 | - `.\\'$fileNameWithoutExt'` 运行可执行文件。 332 | 333 | --- 334 | 335 | ## Debugger 336 | 337 | 打开 `hello.c`,点击 `运行` - `开始调试`(快捷键 `F5`),选 `C++ (GDB/LLDB)`,选 gcc。 338 | 339 | - VSCode 会自动生成调试所需要的文件,例如 `tasks.json` 和 `launch.json` 340 | - 可以自己尝试打断点、单步执行等功能 341 | - 可以参考官网的 [Debug C++ in VSCode](https://code.visualstudio.com/docs/cpp/cpp-debug) 以及 [Configure C/C++ debugging](https://code.visualstudio.com/docs/cpp/launch-json-reference) 342 | 343 | - 个人体会:在学会使用 gdb 之前可能难以理解 VSCode 的调试功能,因为它实际上就是调用 gdb。实在不行就先“print-statement debugging”。 344 | 345 | --- 346 | 347 | ## 配置 VSCode 348 | 349 | - 目前为止,以上配置已经足够,不要胡乱粘贴别人的配置文件。 350 | - 遇到任何问题请首选 [官网](https://code.visualstudio.com) 和 [StackOverflow](https://www.stackoverflow.com),或者在 Piazza 提问,或单独找TA。 351 | 352 | --- 353 | 354 | ### VSCode 的集成终端(Integrated terminal) 355 | 356 | - `View` - `Terminal`(快捷键 `Ctrl`+`` ` ``)可以打开 VSCode 内置的终端,在 Windows 上它默认使用的是 PowerShell。 357 | - Code Runner 的 Run Code 功能也会打开这个集成终端,因为我们在全局设置了 Run In Terminal。 358 | 359 | ### 格式化代码 360 | 361 | 右键 - `Format Document`(快捷键 `Alt`+`Shift`+`f`)可以格式化整个文件。 362 | - 我们已经在全局设置了 Format On Type 和 Format On Save 363 | 364 | --- 365 | 366 | ## 关于我的幻灯片 367 | 368 | 用 Markdown 就能制作幻灯片?你需要 Marp for VSCode。 369 | 370 | - 本学期我的习题课的所有材料在 https://github.com/GKxxQAQ/CS100-recitations-spring2023 ,但我可能不会上传 pdf 文件。 371 | - VSCode 安装 "Marp for VS Code" 插件,然后打开我的 `.md` 文件,它就会自动识别为幻灯片。 372 | ![](img/marp.png) 373 | - 预览和导出:![](img/marp_view_export.png) 374 | 375 | (某些效果更复杂的幻灯片不排除回归 $\LaTeX$ 的可能) -------------------------------------------------------------------------------- /env_setup/vscode-config-files.md: -------------------------------------------------------------------------------- 1 | ## settings.json: 2 | 3 | ```json 4 | { 5 | "code-runner.executorMap": { 6 | "cpp": "cd $dir && g++ '$fileName' -o '$fileNameWithoutExt' -std=c++17 -Wall -Wpedantic -Wextra && echo 'compilation ends.' && ./'$fileNameWithoutExt'", 7 | "c": "cd $dir && gcc '$fileName' -o '$fileNameWithoutExt' -std=c17 -Wall -Wpedantic -Wextra && echo 'compilation ends.' && ./'$fileNameWithoutExt'" 8 | } 9 | } 10 | ``` 11 | 12 | ## c_cpp_properties.json: (用于配置"C/C++"插件) 13 | 14 | ### Windows: 15 | 16 | ```json 17 | { 18 | "configurations": [ 19 | { 20 | "name": "Win32", 21 | "compilerPath": "C:\\mingw64\\bin\\gcc.exe", 22 | "cStandard": "c17", 23 | "cppStandard": "c++17", 24 | "intelliSenseMode": "windows-gcc-x64", 25 | "compilerArgs": [ 26 | "-Wall", 27 | "-Wpedantic", 28 | "-Wextra" 29 | ] 30 | } 31 | ], 32 | "version": 4 33 | } 34 | ``` 35 | 36 | ### Mac: 37 | 38 | ```json 39 | { 40 | "configurations": [ 41 | { 42 | "name": "Mac", 43 | "compilerPath": "/usr/bin/gcc", 44 | "cStandard": "c17", 45 | "cppStandard": "c++17", 46 | "intelliSenseMode": "macos-clang-x64", 47 | "compilerArgs": [ 48 | "-Wall", 49 | "-Wpedantic", 50 | "-Wextra" 51 | ] 52 | } 53 | ], 54 | "version": 4 55 | } 56 | ``` 57 | 58 | 注意事项: 59 | * `code-runner.executorMap` 中的指令就是编译的时候在终端输入的指令,其中的编译选项可以根据需要自行调整。 60 | * 建议始终将 `code-runner.executorMap` 中的语言标准与 `c_cpp_properties.json` 里的 `cStandard` 和 `cppStandard` 保持一致。 61 | * 建议始终将 `code-runner.executorMap` 中的编译选项与 `c_cpp_properties.json` 里的 `compilerArgs` 保持一致。 62 | * `c_cpp_properties.json` 里的 `compilerPath` 需要设置为编译器所在的位置。 -------------------------------------------------------------------------------- /r1/img/bell_labs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/bell_labs.png -------------------------------------------------------------------------------- /r1/img/cpp23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/cpp23.png -------------------------------------------------------------------------------- /r1/img/exit_-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/exit_-1.png -------------------------------------------------------------------------------- /r1/img/principia_mathematica.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/principia_mathematica.png -------------------------------------------------------------------------------- /r1/img/rtfm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/rtfm.jpg -------------------------------------------------------------------------------- /r1/img/the_cpp_pl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/the_cpp_pl.jpg -------------------------------------------------------------------------------- /r1/img/unicode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r1/img/unicode.png -------------------------------------------------------------------------------- /r1/r1.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 1 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - C/C++ 语言标准 15 | - 基本数据类型 16 | - `main` 函数 17 | - 初识运算符和表达式 18 | - 说了 `Hello, world` 之后呢? 19 | 20 | --- 21 | 22 | # C/C++ 语言标准 23 | 24 | --- 25 | 26 | ## C/C++ 的诞生 27 | 28 | > 关于历史的部分我主要说 C++,因为比起 C 我更熟悉 C++。 29 | 30 | - C 诞生于1972年的 Bell Laboratories,由 Dennis Ritchie 发明。 31 | - 1979年,同样来自 Bell Lab 的 Bjarne Stroustrup 试图发明一种新的 C: 32 | - 它和 C 具有同等的抽象粒度, 33 | - 但吸收了一些好东西,尤其是来自 Simula 的“Class”和 strong static type checking。 34 | - 1983年12月,“C with Classes” 正式更名为 “C++”。 35 | 36 | --- 37 | 38 | ![](img/bell_labs.png) 39 | 40 | --- 41 | 42 | ## C++ 的诞生 43 | 44 | 彼时,C++ 的权威指南是1984年1月发布的第一份 C++ Manual。 45 | 46 | 1985年,Stroustrup 的著作《The C++ Programming Language》出版。 47 | 48 | 1990年:《The Annotated C++ Reference Manual》(*C++ ARM*) 49 | 50 | 1991年:《The C++ Programming Language》第二版。 51 | 52 | ![h:300](img/rtfm.jpg)![h:300](img/the_cpp_pl.jpg) 53 | 54 | --- 55 | 56 | ## C++ 的诞生 57 | 58 | 与此同时,不断有新的特性加入 C++: 59 | 60 | - 1990年7月:Templates 61 | - 1990年11月:Exceptions 62 | - 1993年3月:Run-time Type Identification (RTTI) 63 | - 1993年7月:Namespaces 64 | - 1994年:Standard Template Library (STL) 65 | 66 | --- 67 | 68 | ## 暂时尘埃落定:标准化 69 | 70 | 1998年,ISO/IEC 14882:1998 发布,即 C++ 的第一份 ISO 标准:**C++98**。 71 | 72 | - C++ 正式拥有标准文档,而非借助著作、手册等作为参考。 73 | - 标准化意味着不能随意变更,新特性要等下一个标准再加入。 74 | 75 | --- 76 | 77 | ## 标准化 78 | 79 | 1998年,ISO/IEC 14882:1998 发布,即 C++ 的第一份 ISO 标准:**C++98**。 80 | 81 | - 2003年:“C++03”,是 C++98 的一份更正 82 | - “C++0x”:新版本会在200x年到来? 83 | - C++0x -> C++11,一份对于语言内核和标准库的大更新。 84 | - "C++11 feels like a new language." 85 | - "Modern C++" 86 | - 自 C++11 起,每三年开一班列车:C++14, C++17, C++20, **C++23**, ... 87 | 88 | --- 89 | 90 | ![](img/cpp23.png) 91 | 92 | --- 93 | 94 | ## C 的标准化 95 | 96 | - 第一份 C 语言标准:ISO/IEC 9899:1990 (C90) 97 | - ANSI X3.159-1989 (C89) 和 C90 是同一个语言。 98 | - 1999年的 C99 加入了许多重要的东西,例如 `long long`,`bool`这样的类型。 99 | - 可惜 `long long` 没赶上 C++98,只能在 C++11 加入 C++ 标准,不过它早就被编译器支持了。 100 | - C++ 本来就有 `bool`。 101 | - C 比 C++ 轻量很多,标准更新的频率也低一些。自 C11 起每 6 年一次:C11, C17, **C23**, ... 102 | 103 | --- 104 | 105 | ## 编译时设置语言标准 106 | 107 | `gcc a.c -o a -std=c17` 108 | 109 | `g++ a.cpp -o a -std=c++20` 110 | 111 | - 试一试:`printf("%ld\n", __STDC_VERSION__)` 112 | - C++:`std::cout << __cplusplus << std::endl;` 113 | 114 | --- 115 | 116 | ## 去哪看语言标准? 117 | 118 | 直接看标准文档: 119 | - ISO 标准文档需要花钱购买 120 | - open-std 有一些 working draft 可以免费下载,但是对初学者极不友好 121 | 122 | 一份友好的语言标准参考:[cppreference.com](cppreference.com) 123 | - 以更友好、方便查找的方式对内容重新组织 124 | - 大量地引用标准原文,保证内容的权威性 125 | - 还包含 [compiler support](https://en.cppreference.com/w/cpp/compiler_support)、[experimental features](https://en.cppreference.com/w/cpp/experimental)、[常见第三方库](https://en.cppreference.com/w/cpp/links/libs) 等内容 126 | 127 | --- 128 | 129 | # 基本数据类型 130 | 131 | --- 132 | 133 | ## 整数类型 134 | 135 | - `short (int)`, `signed short (int)`, `unsigned short (int)` 136 | - `int`, `signed (int)`, `unsigned (int)` 137 | - `long (int)`, `signed long (int)`, `unsigned long (int)` 138 | - `long long (int)`, `signed long long (int)`, `unsigned long long (int)` 139 | 140 | --- 141 | 142 | ## 整数类型 143 | 144 | - `signed int` 和 `int` 是同一个类型吗?其它的呢? 145 | 146 | --- 147 | 148 | ## 整数类型 149 | 150 | - `signed int` 和 `int` 是同一个类型吗?其它的呢? 151 | - 不带 `unsigned` 的都是带符号类型,`signed` 可以省略。 152 | - **例外:`char` 和 `signed char` 不是同一个类型,和 `unsigned char` 也不是。** 153 | 154 | --- 155 | 156 | ## 整数类型 157 | 158 | - `signed int` 和 `int` 是同一个类型吗?其它的呢? 159 | - 不带 `unsigned` 的都是带符号类型,`signed` 可以省略。 160 | - **例外:`char` 和 `signed char` 不是同一个类型,和 `unsigned char` 也不是。** 161 | - `int` 的大小(表示范围)是多少?`long` 呢? 162 | 163 | --- 164 | 165 | ## 整数类型 166 | 167 | - `signed int` 和 `int` 是同一个类型吗?其它的呢? 168 | - 不带 `unsigned` 的都是带符号类型,`signed` 可以省略。 169 | - **例外:`char` 和 `signed char` 不是同一个类型,和 `unsigned char` 也不是。** 170 | - `int` 的大小(表示范围)是多少?`long` 呢? 171 | - [**implementation-defined**](https://en.cppreference.com/w/c/language/behavior)! 172 | - `short` 和 `int` 至少 16 位;`long` 至少 32 位;`long long` 至少 64 位。 173 | - `1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)` 174 | - https://en.cppreference.com/w/c/language/arithmetic_types 175 | 176 | --- 177 | 178 | ## 整数类型 179 | 180 | 表示范围:以 32 位整数为例(假设 `int` 是 32 位) 181 | 182 | - `int` 的范围是 $\left[-2^{31}, 2^{31}-1\right]$ 183 | - `unsigned int` 的范围是 $\left[0, 2^{32}-1\right]$ 184 | - `int` 和 `unsigned int` 能表示的数一样多 185 | - `int` 最高位是符号位,因此最大只能到 $2^{31}-1$。 186 | 187 | --- 188 | 189 | ## 布尔类型 190 | 191 | - `` 定义了 `bool` 类型 (since C99)。 192 | - `bool` 类型的变量有两种值:`true`(真)和 `false`(假)。 193 | - 在有 `bool` 之前人们一般用 `int` 代替 `bool`,用非零值表示 `true`,零表示 `false`。 194 | - C 是如此地偏爱 `int`,以至于 C23 之前 `true` 和 `false` 居然被 `#define` 为 `1` 和 `0`。(RTFSC) 195 | - C 的逻辑运算符和关系运算符的返回值类型也是 `int`,而非 `bool`。 196 | - 其实也可以不用知道。 197 | - 和其它整数类型之间的转换:非零值 $\Rightarrow$ `true`,零 $\Rightarrow$ `false`;`true` $\Rightarrow$ `1`,`false` $\Rightarrow$ `0`。 198 | 199 | --- 200 | 201 | ## 字符类型 202 | 203 | - `char`, `signed char`, `unsigned char` 204 | - 另外有一些宽字符/Unicode字符类型:`wchar_t`, `char16_t`, `char32_t` 205 | - `char` 既不是 `signed char`,也不是 `unsigned char`!它们是三种不同的类型 206 | - `char` 可能被实现为带符号的或不带符号的,这是 **implementation-defined behavior**。 207 | - 至于宽字符/Unicode... 情况有些复杂 208 | - "That's what Python people laugh at." 209 | - 我们建议暂时避开这个问题。 210 | 211 | --- 212 | 213 | [![](img/unicode.png)](https://www.bilibili.com/video/BV1NE411h7hb?p=9&vd_source=7940495b5667750a71bfa10a4c6eb2d9) 214 | 215 | --- 216 | 217 | ## 究竟选哪个类型? 218 | 219 | - 整数算术用 `int`。如果不够大,就用 `long long`。 220 | - 该用布尔用 `bool`,尤其是在 C++ 中。 221 | - 浮点数用 `double`。除非在特定场合中,否则 `float` 的精度经常不够用。 222 | - 不必担心 `double` 和 `float` 运算速度的差异。 223 | - `long double` 基本上用不到。 224 | 225 | --- 226 | 227 | # `main` 函数 228 | 229 | --- 230 | 231 | ## 初识函数 232 | 233 | 一个函数: 234 | - 接受一些**参数** (parameters), 235 | - 执行一些语句,称为**函数体** (function body), 236 | - 返回一个结果,称为**返回值** (return-value)。 237 | 238 | 例:数学函数 $f(x)=x^2,x\in\mathbb R$: 239 | 240 | ```c 241 | double f(double x) { 242 | return x * x; 243 | } 244 | ``` 245 | 246 | --- 247 | 248 | ## 初识函数 249 | 250 | 例:数学函数 $f(x)=x^2,x\in\mathbb R$: 251 | 252 | ```c 253 | double f(double x) { 254 | return x * x; 255 | } 256 | ``` 257 | 258 | 但函数体完全可以执行更复杂的操作: 259 | 260 | ```c 261 | double f(double x) { 262 | printf("function 'f' is called with x = %lf\n", x); 263 | return x * x; 264 | } 265 | ``` 266 | 267 | --- 268 | 269 | ## `main` 函数 270 | 271 | C 程序的 entrypoint,具有如下三种形式之一: 272 | 273 | - ```c 274 | int main(void) 275 | ``` 276 | - ``` c 277 | int main(int argc, char **argv) 278 | ``` 279 | - Another implementation-defined signature. 280 | 281 | 对于第三种,一个典型的例子是许多操作系统支持的 282 | ```c 283 | int main(int argc, char **argv, char **envp) 284 | ``` 285 | 其中 `envp` 用来传递环境变量。 286 | 287 | https://en.cppreference.com/w/c/language/main_function 288 | 289 | --- 290 | 291 | ## `main` 函数 292 | 293 | 目前我们只需了解 `int main(void)`。 294 | 295 | - 这个函数不接受任何参数。 296 | - `main` 函数应当返回 `int`。如果程序正常退出,返回 `0`;否则返回一个非零值。 297 | - `return 0;` 语句无需显式地写出。若控制流到达函数体末尾而没有遇到一条 `return` 语句,则等价于 `return 0;`。 298 | 299 | 300 | 301 | 302 | 303 | --- 304 | 305 | ## `main` 函数 306 | 307 | 试一试:`void main() {}` 会发生什么? 308 | 309 | > The definition `void main()` is not and never has been C++, nor has it even been C. - Bjarne Stroustrup 310 | 311 | (我当年 CS100 期中考试的故事) 312 | 313 | --- 314 | 315 | ## `main` 函数 316 | 317 | > The definition `void main()` is not and never has been C++, nor has it even been C. - Bjarne Stroustrup 318 | 319 | `f(void)` 和 `f()`: 320 | 321 | - C++ 和 C23 开始,都表示不接受参数。 322 | - C23 之前 `f()` 表示接受任意多个任意类型的参数,`f(void)` 表示不接受参数。 323 | - Stroustrup 发明 C-with-Classes 时引入了 `f(void)` 这个写法。 324 | - `f(void)` 不够优雅,而 `f()` 接收参数不符合直觉。 325 | 326 | --- 327 | 328 | ## `main` 函数 329 | 330 | 你可能看过某些人省去返回值类型: 331 | 332 | ```c 333 | main() { 334 | // ... 335 | } 336 | ``` 337 | 338 | 在 C99 以前,函数的返回值类型可以不显式地指出,这时这个返回值类型默认为 `int`。**但 C99 取消了这一规则**。 339 | 340 | --- 341 | 342 | ## 练习 343 | 344 | 定义函数 `f` 来计算 $f(x)=x^2,x\in\mathbb Z$;输入一个整数,调用 `f` 计算其平方并输出。 345 | 346 | --- 347 | 348 | ## 练习 349 | 350 | 定义函数 `f` 来计算 $f(x)=x^2,x\in\mathbb Z$;输入一个整数,调用 `f` 计算其平方并输出。 351 | 352 | ```c 353 | #include 354 | 355 | int f(int x) { 356 | return x * x; 357 | } 358 | 359 | int main(void) { 360 | int n; 361 | scanf("%d", &n); 362 | printf("%d\n", f(n)); 363 | return 0; 364 | } 365 | ``` 366 | 367 | 注意:**输入**$\neq$**传递参数**,**输出**$\neq$**返回**。 368 | 369 | --- 370 | 371 | # 初识运算符和表达式 372 | 373 | --- 374 | 375 | ## 算术运算符 376 | 377 | 加减乘除模:`+`(一元正号、二元加号), `-`(一元负号、二元减号), `*`, `/`, `%` 378 | 379 | 位运算:`&`, `|`, `~`, `^`, `<<`, `>>` 380 | 381 | 算术表达式在求值时会经历一个非常难以描述的类型转换过程,包括**整型提升** (integral promotion) 和其它算术转换,例如带符号数与无符号数的转换、浮点数与整数的转换等。**最终,两侧运算对象会被转换成相同的类型,再进行算术求值。** 382 | 383 | --- 384 | 385 | ![](img/principia_mathematica.png) 386 | 387 | --- 388 | 389 | ## 算术运算符 390 | 391 | 我们不是语言律师,所以只记住那些经常用到的情况就行。 392 | 393 | - 如果两侧运算对象中至少有一个是浮点数,则另一个整数运算对象(如果有的话)将会被转换成那个浮点数类型,再进行运算。 394 | - 对于 `/`:浮点数相除是浮点数,整数相除是整数。 395 | - C99/C++11 以前,整除的取整方向是 implementation-defined。 396 | - 自 C99/C++11 起,规定**向零取整**。 397 | - `3 / -2` 的结果是? 398 | - `a / 2` 和 `a / 2.0` 的区别? 399 | - `(a + 0.0) / b`, `1.0 * a / b` 400 | 401 | --- 402 | 403 | ## 算术运算符 404 | 405 | - 取模 (modulo):`a % b` 406 | - 如果 `a` 是负数,结果是正的还是负的?`b` 是负数呢?两个都是负数呢? 407 | 408 | --- 409 | 410 | ## 算术运算符 411 | 412 | - 取模 (modulo):`a % b` 413 | - ~~如果 `a` 是负数,结果是正的还是负的?`b` 是负数呢?两个都是负数呢?~~ 414 | - 对任意整数(在不溢出的情况下)恒成立: 415 | ```c 416 | (a / b) * b + a % b == a 417 | ``` 418 | 419 | --- 420 | 421 | ## 复合赋值运算符 422 | 423 | `+=`, `-=`, `*=`, `/=`, `%=`, `<<=`, `>>=`, `&=`, `|=`, `^=` 424 | 425 | - `a = a op b` 等价于 `a op= b`。 426 | - **学会使用这样的运算符,让代码变得简洁、清晰**。 427 | - **简洁即美德**。 428 | 429 | --- 430 | 431 | # 说了 `Hello, world` 之后呢? 432 | 433 | --- 434 | 435 | ## 说了 `Hello, world` 之后呢? 436 | 437 | 一个比 `Hello, world` 稍微复杂一丁点儿的是“A+B Problem”: 438 | 439 | ```c 440 | #include 441 | 442 | int main(void) { 443 | int a, b; 444 | scanf("%d%d", &a, &b); 445 | printf("%d\n", a + b); 446 | return 0; 447 | } 448 | ``` 449 | 450 | --- 451 | 452 | ## A+B Problem 453 | 454 | 约定输入格式:两行,每行一个整数,分别表示 `a` 和 `b`。 455 | 456 | - `scanf("%d\n%d\n", &a, &b)` 会发生什么?试一试。 457 | 458 | --- 459 | 460 | ## A+B Problem 461 | 462 | 约定输入格式:两行,每行一个整数,分别表示 `a` 和 `b`。 463 | 464 | - `scanf("%d\n%d\n", &a, &b)` 会卡住,**直到你输入了一个非空白字符为止**。 465 | - 看看标准:https://en.cppreference.com/w/c/io/fscanf 466 | 467 | > whitespace characters: any single whitespace character in the format string **consumes all available consecutive whitespace characters** from the input (determined as if by calling isspace in a loop). Note that **there is no difference** between `"\n"`, `" "`, `"\t\t"`, or other whitespace in the format string. 468 | - whitespace character: “空白字符”,包括空格、换行、回车、制表等。 469 | 470 | --- 471 | 472 | ## A+B Problem 473 | 474 | 约定输入格式:两行,每行一个整数,分别表示 `a` 和 `b`。 475 | 476 | - `scanf("%d%d", &a, &b)` 可以吗?试一试。 477 | 478 | --- 479 | 480 | ## A+B Problem 481 | 482 | 约定输入格式:两行,每行一个整数,分别表示 `a` 和 `b`。 483 | 484 | - `scanf("%d%d", &a, &b)` 可以。 485 | - 中间的换行符不写也可以?看看标准: 486 | 487 | > `"%d"` matches a decimal integer. 488 | 489 | > The format of the number is the same as expected by `strtol` with the value 10 for the base argument. 490 | 491 | - `strtol` 是什么鬼?[点进去看看](https://en.cppreference.com/w/c/string/byte/strtol): 492 | 493 | > Interprets an integer value in a byte string pointed to by str. 494 | 495 | > **Discards any whitespace characters** (as identified by calling `isspace`) until the first non-whitespace character is found, then... 496 | 497 | --- 498 | 499 | ## 说了 `Hello, world` 之后呢? 500 | 501 | 理论与实践结合: 502 | 503 | - 理论:认准官方文档、手册。只需要高考英语及格的水平和亿点点耐心,你可以得到比任何教程、博客、题解、教材都要权威、准确的解答。 504 | - 英语水平越好,对耐心的要求越低。 505 | - 实践:**计算机科学是一门实践科学**,需要大胆地尝试、实验。 506 | - `long` 的范围是多大? 507 | - 函数里能调用自身吗? 508 | - 如果 `f` 是一个无参函数,调用它的方式是 `f;` 还是 `f();`? 509 | -------------------------------------------------------------------------------- /r10/img/templeRun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r10/img/templeRun.jpg -------------------------------------------------------------------------------- /r10/r10.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 10 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - Homework 5 讲评 15 | - 拷贝控制:总结 16 | - 何时使用 17 | - 默认行为以及三/五法则 18 | - 特别技术:copy-and-swap 19 | - 一个例子 20 | - 其它零碎的知识点 21 | - `friend` 22 | - `emplace_back`, `make_shared` 等函数:“完美转发” 23 | 24 | --- 25 | 26 | # Homework 5 讲评 27 | 28 | --- 29 | 30 | ## 2. 造 Python 31 | 32 | 算法都很简单,但要用高效的方式实现。 33 | 34 | - `std::string` 和 `std::vector` 都必须维护连续存储的特性,所以它们的 `erase` 只能将后面的所有元素集体往前挪,非常慢。 35 | - `s = s + a` 和 `s += a` 的区别,老生常谈的问题。 36 | 37 | --- 38 | 39 | ## `split(str, sep)` 40 | 41 | - 首先找到 `sep` 第一次出现的位置 $[l_1, r_1]$ 42 | - 不断循环,每次在 $[r_{i-1}+1,N]$ 里找 `sep` 第一次出现的位置 $[l_i,r_i]$,这就是 `sep` 第 $i$ 次出现的位置。 43 | - 直到找不到为止。 44 | - 假设找到了 $k$ 次,位置分别为 $[l_1,r_1],\cdots,[l_k,r_k]$,将下标区间 $[0,l_1)$、$(r_k,N]$ 以及每一个 $(r_i,l_{i+1})$ 对应的子串取出来,按顺序放进 vector 里,返回。 45 | 46 | --- 47 | 48 | ## `split(str, sep)` 49 | 50 | - 首先找到 `sep` 第一次出现的位置 $[l_1, r_1]$ 51 | - 不断循环,每次在 $[r_{i-1}+1,N]$ 里找 `sep` 第一次出现的位置 $[l_i,r_i]$,这就是 `sep` 第 $i$ 次出现的位置。 52 | - 直到找不到为止。 53 | - 假设找到了 $k$ 次,位置分别为 $[l_1,r_1],\cdots,[l_k,r_k]$,将下标区间 $[0,l_1)$、$(r_k,N]$ 以及每一个 $(r_i,l_{i+1})$ 对应的子串取出来,按顺序放进 vector 里,返回。 54 | 55 | 关键问题:在 $[r_{i-1}+1,N]$ 里找 `sep`,需要把 $[0,r_{i-1}]$ 先删掉吗? 56 | 57 | --- 58 | 59 | ## `split(str, sep)` 60 | 61 | - 首先找到 `sep` 第一次出现的位置 $[l_1, r_1]$ 62 | - 不断循环,每次在 $[r_{i-1}+1,N]$ 里找 `sep` 第一次出现的位置 $[l_i,r_i]$,这就是 `sep` 第 $i$ 次出现的位置。 63 | - 直到找不到为止。 64 | - 假设找到了 $k$ 次,位置分别为 $[l_1,r_1],\cdots,[l_k,r_k]$,将下标区间 $[0,l_1)$、$(r_k,N]$ 以及每一个 $(r_i,l_{i+1})$ 对应的子串取出来,按顺序放进 vector 里,返回。 65 | 66 | 关键问题:在 $[r_{i-1}+1,N]$ 里找 `sep`,需要把 $[0,r_{i-1}]$ 先删掉吗? 67 | 68 | - 无论是 `str = str.substr(...)` 还是 `str.erase(...)`,都相当于把 $[r_{i-1}+1,N]$ 全拷贝了一遍,时间复杂度直接 $O\left(N^2\right)$。 69 | 70 | --- 71 | 72 | # 拷贝控制:总结 73 | 74 | --- 75 | 76 | ## 拷贝控制成员 77 | 78 | - 拷贝构造函数 copy constructor 79 | - 拷贝赋值运算符 copy assignment operator 80 | - 移动构造函数 move constructor 81 | - 移动赋值运算符 move assignment operator 82 | - 析构函数 destructor 83 | 84 | 虽然后三个名字里没有“拷贝”,但也属于“copy control members”。 85 | 86 | 两个移动操作是 C++11 开始有的。 87 | 88 | --- 89 | 90 | ## 何时需要 91 | 92 | 首先,**分清初始化和赋值**。 93 | 94 | - 初始化是在变量声明语句中的,它必然调用构造函数。 95 | - 赋值是一个**运算符**,它必然在**表达式**中。 96 | 97 | “拷贝”是传统艺能:对于左值必然是拷贝,右值在移动操作存在的情况下被移动。但如果移动操作不存在,右值也被拷贝。(通常情况下) 98 | 99 | 析构函数的调用意味着对象生命期的结束。 100 | - 超出作用域时,程序结束时,以及 `delete`/`delete[]` 表达式 101 | 102 | --- 103 | 104 | ## 默认行为 105 | 106 | 在某些情况下(包括用 `= default` 显式要求时),编译器会合成一个具有默认行为的拷贝控制成员。 107 | 108 | - 默认行为:**先父类,后自己的成员**,且成员按**声明顺序**,逐个执行对应的操作。 109 | - 析构顺序相反。 110 | - 默认的移动行为:等同于将 `std::move` 作用于每个成员。 111 | - 并不是苛求每个成员或父类都采用移动操作。 112 | - 能移动就移动,不能移动就拷贝。 113 | 114 | 如果默认行为中涉及的任何一个操作无法正常进行(不存在或不可访问),这个函数就是删除的 (deleted function)。 115 | 116 | --- 117 | 118 | ## 为何需要 `std::move` 119 | 120 | ```cpp 121 | struct X { 122 | std::string s; 123 | X(X &&other) noexcept : s(other.s) {} 124 | }; 125 | ``` 126 | 127 | **右值引用是左值**:Anything that has a **name** is an lvalue, even if it is an rvalue reference. 128 | 129 | 从生命期的角度理解:右值引用延长了右值的生命期,使用右值引用时就如同在使用一个普通的(左值)变量。 130 | 131 | `other` 是左值,`other.s` 自然是左值,`s(other.s)` 是拷贝而非移动。 132 | 133 | --- 134 | 135 |
136 |
137 | 138 | ## `= delete` 139 | 140 | 删除的函数 (deleted function) 141 | 142 | - **仍然参与重载决议,** 143 | - **但如果被匹配到,就是 error**。 144 | - 任何函数都可以是删除的。 145 | 146 | 特别例外:如果编译器合成了一个删除的移动操作,它不会参与重载决议,这是为了让右值被拷贝。[[CWG1402]](https://cplusplus.github.io/CWG/issues/1402.html) 147 | - 《C++ Primer》在这个问题上说的是不对的。 148 |
149 |
150 | 151 | 152 | 153 |
154 |
155 | 156 | --- 157 | 158 | ## 三/五法则 159 | 160 | C++11 以前是“三”,C++11 以后是“五”。 161 | 162 | 如果你认为有必要自定义这五个函数中的任何一个,通常意味着这五个你都应该定义。 163 | 164 | "Define zero or five or them." 165 | 166 | --- 167 | 168 | ## 三/五法则 169 | 170 | 根据“五法则”:如果五个函数中的任何一个具有用户自定义 (user-provided) 的版本,编译器就不应该再合成其它那些用户没有定义的函数。 171 | 172 | - 重要例外:一个类不能没有析构函数 ~~就像...~~ 173 | - 另一个例外:兼容旧的代码 174 | - 在 C++98 时代,“三法则”并未在编译器的行为上予以体现。 175 | - 如果一个类有自定义的拷贝构造函数或析构函数,而没有自定义拷贝赋值运算符,C++98 编译器会合成这个拷贝赋值运算符。(拷贝构造函数同理) 176 | - 为了兼容旧的代码,不能直接禁止这种行为,只能将它判定为 deprecated。 177 | 178 | --- 179 | 180 | ## Copy-and-swap 181 | 182 | 能不能写出一个简单的 `swap` 函数,交换两个 `Dynarray` 对象的值? 183 | 184 | ```cpp 185 | class Dynarray { 186 | public: 187 | void swap(Dynarray &) noexcept; 188 | }; 189 | ``` 190 | 191 | --- 192 | 193 | ## Copy-and-swap 194 | 195 | 能不能写出一个简单的 `swap` 函数,交换两个 `Dynarray` 对象的值? 196 | 197 | ```cpp 198 | class Dynarray { 199 | public: 200 | void swap(Dynarray &other) noexcept { 201 | std::swap(m_storage, other.m_storage); 202 | std::swap(m_length, other.m_length); 203 | } 204 | }; 205 | ``` 206 | 207 | 直接交换 `m_storage` 指针,就可以快速交换两个“动态数组”。这个 `swap` 甚至是 `noexcept` 的,它远远好过传统的 `auto tmp = a; a = b; b = tmp;` 写法。 208 | 209 | --- 210 | 211 | ## Copy-and-swap 212 | 213 | 赋值 = 拷贝构造 + 析构:拷贝新的数据,销毁原有的数据。 214 | 215 | 能不能利用拷贝构造函数和析构函数写出一个拷贝赋值运算符? 216 | 217 | --- 218 | 219 | ## Copy-and-swap 220 | 221 | 赋值 = 拷贝构造 + 析构:拷贝新的数据,销毁原有的数据。 222 | 223 | 为 `other` 建立一个拷贝 `tmp`,直接将自己和 `tmp` 交换! 224 | 225 | ```cpp 226 | class Dynarray { 227 | public: 228 | Dynarray &operator=(const Dynarray &other) { 229 | auto tmp = other; 230 | swap(tmp); 231 | return *this; 232 | } 233 | }; 234 | ``` 235 | 236 | - 拷贝构造函数会负责正确拷贝 `other`。 237 | - `tmp` 的析构函数会正确销毁旧的数据。 238 | 239 | --- 240 | 241 | ## Copy-and-swap 242 | 243 | 更简洁些: 244 | 245 | ```cpp 246 | class Dynarray { 247 | public: 248 | Dynarray &operator=(const Dynarray &other) { 249 | Dynarray(other).swap(*this); // C++23: auto{other}.swap(*this); 250 | return *this; 251 | } 252 | }; 253 | ``` 254 | 255 | 自我赋值安全吗? 256 | 257 | --- 258 | 259 | ## Copy-and-swap 260 | 261 | ```cpp 262 | class Dynarray { 263 | public: 264 | Dynarray &operator=(const Dynarray &other) { 265 | Dynarray(other).swap(*this); // C++23: auto{other}.swap(*this); 266 | return *this; 267 | } 268 | }; 269 | ``` 270 | 271 | 不仅好写,还自我赋值安全,还提供强异常安全保证! 272 | 273 | --- 274 | 275 | ## "Copy"-and-swap 276 | 277 | 更进一步,直接在传参的时候做好“拷贝”。 278 | 279 | ```cpp 280 | class Dynarray { 281 | public: 282 | Dynarray &operator=(Dynarray other) noexcept { 283 | swap(other); 284 | return *this; 285 | } 286 | }; 287 | ``` 288 | 289 | 且慢——传参的时候真的发生了拷贝吗? 290 | 291 | --- 292 | 293 | ## "Copy"-and-swap 294 | 295 | ```cpp 296 | class Dynarray { 297 | public: 298 | Dynarray &operator=(Dynarray other) noexcept { 299 | swap(other); 300 | return *this; 301 | } 302 | }; 303 | ``` 304 | 305 | 如果参数是右值,`other` 将被**移动初始化**,而不是**拷贝初始化**。 306 | 307 | 也就是说,这个赋值运算符**既是一个拷贝赋值运算符,又是一个移动赋值运算符!** 308 | 309 | --- 310 | 311 | ## Copy-and-swap 312 | 313 | 通过实现一个快速、`noexcept` 的 `swap` 函数,一举多得。 314 | 315 | 利用这个 `swap` 实现赋值运算符:不需要额外做任何操作。 316 | 317 | - 自我赋值安全 318 | - 异常安全(提供强异常安全保证) 319 | - 同时获得拷贝赋值运算符和移动赋值运算符 320 | 321 | --- 322 | 323 | ## 《C++ Primer》13.4:拷贝控制实例 324 | 325 | 13.6.2 讲了 `Message` 类的移动操作。 326 | 327 | --- 328 | 329 | ## 在类外定义成员函数 330 | 331 | `Folder` 和 `Message` 类的众多成员函数相互依赖,因此在两个类型的定义完毕之前,这些成员函数都无法定义。 332 | 333 | 我们先给出两个类的定义,其中只包括成员函数的声明。 334 | - 某些简单的(例如默认构造函数)就顺手定义在类内了。 335 | 336 | 在类外定义一个成员函数:在函数名前面加上 `ClassName::` 就打开了类作用域,之后就和在类内一样了。 337 | 338 | ```cpp 339 | Message::Message(const std::string &contents) : m_contents{contents} {} 340 | void Message::save(Folder &f) { /* ... */ } 341 | Message &Message::operator=(const Message &other) { /* ... */ } 342 | Message::~Message() { /* ... */ } 343 | ``` 344 | 345 | --- 346 | 347 | ## `std::string` 拷贝还是移动? 348 | 349 | 我们已经习惯于将不修改的参数声明为 reference-to-`const`: 350 | 351 | ```cpp 352 | class Message { 353 | public: 354 | Message(const std::string &contents) 355 | : m_contents{contents} {} 356 | }; 357 | ``` 358 | 359 | 但是如果传进来的字符串是右值,能不能直接移动给 `m_contents`? 360 | 361 | --- 362 | 363 | ## `std::string` 拷贝还是移动? 364 | 365 | 如果参数是左值,将它拷贝给 `m_contents`;否则,将它移动给 `m_contents`。 366 | 367 | ```cpp 368 | class Message { 369 | public: 370 | Message(const std::string &contents) : m_contents{contents} {} 371 | Message(std::string &&contents) : m_contents{std::move(contents)} {} 372 | }; 373 | ``` 374 | 375 | --- 376 | 377 | ## `std::string` 拷贝还是移动? 378 | 379 | 如果参数是左值,将它拷贝给 `m_contents`;否则,将它移动给 `m_contents`。 380 | 381 | 直接按值传递不就行了? 382 | 383 | ```cpp 384 | class Message { 385 | public: 386 | Message(std::string contents) : m_contents{std::move(contents)} {} 387 | }; 388 | ``` 389 | 390 | 拷贝/移动会在对参数 `contents` 的初始化中自动决定,而我们只需要把 `contents` 移给 `m_contents`。 391 | 392 | --- 393 | 394 | ## 拷贝左值,移动右值 395 | 396 | 标准库的很多函数也为左值和右值做了这样的区分,比如 `std::vector::push_back`。 397 | 398 | 见 Homework 6 客观题第 1 题。 399 | 400 | --- 401 | 402 | ## `friend` 403 | 404 | `Message` 的 `save(folder)` 不仅会将 `&folder` 添加进自己的 `m_folders`,还会调用 `f.addMsg(this)` 将自己加进 `folder.m_messages`。 405 | 406 | 但是如果它本来就在 `folder.m_messages` 里,这就是重复的操作。 407 | 408 | 能不能允许 `Folder` 直接操作 `Message` 的 `m_folders`? 409 | - 反正这两个类都是我写的,我保证不搞破坏就行了。 410 | 411 | --- 412 | 413 | ## `friend` 414 | 415 | ```cpp 416 | class Message { 417 | friend class Folder; 418 | }; 419 | ``` 420 | 421 | 可以将类 `Folder` 声明为 `Message` 的 `friend`,则 `Folder` 的代码可以访问 `Message` 的 `private` 和 `protected` 成员。 422 | 423 | - 这个 `friend class Folder;` **不是**成员声明,**不受访问限制修饰符的作用**。 424 | - `friend` 声明可以放在任何位置,通常在一个类的开头或末尾集中声明所有 `friend`。 425 | 426 | --- 427 | 428 | ## `friend` 429 | 430 | 也可以声明某个单独的函数 431 | 432 | ```cpp 433 | class Message { 434 | friend class Folder; 435 | friend void modify(Message &m, const std::string &s); 436 | }; 437 | void modify(Message &m, const std::string &s) { 438 | // 在这里可以直接访问、修改 m.m_contents 439 | } 440 | ``` 441 | 442 | - 这个 `friend void modify(...);` **不是**成员函数声明,`friend` 函数**不是**成员函数,**不受访问限制修饰符的作用**。 443 | 444 | --- 445 | 446 | ## `friend` 447 | 448 | `friend` 函数也可以在类内定义,但它们仍然不是成员函数。 449 | 450 | ```cpp 451 | class Message { 452 | friend class Folder; 453 | friend void modify(Message &m, const std::string &s) { /* ... */ } 454 | }; 455 | ``` 456 | 457 | 但是将 `friend` 函数定义在类内会引发特殊的名字查找问题: 458 | 459 |
460 |
461 | 462 | ```cpp 463 | struct X { 464 | friend int add(int x, int y) { 465 | return x + y; 466 | } 467 | }; 468 | ``` 469 |
470 |
471 | 472 | ```cpp 473 | int main() { 474 | auto x = add(1, 2); 475 | // Error: `add` was not declared 476 | // in this scope. 477 | } 478 | ``` 479 |
480 |
481 | 482 | 个人不推荐将 `friend` 函数定义在类内,除了一个和模板有关的特殊情况(以后再说) 483 | 484 | --- 485 | 486 | ## 参数转发 487 | 488 | 回顾 `std::make_shared` 和 `std::make_unique`: 489 | 490 | ```cpp 491 | auto sp = std::make_shared(10, 'c'); // "cccccccccc" 492 | auto sp2 = std::make_shared("hello"); // "hello" 493 | auto up = std::make_unique("Alice", "2020123123"); 494 | ``` 495 | 496 | 甚至,如果传入右值,它们会移动构造那个对象: 497 | 498 | ```cpp 499 | auto sp3 = std::make_shared(std::move(*sp)); 500 | std::cout << *sp << std::endl; // empty string 501 | ``` 502 | 503 | 这种将参数转发给另一个函数,又能保持它们的值类别的操作叫做**完美转发** (perfect forwarding) 504 | 505 | --- 506 | 507 | ## 参数转发 508 | 509 | `std::make_shared/unique(...)` 可以接受任意多个任意类型的参数,并将它们原封不动地转发给 `T` 的构造函数,不丢失值类别,不丢失 `const`。 510 | 511 | 等学了模板,就知道是咋回事了。 512 | 513 | 标准库很多函数都支持这样的操作,其中非常典型的是**容器的 `emplace` 系列操作**: 514 | 515 | ```cpp 516 | std::vector students; 517 | students.emplace_back("Alice", "2020123123"); 518 | std::vector words; 519 | words.emplace_back(10, 'c'); 520 | ``` 521 | 522 | --- 523 | 524 | ## 标准库容器的 `emplace` 525 | 526 | ```cpp 527 | std::vector students; 528 | students.emplace_back("Alice", "2020123123"); 529 | std::vector words; 530 | words.emplace_back(10, 'c'); 531 | ``` 532 | 533 | `emplace` 系列操作利用传入的参数直接原地构造出那个对象,而不是将构造好的对象拷贝/移动进去。 534 | 535 | - 提高效率。 536 | - 对所存储的数据类型的要求进一步降低。尤其是 `std::list`(链表)自 C++11 起不需要 `T` 具备任何拷贝/移动操作,只要有办法构造和析构即可。 537 | - `vector` 由于需要搬家(增长时重新分配内存),无法存储不可拷贝、不可移动的元素,除非你不需要它搬家。 -------------------------------------------------------------------------------- /r11/demo/message/message.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGE_HPP 2 | #define MESSAGE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Folder; 9 | 10 | class Message { 11 | std::string m_contents; 12 | std::set m_folders; 13 | 14 | void addToAll(); 15 | void removeFromAll(); 16 | void moveFolders(Message &); 17 | 18 | public: 19 | explicit Message(std::string contents) : m_contents{std::move(contents)} {} 20 | Message(const Message &other) 21 | : m_contents{other.m_contents}, m_folders{other.m_folders} { 22 | addToAll(); 23 | } 24 | Message(Message &&other) : m_contents{std::move(other.m_contents)} { 25 | moveFolders(other); 26 | } 27 | Message &operator=(const Message &other) { 28 | removeFromAll(); 29 | m_contents = other.m_contents; 30 | m_folders = other.m_folders; 31 | addToAll(); 32 | return *this; 33 | } 34 | Message &operator=(Message &&other) { 35 | if (this != &other) { 36 | removeFromAll(); 37 | m_contents = std::move(other.m_contents); 38 | moveFolders(other); 39 | } 40 | return *this; 41 | } 42 | ~Message() { 43 | removeFromAll(); 44 | } 45 | 46 | void addFolder(Folder &folder) { 47 | m_folders.insert(&folder); 48 | } 49 | void removeFolder(Folder &folder) { 50 | m_folders.erase(&folder); 51 | } 52 | }; 53 | 54 | class Folder { 55 | std::set m_messages; 56 | 57 | public: 58 | void addMessage(Message &msg) { 59 | m_messages.insert(&msg); 60 | } 61 | void removeMessage(Message &msg) { 62 | m_messages.erase(&msg); 63 | } 64 | }; 65 | 66 | void Message::addToAll() { 67 | for (auto f : m_folders) 68 | f->addMessage(*this); 69 | } 70 | 71 | void Message::removeFromAll() { 72 | for (auto f : m_folders) 73 | f->removeMessage(*this); 74 | } 75 | 76 | void Message::moveFolders(Message &other) { 77 | other.removeFromAll(); 78 | m_folders = std::move(other.m_folders); 79 | addToAll(); 80 | } 81 | 82 | #endif // MESSAGE_HPP -------------------------------------------------------------------------------- /r11/demo/sum/a.cpp: -------------------------------------------------------------------------------- 1 | #include "sum.hpp" 2 | 3 | #include 4 | 5 | int main() { 6 | int a, b; 7 | std::cin >> a >> b; 8 | std::cout << sum(a, b) << std::endl; 9 | return 0; 10 | } -------------------------------------------------------------------------------- /r11/demo/sum/sum.cpp: -------------------------------------------------------------------------------- 1 | #include "sum.hpp" 2 | 3 | int sum(int a, int b) { 4 | return a + b; 5 | } -------------------------------------------------------------------------------- /r11/demo/sum/sum.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SUM_HPP 2 | #define SUM_HPP 3 | 4 | int sum(int a, int b); 5 | 6 | #endif // SUM_HPP -------------------------------------------------------------------------------- /r11/r11.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 11 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 拷贝控制实例:Message and Folder 15 | - 多文件和动态链接 16 | 17 | --- 18 | 19 | # 拷贝控制实例 20 | 21 | --- 22 | 23 | ## Message and Folder 24 | 25 | 考虑一个邮件系统: 26 | 27 | - 一个 Folder 中包含一些 Message 28 | - 一个 Message 可能存在于多个 Folder 中,但我们不希望拷贝 Message 的内容 29 | 30 | 复制一个 Message:复制其内容,并且将新的 Message 也加入相应的 Folder 31 | 32 | 复制一个 Folder:将它包含的所有 Message 也加入新的 Folder 33 | 34 | --- 35 | 36 | ## Message and Folder 37 | 38 | ```cpp 39 | class Message { 40 | std::string m_contents; 41 | std::set m_folders; 42 | }; 43 | class Folder { 44 | std::set m_messages; 45 | }; 46 | ``` 47 | 48 | 快速地插入、删除、查找元素的“集合”数据结构:`std::set`(标准库文件 ``) 49 | 50 | \* 编译器报错了? 51 | 52 | ``` 53 | error: ‘Folder’ was not declared in this scope 54 | 6 | std::set m_folders; 55 | | ^~~~~~ 56 | ``` 57 | 58 | --- 59 | 60 | ## Message and Folder 61 | 62 | “循环依赖”的情形:`Message` 需要 `Folder`,`Folder` 需要 `Message`。 63 | 64 | - 无论谁在前,都会有一个名字查找发生失败。 65 | 66 | 幸好,这里不完全类型就足够了 67 | 68 | - 我们只是使用了 `Folder*` 和 `Message*` 69 | - 没有创建其对象,没有使用其成员 70 | 71 | 所以在前面补一个声明即可。 72 | 73 | --- 74 | 75 | ## Message and Folder 76 | 77 | ```cpp 78 | class Folder; // declaration 79 | class Message { 80 | std::string m_contents; 81 | std::set m_folders; 82 | }; 83 | class Folder { 84 | std::set m_messages; 85 | }; 86 | ``` 87 | 88 | 先写几个简单的成员函数:添加一个 message,添加一个 folder 等等。 89 | 90 | --- 91 | 92 | ## Message and Folder 93 | 94 | `s.insert(x)` 向集合 `s` 添加一个元素;`s.erase(x)` 从 `s` 中删除元素 `x`。 95 | 96 |
97 |
98 | 99 | ```cpp 100 | class Message { 101 | std::string m_contents; 102 | std::set m_folders; 103 | public: 104 | void addFolder(Folder &folder) { 105 | m_folders.insert(&folder); 106 | } 107 | void removeFolder(Folder &folder) { 108 | m_folders.erase(&folder); 109 | } 110 | }; 111 | ``` 112 |
113 |
114 | 115 | ```cpp 116 | class Folder { 117 | std::set m_messages; 118 | public: 119 | void addMessage(Message &msg) { 120 | m_messages.insert(&msg); 121 | } 122 | void removeMessage(Message &msg) { 123 | m_messages.erase(&msg); 124 | } 125 | }; 126 | ``` 127 |
128 |
129 | 130 | 有没有哪个地方应该使用 `const`? 131 | 132 | --- 133 | 134 | ## Message and Folder 135 | 136 |
137 |
138 | 139 | ```cpp 140 | class Message { 141 | std::string m_contents; 142 | std::set m_folders; 143 | public: 144 | void addFolder(Folder &folder) { 145 | m_folders.insert(&folder); 146 | } 147 | void removeFolder(Folder &folder) { 148 | m_folders.erase(&folder); 149 | } 150 | }; 151 | ``` 152 | 153 | 有没有哪个地方应该使用 `const`? 154 |
155 |
156 | 157 | - 表面上看,`addFolder` 和 `removeFolder` 不改变被添加的 `folder` 158 | - 但如果声明为 `const Folder &folder`,就意味着 `m_folders` 里的元素也应该是 `const Folder *`。 159 | - 而我们存储这些 `Folder` 的指针,以后必然需要调用它们的 non-`const` 操作(例如添加、删除信息)。 160 | - 因此,`addFolder` 和 `removeFolder` 应该将那些不能修改的 `Folder` 拒之门外,所以这里不能带 `const`。 161 |
162 |
163 | 164 | --- 165 | 166 | ## Message and Folder 167 | 168 | `Message` 的构造函数:接受一个字符串作为信息的内容。 169 | 170 | ```cpp 171 | class Message { 172 | public: 173 | // 拷贝左值,移动右值 174 | Message(std::string contents) : m_contents{std::move(contents)} {} 175 | }; 176 | ``` 177 | 178 | --- 179 | 180 | ## Message and Folder 181 | 182 | `Message` 的拷贝操作: 183 | 184 | ```cpp 185 | class Message { 186 | public: 187 | Message(const Message &other) 188 | : m_contents{other.m_contents}, m_folders{other.m_folders} { 189 | for (auto f : m_folders) 190 | f->addMessage(*this); 191 | } 192 | ~Message() { 193 | for (auto f : m_folders) 194 | f->removeMessage(*this); 195 | } 196 | }; 197 | ``` 198 | 199 | --- 200 | 201 | ## Message and Folder 202 | 203 | `Message` 的拷贝操作: 204 | 205 | ```cpp 206 | class Message { 207 | public: 208 | Message &operator=(const Message &other) { 209 | for (auto f : m_folders) 210 | f->removeMessage(*this); 211 | m_contents = other.m_contents; 212 | m_folders = other.m_folders; 213 | for (auto f : m_folders) 214 | f->addMessage(*this); 215 | return *this; 216 | } 217 | }; 218 | ``` 219 | 220 | 避免重复:写两个 `private` 函数来实现“将自身添加到所有文件夹/从所有文件夹中删除”的功能。 221 | 222 | --- 223 | 224 | ## Message and Folder 225 | 226 | `Message` 的移动操作:移动 `m_contents`,但我们仍然需要将自身从原来的文件夹中删除,以及添加到所有新的文件夹中。 227 | 228 | 很遗憾,这个操作无法 `noexcept`。 229 | 230 | ```cpp 231 | class Message { 232 | public: 233 | Message(Message &&other) : m_contents{std::move(other.m_contents)} { 234 | moveFolders(other); 235 | } 236 | private: 237 | void moveFolders(Message &other) { 238 | other.removeFromAll(); 239 | m_folders = std::move(other.m_folders); 240 | addToAll(); 241 | } 242 | }; 243 | ``` 244 | 245 | --- 246 | 247 | ## Message and Folder 248 | 249 | `Message` 的移动操作:移动 `m_contents`,但我们仍然需要将自身从原来的文件夹中删除,以及添加到所有新的文件夹中。 250 | 251 | 很遗憾,这个操作无法 `noexcept`。 252 | 253 | ```cpp 254 | class Message { 255 | public: 256 | Message &operator=(Message &&other) { 257 | if (this != &other) { 258 | removeFromAll(); 259 | m_contents = std::move(other.m_contents); 260 | moveFolders(other); 261 | } 262 | return *this; 263 | } 264 | }; 265 | ``` 266 | 267 | --- 268 | 269 | ## 循环依赖 270 | 271 | 出现了问题:现在我们不得不使用 `Folder` 的成员函数,但这要求 `Folder` 不是一个不完全类型(其定义必须已经给出)。 272 | 273 | 如果交换两个类的顺序,同样的问题会在 `Folder` 中发生。 274 | 275 | --- 276 | 277 | ## 循环依赖 278 | 279 | 出现了问题:现在我们不得不使用 `Folder` 的成员函数,但这要求 `Folder` 不是一个不完全类型(其定义必须已经给出)。 280 | 281 | 如果交换两个类的顺序,同样的问题会在 `Folder` 中发生。 282 | 283 | 解决方案:**在类内声明函数,等到 `Folder` 的定义给出之后再定义。** 284 | 285 | --- 286 | 287 | ## 循环依赖 288 | 289 | ```cpp 290 | class Message { 291 | private: 292 | void addToAll(); 293 | void removeFromAll(); 294 | }; 295 | class Folder { 296 | // ... 297 | }; 298 | void Message::addToAll() { 299 | for (auto f : m_folders) 300 | f->addMessage(&this); 301 | } 302 | void Message::removeFromAll() { 303 | for (auto f : m_folders) 304 | f->removeMessage(&this); 305 | } 306 | ``` 307 | 308 | --- 309 | 310 | # 多文件和动态链接 311 | 312 | --- 313 | 314 | ## `#include` 315 | 316 | `#include` 机制:在其它真正的编译过程开始之前,由**预处理器**(preprocessor)将被 `#include` 的文件原封不动地复制过来。 317 | 318 | 所以如果一个文件被 `#include` 两遍,它所包含的内容就会出现两次。 319 | 320 | - 如果不加以保护,就会出现重复定义符号的问题。 321 | 322 | --- 323 | 324 | ## Include guard 325 | 326 | ```cpp 327 | #ifndef MY_FILE_HPP 328 | #define MY_FILE_HPP 329 | 330 | // 将头文件的内容放在这里 331 | 332 | #endif // MY_FILE_HPP 333 | ``` 334 | 335 | - `#ifndef X` ... `#endif`:如果宏 `X` 在此前没有定义过,那么编译这部分代码。 336 | - 如果 `MY_FILE_HPP` 此前没有定义过:首先在这里定义它,然后编译下面的代码。 337 | - 当这段代码再次出现时,宏 `MY_FILE_HPP` 已经被定义过了! 338 | 339 | --- 340 | 341 | ## 分离式编译 342 | 343 | 例:将函数的声明写在头文件里,定义写在另一个 `.cpp` 文件里。 344 | 345 | 在 `a.cpp` 里调用函数 `sum` 时,**只要 `sum` 的声明已经出现,其实就可以编译,只是暂时无法生成可执行文件。** 346 | 347 | - 因此只要在 `a.cpp` 里 `#include "sum.hpp"`。 348 | 349 | 编译:`g++ a.cpp sum.cpp -o a` 350 | 351 | - 编译器会将 `a.cpp` 中对于 `sum` 的调用和在 `sum.cpp` 中的定义**链接**起来。 352 | 353 | --- 354 | 355 | ## 分离式编译 356 | 357 | 更进一步:可以单独编译 `sum.cpp`,生成一个中间文件,再在编译 `a.cpp` 时链接它: 358 | 359 | `g++ -shared sum.cpp -o libsum.so` 360 | 361 | 这时生成了 `libsum.so`:**动态链接库文件**(有点儿像 Windows 上的 `.dll`,见过吗?) 362 | 363 | 编译 `a.cpp`:`g++ a.cpp -o a -Wl,-rpath . -L. -lsum` 364 | 365 | - `-Wl,-rpath .` 告知链接器在当前目录(`.`)下寻找 `.so` 文件 366 | - `-L. -lsum` 链接到 `libsum.so`。 367 | 368 | --- 369 | 370 | ## 分离式编译 371 | 372 | 更进一步:可以单独编译 `sum.cpp`,生成一个中间文件,再在编译 `a.cpp` 时链接它。 373 | 374 | 好处? 375 | 376 | --- 377 | 378 | ## 分离式编译 379 | 380 | 更进一步:可以单独编译 `sum.cpp`,生成一个中间文件,再在编译 `a.cpp` 时链接它。 381 | 382 | - 如果要修改 `sum.cpp` 中的具体实现,只需要重新编译 `sum.cpp`,不需要重新编译 `a.cpp`!(试一试) 383 | 384 | C++ 项目的编译可能非常慢:在实际开发中,一次编译耗时半个小时以上是很常见的。 385 | 386 | 合理分离各个部分的代码可以有效提升代码的复用性、开发效率等等。 387 | 388 | - 如果有 $N$ 个程序都使用了 `sum`,只需让它们都链接到 `libsum.so`,而不是在编译时都带着 `sum.cpp` 一起编译。 389 | 390 | --- 391 | 392 | ## 分离式编译 393 | 394 | 将类的定义(包括其成员的声明)写在头文件里,将成员函数的具体实现写在 `.cpp` 里 395 | 396 | - 循环依赖可以得到解决:在每个 `.cpp` 里 `#include` 所需要的头文件即可,可以保证使用一个类型时它已经定义完毕。 397 | - 编译出来的动态库可以重复使用,可以被多个文件链接。 398 | 399 | 如果你看看 C 标准库的头文件,会发现几乎找不到函数定义,全是声明。 400 | 401 | 但是 C++ 标准库文件里却有很多函数给出了定义,这是为什么? 402 | 403 | --- 404 | 405 | ## 分离式编译 406 | 407 | 将类的定义(包括其成员的声明)写在头文件里,将成员函数的具体实现写在 `.cpp` 里 408 | 409 | - 循环依赖可以得到解决:在每个 `.cpp` 里 `#include` 所需要的头文件即可,可以保证使用一个类型时它已经定义完毕。 410 | - 编译出来的动态库可以重复使用,可以被多个文件链接。 411 | 412 | 如果你看看 C 标准库的头文件,会发现几乎找不到函数定义,全是声明。 413 | 414 | 但是 C++ 标准库文件里却有很多函数给出了定义,这是为什么? 415 | 416 | - 在有 `template` 之前,一切都是很美好的... 417 | 418 | --- 419 | 420 | ## Inline(内联)函数 421 | 422 | 函数调用是有开销的... 423 | 424 | 能不能让编译器把 `a = std::min(b, c)` 变成 `a = b < c ? b : c`? 425 | 426 | - 将一些短的函数在调用点内联展开,但又不像 `#define` 那样出现语法/语义的问题 427 | 428 | ```cpp 429 | inline int max(int a, int b) { 430 | return a < b ? b : a; 431 | } 432 | ``` 433 | 434 | --- 435 | 436 | ## `inline` 437 | 438 | `inline` 关键字:**向编译器发出一个请求**,希望将这个函数在调用点内联展开。 439 | 440 | 编译器可以拒绝某些 `inline` 请求,也可能自动为某些没有显式 `inline` 的函数 inline。 441 | 442 | - 递归函数是难以 inline 的:编译器怎么知道它要展开多少层呢? 443 | - 太过复杂的函数的 inline 请求也会被拒绝。 444 | 445 | 拒绝并不会导致报错或 warning。 446 | 447 | 任何写在类内的函数都是隐式 `inline` 的:`static` members, non-`static` members 448 | 449 | --- 450 | 451 | ## `inline` 452 | 453 | 直接把所有函数都加上 `inline` 不好吗? 454 | 455 | --- 456 | 457 | ## `inline` 458 | 459 | 直接把所有函数都加上 `inline` 不好吗? 460 | 461 | 首先,我们只需关注那些编译器真的会 inline 的函数: 462 | 463 | - 将一个函数 inline 会使得它被复制到调用点。如果这个函数在 200 个不同的地方被调用,这个函数的代码就被复制了 200 份——**代码膨胀**。 464 | - 计算机执行指令时会将程序加载到内存里,因此代码膨胀就和你在内存里加载了过大的对象是一样的:cache miss 增多,内存换页等问题(CS110 见)。 465 | - 对于需要预先编译然后动态链接的函数,如果编译器不为它生成函数本体,会导致链接错误。 466 | 467 | 《Effective C++》条款 30 -------------------------------------------------------------------------------- /r12/demo/a.cpp: -------------------------------------------------------------------------------- 1 | void foo(int x) { 2 | auto y = x; 3 | x = y; 4 | } -------------------------------------------------------------------------------- /r12/demo/expr.hpp: -------------------------------------------------------------------------------- 1 | #ifndef EXPR_HPP 2 | #define EXPR_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class NodeBase { 10 | public: 11 | NodeBase() = default; 12 | virtual double eval() const = 0; 13 | virtual std::string rep() const = 0; 14 | virtual ~NodeBase() = default; 15 | }; 16 | 17 | class Expr { 18 | std::shared_ptr m_ptr; 19 | 20 | Expr(std::shared_ptr ptr) : m_ptr{ptr} {} 21 | 22 | public: 23 | Expr(double value); 24 | double eval() const { 25 | return m_ptr->eval(); 26 | } 27 | std::string rep() const { 28 | return m_ptr->rep(); 29 | } 30 | 31 | friend Expr operator-(const Expr &); 32 | friend Expr operator+(const Expr &); 33 | friend Expr operator+(const Expr &, const Expr &); 34 | friend Expr operator-(const Expr &, const Expr &); 35 | friend Expr operator*(const Expr &, const Expr &); 36 | friend Expr operator/(const Expr &, const Expr &); 37 | }; 38 | 39 | class Constant : public NodeBase { 40 | double m_value; 41 | 42 | double eval() const override { 43 | return m_value; 44 | } 45 | std::string rep() const override { 46 | return std::to_string(m_value); 47 | } 48 | 49 | public: 50 | Constant(double value) : m_value{value} {} 51 | }; 52 | 53 | Expr::Expr(double value) : m_ptr{std::make_shared(value)} {} 54 | 55 | class UnaryOperator : public NodeBase { 56 | char m_op; 57 | Expr m_operand; 58 | 59 | double eval() const override { 60 | return m_op == '+' ? m_operand.eval() : -m_operand.eval(); 61 | } 62 | std::string rep() const override { 63 | return m_op + ('(' + m_operand.rep() + ')'); 64 | } 65 | 66 | public: 67 | UnaryOperator(char op, const Expr &operand) : m_op{op}, m_operand{operand} {} 68 | }; 69 | 70 | Expr operator-(const Expr &operand) { 71 | return {std::make_shared('-', operand)}; 72 | } 73 | 74 | Expr operator+(const Expr &operand) { 75 | return {std::make_shared('+', operand)}; 76 | } 77 | 78 | class BinaryOperator : public NodeBase { 79 | char m_op; 80 | Expr m_left; 81 | Expr m_right; 82 | 83 | double eval() const override { 84 | auto left = m_left.eval(), right = m_right.eval(); 85 | if (m_op == '+') 86 | return left + right; 87 | else if (m_op == '-') 88 | return left - right; 89 | else if (m_op == '*') 90 | return left * right; 91 | else // m_op == '/' 92 | return left / right; 93 | } 94 | std::string rep() const override { 95 | return '(' + m_left.rep() + ')' + m_op + '(' + m_right.rep() + ')'; 96 | } 97 | 98 | public: 99 | BinaryOperator(char op, const Expr &left, const Expr &right) 100 | : m_op{op}, m_left{left}, m_right{right} {} 101 | }; 102 | 103 | Expr operator+(const Expr &left, const Expr &right) { 104 | return {std::make_shared('+', left, right)}; 105 | } 106 | 107 | Expr operator-(const Expr &left, const Expr &right) { 108 | return {std::make_shared('-', left, right)}; 109 | } 110 | 111 | Expr operator*(const Expr &left, const Expr &right) { 112 | return {std::make_shared('*', left, right)}; 113 | } 114 | 115 | Expr operator/(const Expr &left, const Expr &right) { 116 | return {std::make_shared('/', left, right)}; 117 | } 118 | 119 | #endif // EXPR_HPP -------------------------------------------------------------------------------- /r12/demo/use.cpp: -------------------------------------------------------------------------------- 1 | #include "expr.hpp" 2 | 3 | #include 4 | 5 | int main() { 6 | auto expr = -Expr(3.14) * (Expr(20) + Expr(42)); 7 | std::cout << expr.eval() << std::endl; 8 | std::cout << expr.rep() << std::endl; 9 | return 0; 10 | } -------------------------------------------------------------------------------- /r12/r12.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 12 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 继承、多态 15 | - 知识点梳理 16 | - 一个例子 17 | 18 | --- 19 | 20 | # 继承 (Inheritance)、多态 (Polymorphism) 21 | 22 | --- 23 | 24 | ## 继承 25 | 26 | - 一个子类对象里一定有一个完整的父类对象* 27 | - 禁止“坑爹”:继承不能破坏父类的封装性 28 | 29 | --- 30 | 31 | ## 继承 32 | 33 | - 一个子类对象里一定有一个完整的父类对象* 34 | 35 | - 父类的**所有成员**(除了构造函数和析构函数)都被继承下来,无论能否访问 36 | - 子类的构造函数必然先调用父类的构造函数来初始化父类的部分,然后再初始化自己的成员 37 | - 子类的析构函数在析构了自己的成员之后,必然调用父类的析构函数 38 | 39 | - 禁止“坑爹”:继承不能破坏父类的封装性 40 | 41 | - 子类不可能改变继承自父类成员的访问限制级别 42 | - 子类不能随意初始化父类成员,必须经过父类的构造函数 43 | 44 | - 先默认初始化后赋值是可以的 45 | 46 | \* 除非父类是空的,这时编译器会做“空基类优化 (Empty Base Optimization, EBO)”。 47 | 48 | --- 49 | 50 | ## 子类的构造函数 51 | 52 | 必然先调用父类的构造函数来初始化父类的部分,然后再初始化自己的成员 53 | 54 | ```cpp 55 | class DiscountedItem : public Item { 56 | public: 57 | DiscountedItem(const std::string &name, double price, 58 | int minQ, double discount) 59 | : Item(name, price), m_minQuantity(minQ), m_discount(discount) {} 60 | }; 61 | ``` 62 | 63 | 可以在初始值列表里调用父类的构造函数 64 | 65 | - 如果没有调用? 66 | 67 | --- 68 | 69 | ## 子类的构造函数 70 | 71 | 必然先调用父类的构造函数来初始化父类的部分,然后再初始化自己的成员 72 | 73 | ```cpp 74 | class DiscountedItem : public Item { 75 | public: 76 | DiscountedItem(const std::string &name, double price, 77 | int minQ, double discount) 78 | : Item(name, price), m_minQuantity(minQ), m_discount(discount) {} 79 | }; 80 | ``` 81 | 82 | 可以在初始值列表里调用父类的构造函数 83 | 84 | - 如果没有调用,则自动调用父类的默认构造函数 85 | 86 | - 如果父类不存在默认构造函数,则报错。 87 | 88 | 合成的默认构造函数的行为? 89 | 90 | --- 91 | 92 | ## 子类的构造函数 93 | 94 | 必然先调用父类的构造函数来初始化父类的部分,然后再初始化自己的成员 95 | 96 | ```cpp 97 | class DiscountedItem : public Item { 98 | public: 99 | DiscountedItem(const std::string &name, double price, 100 | int minQ, double discount) 101 | : Item(name, price), m_minQuantity(minQ), m_discount(discount) {} 102 | }; 103 | ``` 104 | 105 | 可以在初始值列表里调用父类的构造函数 106 | 107 | - 如果没有调用,则自动调用父类的默认构造函数 108 | 109 | - 如果父类不存在默认构造函数,则报错。 110 | 111 | 合成的默认构造函数:先调用父类的默认构造函数,再 ...... 112 | 113 | --- 114 | 115 | ## 子类的析构函数 116 | 117 | 析构函数在执行完函数体之后: 118 | 119 | - 先按成员的声明顺序倒序销毁所有成员。对于含有 non-trivial destructor 的成员,调用其 destructor。 120 | - 然后调用父类的析构函数销毁父类的部分。 121 | 122 | --- 123 | 124 | ## 子类的拷贝控制 125 | 126 | 自定义:不要忘记拷贝/移动父类的部分 127 | 128 | ```cpp 129 | class Derived : public Base { 130 | public: 131 | Derived(const Derived &other) : Base(other), /* ... */ { /* ... */ } 132 | Derived &operator=(const Derived &other) { 133 | Base::operator=(other); 134 | // ... 135 | return *this; 136 | } 137 | }; 138 | ``` 139 | 140 | 合成的拷贝控制成员(不算析构)的行为? 141 | 142 | --- 143 | 144 | ## 子类的拷贝控制 145 | 146 | 合成的拷贝控制成员(不算析构):先父类,后子类自己的成员。 147 | 148 | - 如果这个过程中调用了任何不存在/不可访问的函数,则合成为 implicitly deleted 149 | 150 | --- 151 | 152 | ## 动态绑定 153 | 154 | 向上转型: 155 | 156 | - 一个 `Base *` 可以指向一个 `Derived` 类型的对象 157 | - `Derived *` 可以向 `Base *` 类型转换 158 | - 一个 `Base &` 可以绑定到一个 `Derived` 类型的对象 159 | - 一个 `std::shared/unique_ptr` 可以指向一个 `Derived` 类型的对象 160 | - `std::shared/unique_ptr` 可以向 `std::shared/unique_ptr` 类型转换 161 | 162 | --- 163 | 164 | ## 虚函数 165 | 166 | 继承父类的某个函数 `foo` 时,我们可能希望在子类提供一个新版本(override)。 167 | 168 | 我们希望在一个 `Base *`, `Base &` 或 `std::shared/unique_ptr` 上调用 `foo` 时,可以根据**动态类型**来选择正确的版本,而不是根据静态类型调用 `Base::foo`。 169 | 170 | 在子类里 override 一个虚函数时,函数名、参数列表、`const`ness 必须和父类的那个函数**完全相同**。 171 | 172 | - 返回值类型必须**完全相同**或者随类型*协变* (covariant)。 173 | 174 | 加上 `override` 关键字:让编译器帮你检查它是否真的构成了 override 175 | 176 | --- 177 | 178 | ## 虚函数 179 | 180 | **除了 override,不要以其它任何方式定义和父类中某个成员同名的成员。** 181 | 182 | - 阅读以下章节,你会看到违反这条规则带来的后果。 183 | 184 | 《Effective C++》条款 33:避免遮掩继承而来的名称 185 | 186 | 《Effective C++》条款 36:绝不重新定义继承而来的 non-`virtual` 函数 187 | 188 | 《Effective C++》条款 37:绝不重新定义继承而来的缺省参数值 189 | 190 | --- 191 | 192 | ## 纯虚函数 193 | 194 | 通过将一个函数声明为 `=0`,它就是一个**纯虚函数** (pure virtual function)。 195 | 196 | 一个类如果有某个成员函数是纯虚函数,它就是一个**抽象类**。 197 | 198 | - 不能定义抽象类的对象,不能调用无定义的纯虚函数*。 199 | 200 | \* 事实上一个纯虚函数仍然可以拥有一份定义,阅读《Effective C++》条款 34(必读,HW7 的客观题会涉及此条款的内容)。 201 | 202 | --- 203 | 204 | ## 纯虚函数 205 | 206 | 纯虚函数通常用来定义**接口**:这个函数在所有子类里都应该有一份自己的实现。 207 | 208 | 如果一个子类继承了某个纯虚函数而没有 override 它,这个成员函数就仍然是纯虚的,这个类仍然是抽象类,无法被实例化。 209 | 210 | --- 211 | 212 | ## 运行时类型识别 (RTTI) 213 | 214 | `dynamic_cast` 可以做到“向下转型”: 215 | 216 | - 它会在运行时检测这个转型是否能成功 217 | - 如果不能成功,`dynamic_cast` 返回空指针,`dynamic_cast` 抛出 `std::bad_cast` 异常。 218 | - **非常非常慢**,你几乎总是应该先考虑用一组虚函数来完成你想要做的事。 219 | 220 | `typeid(x)` 可以获取表达式 `x` (忽略顶层 `const` 和引用后)的动态类型信息 221 | 222 | - 通常用 `if (typeid(*ptr) == typeid(A))` 来判断这个动态类型是否是 `A`。 223 | 224 | https://quick-bench.com/q/E0LS3gJgAHlQK0Em_6XzkRzEjnE 225 | 226 | 根据经验,如果你需要获取某个对象的动态类型,通常意味着设计上的缺陷,你应当修改设计而不是硬着头皮做 RTTI。 227 | 228 | --- 229 | 230 | ## 设计 231 | 232 | `public` 继承建模出“is-a”关系:A discounted item **is an** item. 233 | 234 | 但有些时候英语上的“is-a”具有欺骗性: 235 | 236 | - Birds can fly. A penguin is a bird. 237 | - A square is a rectangle. 但矩形的长宽可以随意更改,而正方形不可以。 238 | 239 | 阅读《Effective C++》条款 32(必读,HW7 的客观题会涉及此条款的内容)。 240 | 241 | --- 242 | 243 | ## 继承的访问权限 244 | 245 | 这个 `public` 是什么意思? 246 | 247 | ```cpp 248 | class DiscountedItem : public Item {}; 249 | ``` 250 | 251 | --- 252 | 253 | ## 继承的访问权限 254 | 255 | ```cpp 256 | class DiscountedItem : public Item {}; 257 | ``` 258 | 259 | 继承的访问权限:**这个继承关系(“父子关系”)是否对外公开**。 260 | 261 | 如果采用 `private` 继承,则在外人眼里他们不是父子,任何依赖于这一父子关系的行为都将失败(有哪些?)。 262 | 263 | --- 264 | 265 | ## 继承的访问权限 266 | 267 | ```cpp 268 | class DiscountedItem : public Item {}; 269 | ``` 270 | 271 | 继承的访问权限:**这个继承关系(“父子关系”)是否对外公开**。 272 | 273 | 如果采用 `private` 继承,则在外人眼里他们不是父子,任何依赖于这一父子关系的行为都将失败。 274 | 275 | - 访问继承而来的成员(本质上也是向上转型) 276 | - 向上转型(包括动态绑定等等)、向下转型 277 | 278 | `private` 继承:建模“is-implemented-in-terms-of”。阅读《Effective C++》条款 38、39。 279 | 280 | --- 281 | 282 | ## 继承的访问权限 283 | 284 | ```cpp 285 | struct A : B {}; // public inheritance 286 | class C : B {}; // private inheritance 287 | ``` 288 | 289 | `struct` 和 `class` 仅有两个区别: 290 | 291 | - 默认的成员访问权限是 `public`/`private`。 292 | - 默认的继承访问权限是 `public`/`private`。 293 | 294 | --- 295 | 296 | ## 一个 OOP 的例子 297 | 298 | 抽象语法树的各种结点 -------------------------------------------------------------------------------- /r13/check_result.csv: -------------------------------------------------------------------------------- 1 | liuxh2022,100 2 | jiangyh1,97 3 | zhangrm2022,100 4 | yanglp2022,95 5 | guoyw2022,97 6 | songyi2022,100 7 | caoyl2022,100 8 | hezhj,100 9 | yangjun2,95 10 | yangrk2022,100 11 | hugf2022,95 12 | quchang2022,97 13 | linjl2022,100 14 | liqr2022,95 15 | panqh,97 16 | chenjl2022,100 17 | suyl1,100 18 | yangge,100 19 | yangqy12022,100 20 | lirf2022,90 21 | taopx2022,100 22 | xuchx1,100 23 | zhuych12022,100 24 | sunjx2022,100 25 | zhangzhk2022,100 26 | zhugx2022,90 27 | xuewh1,90 28 | zhangych12022,100 29 | caolei2022,100 30 | jiaoyy2022,100 31 | lvpz,100 32 | lijy5,100 33 | zhouzq1,100 34 | buzhy2022,100 35 | songyh2022,76.8 -------------------------------------------------------------------------------- /r13/r13.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 13 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 重载运算符:总结、补充 15 | - STL 16 | 17 | --- 18 | 19 | # 重载运算符 (Operator overloading) 20 | 21 | --- 22 | 23 | ## 总结:技术 24 | 25 | 运算符重载的方式是定义一个名为 `operator@` 的函数: 26 | 27 | - 各个运算对象从左向右依次作为这个函数的参数。 28 | - 如果是成员函数,则最左侧的运算对象绑定到 `*this`。 29 | 30 | 不能发明新的运算符。不能为内置类型定义运算符。 31 | 32 | 不能被重载: 33 | 34 | - `?:`, `.`, `::` 等,具有特别意义的运算符 35 | 36 | 不建议被重载:(为什么?) 37 | 38 | - `&&`, `||`, `,`, `&` (address-of) 39 | 40 | --- 41 | 42 | ## 总结:技术 43 | 44 | 运算符重载的方式是定义一个名为 `operator@` 的函数: 45 | 46 | - 各个运算对象从左向右依次作为这个函数的参数。 47 | - 如果是成员函数,则最左侧的运算对象绑定到 `*this`。 48 | 49 | 不能发明新的运算符。不能为内置类型定义运算符。 50 | 51 | 不能被重载: 52 | 53 | - `?:`, `.`, `::` 等,具有特别意义的运算符 54 | - `?:` 无法被重载,因为它有一个运算对象不被求值,这一点用函数传参不可能做到 55 | 56 | 不建议被重载:(为什么?) 57 | 58 | - `&&`, `||`, `,`, `&` (一元取地址运算符) 59 | - `&&` 和 `||` 的短路求值会失效。`,` 和 `&` 本来就有特殊含义 60 | 61 | --- 62 | 63 | ## 总结:技术 64 | 65 | 重载的行为需要和内置的行为保持某种程度上的一致,除非你有很好的理由不这么做 66 | 67 | - `++i` 返回 `i` 的引用,`i++` 返回递增前的 `i` 的一份拷贝。 68 | - 赋值、复合赋值都返回左侧运算对象的引用。 69 | - 解引用 `*p` 通常返回左值引用 70 | - 比较运算符需要符合逻辑:`a == b` 意味着 `b == a`,`a != b` 意味着 `!(a == b)`。 71 | 72 | C++20 的比较运算符的相关规则发生了巨大的变化: 73 | 74 | - 允许用 `=default` 让编译器合成 75 | - 新增了 `operator<=>` 用来定义数学上对应的**序关系**(partial ordering, strong ordering, weak ordering) 76 | - 定义了 `operator==` 和 `operator<=>` 后编译器会合成六个关系运算符。 77 | 78 | --- 79 | 80 | ## 总结:技术 81 | 82 | 通常是成员:`++`, `--`, `*` (解引用), `->`, `=` (赋值,必须是成员), 各种复合赋值运算符(`+=`, `-=`, ...) 83 | 84 | 成员或非成员皆可:比较运算符 `<`, `<=`, `>`, `>=`, `==`, `!=`,算术运算符等 85 | 86 | **如果需要左侧运算对象也能隐式转换,则它不能是成员。** 87 | 88 | - `r == 1` 被视为 `r.operator==(1)`,即 `r.operator==(Rational(1))` 89 | - 但 `1 == r` 无法被视为 `1.operator==(r)`。 90 | 91 | --- 92 | 93 | ## 不要漏 `const`! 94 | 95 | ```cpp 96 | class Rational { 97 | public: 98 | bool operator==(const Rational &) const; 99 | }; 100 | bool operator!=(const Rational &, const Rational &); 101 | ``` 102 | 103 | 非成员有两个 `const`,成员也必然有两个 `const`,只是其中一个变成了这个成员函数的 qualifier。 104 | 105 | --- 106 | 107 | ## 特殊的运算符:后置递增 `i++` 108 | 109 | ```cpp 110 | class Rational { 111 | public: 112 | Rational &operator++(); 113 | // 90% 的后置递增运算符都应该这样写 114 | Rational operator++(int) { 115 | auto tmp = *this; // 拷贝原来的对象 116 | ++*this; // 真正的“递增”由前置版本完成 117 | return tmp; // 返回递增前的拷贝 118 | } 119 | }; 120 | ``` 121 | 122 | - `int` 参数:**仅仅是为了区分前置版本和后置版本**,没有实际意义,也不需要用到,自然也就没有名字。 123 | - `++i` 被视为 `i.operator++()`,`i++` 被视为 `i.operator++(0)`。 124 | 125 | --- 126 | 127 | ## 特殊的运算符:`->` 128 | 129 | ```cpp 130 | class SharedPtr { 131 | public: 132 | Object &operator*() const; 133 | Object *operator->() const { 134 | return std::addressof(this->operator*()); 135 | } 136 | }; 137 | ``` 138 | 139 | - 为了让 `p->mem` 和 `(*p).mem` 等价,`operator->` 几乎总是应该这样定义。 140 | - 注意在本例中 `operator*` 和 `operator->` 都是 `const`:它们都允许在 `const SharedPtr` 上调用。 141 | 142 | --- 143 | 144 | ## 避免重复:比较运算符 145 | 146 | 定义 `operator==` 和 `operator<`,让剩下四个依赖于它们。 147 | 148 | ```cpp 149 | bool operator!=(const Rational &lhs, const Rational &rhs) { 150 | return !(lhs == rhs); 151 | } 152 | bool operator>(const Rational &lhs, const Rational &rhs) { 153 | return rhs < lhs; 154 | } 155 | bool operator<=(const Rational &lhs, const Rational &rhs) { 156 | return !(lhs > rhs); // 你只会 lhs < rhs || lhs == rhs ? 157 | } 158 | bool operator>=(const Rational &lhs, const Rational &rhs) { 159 | return !(lhs < rhs); 160 | } 161 | ``` 162 | 163 | --- 164 | 165 | ## 避免重复:算术运算符 166 | 167 | 定义一元负号 `-` 和 `+=`,那么 `+`, `-`, `-=` 都可以用它们实现。 168 | 169 | ```cpp 170 | class Rational { 171 | public: 172 | Rational &operator-=(const Rational &rhs) { 173 | return *this += -rhs; 174 | } 175 | }; 176 | Rational operator+(const Rational &lhs, const Rational &rhs) { 177 | return Rational(lhs) += rhs; 178 | } 179 | Rational operator-(const Rational &lhs, const Rational &rhs) { 180 | return Rational(lhs) -= rhs; 181 | } 182 | ``` 183 | 184 | --- 185 | 186 | ## 输入、输出运算符:`operator<<` 和 `operator>>` 187 | 188 | 首先搞清楚:输入流(例如 `std::cin`)的类型是 `std::istream`,输出流(例如 `std::cout`)的类型是 `std::ostream`。 189 | 190 | 这两个运算符应该是成员还是非成员? 191 | 192 | --- 193 | 194 | ## 输入、输出运算符 195 | 196 | `operator<<` 和 `operator>>` 197 | 198 | 首先搞清楚:输入流(例如 `std::cin`)的类型是 `std::istream`,输出流(例如 `std::cout`)的类型是 `std::ostream`。 199 | 200 | 这两个运算符只能是非成员:你无法给 `std::istream` 和 `std:ostream` 添加成员。 201 | 202 | `std::istream` 和 `std::ostream` 都不能拷贝,必须按引用传递,而且不能是常量引用。 203 | 204 | ```cpp 205 | std::istream &operator>>(std::istream &, Rational &r); 206 | std::ostream &operator<<(std::ostream &, const Rational &r); 207 | ``` 208 | 209 | - 为了支持连续的输入输出 `std::cin >> a >> b >> c`,这两个运算符需要把左侧运算对象返回出来。 210 | 211 | --- 212 | 213 | ## 输入、输出运算符 214 | 215 | 输入运算符需要考虑输入错误的情况:见客观题 18。 216 | 217 | ```cpp 218 | struct Vec3 { 219 | double x_, y_, z_; 220 | double l2_norm_; 221 | }; 222 | std::istream &operator>>(std::istream &is, Vec3 &v) { 223 | is >> v.x_ >> v.y_ >> v.z_; 224 | if (!is) // 如果输入发生错误,要将对象置于有效的状态。 225 | v.x_ = v.y_ = v.z_ = 0; 226 | v.l2_norm_ = std::sqrt(v.x_ * v.x_ + v.y_ * v.y_ + v.z_ * v.z_); 227 | return is; 228 | } 229 | ``` 230 | 231 | --- 232 | 233 | ## 输入、输出运算符 234 | 235 | 不能直接将 `is`、`os` 替换为 `std::cin` 和 `std::cout`:除它们之外还有其它的流对象 236 | 237 | ```cpp 238 | std::ifstream file("infile.txt"); 239 | int x, y, z; 240 | file >> x >> y >> z; 241 | ``` 242 | 243 | 由于 `std::ifstream` 继承自 `std::istream`,它可以被 `std::istream &` 绑定 244 | 245 | 后面课程会讲 246 | 247 | --- 248 | 249 | ## 避免重载运算符的滥用 250 | 251 | 函数的名称可以透露很多信息,而运算符不行: 252 | 253 | - `a * b` 和 `dot_product(a, b)` 254 | - `a < b` 和 `compare_by_id(a, b)` 255 | - `a + b` 和 `concat(a, b)` 256 | 257 | 避免滥用重载运算符:除非这个运算符有唯一明确合理的定义,否则不要定义它。 258 | 259 | - 为什么 `std::string` 可以用 `+` 连接,而 `std::vector` 不行? 260 | - MATLAB 的 `a * b` 和 `a .* b` 的区别 261 | 262 | `operator+` 应该加而不是减,`operator<` 应该是“小于”而非“大于”。 263 | 264 | --- 265 | 266 | ## 函数调用运算符 `operator()` 267 | 268 | 这块内容我放在 STL 部分了,但仍然属于重载运算符的板块。 269 | 270 | ```cpp 271 | struct Adder { 272 | int operator()(int a, int b) const { 273 | return a + b; 274 | } 275 | }; 276 | std::cout << Adder{}(2, 3) << std::endl; // 5 277 | ``` 278 | 279 | - `Adder{}` 创建了一个 `Adder` 类型的对象 280 | - `Adder{}(2, 3)` 相当于 `Adder{}.operator()(2, 3)` 281 | 282 | 一般地,`f(arg1, arg2, ...)` 被视为 `f.operator()(arg1, arg2, ...)`。 283 | 284 | --- 285 | 286 | ## 类型转换运算符 287 | 288 | 除了接受单个参数的构造函数,我们还有另一种方式自定义类型转换: 289 | 290 | ```cpp 291 | struct Rational { 292 | int n, d; 293 | operator double() const { 294 | return static_cast(n) / d; 295 | } 296 | }; 297 | 298 | Rational r{3, 2}; 299 | double d = r; // 1.5 300 | ``` 301 | 302 | --- 303 | 304 | ## 类型转换运算符 305 | 306 | ```cpp 307 | struct Rational { 308 | int n, d; 309 | operator double() const { 310 | return static_cast(n) / d; 311 | } 312 | }; 313 | Rational r{3, 2}; 314 | double d = r; // 1.5 315 | ``` 316 | 317 | - 函数名是 `operator Type` 318 | - 不接受参数,不写返回值类型(因为返回值类型就是 `Type`) 319 | - 通常是 `const`:类型转换不改变这个对象本身。但也可以禁止在 `const` 对象上发生这种类型转换。 320 | 321 | --- 322 | 323 | ## 类型转换运算符 324 | 325 | `std::istream` 对象可以放在条件部分,来判断这个输入流正不正常: 326 | 327 | ```cpp 328 | std::cin >> a >> b; 329 | if (!std::cin) 330 | handle_input_failure(); 331 | ``` 332 | 333 | 所以标准库定义了 `std::istream` 向 `bool` 的类型转换,对吗? 334 | 335 | ```cpp 336 | class istream { 337 | bool fail, bad; 338 | public: 339 | operator bool() const { 340 | return !fail && !bad; 341 | } 342 | }; 343 | ``` 344 | 345 | --- 346 | 347 | ## 类型转换运算符 348 | 349 | 标准库定义了 `std::istream` 向 `bool` 的类型转换,对吗? 350 | 351 | ```cpp 352 | class istream { 353 | bool fail, bad; 354 | public: 355 | operator bool() const { return !fail && !bad; } 356 | }; 357 | istream cin; 358 | ``` 359 | 360 | 下面的代码居然也乐呵呵地编译了!为什么? 361 | 362 | ```cpp 363 | int ival; 364 | cin << ival; // 写反了! 365 | ``` 366 | 367 | --- 368 | 369 | ## 类型转换运算符 370 | 371 | 标准库定义了 `std::istream` 向 `bool` 的类型转换,对吗? 372 | 373 | ```cpp 374 | class istream { 375 | bool fail, bad; 376 | public: 377 | operator bool() const { return !fail && !bad; } 378 | }; 379 | istream cin; 380 | ``` 381 | 382 | 下面的代码居然也乐呵呵地编译了!为什么? 383 | 384 | ```cpp 385 | int ival; 386 | cin << ival; // cin 隐式转换成 bool 类型,又提升成 int 387 | // 这个 << 其实是左移! 388 | ``` 389 | 390 | --- 391 | 392 | ## 类型转换运算符 393 | 394 | 为了避免 `std::cin << ival` 通过编译,在 C++11 以前标准库定义的是向 `void *` 的类型转换:在输入流正常的时候返回一个非空指针,不正常的时候返回空指针。 395 | 396 | (since C++11): `explicit` 类型转换运算符 397 | 398 | ```cpp 399 | struct Rational { 400 | int n, d; 401 | explicit operator double() const { 402 | return static_cast(n) / d; 403 | } 404 | }; 405 | Rational r{3, 2}; 406 | double d = r; // 错误:隐式转换 407 | double d2 = static_cast(r); // 正确 408 | ``` 409 | 410 | --- 411 | 412 | ## 类型转换运算符 413 | 414 | 为了避免 `std::cin << ival` 通过编译,在 C++11 以前标准库定义的是向 `void *` 的类型转换:在输入流正常的时候返回一个非空指针,不正常的时候返回空指针。 415 | 416 | (since C++11): `explicit` 类型转换运算符 417 | 418 | ```cpp 419 | class istream { 420 | bool fail, bad; 421 | public: 422 | explicit operator bool() const { return !fail && !bad; } 423 | }; 424 | istream cin; 425 | ``` 426 | 427 | 从 `istream` 向 `bool` 的类型转换必须显式发生。 428 | 429 | 那 `if (cin)` 还能用吗? 430 | 431 | --- 432 | 433 | ## Contextual conversions 434 | 435 | 下列情况中,`e` 向 `bool` 的类型转换属于 contextual conversion,允许使用 explicit 类型转换运算符: 436 | 437 | - `if (e)`, `while (e)`, `for (xxx; e; xxx)` 438 | - `!e`, `e && e`, `e || e`, `e ? a : b` 439 | - (你们没学过的)`static_assert(e)`, `noexcept(e)`, (since C++20) `explicit(e)` 440 | 441 | 因此 `if (cin)` 也就没问题了。 442 | 443 | --- 444 | 445 | ## 避免类型转换运算符的滥用 446 | 447 | ```cpp 448 | struct Rational { 449 | int n, d; 450 | operator double() const { 451 | return static_cast(n) / d; 452 | } 453 | operator std::string() const { 454 | return std::to_string(n) + '/' + std::to_string(d); 455 | } 456 | }; 457 | 458 | Rational r{3, 2}; 459 | std::cout << r << std::endl; // 1.5 还是 3/2 ? 460 | ``` 461 | 462 | --- 463 | 464 | ## 避免类型转换运算符的滥用 465 | 466 | 更好的设计:定义成普通的函数 467 | 468 | ```cpp 469 | struct Rational { 470 | int n, d; 471 | auto to_double() const { 472 | return static_cast(n) / d; 473 | } 474 | auto to_string() const { 475 | return std::to_string(n) + '/' + std::to_string(d); 476 | } 477 | }; 478 | ``` 479 | 480 | 一般来说,只为某些特殊的行为定义类型转换,尤其是不要轻易定义向内置类型的类型转换! 481 | 482 | --- 483 | 484 | ## 类型转换引发的二义性 485 | 486 | ```cpp 487 | struct A { 488 | A(const B &); 489 | }; 490 | struct B { 491 | operator A() const; 492 | }; 493 | 494 | B b{}; 495 | A a = b; // 到底是调用了 `A::A(const B &)` 还是 `B::operator A() const`? 496 | ``` 497 | 498 | --- 499 | 500 | ## 类型转换引发的二义性 501 | 502 | 当类型转换遇上重载决议,情况就彻底乱套了... 503 | 504 | ```cpp 505 | struct Rational { 506 | operator double() const; 507 | }; 508 | struct X { 509 | X(const Rational &); 510 | }; 511 | void foo(int); 512 | void foo(X); 513 | Rational r{3, 2}; 514 | foo(r); // 到底是先转成 double 然后转成 int 匹配 foo(int) 515 | // 还是直接转成 X 匹配 foo(X)? 516 | ``` 517 | 518 | **避免这样的情况发生!** 不要滥用类型转换运算符。 519 | 520 | --- 521 | 522 | ## 自定义字面值后缀 523 | 524 | 拜托,C++ 这么机械朋克的语言超酷的好吗? 525 | 526 | ```cpp 527 | inline Expr operator""_ex(long double x) { 528 | return {static_cast(x)}; 529 | } 530 | auto e = 3.0_ex + 2.0_ex; // 相当于 Expr(3.0) + Expr(2.0) 531 | inline std::size_t operator""_zu(long long unsigned x) { 532 | return x; 533 | } 534 | for (auto i = 0_zu; i != v.size(); ++i) // 终于可以用 auto 定义 std::size_t 变量了 535 | // ... 536 | ``` 537 | 538 | 标准库定义的字面值后缀: 539 | 540 | ```cpp 541 | using namespace std::string_literals; 542 | auto s = "hello"s; // s 是一个 std::string 对象 543 | ``` 544 | 545 | --- 546 | 547 | # STL 548 | 549 | --- 550 | 551 | ## 特别补充一个 `std::pair` 552 | 553 | 定义在 `` 中 554 | 555 | 一个快速、随意的数据结构 556 | 557 | ```cpp 558 | std::pair p1{a, b}; // 用圆括号也可以 559 | auto p2 = std::make_pair(a, b); // 等价的写法 560 | std::pair p3{a, b}; // 编译器根据 a 和 b 的类型来推导模板参数 561 | // 这里用圆括号也可以,但 {} 更 modern 562 | ``` 563 | 564 | C++17 有了 CTAD,几乎不再需要 `std::make_pair` 了。 565 | 566 | --- 567 | 568 | ## `std::pair` 569 | 570 | 返回两个值:装进 `pair` 里就行 571 | 572 | ```cpp 573 | std::pair foo() { 574 | // ... 575 | return {42, "hello"}; 576 | } 577 | ``` 578 | 579 | 访问 `pair` 中的元素:`p.first`, `p.second` 580 | 581 | ```cpp 582 | auto p = foo(); 583 | std::cout << foo.first << ", " << foo.second << std::endl; 584 | ``` 585 | 586 | --- 587 | 588 | ## `std::pair` 589 | 590 | 访问 `pair` 中的元素:`p.first`, `p.second` 591 | 592 | ```cpp 593 | auto p = foo(); 594 | std::cout << foo.first << ", " << foo.second << std::endl; 595 | ``` 596 | 597 | C++17 structured binding (结构化绑定): 598 | 599 | ```cpp 600 | auto [ival, sval] = foo(); 601 | std::cout << ival << ", " << sval << std::endl; 602 | ``` 603 | 604 | 非常像 Python 605 | 606 | --- 607 | 608 | ## `std::pair` 609 | 610 | `std::pair` 有比较运算符:按照 `.first` 为第一关键字、`.second` 为第二关键字比较。 611 | 612 | 不要滥用 `std::pair`!不要用它来代替一些具有实际意义的类。 613 | 614 | ```cpp 615 | using Complex = std::pair; // bad idea! 616 | using Student = std::pair; // bad idea! 617 | ``` -------------------------------------------------------------------------------- /r14/img/deque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r14/img/deque.png -------------------------------------------------------------------------------- /r14/img/forward_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r14/img/forward_list.png -------------------------------------------------------------------------------- /r14/img/iostream_inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r14/img/iostream_inheritance.png -------------------------------------------------------------------------------- /r14/img/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r14/img/list.png -------------------------------------------------------------------------------- /r14/img/range-begin-end.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 28 | 34 | 35 | 42 | 48 | 49 | 51 | 55 | 59 | 60 | 62 | 66 | 70 | 71 | 80 | 89 | 91 | 95 | 99 | 100 | 109 | 111 | 115 | 119 | 120 | 127 | 133 | 134 | 143 | 144 | 172 | 174 | 175 | 177 | image/svg+xml 178 | 180 | 181 | 182 | 183 | 184 | 189 | 191 | 203 | 213 | 222 | 231 | 240 | 249 | 258 | 267 | 276 | 285 | 294 | 303 | 312 | 321 | 330 | 339 | 351 | 352 | Past-the-last element 367 | 374 | 377 | 385 | begin 397 | 398 | 401 | 409 | end 420 | 421 | 428 | 434 | 435 | 436 | -------------------------------------------------------------------------------- /r14/img/vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r14/img/vector.png -------------------------------------------------------------------------------- /r14/r14.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 14 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 重载运算符:一些别的东西 15 | - IOStream 16 | - STL 总结、补充 17 | 18 | --- 19 | 20 | ## 思路打开 21 | 22 | `std::string` 用 `operator+` 表示连接,很直观的设计。 23 | 24 | 但标准库 IOStream 用 `operator<<` 和 `operator>>` 来表示输入输出,甚至设计出连续的用法,的确是一种很巧妙的设计。 25 | 26 | --- 27 | 28 | ## 思路打开 29 | 30 | `std::string` 用 `operator+` 表示连接,很直观的设计。 31 | 32 | 但标准库 IOStream 用 `operator<<` 和 `operator>>` 来表示输入输出,甚至设计出连续的用法,的确是一种很巧妙的设计。 33 | 34 | 难道 `+`, `-`, `*`, `/` 就只能用来完成加减乘除? 35 | 36 | --- 37 | 38 | ## `filesystem`:`operator/`, `operator/=` 39 | 40 | `/home/gkxx/Courses/CS100/tmp` 41 | 42 | ```cpp 43 | #include 44 | #include 45 | int main() { 46 | namespace fs = std::filesystem; 47 | fs::path p = "/home/gkxx"; 48 | p /= "Courses"; 49 | p = p / "CS100" / "tmp"; 50 | for (const auto &dir_entry : fs::directory_iterator(p)) 51 | std::cout << dir_entry.path() << std::endl; 52 | return 0; 53 | } 54 | ``` 55 | 56 | --- 57 | 58 | ## `chrono`: `operator/` 和 literal suffix 配合使用 (C++20) 59 | 60 | ```cpp 61 | #include 62 | #include 63 | int main() { 64 | using namespace std::chrono; 65 | auto date = 2021y/January/23d; 66 | std::cout << date << std::endl; 67 | return 0; 68 | } 69 | ``` 70 | 71 | `2021y` 是一个 `std::chrono::year`,`std::chrono::January` 是一个 `std::chrono::month`,`23d` 是一个 `std::chrono::day`。 72 | 73 | --- 74 | 75 | ## `std::ranges`: `operator|` (C++20/23) 76 | 77 | ```cpp 78 | #include 79 | #include 80 | #include 81 | int main() { 82 | auto vec = std::views::iota(1, 5) // {1, 2, 3, 4} 83 | | std::views::transform([](auto x) { return x * 2; }) 84 | | std::ranges::to(); 85 | // vec is a std::vector 86 | for (auto x : vec) 87 | std::cout << x << ' '; // 输出 2 4 6 8 88 | std::cout << std::endl; 89 | return 0; 90 | } 91 | ``` 92 | 93 | [CppCon2022: Functional Composable Operations with Unix-Style Pipes](https://www.bilibili.com/video/BV1FB4y1H7VS/?p=7) 94 | 95 | --- 96 | 97 | ## `operator[]` 为什么一定是下标? 98 | 99 | 人家 Python Numpy 可以这样写 100 | 101 | ```python 102 | a = np.array([-1, 2, -6, -3, 5]) 103 | b = (a > 0) # [False, True, False, False, True] 104 | c = a[a > 0] # [2, 5] 105 | ``` 106 | 107 | - 比较运算符返回一个 `bool` 数组 108 | - 比较运算符为什么一定是比较? 109 | - `operator[]` 接受一个 `bool` 数组,返回一个新的数组 110 | 111 | --- 112 | 113 | # IOStream 库 114 | 115 | 注:以下所有来自标准库的名字可能会省去 `std::`,但不代表我在代码里也会省去。 116 | 117 | --- 118 | 119 | 120 | 121 | 122 | 123 | --- 124 | 125 | ## IOStream 126 | 127 | `ifstream`, `ofstream`: 文件输入流,文件输出流,定义在 `` 里 128 | 129 | 一般的文件流(默认情况下既可输入又可输出):`fstream` 130 | 131 | 从文件读入: 132 | 133 | ```cpp 134 | std::ifstream ifs("path/to/myfile.ext"); 135 | int ival; std::string s; 136 | ifs >> ival >> s; 137 | ``` 138 | 139 | 向文件写入: 140 | 141 | ```cpp 142 | std::ofstream ofs("path/to/myfile.ext"); 143 | ofs << "hello world" << std::endl; 144 | ``` 145 | 146 | --- 147 | 148 | ## fstream 149 | 150 | 打开文件的模式:`app`, `binary`, `in`, `out`, `trunc`, `ate`, (since C++23) `noreplace` 151 | 152 | https://en.cppreference.com/w/cpp/io/basic_ifstream#Member_types_and_constants 153 | 154 | 通过 `|` 可以将多个模式并起来,例如 `std::ios::out | std::ios::binary`。 155 | 156 | 默认情况下:`ifstream` 使用 `in`,`ofstream` 使用 `out`,`fstream` 使用 `in | out`。 157 | 158 | - 假如你传入的打开模式是 `mode`,`ifstream` 会以 `mode | in` 打开,`ofstream` 会以 `mode | out` 打开。 159 | - `out` 会清空这个文件原来的内容,而 `app` 是将输出追加到原来的内容的后面。 160 | 161 | ```cpp 162 | std::ofstream file("myfile.txt", std::ios::app); 163 | file << "hello world" << std::endl; 164 | ``` 165 | 166 | --- 167 | 168 | ## fstream 169 | 170 | 可以在构造的时候打开文件,并同时指定打开模式;也可以在稍后用 `open` 打开一个文件,或用 `close` 关闭打开的文件: 171 | 172 | ```cpp 173 | std::ifstream file("myfile.txt", std::ios::binary); 174 | if (!file.is_open()) { 175 | // myfile.txt is not found or cannot be opened. 176 | // handle this error. 177 | file.open("myfile.txt", std::ios::binary); 178 | } 179 | // do something with the file 180 | file.close(); 181 | file.open("another_file.txt"); 182 | // ... 183 | ``` 184 | 185 | `file` 的析构函数会关闭打开的文件。 186 | 187 | --- 188 | 189 | ## stringstream 190 | 191 | 将一个 `std::string` 作为输入内容或输出结果 192 | 193 | `istringstream`, `ostringstream`, `stringstream`:定义在 `` 里。 194 | 195 | ```cpp 196 | std::ostringstream oss; 197 | oss << 42 << 3.14 << "hello world"; 198 | std::string str = oss.str(); // "423.14hello world" 199 | ``` 200 | 201 | --- 202 | 203 | ## C++20 osyncstream 204 | 205 | ```cpp 206 | #include 207 | #include 208 | #include 209 | #include 210 | void thread_func(int id) { 211 | std::osyncstream(std::cout) << "hello world " << "from " << id << std::endl; 212 | // 如果直接用 std::cout,输出的内容就会乱作一团。 213 | } 214 | int main() { 215 | std::vector th; th.reserve(10); 216 | for (auto i = 0; i != 10; ++i) 217 | th.emplace_back(thread_func, i); 218 | for (auto &t : th) t.join(); 219 | return 0; 220 | } 221 | ``` 222 | 223 | --- 224 | 225 | # STL 总结、补充 226 | 227 | 注:以下所有来自标准库的名字可能会省去 `std::`,但不代表我在代码里也会省去。 228 | 229 | --- 230 | 231 | ## STL 概览 232 | 233 | 诞生于 1994 ~~梦想做...~~ 234 | 235 | - 容器 containers:顺序 (sequence) 容器,关联 (associative) 容器 236 | - 迭代器 iterators:5 iterator categories 237 | - 算法 algorithms 238 | - 适配器 adapters:iterator adapters, container adapters 239 | - 仿函数 function objects (functors) 240 | - 空间分配器 allocators 241 | 242 | --- 243 | 244 | ## 容器 245 | 246 | - 顺序容器 sequence containers 247 | - `vector`, `deque`, `list`, `array`, `forward_list` 248 | - 关联容器 associative containers 249 | - `map`, `set`, `multimap`, `multiset` 250 | - `unordered_map`, `unordered_set`, `unordered_multimap`, `unordered_multiset` 251 | 252 | --- 253 | 254 | ## 顺序容器 255 | 256 | - `vector`:可变长数组,支持在末尾快速地添加、删除元素,支持随机访问 (random access) 257 | 258 | 259 | 260 | 261 | 262 | - `deque`:双端队列 (**d**ouble **e**nded **que**ue),支持在开头和末尾快速地添加、删除元素,支持随机访问 263 | 264 | 265 | 266 | 267 | 268 | --- 269 | 270 | ## 顺序容器 271 | 272 | - `list`:双向链表,支持在任意位置快速地添加、删除元素,支持双向遍历,不支持随机访问 273 | 274 | 275 | 276 | 277 | 278 | - `forward_list`:单向链表,支持在任意位置快速地添加、删除元素,仅支持单向遍历,不支持随机访问 279 | 280 | 281 | 282 | 283 | 284 | --- 285 | 286 | ## 顺序容器 287 | 288 | - `array`:内置数组 `T[N]` 的套壳,提供和其它 STL 容器一致的接口(包括迭代器等),不会退化为 `T *`。 289 | - 用 `T[N]` 就像是坐在凳子上。用 `std::array` 感觉像是坐在了沙发上,但其实就是在凳子上加了个垫子。 290 | - 特别的:`string` 不是容器,但非常像一个容器 291 | 292 | --- 293 | 294 | ## 统一的接口:构造 295 | 296 | - `Container c`:一个空的容器 297 | - `Container c(n, x)`:`n` 个 `x` 298 | - `Container c(n)`:`n` 个元素,每个元素都被值初始化 299 | - `string` 不支持这一操作,为什么? 300 | - `Container c(begin, end)`:从迭代器范围 `[begin, end)` 中拷贝元素。 301 | 302 | `array` 只支持默认构造,为什么? 303 | 304 | --- 305 | 306 | ## 统一的接口:[完整列表](https://en.cppreference.com/w/cpp/container#Member_function_table) 307 | 308 | 相同的功能在所有容器上都具有相同的接口(除了 `forward_list` 有一点点特殊) 309 | 310 | - 拷贝构造、拷贝赋值、移动构造、移动赋值 311 | - `c.size()`, `c.empty()`, `c.resize(n)` 312 | - `c.capacity()`, `c.reserve(n)`, `c.shrink_to_fit()` 313 | - `c.push_back(x)`, `c.emplace_back(args...)`, `c.pop_back()` 314 | - `c.push_front(x)`, `c.emplace_front(args...)`, `c.pop_front()` 315 | - `c.at(i)`, `c[i]`, `c.front()`, `c.back()` 316 | - `c.insert(pos, ...)`, `c.emplace(pos, args...)`, `c.erase(...)`, `c.clear()` 317 | 318 | --- 319 | 320 | ## 统一的接口 321 | 322 | 记忆的关键: 323 | 324 | 1. 理解每一种容器的底层数据结构,自然就明白为何支持/不支持某种操作 325 | - 为何链表不支持下标访问?为何 `vector` 不支持在开头添加元素? 326 | 2. 记住这些接口的名字 327 | 3. 如果一个操作应该被支持,它就必然叫那个名字、是那个用法 328 | 329 | --- 330 | 331 | ## `string` 和 `vector` 的“容量” 332 | 333 | `string` 大概率和 `vector` 采用类似的增长方式,分配的内存可能比当前存储的元素所占用的内存大。 334 | - 当前所拥有的内存能放下多少个元素,称为“容量” (capacity),可以通过 `c.capacity()` 查询。 335 | - `c.reserve(n)` 为至少 `n` 个元素预留内存。 336 | - 如果 `n <= c.capacity()`,什么都不会发生。 337 | - 否则,重新分配内存使得 `c` 能装得下至少 `n` 个元素。 338 | - **务必区分 `reserve` 和 `resize`。** 339 | - `c.shrink_to_fit()`:**请求** `c` 释放多余的容量(可能重新分配一块更小的内存) 340 | - 这个函数在 `deque` 上也有。 341 | 342 | --- 343 | 344 | ## `insert` 和 `erase` 345 | 346 | `c.insert(pos, ...)`, `c.emplace(pos, args...)`,其中 `pos` 是一个迭代器。 347 | 348 | 在 `pos` 所指的位置**之前**添加元素。 349 | 350 | `insert` 有很多种写法,可以 `c.insert(pos, x)`, `c.insert(pos, begin, end)`, `c.insert(pos, {a, b, c, ...})` 等等,用到的时候再查。 351 | 352 | `c.erase(...)` 也有很多种写法,用到的时候再查。 353 | 354 | --- 355 | 356 | ## 特殊的 `forward_list` 357 | 358 | `forward_list` 的功能完全被 `list` 包含,那为何我们还需要 `forward_list`? 359 | 360 | --- 361 | 362 | ## 特殊的 `forward_list` 363 | 364 | `forward_list` 的功能完全被 `list` 包含,那为何我们还需要 `forward_list`? 365 | 366 | **为了省时间,省空间!** 367 | 368 | - 单向链表的结点比双向链表的结点少存一个指针 369 | - 维护单向链表上的链接关系也比维护双向链表少一些操作 370 | 371 | 因此,`forward_list` 采取最简的实现:能省则省 372 | 373 | - 它不能 `push_back`/`pop_back`。 374 | - 如果需要,你可以自己保存指向末尾元素的迭代器。 375 | - 它甚至不支持 `size()`。如果需要,你可以自己用一个变量记录。 376 | 377 | --- 378 | 379 | ## 特殊的 `forward_list` 380 | 381 | `insert`, `emplace` 和 `erase` 变成了 `insert_after`, `emplace_after` 和 `erase_after` 382 | 383 | - 单向链表上,操作“下一个元素”比操作“当前元素”或“前一个元素”更方便。 384 | - 到 CS101 你们就知道咋回事了。 385 | 386 | --- 387 | 388 | ## 越界检查 389 | 390 | `c.at(i)` 在越界时抛出 `std::out_of_range` 异常 391 | 392 | `c[i]`, `c.front()`, `c.back()`, `c.pop_back()`, `c.pop_front()` 统统不检查越界,一切为了效率。 393 | 394 | 也许下面这种设计更合理? 395 | 396 | ```cpp 397 | auto &operator[](size_type n) { 398 | #ifndef NDEBUG 399 | if (n >= size()) 400 | throw std::out_of_range{"subscript out of range"}; 401 | #endif 402 | return data[n]; 403 | } 404 | ``` 405 | 406 | --- 407 | 408 | ## 选择正确的容器 409 | 410 | 顺序容器: 411 | - 能维持元素的先后顺序 412 | - 某些情况下的插入、删除、查找可能较慢。 413 | 414 | 关联容器(不带 `unordered` 的): 415 | - 元素总是有序的,默认是升序(因为底层数据结构通常是红黑树) 416 | - 支持 $O(\log n)$ 地插入、删除、查找元素 417 | 418 | 无序关联容器(`unordered`): 419 | - 元素是无序的(因为底层数据结构是哈希表) 420 | - 支持平均情况下 $O(1)$ 地插入、删除、查找元素 421 | 422 | --- 423 | 424 | # 迭代器 425 | 426 | --- 427 | 428 | ## 假如没有迭代器... 429 | 430 | 不同的容器,根据底层数据结构不同,遍历方式自然也不同: 431 | 432 | ```cpp 433 | for (std::size_t i = 0; i != a.size(); ++i) 434 | do_something(a[i]); 435 | // 可能的方式:通过指向结点的“句柄”(指针)遍历一个链表 436 | for (node_handle node = l.first_node(); node; node = node.next()) 437 | do_something(node.value()) 438 | ``` 439 | 440 | 如果是个更复杂的容器呢,比如基于哈希表/红黑树实现的关联容器? 441 | 442 | --- 443 | 444 | ## 迭代器:使用统一的方式访问元素、遍历容器 445 | 446 | 所有容器都有其对应的迭代器类型 `Container::iterator`,例如 `std::string::iterator`, `std::vector::iterator`。 447 | 448 | 所有容器都支持 `c.begin()`, `c.end()`,分别返回指向**首元素**和指向**尾后位置**的迭代器。 449 | 450 | 451 | 452 | 453 | 454 | **\* 我们总是使用左闭右开区间 `[begin, end)` 表示一个“迭代器范围”** 455 | 456 | --- 457 | 458 | ## 迭代器:使用统一的方式访问元素、遍历容器 459 | 460 | - `it1 != it1` 比较两个迭代器是否相等(指向相同位置) 461 | - `++it` 让 `it` 指向下一个位置。 462 | - `*it` 获取 `it` 指向的元素的**引用**。 463 | 464 | ```cpp 465 | for (auto it = c.begin(); it != c.end(); ++it) 466 | do_something(*it); 467 | ``` 468 | 469 | 基于范围的 `for` 语句:完全等价于上面的使用迭代器的遍历 470 | 471 | ```cpp 472 | for (auto &x : c) 473 | do_something(x); 474 | ``` 475 | 476 | --- 477 | 478 | ## `const_iterator` 479 | 480 | 带有“**底层 `const`**”的迭代器。 481 | 482 | - `Container::const_iterator` 483 | - `c.cbegin()`, `c.cend()` 484 | - 在一个 `const` 对象上,`c.begin()` 和 `c.end()` **也返回 `const_iterator`**。 485 | 486 | 对 `const_iterator` 解引用会得到 `const T &` 而非 `T &`,无法通过它修改元素的值。 487 | 488 | --- 489 | 490 | ## `begin`, `end`, `cbegin`, `cend` 491 | 492 | 再次出现了 `const` 和 non-`const` 的重载。 493 | 494 | ```cpp 495 | class MyContainer { 496 | public: 497 | using iterator = /* unspecified */; 498 | using const_iterator = /* unspecified */; 499 | iterator begin(); 500 | const_iterator begin() const; 501 | iterator end(); 502 | const_iterator end() const; 503 | const_iterator cbegin() const; 504 | const_iterator cend() const; 505 | }; 506 | ``` 507 | 508 | --- 509 | 510 | ## 迭代器型别 (iterator category) 511 | 512 | - ForwardIterator: 支持 `*it`, `it->mem`, `++it`, `it++`, `it1 == it2`, `it1 != it2` 513 | - BidirectionalIterator: 在 ForwardIterator 的基础上,支持 `--it`, `it--`。 514 | - RandomAccessIterator: 在 BidirectionalIterator 的基础上,支持 `it[n]`, `it + n`, `it += n`, `n + it`, `it - n`, `it -= n`, `it1 - it2`, `<`, `<=`, `>`, `>=`。 515 | 516 | `vector`, `string`, `array`, `deque` 的迭代器是 RandomAccessIterator;`list` 的迭代器是 BidirectionalIterator;`forward_list` 的迭代器是 ForwardIterator。 517 | 518 | **为什么我这么喜欢在 `for` 循环的终止条件里写 `!=` 而非 `<`?** 519 | 520 | --- 521 | 522 | ## 迭代器型别 (iterator category) 523 | 524 | - ForwardIterator: 支持 `*it`, `it->mem`, `++it`, `it++`, `it1 == it2`, `it1 != it2` 525 | - BidirectionalIterator: 在 ForwardIterator 的基础上,支持 `--it`, `it--`。 526 | - RandomAccessIterator: 在 BidirectionalIterator 的基础上,支持 `it[n]`, `it + n`, `it += n`, `n + it`, `it - n`, `it -= n`, `it1 - it2`, `<`, `<=`, `>`, `>=`。 527 | 528 | `vector`, `string`, `array`, `deque` 的迭代器是 RandomAccessIterator;`list` 的迭代器是 BidirectionalIterator;`forward_list` 的迭代器是 ForwardIterator。 529 | 530 | **为什么我这么喜欢在 `for` 循环的终止条件里写 `!=` 而非 `<`?**——不是所有迭代器都支持 `<`,但所有迭代器都支持 `!=`! 531 | 532 | --- 533 | 534 | ## 迭代器型别 (iterator category) 535 | 536 | 还有两种迭代器型别:InputIterator 和 OutputIterator。 537 | 538 | - InputIterator 表示**可以通过这个迭代器获得元素**(不要求能修改它所指向的元素) 539 | - OutputIterator 表示**可以通过这个迭代器写入元素**(不要求能获得它所指向的元素) 540 | 541 | - A ForwardIterator **is an** InputIterator. 542 | 543 | 稍后我们会看到一些例子。 544 | 545 | --- 546 | 547 | ## 数组和指针 548 | 549 | 指针 `T *` 是数组 `T[N]` 的迭代器,那么它属于哪种 category? 550 | 551 | --- 552 | 553 | ## 数组和指针 554 | 555 | 指针 `T *` 是数组 `T[N]` 的 iterator, `const T *` 是其 const_iterator。 556 | 557 | 指针作为数组的迭代器,其型别为 RandomAccessIterator。 558 | 559 | 标准库 `std::begin(a)`, `std::end(a)`, `std::cbegin(a)`, `std::cend(a)`(定义在 `` 等头文件中):当 `a` 是数组时返回相应的指针,当 `a` 是容器时返回相应的迭代器。 560 | 561 | --- 562 | 563 | ## 反向迭代器 `reverse_iterator` 564 | 565 | 一种 iterator adaptor 566 | 567 | - `Container::reverse_iterator`, `Container::const_reverse_iterator` 568 | - `c.rbegin()`, `c.rend()`, `c.crbegin()`, `c.crend()` 569 | - `++` 和 `--` 在反向迭代器上都是反的。 570 | 571 | ```cpp 572 | std::vector v{1, 2, 3, 4, 5}; 573 | for (auto rit = v.rbegin(); rit != v.rend(); ++rit) 574 | std::cout << *rit << ' '; 575 | ``` 576 | 577 | 输出:`5 4 3 2 1 ` 578 | 579 | --- 580 | 581 | ## 移动迭代器 `move_iterator` 582 | 583 | 一种 iterator adaptor 584 | 585 | - `std::make_move_iterator(iter)` 从一个普通的迭代器 `iter` 变出一个 move_iterator 586 | - `*mit` 会得到右值引用而非左值引用,从而元素更可能被移动而非被拷贝。 587 | 588 | ```cpp 589 | std::vector words = someValues(); 590 | std::vector v(words.size()); 591 | std::copy(std::make_move_iterator(words.begin()), 592 | std::make_move_iterator(words.end()), v.begin()); 593 | ``` 594 | 595 | `words` 中的每个 `string` 被**移动**进了 `v`,而不是拷贝。 596 | 597 | --- 598 | 599 | ## 插入迭代器 600 | 601 | `insert_iterator`, `front_insert_iterator`, `back_insert_iterator` 602 | 603 | 也属于 iterator adaptors。 604 | 605 | 典型的 OutputIterator: 606 | 607 | - 只可向 `*iter` 写入元素,不能从 `*iter` 读取元素 608 | - 它们会调用容器的 `insert`, `push_front` 或 `push_back`,将“写入”的元素插入容器 609 | 610 | ```cpp 611 | std::vector numbers = someValues(); 612 | std::vector v; 613 | std::copy(numbers.begin(), numbers.end(), v.begin()); // 错误! 614 | std::copy(numbers.begin(), numbers.end(), std::back_inserter(v)); // 正确 615 | ``` 616 | 617 | `std::back_inserter(v)` 生成一个 `std::back_insert_iterator`,它会不断调用 `v.push_back(x)` 将向它“写入”的元素添加进 `v`。 618 | 619 | --- 620 | 621 | ## 流迭代器 622 | 623 | 读入一串数存进一个 `vector`: 624 | 625 | ```cpp 626 | std::vector v(std::istream_iterator(std::cin), 627 | std::istream_iterator{}); 628 | ``` 629 | 630 | 利用 `std::copy` 将 `v` 中的元素输出,并且每个后面跟一个 `", "`: 631 | 632 | ```cpp 633 | std::copy(v.begin(), v.end(), std::ostream_iterator(std::cout, ", ")); 634 | ``` 635 | 636 | - `istream_iterator` 是一种 InputIterator,它不断从输入流中获取元素 637 | - `ostream_iterator` 是一种 OutputIterator,它不断将向它“写入”的元素写进输出流 638 | 639 | --- 640 | 641 | ## 迭代器型别 642 | 643 | InputIterator 和 ForwardIterator 都要求支持 `++it`, `it++`, `*it`, `it->mem`, `==`, `!=`。 644 | 645 | 这两类迭代器的区别究竟是什么? 646 | 647 | --- 648 | 649 | ## 迭代器型别 650 | 651 | InputIterator 和 ForwardIterator 都要求支持 `++it`, `it++`, `*it`, `it->mem`, `==`, `!=`。 652 | 653 | 这两类迭代器的区别究竟是什么?—— ForwardIterator 提供 **multi-pass guarantee** 654 | 655 | ```cpp 656 | auto original = iter; // 对当前的 iter 做个拷贝 657 | auto value = *iter; // 现在 iter 指向的元素是 value 658 | ++iter; 659 | assert(*original == value); 660 | ``` 661 | 662 | 对于一个 ForwardIterator 来说,`original` 指向了 `iter` 在递增之前指向的位置,那个位置上的值始终是 `value`。 663 | 664 | InputIterator 不这么认为,它只保证能“input”:`++iter` 就意味着我们打算获取“下一个值”了,先前的值也就无法再被获取了。(考虑“输入”的过程) 665 | -------------------------------------------------------------------------------- /r2/img/doge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r2/img/doge.jpg -------------------------------------------------------------------------------- /r2/img/scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r2/img/scopes.png -------------------------------------------------------------------------------- /r3/img/mdarray/mdarray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r3/img/mdarray/mdarray.png -------------------------------------------------------------------------------- /r3/img/mdarray/mdarray.tex: -------------------------------------------------------------------------------- 1 | \documentclass[tikz, convert={outext=.png}]{standalone} 2 | 3 | % use xelatex 4 | \usepackage{fontspec} 5 | \usepackage{pgffor} 6 | 7 | \setmainfont{Ubuntu Mono} 8 | 9 | \begin{document} 10 | 11 | \begin{tikzpicture} 12 | \node at (-2.5, 0) {Type a[4][3]}; 13 | \foreach \i in {0, 1, 2, 3} { 14 | \node[rectangle, draw, minimum width = 2.1cm, minimum height = 0.7cm, double] (r\i) at (\i * 2.1, 0) {}; 15 | \node at (\i * 2.1, -0.7) {a[\i]}; 16 | \foreach \j in {1, 2} { 17 | \draw[dashed] (\i * 2.1 + \j * 0.7 - 1.05, -0.35) -- (\i * 2.1 + \j * 0.7 - 1.05, 0.35); 18 | } 19 | } 20 | \end{tikzpicture} 21 | 22 | \end{document} -------------------------------------------------------------------------------- /r3/img/ptradd/ptradd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r3/img/ptradd/ptradd.png -------------------------------------------------------------------------------- /r3/img/ptradd/ptradd.tex: -------------------------------------------------------------------------------- 1 | \documentclass[tikz, convert={outext=.png}]{standalone} 2 | 3 | % use xelatex 4 | \usepackage{fontspec} 5 | \usepackage{pgffor} 6 | 7 | \setmainfont{Ubuntu Mono} 8 | 9 | \begin{document} 10 | 11 | \begin{tikzpicture} 12 | \foreach \i in {0, 1, 2, 3} { 13 | \node[rectangle, draw, minimum width = 2cm, minimum height = 1cm] (r\i) at (\i * 2, 0) {}; 14 | \node at (r\i.center) {a[\i]}; 15 | \draw[->] (\i * 2 - 0.9, -1) -- (\i * 2 - 0.9, -0.5); 16 | } 17 | \node[below] at (-0.9, -1) {p}; 18 | \foreach \i in {1, 2, 3} { 19 | \node[below] at (\i * 2 - 0.9, -1) {p+\i}; 20 | } 21 | \end{tikzpicture} 22 | 23 | \end{document} -------------------------------------------------------------------------------- /r4/img/malloc2d/malloc2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r4/img/malloc2d/malloc2d.png -------------------------------------------------------------------------------- /r4/img/malloc2d/malloc2d.tex: -------------------------------------------------------------------------------- 1 | \documentclass[tikz, convert={outext=.png}]{standalone} 2 | 3 | % use xelatex 4 | \usepackage{fontspec} 5 | \usepackage{pgffor} 6 | 7 | \setmainfont{Ubuntu Mono} 8 | 9 | \newcommand{\nullch}{\footnotesize{\textbackslash 0}} 10 | 11 | \tikzstyle{arrgrid} = [rectangle, draw, minimum width = 0.5cm, minimum height = 0.5cm] 12 | 13 | \begin{document} 14 | 15 | \begin{tikzpicture} 16 | \node at (-1, 0) {int **p}; 17 | \foreach \i in {0, 1, 2, 3} { 18 | \node[arrgrid] (r\i) at (\i * 0.5, 0) {*}; 19 | \node at (\i * 0.5, 0.5) {\i}; 20 | } 21 | \draw[-] (1.75, 0.25) -- (4, 0.25); 22 | \draw[-] (1.75, -0.25) -- (4, -0.25); 23 | \node[arrgrid] at (4, 0) {*}; 24 | \node at (4, 0.5) {N-1}; 25 | \node at (2.25, 0) {\(\cdots\)}; 26 | \foreach \i in {0, 1, 2, 3} { 27 | \foreach \j in {0, 1, 2, 3, 4} { 28 | \node[arrgrid] (r\i\j) at (\i * 0.5 + \j * 0.5, -3 + 0.75 * \i) {}; 29 | } 30 | \draw[->] (r\i) -- (r\i0); 31 | } 32 | \foreach \i\j in {0/0, 1/1, 2/2, 3/{\(\cdots\)}, 4/{M-1}} { 33 | \node at (\i * 0.5, -3.5) {\j}; 34 | } 35 | \end{tikzpicture} 36 | 37 | \end{document} -------------------------------------------------------------------------------- /r4/img/malloc2d/malloc2d.xdv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r4/img/malloc2d/malloc2d.xdv -------------------------------------------------------------------------------- /r4/img/translations/translations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r4/img/translations/translations.png -------------------------------------------------------------------------------- /r4/img/translations/translations.tex: -------------------------------------------------------------------------------- 1 | \documentclass[tikz, convert={outext=.png}]{standalone} 2 | 3 | % use xelatex 4 | \usepackage{fontspec} 5 | \usepackage{pgffor} 6 | 7 | \setmainfont{Ubuntu Mono} 8 | 9 | \newcommand{\nullch}{\footnotesize{\textbackslash 0}} 10 | 11 | \tikzstyle{arrgrid} = [rectangle, draw, minimum width = 0.5cm, minimum height = 0.5cm] 12 | 13 | \begin{document} 14 | 15 | \begin{tikzpicture} 16 | \node at (-1.5, 0) {translations}; 17 | \foreach \i in {0, 1, 2, 3} { 18 | \node[arrgrid] (r\i) at (\i * 0.5, 0) {*}; 19 | \node at (\i * 0.5, 0.5) {\i}; 20 | } 21 | \draw[-] (1.75, 0.25) -- (4, 0.25); 22 | \draw[-] (1.75, -0.25) -- (4, -0.25); 23 | \node at (2.25, 0) {\(\cdots\)}; 24 | \foreach \i\j in {0/z, 1/e, 2/r, 3/o, 4/\nullch} { 25 | \node[arrgrid] (zero\i) at (\i * 0.5, -3) {\j}; 26 | } 27 | \foreach \i\j in {0/o, 1/n, 2/e, 3/\nullch} { 28 | \node[arrgrid] (one\i) at (\i * 0.5 + 0.5, -2.25) {\j}; 29 | } 30 | \foreach \i\j in {0/t, 1/w, 2/o, 3/\nullch} { 31 | \node[arrgrid] (two\i) at (\i * 0.5 + 1, -1.5) {\j}; 32 | } 33 | \foreach \i\j in {0/t, 1/h, 2/r, 3/e, 4/e, 5/\nullch} { 34 | \node[arrgrid] (three\i) at (\i * 0.5 + 1.5, -0.75) {\j}; 35 | } 36 | \foreach \i\j in {0/zero, 1/one, 2/two, 3/three} { 37 | \draw[->] (r\i) -- (\j0); 38 | } 39 | \end{tikzpicture} 40 | 41 | \end{document} -------------------------------------------------------------------------------- /r4/img/translations/translations.xdv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r4/img/translations/translations.xdv -------------------------------------------------------------------------------- /r4/r4.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 4 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 指针和数组(续) 15 | - 字符串 16 | - 动态内存 17 | 18 | --- 19 | 20 | # 指针和数组 21 | 22 | --- 23 | 24 | ## 指针类型 25 | 26 | 对于两个不同的类型 `T` 和 `U`,指针类型 `T *` 和 `U *` 是**不同的类型**(尽管它们有可能指向相同的地址)。 27 | 28 | ```c 29 | int i = 42; 30 | float *fp = &i; 31 | *fp += 1; // undefined behavior 32 | ``` 33 | 34 | 尽管在 C 中,不同类型的指针之间可以相互(隐式)转换(在 C++ 中必须显式转换),但如果一个指针指向的对象和它被声明的类型不符,解引用这个指针几乎总是 undefined behavior。 35 | 36 | - 除了[一些情况](https://en.cppreference.com/w/c/language/object#Strict_aliasing)(稍后会看到一种) 37 | 38 | --- 39 | 40 | ## `void *`:“指向 `void` 的指针” 41 | 42 | > 埏埴以为器,当其无,有器之用。——老子《道德经》 43 | 44 | - 任何指针都可以(隐式)转换成 `void *` 45 | - `void *` 可以(隐式)转换成任何指针 46 | - 可以用 `printf("%p", ptr)` 输出一个 `void *` 类型的指针 `ptr` 的值。 47 | - 如果 `ptr` 是其它类型的指针,需要先转换:`printf("%p", (void *)ptr)` 48 | 49 | **\* 注意:`scanf` 和 `printf` 的 conversion specifier 如果和对应的变量类型(在 [default promotion](https://en.cppreference.com/w/c/language/conversion#Default_argument_promotions) 之后)不匹配,则是 undefined behavior。** 50 | - 不能认为 `printf("%f", ival)` 等价于 `printf("%f", (float)ival)`(试一试) 51 | 52 | --- 53 | 54 | ## `void *` 55 | 56 | 在没有 C++ 那样强的静态类型系统支持下,`void *` 经常被用来表示“任意类型的指针”、“(未知类型的)内存的地址”,甚至是“任意的对象”。 57 | 58 | - `malloc` 的返回值类型就是 `void *`,`free` 的参数类型也是 `void *`。 59 | - `pthread_create` 允许给线程函数传任意参数,方法就是用 `void *` 转交。 60 | - 在 C 中,接受 `malloc` 的返回值时**不需要显式转换**。 61 | 62 | **`void *` 是 C 类型系统真正意义上的天窗** 63 | 64 | --- 65 | 66 | ## 退化 67 | 68 | - 数组向指向首元素指针的隐式转换(退化): 69 | - `Type [N]` 会退化为 `Type *` 70 | - “二维数组”其实是“数组的数组”: 71 | - `Type [N][M]` 是一个 `N` 个元素的数组,每个元素都是 `Type [M]` 72 | - `Type [N][M]` 应该退化为什么类型? 73 | 74 | --- 75 | 76 | ## 退化 77 | 78 | - 数组向指向首元素指针的隐式转换(退化): 79 | - `Type [N]` 会退化为 `Type *` 80 | - “二维数组”其实是“数组的数组”: 81 | - `Type [N][M]` 是一个 `N` 个元素的数组,每个元素都是 `Type [M]` 82 | - `Type [N][M]` 退化为“指向 `Type [M]` 的指针” 83 | - 如何定义一个“指向 `Type [M]` 的指针”? 84 | 85 | --- 86 | 87 | ## 稍微复杂一点儿的复合类型 88 | 89 |
90 |
91 | 92 | 指向数组的指针 93 | ```c 94 | int (*parr)[N]; 95 | ``` 96 |
97 |
98 | 99 | 存放指针的数组 100 | ```c 101 | int *arrp[N]; 102 | ``` 103 |
104 |
105 | 106 | - 首先,记住**这两种写法都有,而且是不同的类型**。 107 | - `int (*parr)[N]` 为何要加一个圆括号?当然是因为 `parr` 和“指针”的关系更近 108 | - 所以 `parr` **是指针**, 109 | - 指向的东西是 `int [N]` 110 | - 那么另一种则相反: 111 | - `arrp` **是数组**, 112 | - 数组里存放的东西是指针。 113 | 114 | --- 115 | 116 | ## 向函数传递二维数组 117 | 118 | 以下声明了**同一个函数**:参数类型为 `int (*)[N]`,即一个指向 `int [N]` 的指针。 119 | 120 | ```c 121 | void fun(int (*a)[N]); 122 | void fun(int a[][N]); 123 | void fun(int a[2][N]); 124 | void fun(int a[10][N]); 125 | ``` 126 | 127 | 可以传递 `int [K][N]` 给 `fun`,其中 `K` 可以是任意值。 128 | - 第二维大小必须是 `N`。 `Type [10]` 和 `Type [100]` 是不同的类型,指向它们的指针之间不兼容。 129 | 130 | --- 131 | 132 | ## 向函数传递二维数组 133 | 134 | 以下声明中,参数 `a` 分别具有什么类型?哪些可以接受一个二维数组 `int [N][M]`? 135 | 136 | 1. `void fun(int a[N][M])` 137 | 2. `void fun(int (*a)[M])` 138 | 3. `void fun(int (*a)[N])` 139 | 4. `void fun(int **a)` 140 | 5. `void fun(int *a[])` 141 | 6. `void fun(int *a[N])` 142 | 7. `void fun(int a[100][M])` 143 | 8. `void fun(int a[N][100])` 144 | 145 | --- 146 | 147 | ## 向函数传递二维数组 148 | 149 | 以下声明中,参数 `a` 分别具有什么类型?哪些可以接受一个二维数组 `int [N][M]`? 150 | 151 | 1. `void fun(int a[N][M])`:指向 `int [M]` 的指针,可以 152 | 2. `void fun(int (*a)[M])`:同 1 153 | 3. `void fun(int (*a)[N])`:指向 `int [N]` 的指针,**不可以** 154 | 4. `void fun(int **a)`:指向 `int *` 的指针,**不可以** 155 | 5. `void fun(int *a[])`:同 4 156 | 6. `void fun(int *a[N])`:同 4 157 | 7. `void fun(int a[100][M])`:同 1 158 | 8. `void fun(int a[N][100])`:指向 `int [100]` 的指针,当且仅当 `M==100` 时可以 159 | 160 | --- 161 | 162 | ## 向函数传递二维数组 163 | 164 | 练习:定义一个函数 `transpose`,接受一个 $N\times M$ 的二维数组 `a`,和一个 $M\times N$ 的二维数组 `b`,将 `a` 的转置存入 `b`。 165 | 166 | --- 167 | 168 | ## 向函数传递二维数组 169 | 170 | 练习:定义一个函数 `transpose`,接受一个 $N\times M$ 的二维数组 `a`,和一个 $M\times N$ 的二维数组 `b`,将 `a` 的转置存入 `b`。 171 | 172 | ```c 173 | void transpose(int a[N][M], int b[M][N]) { 174 | for (int i = 0; i != M; ++i) 175 | for (int j = 0; j != N; ++j) 176 | b[i][j] = a[j][i]; 177 | } 178 | ``` 179 | 180 | --- 181 | 182 | ## `const` 183 | 184 | `const` 变量:**一经初始化就不能再改变**(“常量”),所以当然**必须初始化**. 185 | 186 | - “常量”这个词其实很容易引发歧义,C/C++ 中还有一种真正的“常量”,是指**值在编译期已知的量**。 187 | 188 | 可以定义“指向常量的指针”:`const Type *ptr` 或 `Type const *ptr` 189 | 190 | --- 191 | 192 | ## pointer-to-`const` 193 | 194 | 一个“指向常量的指针”也可以指向一个 non-`const` variable 195 | 196 | - 但它**自以为**自己指向了“常量”,所以不允许你通过它修改它所指向的变量。 197 | 198 | ```c 199 | int i = 42; 200 | const int *cip = &i; 201 | int *ip = &i; 202 | ++i; // OK 203 | ++*ip; // OK 204 | ++*cip; // Error 205 | ``` 206 | - “底层 `const`”(low-level `const`) 207 | 208 | --- 209 | 210 | ## pointer-to-`const` 211 | 212 | 不能用 pointer-to-non-`const` 指向一个真正的 `const` 变量,也不能用一个 pointer-to-`const` 给它赋值或初始化(“不能去除底层 `const`”) 213 | 214 | - 如果把 `const` 视为一把锁,这就是在试图拆掉锁。 215 | - 你可以用 explicit cast(显式转换)强行拆锁,但由此引发的对于 `const` 变量的修改是 undefined behavior 216 | 217 |
218 |
219 | 220 | ```c 221 | const int ci = 42; 222 | int *ip = (int *)&ci; 223 | ++*ip; // Undefined behavior 224 | ``` 225 |
226 |
227 | 228 | ```c 229 | int i = 42; 230 | const int *cip = &i; 231 | int *ip = cip; // Warning in C, Error in C++ 232 | int *ip2 = (int *)cip; // OK 233 | ``` 234 |
235 |
236 | 237 | --- 238 | 239 | ## 顶层 `const`(top-level `const`) 240 | 241 | 一个指针自己是 `const` 变量:它永远指向它初始化时指向的那个对象 242 | 243 | 有时称为“常量指针” 244 | 245 | ```c 246 | int ival = 42; 247 | int *const ipc = &ival; 248 | ++*ipc; // Correct 249 | int ival2 = 35; 250 | ipc = &ival2; // Error. ipc is not modifiable. 251 | ``` 252 | 253 | 当然也可以同时带有底层、顶层 `const`: 254 | 255 | ```c 256 | const int *const cipc = &ival; 257 | ``` 258 | 259 | --- 260 | 261 | ## 总结 262 | 263 | - 不要随意转换不同类型之间的指针,极易引发 undefined behavior 264 | - `void *` 是万金油,可以接受一切指针,向一切指针转换(类型系统的天窗) 265 | - 用 `"%p"` 输出 `void *` 266 | - 通常用来表示“任何指针”、“某片内存的地址”(“内存”不谈类型)、“任何参数”等。 267 | - 二维数组 `Type [N][M]` 会退化为 `Type (*)[M]`,即“指向 `Type [M]` 的指针” 268 | - `Type *a[M]` 是“存放 `Type *` 的数组” 269 | - 声明一个二维数组参数 `Type [N][M]` 时,`N` 随意(反正会退化为指针),但 `M` 必须和传进来的实参的对应维度大小相等。 270 | 271 | --- 272 | 273 | ## 总结 274 | 275 | - 底层 `const`:(自以为)指向的东西是 `const` 276 | - `const Type *ptr` 或 `Type const *ptr` 277 | - 有可能其实指向了一个 non-`const`,但它不管 278 | - 不允许通过这个指针修改它所指向的变量 279 | - 顶层 `const`:自己是不可修改的 `const` 变量 280 | - `Type *const ptr` 281 | - 初始化的时候指向谁,就永远指向谁,不可修改 282 | 283 | --- 284 | 285 | # 字符串 286 | 287 | --- 288 | 289 | ## “C 风格字符串”(C-style strings) 290 | 291 | C 语言没有对应“字符串”的抽象,字符串就是一群字符排在一起。 292 | - 可以是数组,也可以是动态分配的内存 293 | - **末尾必须有一个 `'\0'`,`'\0'` 在哪末尾就在哪。** 294 | 295 | ```c 296 | char s[10] = "abcde"; // s = {'a', 'b', 'c', 'd', 'e', '\0'} 297 | printf("%s\n", s); // abcde 298 | printf("%s\n", s + 1); // bcde 299 | s[2] = ';'; // s = "ab;de" 300 | printf("%s\n", s); // ab;de 301 | s[2] = '\0'; 302 | printf("%s\n", s); // ab 303 | ``` 304 | 305 | --- 306 | 307 | ## ~~愚蠢的~~结束符 `'\0'` 308 | 309 | `'\0'` 是所谓的“空字符”,其 ASCII 值为 `0`。 310 | 311 | C 风格字符串结束的**唯一**判断标志 312 | - 我们已经知道,一维数组被传递给函数时会退化,长度信息根本带不进去。 313 | 314 | **C 语言标准库的所有处理字符串的函数都会去找 `'\0'`** 315 | - 缺少 `'\0'` 会让他们不停地走下去,(很可能)导致越界访问。 316 | 317 | 用数组存储字符串时,记得为空字符多开一格。 318 | 319 | ```c 320 | char s[5] = "abcde"; // OK, but no place for '\0'. 321 | puts(s); // undefined behavior (missing '\0') 322 | ``` 323 | 324 | --- 325 | 326 | ## 字符串 IO 327 | 328 | - `scanf`/`printf`:`"%s"` 329 | - `scanf` 读 `"%s"` 有内存安全问题:它并不知道你传给它的数组有多长 330 | - `scanf` 没被踢出去,(我猜)是因为它还有别的用途 331 | - `gets`:**自 C11 起被踢出标准**,因为它只有这一个用途 332 | - 替代品 `gets_s` 在标准的附录里,很遗憾 GCC 没有支持它 333 | - [`fgets`](https://en.cppreference.com/w/c/io/fgets):更通用,更安全 334 | 335 | ```c 336 | char str[100]; 337 | fgets(str, 100, stdin); 338 | ``` 339 | - `puts(str);` 输出字符串 `str` 并换行 340 | 341 | --- 342 | 343 | ## 求字符串的长度 344 | 345 | 练习:实现你自己的 `strlen` 函数,接受一个字符串(起始位置的指针),返回这个字符串的长度。 346 | 347 | **字符串的“长度”不算末尾的空字符,任何时候都是这样。** 348 | 349 | --- 350 | 351 | ## 求字符串的长度 352 | 353 | 练习:实现你自己的 `strlen` 函数,接受一个字符串(起始位置的指针),返回这个字符串的长度。 354 | 355 |
356 |
357 | 358 | ```c 359 | size_t my_strlen(const char *str) { 360 | size_t ans = 0; 361 | while (*str != '\0') { 362 | ++ans; 363 | ++str; 364 | } 365 | return ans; 366 | } 367 | ``` 368 |
369 |
370 | 371 | ```c 372 | size_t my_strlen(const char *start) { 373 | const char *end = start; 374 | while (*end != '\0') 375 | ++end; 376 | return end - start; 377 | } 378 | ``` 379 |
380 |
381 | 382 | --- 383 | 384 | ## 求字符串的长度 385 | 386 | 练习:实现你自己的 `strlen` 函数,接受一个字符串(起始位置的指针),返回这个字符串的长度。 387 | 388 | ```c 389 | size_t my_strlen(const char *str) { 390 | size_t ans = 0; 391 | while (*str++ != '\0') // 理解这个看起来很秀的写法 392 | ++ans; 393 | return ans; 394 | } 395 | ``` 396 | 397 | - 甚至可以 `while (*str++)`(为什么?),但不好。 398 | 399 | --- 400 | 401 | ## 千万不能这样写! 402 | 403 | ```c 404 | for (size_t i = 0; i < strlen(s); ++i) 405 | // ... 406 | ``` 407 | 每次循环体执行完毕时,都要执行一次判断条件 `i < strlen(s)`,而每次算 `strlen(s)` 都需要遍历整个字符串,**非常慢**(时间复杂度为 $O\left(n^2\right)$) 408 | 409 | **应该改为** 410 | 411 | ```c 412 | int n = strlen(s); 413 | for (int i = 0; i < n; ++i) 414 | // ... 415 | ``` 416 | 417 | --- 418 | 419 | ## 一个小问题 420 | 421 | ```c 422 | for (int i = 0; i < strlen(s); ++i) 423 | // ... 424 | ``` 425 | 编译器在 `i < strlen(s)` 上报了个 warning? 426 | - `strlen` 返回值类型为 `size_t`:无符号整数 427 | - 将 `int` 和 `size_t` 放在一起运算/比较时,**`int` 值会被转换为 `size_t` 类型** 428 | - `-1 < strlen(s)` 几乎肯定是 `false` 429 | 430 | **\* 不要混用带符号数和无符号数** 431 | 432 | --- 433 | 434 | ## 字符串字面值 (string literals) 435 | 436 | 字符串字面值:类似这种 `"abcde"`(**双引号!!!**) 437 | - 类型为 `char [N+1]`,其中 `N` 是它的长度,`+1` 是为了放空字符。 438 | - **但它会被放在只读的内存区域**(可能是 `.rodata`),所以它实际上应该带 `const` 439 | - 在 C++ 中,它的类型是 `const char [N+1]`,非常合理。 440 | 441 | 用不带底层 `const` 的指针指向一个 string literal 是合法的,但**极易导致 undefined behavior**: 442 | 443 | ```c 444 | char *p = "abcde"; 445 | p[3] = 'a'; // undefined behavior, and possibly runtime-error. 446 | ``` 447 | 448 | --- 449 | 450 | ## 字符串字面值 (string literals) 451 | 452 | 用不带底层 `const` 的指针指向一个 string literal 是合法的,但**极易导致 undefined behavior**: 453 | 454 | ```c 455 | char *p = "abcde"; 456 | p[3] = 'a'; // undefined behavior, and possibly runtime-error. 457 | ``` 458 | 459 | 正确的做法: 460 | 461 |
462 |
463 | 464 | 加上底层 `const` 的保护 465 | ```c 466 | const char *str = "abcde"; 467 | ``` 468 |
469 |
470 | 471 | 或者将内容拷贝进数组 472 | ```c 473 | char arr[] = "abcde"; 474 | ``` 475 |
476 |
477 | 478 | --- 479 | 480 | ## 字符串数组 481 | 482 | ```c 483 | const char *translations[] = { 484 | "zero", "one", "two", "three", "four", 485 | "five", "six", "seven", "eight", "nine" 486 | }; 487 | ``` 488 | 489 |
490 |
491 | 492 | - `translations` 是一个数组,存放的元素是指针,每个指针都指向一个 string literal 493 | - `translations` **不是二维数组!** 494 |
495 |
496 | 497 | 498 | 499 |
500 |
501 | 502 | --- 503 | 504 | ## 标准库函数 505 | 506 | [完整列表](https://en.cppreference.com/w/c/string/byte),你想要的都在这里 507 | 508 | 常见的几个: 509 | 510 | - `strcmp(s1, s2)` 按字典序 (lexicographical order) 比较两个字符串的大小。 511 | - 如果 `s1`“小于”`s2`,返回**一个负数** 512 | - 如果相等,返回 `0` 513 | - 如果 `s1`“大于”`s2`,返回**一个正数** 514 | - **不可以认为它的返回值 $\in\{-1,0,1\}$!!!** 515 | 516 | --- 517 | 518 | ## 标准库函数 519 | 520 | - `strcpy(to, from)` 将 `from` 的内容拷贝给 `to`。 521 | - `strtol`, `strtoll`, `strtoul`, `strtoull`, `strtof`, `strtod`, `strtold`:将一个以字符串形式表示的整数/浮点数的值提取出来。 522 | - 这些是 `atoi`、`atol` 等函数的替代品,**能自定义进制**,且有更好的错误处理 523 | - C23 还有 `strfromf` 之类的函数 524 | 525 | **\* 一个很好的练习是实现自己的 `strcpy`、`strcmp` 等函数** 526 | 527 | --- 528 | 529 | ## 标准库函数 530 | 531 | `` 里有一些识别、操纵单个字符的函数: 532 | - `isdigit(c)` 判断 `c` 是否是数字字符 533 | - `isxdigit(c)` 判断 `c` 是否是十六进制数字字符 534 | - `islower(c)` 判断 `c` 是否是小写字母 535 | - `isspace(c)` 判断 `c` 是否是空白(空格、回车、换行、制表等) 536 | - `toupper(c)`:如果 `c` 是小写字母,返回其大写形式,否则返回它本身 537 | 538 | --- 539 | 540 | ## 总结 541 | 542 | C 风格字符串是**以空字符结尾的**字符数组(或动态分配的连续内存) 543 | 544 | - 永远不要忘记空字符 545 | - 字面值不能修改,尽管它不带 `const` 546 | - 建议用带底层 `const` 的指针指向字面值 547 | - IO:`scanf`/`printf`, `fgets`, `puts` 548 | - `strcmp`, `strlen`, `strcpy` 549 | - 数字字符串的转换:`strtol`, `strtoll` 等 550 | - `` 一些处理单个字符的函数 551 | 552 | --- 553 | 554 | # 动态内存 555 | 556 | --- 557 | 558 | ## 使用 `malloc` 和 `free` 559 | 560 | 创建一个“动态数组”:大小在运行时确定 561 | 562 | ```c 563 | Type *ptr = malloc(sizeof(Type) * n); 564 | for (int i = 0; i != n; ++i) 565 | ptr[i] = /* ... */ 566 | // ... 567 | free(ptr); 568 | ``` 569 | 570 | --- 571 | 572 | ## 使用 `malloc` 和 `free` 573 | 574 | 也可以动态创建一个对象 575 | 576 | ```c 577 | int *ptr = malloc(sizeof(int)); 578 | *ptr = 42; 579 | // ... 580 | free(ptr); 581 | ``` 582 | 583 | 但是为什么需要这样?直接创建 `int ival = 42;` 不香吗? 584 | 585 | --- 586 | 587 | ## 使用 `malloc` 和 `free` 588 | 589 | 动态创建对象的好处:**生命期跨越作用域** 590 | 591 | ```c 592 | int *create_array(void) { 593 | int a[N]; 594 | return a; // 返回了一个指向局部变量 a 的指针 595 | // 当函数返回时,局部变量 a 已经被销毁,该地址无意义 596 | // 解引用这个指针将导致 undefined behavior 597 | } 598 | int *create_array2(int n) { 599 | return malloc(sizeof(int) * n); // OK. 动态分配的内存直到被 free 才被销毁 600 | } 601 | ``` 602 | 603 | --- 604 | 605 | ## 使用 `malloc` 和 `free` 606 | 607 | 动态创建一个“二维数组”? 608 | 609 |
610 |
611 | 612 | ```c 613 | int **p = malloc(sizeof(int *) * n); 614 | for (int i = 0; i != n; ++i) 615 | p[i] = malloc(sizeof(int) * m); 616 | for (int i = 0; i != n; ++i) 617 | for (int j = 0; j != m; ++j) 618 | p[i][j] = /* ... */ 619 | for (int i = 0; i != n; ++i) 620 | free(p[i]); 621 | free(p); 622 | ``` 623 |
624 |
625 | 626 | 627 | 628 |
629 |
630 | 631 | 632 | --- 633 | 634 | ## 使用 `malloc` 和 `free` 635 | 636 | 动态创建一个“二维数组”——另一种方法:创建一维数组 637 | 638 | ```c 639 | int *p = malloc(sizeof(int) * n * m); 640 | for (int i = 0; i != n; ++i) 641 | for (int j = 0; j != m; ++j) 642 | p[i * m + j] = /* ... */ 643 | // ... 644 | free(p); 645 | ``` 646 | 647 | --- 648 | 649 | ## `malloc`, `calloc` 和 `free` 650 | 651 | 看标准![`malloc`](https://en.cppreference.com/w/c/memory/malloc) [`calloc`](https://en.cppreference.com/w/c/memory/calloc) [`free`](https://en.cppreference.com/w/c/memory/free) 652 | 653 | ```c 654 | void *malloc(size_t size); 655 | void *calloc(size_t num, size_t each_size); 656 | void free(void *ptr); 657 | ``` 658 | 659 | - `calloc` 分配的内存大小为 `num * each_size`(其实不一定,但暂时不用管) 660 | - `malloc` 分配**未初始化的内存**,每个元素都具有未定义的值 661 | - 但你可能试来试去发现都是 `0`?这是巧合吗? 662 | - `calloc` 会将分配的内存的每个字节都初始化为零。 663 | - `free` 释放动态分配的内存。在调用 `free(ptr)` 后,`ptr` 具有**未定义的值**(dangling pointer) 664 | 665 | --- 666 | 667 | ## `malloc`, `calloc` 和 `free` 668 | 669 | `malloc(0)`, `calloc(0, N)` 和 `calloc(N, 0)` 的行为是 **implementation-defined** 670 | 671 | - 有可能不分配内存,返回空指针 672 | - 也有可能分配一定量的内存,返回指向这个内存起始位置的指针 673 | 674 | - 但解引用这个指针是 undefined behavior 675 | - 这个内存同样需要记得 `free`,否则也构成内存泄漏 676 | 677 | --- 678 | 679 | ## 正确使用 `free` 680 | 681 | - 忘记 `free`:内存泄漏,OJ 上无法通过测试。 682 | - `free(ptr)` 的 `ptr` 必须是之前由 `malloc`, `calloc`, `realloc` 或 C11 的 `aligned_alloc` 返回的一个地址 683 | - 必须指向动态分配的内存的**开头**,不可以从中间某个位置开始 `free` 684 | - 会释放由 `ptr` 开头的整片内存,它自有办法知道这片内存有多长 685 | - `free` 一个空指针是**无害的**,不需要额外判断 `ptr != NULL` 686 | - `free` 过后,`ptr` 指向**无效的地址**,不可对其解引用。再次 `free` 这个地址的行为 (double free) 是 undefined behavior。 687 | 688 | --- 689 | 690 | ## 为何需要动态内存? 691 | 692 | - `malloc`, `calloc` 等函数分配的是**堆内存**,而作为局部变量的数组使用的是**栈内存** 693 | - 通常来说栈内存比堆内存小,大对象通常建议分配在堆上 694 | 695 | - 但也不是没办法调整 696 | - 关键:堆上创建的对象不受作用域限制,什么时候 `free` 就什么时候销毁 697 | - 写一个函数 `slice`,接受一个字符串和两个下标 `l`, `r`,返回这个字符串的下标范围为 $[l,r)$ 的子串(切片)? 698 | 699 | --- 700 | 701 | ## 切片 702 | 703 | 写一个函数 `slice`,接受一个字符串和两个下标 `l`, `r`,返回这个字符串的下标范围为 $[l,r)$ 的子串(切片)? 704 | 705 | 706 | ```c 707 | char *slice(const char *str, int l, int r) { 708 | ??? result = ???; 709 | for (int i = l; i < r; ++i) 710 | result[i - l] = str[i]; 711 | result[r - l] = '\0'; 712 | return result; 713 | } 714 | ``` 715 | 716 | 如何存储这个结果? 717 | 718 | --- 719 | 720 | ## 用数组? 721 | 722 | **不要返回局部变量的地址!** 723 | 724 | ```c 725 | char *slice(const char *str, int l, int r) { 726 | char result[100]; 727 | for (int i = l; i < r; ++i) 728 | result[i - l] = str[i]; 729 | result[r - l] = '\0'; 730 | return result; 731 | } 732 | ``` 733 | 734 | `result` 在函数执行完毕时就被销毁了,访问这个地址就是访问无效的内存! 735 | 736 | (而且这个 `100` 真的够大吗?也有待商榷) 737 | 738 | --- 739 | 740 | ## 动态分配内存? 741 | 742 | 似乎唯一可行的方法是 743 | 744 | ```c 745 | char *slice(const char *str, int l, int r) { 746 | char *result = malloc(sizeof(char) * (r - l + 1)); 747 | for (int i = l; i < r; ++i) 748 | result[i - l] = str[i]; 749 | result[r - l] = '\0'; 750 | return result; 751 | } 752 | ``` 753 | 754 | 但这样的代码极易引发内存泄漏:**用户必须自己记得释放这个内存** 755 | 756 | --- 757 | 758 | ## 动态分配内存? 759 | 760 | ```c 761 | /* read the file mode in octal */ 762 | param = getfield(tf); 763 | mode = cvlong(param, strlen(param), 8); 764 | 765 | /* read the user id */ 766 | uid = numuid(getfield(tf)); 767 | 768 | /* read the group id */ 769 | gid = numgid(getfield(tf)); 770 | 771 | /* read the file name (path) */ 772 | path = transname(getfield(tf)); 773 | 774 | /* insist on end of line */ 775 | geteol(tf); 776 | ``` 777 | 778 | 如果程序中充斥着这些默默分配内存的函数,内存泄漏将取得彻头彻尾的胜利! 779 | 780 | --- 781 | 782 | ## 问题 783 | 784 | ```c 785 | char *slice(const char *str, int l, int r) { 786 | char *result = malloc(sizeof(char) * (r - l + 1)); 787 | for (int i = l; i < r; ++i) 788 | result[i - l] = str[i]; 789 | result[r - l] = '\0'; 790 | return result; 791 | } 792 | ``` 793 | 794 | 根本的问题是:**内存的分配和释放不由同一个人完成** 795 | 796 | 有没有一种叫做“字符串”的东西来自己管理好这个内存? 797 | 798 | --- 799 | 800 | ## 问题 801 | 802 | 根本的问题是:**内存的分配和释放不由同一个人完成** 803 | 804 | 有没有一种叫做“字符串”的东西来自己管理好这个内存? 805 | 806 | - 上个世纪人们已经把这个问题讨论完了: 807 | 808 | 《C Traps and Pitfalls》《Ruminations on C++》 809 | 810 | C 标准库的解决方案:干脆让用户自己创建存放结果的内存,把这个地址传进来 811 | 812 | - ```c 813 | char *strcpy(char *restrict dest, const char *restrict source); 814 | ``` -------------------------------------------------------------------------------- /r5/img/yangge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r5/img/yangge.png -------------------------------------------------------------------------------- /r5/img/yanglp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r5/img/yanglp.jpg -------------------------------------------------------------------------------- /r5/r5.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 5 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | # Contents 13 | 14 | Homework 2 讲解,如何 debug,如何写出容易 debug、容易维护的代码。 15 | 16 | --- 17 | 18 | ## 1. Happy Coding 19 | 20 | 输入 $n$ 个数,倒序输出所有正数的平方 21 | 22 | --- 23 | 24 | ## 1. Happy Coding 25 | 26 | 输入 $n$ 个数,倒序输出所有正数的平方 27 | 28 | **注意数据范围** 29 | 30 | - `int` 的范围是多大?`long` 呢?`long long` 呢? 31 | - `long long result = ival * ival;` 会溢出吗?溢出的后果是什么? 32 | 33 | --- 34 | 35 | ## 1. Happy Coding 36 | 37 | 输入 $n$ 个数,倒序输出所有正数的平方 38 | 39 | **注意数据范围** 40 | 41 | - `int` 的范围是多大?`long` 呢?`long long` 呢? 42 | - `sizeof(int) >= 2`,通常是 `4`。 43 | - `sizeof(long) >= 4`,通常是 `4` 或者 `8`。 44 | - `sizeof(long long) >= 8`,通常是 `8`。 45 | - `sizeof(int) <= sizeof(long) <= sizeof(long long)` 46 | - `long long result = ival * ival;` 会溢出吗?溢出的后果是什么? 47 | - **会**:计算 `ival * ival` 时已经溢出。带符号整数溢出是 **undefined behavior** 48 | 49 | --- 50 | 51 | ## 1. Happy Coding 52 | 53 | 输入 $n$ 个数,倒序输出所有正数的平方 54 | 55 | 开数组之前问一下你自己:真的需要开这个数组吗? 56 | 57 | ```c 58 | for (int i = 0; i != n; ++i) 59 | scanf("%d", &a[i]); 60 | int cnt = 0; 61 | for (int i = n - 1; i >= 0; --i) 62 | if (a[i] > 0) { 63 | printf("%lld\n", 1ll * a[i] * a[i]); 64 | ++cnt; 65 | } 66 | printf("%d\n", cnt); 67 | ``` 68 | 69 | --- 70 | 71 | ## 2. Quadratic Equation 72 | 73 | 输入一元方程 $ax^2+bx+c=0$ 的三个系数,按要求输出方程的解。 74 | 75 | - (本应该)唯一的坑点:输出 `x\in\mathbb{R}` 时,`'\'` 需要转义 76 | - 实际出现的坑点:如何判断两个根谁大谁小? 77 | - 初中老师流下了伤心的泪水 78 | 79 | 注意:题目保证了输入的 $a$, $b$, $c$ 都是**整数**。**不要随便用浮点数代替整数** 80 | 81 | --- 82 | 83 | ## 3. Hexadecimal Calculator 84 | 85 | 输入两个十六进制数,以及一个运算(加或减),列竖式计算其结果。 86 | 87 | **真正的难题,所以一会儿再说** 88 | 89 | --- 90 | 91 | ## 4. Bit Operation 92 | 93 | 将一个整数 $x$ 拆分成末 $m+1$ 位和剩下的部分,交换后拼成一个新的数,并计算最右边的 $1$ 是第几位 94 | 95 | **完全不需要用数组的题,但大家都在开数组** 96 | 97 | --- 98 | 99 | ## 4. Bit Operation 100 | 101 | 第一个问题:“进制”(和第三题也有关) 102 | 103 | - “数”有“进制”吗? 104 | - `int` 是几进制数? 105 | - $42$ 是几进制数?$101010_{\text{two}}$ 是几进制数? 106 | - 设 $x\in\mathbb R$。$x$ 是几进制数?$x^2$ 是几进制数? 107 | 108 | --- 109 | 110 | ## 4. Bit Operation 111 | 112 | **“数”没有“进制”的属性**,只有当它被读或被写出来时才有。 113 | 114 | - 计算机中一切都是二进制,人脑中的计算常用十进制 115 | - 但这不妨碍计算机算出人脑认可的值。 116 | - 运算的定义、结果都不受进制的影响,受影响的是计算的方式 117 | 118 | --- 119 | 120 | ## 位运算 121 | 122 | | 按位与 | 按位或 | 按位异或 | 左移 | 右移 | 位求反 | 123 | | ------- | -------- | -------- | -------- | -------- | ------ | 124 | | `a & b` | `a \| b` | `a ^ b` | `a << i` | `a >> i` | `~a` | 125 | 126 | 以及复合赋值运算符 `a &= b`, `a |= b`, `a ^= b`, `a <<= i`, `a >>= i` 127 | 128 | - `a & b` 的第 $i$ 位是 `1` 当且仅当 `a` 和 `b` 的第 $i$ 位都是 `1`。 129 | - `a | b` 的第 $i$ 位是 `1` 当且仅当 `a` 和 `b` 的第 $i$ 位至少有一个是 `1`。 130 | - `a ^ b` 的第 $i$ 位是 `1` 当且仅当 `a` 和 `b` 的第 $i$ 位不同。 131 | - `~a` 的第 $i$ 位是 `1` 当且仅当 `a` 的第 $i$ 位是 `0`。 132 | 133 | --- 134 | 135 | ## 位运算 136 | 137 | | 按位与 | 按位或 | 按位异或 | 左移 | 右移 | 位求反 | 138 | | ------- | -------- | -------- | -------- | -------- | ------ | 139 | | `a & b` | `a \| b` | `a ^ b` | `a << i` | `a >> i` | `~a` | 140 | 141 | 以及复合赋值运算符 `a &= b`, `a |= b`, `a ^= b`, `a <<= i`, `a >>= i` 142 | 143 | - `a << i` 将 `a` 整体往左移 $i$ 位,左边的位(高位)被去掉,右边多出来的位为 `0` 144 | - `a >> i` 将 `a` 整体往右移 $i$ 位,右边的位(低位)被去掉,左边多出来的位为 ? 145 | 146 | - 暂时只考虑非负整数的情形,右移时左边多出来的位为 `0`。 147 | - logical shift vs. arithmetic shift 148 | 149 | --- 150 | 151 | ## 位运算 152 | 153 | 暂时只考虑非负整数的情形 154 | 155 | - `x * 2`:`x << 1` 156 | - `x / 2`:`x >> 1` 157 | - $2^i$:`1 << i`,但这是 `int`。如果超出 `int` 范围,可以 `1L << i` 或 `1LL << i` 158 | - **不要用 `pow`!`pow` 是浮点数函数,精度有限** 159 | - 试一试:$10^{20}+1-10^{20}$ 和 $10^{20}-10^{20}+1$ 160 | - `x % 2`:`x & 1` 161 | 162 | --- 163 | 164 | ## 4. Bit Operation 165 | 166 | 将一个整数 $x$ 拆分成末 $m+1$ 位($p$)和剩下的部分($q$),交换后拼成一个新的数 $y$,并计算最右边的 $1$ 是第几位 167 | 168 | - `q = x >> (m + 1);` 169 | - 如何获得 `p`? 170 | 171 | --- 172 | 173 | ## 4. Bit Operation 174 | 175 | 将一个整数 $x$ 拆分成末 $m+1$ 位($p$)和剩下的部分($q$),交换后拼成一个新的数 $y$,并计算最右边的 $1$ 是第几位 176 | 177 | - `q = x >> (m + 1)` 178 | - 如何获得 `p`? 179 | - `p = x - (q << (m +1))` 180 | - 另一种方法:构造一个数 $00\cdots 011\cdots 1_{\text{two}}$(末 $m+1$ 位为 `1`),然后和 $x$ 按位与一下 181 | - `p = x & ((1LL << (m + 1)) - 1)` 182 | 183 | --- 184 | 185 | ## 4. Bit Operation 186 | 187 | 将一个整数 $x$ 拆分成末 $m+1$ 位($p$)和剩下的部分($q$),交换后拼成一个新的数 $y$,并计算最右边的 $1$ 是第几位 188 | 189 | - 假设 `q` 有 `L` 位,那么拼接出来的新数就是 `y = (p << L) + q` 190 | - `p << L` 把 `q` 的位置腾出来,然后加上 `q` 就行 191 | - 需要计算一个数的二进制表示有多少位:不断除以 `2`(不断右移 `1`),直到得到 `0` 为止。 192 | 193 | --- 194 | 195 | ## 4. Bit Operation 196 | 197 | 将一个整数 $x$ 拆分成末 $m+1$ 位($p$)和剩下的部分($q$),交换后拼成一个新的数 $y$,并计算最右边的 $1$ 是第几位 198 | 199 | - 计算最右边的 $1$ 是第几位:从小到大枚举 `i`,判断第 `i` 位是不是 `1`。 200 | - 看 `y & (1LL << i)` 是否非零,或者看 `(y >> i) & 1` 是否为 `1`。 201 | 202 | --- 203 | 204 | ## 5. No-Horse Sudoku 205 | 206 | 给一个数独局面,判断是否合法。需要检查行、列、宫、八个马。 207 | 208 | **输入一个二维数组** 209 | 210 | ```c 211 | for (int i = 0; i != 9; ++i) 212 | for (int j = 0; j != 9; ++j) 213 | scanf("%d", &board[i][j]); 214 | ``` 215 | 216 | **`"%d"` 会跳过前导的空白!!!!!不要再写愚蠢的 `'\n'`、`' '` 了!!!** 217 | 218 | --- 219 | 220 | ## 5. No-Horse Sudoku 221 | 222 | 给一个数独局面,判断是否合法。需要检查行、列、宫、八个马。 223 | 224 | **输入一个二维数组** 225 | 226 | “会循环,但不完全会”型: 227 | 228 | ```c 229 | for (int i = 0; i != 9; ++i) 230 | scanf("%d %d %d %d %d %d %d %d %d", 231 | &board[i][0], &board[i][1], &board[i][2], 232 | &board[i][3], &board[i][4], &board[i][5], 233 | &board[i][6], &board[i][7], &board[i][8]); 234 | ``` 235 | 236 | --- 237 | 238 | ## 5. No-Horse Sudoku 239 | 240 | 给一个数独局面,判断是否合法。需要检查行、列、宫、八个马。 241 | 242 | 框架:枚举每个位置,判断单个位置是否合法 243 | 244 | ```c 245 | int main(void) { 246 | // ... input 247 | for (int i = 0; i != 9; ++i) 248 | for (int j = 0; j != 9; ++j) 249 | if (!checkOneNumber(board, i, j)) { 250 | puts("0"); // 不需要 printf("%d", 0); 251 | // 更不需要 int x = 0; printf("%d", x); 252 | return 0; 253 | } 254 | puts("1"); 255 | return 0; 256 | } 257 | ``` 258 | 259 | --- 260 | 261 | ## 5. No-Horse Sudoku 262 | 263 | 检查一个位置是否合法: 264 | 265 | ```c 266 | bool checkOneNumber(int board[9][9], int row, int col) { 267 | return checkRow(board, row, col) 268 | && checkCol(board, row, col) 269 | && checkPalace(board, row, col) 270 | && checkHorses(board, row, col); 271 | } 272 | ``` 273 | 274 | 我的代码不需要注释了? 275 | 276 | --- 277 | 278 | ## 5. No-Horse Sudoku 279 | 280 | 检查行、列:直接枚举这一行/列判断就行 281 | 282 | ```c 283 | bool checkRow(int board[9][9], int row, int col) { 284 | for (int c = 0; c != 9; ++c) 285 | if (c != col && board[row][col] == board[row][c]) 286 | return false; 287 | return true; 288 | } 289 | ``` 290 | 291 | --- 292 | 293 | ## 5. No-Horse Sudoku 294 | 295 | 检查宫:先计算出这个宫的左上角的坐标,然后枚举偏移量 296 | 297 | ```c 298 | bool checkPalace(int board[9][9], int row, int col) { 299 | int rStart = row / 3 * 3, cStart = col / 3 * 3; 300 | for (int i = 0; i != 3; ++i) 301 | for (int j = 0; j != 3; ++j) { 302 | int r = rStart + i, c = cStart + j; 303 | if ((r != row || c != col) && board[row][col] == board[r][c]) 304 | return false; 305 | } 306 | return true; 307 | } 308 | ``` 309 | 310 | `row / 3 * 3, col / 3 * 3` 这个计算太难想啦,但也没有超过小学二年级难度。 311 | 312 | --- 313 | 314 | ## 5. No-Horse Sudoku 315 | 316 | 检查马:关键是如何以优雅的方式枚举八个马的位置。 317 | 318 | ```c 319 | bool checkHorses(int board[9][9], int row, int col) { 320 | if (board[row - 1][col - 2] == board[row][col]) 321 | return false; 322 | if (board[row - 2][col - 1] == board[row][col]) 323 | return false; 324 | if (board[row - 2][col + 1] == board[row][col]) 325 | return false; 326 | // ... 327 | } 328 | ``` 329 | 330 | 冗长,重复,而且你还忘记判断越界了。 331 | 332 | --- 333 | 334 | ## 5. No-Horse Sudoku 335 | 336 | 检查马:仍然考虑**偏移量** 337 | 338 | - 别说 8 个马,再复杂的规则也一样 339 | 340 | ```c 341 | bool checkHorses(int board[9][9], int row, int col) { 342 | static const int dr[] = {-1, -2, -2, -1, 1, 2, 2, 1}; 343 | static const int dc[] = {-2, -1, 1, 2, 2, 1, -1, -2}; 344 | for (int i = 0; i != 8; ++i) { 345 | int r = row + dr[i], c = col + dc[i]; 346 | if (validCoord(r, c) && board[row][col] == board[r][c]) 347 | return false; 348 | } 349 | return true; 350 | } 351 | ``` 352 | 353 | `validCoord` 是啥?你急什么,又不是写不出来 354 | 355 | --- 356 | 357 | ## 5. No-Horse Sudoku 358 | 359 | - 检查一个元素是否合法的关键:**以合理、优雅的方式 `for` 出所有关键的位置** 360 | - 学会写清晰的、简洁的、self-documenting 的代码: 361 | 362 | ```c 363 | return checkRow(board, row, col) && checkCol(board, row, col) 364 | && checkPalace(board, row, col) && checkHorses(board, row, col); 365 | ``` 366 | 如果这是一个组队任务,接下来你们就可以分工了 367 | 368 | --- 369 | 370 | ## 5. No-Horse Sudoku 371 | 372 | 开数组、拷贝数据、开内存这种事,**三思而后行**。 373 | - 你真的需要开这个数组吗?真的需要把所有的东西都记下来才好干活吗?(hw3/4) 374 | - 什么时候你需要拷贝一笔数据? 375 | - 不仅影响效率,也会增加代码的复杂性,也增加你命名的难度 376 | - 代码中同时存在 `name`, `Name`, `name1`, `name_1`, `name2`, `name_2` 这样的东西,将大大增加你 debug 的难度 377 | 378 | --- 379 | 380 | ## 3. Hexadecimal Calculator 381 | 382 | 输入两个十六进制数,以及一个运算(加或减),列竖式计算其结果。 383 | 384 | **数据范围**:50 位十六进制数,是多大? 385 | 386 | --- 387 | 388 | ## 3. Hexadecimal Calculator 389 | 390 | 输入 391 | 392 | - 可以以字符串的方式读入,也可以手动一个字符一个字符读 393 | - 读进来可以存成字符,也可以存对应的十六进制数值 394 | - **如果是字符串,记得给 `'\0'` 留位置** 395 | 396 | --- 397 | 398 | ## 3. Hexadecimal Calculator 399 | 400 | 数位对齐 401 | 402 | - 可以给短的数前面补零,也可以把两个数都反过来 403 | - 无论哪一种方法,都会出现重复的代码。**提出来作为一个函数**。 404 | 405 | --- 406 | 407 | ## 3. Hexadecimal Calculator 408 | 409 | 计算加减法 410 | 411 | - 一位一位算,可以同时处理进退位,也可以先算完再统一处理 412 | - **避免重复**! 413 | 414 | ```c 415 | for (int i = 0; i != length; ++i) 416 | result[i] = to_int(a[i]) + to_int(b[i]); 417 | ``` 418 | 419 | - 可以是 `to_int(a[i])`,也可以是 `to_int[a[i]]`,但后者比前者麻烦一点。 420 | 421 | ```c 422 | int to_int(char c) { 423 | if (c <= '9') 424 | return c - '0'; 425 | else 426 | return c - 'a' + 10; 427 | } 428 | ``` 429 | 430 | --- 431 | 432 | ## 3. Hexadecimal Calculator 433 | 434 | ```c 435 | for (int i = 0; i != length; ++i) 436 | result[i] = to_int(a[i]) + to_int(b[i]); 437 | ``` 438 | 439 | 不好的写法: 440 | 441 | ```c 442 | for (int i = 0; i != length; ++i) { 443 | int x, y; 444 | if (a[i] <= '9') 445 | x = a[i] - '0'; 446 | else 447 | x = a[i] - 'a' + 10; 448 | if (b[i] <= '9') 449 | y = b[i] - '0'; 450 | else 451 | y = b[i] - 'a' + 10; 452 | result[i] = x + y; 453 | } 454 | ``` 455 | 456 | --- 457 | 458 | ## 3. Hexadecimal Calculator 459 | 460 | 完全没学过 ASCII 的写法: 461 | 462 | ```c 463 | for (int i = 0; i != length; ++i) { 464 | int x, y; 465 | if (a[i] == '0') x = 0; 466 | if (a[i] == '1') x = 1; 467 | // ... 468 | if (a[i] == 'a') x = 10; 469 | if (a[i] == 'b') x = 11; 470 | if (a[i] == 'c') x = 12; 471 | // ... 472 | if (b[i] == '0') y = 0; 473 | if (b[i] == '1') y = 1; 474 | // ... 475 | if (b[i] == 'a') y = 10; 476 | if (b[i] == 'b') y = 11; 477 | if (b[i] == 'c') y = 12; 478 | // ... 479 | result[i] = x + y; 480 | } 481 | ``` 482 | 483 | --- 484 | 485 | ## 3. Hexadecimal Calculator 486 | 487 | 输出 488 | 489 | - 字符串**直接用 `puts(s)` 或 `printf("%s", s)`!!不要一个字符一个字符往外蹦!** 490 | - 要从第 `i` 位开始输出,只要 `puts(s + i)` 或 `printf("%s", s + i)` 491 | - 如果你需要倒着输出... 那只能自己写了 492 | - 减法注意去掉结果中的前导 `0`,但也有可能这个数就是 `0`,注意特判。 493 | 494 | --- 495 | 496 | ## 如何写出好的代码? 497 | 498 | 1. 先想清楚整体的流程。 499 | 2. 对于细节和重复的部分,提出来写成函数 500 | - 在想流程的时候,只考虑这些函数“需要什么”、“做什么事”、“返回什么” 501 | 3. 善用 `assert`:**防御性编程** 502 | - 用 `assert` 将一些必要的假设、约定写进代码里,一旦违反就有报告。 503 | - 在开头 `#define NDEBUG` 或编译时 `-DNDEBUG` 就可以关闭所有 `assert`,防止 `assert` 影响最终的程序的效率。 504 | 4. **避免重复!!** 505 | 506 | --- 507 | 508 | ## 善用 `assert` 509 | 510 | `#include ` 511 | 512 | - 将你想象中的应该成立的条件打在公屏上 513 | 514 | ```c 515 | void add_leading_zeros(char *str, int num_of_zeros) { 516 | int len = strlen(str); 517 | for (int i = len; i >= 0; --i) 518 | str[i + num_of_zeros] = str[i]; 519 | for (int i = 0; i != num_of_zeros; ++i) 520 | str[i] = '0'; 521 | assert(strlen(str) == len + num_of_zeros); 522 | } 523 | ``` 524 | 525 | --- 526 | 527 | ## 善用 `assert` 528 | 529 | ```c 530 | int to_int(char c) { 531 | assert(isxdigit(c) && !isupper(c)); 532 | if (isdigit(c)) 533 | return c - '0'; 534 | else 535 | return c - 'a' + 10; 536 | } 537 | char to_hexchar(int x) { 538 | assert(x >= 0 && x <= 15); 539 | if (x <= 9) 540 | return x + '0'; 541 | else 542 | return x - 10 + 'a'; 543 | } 544 | ``` 545 | 546 | --- 547 | 548 | ## 善用 `assert` 549 | 550 | ```c 551 | char to_hexchar(int x) { 552 | assert(x >= 0 && x <= 15); 553 | if (x <= 9) 554 | return x + '0'; 555 | else 556 | return x - 10 + 'a'; 557 | } 558 | void print_result(int *result, int length) { 559 | assert(length >= 1); 560 | assert(length <= len_a && length <= len_b); 561 | for (int i = 0; i != length; ++i) 562 | putchar(to_hexchar(result[i])); 563 | puts(""); 564 | } 565 | ``` 566 | 567 | --- 568 | 569 | ## 善用 `assert` 570 | 571 | ```c 572 | assert(max_len >= 1); 573 | // 处理进位 574 | for (int i = 0; i < max_len - 1; ++i) { 575 | if (result[i] >= 16) { 576 | ++result[i + 1]; 577 | result[i] -= 16; 578 | assert(result[i] >= 0 && result[i] <= 15); 579 | } 580 | } 581 | // 最高位不会进位 582 | assert(result[max_len - 1] >= 0 && result[max_len - 1] <= 15); 583 | ``` 584 | 585 | --- 586 | 587 | ## 如何写出好的代码? 588 | 589 | 程序的 bug 大致分为两类: 590 | 591 | - 没想好就乱写 592 | - 想的和写的不同义 593 | 594 | `assert`、`const` 等特性存在的意义之一:让程序自动检查一些假设、约定是否成立。 595 | 596 | --- 597 | 598 | ## 充分了解你用的每个东西 599 | 600 | - `long long` 的范围到底是多大? 601 | - `char` 的范围是多大? 602 | - `atoi` 跳不跳过空白?在遇到错误时会怎样? 603 | - `scanf` 读入时到底会不会跳过空白? 604 | - `'\0'` 和 `0` 一样吗? 605 | - `char str[50] = "\0";` 是在干什么?`char str[50] = {'0'};` 是在干什么? 606 | - `"hello"` 的类型是什么? 607 | 608 | --- 609 | 610 | ## 充分了解你用的每个东西 611 | 612 | - `fgets` 至多读多少个字符?读不读换行?存不存换行?会不会放空字符? 613 | - `scanf("%s", ...)` 的行为?`scanf("%c", ...)` 的行为? 614 | - `pow` 的参数和返回值是什么类型? 615 | - 上网查来的代码 `while ((ch = getchar()) != EOF && ch != '\n') {}` 是什么意思? 616 | 617 | --- 618 | 619 | ## 常见的误区 620 | 621 | - “能跑就行,规范不规范回头再说” 622 | - “XXX 是大佬的做法,我只要用 YYY 就好了” 623 | - “我先用一个笨办法让它跑起来,这样比较稳妥” 624 | - 迷信玄学:“虽然题目保证了结果非负,但考虑负数会多过几个点” 625 | 626 | 有时候不是你 debug 能力不行,而是**你写的那个代码没法 debug** 627 | 628 | - 不要畏惧新的知识、更好的方法:**学习就是不断推翻自己的过程** 629 | 630 | --- 631 | 632 | ## Debug 和测试 633 | 634 | > 机器永远是对的,未经测试的程序永远是错的。——南京大学 JYY 635 | 636 | 第一步:解决所有的编译错误和 warning 637 | 638 | - 虽然 C/C++ 的某些编译报错难以理解(可能是由于历史遗留问题),但编译器给出的信息仍然是第一线索。 639 | - 遇到一长串报错时,**首先解决最上面的那条**通常是有效的办法。 640 | - 编译器一旦遇到了一个错误,后面可能就不知所措了 641 | - 理解编译器的报错,不要急着上网查 642 | 643 | --- 644 | 645 | ## Debug 和测试 646 | 647 | 理解编译器的报错,不要急着上网查 648 | 649 | ```python 650 | ---> initial = np.random.rand((num, 2)) 651 | TypeError: `tuple` object cannot be interpreted as an integer 652 | ``` 653 | 654 | > “这个咋办?我查了下是说矩阵不能这么算,但我寻思之前我写的也是矩阵啊?” 655 | > “但我其实是想搞个 num * 2 的矩阵” 656 | > “所以是写随机矩阵这个写法也不对?” 657 | 658 | --- 659 | 660 | ## Debug 和测试 661 | 662 | > 计算机的世界没有魔法。——南京大学 JYY 663 | 664 | 遇到问题应该解决问题,而不是解决提出问题的人(产生问题的东西)。 665 | 666 | > ——“这里写这么麻烦干什么?你为什么不用 XXX?” 667 | > ——“因为改用 XXX 之后会报错” 668 | > ——“报的什么错?” 669 | > ——“不知道” 670 | > ——“然后你就改成这种写法了?” 671 | > ——“嗯...” 672 | 673 | --- 674 | 675 | ## Debug 和测试 676 | 677 | > 计算机的世界没有魔法。——南京大学 JYY 678 | 679 | “凭经验猜,凭直觉猜,凭意大利纸牌猜,或者请碟仙” 680 | 681 | > “我现在的代码用了 malloc 和 calloc 但是都有 free” 682 | > “然后还用了个 fgets” 683 | > “fgets 里有个 stdin” 684 | > “会不会是这个 stdin 的问题” 685 | 686 | --- 687 | 688 | ## 测试 689 | 690 | 测试的关键是**造数据**: 691 | 692 | - 仔细考虑可能的 case,尝试构造出让你的程序出错的数据 693 | - `1+1`、`aaaaa-aaaaa`、`aaaaa-a0000` 这样的数据太弱了 694 | - 可能出错的点:进位、退位、结果为零、长度达到上界 50 等。 695 | - 找出一个有效的数据后,就可以开始 debug 了。 696 | 697 | 批量生成测试数据?通常用 Python 或者 Shell Script,虽然 C/C++ 也不是不行。 698 | 699 | --- 700 | 701 | ## Debug 702 | 703 | 抓着一个有效的数据开始调试 704 | 705 | - 将程序大致分为几个部分(最好在写的时候就想清楚这一点),一步一步检查每个部分的功能是否正常 706 | - 可以通过 debugger 或者 print-statement 的方式检查 707 | - print-statement 虽然原始,但也是十分有效的方法(我整个竞赛生涯都是这样 debug 的) 708 | - 以后你们会真正需要更高级的 debug 功能,以后再说 709 | 710 | --- 711 | 712 | ## Debug 713 | 714 | 其实 debug 没有什么捷径/魔法 715 | 716 | - 瞪眼法/小黄鸭 717 | - print-statement 718 | - 使用工具:GDB, strace, sanitizer, valgrind, Visual Studio 719 | 720 | 当年我们竞赛的教练这样讲,[CppCon2022 的 talk](https://www.bilibili.com/video/BV1FB4y1H7VS?p=6&vd_source=7940495b5667750a71bfa10a4c6eb2d9) 也这样讲 721 | 722 | --- 723 | 724 | 725 | 726 | 727 | 728 | --- 729 | 730 | 本可以通过瞪眼法看出的错误 731 | 732 | 733 | 734 | 735 | 736 | 但如果你不知道字符串末尾有 `'\0'`,就没办法了。 -------------------------------------------------------------------------------- /r6/r6.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitations 6 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - Homework 3 讲评 15 | - `struct` 16 | - C++ 的开始 17 | - 初识 `iostream` 和 `std::string` 18 | 19 | --- 20 | 21 | # Homework 3 22 | 23 | --- 24 | 25 | ## 你真的了解 `fgets` 吗? 26 | 27 | - `fgets` 至多读取输入中的几个字符? 28 | - `fgets` 碰到换行符时会终止,换行符读了没有?如果读了,存了没有? 29 | - `fgets` 会在末尾放 `'\0'` 吗? 30 | 31 | --- 32 | 33 | ## 你真的了解 `fgets` 吗? 34 | 35 | - `fgets` 至多读取输入中的几个字符? 36 | - `fgets` 碰到换行符时会终止,换行符读了没有?如果读了,存了没有? 37 | - `fgets` 会在末尾放 `'\0'` 吗? 38 | 39 | [答案在这里](https://en.cppreference.com/w/c/io/fgets) 40 | 41 | **如果你只是道听途说 “`fgets` 可以读一行” 就在代码里用它,这就是通向彻夜调试大会的直达车票。** 42 | 43 | --- 44 | 45 | ## 1. 简单题 46 | 47 | 用 ASCII 码循环移动一下就好了。 48 | 49 | **不需要存输入的字符串,读一个处理一个就行** 50 | 51 | 不会 [ASCII 码](https://en.cppreference.com/w/cpp/language/ascii)?你将无法做 HW4 52 | 53 | --- 54 | 55 | ## 2. 规则说起来有点麻烦 56 | 57 | 给一个 keyword(例如 `Wednesday`),首先制作一个表,分为以下几步: 58 | 1. 去重,填入前几位:`wednsay` 59 | 2. 从最后一个字母(`y`)开始顺着往后接,跳过前面出现过的字母,到 `z` 则循环:`wednsayzbcfghijklmopqrtuvx` 60 | 3. 将以上内容视为一个数组 `char encoding[26];`,`encoding[i]` 对应了 `i + 'a'`(或者 `i + 'A'`),按照这个对应关系处理输入的每个字符。 61 | 62 | --- 63 | 64 | ## 去重,填入前几位 65 | 66 | ```c 67 | int len_keyword = strlen(keyword); 68 | bool used[26] = {0}; 69 | int len_encoding = 0; 70 | for (int i = 0; i != len_keyword; ++i) { 71 | char ch = tolower(keyword[i]); 72 | if (!used[ch - 'a']) { 73 | used[ch - 'a'] = true; 74 | encoding[len_encoding++] = ch; 75 | } 76 | } 77 | assert(len_encoding < 26); 78 | ``` 79 | 80 | --- 81 | 82 | ## 接着往后填 83 | 84 | 继续使用 `used` 数组 85 | 86 | ```c 87 | char ch = encoding[len_encoding - 1]; 88 | while (len_encoding < 26) { 89 | while (used[ch - 'a']) 90 | ch = (ch == 'z') ? 'a' : (ch + 1); 91 | used[ch - 'a'] = true; 92 | encoding[len_encoding++] = ch; 93 | } 94 | ``` 95 | 96 | --- 97 | 98 | ## 处理输入 99 | 100 | 密文根本不用存,来一个走一个(没人规定读完所有输入才能开始输出) 101 | 102 | ```c 103 | char ch; 104 | while ((ch = getchar()) != EOF && ch != '\n') 105 | putchar(decode(ch)); 106 | ``` 107 | 108 | 不要看到一串文本,就想到字符串,就想到 `fgets` 读入,就想到开数组存储。~~你们的想象惟在这一层能够如此跃进~~ 109 | 110 | --- 111 | 112 | ## decode 113 | 114 | 暴力:直接在 `encoding` 数组里找 115 | 116 | ```c 117 | char decode(char ch) { 118 | if (isalpha(ch)) { 119 | int pos = -1; 120 | for (int i = 0; i != 26; ++i) 121 | if (encoding[i] == ch) { 122 | pos = i; 123 | break; 124 | } 125 | assert(pos != -1); 126 | return pos + 'a'; 127 | } else 128 | return ch; 129 | } 130 | ``` 131 | 132 | --- 133 | 134 | ## decode 135 | 136 | 大写与小写仅有一步之遥,没有本质区别。 137 | 138 | ```c 139 | char decode(char ch) { 140 | if (isalpha(ch)) { 141 | bool is_upper = isupper(ch); 142 | ch = tolower(ch); 143 | int pos = -1; 144 | for (int i = 0; i != 26; ++i) 145 | if (encoding[i] == ch) { 146 | pos = i; 147 | break; 148 | } 149 | assert(pos != -1); 150 | return pos + (is_upper ? 'A' : 'a'); 151 | } else 152 | return ch; 153 | } 154 | ``` 155 | 156 | --- 157 | 158 | ## 3. [回文日期](https://www.luogu.com.cn/problem/P2010) 加强版 159 | 160 | 一年至多可能有多少个回文日期? 161 | 162 | --- 163 | 164 | ## 4. 找最喜欢的游戏类型 165 | 166 | 每个游戏有一个 price 和一个 type,type 为 $10^6$ 以内的正整数。找出 price 总和最大的那个 type。 167 | 168 | **游戏名字根本没有用**:`scanf` 的 `%` 和 conversion format specifier 之间加个 `*` 就表示只匹配、不存储。 169 | - https://en.cppreference.com/w/c/io/fscanf#Parameters 170 | - 或者用循环 + `getchar` 跳过。 171 | 172 | --- 173 | 174 | ## 4. 找最喜欢的游戏类型 175 | 176 | 每个游戏有一个 price 和一个 type,type 为 $10^6$ 以内的正整数。找出 price 总和最大的那个 type。 177 | 178 | - 开个数组 `long long price_sum_of_type[1000001];` 179 | - 对于每个游戏,`price_sum_of_type[type] += price;` 180 | - 枚举所有可能的 `type`,找出 `price_sum_of_type[type]` 最大的那个。 181 | - 可以 `for (int type = 1; type <= 1000000; ++type)` 182 | - 也可以在读入的时候顺便记下 `type` 的最大值,缩小枚举范围。 183 | 184 | --- 185 | 186 | ## 学会计算内存用量 187 | 188 | ```c 189 | struct Game { 190 | char name[10000001]; 191 | int price; 192 | int type; 193 | }; 194 | 195 | struct Game games[1000000]; 196 | ``` 197 | 198 | 你花了 $\dfrac{10^7\times 10^6}{1024\times 1024\times 1024}\approx 9300$ **GB** 来存储那一堆没有用的名字。 199 | 200 | --- 201 | 202 | ## 5. 二维版 [玩具谜题](https://www.luogu.com.cn/problem/P1563) 203 | 204 | 首先还是那个问题:你真的需要开数组存某个东西吗? 205 | 206 | - 读完了 $r,c,q$、朝向信息以及起始位置之后,就可以读一步、动一步。 207 | - 发现 Mistake 了就直接结束:**没人规定你必须把输入全读完** 208 | 209 | --- 210 | 211 | ## 判断一个位置是否走过 212 | 213 | 和刚才那个 `used` 类似的做法: 214 | 215 | ```c 216 | bool visited[501][501]; 217 | 218 | for (int i = 0; i != q; ++i) { 219 | // 输入这一步的方向、距离 220 | // 算出新的位置 (row, col) 221 | if (valid_coord(row, col) && !visited[row][col]) { 222 | printf("(%d, %d)\n", row, col); 223 | visited[row][col] = true; 224 | } else { 225 | puts("Mistake!"); 226 | break; 227 | } 228 | } 229 | ``` 230 | 231 | --- 232 | 233 | ## 朝向、方向,一共 16 种情况,如何避免重复? 234 | 235 | - 朝向:以“向上”为基准,“向上”、“向右”、“向下”、“向左”分别是顺时针转 $0^\circ$, $90^\circ$, $180^\circ$, $270^\circ$ 236 | - 在我的“前”/“右”/“后”/“左”方:再顺时针转 $0^\circ$ / $90^\circ$ / $180^\circ$ / $270^\circ$ 237 | - 角度加起来对 $360^\circ$ 取模,就是实际转了多少度,对应四种情况。 238 | 239 | --- 240 | 241 | ## 朝向、方向,一共 16 种情况,如何避免重复? 242 | 243 | - 朝向:以“向上”为基准,“向上”、“向右”、“向下”、“向左”分别是顺时针转 $0^\circ$, $90^\circ$, $180^\circ$, $270^\circ$ 244 | - 在我的“前”/“右”/“后”/“左”方:再顺时针转 $0^\circ$ / $90^\circ$ / $180^\circ$ / $270^\circ$ 245 | - 角度加起来对 $360^\circ$ 取模,就是实际转了多少度,对应四种情况。 246 | - 将四个朝向和四个方向都按顺序编码为 `0`, `1`, `2`, `3` 247 | - 只要看 `(direction + face) % 4` 248 | 249 | --- 250 | 251 | ## `valid_coord` 252 | 253 | ```c 254 | bool valid_coord(int row, int col) { 255 | return row >= 1 && row <= r && col >= 1 && col <= c; 256 | } 257 | ``` 258 | 259 | **不要再写啰嗦的 `return condition ? 1 : 0;` 了!!** 260 | 261 | --- 262 | 263 | # `struct` 264 | 265 | --- 266 | 267 | ## `struct` 268 | 269 | 把几个东西结合在一起,定义成一个新的数据结构 270 | 271 |
272 |
273 | 274 | ```c 275 | struct Student { 276 | const char *name; 277 | const char *id; 278 | int entrance_year; 279 | int dorm; 280 | }; 281 | ``` 282 |
283 |
284 | 285 | ```c 286 | struct Record { 287 | void *ptr; 288 | size_t size; 289 | int line_no; 290 | const char *file_name; 291 | }; 292 | ``` 293 |
294 |
295 | 296 | ```c 297 | struct brainfuck_state { 298 | uint8_t *memory_buffer; 299 | size_t offset; 300 | // ... 301 | }; 302 | ``` 303 |
304 |
305 | 306 |
307 |
308 | 309 | ```c 310 | struct Point3d { 311 | double x, y, z; 312 | }; 313 | ``` 314 |
315 |
316 | 317 | ```c 318 | struct Line3d { 319 | struct Point3d p0, direction; 320 | }; 321 | ``` 322 |
323 |
324 | 325 | --- 326 | 327 | ## `struct` 类型 328 | 329 | `struct` + 名字。C 中 `struct` 关键字不可省略,C++ 中必须省略。 330 | 331 | ```c 332 | struct Student student; 333 | struct Record records[1000]; 334 | ``` 335 | 336 | ### `typedef` 定义类型别名 337 | 338 | ```c 339 | typedef long long LL; 340 | typedef struct { double x, y, z; } Point3d; 341 | 342 | LL llval = 0; // llval is long long 343 | Point3d p; 344 | ``` 345 | 346 | **不要用 `#define` 代替 `typedef`** 347 | 348 | --- 349 | 350 | ## `struct` 的成员 351 | 352 | `name.mem` 353 | 354 | ```c 355 | struct Student student; 356 | student.name = "Alice"; 357 | student.id = "2023533000"; 358 | student.entrance_year = 2023; 359 | student.dorm = 8; 360 | printf("%d\n", student.dorm); 361 | ++student.entrance_year; 362 | puts(student.name); 363 | ``` 364 | 365 | --- 366 | 367 | ## `struct` 的成员 368 | 369 | `ptr->mem`:等价于 `(*ptr).name`。**不是 `*ptr.name` !!**(`.` 优先级高于 `*`) 370 | 371 | ```c 372 | struct Student *ptr = &student; 373 | ptr->name = "Alice"; 374 | ptr->id = "2023533000"; 375 | (*ptr).entrance_year = 2023; // equivalent to ptr->entrance_year = 2023; 376 | ptr->dorm = 8; 377 | printf("%d\n", ptr->dorm); 378 | ++ptr->entrance_year; 379 | puts(ptr->name); 380 | ``` 381 | 382 | --- 383 | 384 | ## `struct` 初始化 385 | 386 | 老生常谈的问题:不显式初始化时会发生什么? 387 | 388 | ```c 389 | struct Student gs; 390 | int main(void) { 391 | struct Student ls; 392 | } 393 | ``` 394 | 395 | --- 396 | 397 | ## `struct` 初始化 398 | 399 | 老生常谈的问题:不显式初始化时会发生什么? 400 | 401 | ```c 402 | struct Student gs; 403 | int main(void) { 404 | struct Student ls; 405 | } 406 | ``` 407 | 408 | - 全局或局部 `static`:**空初始化**:结构体的所有成员都被空初始化。 409 | - 局部非 `static`:不初始化,所有成员都具有未定义的值。 410 | 411 | --- 412 | 413 | ## `struct` 的初始化 414 | 415 | Initializer list: 416 | 417 | ```c 418 | struct Record r = {p, cnt * each_size, __LINE__, __FILE__}; 419 | ``` 420 | 421 | 隔壁 C++20 才有的 designators,C99 就有了!(是不是非常像 Python?) 422 | 423 | ```c 424 | struct Record r2 = {.ptr = p, .size = cnt * each_size, 425 | .line_no = __LINE__, .file = __FILE__}; 426 | ``` 427 | 428 | C 允许 designators 以任意顺序出现,C++不允许。 429 | 430 | --- 431 | 432 | ## `struct` 的初始化 433 | 434 | ```c 435 | struct Record r = {p, cnt * each_size, __LINE__, __FILE__}; 436 | struct Record r2 = {.ptr = p, .size = cnt * each_size, 437 | .line_no = __LINE__, .file = __FILE__}; 438 | ``` 439 | 440 | **赋值**不行: 441 | 442 | ```c 443 | r = {p, cnt * each_size, __LINE__, __FILE__}; // Error 444 | records[i] = {.ptr = p, .size = cnt * each_size, 445 | .line_no = __LINE__, .file = __FILE__}; // Error 446 | ``` 447 | 448 | 但加个类型转换就好了: 449 | 450 | ```c 451 | r = (struct Record){p, cnt * each_size, __LINE__, __FILE__}; // OK 452 | records[i] = (struct Record){.ptr = p, .size = cnt * each_size, 453 | .line_no = __LINE__, .file = __FILE__}; // OK 454 | ``` 455 | 456 | --- 457 | 458 | ## 在函数之间传递 `struct` 459 | 460 | 传参的语义是**拷贝**。 461 | 462 | ```c 463 | void print_info(struct Record r) { 464 | printf("%p, %zu, %d, %s\n", r.ptr, r.size, r.line_no, r.file_name); 465 | } 466 | 467 | print_info(records[i]); 468 | ``` 469 | 470 | 传参时发生了这样的**初始化**: 471 | 472 | ```c 473 | struct Record r = records[i]; 474 | ``` 475 | 476 | `struct` 的拷贝:**逐元素拷贝**。 477 | 478 | --- 479 | 480 | ## 在函数之间传递 `struct` 481 | 482 | 传参时发生了这样的**初始化**: 483 | 484 | ```c 485 | struct Record r = records[i]; 486 | ``` 487 | 488 | `struct` 的拷贝:**逐元素拷贝**。就如同 489 | 490 | ```c 491 | r.ptr = records[i].ptr; 492 | r.size = records[i].size; 493 | r.line_no = records[i].line_no; 494 | r.file_name = records[i].file_name; 495 | ``` 496 | 497 | --- 498 | 499 | ## 在函数之间传递 `struct` 500 | 501 | 返回一个 `struct`:严格按照语法来说,也是**拷贝**: 502 | 503 | ```c 504 | struct Record fun(void) { 505 | struct Record r = something(); 506 | some_computations(r); 507 | return r; 508 | } 509 | 510 | records[i] = fun(); 511 | ``` 512 | 513 | `return r;`:发生了形如 `struct Record tmp = r;` 的**拷贝**,临时对象 `tmp` 是表达式 `fun()` 的求值结果。然后发生了形如 `records[i] = tmp;` 的**拷贝**。 514 | 515 | **但实际上这个过程会被编译器优化,标准也是允许这种优化的。**(我们以后在 C++ 里进一步讨论这个问题) 516 | 517 | --- 518 | 519 | ## 数组成员 520 | 521 | ```c 522 | struct A { 523 | int array[10]; 524 | // other members 525 | }; 526 | ``` 527 | 528 | 虽然编译器拒绝直接拷贝数组,但它其实有能力做到。 529 | 530 | 拷贝一个 `struct A` 时,编译器会自动逐元素拷贝数组。 531 | 532 |
533 |
534 | 535 | ```c 536 | int a[10]; 537 | int b[10] = a; // Error! 538 | ``` 539 |
540 |
541 | 542 | ```c 543 | struct A a; 544 | struct A b = a; // OK 545 | ``` 546 |
547 |
548 | 549 | --- 550 | 551 | ## `struct` 的大小 552 | 553 | ```c 554 | struct A { 555 | int x; 556 | char y; 557 | double z; 558 | }; 559 | ``` 560 | 561 | `sizeof(struct A)` 是多少? 562 | 563 | --- 564 | 565 | ## `struct` 的大小 566 | 567 | ```c 568 | struct A { 569 | int x; 570 | char y; 571 | double z; 572 | }; 573 | ``` 574 | 575 | `sizeof(struct A) >= sizeof(int) + sizeof(char) + sizeof(double)`。由于内存对齐的问题,编译器可能会在某些地方插入一定的空白。 576 | 577 | --- 578 | 579 | ## `struct` 的大小 580 | 581 | ```c 582 | struct A { 583 | int x; 584 | struct A a; 585 | }; 586 | ``` 587 | 588 | `sizeof(struct A)` 是多少? 589 | 590 | --- 591 | 592 | ## `struct` 的大小 593 | 594 | ```c 595 | struct A { 596 | int x; 597 | struct A a; 598 | }; 599 | ``` 600 | 601 | `sizeof(struct A)`$=+\infty$。因此这种行为是**不允许的**。 602 | 603 | - 从物理上讲:这样的东西无法存储。 604 | - C 类型系统认为:在 `};` 之前(定义完毕之前)这个类型是**不完全类型** (incomplete type)。对于不完全类型,不能定义这个类型的对象,不能访问这个类型的成员,只能定义这个类型的指针,并且不能解引用。 605 | 606 | 607 | --- 608 | 609 | ### 练习 610 | 611 | 考虑三维空间中的点 $(x,y,z)$ 以及直线 $\mathbf P(t)=\mathbf P_0+t\mathbf v$。定义 `struct` 表示这两个概念。定义一些函数,例如计算点到直线的距离、计算给定参数 $t$ 的值时的点、输出一些信息。试着使用 initializer list 和 designators 进行初始化。 612 | 613 | ```c 614 | double dist(struct Point3d p, struct Line3d line); 615 | struct Point3d line_at(struct Line3d line, double t); 616 | ``` 617 | 618 | --- 619 | 620 | ### 练习 621 | 622 | ```c 623 | struct Point3d { 624 | double x, y, z; 625 | }; 626 | struct Line3d { 627 | struct Point3d p0, v; 628 | }; 629 | struct Point3d line_at(struct Line3d line, double t) { 630 | return (struct Point3d) { 631 | .x = line.p0.x + t * line.v.x, 632 | .y = line.p0.y + t * line.v.y, 633 | .z = line.p0.z + t * line.v.z 634 | }; 635 | } 636 | ``` 637 | 638 | --- 639 | 640 | ## 总结 641 | 642 | 没有总结。独立完成上面的练习即可真正理解 `struct`。 643 | 644 | --- 645 | 646 | # C++ 的开始 647 | 648 | --- 649 | 650 | ## C++ 的开始 651 | 652 | 首先,我们采用 C++17 语言标准。 653 | 654 | - `settings.json`:`code-runner.executorMap` 里 `"cpp"` 项,把 `-std=c++20` 改成 `-std=c++17` 655 | - `c_cpp_properties.json`:`"cppStandard"` 项设置为 `c++17` 656 | 657 | 调试:最简单粗暴的方法是把 `tasks.json` 和 `launch.json` 都删掉,然后调试 C++ 程序,VSCode 会自动生成。 658 | - 调试 C++ 时要选 `g++.exe`。 659 | 660 | --- 661 | 662 | ## Hello C++ World 663 | 664 | ```cpp 665 | #include 666 | 667 | int main() { 668 | std::cout << "Hello C++ World\n"; 669 | return 0; 670 | } 671 | ``` 672 | 673 | --- 674 | 675 | ## `iostream` 676 | 677 | ```cpp 678 | #include 679 | 680 | int main() { 681 | int a, b; 682 | std::cin >> a >> b; 683 | std::cout << a + b << '\n'; 684 | return 0; 685 | } 686 | ``` 687 | 688 | `std::cin` 和 `std::cout` 是定义在 `` 中的两个**对象**,分别表示标准输入流和标准输出流。 689 | 690 | --- 691 | 692 | ## `iostream` 693 | 694 | `std::cin >> x` 输入一个东西给 `x`。 695 | - `x` 可以是任何受支持的类型,例如整数、浮点数、字符、字符串。 696 | - **C++ 有能力识别 `x` 的类型并选择正确的方式输入,不需要丑陋的 "%d"、"%c" 来告诉它。** 697 | - C++ 有办法获得 `x` 的**引用**,因此不需要取 `x` 的地址。 698 | - 表达式 `std::cin >> x` 执行完毕后会把 `std::cin` 返回出来,所以可以连着写:`std::cin >> x >> y >> z` 699 | 700 | 输出也是一样的:`std::cout << x << y << z` 701 | 702 | --- 703 | 704 | ## `iostream` 705 | 706 | `std::cout << std::endl;` 707 | 708 | - `std::endl` 是一个“操纵符”。`std::cout << std::endl` 的含义是**输出换行符,并刷新输出缓冲区**。 709 | 710 | 如果你不手动刷新缓冲区,`std::cout` 自有规则在特定情况下刷新缓冲区。(C `stdout` 也是一样) 711 | 712 | --- 713 | 714 | ## namespace `std` 715 | 716 | C++ 有一套非常庞大的标准库,为了避免名字冲突,所有的名字(函数、类、类型别名、模板、全局对象等)都在一个名为 `std` 的**命名空间**下。 717 | 718 | - 你可以用 `using std::cin;` 将 `cin` 引入当前作用域,那么在当前作用域内就可以省略 `std::cin` 的 `std::`。 719 | - 你可以用 `using namespace std;` 将 `std` 中的所有名字都引入当前作用域,**但这将使得命名空间形同虚设,并且重新引入了名字冲突的风险**。(我个人极不推荐,并且我自己从来不写) 720 | 721 | --- 722 | 723 | ## 对 C 标准库的兼容 724 | 725 | C++ 标准库包含了 C 标准库的设施,**但并不完全一样**。 726 | - 因为一些历史问题(向后兼容),C 有很多不合理之处,例如 `strchr` 接受 `const char *` 却返回 `char *`,某些本应该是函数的东西被实现为宏。 727 | - C 缺乏 C++ 的 function overloading 等机制,因此某些设计显得繁琐。 728 | - C++ 的编译期计算能力远远强过 C,例如 `` 里的数学函数自 C++23 起可以在编译时计算。 729 | 730 | C 的标准库文件 `` 在 C++ 中的版本是 ``,并且所有名字也被引入了 `namespace std`。 731 | 732 | ```cpp 733 | #include 734 | int main() { std::printf("Hello world\n"); } 735 | ``` 736 | 737 | **\* 在 C++ 中使用来自 C 的标准库文件时,请使用 `` 而非 ``** 738 | 739 | --- 740 | 741 | ## C++ 中的 C 742 | 743 | 更合理的设计 744 | 745 | - `bool`、`true`、`false` 是内置的,不需要额外头文件 746 | - 逻辑运算符和关系运算符的返回值是 `bool` 而非 `int` 747 | - `"hello"` 的类型是 `const char [6]` 而非 `char [6]` 748 | - 字符字面值 `'a'` 的类型是 `char` 而非 `int` 749 | - 所有有潜在风险的类型转换都不允许隐式发生,不是 warning,而是 error。 750 | - 由 `const int maxn = 100;` 声明的 `maxn` 是编译期常量,可以作为数组大小。 751 | - `int fun()` **不接受参数**,而非接受任意参数。 752 | 753 | --- 754 | 755 | # `std::string` 756 | 757 | 定义在标准库文件 `` 中(**不是 ``,不是 ``!!**) 758 | 759 | 真正意义上的“字符串”。 760 | 761 | --- 762 | 763 | ## 定义并初始化一个字符串 764 | 765 | ```cpp 766 | std::string str = "Hello world"; 767 | // equivalent: std::string str("Hello world"); 768 | // equivalent: std::string str{"Hello world"}; (modern) 769 | std::cout << str << std::endl; 770 | 771 | std::string s1(7, 'a'); 772 | std::cout << s1 << std::endl; // aaaaaaa 773 | 774 | std::string s2 = s1; // s2 is a copy of s1 775 | std::cout << s2 << std::endl; // aaaaaaa 776 | 777 | std::string s; // "" (empty string) 778 | ``` 779 | 780 | **默认初始化一个 `std::string` 对象会得到空串,而非未定义的值!** 781 | 782 | --- 783 | 784 | ## 一些问题 785 | 786 | - `std::string` 的内存:**自动管理,自动分配,允许增长,自动释放** 787 | - `std::string` **不是以空字符结尾的**,它自有办法知道在哪里结束。 788 | - 它也可能被实现为以空字符结尾的,但**你看不见那个空字符** 789 | - 使用 `std::string` 时,**关注字符串的内容本身,而非它的实现细节** 790 | 791 | --- 792 | 793 | ## `std::string` 的长度 794 | 795 | ### `s.size()` 成员函数 796 | 797 | ```cpp 798 | std::string str{"Hello world"}; 799 | std::cout << str.size() << std::endl; 800 | ``` 801 | 802 | **不是 `strlen`,更不是 `sizeof`!!!** 803 | 804 | ### `s.empty()` 成员函数 805 | 806 | ```cpp 807 | if (str.empty()) { 808 | // ... 809 | } 810 | ``` 811 | 812 | --- 813 | 814 | ## 字符串的连接 815 | 816 | 直接用 `+` 和 `+=` 就行了! 817 | 818 | **不需要考虑内存怎么分配,不需要 `strcat` 这样的函数**。 819 | 820 | ```cpp 821 | std::string s1 = "Hello"; 822 | std::string s2 = "world"; 823 | std::string s3 = s1 + ' ' + s2; // "Hello world" 824 | s1 += s2; // s1 becomes "Helloworld" 825 | s2 += "C++string"; // s2 becomes "worldC++string" 826 | ``` 827 | 828 | --- 829 | 830 | ## 字符串的连接 831 | 832 | `+` 两侧至少有一个得是 `std::string` 对象。 833 | 834 | ```cpp 835 | const char *old_bad_ugly_C_style_string = "hello"; 836 | std::string s = old_bad_ugly_C_style_string + "aaaaa"; // Error 837 | ``` 838 | 839 | 下面这个是否合法? 840 | 841 | ```cpp 842 | std::string hello{"hello"}; 843 | std::string s = hello + "world" + "C++"; 844 | ``` 845 | 846 | --- 847 | 848 | ## 字符串的连接 849 | 850 | `+` 两侧至少有一个得是 `std::string` 对象。 851 | 852 | ```cpp 853 | const char *old_bad_ugly_C_style_string = "hello"; 854 | std::string s = old_bad_ugly_C_style_string + "aaaaa"; // Error 855 | ``` 856 | 857 | 下面这个是否合法? 858 | 859 | ```cpp 860 | std::string hello{"hello"}; 861 | std::string s = hello + "world" + "C++"; 862 | ``` 863 | 864 | **合法**:`+` 是**左结合**的,`(hello + "world")` 是一个 `std::string` 对象。 865 | 866 | --- 867 | 868 | ## 使用 `+=` 869 | 870 | `s1 = s1 + s2` 会先为 `s1 + s2` 构造一个临时对象,必然要拷贝一遍 `s1` 的内容。 871 | 872 | 而 `s1 += s2` 是直接在 `s1` 后面连接 `s2`。 873 | 874 | 试一试: 875 | 876 |
877 |
878 | 879 | ```cpp 880 | std::string result; 881 | for (int i = 0; i != n; ++i) 882 | result += 'a'; 883 | ``` 884 |
885 |
886 | 887 | ```cpp 888 | std::string result; 889 | for (int i = 0; i != n; ++i) 890 | result = result + 'a'; 891 | ``` 892 |
893 |
894 | 895 | --- 896 | 897 | ## 字符串比较(字典序) 898 | 899 | 直接用 `<`, `<=`, `>`, `>=`, `==`, `!=`,不需要循环,不需要其它函数! 900 | 901 | ## 字符串的拷贝赋值 902 | 903 | 直接用 `=` 就行 904 | 905 | ```cpp 906 | std::string s1{"Hello"}; 907 | std::string s2{"world"}; 908 | s2 = s1; // s2 is a copy of s1 909 | s1 += 'a'; // s2 is still "Hello" 910 | ``` 911 | 912 | `std::string` 的 `=` 会**拷贝**这个字符串的内容。 913 | 914 | --- 915 | 916 | ## 遍历字符串:基于范围的 `for` 语句 917 | 918 | 例:输出所有大写字母(`std::isupper` 在 `` 里) 919 | 920 | ```cpp 921 | for (char c : s) 922 | if (std::isupper(c)) 923 | std::cout << c; 924 | std::cout << std::endl; 925 | ``` 926 | 927 | 等价的方法:使用下标,但不够 modern,比较啰嗦。 928 | 929 | ```cpp 930 | for (std::size_t i = 0; i != s.size(); ++i) 931 | if (std::isupper(s[i])) 932 | std::cout << s[i]; 933 | std::cout << std::endl; 934 | ``` 935 | 936 | 基于范围的 `for` 语句**更好,更清晰,更简洁,更通用,更现代,更推荐**。 937 | 938 | --- 939 | 940 | ## 字符串的 IO 941 | 942 | `std::cin >> s` 可以输入字符串,`std::cout << s` 可以输出字符串。 943 | 944 | - `std::cin >> s` 跳不跳过空白?是读到空白结束还是读一行结束?可以自己试试 945 | 946 | `std::getline(std::cin, s)`:从当前位置开始读一行,**换行符会读掉,但不会存进来** 947 | 948 | --- 949 | 950 | ## 总结 951 | 952 | 真正意义上的“字符串”:`std::string` 953 | 954 | - **不以空字符结尾,并且所有内存自动管理**。 955 | - `s.size()` 获得长度,`s.empty()` 判断是否为空串 956 | - 用 `+` 和 `+=` 连接,`<`, `<=`, `>`, `>=`, `==`, `!=` 字典序比较,`=` 拷贝赋值 957 | - `>>` 和 `<<` IO,以及 `std::getline` 958 | - 可以用 `s[i]` 访问元素。 959 | - 遍历:**使用基于范围的 `for` 语句** (range-based `for` loops) 960 | - `std::string` 所有函数(成员、非成员)的完整列表:https://en.cppreference.com/w/cpp/string/basic_string -------------------------------------------------------------------------------- /r7/r7.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 7 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 引用 15 | - 左值和右值 16 | - `std::vector` 17 | - `new` 和 `delete` 18 | 19 | --- 20 | 21 | # 引用 (Reference) 22 | 23 | --- 24 | 25 | ## 动机 26 | 27 | 练习:使用基于范围的 `for` 语句遍历一个字符串,将大写改为小写 28 | 29 | --- 30 | 31 | ## 动机 32 | 33 | 练习:使用基于范围的 `for` 语句遍历一个字符串,将大写改为小写 34 | 35 | ```cpp 36 | for (char c : str) 37 | c = std::tolower(c); 38 | ``` 39 | 40 | 这样写是**不行的**:`for (char c : str)` 相当于让 `char c` 依次成为 `str` 中的每个字符的拷贝,在 `c` 上修改不会影响 `str` 的内容。 41 | 42 | 如同 43 | 44 | ```cpp 45 | for (std::size_t i = 0; i != str.size(); ++i) { 46 | char c = str[i]; 47 | c = std::tolower(c); 48 | } 49 | ``` 50 | 51 | --- 52 | 53 | ## 动机 54 | 55 | 练习:使用基于范围的 `for` 语句遍历一个字符串,将大写改为小写 56 | 57 | ```cpp 58 | for (char &c : str) 59 | c = std::tolower(c); 60 | ``` 61 | 62 | `char &c` 定义了一个**绑定到 `char` 的引用**,我们让 `c` 依次绑定到 `str` 中的每个字符。 63 | 64 | 如同 65 | 66 | ```cpp 67 | for (std::size_t i = 0; i != str.size(); ++i) { 68 | char &c = str[i]; 69 | c = std::tolower(c); // effectively str[i] = std::tolower(str[i]); 70 | } 71 | ``` 72 | 73 | --- 74 | 75 | ## 引用即别名 76 | 77 | ```cpp 78 | int ival = 42; 79 | int &iref = ival; 80 | ++iref; // ival becomes 43 81 | iref = 50; // ival becomes 50 82 | std::cout << ival << std::endl; // 50 83 | std::cout << iref << std::endl; // 50 84 | int &iref2 = iref; // Equivalent to int &iref2 = ival; 85 | // iref2 also bounds to ival. 86 | ``` 87 | 88 | 可以写 `Type &r`,也可以 `Type& r` 89 | - 但 `Type& r1, r2, r3;` 只有 `r1` 是引用。 90 | - 如果想定义多个引用,需要 `Type &r1, &r2, &r3;` 91 | 92 | **引用必须初始化(即在定义时就指明它绑定到谁),并且这个绑定关系不可修改。** 93 | 94 | --- 95 | 96 | ## 引用只能绑定到实际的对象 97 | 98 | 变量、数组、指针等都是实际存在的对象,但“引用”本身只是一个傀儡,是一个实际存在的对象的“别名”。 99 | 100 | - 可以定义绑定到数组的引用: 101 | 102 | ```cpp 103 | int a[10]; 104 | int (&ar)[10] = a; 105 | ar[0] = 42; // a[0] = 42 106 | ``` 107 | - 可以定义绑定到指针的引用: 108 | 109 | ```cpp 110 | int *p; 111 | int *&pr = p; 112 | pr = &ival; // p = &ival; 113 | ``` 114 | 115 | --- 116 | 117 | ## 引用只能绑定到实际的对象 118 | 119 | 变量、数组、指针等都是实际存在的对象,但“引用”本身只是一个傀儡,是一个实际存在的对象的“别名”。 120 | 121 | - 不能定义绑定到引用的引用。 122 | - 也不能定义指向引用的指针,因为指针也必须指向实际的对象。 123 | 124 | --- 125 | 126 | ## 传引用参数 (pass-by-reference) 127 | 128 | 可以定义绑定到数组的引用: 129 | 130 | ```cpp 131 | void print_array(int (&array)[10]) { 132 | for (int x : array) // 基于范围的 for 语句可以用来遍历数组! 133 | std::cout << x << ' '; 134 | std::cout << std::endl; 135 | } 136 | int a[10], ival = 42, b[20]; 137 | print_array(a); // Correct 138 | print_array(&ival); // Error! 139 | print_array(b); // Error! 140 | ``` 141 | 142 | 这个 `array` 真的是(绑定到了)一个数组,而不是指向数组首元素的指针。 143 | - `array` 只能绑定到 `int[10]`,其它牛鬼蛇神都会 Error(C 做不到这一点)。 144 | 145 | --- 146 | 147 | ## 传引用参数 (pass-by-reference) 148 | 149 | 定义一个函数,接受一个字符串,输出其中的大写字母。 150 | 151 | ```cpp 152 | void print_upper(std::string str) { 153 | for (char c : str) 154 | if (std::isupper(c)) 155 | std::cout << c; 156 | std::cout << std::endl; 157 | } 158 | ``` 159 | 160 | --- 161 | 162 | ## 传引用参数 (pass-by-reference) 163 | 164 | ```cpp 165 | void print_upper(std::string str) { 166 | for (char c : str) 167 | if (std::isupper(c)) 168 | std::cout << c; 169 | std::cout << std::endl; 170 | } 171 | std::string s = some_very_long_string(); 172 | print_upper(s); 173 | ``` 174 | 175 | 传参的过程中发生了形如 `std::string str = s;` 的**拷贝初始化**。如果 `s` 很长,这将非常耗时。 176 | 177 | --- 178 | 179 | ## 传引用参数 (pass-by-reference) 180 | 181 | 以**引用**的方式传参,避免拷贝: 182 | 183 | ```cpp 184 | void print_upper(std::string &str) { 185 | for (char c : str) 186 | if (std::isupper(c)) 187 | std::cout << c; 188 | std::cout << std::endl; 189 | } 190 | std::string s = some_very_long_string(); 191 | print_upper(s); // std::string &str = s; 192 | ``` 193 | 194 | --- 195 | 196 | ## 传引用参数 (pass-by-reference) 197 | 198 | 以**引用**的方式传参,避免拷贝: 199 | 200 | ```cpp 201 | void print_upper(std::string &str) { 202 | for (char c : str) 203 | if (std::isupper(c)) 204 | std::cout << c; 205 | std::cout << std::endl; 206 | } 207 | std::string s = some_very_long_string(); 208 | print_upper(s); // std::string &str = s; 209 | ``` 210 | 211 | 但这样有问题: 212 | 213 | ```cpp 214 | print_upper("Hello"); // Error 215 | ``` 216 | 217 | --- 218 | 219 | # 左值 (lvalue) 和右值 (rvalue) 220 | 221 | --- 222 | 223 | ## 左值和右值 224 | 225 | 一个表达式在被使用时,有时我们使用的是它代表的**对象**,有时我们仅仅是使用了那个对象的**值**。 226 | - `str[i] = ch` 中,我们使用的是表达式 `str[i]` 所代表的**对象** 227 | - `ch = str[i]` 中,我们使用的是表达式 `str[i]` 所代表的对象的**值**。 228 | 229 | --- 230 | 231 | ## 左值和右值 232 | 233 | 一个表达式在被使用时,有时我们使用的是它代表的**对象**,有时我们仅仅是使用了那个对象的**值**。 234 | - `str[i] = ch` 中,我们使用的是表达式 `str[i]` 所代表的**对象** 235 | - `ch = str[i]` 中,我们使用的是表达式 `str[i]` 所代表的对象的**值**。 236 | 237 | 一个表达式本身带有**值类别** (value category) 的属性:它要么是左值,要么是右值 238 | - 左值:它代表了一个实际的对象 239 | - 右值:它仅仅代表一个值 240 | 241 | --- 242 | 243 | ## 左值和右值 244 | 245 | 一个表达式本身带有**值类别** (value category) 的属性:它要么是左值,要么是右值 246 | - 左值:它代表了一个实际的对象 247 | - 右值:它仅仅代表一个值 248 | 249 | 哪些表达式能代表一个实际的对象? 250 | 251 | 哪些表达式产生的仅仅是一个值? 252 | 253 | --- 254 | 255 | ## 左值和右值 256 | 257 | 一个表达式本身带有**值类别** (value category) 的属性:它要么是左值,要么是右值 258 | - 左值:它代表了一个实际的对象 259 | - 右值:它仅仅代表一个值 260 | 261 | 哪些表达式能代表一个实际的对象? 262 | - `*p`, `a[i]` 263 | 264 | 哪些表达式产生的仅仅是一个值? 265 | - `a + b`, `&val`, `cond1 && cond2` 等等。 266 | 267 | **\* “左值”和“右值”这两个名字是怎么来的?** 268 | 269 | --- 270 | 271 | ## 左值和右值 272 | 273 | 在 C 中,左值可以放在赋值语句的左侧,右值不能。 274 | 275 | 但在 C++ 中,二者的区别远没有这么简单。 276 | 277 | 目前已经见过的返回左值的表达式:`*p`, `a[i]` 278 | 279 | **特别地**:在 C++ 中,前置递增/递减运算符返回**左值**,`++i = 42` 是合法的。 280 | 281 | **赋值表达式返回左值**:`a = b` 的返回值是 `a` 这个对象。 282 | - 试着解释表达式 `a = b = c`? 283 | 284 | --- 285 | 286 | ## 左值和右值 287 | 288 | 在 C 中,左值可以放在赋值语句的左侧,右值不能。 289 | 290 | 但在 C++ 中,二者的区别远没有这么简单。 291 | 292 | 目前已经见过的返回左值的表达式:`*p`, `a[i]` 293 | 294 | **特别地**:在 C++ 中,前置递增/递减运算符返回**左值**,`++i = 42` 是合法的。 295 | 296 | **赋值表达式返回左值**:`a = b` 的返回值是 `a` 这个对象。 297 | - 赋值运算符**右结合**,表达式 `a = b = c` 等价于 `a = (b = c)` 298 | - 先执行 `b = c`,然后相当于 `a = b`。 299 | 300 | --- 301 | 302 | ## 左值和右值 303 | 304 | 右值仅仅代表一个值,不代表一个实际的对象。常见的右值有**表达式执行产生的临时对象**和**字面值**。 305 | - ```cpp 306 | std::string fun(); // a function that returns a std::string object 307 | std::string a = fun(); 308 | ``` 309 | 函数调用 `fun()` 生成的临时对象是**右值**。 310 | - 特别的例外:**字符串字面值 `"hello"` 是左值**,它其实是真实存在于内存中的对象。 311 | - 相比之下,整数字面值 `42` 仅仅产生一个临时对象,是右值。 312 | 313 | --- 314 | 315 | ## 引用只能绑定到左值 316 | 317 | ```cpp 318 | int ival = 42; 319 | int &iref = 42; // Error 320 | int &iref2 = ival; // Correct 321 | int &iref3 = ival + 42; // Error 322 | int fun(); 323 | int &iref4 = fun(); // Error 324 | ``` 325 | 326 | C++11 引入了所谓的“右值引用”,我们在介绍**移动**的时候再讲。一般来说,“引用”指的是“左值引用”。 327 | 328 | --- 329 | 330 | ## 引用是实际对象的别名,所以引用也是左值 331 | 332 | ```cpp 333 | int arr[10]; 334 | int &subscript(int i) { // function returning int& 335 | return arr[i]; 336 | } 337 | subscript(3) = 42; // Correct. 338 | int &ref = subscript(7); // Correct. ref bounds to arr[7] 339 | ``` 340 | 341 | --- 342 | 343 | ## Reference-to-`const` 344 | 345 | 类似于“指向常量的指针”(即带有“底层 `const`”的指针),我们也有“绑定到常量的引用” 346 | 347 | ```cpp 348 | int ival = 42; 349 | const int cival = 42; 350 | const int &cref = cival; // Correct 351 | const int &cref2 = ival; // Also correct. 352 | int &ref = cival; // Error: Casting-away low-level const is not allowed. 353 | int &ref2 = cref; // Error: Casting-away low-level const is not allowed. 354 | int &ref3 = cref2; // Error: Even though cref2 bounds to non-const ('ival'), 355 | // this is still casting-away low-level const. 356 | ``` 357 | 358 | 一个 reference-to-`const` **自认为自己绑定到 `const` 对象**,所以不允许通过它修改它所绑定的对象的值,也不能让一个不带 `const` 的引用绑定到它。(不允许“去除底层 `const`”) 359 | 360 | --- 361 | 362 | ## Reference-to-`const` 363 | 364 | 指针既可以带顶层 `const`(本身是常量),也可以带底层 `const`(指向的东西是常量),但引用**不谈**“顶层 `const`”。 365 | - 即,只有“绑定到常量的引用”。引用本身不是对象,不谈是否带 `const`。 366 | - 从另一个角度讲,引用本身一定带有“顶层 `const`”,因为绑定关系不能修改。 367 | - 在不引起歧义的情况下,通常用**常量引用**这个词来代表“绑定到常量的引用”。 368 | 369 | --- 370 | 371 | ## Reference-to-`const` 372 | 373 | 特殊规则:常量引用可以绑定到右值: 374 | 375 | ```cpp 376 | const int &cref = 42; // Correct 377 | int fun(); 378 | const int &cref2 = fun(); // Correct 379 | int &ref = fun(); // Error 380 | ``` 381 | 382 | 当一个常量引用被绑定到右值时,实际上就是让它绑定到了一个临时对象。 383 | - 这是合理的,反正你也不能通过常量引用修改那个对象的值 384 | 385 | --- 386 | 387 | ## Pass-by-reference-to-`const` 388 | 389 | ```cpp 390 | void print_upper(std::string &str) { 391 | for (char c : str) 392 | if (std::isupper(c)) 393 | std::cout << c; 394 | std::cout << std::endl; 395 | } 396 | print_upper("Hello"); // Error 397 | ``` 398 | 399 | 当我们传递 `"Hello"` 给 `std::string` 参数时,实际上发生了一个由 `const char [6]` 到 `std::string` 的**隐式转换**,这个隐式转换产生**右值**,无法被 `std::string&` 绑定。 400 | 401 | ```cpp 402 | const std::string s = "hello"; 403 | print_upper(s); // Error: Casting-away low-level const 404 | ``` 405 | 406 | 不带底层 `const` 的引用无法绑定到 `const` 对象。 407 | 408 | --- 409 | 410 | ## Pass-by-reference-to-`const` 411 | 412 | 将参数声明为**常量引用**,既可以避免拷贝,又可以允许传递右值 413 | 414 | ```cpp 415 | void print_upper(const std::string &str) { 416 | for (char c : str) 417 | if (std::isupper(c)) 418 | std::cout << c; 419 | std::cout << std::endl; 420 | } 421 | std::string s = some_very_long_string(); 422 | print_upper(s); // const std::string &str = s; 423 | print_upper("Hello"); // const std::string &str = "Hello";, Correct 424 | ``` 425 | 426 | 也可以传递常量对象: 427 | 428 | ```cpp 429 | const std::string s = "hello"; 430 | print_upper(s); // OK 431 | ``` 432 | 433 | --- 434 | 435 | ## Pass-by-reference-to-`const` 436 | 437 | 将参数声明为**常量引用**,既可以避免拷贝,又可以允许传递右值,也可以传递常量对象,也可以**防止你不小心修改了它**。 438 | 439 | 在 C++ 中声明函数的参数时,**尽可能使用常量引用**(如果你不需要修改它)。 440 | 441 | (如果仅仅是 `int` 或者指针这样的内置类型,可以不需要常量引用) 442 | 443 | --- 444 | 445 | ## Pass-by-reference-to-`const` 446 | 447 | 练习:编写一个函数,接受一个字符串,倒序输出其中所有的小写字母。 448 | 449 | --- 450 | 451 | ## Pass-by-reference-to-`const` 452 | 453 | 练习:编写一个函数,接受一个字符串,倒序输出其中所有的小写字母。 454 | 455 | ```cpp 456 | void print_lower_reversed(const std::string &str) { 457 | for (int i = str.size() - 1; i >= 0; --i) 458 | if (std::islower(str[i])) 459 | std::cout << str[i]; 460 | std::cout << std::endl; 461 | } 462 | ``` 463 | 464 | 这里的 `i` 必须带符号! 465 | 466 | --- 467 | 468 | ## 总结 469 | 470 | 引用: 471 | - 引用即别名 472 | - 引用必须绑定到实际的对象 473 | 474 | 左值和右值: 475 | - 左值是实际的对象,右值通常是一些临时对象或者字面值 476 | - 使用左值是使用实际的对象,使用右值仅仅是使用那个值 477 | - 例如:你可以修改一个对象,但无法修改一个值 478 | - 常见的返回左值的表达式:`*p`, `a[i]`, `++i`, `a = b` 479 | - 引用只能绑定到左值 480 | 481 | --- 482 | 483 | ## 总结 484 | 485 | 常量引用: 486 | - 常量引用可以绑定到左值,也可以绑定到右值 487 | - 常量引用“自以为”自己绑定到了常量,所以自发地保护它所绑定到的对象 488 | - `const` 是一把锁,**只允许加锁,不允许解锁** 489 | - pass-by-reference-to-`const` 的好处: 490 | - 避免拷贝 491 | - 允许传递右值和常量 492 | - 避免一不小心修改这个对象 493 | 494 | --- 495 | 496 | ## 真正的“值类别” 497 | 498 | [(语言律师需要掌握)](https://en.cppreference.com/w/cpp/language/value_category) 499 | 500 | C++ 中的表达式依值类别被划分为如下三种: 501 | 502 | | 英文 | 中文 | has identity? | can be moved from? | 503 | | ---------------------- | ------ | ------------- | ------------------ | 504 | | lvalue | 左值 | yes | no | 505 | | xvalue (expired value) | 亡值 | yes | yes | 506 | | prvalue (pure rvalue) | 纯右值 | no | yes | 507 | 508 | lvalue + xvalue = glvalue(广义左值),xvalue + prvalue = rvalue(右值) 509 | 510 | - 所以实际上“左值是实际的对象”是不严谨的,右值也可能是实际的对象(xvalue) 511 | 512 | --- 513 | 514 | # `std::vector` 515 | 516 | 定义在标准库文件 `` 中 517 | 518 | 真正好用的“动态数组” 519 | 520 | --- 521 | 522 | ## 创建一个 `std::vector` 对象 523 | 524 | `std::vector` 是一个**类模板**,只有给出了模板参数之后才成为一个真正的类型。 525 | 526 | ```cpp 527 | std::vector vi; // An empty vector of ints 528 | std::vector vs; // An empty vector of strings 529 | std::vector vd; // An empty vector of doubles 530 | ``` 531 | 532 | 不同模板参数的 `vector` 是**不同的类型**。 533 | 534 | --- 535 | 536 | ## 创建一个 `std::vector` 对象 537 | 538 | ```cpp 539 | std::vector v{2, 3, 5, 7}; // A vector of ints, 540 | // whose elements are {2, 3, 5, 7}. 541 | std::vector v2 = {2, 3, 5, 7}; // Equivalent to ↑ 542 | 543 | std::vector vs{"hello", "world"}; // A vector of strings, 544 | // whose elements are {"hello", "world"}. 545 | std::vector vs2 = {"hello", "world"}; // Equivalent to ↑ 546 | 547 | std::vector v3(10); // A vector of ten ints, all initialized to 0. 548 | std::vector v4(10, 42); // A vector of ten ints, all initialized to 42. 549 | ``` 550 | 551 | `vector v(n)` 这种构造方式会将 `n` 个元素都**值初始化**(类似于 C 中的“空初始化”),而不是得到一串未定义的值! 552 | 553 | --- 554 | 555 | ## 创建一个 `std::vector` 对象 556 | 557 | ```cpp 558 | std::vector v{2, 3, 5, 7}; 559 | std::vector v2 = v; // v2 is a copy of v 560 | std::vector v3(v); // Equivalent 561 | std::vector v4{v}; // Equivalent 562 | ``` 563 | 564 | 去年 CS100 一直到期末居然还有人用循环一个元素一个元素拷贝 `vector`,**太愚蠢了!** 565 | 566 | ```cpp 567 | std::vector> v; 568 | ``` 569 | 570 | “二维 `vector`”,也就是“`vector` of `vector`”,当然也是可以的。 571 | 572 | --- 573 | 574 | ## C++17 CTAD 575 | 576 | Class Template Argument Deduction:只要你给出了足够的信息,编译器可以自动推导元素的类型! 577 | 578 | ```cpp 579 | std::vector v{2, 3, 5, 7}; // vector 580 | std::vector v2{3.14, 6.28}; // vector 581 | std::vector v3(10, 42); // vector 582 | std::vector v4(10); // Error: cannot deduce template argument type 583 | ``` 584 | 585 | --- 586 | 587 | ## `std::vector` 的大小 588 | 589 | `v.size()` 和 `v.empty()` 590 | 591 | ```cpp 592 | std::vector v{2, 3, 5, 7}; 593 | std::cout << v.size() << std::endl; 594 | if (v.empty()) { 595 | // ... 596 | } 597 | ``` 598 | 599 | ## 清空 `std::vector` 600 | 601 | `v.clear()`。不要写愚蠢的 `while (!v.empty()) v.pop_back();` 602 | 603 | --- 604 | 605 | ## 向 `std::vector` 添加元素 606 | 607 | `v.push_back(x)` 将元素 `x` 添加到 `v` 的末尾 608 | 609 | ```cpp 610 | int n; 611 | std::cin >> n; 612 | std::vector v; 613 | for (int i = 0; i != n; ++i) { 614 | int x; 615 | std::cin >> x; 616 | v.push_back(x); 617 | } 618 | ``` 619 | 620 | --- 621 | 622 | ## 删除 `std::vector` 最后一个元素 623 | 624 | `v.pop_back()` 625 | 626 | 练习:将末尾的偶数删掉,直到末尾是奇数为止 627 | 628 | ```cpp 629 | while (!v.empty() && v.back() % 2 == 0) 630 | v.pop_back(); 631 | ``` 632 | 633 | `v.back()`:获得末尾元素**的引用**(这意味着什么?) 634 | 635 | --- 636 | 637 | ## `v.back()` 和 `v.front()` 638 | 639 | 分别获得最后一个元素、第一个元素的**引用**。 640 | 641 | “引用”意味着你可以通过这两个成员函数修改它们: 642 | 643 | ```cpp 644 | v.front() = 42; 645 | ++v.back(); 646 | ``` 647 | 648 | `v.back()`, `v.front()`, `v.pop_back()` 在 `v` 为空的情况下是 undefined behavior,而且实际上是**严重的运行时错误**。 649 | 650 | --- 651 | 652 | ## 基于范围的 `for` 语句 653 | 654 | 遍历一个 `std::vector`,同样可以使用基于范围的 `for` 语句: 655 | 656 | ```cpp 657 | std::vector vi = some_values(); 658 | for (int x : vi) 659 | std::cout << x << std::endl; 660 | std::vector vs = some_strings(); 661 | for (const std::string &s : vs) // use reference-to-const to avoid copying 662 | std::cout << s << std::endl; 663 | ``` 664 | 665 | 练习:使用基于范围的 `for` 语句,将一个 `vector` 中的每个字符串的大写字母打印出来。 666 | 667 | --- 668 | 669 | ## 基于范围的 `for` 语句 670 | 671 | 练习:使用基于范围的 `for` 语句,将一个 `vector` 中的每个字符串的大写字母打印出来。 672 | 673 | ```cpp 674 | for (const std::string &s : vs) { 675 | for (char c : s) 676 | if (std::isupper(c)) 677 | std::cout << c; 678 | std::cout << std::endl; 679 | } 680 | ``` 681 | 682 | --- 683 | 684 | ## 使用下标访问 685 | 686 | 可以使用 `v[i]` 来获得第 `i` 个元素 687 | - `i` 的有效范围是 $[0,N)$,其中 `N = v.size()` 688 | - 越界访问是**未定义行为**,并且通常是**严重的运行时错误**。 689 | - `std::vector` 的下标运算符 `v[i]` **并不检查越界**,目的是为了保证效率。 690 | - 事实上标准库容器的大多数操作都没有对合法性进行检查,为了效率。 691 | - 一种检查越界的下标是 `v.at(i)`,它会在越界时抛出 `std::out_of_range` 异常。 692 | - 不妨自己试一试。 693 | - C++ 中的异常处理?看看[我的视频](https://www.bilibili.com/video/BV1PS4y1H73n/?spm_id_from=333.999.0.0&vd_source=7940495b5667750a71bfa10a4c6eb2d9) 694 | 695 | --- 696 | 697 | ## 接口的统一性 698 | 699 | 事实上 `std::string` 也有 `.at()`, `.front()`, `.back()`, `.push_back(x)`, `.pop_back()`, `.clear()` 等函数。C++ 标准库的各种设施是讲究统一性的。 700 | 701 | [完整列表](https://en.cppreference.com/w/cpp/container#Function_table) 702 | 703 | 练习:实现 Python 的 `rstrip` 函数,接受一个 `std::string`,返回它删去末尾的连续空白后的结果。 704 | 705 | --- 706 | 707 | ## 接口的统一性 708 | 709 | 练习:实现 Python 的 `rstrip` 函数,接受一个 `std::string`,返回它删去末尾的连续空白后的结果。 710 | 711 | ```cpp 712 | std::string rstrip(std::string str) { 713 | while (!str.empty() && std::isspace(str.back())) 714 | str.pop_back(); 715 | return str; 716 | } 717 | ``` 718 | 719 | 在 C++17 下,这个 `return str;` 是不会产生拷贝的,不必担心。(看看[我的视频](https://www.bilibili.com/video/BV1ut4y1g72B/?spm_id_from=333.999.0.0&vd_source=7940495b5667750a71bfa10a4c6eb2d9)) 720 | 721 | --- 722 | 723 | ## `std::vector` 的增长策略 724 | 725 | 考虑像这样连续 `push_back` `n` 次得到一个 `vector` 的代码: 726 | 727 | ```cpp 728 | std::vector v; 729 | for (int i = 0; i != n; ++i) 730 | v.push_back(i); 731 | ``` 732 | 733 | `vector` 是如何做到快速增长的? 734 | 735 | --- 736 | 737 | ## `std::vector` 的增长策略 738 | 739 | 假设现在有一片动态分配的内存,长度为 `i`。 740 | 741 | 当第 `i+1` 个元素到来时,朴素做法: 742 | 1. 分配一片长度为 `i+1` 的内存 743 | 2. 将原有的 `i` 个元素拷贝过来 744 | 3. 将新的元素放在后面 745 | 4. 释放原来的那片内存 746 | 747 | 但这需要拷贝 `i` 个元素。`n` 次 `push_back` 总共就需要 $\displaystyle\sum_{i=0}^{n-1}i=O\left(n^2\right)$ 次拷贝! 748 | 749 | --- 750 | 751 | ## `std::vector` 的增长策略 752 | 753 | 假设现在有一片动态分配的内存,长度为 `i`。 754 | 755 | 当第 `i+1` 个元素到来时, 756 | 1. 分配一片长度为 `2*i` 的内存 757 | 2. 将原有的 `i` 个元素拷贝过来 758 | 3. 将新的元素放在后面 759 | 4. 释放原来的那片内存 760 | 761 | 而当第 `i+2`, `i+3`, ..., `2*i` 个元素到来时,我们不需要分配新的内存,也不需要拷贝任何对象! 762 | 763 | --- 764 | 765 | ## `std::vector` 的增长策略 766 | 767 | $0\to 1\to 2\to 4\to 8\to 16\to\cdots$ 768 | 769 | 假设 $n=2^m$,那么总的拷贝次数就是 $\displaystyle\sum_{i=0}^{m-1}2^i=O(n)$,**平均**(“均摊”)一次 `push_back` 的耗时是 $O(1)$(常数),可以接受。 770 | 771 | 使用 `v.capacity()` 来获得它目前所分配的内存的实际容量,看看是不是真的这样。 772 | - 注意:这仅仅是一种可能的策略,标准并未对此有所规定。 773 | 774 | **\* 看看 hw4/prob2 `attachments/testcases/vector.c`** 775 | 776 | --- 777 | 778 | ## `std::vector` 动态增长带来的影响 779 | 780 | 我们已经看到,改变 `vector` 的大小可能会导致它所保存的元素“搬家”,这会使得所有指针、引用、迭代器失效。 781 | 782 | - 最直接的影响:下面的代码是 undefined behavior,因为基于范围的 `for` 语句本质上依赖于迭代器。 783 | 784 | ```cpp 785 | for (int i : vec) 786 | if (i % 2 == 0) 787 | vec.push_back(i + 1); 788 | ``` 789 | 790 | **不要在用基于范围的 `for` 语句遍历容器的同时改变容器的大小!** 791 | 792 | --- 793 | 794 | # `new` 和 `delete`(初步) 795 | 796 | --- 797 | 798 | ## `new` 表达式 799 | 800 | 动态分配内存,**并构造对象** 801 | 802 | ```cpp 803 | int *pi1 = new int; // 动态创建一个默认初始化的 int 804 | int *pi2 = new int(); // 动态创建一个值初始化的 int 805 | int *pi3 = new int{}; // 同上,但是更 modern 806 | int *pi4 = new int(42); // 动态创建一个 int,并初始化为 42 807 | int *pi5 = new int{42}; // 同上,但是更 modern 808 | ``` 809 | 810 | 对于内置类型: 811 | - **默认初始化** (default-initialization):就是未初始化,具有未定义的值 812 | - **值初始化** (value-initialization):类似于 C 中的“空初始化”,是各种零。 813 | 814 | --- 815 | 816 | ## `new[]` 表达式 817 | 818 | 动态分配“数组”,**并构造对象** 819 | 820 | ```cpp 821 | int *pai1 = new int[n]; // 动态创建了 n 个 int,默认初始化 822 | int *pai2 = new int[n](); // 动态创建了 n 个 int,值初始化 823 | int *pai3 = new int[n]{}; // 动态创建了 n 个 int,值初始化 824 | int *pai4 = new int[n]{2, 3, 5}; // 动态创建了 n 个 int,前三个元素初始化为 2,3,5 825 | // 其余元素都被值初始化(为零) 826 | // 如果 n<3,抛出 std::bad_array_new_length 异常 827 | ``` 828 | 829 | 对于内置类型: 830 | - **默认初始化** (default-initialization):就是未初始化,具有未定义的值 831 | - **值初始化** (value-initialization):类似于 C 中的“空初始化”,是各种零。 832 | 833 | --- 834 | 835 | ## `delete` 和 `delete[]` 表达式 836 | 837 | 销毁动态创建的对象,并释放其内存 838 | 839 | ```cpp 840 | int *p = new int{42}; 841 | delete p; 842 | int *a = new int[n]; 843 | delete[] a; 844 | ``` 845 | 846 | - `new` 必须对应 `delete`,`new[]` 必须对应 `delete[]`,否则是 **undefined behavior** 847 | - 忘记 `delete`:内存泄漏 848 | 849 | --- 850 | 851 | ## 一一对应,不得混用 852 | 853 | 违反下列规则的一律是 undefined behavior: 854 | 855 | - `delete ptr` 中的 `ptr` 必须等于某个先前由 `new` 返回的地址 856 | - `delete[] ptr` 中的 `ptr` 必须等于某个先前由 `new[]` 返回的地址 857 | - `free(ptr)` 中的 `ptr` 必须等于某个先前由 `malloc`, `calloc`, `realloc` 或 `aligned_alloc` 返回的地址。 858 | 859 | --- 860 | 861 | ## `new`/`delete` vs `malloc`/`free` 862 | 863 | C++ 的对象模型比 C 复杂得多,而 `new`/`delete` 也比 `malloc`/`free` 做了更多的事: 864 | - `new`/`new[]` 表达式会**先分配内存,然后构造对象**。对于类类型的对象,它可能会调用一个合适的**构造函数**。 865 | - `delete`/`delete[]` 表达式会**先销毁对象,然后释放内存**。对于类类型的对象,它会调用**析构函数**。 866 | 867 | --- 868 | 869 | ## 在 C++ 中,非必要不手动管理内存 870 | 871 | - 当你需要创建“一列数”、“一列对象”,或者“一张表”、“一个集合”时,**优先考虑标准库容器等设施**,例如 `std::string`, `std::vector`, `std::deque` (双端队列), `std::list`/`std::forward_list` (链表), `std::map`/`std::set` (红黑树), `std::unordered_map`/`std::unordered_set` (哈希表) 872 | - 当你需要创建单个对象时,应该优先考虑**智能指针** (`std::shared_ptr`, `std::unique_ptr`, `std::weak_ptr`) 873 | - 只有在特殊情况下(例如手搓一个标准库没有的数据结构,并且对效率有极高的要求),使用 `new`/`delete` 来管理动态内存 874 | - 当你对于内存分配本身也有特殊的要求时,才需要使用 C 的内存分配/释放函数,但通常也是用它们来[定制 `new` 和 `delete`](https://www.bilibili.com/video/BV17N4y1u7Cn/?spm_id_from=333.999.0.0&vd_source=7940495b5667750a71bfa10a4c6eb2d9) 875 | 876 | --- 877 | 878 | ## 特别提一下:`NULL` vs `nullptr` 879 | 880 | - `NULL` 是一个用 `#define` 定义的宏,可能的定义有 `0`, `(long)0`, `(void *)0` 等等 881 | - C++ 不会将 `NULL` 定义为 `(void *)0`,因为 C++ 不允许 `void *` 向其它指针类型的隐式转换。 882 | - C++ 中的 `NULL` 大概率是 `0`, `(long)0` 或者 `(long long)0`,**但这是一个整数而非指针**,会使得一些类型推导和重载决议发生错误。 883 | - 从语言设计上来说,为了支持 `NULL`,C++ 也不得不引入一些丑陋的特殊规则。 884 | 885 | --- 886 | 887 | ## 更好的空指针:`nullptr` 888 | 889 | `nullptr` 是真正的“空指针”,于 C++11 引入,并即将加入 C23。 890 | 891 | `nullptr` 具有独一无二的类型 `std::nullptr_t`,无需破坏原有的类型规则,在重载决议时也会优先匹配指针而非整数。 892 | 893 | **\* 在 C++ 中,请使用 `nullptr` 表示空指针,而不是 `NULL` 或者数 `0`。** -------------------------------------------------------------------------------- /r8/check.csv: -------------------------------------------------------------------------------- 1 | name,char,strcat,strchr,strcmp,strcpy,strlen 2 | jiangyh1,0,0,0,0,0,0 3 | songyi2022,0,0,0,0,0,0 4 | zhangyq2022,0,0,0,0,0,0 5 | hezhj,0,0,0,0,0,0 6 | liuxh2022,0,0,0,0,0,0 7 | zhugx2022,-36,0,0,0,0,0 8 | guoyw2022,0,0,0,0,0,0 9 | yanglp2022,0,0,0,0,0,0 10 | hugf2022,0,0,0,0,0,0 11 | lijy5,0,0,0,0,0,0 12 | zhuych12022,0,0,0,0,0,0 13 | yangge,0,0,0,0,0,0 14 | quchang2022,0,0,0,0,0,0 15 | gongrui,0,0,0,0,0,0 16 | zhangrm2022,0,0,0,0,0,0 17 | yangqy12022,0,0,0,0,0,0 18 | chenjl2022,0,0,0,0,0,0 19 | buzhy2022,0,0,0,0,0,0 20 | zhangzhk2022,0,0,0,0,0,0 21 | lirf2022,0,0,0,0,0,0 22 | liutx12022,0,0,0,0,0,0 23 | songyh2022,-36,0,0,0,0,0 24 | zhangych12022,0,0,0,0,0,0 25 | sunjx2022,0,0,0,0,0,0 26 | taopx2022,-36,0,0,0,0,0 27 | liqr2022,0,0,0,0,0,0 28 | caoyl2022,0,0,0,0,0,0 29 | luoyf,0,0,0,0,0,0 30 | xuchx1,0,0,0,0,0,0 31 | lvpz,0,0,0,0,0,0 32 | yangjun2,0,0,0,0,0,0 33 | xuewh1,0,0,0,0,0,0 34 | wangjl3,0,0,0,0,0,0 35 | liuyw2022,0,0,0,0,0,0 36 | linjl2022,0,0,0,0,0,0 37 | qinyao,0,0,0,0,0,0 38 | jiaoyy2022,0,0,0,0,0,0 39 | lichy32022,0,0,0,0,0,0 40 | mayh6,0,0,0,0,0,0 41 | zhangjt1,,,,,, 42 | zhouzhj,,,,,, 43 | zhouzq1,,,,,, 44 | panqh,0,0,0,0,0,0 45 | zhangkx22022,0,0,0,0,0,0 46 | dingqh,0,0,0,0,0,0 47 | yangrk2022,0,0,0,0,0,0 48 | wangyang2022,0,0,0,0,0,0 -------------------------------------------------------------------------------- /r8/img/tle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GKxxUCAS/CS100-recitations-spring2023/575821a52913bc3db7a81c3c3d3ab2a14f3762f1/r8/img/tle.png -------------------------------------------------------------------------------- /r8/r8.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 8 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - Homework 4 讲评 15 | - `class` 初步 16 | - 成员 17 | - 访问限制 18 | - 构造函数 19 | 20 | --- 21 | 22 | # Homework 4 23 | 24 | --- 25 | 26 | ## 1. 造标准库 27 | 28 | 出题意图: 29 | - 逼着你们读 cppreference,了解这些标准库函数 30 | - 学会使用 ASCII 码 31 | - 学会用简单的方式实现简单的功能 32 | 33 | --- 34 | 35 | ## 1. 造标准库 36 | 37 | - `islower`, `isupper`, `isalpha`, `isdigit`, `tolower`, `toupper`:用 ASCII 码 38 | - `strcat`, `strlen`, `strcpy`, `strchr`, `strcmp`:“扫一遍”就应该解决问题。 39 | 40 | `tolower`, `toupper`:如果不是大写/小写字母,就原样返回。 41 | 42 | `strchr`: 43 | > The terminating null character is considered to be a part of the string and can be found when searching for `'\0'`. 44 | 45 | `strcmp`:返回值是负值、零、正值,而非一些人一开始写的 0 / 1。 46 | 47 | --- 48 | 49 | ## 错误处理 50 | 51 | 谈一个设计问题:应该如何处理参数非法的情况? 52 | 53 | ```c 54 | char *strchr(const char *str, int ch) { 55 | if (str == NULL) 56 | return NULL; 57 | // ... 58 | } 59 | ``` 60 | 61 | 这个做法合理吗? 62 | 63 | --- 64 | 65 | ## 错误处理 66 | 67 | ```c 68 | char *strchr(const char *str, int ch) { 69 | if (str == NULL) 70 | return NULL; 71 | // ... 72 | } 73 | ``` 74 | 75 | 标准规定这属于 undefined behavior,因此你自然可以任意处置。 76 | - 但是,`return NULL;` 表示“未找到”,不应该将它和错误行为混起来。 77 | 78 | --- 79 | 80 | ## 错误处理 81 | 82 |
83 |
84 | 85 | 使用 `assert`,在条件不满足时退出程序 86 | ```c 87 | char *strchr(const char *str, int ch) { 88 | assert(str != NULL); 89 | // ... 90 | } 91 | ``` 92 |
93 |
94 | 95 | 对错误静默处理,直接假定参数正确 96 | ```c 97 | char *strchr(const char *str, int ch) { 98 | // 压根不检查 str != NULL 99 | // ... 100 | } 101 | ``` 102 |
103 |
104 | 105 | - 在 debug 模式下(未定义 `NDEBUG` 宏,`assert` 有效),将错误显式地报告出来。 106 | - 在 release 模式下(定义 `NDEBUG` 以关闭所有 `assert`),直接假定错误行为不会发生,追求效率。 107 | 108 | 看看[孟岩老师的观点](https://blog.csdn.net/myan/article/details/1921?spm=1001.2014.3001.5506)以及[为什么 C++ IOStream 在这方面是失败的](https://blog.csdn.net/myan/article/details/1922?spm=1001.2014.3001.5506)。 109 | 110 | --- 111 | 112 | ## 为什么用 `int`? 113 | 114 | `char` 可能无符号(这是 implementation-defined 的),但有一个重要的字符是负数:**文件结束符 `EOF`**(定义为 `-1`)。 115 | - 这意味着用 `char` 存储 `EOF` 可能会在之后的运算中迷失。 116 | 117 | `getchar()` 的返回值类型也是 `int`。如果 `char` 被实现为无符号的,那么下面的代码就会出问题: 118 | 119 | ```c 120 | char ch; 121 | while ((ch = getchar()) != EOF && ch != '\n') 122 | // ... 123 | ``` 124 | 125 | `EOF` 赋值给 `ch` 就立刻变成了 `255`,不会和 `-1` 相等。 126 | 127 | --- 128 | 129 | ## 2. Memory Leak Checker 130 | 131 | 目的:实现内存泄漏和非法 `free` 的检查,要对于用户代码的改动尽可能小。 132 | 133 | 通过 `#define` 将 `malloc`, `calloc` 和 `free` 替换成我们自己的函数,就可以为所欲为了 134 | - 只需要在用户代码的开头 `#include` 我们的代码即可。 135 | 136 | 保姆级教程,阅读量大,实际上很简单。 137 | 138 | 使用了 GCC extension,向 VS 用户们道歉,以后会尽量避免。 139 | 140 | --- 141 | 142 | ## 2. Memory Leak Checker 143 | 144 | 用一个数据结构记录内存分配申请。需要支持以下操作: 145 | - 添加一条记录 146 | - 查找一条记录 147 | - 删除一条记录 148 | 149 | 本题数据规模限制在了 `1000` 次分配以内,所以数组即可解决:用一个变量记录“当前使用到哪了”。 150 | - 添加一条记录:`records[cnt++] = ...` 151 | - 查找的时候,只需遍历下标在 `[0, cnt)` 范围内的。 152 | 153 | --- 154 | 155 | ## 2. Memory Leak Checker 156 | 157 | 注意 `free(NULL)` 不应该做任何事,不要浪费时间去查找。测试数据中有连着调用 `free(NULL)` 一百万次的。 158 | 159 | 删除一条记录:“懒惰删除”,直接给 `ptr` 设置为 `NULL`,相当于打了个标记。 160 | 161 | 其它注意事项在题目里已经唠叨过了,例如 `malloc` 和 `calloc` 在 `size == 0` 时的行为,以及它们可能分配更多内存等等。 162 | 163 | --- 164 | 165 | ## 2. Memory Leak Checker 166 | 167 | 用一个数据结构记录内存分配申请。需要支持以下操作: 168 | - 添加一条记录 169 | - 查找一条记录 170 | - 删除一条记录 171 | 172 | 适合这个问题的数据结构:哈希表、红黑树等。(大家 CS101 再见) 173 | 174 | 我们会发一个哈希表的实现给大家看一看。理想状况下,哈希表可以做到 $O(1)$ 完成以上三种操作,而朴素的数组上查找只能 $O(n)$,其中 $n$ 是总的记录条数。 175 | 176 | --- 177 | 178 | ## 3. Brainfuck 179 | 180 |
181 |
182 | 183 | | 运算符 | C语言的等价表达 | 184 | | ------ | --------------------- | 185 | | `>` | `ptr++` | 186 | | `<` | `ptr--` | 187 | | `+` | `(*ptr)++` | 188 | | `-` | `(*ptr)--` | 189 | | `.` | `putchar(*ptr)` | 190 | | `,` | `*ptr = getchar()` | 191 | | `[` | `while (*ptr != 0) {` | 192 | | `]` | `}` | 193 |
194 |
195 | 196 | 一个 brainfuck 程序有一个 30000 字节的数组,以及一个指向其中某位置的指针 `ptr`。 197 | 198 | 这个数组是谁? 199 |
200 |
201 | 202 | --- 203 | 204 | ## 3. Brainfuck 205 | 206 |
207 |
208 | 209 | | 运算符 | C语言的等价表达 | 210 | | ------ | --------------------- | 211 | | `>` | `ptr++` | 212 | | `<` | `ptr--` | 213 | | `+` | `(*ptr)++` | 214 | | `-` | `(*ptr)--` | 215 | | `.` | `putchar(*ptr)` | 216 | | `,` | `*ptr = getchar()` | 217 | | `[` | `while (*ptr != 0) {` | 218 | | `]` | `}` | 219 |
220 |
221 | 222 | 一个 brainfuck 程序有一个 30000 字节的数组,以及一个指向其中某位置的指针 `ptr`。 223 | 224 | - `uint8_t` 代表“字节”(8 位) 225 | - `state->memory_buffer` 正是一个指向“字节”的指针,自然是让它指向一片 30000 个 `uint8_t` 的内存,来表示这个“数组”。 226 | 227 | 如何表示 `ptr`? 228 |
229 |
230 | 231 | --- 232 | 233 | ## 3. Brainfuck 234 | 235 |
236 |
237 | 238 | | 运算符 | C语言的等价表达 | 239 | | ------ | --------------------- | 240 | | `>` | `ptr++` | 241 | | `<` | `ptr--` | 242 | | `+` | `(*ptr)++` | 243 | | `-` | `(*ptr)--` | 244 | | `.` | `putchar(*ptr)` | 245 | | `,` | `*ptr = getchar()` | 246 | | `[` | `while (*ptr != 0) {` | 247 | | `]` | `}` | 248 |
249 |
250 | 251 | 一个 brainfuck 程序有一个 30000 字节的数组,以及一个指向其中某位置的指针 `ptr`。 252 | 253 | - `uint8_t` 代表“字节”(8 位) 254 | - `state->memory_buffer` 正是一个指向“字节”的指针,自然是让它指向一片 30000 个 `uint8_t` 的内存,来表示这个“数组”。 255 | 256 | 如何表示 `ptr`? 257 | - 要么真的用一个指针,要么用下标。 258 | - `state->memory_pointer_offset` 是 `size_t` 类型的,自然是一个下标。 259 |
260 |
261 | 262 | --- 263 | 264 | ## 3. Brainfuck 265 | 266 |
267 |
268 | 269 | 如同在 C 里做“面向对象编程”: 270 | - `bf_state_new()` 就是“构造函数” 271 | - `bf_state_delete(state)` 是“析构函数” 272 | 273 | `bf_state_new()` 应当创建一个能长久存在的 `bf_state` 对象,直到它被传给 `bf_state_delete` 才被销毁。 274 | - 要么用动态内存,要么用 `static`。 275 | - `static` 类似于全局变量,这要求在每个时刻只能有一个 `bf_state` 对象。(如果确实如此,可以这样做) 276 |
277 |
278 | 279 | ```c 280 | struct bf_state { 281 | uint8_t *buffer; 282 | size_t offset; 283 | }; 284 | 285 | struct bf_state *bf_state_new(void) { 286 | struct bf_state *state 287 | = malloc(sizeof(struct bf_state)); 288 | state->buffer 289 | = calloc(30000, sizeof(uint8_t)); 290 | state->offset = 0; 291 | return state; 292 | } 293 | 294 | void bf_state_delete 295 | (struct bf_state *state) { 296 | free(state->buffer); 297 | free(state); 298 | } 299 | ``` 300 |
301 |
302 | 303 | --- 304 | 305 | ## 3. Brainfuck 306 | 307 | 题目要求 `(*ptr)++` 需要对 $256$ 取模 308 | - 这事实上不需要我们手动判断,因为无符号整数不会溢出 309 | - 直接 `++state->memory_buffer[state->memory_pointer_offset]` 即可 310 | 311 | `--` 也是如此。 312 | 313 | Brainfuck 代码中的任何其它字符都**视为注释**,跳过,而非判为语法错误。 314 | - **常看 Piazza!** 315 | 316 | --- 317 | 318 | ## 3. Brainfuck 319 | 320 | 难点:循环 321 | - 如果 `src` 中出现了未匹配的 `[`,那这行代码就不能执行,需要你把它存进某个 buffer。 322 | - 每次如果那个 buffer 里有代码,都要将它和当前的 `src` 连起来看,如果仍然有未匹配的 `[` 就仍然不能执行,继续往后存。 323 | - 直到所有 `[` 都被匹配再执行。 324 | 325 | --- 326 | 327 | ## 3. Brainfuck 328 | 329 | 但是其实这件事我们已经帮你做了... 330 | 331 | - 如果你阅读 `ibf.h` 中调用 `brainfuck_main` 的上下文,或者在 `brainfuck_main` 的开头 `puts(src);`,就会发现这一点:**`src` 中并不存在不匹配的方括号**。 332 | 333 | 因此 `struct brainfuck_state` 中不需要添加任何成员。 334 | 335 | --- 336 | 337 | ## 3. Brainfuck 338 | 339 | ```c 340 | const char *cur = src; 341 | while (cur != '\0') { 342 | switch (*cur) { 343 | // cases +-<>,. 344 | case '[': 345 | if (!loop_condition(state)) 346 | cur = find_matching_end(cur); // Now *cur == ']' 347 | break; 348 | case ']': 349 | if (loop_condition(state)) 350 | cur = find_matching_start(cur); // Now *cur == '[' 351 | break; 352 | } 353 | ++cur; 354 | } 355 | ``` 356 | 357 | --- 358 | 359 | ## 3. Brainfuck 360 | 361 | 如何找到和某个 `[` 匹配的 `]`?~~阅读 `ibf.h` 即可找到答案。~~ 362 | - 使用一个变量 `unmatched` 表示目前未匹配的 `'['` 的个数。 363 | - 向右扫描,遇到 `'['` 则 `++unmatched`,遇到 `']'` 则 `--unmatched` 364 | - 当 `unmatched == 0` 时返回当前位置。 365 | 366 | 当然可以用 stack 实现得更精细一些,但这个做法的效率已经足够。 367 | 368 | --- 369 | 370 | ## 绝大多数 TLE 的原因 371 | 372 | 373 | 374 | 375 | 376 | (当然也有 `for` 的) 377 | 378 | --- 379 | 380 | # `class` 初步 381 | 382 | --- 383 | 384 | ## 一个简单的 `class` 385 | 386 | `class` 除了可以有数据成员,还可以有成员函数等。 387 | 388 | ```cpp 389 | class Student { 390 | std::string name; 391 | std::string id; 392 | int entranceYear; 393 | void printInfo() const { 394 | std::cout << "I am " << name << ", id " << id 395 | << ", entrance year: " << entranceYear << std::endl; 396 | } 397 | bool graduated(int year) const { 398 | return year - entranceYear >= 4; 399 | } 400 | }; 401 | ``` 402 | 403 | --- 404 | 405 | ## 访问限制 406 | 407 | ```cpp 408 | class Student { 409 | private: 410 | std::string name; 411 | std::string id; 412 | int entranceYear; 413 | public: 414 | void printInfo() const { 415 | std::cout << "I am " << name << ", id " << id 416 | << ", entrance year: " << entranceYear << std::endl; 417 | } 418 | bool graduated(int year) const { 419 | return year - entranceYear >= 4; 420 | } 421 | }; 422 | ``` 423 | 424 | - `private`:外部代码不能访问,只有类内和 `friend` 代码可以访问。 425 | - `public`:所有代码都可以访问。 426 | 427 | --- 428 | 429 | ## 访问限制 430 | 431 |
432 |
433 | 434 | ```cpp 435 | class Student { 436 | std::string name; 437 | std::string id; 438 | int entranceYear; 439 | public: 440 | void printInfo() const; 441 | bool graduated(int year) const; 442 | }; 443 | ``` 444 |
445 |
446 | 447 | 一个**访问限制说明符**的作用范围是从当前位置开始,一直到下一个访问限制说明符之前(或者到类的定义结束)。 448 | 449 | 开头有一段没有写访问限制的成员? 450 | - 如果是 `class`,它们是 `private`。 451 | - 如果是 `struct`,它们是 `public`。 452 |
453 |
454 | 455 | C++ `struct` 和 `class` **仅有两个区别**,这里是区别之一。 456 | - 为何需要将数据成员“藏起来”?《Effective C++》条款 22。(下次有时间的话讲) 457 | 458 | --- 459 | 460 | ## `this` 指针 461 | 462 | 访问一个普通成员的方法是 `a.mem`,其中 `a` 是该类类型的一个**对象**。 463 | - **(非 `static`)成员**是属于一个对象的:每个学生都有一个姓名、学号、入学年份。你需要指明“谁的”姓名/学号... 464 | 465 | ```cpp 466 | Student s = someValue(); // 假设它具有一定的值 467 | s.printInfo(); // 调用它的 printInfo() 输出相关信息 468 | if (s.graduated(2023)) { 469 | // ... 470 | } 471 | ``` 472 | 473 | --- 474 | 475 | ## `this` 指针 476 | 477 | ```cpp 478 | class Student { 479 | // ... 480 | public: 481 | bool graduated(int year) const; 482 | }; 483 | Student s = someValue(); 484 | if (s.graduated(2023)) // ... 485 | ``` 486 | 487 | `graduated` 函数有几个参数? 488 | 489 | --- 490 | 491 | ## `this` 指针 492 | 493 | ```cpp 494 | class Student { 495 | // ... 496 | public: 497 | bool graduated(int year) const; 498 | }; 499 | Student s = someValue(); 500 | if (s.graduated(2023)) // ... 501 | ``` 502 | 503 | `graduated` 函数有几个参数? 504 | 505 | - **看似是一个,其实是两个**:`s` 也是调用这个函数时必须知道的信息! 506 | 507 | --- 508 | 509 | ## `this` 指针 510 | 511 |
512 |
513 | 514 | ```cpp 515 | class Student { 516 | public: 517 | bool graduated(int year) const { 518 | return year - entranceYear >= 4; 519 | } 520 | }; 521 | Student s = someValue(); 522 | if (s.graduated(2023)) 523 | // ... 524 | ``` 525 |
526 |
527 | 528 | 左边的代码就如同: 529 | 530 | ```cpp 531 | bool graduated(const Student *this, 532 | int year) { 533 | return year - this->entranceYear >=4; 534 | } 535 | Student s = someValue(); 536 | if (graduated(&s, 2023)) 537 | // ... 538 | ``` 539 |
540 |
541 | 542 | --- 543 | 544 | ## `this` 指针 545 | 546 | 在成员函数内部,正有一个名为 `this` 的指针:它具有 `X *` 或者 `const X *` 类型(其中 `X` 是这个类的名字)。 547 | 548 | 在成员函数内部对任何数据成员 `mem` 的访问,实际上都是 `this->mem`。 549 | 550 | 你也可以显式地写 `this->mem`。 551 | 552 | ```cpp 553 | class Student { 554 | public: 555 | bool graduated(int year) const { 556 | return year - this->entranceYear >= 4; 557 | } 558 | }; 559 | ``` 560 | 561 | 许多语言都有类似的设施:Python 里有 `self`。[(C++23 当然也可以有 `self`)](https://en.cppreference.com/w/cpp/language/member_functions#Explicit_object_parameter) 562 | 563 | --- 564 | 565 | ## `const` 成员函数 566 | 567 | 在参数列表后面、函数体的 `{` 前面标一个 `const` 568 | - 在 `const` 对象上(包括用 reference-to-`const` 时),**只能调用 `const` 成员函数** 569 | - C++ 中对于 `const` 和 reference-to-`const` 的使用随处可见, 570 | - 因此如果一个成员函数逻辑上不修改这个对象的状态,它就应该是 `const`,否则在 `const` 对象上无法调用。 571 | 572 | 这个 `const` 相当于施加在 `this` 指针上,作为**底层 `const`**。 573 | - `const` 成员函数不能修改数据成员,不能调用非 `const` 的成员函数。 574 | - `const` 成员函数不能调用数据成员的非 `const` 成员函数。 575 | 576 | 《Effective C++》条款 3:尽可能使用 `const`。(下次讲) 577 | 578 | --- 579 | 580 | ## 构造函数 581 | 582 | **构造函数** (constructors, ctors) 负责对象的初始化方式。 583 | 584 | - 构造函数往往是**重载** (overloaded) 的,因为一个对象很可能具有多种合理的初始化方式。 585 | 586 | ```cpp 587 | class Student { 588 | std::string name; 589 | std::string id; 590 | int entranceYear; 591 | public: 592 | Student(const std::string &name_, const std::string &id_, int ey) 593 | : name(name_), id(id_), entranceYear(ey) {} 594 | Student(const std::string &name_, const std::string &id_) 595 | : name(name_), id(id_), entranceYear(std::stoi(id_.substr(0, 4))) {} 596 | }; 597 | Student a("Alice", "2020123123", 2020); 598 | Student b("Bob", "2020123124"); // entranceYear = 2020 599 | ``` 600 | 601 | --- 602 | 603 | ## 构造函数 604 | 605 | ```cpp 606 | class Student { 607 | std::string name; 608 | std::string id; 609 | int entranceYear; 610 | public: 611 | Student(const std::string &name_, const std::string &id_) 612 | : name(name_), id(id_), entranceYear(std::stoi(id_.substr(0, 4))) {} 613 | }; 614 | ``` 615 | 616 | - 构造函数的函数名就是类名本身:`Student` 617 | - 构造函数不声明返回值类型,可以含有 `return;` 但不能返回一个值,**但不能认为它的返回值类型是 `void`。** 618 | - 上面这个构造函数的函数体是空的:`{}` 619 | 620 | --- 621 | 622 | ## 构造函数初始值列表 623 | 624 | 构造函数负责这个对象的初始化,包括**所有成员**的初始化。 625 | 626 | **一切成员**的初始化过程都在**进入函数体之前结束**,它们的初始化方式(部分是)由**构造函数初始值列表** (constructor initializer list) 决定: 627 | 628 | ```cpp 629 | class Student { 630 | // ... 631 | public: 632 | Student(const std::string &name_, const std::string &id_) 633 | : name(name_), id(id_), entranceYear(std::stoi(id_.substr(0, 4))) {} 634 | }; 635 | ``` 636 | 637 | 构造函数初始值列表由冒号 `:` 开头,依次给出各个数据成员的初始化器,用 `,` 隔开。 638 | 639 | --- 640 | 641 | ## 成员的初始化顺序 642 | 643 | 数据成员按照**它们在类内声明的顺序**进行初始化,而非它们在构造函数初始值列表中出现的顺序。 644 | - 如果你的构造函数初始值列表中的成员的顺序和它们在类内的声明顺序不同,编译器会以 warning 的方式提醒你。 645 | 646 | 典型的错误:初始化 `entranceYear` 时用到了成员 `id`,而此时 `id` 还未初始化! 647 | 648 | ```cpp 649 | class Student { 650 | std::string name; 651 | int entranceYear; 652 | std::string id; 653 | public: 654 | Student(const std::string &name_, const std::string &id_) 655 | : name(name_), id(id_), entranceYear(std::stoi(id.substr(0, 4))) {} 656 | }; 657 | ``` 658 | 659 | --- 660 | 661 | ## 构造函数初始值列表 662 | 663 | 数据成员按照**它们在类内声明的顺序**进行初始化,而非它们在构造函数初始值列表中出现的顺序。 664 | - 如果你的构造函数初始值列表中的成员的顺序和它们在类内的声明顺序不同,编译器会以 warning 的方式提醒你。 665 | - 未在初始值列表中出现的成员: 666 | - 如果有**类内初始值**(稍后再说),则按照类内初始值初始化; 667 | - 否则,**默认初始化**。 668 | 669 | **默认初始化**对于内置类型来说就是*不初始化*,但是对于类类型呢? 670 | 671 | --- 672 | 673 | ## 构造函数初始值列表 674 | 675 | 下面是典型的“先默认初始化,后赋值”: 676 | 677 | ```cpp 678 | class Student { 679 | // ... 680 | public: 681 | Student(const std::string &name_, const std::string &id_) { 682 | name = name_; 683 | id = id_; 684 | entranceYear = std::stoi(id_.substr(0, 4)); 685 | } 686 | }; 687 | ``` 688 | 689 | “先默认初始化,后赋值”有什么坏处? 690 | 691 | --- 692 | 693 | ## 构造函数初始值列表 694 | 695 | “先默认初始化,后赋值”有什么坏处? 696 | - 不是所有类型都能默认初始化,不是所有类型都能赋值。(有哪些反例?) 697 | 698 | --- 699 | 700 | ## 构造函数初始值列表 701 | 702 | “先默认初始化,后赋值”有什么坏处? 703 | - 不是所有类型都能默认初始化,不是所有类型都能赋值。 704 | - 引用无法被默认初始化,无法被赋值。`const` 对象无法被赋值。 705 | - (内置类型的)`const` 对象无法被默认初始化。 706 | - 我们将在后面看到,类类型可以拒绝支持默认初始化和赋值,这取决于设计。 707 | - 如果你把能默认初始化、能赋值的成员在函数体里赋值,其它成员在初始值列表里初始化,**你的成员初始化顺序将会非常混乱**,很容易导致错误。 708 | 709 | ### Best practice 710 | 711 | 只要有可能,就用构造函数初始值列表,按照成员的声明顺序逐个初始化它们。 712 | 713 | --- 714 | 715 | ## 默认构造函数 716 | 717 | 特殊的构造函数:不接受参数。 718 | - 猜猜是用来干什么的? 719 | 720 | --- 721 | 722 | ## 默认构造函数 723 | 724 | 特殊的构造函数:不接受参数。 725 | - 专门负责对象的**默认初始化**,因为调用它时不需要传递参数,相当于不需要任何初始化器。 726 | - 特别地,类类型的**值初始化**就是**默认初始化**。 727 | 728 | ```cpp 729 | class Point2d { 730 | double x, y; 731 | public: 732 | Point2d() : x(0), y(0) {} 733 | Point2d(double x_, double y_) : x(x_), y(y_) {} 734 | }; 735 | Point2d p1; // 调用默认构造函数,(0, 0) 736 | Point2d p2(3, 4); // 调用 Point2d(double, double), (3, 4) 737 | Point2d p3(); // 这是在调用默认构造函数吗? 738 | ``` 739 | 740 | --- 741 | 742 | ## 你的类需要一个默认构造函数吗? 743 | 744 | 首先,如果需要开数组,你几乎肯定需要默认构造函数: 745 | 746 | ```cpp 747 | Student s[1000]; // 全部默认初始化 748 | Student s2[1000] = {a, b}; // 前两个元素是 a 和 b,后面的全部值初始化, 749 | // 而值初始化也是调用默认构造函数 750 | ``` 751 | 752 | 但关键问题是**它是否具有一个合理的定义?** 753 | 754 | - 一个“默认的学生”应该具有什么姓名和学号? 755 | 756 | --- 757 | 758 | ## 你的类需要一个默认构造函数吗? 759 | 760 | 如果一个类没有 user-declared 的构造函数,编译器会试图帮你合成一个默认构造函数。 761 | 762 | ```cpp 763 | struct X {}; // 什么构造函数都没写 764 | X x; // OK:调用了编译器合成的默认构造函数 765 | ``` 766 | 767 | 编译器合成的默认构造函数的行为非常简单:逐个成员地进行默认初始化 768 | - 如果有成员有类内初始值,则使用类内初始值。 769 | - 如果有成员没有类内初始值,但又无法默认初始化,则编译器会放弃合成这个默认构造函数。 770 | 771 | --- 772 | 773 | ## 你的类需要一个默认构造函数吗? 774 | 775 | 如果一个类有至少一个 user-declared 的构造函数,但没有默认构造函数,则编译器**不会**帮你合成默认构造函数。 776 | - 在它看来,这个类的所有合理的初始化行为就是你给出的那几个构造函数, 777 | - 因此不应该再画蛇添足地定义一个默认行为。 778 | 779 | 你可以通过 `= default;` 来显式地要求编译器合成一个具有默认行为的默认构造函数: 780 | 781 | ```cpp 782 | class Student { 783 | public: 784 | Student(const std::string &name_, const std::string &id_, int ey) 785 | : name(name_), id(id_), entranceYear(ey) {} 786 | Student(const std::string &name_, const std::string &id_) 787 | : name(name_), id(id_), entranceYear(std::stoi(id_.substr(0, 4))) {} 788 | Student() = default; 789 | }; 790 | ``` 791 | 792 | --- 793 | 794 | ## 你的类需要一个默认构造函数吗? 795 | 796 | **宁缺毋滥**:如果它没有一个合理的默认构造行为,就不应该有默认构造函数! 797 | - 而不是胡乱定义一个或者 `=defualt`,这会留下隐患。 798 | - 对默认构造函数的调用应当引发**编译错误**,而不是随意地允许。 799 | 800 | 《Effective C++》条款 6:如果编译器合成的函数你不需要,应当显式地禁止。 801 | 802 | --- 803 | 804 | ## 类内初始值 805 | 806 | 可以在声明数据成员的时候顺便给它一个初始值。 807 | 808 | ```cpp 809 | struct Point2d { 810 | double x; 811 | double y = 0; 812 | Point2d() = default; 813 | Point2d(double x_) : x(x_) {} // y = 0 814 | }; 815 | Point2d p; // x 具有未定义的值,而 y = 0 816 | Point2d p2(3.14); // x = 3.14, y = 0 817 | ``` 818 | 819 | 如果这个成员在某个构造函数初始值列表里没有出现,它会采用类内初始值,而不是默认初始化。 820 | 821 | --- 822 | 823 | ## 类内初始值 824 | 825 | 但是类内初始值不能用圆括号... 826 | 827 | ```cpp 828 | struct X { 829 | // 错误:受限于编译器的设计,它会在语法分析阶段被认为是函数声明 830 | std::vector v(10); 831 | 832 | // 这毫无疑问是声明一个函数,而非设定类内初始值 833 | std::string s(); 834 | }; 835 | ``` 836 | -------------------------------------------------------------------------------- /r9/r9.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | math: mathjax 4 | --- 5 | 6 | # CS100 Recitation 9 7 | 8 | GKxx 9 | 10 | --- 11 | 12 | ## Contents 13 | 14 | - 拷贝控制:以 `Dynarray` 为例 15 | - `static` 成员 16 | - 初识类型推导:`auto` 17 | 18 | --- 19 | 20 | # 拷贝控制:以 `Dynarray` 为例 21 | 22 | --- 23 | 24 | ## 设计 25 | 26 | 实现一个“动态数组”: 27 | 28 | ```cpp 29 | int n; std::cin >> n; 30 | Dynarray arr(n); // arr has n value-initialized ints. 31 | for (int i = 0; i != n; ++i) 32 | std::cin >> arr.at(i); // arr.at(i) is just like arr[i] 33 | // 如何做到 arr[i]?以后再说 34 | for (std::size_t i = 0; i != arr.size(); ++i) 35 | arr.at(i) += arr.at(i - 1) * 2; 36 | // ... 37 | ``` 38 | 39 | 最后 `arr` 应当自己释放自己所占用的内存。 40 | 41 | --- 42 | 43 | ## Dynarray:成员 44 | 45 | ```cpp 46 | class Dynarray { 47 | std::size_t m_size; 48 | int *m_storage; 49 | public: 50 | bool empty() const { 51 | return m_size == 0u; 52 | } 53 | std::size_t size() const { 54 | return m_size; 55 | } 56 | }; 57 | ``` 58 | 59 | --- 60 | 61 | ## Dynarray:构造函数 62 | 63 | 构造函数定义的是**初始化的方式**。 64 | 65 | ```cpp 66 | Dynarray a; // 对应默认构造函数 Dynarray() 67 | Dynarray b(n); // 对应构造函数 Dynarray(std::size_t) 68 | Dynarray c(n, x); // 对应构造函数 Dynarray(std::size_t, int) 69 | ``` 70 | 71 | ```cpp 72 | class Dynarray { 73 | public: 74 | Dynarray(); 75 | Dynarray(std::size_t); 76 | Dynarray(std::size_t, int); 77 | }; 78 | ``` 79 | 80 | --- 81 | 82 | ## Dynarray:构造函数 83 | 84 | 假如我们需要一个默认构造函数,它应当具有什么行为? 85 | - 初始化为 `0` 个元素的数组? 86 | - 这样的数组能使用吗? 87 | - 初始化为 `DYNARRAY_DEFAULT_SIZE` 个元素的数组? 88 | 89 | --- 90 | 91 | ## Dynarray:构造函数 92 | 93 | 假如我们需要一个默认构造函数,它应当具有什么行为? 94 | - 初始化为 `0` 个元素的数组? 95 | - 在不知道如何拷贝的情况下,我们似乎没有什么能改变数组大小的操作。 96 | - 如果不能改变大小,那“`0` 个元素的数组”有什么用? 97 | - 初始化为 `DYNARRAY_DEFAULT_SIZE` 个元素的数组? 98 | - 也许在某些应用场景下,这是合理的。 99 | 100 | --- 101 | 102 | ## 默认构造函数 (default constructor):复习 103 | 104 | - 默认构造函数的“默认”体现在哪? 105 | - 哪些使用方式依赖于默认构造函数? 106 | - 如果没有自定义默认构造函数,这个类会有默认构造函数吗? 107 | - 如果有,其行为是什么? 108 | - 默认构造函数 `= default` 意味着什么? 109 | 110 | --- 111 | 112 | ## 默认构造函数 (default constructor):复习 113 | 114 | - 默认构造函数的“默认”体现在哪?——无参数 115 | - 哪些使用方式依赖于默认构造函数?——默认初始化和值初始化 116 | - ```cpp 117 | Dynarray a, b{}; 118 | Dynarray *c = new Dynarray, *d = new Dynarray(), *e = new Dynarray{}; 119 | Dynarray f[1000]; std::array g; // 这是啥? 120 | ``` 121 | - 如果没有自定义默认构造函数,这个类会有默认构造函数吗? 122 | ——如果有其它自定义的构造函数,编译器就不会合成默认构造函数 123 | - 默认构造函数 `= default` 意味着什么? 124 | 125 | --- 126 | 127 | ## 默认构造函数 (default constructor):复习 128 | 129 | 如果没有其它自定义的构造函数,或者我们显式地以 `= default` 要求编译器合成一个默认构造函数: 130 | - 编译器会合成一个具有默认行为的默认构造函数。 131 | - “默认行为”:按照成员声明顺序逐个初始化它们 132 | - 对于有类内初始值的成员,使用类内初始值 133 | - 对于其它成员,进行**默认初始化** 134 | 135 | --- 136 | 137 | ## 析构函数 (destructor, dtor) 138 | 139 | `Dynarray` 必须在自己不再被使用的时候释放所拥有的内存: 140 | 141 | ```cpp 142 | void fun() { 143 | Dynarray a(n); 144 | if (condition) { 145 | Dynarray b(m); 146 | // ... 147 | } // Now b should release the memory it allocated. 148 | // ... 149 | } // Now a should release the memory it allocated. 150 | ``` 151 | 152 | --- 153 | 154 | ## 析构函数 (destructor, dtor) 155 | 156 | **析构函数**是在这个对象被销毁的时候自动调用的函数。 157 | - 它通常用来完成一些最后的清理 158 | - 特别地,那些**拥有一定资源**的类通常在析构函数里释放它们所拥有的资源 159 | 160 | --- 161 | 162 | ## 定义一个析构函数 163 | 164 |
165 |
166 | 167 | ```cpp 168 | class Student { 169 | std::string name; 170 | std::string id; 171 | int entranceYear; 172 | public: 173 | ~Student() { 174 | std::cout << "dtor of " 175 | << name << "called.\n"; 176 | } 177 | }; 178 | ``` 179 |
180 |
181 | 182 | ```cpp 183 | { 184 | Student alice("Alice", "2020123123"); 185 | if (condition) { 186 | Student bob("Bob", "2020123124"); 187 | // ... 188 | } // Output "dtor of Bob called." 189 | } // Output "dtor of Alice called." 190 | ``` 191 |
192 |
193 | 194 | - 析构函数的名字是 `~ClassName` 195 | - 析构函数不接受参数,不声明返回值类型 196 | - 一个类只能有一个析构函数 [(until C++20)](https://en.cppreference.com/w/cpp/language/destructor#Prospective_destructor) 197 | 198 | --- 199 | 200 | ## Dynarray:析构函数 201 | 202 | 非常直接的实现 203 | 204 | ```cpp 205 | class Dynarray { 206 | std::size_t m_size; 207 | int *m_storage; 208 | public: 209 | Dynarray(std::size_t n) 210 | : m_size(n), m_storage(new int[n]{}) {} 211 | ~Dynarray() { 212 | delete[] m_storage; 213 | } 214 | }; 215 | ``` 216 | 217 | --- 218 | 219 | ## 析构函数的具体行为 220 | 221 | 考虑一个问题:`Student` 是否拥有什么资源? 222 | 223 | ```cpp 224 | class Student { 225 | std::string name; 226 | std::string id; 227 | int entranceYear; 228 | // ... 229 | }; 230 | ``` 231 | 232 | --- 233 | 234 | ## 析构函数的具体行为 235 | 236 | `Student` 本身不拥有什么资源,但是 `std::string` 有。 237 | - `std::string` 当然会在它自己的析构函数里释放内存。 238 | 239 | ```cpp 240 | class Student { 241 | std::string name; 242 | std::string id; 243 | int entranceYear; 244 | // ... 245 | }; 246 | ``` 247 | 248 | 在一个 `Student` 被销毁的时候: 249 | - 它显然应该销毁它的所有成员 250 | - 销毁一个成员,显然应该调用那个成员的析构函数。 251 | 252 | --- 253 | 254 | ## 析构函数的具体行为 255 | 256 | **析构函数在执行完函数体之后,会自动销毁它的所有数据成员** 257 | - 对于类类型成员,会调用它的析构函数来销毁它。 258 | - 猜猜销毁成员的顺序? 259 | 260 | --- 261 | 262 | ## 析构函数的具体行为 263 | 264 | **析构函数在执行完函数体之后,会自动销毁它的所有数据成员** 265 | - 对于类类型成员,会调用它的析构函数来销毁它。 266 | - 按照成员的声明顺序**倒序**销毁它们。 267 | 268 | 对比一下构造函数: 269 | - 在执行函数体**之前**初始化所有成员。 270 | - 对于类类型成员,调用它的构造函数进行初始化。 271 | - 初始化的顺序是成员的声明顺序。 272 | 273 | --- 274 | 275 | ## 析构函数的具体行为 276 | 277 | 对于成员的销毁是不需要我们写的,会自动完成。 278 | 279 | ```cpp 280 | class Student { 281 | std::string name; 282 | std::string id; 283 | int entranceYear; 284 | public: 285 | ~Student() {} // 编译器会在最后插入代码来调用 id 和 name 的析构函数。 286 | }; 287 | ``` 288 | 289 | `Student` 类的析构函数只需一个空函数体即可。 290 | 291 | 等价的写法:`~Student() = default;` 292 | 293 | --- 294 | 295 | ## 一个类不能没有析构函数,就像... 296 | 297 | 如果一个类的析构函数是不可调用的,就意味着它无法被销毁 298 | - 就如同不可降解的塑料 299 | 300 | 因此 C++ 不允许定义这样的类的对象! 301 | 302 | 什么情况下析构函数不可调用? 303 | 304 | --- 305 | 306 | ## 一个类不能没有析构函数,就像... 307 | 308 | 如果一个类的析构函数是不可调用的,就意味着它无法被销毁 309 | 310 | 因此 C++ 不允许定义这样的类的对象! 311 | 312 | 什么情况下析构函数不可调用? 313 | - 你可以显式地 `~ClassName() = delete;` 将析构函数定义为“删除的”。 314 | - 如果析构函数是 `private` 的,它就不能在类外、`friend` 外被调用。 315 | 316 | --- 317 | 318 | ## `new` 和 `delete` 319 | 320 | ```cpp 321 | std::string *p = new std::string("Hello"); 322 | std::cout << *p << std::endl; 323 | delete p; 324 | p = new std::string; // 调用默认构造函数,*p 为空串 "" 325 | std::cout << p->size() << std::endl; 326 | delete p; 327 | ``` 328 | 329 | `new` 表达式会**先分配内存**,然后**构造对象**。 330 | - 如果这是一个类类型,它必然会调用一个构造函数来构造对象。在没有指定如何构造的情况下,它会调用**默认构造函数**。 331 | - 相比之下,来自 C 的 `malloc` 只会分配内存,不构造任何对象(不做任何初始化)。`calloc` 会将这个内存**清零**,而非调用默认构造函数。 332 | 333 | --- 334 | 335 | ## `new` 和 `delete` 336 | 337 | ```cpp 338 | std::string *p = new std::string("Hello"); 339 | std::cout << *p << std::endl; 340 | delete p; 341 | p = new std::string; // 调用了默认构造函数,*p 为空串 "" 342 | std::cout << p->size() << std::endl; 343 | delete p; 344 | ``` 345 | 346 | `delete` 表达式会**先销毁对象**,然后释放这片内存。 347 | - 如果这是一个类类型,它必然会调用析构函数来销毁这个对象。 348 | - 相比之下,`free` 只释放内存,不调用析构函数。 349 | 350 | --- 351 | 352 | ## `Dynarray`:元素访问 353 | 354 | `.at(i)` 应该返回什么? 355 | 356 | ```cpp 357 | class Dynarray { 358 | std::size_t m_size; 359 | int *m_storage; 360 | public: 361 | ??? at(std::size_t n) { 362 | return m_storage[n]; 363 | } 364 | }; 365 | ``` 366 | 367 | --- 368 | 369 | ## `Dynarray`:元素访问 370 | 371 | ```cpp 372 | arr.at(0) = 42; 373 | std::cout << arr.at(1); 374 | ``` 375 | 376 | 如果我们希望通过 `arr.at(i)` 来修改这个元素,那必然要返回引用。 377 | 378 | ```cpp 379 | class Dynarray { 380 | std::size_t m_size; 381 | int *m_storage; 382 | public: 383 | int &at(std::size_t n) { 384 | return m_storage[n]; 385 | } 386 | }; 387 | ``` 388 | 389 | 这样可以吗? 390 | 391 | --- 392 | 393 | ## `Dynarray`:元素访问 394 | 395 | 试一试: 396 | 397 | ```cpp 398 | void print(const Dynarray &arr) { 399 | for (std::size_t i = 0; i != arr.size(); ++i) 400 | std::cout << arr.at(i) << ' '; 401 | } 402 | ``` 403 | 404 | 无法编译:`at` 不是 `const` 成员函数,无法在 `const Dynarray &` 上调用! 405 | 406 | --- 407 | 408 | ## `const` 成员函数:复习 409 | 410 | - `const` 写在哪? 411 | - `const` 成员函数的 `const` 是作用于谁的? 412 | - 在什么对象上能调用 `const` 成员函数? 413 | - `const` 成员函数能做哪些事?不能做哪些事? 414 | 415 | --- 416 | 417 | ## `const` 成员函数:复习 418 | 419 | - `const` 写在哪?——参数列表后,函数体之前 420 | - `const` 成员函数的 `const` 是作用于谁的? 421 | - 加在隐式的 `this` 指针上的**底层 `const`** 422 | - 表示当前对象是 `const`,其所有数据成员也都是 `const` 423 | - 在什么对象上能调用 `const` 成员函数? 424 | - 什么对象上都可以,因为添加底层 `const` 永远没问题。 425 | - `const` 成员函数能做哪些事?不能做哪些事? 426 | - 不能修改数据成员,不能调用数据成员的 non-`const` 成员函数 427 | - 不能调用自身的 non-`const` 成员函数 428 | 429 | --- 430 | 431 | ## `Dynarray`:元素访问 432 | 433 | 加个 `const`? 434 | 435 | ```cpp 436 | class Dynarray { 437 | std::size_t m_size; 438 | int *m_storage; 439 | public: 440 | int &at(std::size_t n) const { 441 | return m_storage[n]; 442 | } 443 | }; 444 | ``` 445 | 446 | 结果是在 `const Dynarray` 上,你可以得到其中元素的 non-`const` 引用,进而修改它! 447 | 448 | --- 449 | 450 | ## `Dynarray`:元素访问 451 | 452 | 正确的解决方案:`const` 和 non-`const` 的重载 453 | 454 | ```cpp 455 | class Dynarray { 456 | std::size_t m_size; 457 | int *m_storage; 458 | public: 459 | const int &at(std::size_t n) const { 460 | return m_storage[n]; 461 | } 462 | int &at(std::size_t n) { 463 | return m_storage[n]; 464 | } 465 | }; 466 | ``` 467 | 468 | - 在 `const` 对象上,它只能调用 `const` 版本,得到 reference-to-`const`,无法修改 469 | - 在非 `const` 对象上会调用哪个? 470 | 471 | --- 472 | 473 | ## `Dynarray`:元素访问 474 | 475 |
476 |
477 | 478 | ```cpp 479 | class Dynarray { 480 | public: 481 | const int &at(std::size_t n) const { 482 | return m_storage[n]; 483 | } 484 | int &at(std::size_t n) { 485 | return m_storage[n]; 486 | } 487 | }; 488 | arr.at(i) = 42; 489 | ``` 490 |
491 |
492 | 493 | ```cpp 494 | // 左边的代码就如同: 495 | const int &at(const Dynarray *this, 496 | std::size_t n) { 497 | return this->m_storage[n]; 498 | } 499 | int &at(Dynarray *this, std::size_t n){ 500 | return this->m_storage[n]; 501 | } 502 | at(&arr, i) = 42; 503 | ``` 504 |
505 |
506 | 507 | 508 | 509 | - 在 `const` 对象上,它只能调用 `const` 版本,得到 reference-to-`const`,无法修改 510 | - 在非 `const` 对象上:两个版本都可以调用,但是 511 | - 调用 non-`const` 版本是完美匹配,调用 `const` 版本是**添加底层 `const`**,因此前者是更好的匹配。 512 | 513 | --- 514 | 515 | ## 在 `const` vs non-`const` 重载中避免重复 516 | 517 | 假如我们要模仿标准库的行为的话,`at` 函数应该提供边界检查... 518 | 519 | ```cpp 520 | class Dynarray { 521 | public: 522 | const int &at(std::size_t n) const { 523 | if (n >= m_length) // 为什么不需要判断 n < 0? 524 | throw std::out_of_range{"Dynarray subscript out of range."}; 525 | return m_storage[n]; 526 | } 527 | int &at(std::size_t n) { 528 | if (n >= m_length) 529 | throw std::out_of_range{"Dynarray subscript out of range."}; 530 | return m_storage[n]; 531 | } 532 | }; 533 | ``` 534 | 535 | --- 536 | 537 | ## 在 `const` vs non-`const` 重载中避免重复 538 | 539 | 假如我们还想在 `at` 访问中做一些其它的记录和检查... 540 | 541 | ```cpp 542 | class Dynarray { 543 | public: 544 | const int &at(std::size_t n) const { 545 | if (n >= m_length) 546 | throw std::out_of_range{"Dynarray subscript out of range."}; 547 | log_access(); 548 | verify_integrity(); 549 | return m_storage[n]; 550 | } 551 | int &at(std::size_t n) { 552 | if (n >= m_length) 553 | throw std::out_of_range{"Dynarray subscript out of range."}; 554 | log_access(); 555 | verify_integrity(); 556 | return m_storage[n]; 557 | } 558 | }; 559 | ``` 560 | 561 | --- 562 | 563 | ## 在 `const` vs non-`const` 重载中避免重复 564 | 565 | 将一模一样的代码编写两遍实在是太麻烦了... 有没有办法避免重复? 566 | - [C++23 deducing-this](https://en.cppreference.com/w/cpp/language/member_functions#Explicit_object_parameter) 能帮得上忙 567 | 568 | 假如没有额外的语法特性... 能不能让一个函数调用另一个? 569 | - 让谁调用谁? 570 | 571 | --- 572 | 573 | ## 在 `const` vs non-`const` 重载中避免重复 574 | 575 | 将一模一样的代码编写两遍实在是太麻烦了... 有没有办法避免重复? 576 | - [C++23 deducing-this](https://en.cppreference.com/w/cpp/language/member_functions#Explicit_object_parameter) 能帮得上忙 577 | 578 | 假如没有额外的语法特性... 能不能让一个函数调用另一个? 579 | 580 | 假如我们让 non-`const` 版本的函数调用 `const` 版本的函数: 581 | - 首先,我们需要显式地为 `this` 添加底层 `const`。 582 | - `const` 版本的函数返回的是 `const int &`,我们得把它的底层 `const` 去除。 583 | 584 | --- 585 | 586 | ## 在 `const` vs non-`const` 重载中避免重复 587 | 588 | - 先用 `static_cast(this)` 为 `this` 添加底层 `const` 589 | - 这时调用 `->at(n)`,就会匹配 `const` 版本的 `at` 590 | - 将返回的 `const int &` 的底层 `const` 用 `const_cast` 去除 591 | 592 | ```cpp 593 | class Dynarray { 594 | public: 595 | const int &at(std::size_t n) const { 596 | if (n >= m_length) 597 | throw std::out_of_range{"Dynarray subscript out of range."}; 598 | log_access(); 599 | verify_integrity(); 600 | return m_storage[n]; 601 | } 602 | int &at(std::size_t n) { 603 | return const_cast(static_cast(this)->at(n)); 604 | } 605 | }; 606 | ``` 607 | 608 | --- 609 | 610 | ## 在 `const` vs non-`const` 重载中避免重复 611 | 612 | 能不能反过来,让 `const` 版本调用 non-`const` 版本? 613 | 614 | ```cpp 615 | class Dynarray { 616 | public: 617 | int &at(std::size_t n) { 618 | if (n >= m_length) 619 | throw std::out_of_range{"Dynarray subscript out of range."}; 620 | log_access(); 621 | verify_integrity(); 622 | return m_storage[n]; 623 | } 624 | const int &at(std::size_t n) const { 625 | return const_cast(this)->at(n); 626 | } 627 | }; 628 | ``` 629 | 630 | --- 631 | 632 | ## 在 `const` vs non-`const` 重载中避免重复 633 | 634 | 能不能反过来,让 `const` 版本调用 non-`const` 版本? 635 | - **不能**!`const` 成员函数里一定不会修改对象的状态,但是 non-`const` 成员函数并没有这般承诺! 636 | - 如果在 non-`const` 版本的实现里一不小心修改了对象的状态,让 `const` 版本调用它将导致灾难。 637 | 638 | 比较一下两种方法中对于“危险的” `const_cast` 的使用? 639 | - “先添加,再去除”:OK 640 | - “先去除,再添加”:危险 641 | 642 | --- 643 | 644 | # `static` 成员 645 | 646 | --- 647 | 648 | ## `static` 数据成员 649 | 650 | 假如我们想要为我们的类的每个对象都设置一个独一无二的编号 651 | 652 | ```cpp 653 | int cnt = 0; 654 | class Dynarray { 655 | int *m_storage; 656 | std::size_t m_length; 657 | int m_id; 658 | public: 659 | Dynarray(std::size_t n) 660 | : m_storage(new int[n]{}), m_length(n), m_id(cnt++) {} 661 | Dynarray() : m_storage(nullptr), m_length(0), m_id(cnt++) {} 662 | // ... 663 | }; 664 | ``` 665 | 666 | 使用一个全局变量 `cnt` 作为“计数器”。这样做好吗? 667 | 668 | --- 669 | 670 | ## `static` 数据成员 671 | 672 | 假如有很多类都需要这样的编号怎么办? 673 | 674 |
675 |
676 | 677 | ```cpp 678 | int X_cnt = 0, Y_cnt = 0, Z_cnt = 0; 679 | struct X { 680 | X() : m_id(X_cnt++) {} 681 | }; 682 | struct Y { 683 | Y() : m_id(Y_cnt++) {} 684 | }; 685 | struct Z { 686 | Z() : m_id(Z_cnt++) {} 687 | }; 688 | ``` 689 |
690 |
691 | 692 | - 散落满地的全局变量,乱! 693 | - 而且毫无保护:假如用错了会怎样? 694 | 695 | ```cpp 696 | struct Y { 697 | Y() : m_id(X_cnt++) {} 698 | }; 699 | ``` 700 | 701 | - 错误会悄无声息地发生。 702 |
703 |
704 | 705 | --- 706 | 707 | ## `static` 数据成员 708 | 709 | **\* 将这个计数器限定在类的作用域内**:将它定义为类的 `static` 成员。 710 | 711 | ```cpp 712 | class Dynarray { 713 | static int s_cnt; // !!! 714 | int *m_storage; 715 | std::size_t m_length; 716 | int m_id; 717 | public: 718 | Dynarray(std::size_t n) 719 | : m_storage(new int[n]{}), m_length(n), m_id(s_cnt++) {} 720 | Dynarray() : m_storage(nullptr), m_length(0), m_id(s_cnt++) {} 721 | // ... 722 | }; 723 | ``` 724 | 725 | 根据 C++ 的规则,你还需要在类外初始化一下它: 726 | ```cpp 727 | int Dynarray::s_cnt = 0; 728 | ``` 729 | 730 | --- 731 | 732 | ## `static` 数据成员 733 | 734 | `static` 数据成员:一个限定在类的作用域内的“全局变量” 735 | 736 | - 限定在类的作用域内:受访问限制说明符的影响,如果是 `private`,那么外部代码不可访问。 737 | - “全局变量”:每个程序只有一个 `s_cnt`,这个 `s_cnt` 不属于任何的对象,而是属于这个类。 738 | 739 | **访问:`A::s_cnt`**,而非 `a.s_cnt`。 740 | 741 | - 实际上 `a.s_cnt` 也可以,但是 `a.s_cnt` 和 `b.s_cnt` 访问的是同一个 `s_cnt`。 742 | 743 | --- 744 | 745 | ## `static` 数据成员 746 | 747 | 练习:为你的 `Dynarray` 添加 `find` 函数,模仿 `std::string` 的 `find`。如果所查找的元素不存在,模仿 `std::string::npos` 返回一个 `Dynarray::npos`。 748 | 749 | ```cpp 750 | class Dynarray { 751 | public: 752 | static std::size_t npos; 753 | std::size_t find(int x) /* 它应该是 const 吗? */ { 754 | // 如果没找到 `x`,return npos; 755 | } 756 | }; 757 | 758 | std::size_t Dynarray::npos = -1; 759 | ``` 760 | 761 | --- 762 | 763 | ## `static` 成员函数 764 | 765 | 类似于 `static` 数据成员,我们可以定义一个**限定在类的作用域内**,但**不属于任何对象**,而是**属于这个类**的函数: 766 | 767 | ```cpp 768 | class A { 769 | int x; 770 | public: 771 | static int fun() { 772 | return 42; 773 | } 774 | static int foo() { 775 | return x; // 错误:foo 是 static 成员函数,它不属于任何对象,无法知道访问了谁的 x 776 | // static 成员函数不含有 this 指针 777 | } 778 | }; 779 | 780 | std::cout << A::fun() << std::endl; // 42 781 | ``` 782 | 783 | --- 784 | 785 | ## 单例 (singleton) 模式 786 | 787 | 假如我们定义了一个类 `Widget`。如果我们希望这个世界上只有一个 `Widget` 类型的对象,怎么办? 788 | 789 | --- 790 | 791 | ## 单例 (singleton) 模式 792 | 793 | 假如我们定义了一个类 `Widget`。如果我们希望这个世界上只有一个 `Widget` 类型的对象,怎么办? 794 | 795 | - 只能我们自己创建这个对象:所有构造函数都是 `private` 的 796 | - 禁止拷贝 797 | 798 | --- 799 | 800 | ## 单例 (singleton) 模式 801 | 802 | ```cpp 803 | class Widget { 804 | // 构造函数是 private 的 805 | Widget(); 806 | // ... 807 | public: 808 | Widget(const Widget &) = delete; 809 | Widget &operator=(const Widget &) = delete; 810 | // Magic happens here!! 811 | static Widget &get_instance() { 812 | static Widget w; // 这个 static 是什么意思? 813 | return w; 814 | } 815 | }; 816 | ``` 817 | 818 | 外部代码获得这个对象的唯一方式是 `Widget::get_instance()`。 819 | 820 | --- 821 | 822 | # 初识类型推导:`auto` 823 | 824 | --- 825 | 826 | ## 为何需要类型推导? 827 | 828 | 有些类型是很复杂的,它们具有很长的名字: 829 | 830 | ```cpp 831 | std::vector>::const_reverse_iterator 832 | ``` 833 | 834 | 哪怕是中等长度的 `std::vector::iterator`,老这么写谁也受不了 835 | 836 | 还有一些东西的类型无法写出来,比方说 lambda 表达式 837 | 838 | --- 839 | 840 | ## `auto` 类型推导 841 | 842 | ```cpp 843 | auto i = 42; // `i` is int 844 | auto j = i; // `j` is int 845 | auto len = s.size(); // `len` is std::string::size_type, 846 | // which is std::size_t 847 | auto x = len + i; // `x` is std::size_t 848 | ``` 849 | 850 | 可以定义复合类型: 851 | 852 | ```cpp 853 | auto &ref = i; // `ref` is int& 854 | std::vector vs{"hello", "world"}; 855 | for (const auto &s : vs) // `s` is const std::string & 856 | std::cout << s << std::endl; 857 | ``` 858 | 859 | --- 860 | 861 | ## `auto` 类型推导 862 | 863 | C++14 起,还允许用 `auto` 推断返回值类型(在一定条件下) 864 | 865 | ```cpp 866 | auto add(int a, int b) { 867 | return a + b; // return type is int 868 | } 869 | class Dynarray { 870 | auto size() const { 871 | return m_length; // return type is std::size_t 872 | } 873 | auto &at(std::size_t n) { 874 | return m_storage[n]; // return type is int& 875 | } 876 | const auto &at(std::size_t n) const { 877 | return m_storage[n]; // return type is const int & 878 | } 879 | }; 880 | ``` 881 | --------------------------------------------------------------------------------