[{"content":" 知识点 # 2.1 常量, 变量, 整形, 实型, 字符型 # @ 数据类型图 ![[白板/P1 数据类型.canvas|P1 数据类型]] 2.1.1 如何创建基本的能运行的 C 程序 # 2.1.2 C 语言的数据类型 # 数据类型 32 位系统（字节） 64 位系统 最小值 最大值 char 1 1 -128 127 short 2 2 -32767 32768 unsigned short 2 2 0 65536 int 4 4 -2147483648 2147483647 unsigned int 4 4 0 4294967295 float 4 4 double 8 8 long 4 8 -2147483648 2147483647 long long 8 8 -9223372036854775807 9223372036854775808 unsigned long long 8 8 0 18446744073709551615 2.1.3 常量和变量 # [!tip] 常量和变量\n常量: ==在程序运行过程中, 其值不能被改变的量==\n整形常量 浮点型(实型)常量 字符常量: 用一对单引号包含起来的一个字符, 如`a` 变量: ==其值可以改变的量==\n! 变量肯定会有一个变量名, 在内存中会占用一定的存储空间,变量名就是一个标识符，并且大小写敏感 @ 标识符: 由字母, 数字, 下划线三种字符组成, 并且第一个字符为字母或者下划线\n! 第一个字符绝对不能是数字 @ 保留字: 系统保留起来, 有特殊用途\n! 不能将保留字作为标识符来使用 变量的定义 # [!tip] 定义变量\n! 定义变量的声明格式 类型名 变量名 = 变量初值; int a = 0; 2.1.4 整型数据 # [!tip] 整型数据\n十进制数 - $ %d 默认显示十进制数 八进制数: 以 0 开头的数字是八进制数 十六进制数:==以 0x 开头的数是十六进制数== 整型变量的分类 # 基本型: int 短整型: short int, 简写 short 长整型: long int, 简写 long 无符号型: unsigned int unsigned short unsigned long ! 无符号整数,不能存放负数, 所以无符号型存放的数据范围比带符号整形变量存放的数字范围大一倍 % sizeof() 运算符可以计算变量的数据类型的大小, 但是无法计算变量的具体大小 int a; printf(\u0026#34;%d字节\u0026#34;,sizeof(a)); // 输出: 4字节 整型变量的定义 # int a = 0; int a = b = c = 0; 常量的类型 # [!Note]\n% 在一个常数后面加一个字母 U 或 u, 表示该常数用无符号整型方式存储, 相当于 unsigned int % 在一个常数后面加一个字母 L 或 l, 表示该常数用长整型方式存储, 相当于 long % 在一个常数后面加一个字母 F 或 f, 表示该常数用浮点方式存储, 相当于 float　! 常量的类型并不会改变常量赋值的变量的数据类型 int a = 23.12F; //变量a依旧是int型 2.1.5 实型数据 # @ 实型数据: 简称实数, 在 C 语言中称为==浮点数 (带小数部分的数)== 实型常量的两种表示形式 # 十进制数表示形式: 0.12, 3.14159 指数表示形式: 168E3 等价于 168×10³=16800.0 字母 E 可以大写也可以小写 168E+2 等价于 168×10² 168E-8 等价于 168×10⁻⁸ 实型变量的分类 # % 单精度: float 类型, 占 4 字节 % 双精度: double 类型, 占 8 字节 实型变量的定义 # float d, e, f; double i, k; 精度问题 # [!tip] 浮点数精度\nfloat 类型: 提供 7 位有效数字 double 类型: 提供 15-16 位有效数字 ! 浮点数在内存中以指数形式存储, 能够存储的数据范围很大 类型 取值范围 float 3.4E-38 ~ 3.4E+38 double 1.7E-308 ~ 1.7E+308 ! 将十进制数赋给实型变量时, 会先转换成二进制存储, 再转换回十进制显示, 这个过程可能导致精度丢失 float af = 0.51; // 实际显示可能是 0.509999990 2.1.6 字符型数据 # 字符常量 # @ 字符常量: 用单引号包含起来的一个字符, 如 'a', '$' ! 单引号中只能放一个字符, 放多个字符无法得到正确结果 转义字符 # [!tip] 转义字符 转义字符是以 \\ 开头的字符序列, 将反斜杠后面的字符转变成另外一种含义\n转义字符 含义 \\n 换行 \\t 制表符(Tab键) \\\\ 反斜杠 \\' 单引号 \\\u0026quot; 双引号 \\ddd 1-3位八进制数所代表的字符 \\xhh 1-2位十六进制数所代表的字符 \\0 空字符(字符串结束标记) printf(\u0026#34;abc\\ndef\\\u0026#39;ghi\\\u0026#34;jkl\u0026#34;); // 输出: // abc // def\u0026#39;ghi\u0026#34;jkl 字符变量 # @ 字符变量: 用来存放字符常量, 只能存一个字符 ! 字符变量在内存中只占 1 字节 char c1, c2, c3; c1 = \u0026#39;a\u0026#39;; c2 = \u0026#39;b\u0026#39;; c3 = \u0026#39;\\\u0026#39;\u0026#39;; // 转义字符, 代表单引号 字符与 ASCII 码 # @ ASCII 码: 范围处于 0~127 之间的一个整数 ! 字符数据在内存中以 ASCII 码形式存储, 与整型数据存储形式类似 ! 字符型数据和整型数据可以互通使用 十进制(ASCII码) 字符 十进制(ASCII码) 字符 97 a 98 b 99 c 100 d 65 A 66 B char c1, c2; c1 = 97; // 相当于 c1 = \u0026#39;a\u0026#39; c2 = 98; // 相当于 c2 = \u0026#39;b\u0026#39; printf(\u0026#34;c1=%c, c2=%c\\n\u0026#34;, c1, c2); // c1=a, c2=b printf(\u0026#34;c1=%d, c2=%d\\n\u0026#34;, c1, c2); // c1=97, c2=98 2.1.7 字符串常量 # @ 字符串常量: 用双引号包含起来的一堆字符, 如 \u0026quot;Hello world!\u0026quot;, \u0026quot;zhangsan\u0026quot; ! 不要混淆字符常量和字符串常量: 'a' 是字符常量, \u0026quot;a\u0026quot; 是字符串常量 字符与字符串的区别 # ! 字符 'a' 在内存中占 1 字节 ! 字符串 \u0026quot;a\u0026quot; 在内存中占 2 字节 (包含 'a' 和 '\\0') @ 字符串末尾自动添加 '\\0' 作为字符串结束标记 char c; c = \u0026#39;a\u0026#39;; // 正确 c = \u0026#34;a\u0026#34;; // 错误! 不能把字符串赋值给字符变量 ! '\\0' 如果出现在 printf 中进行输出, 不会输出任何内容, 而是标记字符串结束 printf(\u0026#34;abc\\0def\u0026#34;); // 输出: abc,系统自动截断 2.1.8 不同类型数据混合运算 # [!tip] 自动类型转换\n! 不同类型数值变量进行混合运算时, 系统会尝试将它们的变量类型统一, 选取能表达最大数字的类型作为目标类型 ! 转换规则: 从低类型往高类型转换 高 double ↑ float ↑ unsigned long ↑ long ↑ unsigned int ↑ int ↑ char, short 低 % char 和 short 参与运算时, 必须先转成 int % float 参与运算时, 必须先转成 double 2.2 算术运算符和表达式 # @ 运算符图 ![[白板/P2 运算符.canvas|P2 运算符]] 2.2.1 C 语言的运算符分类 # 序号 名称 内容 1 算术运算符 +, -, *, /, % 2 关系运算符 \u0026gt;, \u0026lt;, ==, \u0026gt;=, \u0026lt;=, != 3 逻辑运算符 !, \u0026amp;\u0026amp;, || 4 位运算符 \u0026lt;\u0026lt;, \u0026gt;\u0026gt;, ~, |, ^, \u0026amp; 5 赋值运算符 = 以及复合赋值运算符 6 条件运算符 ?: 7 逗号运算符 , 8 指针运算符 *, \u0026amp; 9 求字节数运算符 sizeof() 10 强制类型转换运算符 (类型名) 11 成员变量运算符 ., -\u0026gt; 12 下标运算符 [] 2.2.2 算术运算符和算术表达式 # [!tip] 基本算术运算符\n+ 加法, 如 3+5 - 减法, 如 5-2, 2-5 * 乘法, 如 3*9 / 除法, 如 5/3 (整数相除会舍弃小数部分) % 取余(模运算), 两侧都要求为整型数, 如 7%4=3 printf(\u0026#34;5 + 2 = %d\\n\u0026#34;, 5 + 2); // 5 + 2 = 7 printf(\u0026#34;5 - 2 = %d\\n\u0026#34;, 5 - 2); // 5 - 2 = 3 printf(\u0026#34;2 - 5 = %d\\n\u0026#34;, 2 - 5); // 2 - 5 = -3 printf(\u0026#34;3 * 9 = %d\\n\u0026#34;, 3 * 9); // 3 * 9 = 27 printf(\u0026#34;5 / 3 = %d\\n\u0026#34;, 5 / 3); // 5 / 3 = 1 (舍弃小数) printf(\u0026#34;7 %% 4 = %d\\n\u0026#34;, 7 % 4); // 7 % 4 = 3 2.2.3 运算符优先级 # ! 优先级规则: 先乘除, 后加减, 优先级相同则从左到右 ! 记不住优先级时, 用 () 将需要优先计算的内容括起来 printf(\u0026#34;2 + 3 * 5 = %d\\n\u0026#34;, 2+3*5); // 2+15=17 printf(\u0026#34;2 + 8 - 5 = %d\\n\u0026#34;, 2+8-5); // 10-5=5 printf(\u0026#34;(3+2) * 8 = %d\\n\u0026#34;, (3+2)*8); // 5*8=40 运算符优先级表 # 优先级 运算符 含义 对象个数 结合方向 1 () [] -\u0026gt; . 圆括号/下标/成员 从左到右 2 ! ~ ++ -- - (类型) * \u0026amp; sizeof 单目运算符 1个 从右到左 3 * / % 乘除取余 2个 从左到右 4 + - 加减 2个 从左到右 5 \u0026lt;\u0026lt; \u0026gt;\u0026gt; 左移右移 2个 从左到右 6 \u0026lt; \u0026lt;= \u0026gt; \u0026gt;= 关系运算符 2个 从左到右 7 == != 等于不等于 2个 从左到右 8 \u0026amp; 按位与 2个 从左到右 9 ^ 按位异或 2个 从左到右 10 | 按位或 2个 从左到右 11 \u0026amp;\u0026amp; 逻辑与 2个 从左到右 12 || 逻辑或 2个 从左到右 13 ?: 条件运算符 3个 从右到左 14 = += -= *= /= %= 赋值运算符 2个 从右到左 15 , 逗号运算符 从左到右 2.2.4 强制类型转换运算符 # @ 强制类型转换: 将一个表达式转换成所需要的类型 ! 一般形式: (类型名)(表达式) (double)a // 将a转换成double型 (int)(x+y) // 将x+y的值转换成int型 (float)(5%3) // 将5%3的结果转换成float型 ! 强制类型转换得到一个所需类型的中间变量, 原来变量的类型没有发生变化 float x = 3.6; int i; i = (int)x; // i = 3, 但x依旧是float类型 printf(\u0026#34;x=%f, i=%d\\n\u0026#34;, x, i); // x=3.600000, i=3 两种类型转换总结 # % 自动类型转换: 混合运算时系统自动进行, 如 3+5.8 % 强制类型转换: 当自动类型转换无法达到目的时使用, 如 (int)x%3 2.2.5 自增和自减运算符 # [!tip] 自增自减运算符\n++ 自增运算符: 使变量值增加 1 -- 自减运算符: 使变量值减少 1 ! 都是单目运算符, 只能用于变量, 不能用于常量或表达式 使用口诀 # ++i 先加 1 后使用 --i 先减 1 后使用 i++ 先使用后加 1 i-- 先使用后减 1 单独使用 # int i = 8; i++; // i = 8 + 1 = 9 i--; // i = 9 - 1 = 8 ++i; // i = 8 + 1 = 9 --i; // i = 9 - 1 = 8 printf(\u0026#34;i = %d\\n\u0026#34;, i); // i = 8 在表达式中使用 # int i = 3; printf(\u0026#34;j = %d\\n\u0026#34;, ++i); // j = 4, 先加后用 i = 3; printf(\u0026#34;j = %d\\n\u0026#34;, i++); // j = 3, 先用后加 ! 自增自减运算符常用于循环语句中循环变量自增或自减 2.3 赋值运算符和逗号运算符 # @ 赋值运算符图 ![[白板/P3 赋值运算符.canvas|P3 赋值运算符]] 2.3.1 赋值运算符和赋值表达式 # @ 表达式：用运算符串起来的一个式子 赋值运算符 # @ 赋值运算符: =, 含义是将等号右边的值赋给等号左边的变量 ! 等号左边必须是一个变量 int a; a = 5; a = 6.5; // 将实型数据给整型变量, 小数部分被舍弃, a = 6 a = 18+50; // 先计算右边表达式, 再赋值, a = 68 赋值原则 # ! 赋值符号左侧和右侧的数据类型应该相同 ! 类型不同时可用强制类型转换, 但要确保不会造成数据溢出 int a; a = 1000000 * 1000000; // 溢出! 结果不可预料 复合赋值运算符 # @ 在赋值运算符 = 之前增加其他运算符, 构成复合赋值运算符 a += 3; // 等价于 a = a + 3 x *= y+8; // 等价于 x = x * (y+8) x %= 3; // 等价于 x = x % 3 ! 复合赋值运算符的优先级比 + 低, 右侧是一个整体 赋值表达式的值 # @ 赋值表达式本身是有值的, 值就是赋值运算符右侧的值 int a; printf(\u0026#34;a=5这个赋值表达式的值是%d\\n\u0026#34;, a=5); // 值是5 ! 赋值运算符的结合顺序是从右到左 int a, b; a = b = 5; // 5赋给b, b=5的值为5, 再赋给a 2.3.2 逗号运算符和逗号表达式 # @ 逗号运算符: ,, 是优先级最低的运算符 @ 逗号表达式: 用逗号将两个表达式连接起来 ! 一般形式: 表达式1, 表达式2 ! 求解过程: 先求解表达式1, 再求解表达式2, 整个表达式的值是表达式2的值 int a; a = (4,5); // a = 5, 必须加()因为逗号优先级最低 a = (3+5, 6+8); // a = 14 a = 3*5, a*4; // 表达式值为60, 先计算a=15, 再计算a*4 扩展形式 # ! 多个表达式用逗号连接: 表达式1, 表达式2, 表达式3, ..., 表达式N ! 整个表达式的值是表达式N的值 int x, a; x = (a=3, 6*3); // a = 3, x = 18 x = a = 3, 6*a; // a = 3, x = 3, 逗号表达式的结果18未被使用 int result = (x = a = 3, 6*a); // result = 18 ! 并非任何出现逗号的地方都是逗号表达式, 如 printf(\u0026quot;%d,%d,%d\u0026quot;, 3, 4, 5); 中的 3,4,5 是函数参数 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p1-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E8%BF%90%E7%AE%97%E7%AC%A6%E4%B8%8E%E8%A1%A8%E8%BE%BE%E5%BC%8F/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e2.1 常量, 变量, 整形, 实型, 字符型\n    \u003cdiv id=\"21-常量-变量-整形-实型-字符型\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#21-%e5%b8%b8%e9%87%8f-%e5%8f%98%e9%87%8f-%e6%95%b4%e5%bd%a2-%e5%ae%9e%e5%9e%8b-%e5%ad%97%e7%ac%a6%e5%9e%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 数据类型图\n\u003cul\u003e\n\u003cli\u003e![[白板/P1 数据类型.canvas|P1 数据类型]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e2.1.1 如何创建基本的能运行的 C 程序\n    \u003cdiv id=\"211-如何创建基本的能运行的-c-程序\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#211-%e5%a6%82%e4%bd%95%e5%88%9b%e5%bb%ba%e5%9f%ba%e6%9c%ac%e7%9a%84%e8%83%bd%e8%bf%90%e8%a1%8c%e7%9a%84-c-%e7%a8%8b%e5%ba%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\n\u003ch3 class=\"relative group\"\u003e2.1.2 C 语言的数据类型\n    \u003cdiv id=\"212-c-语言的数据类型\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#212-c-%e8%af%ad%e8%a8%80%e7%9a%84%e6%95%b0%e6%8d%ae%e7%b1%bb%e5%9e%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e数据类型\u003c/th\u003e\n          \u003cth\u003e32 位系统（字节）\u003c/th\u003e\n          \u003cth\u003e64 位系统\u003c/th\u003e\n          \u003cth\u003e最小值\u003c/th\u003e\n          \u003cth\u003e最大值\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003echar\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e1\u003c/td\u003e\n          \u003ctd\u003e1\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e-128\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e127\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eshort\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e-32767\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e32768\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eunsigned short\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e65536\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eint\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e-2147483648\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e2147483647\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eunsigned int\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e4294967295\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003efloat\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edouble\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003elong\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e-2147483648\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e2147483647\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003elong long\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e-9223372036854775807\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e9223372036854775808\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eunsigned long long\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e18446744073709551615\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch3 class=\"relative group\"\u003e2.1.3 常量和变量\n    \u003cdiv id=\"213-常量和变量\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#213-%e5%b8%b8%e9%87%8f%e5%92%8c%e5%8f%98%e9%87%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 常量和变量\u003c/p\u003e","title":" P1 数据类型,运算符,与表达式","type":"notes"},{"content":" 知识点 # 3.1 程序的基本结构 # @ 程序结构图 ![[白板/P4 程序结构.canvas|P4 程序结构]] 3.1.1 C 语言的语句 # [!tip] C 语言语句\n@ 语句: C 语言程序的基本执行单位，以分号 ; 结尾 % 语句分类: 表达式语句 函数调用语句 控制语句 复合语句 空语句 语句的书写规则 # ! 一行可以写多个语句，每个语句以 ; 结尾 ! 一个语句可以分多行写（当一行代码过长时） printf(\u0026#34;断点停止在\\ 这里\\n\u0026#34;); // 语句分多行书写 3.1.2 程序的三种基本结构 # [!tip] 结构化程序设计\n@ 结构化程序设计: 使程序结构清晰、可读性强，提高程序设计的质量 % 三种基本结构: 顺序结构 选择结构 循环结构 1. 顺序结构 # @ 顺序结构: 先执行 A 操作，再执行 B 操作 ! 语句从上到下、从左到右逐条执行 printf(\u0026#34;1\\n\u0026#34;); printf(\u0026#34;2\\n\u0026#34;); printf(\u0026#34;3\\n\u0026#34;); // 输出: 1 2 3 2. 选择结构 # @ 选择结构: 根据条件 P 选择执行 A 或 B 操作 ! 条件为真执行 A，条件为假执行 B if(3 \u0026gt; 1) printf(\u0026#34;3 \u0026gt; 1\\n\u0026#34;); // 这行被执行 else printf(\u0026#34;3 \u0026lt; 1\\n\u0026#34;); // 这行不会被执行 多分支选择结构 # % k 与 k1, k2, \u0026hellip;, kn 比较，选择执行对应的操作 % 典型语句: switch 语句 int k = 5; switch(k) { case 1: printf(\u0026#34;k = 1\\n\u0026#34;); // 不会被执行 break; case 2: printf(\u0026#34;k = 2\\n\u0026#34;); // 不会被执行 break; case 5: printf(\u0026#34;k = 5\\n\u0026#34;); // 会被执行 break; } 3. 循环结构 # 当型循环结构 (while) # @ 当型循环: 先判断条件 P，再决定是否执行 A 操作 ! 条件 P 为真时反复执行 A，直到条件为假 int icount = 5; while(icount \u0026gt;= 0) { printf(\u0026#34;icount = %d\\n\u0026#34;, icount); // 5,4,3,2,1,0 icount = icount - 1; } 直到型循环结构 (do-while) # @ 直到型循环: 先执行一次 A 操作，再判断条件 P ! 至少执行一次 A 操作 ! 条件 P 为真时继续执行 A int icount = 5; do { printf(\u0026#34;icount = %d\\n\u0026#34;, icount); // 5,4,3,2,1,0 icount = icount - 1; } while(icount \u0026gt;= 0); 3.1.3 赋值语句的特殊写法 # [!warning] 注意\n! 赋值表达式可以被包含在其他表达式中 ! 但不推荐这种写法，程序清晰性和易读性更重要 int x; printf(\u0026#34;x = 8的值是%d\\n\u0026#34;, x = 8); // x = 8的值是8 int a = 3, b = 5, t = 1; if((a = b) \u0026gt; 0) t = a; // 执行后 a = 5 printf(\u0026#34;a = %d\\n\u0026#34;, a); // a = 5 3.2 数据的输出与数据的输入 # @ 输入输出图 ![[白板/P5 输入输出.canvas|P5 输入输出]] 3.2.1 数据的输出 # 1. putchar 函数 # [!tip] putchar 函数\n@ putchar: 向屏幕输出一个字符 格式: putchar(c); c 可以是字符型变量或整型变量 #include \u0026lt;stdio.h\u0026gt; int main() { char a, b, c; a = \u0026#39;F\u0026#39;; b = \u0026#39;A\u0026#39;; c = \u0026#39;T\u0026#39;; putchar(a); // F putchar(b); // A putchar(c); // T putchar(\u0026#39;\\n\u0026#39;); // 换行 a = 97; // 字符a的ASCII码 b = 98; // 字符b的ASCII码 putchar(a); // a putchar(\u0026#39;\\n\u0026#39;); // 换行 putchar(b); // b return 0; } #include 命令说明 # ! #include \u0026lt;stdio.h\u0026gt; - 在系统目录中寻找头文件 ! #include \u0026quot;stdio.h\u0026quot; - 先在当前目录寻找，再到系统目录 % 系统头文件用 \u0026lt;\u0026gt;，自定义头文件用 \u0026quot;\u0026quot; 2. printf 函数 # [!tip] printf 函数\n@ printf: 向屏幕输出若干个任意类型数据 格式: printf(格式控制字符串, 输出表列); 格式控制字符串包含: 原样输出的普通字符 格式字符 (%d, %f, %c 等) printf 格式字符 # 格式字符 含义 示例 %d 十进制整数 printf(\u0026quot;%d\u0026quot;, 10); → 10 %o 八进制数 printf(\u0026quot;%o\u0026quot;, 15); → 17 %x 十六进制数 printf(\u0026quot;%x\u0026quot;, 15); → f %u 无符号十进制 printf(\u0026quot;%u\u0026quot;, 100); → 100 %c 字符 printf(\u0026quot;%c\u0026quot;, 'a'); → a %s 字符串 printf(\u0026quot;%s\u0026quot;, \u0026quot;CHINA\u0026quot;); → CHINA %f 浮点数 printf(\u0026quot;%f\u0026quot;, 2.15+3.12); → 5.270000 格式字符详细说明 # %d 十进制整数\nint a = 10; printf(\u0026#34;a = %d\\n\u0026#34;, a); // a = 10 %o 八进制数\n! 输出的数字不能是负数 int abc = 15; printf(\u0026#34;abc = %o\\n\u0026#34;, abc); // abc = 17 (八进制) %x 十六进制数\n! 输出的数字不能是负数 int abc = 15; printf(\u0026#34;abc = %x\\n\u0026#34;, abc); // abc = f (十六进制) %u 无符号十进制\n! 用于输出 unsigned 类型数据 ! 无符号类型表示的数据范围比有符号类型大 1 倍 short aaa = -10000; printf(\u0026#34;aaa = %u\\n\u0026#34;, aaa); // aaa = 4294957296 (意外值) printf(\u0026#34;aaa = %d\\n\u0026#34;, aaa); // aaa = -10000 (正确) %c 字符\n! 整数范围 0-255 可以用字符形式输出（ASCII码） int a = 97; printf(\u0026#34;%c\\n\u0026#34;, a); // a (输出ASCII码对应的字符) char c = \u0026#39;a\u0026#39;; printf(\u0026#34;%d\\n\u0026#34;, c); // 97 (输出字符对应的ASCII码) %s 字符串\nprintf(\u0026#34;中国的英文拼写是%s\\n\u0026#34;, \u0026#34;CHINA\u0026#34;); // 中国的英文拼写是CHINA %f 浮点数\nfloat x = 2.15f, y = 3.12f; printf(\u0026#34;%f\\n\u0026#34;, x + y); // 5.270000 float abc = 0.789058f; printf(\u0026#34;%.4f\\n\u0026#34;, abc); // 0.7891 (小数点后4位) printf(\u0026#34;%f\\n\u0026#34;, abc); // 0.789058 printf 注意事项 # ! 格式字符要与输出的数据类型相匹配 ! 输出 % 的三种方法: printf(\u0026#34;5%%\\n\u0026#34;); // 5% (两个%输出一个%) printf(\u0026#34;%c\\n\u0026#34;, \u0026#39;%\u0026#39;); // % printf(\u0026#34;%%s\\n\u0026#34;); // %s 3.2.2 数据的输入 # [!warning] 注意\n! 数据输入不常用，实际项目中一般不从键盘输入数据 ! 输入错误可能导致程序异常 1. getchar 函数 # [!tip] getchar 函数\n@ getchar: 等待用户从键盘输入一个字符 格式: getchar(); 按 Enter 键后程序才继续执行 char c; c = getchar(); // 等待输入 putchar(c); // 输出输入的字符 printf(\u0026#34;%c\u0026#34;, getchar()); // 直接输出输入的字符 getchar 的特殊用途 # ! 可用于防止黑色执行窗口一闪而过 ! 放在 main 函数最后一行，等待用户输入 getchar 的注意事项 # ! 用户输入字符后按回车，实际输入了两个字符 ! 第二次 getchar() 会读取回车符 ! 建议整个程序只使用一次 getchar() 2. scanf 函数 # [!tip] scanf 函数\n@ scanf: 格式化输入函数，从键盘输入任意类型数据 格式: scanf(格式控制字符串, 地址表列); ! 参数必须是变量的地址，使用 \u0026amp; 运算符 #pragma warning(disable:4996) // 忽略scanf安全警告 int a, b, c; scanf(\u0026#34;%d%d%d\u0026#34;, \u0026amp;a, \u0026amp;b, \u0026amp;c); // 输入三个整数 printf(\u0026#34;a + b + c = %d\u0026#34;, a + b + c); scanf 输入格式 # ! 多个数据之间用空格、回车、Tab键分隔 ! 不能用逗号分隔（除非格式字符串中有逗号） // 格式字符串中有逗号，输入时也要用逗号 scanf(\u0026#34;%d,%d,%d\u0026#34;, \u0026amp;a, \u0026amp;b, \u0026amp;c); // 输入: 1,2,3 // 格式字符串中有冒号，输入时也要用冒号 scanf(\u0026#34;%d:%d:%d\u0026#34;, \u0026amp;a, \u0026amp;b, \u0026amp;c); // 输入: 1:2:3 scanf 注意事项 # 注意点 说明 地址运算符 参数必须用 \u0026amp;，不要丢掉 验证输入 建议在 scanf 后立即 printf 验证 输入结束 按回车结束输入，未输完数据会出错 实用性 学习有价值，实际工作几乎无实用价值 int a, b, c; scanf(\u0026#34;%d%d%d\u0026#34;, \u0026amp;a, \u0026amp;b, \u0026amp;c); // 验证输入是否正确 printf(\u0026#34;从scanf得到的三个变量值为%d, %d, %d\\n\u0026#34;, a, b, c); printf(\u0026#34;a + b + c = %d\u0026#34;, a + b + c); ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p2-%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%BB%93%E6%9E%84%E5%92%8C%E8%AF%AD%E5%8F%A5/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e3.1 程序的基本结构\n    \u003cdiv id=\"31-程序的基本结构\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#31-%e7%a8%8b%e5%ba%8f%e7%9a%84%e5%9f%ba%e6%9c%ac%e7%bb%93%e6%9e%84\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 程序结构图\n\u003cul\u003e\n\u003cli\u003e![[白板/P4 程序结构.canvas|P4 程序结构]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e3.1.1 C 语言的语句\n    \u003cdiv id=\"311-c-语言的语句\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#311-c-%e8%af%ad%e8%a8%80%e7%9a%84%e8%af%ad%e5%8f%a5\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] C 语言语句\u003c/p\u003e","title":"P2 程序的基本结构和语句","type":"notes"},{"content":" 知识点 # 4.1 关系运算符, 关系表达式与逻辑运算符, 逻辑表达式 # @ 关系与逻辑运算图 ![[白板/P6 关系与逻辑运算.canvas|P6 关系与逻辑运算]] 4.1.1 关系运算符和关系表达式 # 关系运算符 # @ 关系运算: 又称比较运算，将两个值进行比较，判断比较结果是否符合给定条件 C 语言提供了 6 种关系运算符 编号 关系运算符 含义 优先级 1 \u0026lt; 小于 高（前四个优先级相同） 2 \u0026lt;= 小于或等于 高 3 \u0026gt; 大于 高 4 \u0026gt;= 大于或等于 高 5 == 等于 低（后两个优先级相同） 6 != 不等于 低 关系运算符优先级要点 # ! 前四个关系运算符优先级相同，后两个优先级也相同，前四个高于后两个 ! 关系运算符优先级低于算术运算符 ! 关系运算符优先级高于赋值运算符 ! == 是两个等号，千万不能写成一个等号 =，否则变成赋值运算符 三种运算符优先级关系 # 高 算术运算符 关系运算符 低 赋值运算符 表达式范例 # c \u0026gt; a+b // 等价于 c \u0026gt; (a+b)，关系运算符优先级低于算术运算符 \u0026#34;+\u0026#34; a \u0026gt; b == c // 等价于 (a\u0026gt;b) == c，\u0026#34;\u0026gt;\u0026#34; 优先级高于 \u0026#34;==\u0026#34; a == b \u0026lt; c // 等价于 a == (b\u0026lt;c)，\u0026#34;\u0026lt;\u0026#34; 优先级高于 \u0026#34;==\u0026#34; a = b \u0026gt; c // 等价于 a = (b\u0026gt;c)，赋值运算符优先级低于关系运算符 \u0026#34;\u0026gt;\u0026#34; 关系表达式 # @ 关系表达式: 用关系运算符将两个表达式连接起来的式子 ! 关系表达式的值是一个逻辑值，即\u0026quot;真\u0026quot;或\u0026quot;假\u0026quot; ! 在 C 语言中，\u0026ldquo;真\u0026quot;用 true 表示，也可以用 1 表示；\u0026ldquo;假\u0026quot;用 false 表示，也可以用 0 表示 ! 关系表达式的结果值是 0 或 1 printf(\u0026#34;5\u0026gt;3的值为%d\\n\u0026#34;, 5\u0026gt;3); // 5\u0026gt;3的值为1 if(true == 1) printf(\u0026#34;true==1\\n\u0026#34;); // 这行被执行 else printf(\u0026#34;true!=1\\n\u0026#34;); // 如果 a=3, b=2, c=1 // (a\u0026gt;b) == c 为真，因为 a\u0026gt;b 的值为1，等于 c 的值 // b+c \u0026lt; a 为假，因为 b+c 的值为3，并不小于 a 4.1.2 逻辑运算符和逻辑表达式 # 逻辑运算符 # C 语言提供了三种逻辑运算符 编号 逻辑运算符 含义 运算分量 1 \u0026amp;\u0026amp; 逻辑与（and） 双目 2 || 逻辑或（or） 双目 3 ! 逻辑非（not） 单目 逻辑运算规则 # ! \u0026amp;\u0026amp; 全真出真，有假出假：两个运算分量必须全为真，结果才为真 ! \\|\\| 有真出真，全假出假：只要有一个为真，结果就为真 ! ! 取反：原来为真变假，原来为假变真 逻辑运算真值表 # a b !a !b a\u0026amp;\u0026amp;b a||b 真 真 假 假 真 真 真 假 假 真 假 真 假 真 真 假 假 真 假 假 真 真 假 假 逻辑运算符易错点 # ! 千万不要把 \u0026amp;\u0026amp; 写成 \u0026amp;（位运算符），程序可能不报错但结果不对 ! 千万不要把 \\|\\| 写成 \\|（位运算符），同样程序可能不报错但结果不对 ! 进行逻辑判断时，不等于 0 的数值全部被认为是 true ! 系统给出的逻辑运算结果不是 0（假）就是 1（真），不可能是其他数值 ! 作为参加逻辑运算的对象，是 0 就表示假，非 0 就表示真 printf(\u0026#34;4==true，结果为%d\\n\u0026#34;, 4==true); // 0（假），4不等于1 printf(\u0026#34;1==true，结果为%d\\n\u0026#34;, 1==true); // 1（真） printf(\u0026#34;4\u0026amp;\u0026amp;5，结果为%d\\n\u0026#34;, 4\u0026amp;\u0026amp;5); // 1（真），4和5都是非0，都为真 逻辑运算符优先级 # 优先级（数字越小越高） 运算符 1 逻辑非 ! 2 算术运算符 3 关系运算符 4 \u0026amp;\u0026amp; 和 || 5 赋值运算符 ! 逻辑运算符中的 \u0026amp;\u0026amp; 和 \\|\\| 优先级低于关系运算符 ! 逻辑运算符中的 ! 优先级高于算术运算符 a\u0026gt;b \u0026amp;\u0026amp; x\u0026gt;y // 等价于 (a\u0026gt;b) \u0026amp;\u0026amp; (x\u0026gt;y) a==b \\|\\| x==y // 等价于 (a==b) \\|\\| (x==y) !a \\|\\| a\u0026gt;b // 等价于 (!a) \\|\\| (a\u0026gt;b) \u0026#39;c\u0026#39; \u0026amp;\u0026amp; \u0026#39;d\u0026#39; // 比较的是字符的ASCII码，都不为0，结果为真 短路求值 # @ 短路求值: 只要最终的结果已经可以确定是真或假，求值过程便宣告终止 ! 这是面试常考的陷阱 \u0026amp;\u0026amp; 短路求值:\n只有 a 为真才需要判断 b，只有 a 和 b 都为真才需要判断 c 只要 a 为假，就不必判断 b 和 c int a = 6; if(3\u0026lt;5 \u0026amp;\u0026amp; (a=8)) // 3\u0026lt;5为真，继续执行 a=8，a = 8 printf(\u0026#34;a = %d\\n\u0026#34;, a); // a = 8 int a = 6; if(3\u0026gt;5 \u0026amp;\u0026amp; (a=8)) // 3\u0026gt;5为假，短路！a=8 不被执行 printf(\u0026#34;a = %d\\n\u0026#34;, a); printf(\u0026#34;a = %d\\n\u0026#34;, a); // a = 6 \\|\\| 短路求值:\n只要 a 为真就不必判断 b 和 c 只有 a 为假才需要判断 b int a = 6; if(3\u0026gt;5 \\|\\| (a=8)) // 3\u0026gt;5为假，继续执行 a=8，a = 8 printf(\u0026#34;a = %d\\n\u0026#34;, a); // a = 8 int a = 6; if(3\u0026lt;5 \\|\\| (a=8)) // 3\u0026lt;5为真，短路！a=8 不被执行 printf(\u0026#34;a = %d\\n\u0026#34;, a); printf(\u0026#34;a = %d\\n\u0026#34;, a); // a = 6 4.2 if 语句详解 # @ if语句图 ![[白板/P7 if语句.canvas|P7 if语句]] 4.2.1 if 语句的三种形式 # 第一种形式 # if(判断表达式) 语句; 含义：如果判断表达式中的条件满足，则执行对应的语句，否则直接跳过 int x = 8; if(x \u0026gt; 5) printf(\u0026#34;x值\u0026gt;5\\n\u0026#34;); // x值\u0026gt;5 // 执行多条语句必须用 {} 构成复合语句 int x = 8; if(x \u0026gt; 5) { printf(\u0026#34;x值\u0026gt;5\\n\u0026#34;); // x值\u0026gt;5 printf(\u0026#34;x值确实\u0026gt;5\\n\u0026#34;); // x值确实\u0026gt;5 } ! 如果不用 {}，if 语句在条件成立时只会执行 if 后面第一次遇到分号之前的语句 ! {} 后面没有分号 int x = 8; if(x \u0026gt; 5) printf(\u0026#34;x值\u0026gt;5\\n\u0026#34;); // 属于if printf(\u0026#34;x值确实\u0026gt;5\\n\u0026#34;); // 不属于if！不管条件是否成立都会执行 第二种形式 # if(判断表达式) 语句1; else 语句2; 含义：条件满足执行语句1，否则执行语句2 int x = 5, y = 2; if(x \u0026gt; y) printf(\u0026#34;x\u0026gt;y是OK的\\n\u0026#34;); // 这行被执行 else printf(\u0026#34;x\u0026gt;y不OK\\n\u0026#34;); // 这行不会被执行 ! 多条语句必须用 {} 括起来，否则会产生语法错误 // 错误写法！第二个printf导致if和else之间夹了不该出现的语句 int x = 5, y = 2; if(x \u0026gt; y) printf(\u0026#34;x\u0026gt;y是OK的1\\n\u0026#34;); printf(\u0026#34;x\u0026gt;y是OK的2\\n\u0026#34;); // 这行会导致语法错误 else printf(\u0026#34;x\u0026gt;y不OK1\\n\u0026#34;); 第三种形式 # if(判断表达式1) 语句1; else if(判断表达式2) 语句2; else if(判断表达式3) 语句3; else if(判断表达式m) 语句m; else 语句n; 含义：从上到下依次判断，哪个条件成立就执行对应的语句，都不成立则执行 else 的语句 int number = 380; int cost = 0; if(number \u0026gt; 500) cost = 1; else if(number \u0026gt; 300) // 本条件成立 cost = 2; // 这行被执行 else if(number \u0026gt; 100) cost = 3; else cost = 4; ! 最后一个 else 可以省略，省略后如果所有条件都不成立，if 语句不执行任何代码 if 语句说明 # ! 判断表达式一般为逻辑表达式或关系表达式 ! 判断表达式的值为 0 按\u0026quot;假\u0026quot;处理，非 0 按\u0026quot;真\u0026quot;处理 ! if 语句可以单独使用，但 else 语句不能单独使用，必须与 if 配对 if(15) // 条件成立，15非0 printf(\u0026#34;成立哦\\n\u0026#34;); // 本条语句会被执行 if(\u0026#39;a\u0026#39;) // 条件成立，\u0026#39;a\u0026#39;的ASCII码非0 printf(\u0026#34;条件为真哦\\n\u0026#34;); // 本条语句会被执行 4.2.2 if 语句的嵌套 # @ if 嵌套: 在 if 语句中包含一个或多个 if 语句 ! else 总是跟它上面最近的尚未配对的 if 进行配对，不要被代码对齐所迷惑 int x = 5; if(x \u0026gt; 1) if(x \u0026gt; 8) printf(\u0026#34;x\u0026gt;8\\n\u0026#34;); else // 这个else和第二个if配对！不是和第一个if if(x \u0026gt; 3) printf(\u0026#34;x\u0026gt;3\\n\u0026#34;); else printf(\u0026#34;x为其他值\\n\u0026#34;); ! 建议用 {} 明确配对关系，防止出错 int x = 5; if(x \u0026gt; 3) { if(x \u0026gt; 4) printf(\u0026#34;x\u0026gt;4\\n\u0026#34;); // 这个printf语句会被执行 } else { printf(\u0026#34;执行else\\n\u0026#34;); } 4.3 条件运算符和 switch 语句 # @ 条件与switch图 ![[白板/P8 条件与switch.canvas|P8 条件与switch]] 4.3.1 条件运算符 # [!tip] 条件运算符\n@ 条件运算符: ? :，C 语言中唯一的三目运算符 一般形式: 表达式1 ? 表达式2 : 表达式3 执行流程: 先求解表达式1，若为真则取表达式2的值，否则取表达式3的值 // 用if语句 int a = 4, b = 5, max; if(a \u0026gt; b) max = a; else max = b; // 用条件运算符（等价写法） max = (a \u0026gt; b) ? a : b; ! 条件运算符优先级高于赋值运算符 ! 条件运算符优先级低于关系运算符，所以 max = (a\u0026gt;b) ? a : b 可以写成 max = a\u0026gt;b ? a : b ! 条件运算符结合顺序是从右到左 a\u0026gt;b ? a : c\u0026gt;d ? c : d // 等价于 a\u0026gt;b ? a : (c\u0026gt;d ? c : d) 4.3.2 switch 语句 # [!tip] switch 语句\n@ switch 语句: 用于处理多分支选择，可读性比多个 else if 更好 格式: switch(表达式) { case 常量表达式1: 语句; break; case 常量表达式2: 语句; break; default: 语句; break; } int abc = 3; switch(abc) { case 1: printf(\u0026#34;值为1\\n\u0026#34;); break; case 2: printf(\u0026#34;值为2\\n\u0026#34;); break; case 3: printf(\u0026#34;值为3\\n\u0026#34;); // 这行会被执行 break; default: break; } switch 语句要点 # 要点 说明 表达式类型 可以是整型、字符型、枚举型表达式 case 值互不相同 每个 case 后的常量表达式值必须互不相同，否则编译错误 顺序无关 各 case 之间、case 和 default 之间的顺序没有影响 break 不可省略 绝不要忘记 break，否则会继续执行下一个 case default 可省略 省略后所有 case 不满足时整个 switch 不执行 忘记 break 的后果 # ! 如果在一个 case 的最后不加 break，程序执行流程会继续执行下一个 case 中的语句，而不管该 case 条件是否满足 ! 程序执行流程从满足条件的 case 作为入口一直执行下去，除非遇到 break 或整个 switch 执行完毕 int tees = 1; switch(tees) { case 1: printf(\u0026#34;值为1\\n\u0026#34;); // 这行会被执行 // 这里漏掉了 break! case 2: printf(\u0026#34;值为2\\n\u0026#34;); // 漏掉break的后果：这行也会被执行！ break; case 3: printf(\u0026#34;值为3\\n\u0026#34;); break; default: break; } // 输出: 值为1 值为2 多个 case 共用执行语句 # case 3: case 4: printf(\u0026#34;值为3或者4\\n\u0026#34;); // switch后表达式的值为3或4都执行这行代码 break; case 中多行语句 # ! case 中如果包含多行语句，不需要使用 {} 将多行语句括在一起 case 2: printf(\u0026#34;值为2\\n\u0026#34;); printf(\u0026#34;值为2\\n\u0026#34;); printf(\u0026#34;值为2\\n\u0026#34;); break; ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p3-%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97%E5%92%8C%E5%88%A4%E6%96%AD%E9%80%89%E6%8B%A9/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e4.1 关系运算符, 关系表达式与逻辑运算符, 逻辑表达式\n    \u003cdiv id=\"41-关系运算符-关系表达式与逻辑运算符-逻辑表达式\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#41-%e5%85%b3%e7%b3%bb%e8%bf%90%e7%ae%97%e7%ac%a6-%e5%85%b3%e7%b3%bb%e8%a1%a8%e8%be%be%e5%bc%8f%e4%b8%8e%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e7%ac%a6-%e9%80%bb%e8%be%91%e8%a1%a8%e8%be%be%e5%bc%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 关系与逻辑运算图\n\u003cul\u003e\n\u003cli\u003e![[白板/P6 关系与逻辑运算.canvas|P6 关系与逻辑运算]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e4.1.1 关系运算符和关系表达式\n    \u003cdiv id=\"411-关系运算符和关系表达式\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#411-%e5%85%b3%e7%b3%bb%e8%bf%90%e7%ae%97%e7%ac%a6%e5%92%8c%e5%85%b3%e7%b3%bb%e8%a1%a8%e8%be%be%e5%bc%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\n\u003ch4 class=\"relative group\"\u003e关系运算符\n    \u003cdiv id=\"关系运算符\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e5%85%b3%e7%b3%bb%e8%bf%90%e7%ae%97%e7%ac%a6\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e@ \u003cstrong\u003e关系运算\u003c/strong\u003e: 又称比较运算，将两个值进行比较，判断比较结果是否符合给定条件\u003c/li\u003e\n\u003cli\u003eC 语言提供了 6 种关系运算符\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e编号\u003c/th\u003e\n          \u003cth\u003e关系运算符\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n          \u003cth\u003e优先级\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e1\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026lt;\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e小于\u003c/td\u003e\n          \u003ctd\u003e高（前四个优先级相同）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026lt;=\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e小于或等于\u003c/td\u003e\n          \u003ctd\u003e高\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e3\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026gt;\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e大于\u003c/td\u003e\n          \u003ctd\u003e高\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026gt;=\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e大于或等于\u003c/td\u003e\n          \u003ctd\u003e高\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e5\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e==\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e等于\u003c/td\u003e\n          \u003ctd\u003e低（后两个优先级相同）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e6\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e!=\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e不等于\u003c/td\u003e\n          \u003ctd\u003e低\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch4 class=\"relative group\"\u003e关系运算符优先级要点\n    \u003cdiv id=\"关系运算符优先级要点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e5%85%b3%e7%b3%bb%e8%bf%90%e7%ae%97%e7%ac%a6%e4%bc%98%e5%85%88%e7%ba%a7%e8%a6%81%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e! 前四个关系运算符优先级相同，后两个优先级也相同，\u003cstrong\u003e前四个高于后两个\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e! 关系运算符优先级\u003cstrong\u003e低于\u003c/strong\u003e算术运算符\u003c/li\u003e\n\u003cli\u003e! 关系运算符优先级\u003cstrong\u003e高于\u003c/strong\u003e赋值运算符\u003c/li\u003e\n\u003cli\u003e! \u003ccode\u003e==\u003c/code\u003e 是两个等号，\u003cstrong\u003e千万不能写成一个等号\u003c/strong\u003e \u003ccode\u003e=\u003c/code\u003e，否则变成赋值运算符\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch4 class=\"relative group\"\u003e三种运算符优先级关系\n    \u003cdiv id=\"三种运算符优先级关系\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e4%b8%89%e7%a7%8d%e8%bf%90%e7%ae%97%e7%ac%a6%e4%bc%98%e5%85%88%e7%ba%a7%e5%85%b3%e7%b3%bb\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e高  算术运算符\n    关系运算符\n低  赋值运算符\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch4 class=\"relative group\"\u003e表达式范例\n    \u003cdiv id=\"表达式范例\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e8%a1%a8%e8%be%be%e5%bc%8f%e8%8c%83%e4%be%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e     \u003cspan class=\"c1\"\u003e// 等价于 c \u0026gt; (a+b)，关系运算符优先级低于算术运算符 \u0026#34;+\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 等价于 (a\u0026gt;b) == c，\u0026#34;\u0026gt;\u0026#34; 优先级高于 \u0026#34;==\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 等价于 a == (b\u0026lt;c)，\u0026#34;\u0026lt;\u0026#34; 优先级高于 \u0026#34;==\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// 等价于 a = (b\u0026gt;c)，赋值运算符优先级低于关系运算符 \u0026#34;\u0026gt;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003ch4 class=\"relative group\"\u003e关系表达式\n    \u003cdiv id=\"关系表达式\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e5%85%b3%e7%b3%bb%e8%a1%a8%e8%be%be%e5%bc%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e@ \u003cstrong\u003e关系表达式\u003c/strong\u003e: 用关系运算符将两个表达式连接起来的式子\u003c/li\u003e\n\u003cli\u003e! 关系表达式的值是一个逻辑值，即\u0026quot;真\u0026quot;或\u0026quot;假\u0026quot;\u003c/li\u003e\n\u003cli\u003e! 在 C 语言中，\u0026ldquo;真\u0026quot;用 \u003ccode\u003etrue\u003c/code\u003e 表示，也可以用 \u003ccode\u003e1\u003c/code\u003e 表示；\u0026ldquo;假\u0026quot;用 \u003ccode\u003efalse\u003c/code\u003e 表示，也可以用 \u003ccode\u003e0\u003c/code\u003e 表示\u003c/li\u003e\n\u003cli\u003e! 关系表达式的结果值是 \u003ccode\u003e0\u003c/code\u003e 或 \u003ccode\u003e1\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;5\u0026gt;3的值为%d\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 5\u0026gt;3的值为1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003etrue\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;true==1\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// 这行被执行\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;true!=1\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 如果 a=3, b=2, c=1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// (a\u0026gt;b) == c  为真，因为 a\u0026gt;b 的值为1，等于 c 的值\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// b+c \u0026lt; a     为假，因为 b+c 的值为3，并不小于 a\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003ch3 class=\"relative group\"\u003e4.1.2 逻辑运算符和逻辑表达式\n    \u003cdiv id=\"412-逻辑运算符和逻辑表达式\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#412-%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e7%ac%a6%e5%92%8c%e9%80%bb%e8%be%91%e8%a1%a8%e8%be%be%e5%bc%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\n\u003ch4 class=\"relative group\"\u003e逻辑运算符\n    \u003cdiv id=\"逻辑运算符\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e7%ac%a6\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eC 语言提供了三种逻辑运算符\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e编号\u003c/th\u003e\n          \u003cth\u003e逻辑运算符\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n          \u003cth\u003e运算分量\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e1\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e逻辑与（\u003ccode\u003eand\u003c/code\u003e）\u003c/td\u003e\n          \u003ctd\u003e双目\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e||\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e逻辑或（\u003ccode\u003eor\u003c/code\u003e）\u003c/td\u003e\n          \u003ctd\u003e双目\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e3\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e!\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e逻辑非（\u003ccode\u003enot\u003c/code\u003e）\u003c/td\u003e\n          \u003ctd\u003e单目\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch4 class=\"relative group\"\u003e逻辑运算规则\n    \u003cdiv id=\"逻辑运算规则\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e8%a7%84%e5%88%99\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e! \u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e \u003cstrong\u003e全真出真，有假出假\u003c/strong\u003e：两个运算分量必须全为真，结果才为真\u003c/li\u003e\n\u003cli\u003e! \u003ccode\u003e\\|\\|\u003c/code\u003e \u003cstrong\u003e有真出真，全假出假\u003c/strong\u003e：只要有一个为真，结果就为真\u003c/li\u003e\n\u003cli\u003e! \u003ccode\u003e!\u003c/code\u003e \u003cstrong\u003e取反\u003c/strong\u003e：原来为真变假，原来为假变真\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch4 class=\"relative group\"\u003e逻辑运算真值表\n    \u003cdiv id=\"逻辑运算真值表\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e7%9c%9f%e5%80%bc%e8%a1%a8\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003ccode\u003ea\u003c/code\u003e\u003c/th\u003e\n          \u003cth\u003e\u003ccode\u003eb\u003c/code\u003e\u003c/th\u003e\n          \u003cth\u003e\u003ccode\u003e!a\u003c/code\u003e\u003c/th\u003e\n          \u003cth\u003e\u003ccode\u003e!b\u003c/code\u003e\u003c/th\u003e\n          \u003cth\u003e\u003ccode\u003ea\u0026amp;\u0026amp;b\u003c/code\u003e\u003c/th\u003e\n          \u003cth\u003e\u003ccode\u003ea||b\u003c/code\u003e\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e真\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n          \u003ctd\u003e假\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch4 class=\"relative group\"\u003e逻辑运算符易错点\n    \u003cdiv id=\"逻辑运算符易错点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e7%ac%a6%e6%98%93%e9%94%99%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e! 千万不要把 \u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e 写成 \u003ccode\u003e\u0026amp;\u003c/code\u003e（位运算符），程序可能不报错但结果不对\u003c/li\u003e\n\u003cli\u003e! 千万不要把 \u003ccode\u003e\\|\\|\u003c/code\u003e 写成 \u003ccode\u003e\\|\u003c/code\u003e（位运算符），同样程序可能不报错但结果不对\u003c/li\u003e\n\u003cli\u003e! 进行逻辑判断时，\u003cstrong\u003e不等于 0 的数值全部被认为是 \u003ccode\u003etrue\u003c/code\u003e\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e! 系统给出的逻辑运算结果不是 \u003ccode\u003e0\u003c/code\u003e（假）就是 \u003ccode\u003e1\u003c/code\u003e（真），不可能是其他数值\u003c/li\u003e\n\u003cli\u003e! 作为参加逻辑运算的对象，是 \u003ccode\u003e0\u003c/code\u003e 就表示假，\u003cstrong\u003e非 \u003ccode\u003e0\u003c/code\u003e 就表示真\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;4==true，结果为%d\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// 0（假），4不等于1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1==true，结果为%d\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// 1（真）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"nf\"\u003eprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;4\u0026amp;\u0026amp;5，结果为%d\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e\u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e         \u003cspan class=\"c1\"\u003e// 1（真），4和5都是非0，都为真\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003ch4 class=\"relative group\"\u003e逻辑运算符优先级\n    \u003cdiv id=\"逻辑运算符优先级\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%80%bb%e8%be%91%e8%bf%90%e7%ae%97%e7%ac%a6%e4%bc%98%e5%85%88%e7%ba%a7\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e优先级（数字越小越高）\u003c/th\u003e\n          \u003cth\u003e运算符\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e1\u003c/td\u003e\n          \u003ctd\u003e逻辑非 \u003ccode\u003e!\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e2\u003c/td\u003e\n          \u003ctd\u003e算术运算符\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e3\u003c/td\u003e\n          \u003ctd\u003e关系运算符\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e 和 \u003ccode\u003e||\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e5\u003c/td\u003e\n          \u003ctd\u003e赋值运算符\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cul\u003e\n\u003cli\u003e! 逻辑运算符中的 \u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e 和 \u003ccode\u003e\\|\\|\u003c/code\u003e 优先级\u003cstrong\u003e低于\u003c/strong\u003e关系运算符\u003c/li\u003e\n\u003cli\u003e! 逻辑运算符中的 \u003ccode\u003e!\u003c/code\u003e 优先级\u003cstrong\u003e高于\u003c/strong\u003e算术运算符\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// 等价于 (a\u0026gt;b) \u0026amp;\u0026amp; (x\u0026gt;y)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"err\"\u003e\\\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"err\"\u003e\\\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 等价于 (a==b) \\|\\| (x==y)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"err\"\u003e\\\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"err\"\u003e\\\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// 等价于 (!a) \\|\\| (a\u0026gt;b)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;c\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;d\u0026#39;\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// 比较的是字符的ASCII码，都不为0，结果为真\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003ch4 class=\"relative group\"\u003e短路求值\n    \u003cdiv id=\"短路求值\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%ad%e8%b7%af%e6%b1%82%e5%80%bc\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e@ \u003cstrong\u003e短路求值\u003c/strong\u003e: 只要最终的结果已经可以确定是真或假，求值过程便宣告终止\u003c/li\u003e\n\u003cli\u003e! 这是面试常考的陷阱\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e 短路求值\u003c/strong\u003e:\u003c/p\u003e","title":"P3 逻辑运算和判断选择","type":"notes"},{"content":" 知识点 # 5.1 循环控制语句简介与 goto, while, do...while 语句精解 # @ 循环控制图 ![[白板/P9 循环控制.canvas|P9 循环控制]] 5.1.1 循环控制语句简介 # @ 循环控制语句: 使程序代码被反复多次执行的语句 % C 语言循环语句分类: goto 语句 while 语句 do...while 语句 for 语句 ! 几乎所有有实用价值的应用程序都会包含循环结构 5.1.2 goto 语句 # [!tip] goto 语句\n@ goto 语句: 无条件转向语句，用来跳转到某个程序位置进行执行 一般形式: goto 语句标号; 语句标号是一个标识符，只能由字母、数字、下划线组成，第一个字符必须是字母或下划线 goto label1; // 合法 goto 123; // 不合法，第一个字符是数字 goto 语句的主要用途 # % 与 if 语句一起构成循环结构 % 从循环体内跳转到循环体外（不推荐，破坏结构化程序设计原则） // 用 goto 语句实现 1 到 100 的加法 int i = 1, sum = 0; loop: if(i \u0026lt;= 100) { sum = sum + i; i++; goto loop; } printf(\u0026#34;1+2+...+100的和值为%d\\n\u0026#34;, sum); // 5050 goto 语句注意事项 # ! goto 语句不能跨函数使用 ! 多数情况下可以用其他循环语句代替 goto，应用场合较少 void func1() { lbl1: int k = 1; goto lbl1; // 合法，同一个函数内 // goto lbl2; // 不合法！不能跨函数跳转 } void func2() { lbl2: int a = 1; } 5.1.3 while 语句 # [!tip] while 语句\n@ while 语句: 当型循环结构，先判断后执行 一般形式: while(表达式) 要执行的语句; ! 最大特点: 先判断表达式的值，为真才执行循环体，为假则一次都不执行 // 用 while 语句实现 1 到 100 的加法 int i = 1, sum = 0; while(i \u0026lt;= 100) { sum = sum + i; i++; } printf(\u0026#34;1+2+...+100的和值为%d\\n\u0026#34;, sum); // 5050 while 语句要点 # ! 多条语句必须用 {} 括起来，否则 while 只执行到第一个分号 ! 循环体中应该有使循环趋向于结束的语句（如 i++） ! 如果没有使循环结束的语句，会导致死循环（程序卡死） 5.1.4 do...while 语句 # [!tip] do\u0026hellip;while 语句\n@ do...while 语句: 直到型循环结构，先执行后判断 一般形式: do 要执行的语句 while(表达式); ! 最大特点: 至少执行一次循环体，然后再判断表达式 // 用 do...while 语句实现 1 到 100 的加法 int i = 1, sum = 0; do { sum = sum + i; i++; } while(i \u0026lt;= 100); printf(\u0026#34;1+2+...+100的和值为%d\\n\u0026#34;, sum); // 5050 while 与 do...while 的区别 # ! 当表达式第一次判断为真时，两种循环结果相同 ! 当表达式第一次判断为假时，while 循环体一次不执行，do...while 至少执行一次 // while: i=20, 条件不满足，循环体一次不执行 int sum = 0, i = 20; while(i \u0026lt;= 10) { sum = sum + i; i++; } printf(\u0026#34;sum=%d\\n\u0026#34;, sum); // sum=0 // do...while: i=20, 条件不满足，但循环体执行了一次 int sum = 0, i = 20; do { sum = sum + i; i++; } while(i \u0026lt;= 10); printf(\u0026#34;sum=%d\\n\u0026#34;, sum); // sum=20 ! 实际应用中 while 使用更多，do...while 使用较少 5.2 for 语句精解 # @ for循环图 ![[白板/P10 for循环.canvas|P10 for循环]] 5.2.1 for 语句的一般形式 # [!tip] for 语句\n@ for 语句: 非常灵活的循环语句，可处理确定和不确定循环次数的情况 一般形式: for(表达式1; 表达式2; 表达式3) 内嵌的语句; 最常用形式: for(循环变量赋初值; 循环变量结束条件; 循环变量增加值) 内嵌的语句; for 语句执行步骤 # 1 求解表达式1的值（只执行一次） 2 求解表达式2的值 3 若表达式2为真（非0），执行内嵌语句，同时求解表达式3，反复循环步骤2 4 若表达式2为假（0），循环结束 // 用 for 语句实现 1 到 100 的加法 int i, sum = 0; for(i = 1; i \u0026lt;= 100; i++) { sum = sum + i; } printf(\u0026#34;sum=%d\\n\u0026#34;, sum); // sum=5050 5.2.2 for 语句的主要说明 # 省略表达式1 # ! 表达式1可以省略，但分号不能省略 ! 应在 for 语句之前给循环变量赋初值 int i, sum = 0; i = 1; // 给循环变量赋初值 for(; i \u0026lt;= 100; i++) { sum = sum + i; } 省略表达式2 # ! 省略表达式2则不判断循环结束条件，循环无终止进行 ! 必须用 break 语句终止循环 int i, sum = 0; for(i = 1; ; i++) { sum = sum + i; if(i \u0026gt;= 100) break; // 退出 for 循环 } 省略表达式3 # ! 省略表达式3则必须保证循环能正常结束 int i, sum; for(sum = 0, i = 1; i \u0026lt;= 100;) { // 逗号表达式作为表达式1 sum = sum + i; i++; // 放在循环体内保证循环结束 } 省略表达式1和表达式3 # int i, sum; sum = 0; i = 1; for(; i \u0026lt;= 100;) { sum = sum + i; i++; } 三个表达式都省略 # ! for(;;) 等价于 while(1)，导致无终止循环 ! 必须在内嵌语句中用 break 跳出 int i, sum; sum = 0; i = 1; for(;;) { sum = sum + i; if(i \u0026gt;= 100) break; i++; } 表达式1可以与循环变量无关 # int i, sum; i = 1; for(sum = 0; i \u0026lt;= 100; i++) { // 表达式1设置sum初值 sum = sum + i; } 表达式1和表达式3可以是逗号表达式 # int i, j = 10000, sum; for(sum = 0, i = 1; i \u0026lt;= 100; i++, j--) { // \u0026#34;sum=0,i=1\u0026#34;作为表达式1 sum = sum + i; } printf(\u0026#34;sum=%d\\n\u0026#34;, sum); // sum=5050 printf(\u0026#34;j=%d\\n\u0026#34;, j); // j=9900 表达式2的值只要非0就执行循环体 # int i, sum; sum = 0; for(i = 0; 8888; i++) { // 8888非0，永远为真 sum = sum + i; if(i \u0026gt;= 100) break; } ! 建议中规中矩使用 for 语句，最传统的写法： int i; for(i = 0; i \u0026lt;= 100; i++) { // 一系列要执行的语句 } 5.3 循环的嵌套, 比较与 break 语句, continue 语句 # @ 循环嵌套与控制图 ![[白板/P11 循环嵌套与控制.canvas|P11 循环嵌套与控制]] 5.3.1 循环的嵌套 # @ 循环嵌套: 一个循环体内又包含另外一个完整的循环结构 ! 最常用的是 for 语句内部嵌套 for 语句 // 九九乘法表 int i, j, k; int icount = 0; for(i = 1; i \u0026lt;= 9; i++) { for(j = 1; j \u0026lt;= i; j++) { k = i * j; printf(\u0026#34;%d*%d=%d \u0026#34;, i, j, k); icount++; } printf(\u0026#34;\\n\u0026#34;); } printf(\u0026#34;内循环的循环次数为%d次\\n\u0026#34;, icount); // 45次 // 内循环次数: 1+2+3+4+5+6+7+8+9 = 45 5.3.2 几种循环语句的比较 # 比较项 while do...while for 判断时机 先判断后执行 先执行后判断 先判断后执行 循环体最少执行次数 0次 1次 0次 可否相互替代 可以 可以 可以 适用场景 不确定循环次数 至少执行一次 确定循环次数 ! 不提倡使用 goto 循环，它破坏结构化程序设计 ! 对 while、do...while、for 可以用 break 跳出循环，用 continue 结束本次循环 ! 对 goto 和 if 构成的循环，不能用 break 和 continue 5.3.3 break 语句和 continue 语句 # break 语句 # [!tip] break 语句\n@ break 语句: 跳出循环体或 switch 语句，提前结束循环 ! break 只能用于循环语句和 switch 语句中，不能用于其他语句 ! break 只能跳出它所在的那层循环 // 从1加到100，和值达到4000时退出循环 int i, sum = 0; for(i = 1; i \u0026lt;= 100; i++) { sum = sum + i; if(sum \u0026gt;= 4000) break; // 跳出整个 for 循环 } printf(\u0026#34;sum=%d\\n\u0026#34;, sum); // sum=4005 printf(\u0026#34;i=%d\\n\u0026#34;, i); // i=89 break 在嵌套循环中的行为 # ! break 只跳出它所在的那一层循环，不会跳出外层循环 int i, j, k; for(i = 1; i \u0026lt;= 9; i++) { for(j = 1; j \u0026lt;= i; j++) { k = i * j; printf(\u0026#34;%d*%d=%d \u0026#34;, i, j, k); break; // 只跳出内部 for(j) 循环 } printf(\u0026#34;\\n\u0026#34;); break; // 跳出外部 for(i) 循环 } printf(\u0026#34;流程走出来了!\\n\u0026#34;); continue 语句 # [!tip] continue 语句\n@ continue 语句: 结束本次循环，跳过循环体中余下未执行的语句，继续下一次循环判断 ! continue 只能用于三种循环语句（while、do...while、for），不能用于 switch break 与 continue 的区别 # 语句 作用 范围 break 结束整个循环，跳到循环后面的语句 循环语句 + switch continue 只结束本次循环，继续下一次循环判断 仅循环语句 // 输出1到100之间不能被3整除的数 int i; for(i = 1; i \u0026lt;= 100; i++) { if(i % 3 == 0) continue; // 跳过本次循环，不执行下面的 printf printf(\u0026#34;%d \u0026#34;, i); } ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p4-%E5%BE%AA%E7%8E%AF%E6%8E%A7%E5%88%B6/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e5.1 循环控制语句简介与 \u003ccode\u003egoto\u003c/code\u003e, \u003ccode\u003ewhile\u003c/code\u003e, \u003ccode\u003edo...while\u003c/code\u003e 语句精解\n    \u003cdiv id=\"51-循环控制语句简介与-goto-while-dowhile-语句精解\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#51-%e5%be%aa%e7%8e%af%e6%8e%a7%e5%88%b6%e8%af%ad%e5%8f%a5%e7%ae%80%e4%bb%8b%e4%b8%8e-goto-while-dowhile-%e8%af%ad%e5%8f%a5%e7%b2%be%e8%a7%a3\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 循环控制图\n\u003cul\u003e\n\u003cli\u003e![[白板/P9 循环控制.canvas|P9 循环控制]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e5.1.1 循环控制语句简介\n    \u003cdiv id=\"511-循环控制语句简介\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#511-%e5%be%aa%e7%8e%af%e6%8e%a7%e5%88%b6%e8%af%ad%e5%8f%a5%e7%ae%80%e4%bb%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e@ \u003cstrong\u003e循环控制语句\u003c/strong\u003e: 使程序代码被反复多次执行的语句\u003c/li\u003e\n\u003cli\u003e% C 语言循环语句分类:\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003egoto\u003c/code\u003e 语句\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ewhile\u003c/code\u003e 语句\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003edo...while\u003c/code\u003e 语句\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efor\u003c/code\u003e 语句\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e! 几乎所有有实用价值的应用程序都会包含循环结构\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e5.1.2 \u003ccode\u003egoto\u003c/code\u003e 语句\n    \u003cdiv id=\"512-goto-语句\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#512-goto-%e8%af%ad%e5%8f%a5\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] goto 语句\u003c/p\u003e","title":"P4 循环控制","type":"notes"},{"content":" 知识点 # 6.1 一维数组 # @ 一维数组图 ![[白板/P12 一维数组.canvas|P12 一维数组]] 6.1.1 一维数组的一般形式 # [!tip] 一维数组定义\n@ 一维数组: 有序数据的集合，包含若干个同类型元素，通过数组名加下标引用 一般形式: 类型说明 数组名[常量表达式]; int a[10]; // 定义数组a，有10个元素 定义要点 # ! 数组名后面是中括号 []，不能写成圆括号 () ! [10] 中的10表示有10个元素，下标从 0 开始，即 a[0]~a[9]，不包括 a[10] ! 非法引用 a[10] 时系统不提示错误，但会产生巨大程序隐患（覆盖其他变量内存） int a[10]; a[10] = 8; // 非法！超出下标范围，系统不报错但会留下巨大隐患 ! 常量表达式不能包含变量，C 语言不允许对数组大小做动态定义 int i = 8; int a[i]; // 非法！变量不能用于定义数组大小 6.1.2 一维数组元素的引用 # [!tip] 数组元素引用\n只能引用数组中的元素，不能引用整个数组 引用形式: 数组名[下标] int i, a[10]; for(i = 0; i \u0026lt; 10; i++) // 给数组赋值 a[i] = i; for(i = 9; i \u0026gt;= 0; i--) // 逆序输出 printf(\u0026#34;a[%d]=%d\\n\u0026#34;, i, a[i]); 6.1.3 一维数组的初始化 # 初始化方式 示例 说明 不给初值 int a[10]; 元素值为随机值 全部赋初值 int a[10] = {9,8,10,20,9,8,7,6,5,4}; 每个元素都有值 部分赋初值 int a[10] = {9,8,10,20}; 未赋值的元素自动为 0 省略数组长度 int a[] = {1,2,3,4,5}; 等价于 int a[5] ! 如果只给部分元素赋初值，其余元素自动赋为 0 ! 只有在全部赋初值时才能省略数组长度 ! 若数组长度与初值个数不同，长度数字不能省略 int a[10] = {1,2,3,4,5}; // 只初始化了5个元素，后5个为0 6.2 二维数组 # @ 二维数组图 ![[白板/P13 二维数组.canvas|P13 二维数组]] 6.2.1 二维数组的一般形式 # [!tip] 二维数组定义\n一般形式: 类型说明 数组名[常量表达式][常量表达式]; 理解方式: 按行和列理解，也可以看成\u0026quot;元素为一维数组\u0026quot;的一维数组 float a[3][4]; // 3行4列的数组，共12个元素 // 不能写成 a[3,4]，这是错误的写法 二维数组的理解 # a[3][4] 可以把 a 看成一维数组，有3个元素 a[0]、a[1]、a[2] 每个元素又是一个包含4个元素的一维数组 元素引用范围 # ! 第一维下标范围: 02，第二维下标范围: 03 ! a[3][4] 在定义时表示维数，在引用时表示下标值，完全是两个意思 内存存放顺序 # ! 二维数组在内存中按行存放：先存第一行，再存第二行 ! 多维数组：第一维下标变化最慢，最右边维度的下标变化最快 6.2.2 二维数组元素的引用 # int a[3][4]; int i, j; // 用循环嵌套给二维数组赋值 for(i = 0; i \u0026lt; 3; i++) for(j = 0; j \u0026lt; 4; j++) a[i][j] = i * j; // 用循环嵌套输出 for(i = 0; i \u0026lt; 3; i++) for(j = 0; j \u0026lt; 4; j++) printf(\u0026#34;a[%d][%d]=%d\\n\u0026#34;, i, j, a[i][j]); 6.2.3 二维数组的初始化 # 初始化方式 示例 说明 按行赋值 int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; 清晰明了 所有数据在一个大括号 int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 不够清晰 部分元素赋值 int a[3][4] = {{1},{3,4}}; 未赋值的为 0 省略第一维长度 int a[][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 第二维不能省 ! 全部赋初值时可省略第一维长度，但第二维长度不能省略 ! 部分赋初值时也可省略第一维长度，但应分行赋初值 int a[][4] = {{0,0,3},{0},{0,10}}; 6.3 字符数组 # @ 字符数组图 ![[白板/P14 字符数组.canvas|P14 字符数组]] 6.3.1 字符数组的定义 # [!tip] 字符数组\n@ 字符数组: 用来存放字符数据的数组，一个元素存放一个字符 定义方式与整型数组类似，类型为 char char c[10]; c[0] = \u0026#39;I\u0026#39;; c[1] = \u0026#39; \u0026#39;; c[2] = \u0026#39;a\u0026#39;; c[3] = \u0026#39;m\u0026#39;; c[4] = \u0026#39; \u0026#39;; c[5] = \u0026#39;h\u0026#39;; c[6] = \u0026#39;a\u0026#39;; c[7] = \u0026#39;p\u0026#39;; c[8] = \u0026#39;p\u0026#39;; c[9] = \u0026#39;y\u0026#39;; 6.3.2 字符数组的初始化 # 初始化方式 示例 说明 逐个字符赋值 char c[10] = {'I',' ','a','m',' ','h','a','p','p','y'}; 好理解 省略数组长度 char c[] = {'I',' ','a','m',' ','h','a','p','p','y'}; 等价于 char c[10] 初值个数大于数组长度 char c[8] = {'I',...共10个...}; 编译报错 初值个数小于数组长度 char c[12] = {'I',' ','a','m',' ','h','a','p','p','y'}; 多余元素可能为 '\\0' 6.3.3 字符串和字符串结束标记 # [!tip] 字符串结束标记\n@ '\\0': 字符串结束标志，ASCII 码为 0，用来标记字符串的结束 ! 重点中的重点，面试常考 用字符串常量初始化字符数组 # // 逐个字符初始化：数组长度为10 char c[] = {\u0026#39;I\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;m\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;h\u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;y\u0026#39;}; // 用字符串常量初始化：数组长度为11！系统自动在末尾加 \u0026#39;\\0\u0026#39; char c[] = \u0026#34;I am happy\u0026#34;; // 等价于 char c[11] ! 用字符串常量初始化时，系统自动在末尾增加 '\\0'，所以数组长度比可见字符数多1 ! 用逐个字符初始化时，系统不会自动加 '\\0' 两种初始化的区别 # char c[] = {\u0026#39;I\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;m\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;h\u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;y\u0026#39;}; // 长度10，没有\u0026#39;\\0\u0026#39; char c[] = \u0026#34;I am happy\u0026#34;; // 长度11，有\u0026#39;\\0\u0026#39; // 这两种写法不等价！ // 但下面这种写法等价： char c[] = {\u0026#39;I\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;m\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;h\u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;y\u0026#39;,\u0026#39;\\0\u0026#39;}; // 长度11 char c[] = \u0026#34;I am happy\u0026#34;; // 长度11 字符数组大小必须够放 '\\0' # char c[10] = \u0026#34;I am happy\u0026#34;; // 错误！需要11个位置（10个字符+\u0026#39;\\0\u0026#39;） char c[11] = \u0026#34;I am happy\u0026#34;; // 正确，刚好能放下 char c[100] = \u0026#34;I am happy\u0026#34;; // 正确，更可以 ! 没有 '\\0' 会导致 printf 输出时出现垃圾信息（一直输出直到偶然遇到 '\\0'） ! 建议使用字符串常量方式初始化：char c[100] = \u0026quot;I am happy\u0026quot;; 6.3.4 字符数组的输入/输出 # 输出字符串 # 用 %s 格式符输出整个字符串 printf 遇到 '\\0' 就停止输出 ! 输出的字符串中不包含 '\\0' ! 数组长度大于字符串实际长度时，也只输出到 '\\0' 结束 ! 包含多个 '\\0' 时，遇到第一个 '\\0' 就停止 char c[] = \u0026#34;China\u0026#34;; printf(\u0026#34;%s\\n\u0026#34;, c); // China 输入字符串 # 用 scanf 和 %s 格式符输入 ! 输入的字符串长度必须短于数组长度，要给 '\\0' 留位置 ! scanf 遇到空格就认为字符串结束 char c[6]; scanf(\u0026#34;%s\u0026#34;, c); // 最多输入5个字符，给\u0026#39;\\0\u0026#39;留位置 printf(\u0026#34;%s\\n\u0026#34;, c); ! scanf 中字符数组名前不需要加 \u0026amp;，因为数组名代表首地址 // 输入多个字符串，以空格分隔 char str1[10], str2[10], str3[10]; scanf(\u0026#34;%s %s %s\u0026#34;, str1, str2, str3); // 输入: how are you // str1=\u0026#34;how\u0026#34;, str2=\u0026#34;are\u0026#34;, str3=\u0026#34;you\u0026#34; // 但如果只用一个变量接收 char str[100]; scanf(\u0026#34;%s\u0026#34;, str); // 输入: how are you // str只得到\u0026#34;how\u0026#34;，后面的被舍弃 6.3.5 字符串处理函数 # [!tip] 字符串处理函数\n使用这些函数需要包含头文件 #include \u0026lt;string.h\u0026gt; puts(字符数组) — 输出字符串 # char str[100] = \u0026#34;Are you ok?\u0026#34;; puts(str); // Are you ok? // 等价于 printf(\u0026#34;%s\\n\u0026#34;, str); strcat(字符数组1, 字符数组2) — 连接字符串 # char str1[10] = \u0026#34;one\u0026#34;; char str2[15] = \u0026#34;two\u0026#34;; strcat(str1, str2); printf(\u0026#34;%s\\n\u0026#34;, str1); // onetwo ! 字符数组1必须足够大，能容纳连接后的新字符串 ! 连接时将字符串1末尾的 '\\0' 删除，连接字符串2的内容 ! 连接后只在新字符串末尾保留一个 '\\0' ! 连接后 str2 内容不发生变化 strcpy(字符数组1, 字符串2) — 复制字符串 # char str1[10] = \u0026#34;one1234\u0026#34;; char str2[15] = \u0026#34;two\u0026#34;; strcpy(str1, str2); // str1 变为 \u0026#34;two\u0026#34; ! 字符数组1必须足够大，能容纳被复制的字符串（包括 '\\0'） ! 字符数组1必须是数组名，字符串2可以是数组名或字符串常量 ! 复制时连同 '\\0' 一起复制 ! 不能用赋值语句将字符串直接赋给字符数组 str1 = \u0026#34;China\u0026#34;; // 非法！ str1 = str2; // 非法！ strcpy(str1, \u0026#34;China\u0026#34;); // 正确 ! 区分\u0026quot;定义时初始化\u0026quot;和\u0026quot;赋值\u0026quot; char str1[10] = \u0026#34;one1234\u0026#34;; // 定义时初始化，合法 str1 = \u0026#34;one1234\u0026#34;; // 赋值，非法！ strcmp(字符串1, 字符串2) — 比较字符串 # 比较结果 返回值 字符串1 == 字符串2 0 字符串1 \u0026gt; 字符串2 正整数 字符串1 \u0026lt; 字符串2 负整数 ! 比较规则：自左至右逐个字符按 ASCII 码值比较，直到出现不同字符或遇到 '\\0' ! 不能用 == 比较两个字符串，那比较的是地址 // 错误写法！比较的是地址 if(str1 == str2) // 正确写法：比较字符串内容 if(strcmp(str1, str2) == 0) strlen(字符数组) — 获取字符串长度 # char str1[120] = \u0026#34;opel\u0026#34;; int len = strlen(str1); // 4，不包括末尾的\u0026#39;\\0\u0026#39; ! 返回字符串的实际长度，不包括 '\\0' ! 面试常考：strlen 与 sizeof 的区别 函数/运算符 strlen(str) sizeof(str) 计算内容 字符串实际长度 变量所占内存大小 是否包含 '\\0' 不包含 包含 与内容关系 有关 无关 与定义大小关系 无关 有关 char str1[120] = \u0026#34;opel\u0026#34;; int stlen = strlen(str1); // 4，与str1中内容有关 int sostr = sizeof(str1); // 120，与str1定义时的大小有关 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p5-%E6%95%B0%E7%BB%84/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e6.1 一维数组\n    \u003cdiv id=\"61-一维数组\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#61-%e4%b8%80%e7%bb%b4%e6%95%b0%e7%bb%84\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 一维数组图\n\u003cul\u003e\n\u003cli\u003e![[白板/P12 一维数组.canvas|P12 一维数组]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e6.1.1 一维数组的一般形式\n    \u003cdiv id=\"611-一维数组的一般形式\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#611-%e4%b8%80%e7%bb%b4%e6%95%b0%e7%bb%84%e7%9a%84%e4%b8%80%e8%88%ac%e5%bd%a2%e5%bc%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 一维数组定义\u003c/p\u003e","title":"P5 数组","type":"notes"},{"content":" 知识点 # 7.1 函数的基本概念和定义 # @ 函数基本概念图 ![[白板/P15 函数基本概念.canvas|P15 函数基本概念]] 7.1.1 函数的基本概念 # @ 函数: 代表一段可以复用的代码，减少重复代码，实现相对独立的功能 % C 程序结构: 一个主函数（main）+ 若干个其他函数 ! main 函数是程序执行的入口，由系统调用，名字固定 ! 函数之间可以互相调用，但不要调用 main 函数 ! 函数不能嵌套定义（不能在函数内部定义另一个函数） 函数分类 # % 库函数: 如 printf，直接使用，不需要自己定义 % 自定义函数: 开发者自己写的函数 7.1.2 函数的定义和返回值 # [!tip] 函数定义\n一般形式: 返回类型 函数名(形式参数列表) { 一条或多条语句 return 返回值; } 圆括号内的参数叫形式参数（形参） 函数定义范例 # 无返回类型无形参:\nvoid printhello() { printf(\u0026#34;hello world\u0026#34;); return; // 可以没有，因为无返回类型 } 有返回值有形参:\nint addtwoshu(int a, int b) { int sum = a + b; return sum; // 返回变量 sum 的值 } 函数定义要点 # ! 函数定义的第一行末尾没有分号 ! 调用函数时为形参分配内存，调用结束后释放，形参只能在函数内部使用 ! 实参可以是常量、变量、表达式 ! 实参和形参的数量、类型要一致 ! 参数传递是单向值传递：只由实参传给形参，不能由形参传给实参 int result = addtwoshu(3, 4); // 常量作实参 int result = addtwoshu(1+2, 2+2); // 表达式作实参 int result = addtwoshu(i, j); // 变量作实参 return 语句的多种形式 # // 范例1: 返回表达式的值 int addtwoshu(int a, int b) { return a + b; } // 范例2: 根据条件有多个 return int whichmax(int a, int b) { if(a \u0026gt; b) return a; return b; } // 范例3: 返回类型不一致时系统自动转换（不建议） int testf() { return 3.45F; // 实际返回3，不建议 } 7.2 函数调用方式和嵌套调用 # @ 函数调用图 ![[白板/P16 函数调用.canvas|P16 函数调用]] 7.2.1 函数调用的一般形式 # 函数调用一般形式: 函数名(实参列表); ! 无形参时圆括号不能省略 ! 实参和形参个数相等、类型一致、按顺序对应 7.2.2 函数调用的方式 # 调用方式 示例 说明 作为语句 printhello(); 末尾加分号 在表达式中 int result = addtwoshu(3,4) * 100; 需要带回确定值 作为另一个函数的参数 whichmax(44, whichmax(12,19)) 先计算内层参数 函数声明 # [!tip] 函数声明\n@ 函数声明: 在调用函数之前声明该函数，使编译器知道函数的参数类型和返回值类型 一般形式: 返回类型 函数名(形式参数列表); ! 函数声明末尾有分号，函数定义末尾没有分号 // 函数定义（无分号，有函数体） void printhello() { printf(\u0026#34;hello, how are you!\\n\u0026#34;); } // 函数声明（有分号，无函数体） void printhello(); // 函数调用 printhello(); ! 可以把函数声明写在 .h 头文件中，用 #include 包含 ! 严格区分函数定义和函数声明：定义包含函数体，声明不包含 7.2.3 函数的嵌套调用 # ! C 语言不允许嵌套定义函数（不能在函数内定义另一个函数） ! C 语言允许嵌套调用函数（在函数内调用另一个函数） void qtfunc1() { printf(\u0026#34;qtfunc1开始执行\\n\u0026#34;); qtfunc2(); // 嵌套调用 qtfunc2 printf(\u0026#34;qtfunc1结束执行\\n\u0026#34;); } void qtfunc2() { printf(\u0026#34;qtfunc2开始执行\\n\u0026#34;); qtfunc3(); // 嵌套调用 qtfunc3 printf(\u0026#34;qtfunc2结束执行\\n\u0026#34;); } void qtfunc3() { printf(\u0026#34;qtfunc3开始执行\\n\u0026#34;); printf(\u0026#34;qtfunc3结束执行\\n\u0026#34;); } 7.3 函数递归调用精彩演绎 # @ 递归调用图 ![[白板/P17 递归调用.canvas|P17 递归调用]] 7.3.1 函数递归调用的定义 # [!tip] 递归调用\n@ 递归调用: 函数自己调用自己，属于特殊的嵌套调用 ! 递归必须有出口条件，否则会无限调用导致内存耗尽、程序崩溃 // 错误示范：无出口条件的递归，会导致程序崩溃 void diguifunc() { printf(\u0026#34;diguifunc()函数执行\\n\u0026#34;); diguifunc(); // 自己调用自己，无出口！ } ! 每次调用函数都会占用内存（局部变量、函数参数、调用关系等），嵌套层次越深内存越大 ! 系统分配的内存是有限的，超过限制程序就会崩溃 7.3.2 递归的两种形式 # % 直接递归: 函数直接调用自己 % 间接递归: 函数 A 调用函数 B，函数 B 又调用函数 A 7.3.3 递归的实际运用 # ! 递归的两个出口条件：一是判断条件是否满足，二是是否有剩余资源 // 伪代码：战棋游戏寻路（递归实现） void pf_zhanqiFindPath(int posx, int posy, int shengyuyidongli) { if(上方图块存在) { 判断是否能走; 如果能走 { 保存位置，扣除移动力; pf_zhanqiFindPath(); // 递归调用 } } // 下方、左边、右边同理... } 7.4 数组作为函数参数 # @ 数组参数图 ![[白板/P18 数组参数.canvas|P18 数组参数]] 7.4.1 数组元素作为函数实参 # 数组元素可以当作普通变量使用，作为实参调用函数 ! 依旧是值传递 int whichmax(int x, int y) { if(x \u0026gt; y) return x; return y; } int main() { int a[10]; a[1] = 5; a[4] = 7; int b = whichmax(a[1], a[4]); // 数组元素作实参，值传递 return 0; } 7.4.2 数组名作为函数实参 # [!tip] 数组名作参数\n数组名代表数组的首地址 ! 数组名作参数时是地址传递，不是值传递 ! 形参数组元素改变，实参数组元素也跟着改变 void changevalue(int ba[]) { // 形参为数组名 ba[3] = 27; // 修改形参数组，实参数组也改变！ ba[4] = 36; return; } int main() { int a[5] = {85, 70, 68, 92, 78}; changevalue(a); // 数组名作实参，传递首地址 // a[3] 变为 27，a[4] 变为 36 return 0; } 数组名作参数要点 # ! 实参为数组名，形参也应为数组名 ! 实参数组与形参数组类型必须一致 ! 形参数组大小可以不指定（C 编译器不做检查） ! 超过实参数组大小的部分不要引用，否则程序崩溃 7.4.3 用多维数组作为函数实参 # 形参数组可以省略第一维大小，但不能省略第二维大小 ! 实参是多少行多少列，形参尽量跟实参一样 void changevalue2(int ba[5][8]) { ba[0][2] = 15; return; } int main() { int a[5][8]; a[0][2] = 12; changevalue2(a); return 0; } 7.5 局部变量和全局变量 # @ 变量作用域图 ![[白板/P19 变量作用域.canvas|P19 变量作用域]] 7.5.1 局部变量 # [!tip] 局部变量\n@ 局部变量: 在函数内部定义的变量，只在本函数范围内有效 ! 不同函数中可以使用相同变量名，互不干扰 ! 形参也是局部变量 ! 复合语句 {} 中定义的变量只在该复合语句内有效 void funcl(int tmpvalue) { // 无法使用 main 中的 m, n, k int x, y; // 只在 funcl 内有效 } int main() { int m, n; int k = 4; { int c; // 只在本复合语句内有效 c = a + b; } // c 在这里无效，内存已释放 return 0; } 7.5.2 全局变量 # [!tip] 全局变量\n@ 全局变量（外部变量）: 在函数外定义的变量，可为本文件中其他函数共用 有效范围: 从定义位置开始到本源程序文件结束 int p = 1, q = 5; // 全局变量，从这行开始有效 int f1(int a) { // 可以使用 p, q // 不能使用 c1, c2（定义在下面） } char c1, c2; // 全局变量，从这行开始有效 char f2(int x, int y) { // 可以使用 p, q, c1, c2 } int main() { // 可以使用 p, q, c1, c2 } 全局变量的优缺点 # 优点 缺点 增加函数间数据联系通道 程序执行期间一直占用内存 相当于能返回多个值 降低函数通用性 不需要通过形参传递 降低程序清晰性和可读性 ! 要限制使用全局变量 extern 关键字 # ! 如果函数想引用在它后面定义的全局变量，用 extern 做外部变量说明 ! 外部变量说明不能给初值，全局变量定义时可以给初值 extern int cl, c2; // 外部变量说明，不能写成 extern int cl=0, c2=1 void lookvalue() { cl = 5; // 因为前面用了 extern，所以可以使用 c2 = 8; } int cl, c2; // 这里才是全局变量定义的地方 ! 全局变量定义只能有一次，外部变量说明可以有多次 ! 同一文件中全局变量和局部变量同名时，在局部变量作用域内全局变量不起作用 int a = 4, b = 5; // 全局变量 void lookvalue(int a, int b) { a = 123; // 修改的是局部变量，不影响全局变量 b = 456; } int main() { lookvalue(i, j); printf(\u0026#34;a=%d\\n\u0026#34;, a); // a=4，全局变量不变 printf(\u0026#34;b=%d\\n\u0026#34;, b); // b=5，全局变量不变 } 跨文件引用全局变量 # ! 在其他源程序文件中用 extern 说明即可引用 // MyProject2.cpp 中定义 int g_a = 6; // MyProject.cpp 中引用 extern int g_a; // 外部变量说明 7.6 变量的存储和引用与内部和外部函数 # @ 变量存储与函数图 ![[白板/P20 变量存储与函数.canvas|P20 变量存储与函数]] 7.6.1 变量的存储类别 # 存储方式 说明 存储区 静态存储方式 程序运行期间分配固定存储空间 静态存储区 动态存储方式 程序运行期间动态分配和释放存储空间 动态存储区 % 静态存储区: 存放全局变量，程序开始执行时分配，执行完毕后释放 % 动态存储区: 存放函数形参、局部变量、函数调用现场数据等 7.6.2 局部变量的存储方式 # 传统情形 # 函数中的局部变量一般动态分配存储空间 函数调用时分配，执行完成后自动释放 特殊情形：static 局部变量 # ! 用 static 修饰的局部变量保存在静态存储区 ! 函数调用结束后不释放，下次调用时保持上次的值 ! 如果不给初值，自动初始化为 0 // 普通局部变量：每次调用都重新初始化 void funcTest() { int c = 4; // 每次调用 c 都是 4 printf(\u0026#34;c=%d\\n\u0026#34;, c); c++; } // 输出: c=4, c=4, c=4 // static 局部变量：保持上次离开时的值 void funcTest() { static int c = 4; // 只在第一次调用时初始化 printf(\u0026#34;c=%d\\n\u0026#34;, c); c++; } // 输出: c=4, c=5, c=6 7.6.3 全局变量的跨文件引用 # ! 用 extern 在其他源程序文件开头说明即可引用 ! 用 static 修饰全局变量，则该变量只能在本文件中使用 // MyProject2.cpp static int g_a = 6; // 只能在本文件中使用，其他文件无法 extern 引用 ! 不同文件中定义同名全局变量时，用 static 修饰可以避免冲突 7.6.4 函数的跨文件调用 # 函数类型 定义方式 说明 内部函数 static 返回类型 函数名(形参表){...} 只能被本文件调用 外部函数 返回类型 函数名(形参表){...} 可被其他文件调用 ! 外部函数在其他文件中调用时，只需增加函数声明即可 ! 建议将函数声明统一放在 .h 头文件中 // MyProject2.cpp 中定义外部函数 void g_otherfunc() { printf(\u0026#34;外部函数g_otherfunc()\\n\u0026#34;); } // MyProject.cpp 中声明并调用 void g_otherfunc(); // 函数声明 int main() { g_otherfunc(); // 调用 } 7.6.5 static 关键字用法总结 # 用法 示例 说明 局部变量前加 static static int tmpvalue = 1; 保存在静态存储区，保持上次值 全局变量前加 static static int g_a = 6; 只能在本文件中使用 函数定义前加 static static void g_func(){...} 只能在本文件中调用 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p6-%E5%87%BD%E6%95%B0/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e7.1 函数的基本概念和定义\n    \u003cdiv id=\"71-函数的基本概念和定义\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#71-%e5%87%bd%e6%95%b0%e7%9a%84%e5%9f%ba%e6%9c%ac%e6%a6%82%e5%bf%b5%e5%92%8c%e5%ae%9a%e4%b9%89\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 函数基本概念图\n\u003cul\u003e\n\u003cli\u003e![[白板/P15 函数基本概念.canvas|P15 函数基本概念]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e7.1.1 函数的基本概念\n    \u003cdiv id=\"711-函数的基本概念\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#711-%e5%87%bd%e6%95%b0%e7%9a%84%e5%9f%ba%e6%9c%ac%e6%a6%82%e5%bf%b5\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e@ \u003cstrong\u003e函数\u003c/strong\u003e: 代表一段可以复用的代码，减少重复代码，实现相对独立的功能\u003c/li\u003e\n\u003cli\u003e% C 程序结构: 一个主函数（\u003ccode\u003emain\u003c/code\u003e）+ 若干个其他函数\u003c/li\u003e\n\u003cli\u003e! \u003ccode\u003emain\u003c/code\u003e 函数是程序执行的入口，由系统调用，名字固定\u003c/li\u003e\n\u003cli\u003e! 函数之间可以互相调用，但\u003cstrong\u003e不要调用 \u003ccode\u003emain\u003c/code\u003e 函数\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e! 函数不能嵌套定义（不能在函数内部定义另一个函数）\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch4 class=\"relative group\"\u003e函数分类\n    \u003cdiv id=\"函数分类\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e5%87%bd%e6%95%b0%e5%88%86%e7%b1%bb\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e% \u003cstrong\u003e库函数\u003c/strong\u003e: 如 \u003ccode\u003eprintf\u003c/code\u003e，直接使用，不需要自己定义\u003c/li\u003e\n\u003cli\u003e% \u003cstrong\u003e自定义函数\u003c/strong\u003e: 开发者自己写的函数\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e7.1.2 函数的定义和返回值\n    \u003cdiv id=\"712-函数的定义和返回值\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#712-%e5%87%bd%e6%95%b0%e7%9a%84%e5%ae%9a%e4%b9%89%e5%92%8c%e8%bf%94%e5%9b%9e%e5%80%bc\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 函数定义\u003c/p\u003e","title":"P6 函数","type":"notes"},{"content":" 知识点 # 8.1 宏定义 # @ 宏定义图 ![[白板/P21 宏定义.canvas|P21 宏定义]] 8.1.1 不带参数的宏定义 # [!tip] 不带参数的宏定义\n@ 宏定义: 用一个指定的宏名来代表一串内容 一般形式: #define 宏名 被替换的内容 预处理时将宏名替换成指定内容的过程称为==宏展开==（宏替换） #define PI 3.1415926 // 末尾没有分号 int main() { float ftmp; ftmp = 2 * PI; // 宏展开后: ftmp = 2 * 3.1415926; printf(\u0026#34;ftmp = %f\\n\u0026#34;, ftmp); // ftmp: 6.283185 return 0; } 不带参数宏定义要点 # ! 宏名一般用大写字母表示（习惯约定） ! 宏定义行末不加 分号，加了会连分号一起替换 ! 宏定义不是 C 语言语句，不做语法检查 ! #define 出现在函数外面，有效范围从 #define 之后到本源程序文件结束 ! 不能跨文件使用，跨文件需在另一个文件中重新定义或放到公共 .h 头文件中 // 错误：加了分号 #define PI 3.1415926; ftmp = 2 * PI; // 替换为: ftmp = 2 * 3.1415926;; 还好 ftmp = PI * 2; // 替换为: ftmp = 3.1415926; * 2; 语法错误！ #undef 终止宏定义 # #define PI 3.1415926 int main() { float ftmp; ftmp = PI * 2; // 正常使用 return 0; } #undef PI // 终止 PI 的作用域 void func1() { float ftmp; ftmp = PI * 2; // 报错！PI 已被 #undef 终止 } 宏定义的嵌套引用 # #define PI 3.1415926 #define DPI 2 * PI // 引用已定义的宏 PI #define DPICPI PI * PI // 引用已定义的宏 PI // 宏展开后: // DPI → 2 * 3.1415926 // DPICPI → 3.1415926 * 3.1415926 ! 字符串内的字符即使与宏名相同，也不进行替换 char stmp[100] = \u0026#34;DPICPI\u0026#34;; // 这里的 DPICPI 不会被替换 printf(\u0026#34;stmp = %s\\n\u0026#34;, stmp); // stmp = DPICPI 8.1.2 带参数的宏定义 # [!tip] 带参数的宏定义\n一般形式: #define 宏名(参数表) 被替换的内容 不仅进行内容替换，还要进行参数替换 #define S(a, b) a * b int Area = S(3, 2); // 宏展开: int Area = 3 * 2; #define PI 3.1415926 #define S(r) PI * r * r float area; area = S(3.6); // 宏展开: area = 3.1415926 * 3.6 * 3.6; 带参数宏定义的陷阱 # [!warning] 形参必须加括号！\n! S(1+5) 如果定义为 #define S(r) PI * r * r，展开后变成 3.1415926 * 1+5 * 1+5，结果错误 ! 正确写法: #define S(r) PI * (r) * (r)，展开后变成 3.1415926 * (1+5) * (1+5) ! 宏名和带参数的括号之间不能加空格，否则空格后内容都作为替换内容 // 错误：宏名和括号之间有空格 #define S (r) PI * (r) * (r) // S 变成不带参数的宏，代表 \u0026#34;(r) PI * (r) * (r)\u0026#34; 带参数宏 vs 函数 # 对比项 带参数宏 函数 处理时机 编译阶段（预处理时展开） 程序运行阶段 参数处理 简单内容替换，不求值 先求实参值，再传给形参 内存分配 不分配内存 调用时分配临时内存 参数类型 无类型概念，只是符号 有类型检查 代码长度 每次展开代码增多 不增加源代码长度 执行速度 只占编译时间，不占运行时间 占运行时间 复杂宏定义范例 # // 求 x 和 y 的最大值 #define MAX(x, y) (x) \u0026gt; (y) ? (x) : (y) int result = MAX(3, 4); printf(\u0026#34;result = %d\\n\u0026#34;, result); // result = 4 // 多行宏定义（用 \\ 续行） #define PRINTF(str) do { \\ printf(str); \\ } while(0) 8.2 文件包含和条件编译 # @ 文件包含与条件编译图 ![[白板/P22 文件包含与条件编译.canvas|P22 文件包含与条件编译]] 8.2.1 文件包含 # [!tip] 文件包含\n@ 文件包含: 一个文件将另一个文件的全部内容包含进来 一般形式: #include \u0026quot;文件名\u0026quot; 文件包含要点 # ! 一条 #include 命令只能包含一个文件 ! #include 可以嵌套（.h 文件内可以再 #include 其他 .h 文件） ! 修改 .h 文件后，所有包含该 .h 的源文件都需要重新编译 \u0026quot;\u0026quot; vs \u0026lt;\u0026gt; 的区别 # 形式 查找顺序 用途 #include \u0026lt;文件名\u0026gt; 只在系统目录查找 系统提供的头文件，如 \u0026lt;stdio.h\u0026gt; #include \u0026quot;文件名\u0026quot; 先当前目录，再系统目录 自己写的头文件 // head.h 文件内容 #define PI 3.1415926 // MyProject.cpp 文件 #include \u0026#34;head.h\u0026#34; // 包含 head.h 的内容到当前文件 int main() { float ftmp; ftmp = PI * 2; printf(\u0026#34;ftmp = %f\\n\u0026#34;, ftmp); // ftmp = 6.283185 return 0; } 8.2.2 条件编译 # [!tip] 条件编译\n@ 条件编译: 对一部分内容指定编译条件，满足条件才编译 ! 条件编译可以减少目标程序长度 ! 条件编译常用于跨平台代码 形式 1: #ifdef # #ifdef 标识符 程序段1 // 标识符被定义过时编译 #else 程序段2 // 标识符未被定义时编译 #endif #define DEBUG 1 // 不想调试时注释掉这行 #ifdef DEBUG printf(\u0026#34;输出一些变量信息作为调试信息\\n\u0026#34;); #endif 形式 2: #ifndef # #ifndef 标识符 程序段1 // 标识符未被定义时编译 #else 程序段2 // 标识符被定义过时编译 #endif #define RELEASE 1 // 定义 RELEASE #ifndef RELEASE printf(\u0026#34;输出一些变量信息作为调试信息\u0026#34;); // 未定义 RELEASE 时才编译 #endif 形式 3: #if # #if 表达式 程序段1 // 表达式为真(非0)时编译 #else 程序段2 // 表达式为假时编译 #endif #elif 扩展形式 # #if 表达式1 程序段1 #elif 表达式2 程序段2 #else 程序段3 #endif 跨平台代码范例 # #if _WIN32 // Windows 平台（系统自动定义该宏） // Windows 专用函数 // WaitForSingleObject(...); printf(\u0026#34;当前是Windows平台\\n\u0026#34;); #elif __linux__ // Linux 平台（系统自动定义该宏） // Linux 专用函数 // epoll_create(...); printf(\u0026#34;当前是Linux平台\\n\u0026#34;); #else // 其他平台的处理代码 #endif 条件编译的好处 # ! 减少目标程序长度（不满足条件的代码不编译） ! 增加程序可移植性（跨平台代码无需修改即可在不同平台编译） ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p7-%E7%BC%96%E8%AF%91%E9%A2%84%E5%A4%84%E7%90%86/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e8.1 宏定义\n    \u003cdiv id=\"81-宏定义\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#81-%e5%ae%8f%e5%ae%9a%e4%b9%89\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 宏定义图\n\u003cul\u003e\n\u003cli\u003e![[白板/P21 宏定义.canvas|P21 宏定义]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e8.1.1 不带参数的宏定义\n    \u003cdiv id=\"811-不带参数的宏定义\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#811-%e4%b8%8d%e5%b8%a6%e5%8f%82%e6%95%b0%e7%9a%84%e5%ae%8f%e5%ae%9a%e4%b9%89\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 不带参数的宏定义\u003c/p\u003e","title":"P7 编译预处理","type":"notes"},{"content":" 知识点 # 9.1 指针的基本概念详解 # @ 指针基本概念图 ![[白板/P23 指针基本概念.canvas|P23 指针基本概念]] 9.1.1 前提知识 # 变量存储在内存中，不同类型的变量保存在不同存储区（静态存储区、动态存储区） 变量的内存在编译时或程序执行时分配 各类型占用字节数（x86 平台）: 类型 字节数 int 4 char 1 float 4 double 8 9.1.2 地址的概念 # [!tip] 地址\n@ 地址: 计算机中用一个数字来描述内存位置 内存地址从上到下从小到大排列 每个变量占若干字节，系统通过变量名与地址的对应表来访问变量 9.1.3 直接访问和间接访问 # [!tip] 访问方式\n@ 直接访问: 按变量地址存取变量值（如 printf(\u0026quot;%d\u0026quot;, i);） @ 间接访问: 通过存放地址的特殊变量来访问另一个变量的值 @ 指针变量: 专门用来存放另一个变量地址的变量 ! 指针变量本身也占内存（x86 下 4 字节，x64 下 8 字节） ! 区分\u0026quot;指针\u0026quot;和\u0026quot;指针变量\u0026quot;：==指针就是地址==，==指针变量是存放地址的变量== 9.2 变量的指针和指向变量的指针变量 # @ 指针变量图 ![[白板/P24 指针变量.canvas|P24 指针变量]] 9.2.1 指针变量的定义 # [!tip] 指针变量定义\n一般形式: 类型标识符 *标识符; 三种写法等价: int *p; / int* p; / int * p; int i = 7, j = 9; float k = 12.6f; int *mypoint1, *mypoint2; // 指向整型变量的指针 float *pm3; // 指向实型变量的指针 char *pm4; // 指向字符型变量的指针 mypoint1 = \u0026amp;i; // mypoint1 指向 i mypoint2 = \u0026amp;j; // mypoint2 指向 j 定义要点 # ! 定义时 * 表示正在定义指针变量，使用时变量名前没有 * ! 指针变量名是 mypoint1，不是 *mypoint1 ! 一个指针变量只能指向同一类型的变量 9.2.2 指针变量的引用 # [!tip] 两个关键运算符\n@ \u0026amp;: 取地址运算符 @ *: 指针运算符（间接访问运算符），访问指针所指向的变量 int a = 100, b = 200; int *p1, *p2; p1 = \u0026amp;a; // p1 指向 a p2 = \u0026amp;b; // p2 指向 b int *p3 = \u0026amp;a; // 定义时初始化 printf(\u0026#34;%d, %d\\n\u0026#34;, a, b); // 100, 200 printf(\u0026#34;%d, %d\\n\u0026#34;, *p1, *p2); // 100, 200，*p1 代表 a，*p2 代表 b 易混淆写法 # 写法 含义 \u0026amp;*p1 等价于 \u0026amp;a，也就是 p1 本身 *\u0026amp;a 等价于 *p1，也就是 a 本身 (*p1)++ 等价于 a++，值加 1 *p1++ 等价于 *(p1++)，先取值再移动指针 指针自增的含义 # ! p1++ 不是地址简单 +1，而是跳过所指向类型占用的字节数 int 指针 p1++ 地址 +4，char 指针 p1++ 地址 +1 // 交换两个指针指向的值 int *pmax, *pmin, *p, a = 5, b = 8; pmax = \u0026amp;a; pmin = \u0026amp;b; if(a \u0026lt; b) { p = pmax; pmax = pmin; // pmax 指向大的值 pmin = p; // pmin 指向小的值 } printf(\u0026#34;max = %d, min = %d\\n\u0026#34;, *pmax, *pmin); // max = 8, min = 5 9.2.3 指针变量作为函数参数 # [!tip] 指针作函数参数\n将变量的地址传送到函数中，可以在函数中改变调用者的变量值 ! 形参和实参依然是指针变量的值传递，但因为传递的是地址，所以可以间接修改原变量 void swap(int *pdest1, int *pdest2) { int temp; temp = *pdest1; // 取 pdest1 指向的值 *pdest1 = *pdest2; // 赋值给 pdest1 指向的变量 *pdest2 = temp; // 赋值给 pdest2 指向的变量 } int main() { int a = 5, b = 6; int *p1 = \u0026amp;a, *p2 = \u0026amp;b; if(a \u0026lt; b) swap(p1, p2); printf(\u0026#34;a=%d, b=%d\\n\u0026#34;, a, b); // a=6, b=5 return 0; } 9.3 数组的指针和指向数组的指针变量 # @ 数组指针图 ![[白板/P25 数组指针.canvas|P25 数组指针]] 9.3.1 指向数组元素的指针 # int a[5]; int *p; p = \u0026amp;a[0]; // 等价于 p = a; 数组名就是首地址 // 定义时初始化 int *p = \u0026amp;a[0]; // 等价于 int *p = a; 9.3.2 通过指针引用数组元素 # 表达式 含义 类型 p + i / a + i a[i] 的地址 地址 *(p + i) / *(a + i) a[i] 的值 值 p[i] 等价于 *(p + i)，即 a[i] 值 int a[5] = {12, 14, 20, 18, 50}; int *p; // 方法1: 下标法 for(int i = 0; i \u0026lt; 5; i++) printf(\u0026#34;%d\\n\u0026#34;, a[i]); // 方法2: 地址法 for(int i = 0; i \u0026lt; 5; i++) printf(\u0026#34;%d\\n\u0026#34;, *(a + i)); // 方法3: 指针法（最快） for(p = a; p \u0026lt; (a + 5); p++) printf(\u0026#34;%d\\n\u0026#34;, *p); 注意事项 # ! a++ 不合法，数组名是常量，不能自增 ! 指针法中循环结束后 p 已指向数组后面的内存，不能操作 ! a[5] 虽然有对应地址，但不能操作 指针自增/自减运算 # 写法 含义 *p++ 先取 *p 的值，再 p++ 指向下一元素 *++p 先 p++，再取新指向的值 (*p)++ p 指向的元素值 +1，指针不变 9.3.3 数组名作为函数参数 # [!tip] 实参与形参的 4 种对应关系\n实参 形参 数组名 数组名 数组名 指针变量 指针变量 指针变量 指针变量 数组名 // 实参用数组名，形参用指针变量 void changevalue(int *p) { *(p + 2) = 888; // 等价于给 a[2] 赋值 888 } int main() { int a[5] = {85, 70, 98, 92, 78}; changevalue(a); // 传递数组首地址 return 0; } 9.3.4 回顾二维数组和多维数组的概念 # 二维数组 int a[3][4] 可看作 3 个一维数组 a[0]、a[1]、a[2] 每个一维数组包含 4 个元素 二维数组在内存中按行连续存放 多维数组：最右边维度下标变化最快 9.3.5 指向多维数组的指针和指针变量 # [!tip] 二维数组地址与值\n表现形式 含义 类型 a 第 0 行首地址 地址 a + i 第 i 行首地址 地址 a[i] / *(a + i) 第 i 行首地址 地址 a[i] + j / *(a + i) + j a[i][j] 的地址 地址 *(a[i] + j) / *(*(a + i) + j) a[i][j] 的值 值 ! a 和 *a 都是地址，值相同但含义不同 ! a[i] 在二维数组中代表地址（一维数组名），不是值 int a[3][4]; int *p; p = (int *)(a + 1); // 第 1 行首地址 *p = 56; // 相当于 a[1][0] = 56 p++; *p = 78; // 相当于 a[1][1] = 78 9.3.6 指针数组和数组指针 # [!tip] 指针数组 vs 数组指针\n@ 指针数组: 本质是数组，每个元素都是指针 int *p[10]; — 10 个指针变量的数组 @ 数组指针: 本质是指针，指向一个数组 int (*p)[4]; — 指向含 4 个元素的一维数组的指针 ! 记忆技巧：以什么结尾就是什么。\u0026ldquo;指针数组\u0026quot;是数组，\u0026ldquo;数组指针\u0026quot;是指针 // 指针数组：适合指向若干字符串 const char *pName[] = {\u0026#34;C++\u0026#34;, \u0026#34;Java\u0026#34;, \u0026#34;Python\u0026#34;, \u0026#34;Go\u0026#34;, \u0026#34;CSharp\u0026#34;}; // 交换指针数组中的指针 const char *ptmp; ptmp = pName[0]; pName[0] = pName[1]; // pName[0] 指向 \u0026#34;Java\u0026#34; pName[1] = ptmp; // pName[1] 指向 \u0026#34;C++\u0026#34; 9.3.7 多维数组的指针作为函数参数 # 与一维数组类似，多维数组的地址也可以作为函数参数传递 9.4 字符串的指针和指向字符串的指针变量 # @ 字符串指针图 ![[白板/P26 字符串指针.canvas|P26 字符串指针]] 9.4.1 字符串表示形式 # 用字符数组实现 # char mystr[] = \u0026#34;I love China\u0026#34;; // 字符串复制到数组中 printf(\u0026#34;%s\\n\u0026#34;, mystr); 用字符指针实现 # const char *pmystr = \u0026#34;I love China\u0026#34;; // 指针指向字符串常量 printf(\u0026#34;%s\\n\u0026#34;, pmystr); ! 字符数组：每个数组有独立内存，字符串常量被复制到数组中 ! 字符指针：指向字符串常量的首地址，多个指针可能指向同一地址 ! 字符串常量所在内存是只读的，不能修改 字符串复制范例 # // 下标法 char a[] = \u0026#34;I love China!\u0026#34;; char b[100]; for(int i = 0; *(a + i) != \u0026#39;\\0\u0026#39;; i++) *(b + i) = *(a + i); *(b + i) = \u0026#39;\\0\u0026#39;; // 指针法 char *p1 = a, *p2 = b; for(; *p1 != \u0026#39;\\0\u0026#39;; p1++, p2++) *p2 = *p1; *p2 = \u0026#39;\\0\u0026#39;; 9.4.2 字符串指针作为函数参数 # 用字符数组名或指向字符串的指针变量作为参数，可以在被调函数中改变字符串内容 void copystr(char *from, char *to) { while(*from) // 不遇到 \u0026#39;\\0\u0026#39; 就继续 *to++ = *from++; // 先赋值再各自后移 *to = \u0026#39;\\0\u0026#39;; } 9.4.3 字符指针变量与字符数组 # 对比项 字符数组 字符指针变量 存储方式 各元素存放字符 存放字符串首地址 赋值方式 定义时初始化或 strcpy 可以直接指向字符串常量 值能否改变 数组名值不能改变 指针值可以改变 // 字符数组：不能直接赋值 char str[100]; // str = \u0026#34;I love China!\u0026#34;; // 不允许！ strcpy(str, \u0026#34;I love China!\u0026#34;); // 正确 // 字符指针：可以直接指向 const char *a = \u0026#34;I love China!\u0026#34;; a = a + 7; // 指针可以改变，现在指向 \u0026#34;China\u0026#34; printf(\u0026#34;%s\\n\u0026#34;, a); // China 9.5 函数指针和返回指针值的函数 # @ 函数指针图 ![[白板/P27 函数指针.canvas|P27 函数指针]] 9.5.1 用函数指针变量调用函数 # [!tip] 函数指针\n@ 函数指针: 指向函数的指针变量，通过它可以调用所指向的函数 定义形式: 数据类型 (*指针变量名)(形参列表); int max(int x, int y) { if(x \u0026gt; y) return x; return y; } int (*p)(int, int); // 定义函数指针变量 p = max; // 将函数入口地址赋给 p int c = (*p)(5, 19); // 通过函数指针调用，等价于 c = max(5, 19); // 也可以写成 c = p(5, 19); ! int *p(int, int); 是返回指针的函数声明，不是函数指针！ ! (*p) 的括号不能省，否则优先级不同含义完全不同 ! 对函数指针做 p++、p--、p+n 等运算没有意义，也不合法 9.5.2 把指向函数的指针变量作为函数参数 # 将函数地址传递给形参，可以在函数中调用不同的函数 int callmax(int x, int y, int (*idfunc)(int, int)) { int result = idfunc(x, y); // 通过函数指针调用 return result; } int main() { int c = callmax(5, 19, max); // 传递函数名 // 也可以用函数指针变量 int (*p)(int, int) = max; c = callmax(45, 21, p); return 0; } 9.5.3 返回指针值的函数 # [!tip] 返回指针的函数\n定义形式: 数据类型 *函数名(参数列表) { ... } ! 不要返回局部变量的地址！函数调用完毕后局部变量内存被回收 // 错误示范：返回局部变量地址 int *add(int x, int y) { int sum = x + y; return \u0026amp;sum; // 致命问题！sum 的内存会被回收 } // 正确做法：使用全局变量 int sum; // 全局变量，生存期到程序结束 int *add(int x, int y) { sum = x + y; return \u0026amp;sum; // 安全 } 9.6 指针数组、指针的指针与 main 函数参数 # @ 指针进阶图 ![[白板/P28 指针进阶.canvas|P28 指针进阶]] 9.6.1 指针数组概念回顾 # 指针数组定义: 类型标识符 *数组名[数组长度]; int *p[4]; — 4 个指针元素的数组 int (*p)[4]; — 指向含 4 个元素的一维数组的指针 9.6.2 指向指针的指针 # [!tip] 指向指针的指针\n@ 指向指针的指针: 一个指针变量指向另一个指针变量 定义: int **p; — p 指向一个 int * 类型的指针变量 *p 是 p 指向的指针变量，**p 是最终指向的值 9.6.3 main 函数参数 # main 函数可以接收命令行参数: int main(int argc, char *argv[]) argc: 参数个数 argv: 指针数组，每个元素指向一个参数字符串 ! argv[0] 是可执行文件的完整路径文件名 int main(int argc, char *argv[]) { for(int i = 0; i \u0026lt; argc; i++) printf(\u0026#34;argv[%d] = %s\\n\u0026#34;, i, argv[i]); return 0; } 9.7 本章小结 # 指针数据类型小结 # 定义 含义 int i; 整型变量 i int *p; 指向整型数据的指针变量 int a[n]; 整型数组，n 个元素 int *p[n]; 指针数组，n 个指向整型的指针 int (*p)[n]; 指向含 n 个元素的一维数组的指针 int f(); 函数 f，返回整型值 int *p(); 函数 p，返回指向整型的指针 int (*p)(); 指向函数的指针，函数返回整型值 int **p; 指向指针的指针 指针运算小结 # p++/p--: 地址加减所指向类型占用的字节数 指针变量不能直接赋数字地址，只能赋已分配的变量地址 指针可以指向 NULL（空），表示不指向任何有效内容 void * 型指针 # void * 是万能型指针，可以指向任意数据类型 使用时需要强制类型转换回原类型 void *pvn = NULL; int a = 3; int *p = \u0026amp;a; float f = 5.6f; float *pf = \u0026amp;f; pvn = p; // 不报错 pvn = pf; // 不报错 pf = (float *)pvn; // 强制转换回原类型 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p8-%E6%8C%87%E9%92%88/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e9.1 指针的基本概念详解\n    \u003cdiv id=\"91-指针的基本概念详解\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#91-%e6%8c%87%e9%92%88%e7%9a%84%e5%9f%ba%e6%9c%ac%e6%a6%82%e5%bf%b5%e8%af%a6%e8%a7%a3\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 指针基本概念图\n\u003cul\u003e\n\u003cli\u003e![[白板/P23 指针基本概念.canvas|P23 指针基本概念]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e9.1.1 前提知识\n    \u003cdiv id=\"911-前提知识\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#911-%e5%89%8d%e6%8f%90%e7%9f%a5%e8%af%86\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e变量存储在内存中，不同类型的变量保存在不同存储区（静态存储区、动态存储区）\u003c/li\u003e\n\u003cli\u003e变量的内存在编译时或程序执行时分配\u003c/li\u003e\n\u003cli\u003e各类型占用字节数（x86 平台）:\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e类型\u003c/th\u003e\n          \u003cth\u003e字节数\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eint\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003echar\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003efloat\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e4\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edouble\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e8\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch3 class=\"relative group\"\u003e9.1.2 地址的概念\n    \u003cdiv id=\"912-地址的概念\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#912-%e5%9c%b0%e5%9d%80%e7%9a%84%e6%a6%82%e5%bf%b5\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 地址\u003c/p\u003e","title":"P8 指针","type":"notes"},{"content":" 知识点 # 10.1 结构体变量定义、引用与初始化 # @ 结构体基础图 ![[白板/P29 结构体基础.canvas|P29 结构体基础]] 10.1.1 结构体简介 # [!tip] 结构体\n@ 结构体: 把多种不同类型的数据组合在一起构成的新数据类型 目的: 表达更丰富、更全面的信息 结构体中的每个数据称为一个==结构体成员== ! 结构体是 C++ 中\u0026quot;类\u0026quot;概念的基础 struct student { // 定义一个结构体类型 int num; // 学号 char name[100]; // 姓名 int sex; // 性别 0女, 1男 int age; // 年龄 char address[100]; // 地址 }; // 这里的分号不要忘记! 一般形式: struct 结构体名 { 成员列表 }; 10.1.2 定义结构体类型变量的方法 # 方法一: 先定义类型，再定义变量 # struct student s1, s2; // 定义两个结构体变量 方法二: 定义类型的同时定义变量 # struct student { int num; char name[100]; int sex; int age; char address[100]; } s1, s2; // 同时定义变量，多个变量用逗号分隔 方法三: 直接定义结构体类型变量（省略结构体名） # struct { int num; char name[100]; int sex; } s1, s2; // 省略了结构体名 结构体嵌套 # struct date { // 定义一个结构体类型 int month; int day; int year; }; struct student { int num; char name[100]; int sex; int age; char address[100]; struct date birthday; // 生日，是一个结构体成员 }; ! 结构体内成员名可以与程序中的变量名相同，互不影响 10.1.3 结构体类型变量的引用 # [!tip] 结构体成员运算符\n@ .: 结构体成员运算符，优先级非常高，与圆括号 () 平级 引用方式: 结构体变量名.成员名 引用规则 # ! 不能将结构体变量作为整体引用，只能对各个成员分别引用 ! 嵌套结构体要一级一级找到最低级成员 s1.num = 1001; // 直接赋值 s1.birthday.month = 12; // 嵌套结构体要逐级引用 s1.birthday.day = 30; s1.birthday.year = 2018; // 成员变量可以像普通变量一样运算 s2.age = s1.age; // 赋值 int agesum = s1.age + s2.age; // 求和 s1.age++; // 自增（.优先级最高，s1.age是整体） // 成员变量有地址 int *p = \u0026amp;s1.num; printf(\u0026#34;%d\\n\u0026#34;, *p); 10.1.4 结构体变量的初始化 # struct student s5 = {100, \u0026#34;王五\u0026#34;, 1, 16, \u0026#34;3栋5楼\u0026#34;, 10, 14, 2018}; 10.2 结构体数组与结构体指针 # @ 结构体数组与指针图 ![[白板/P30 结构体数组与指针.canvas|P30 结构体数组与指针]] 10.2.1 结构体数组 # [!tip] 结构体数组\n每个数组元素都是一个结构体变量 可以存放多组数据 定义方式 # // 方式1: 单独定义 struct student stuArr[3]; // 可用下标 0-2 // 方式2: 定义结构体时顺便定义 struct student { int num; // ... } stuArr[3]; 初始化 # struct student stuArr[3] = { {1001, \u0026#34;张三\u0026#34;, 1, 18, \u0026#34;1栋1单元\u0026#34;, 12, 30, 2000}, {1002, \u0026#34;李四\u0026#34;, 1, 20, \u0026#34;2栋2单元\u0026#34;, 11, 15, 1998}, {1003, \u0026#34;王五\u0026#34;, 1, 22, \u0026#34;3栋3单元\u0026#34;, 10, 15, 1996} }; // 不指定元素个数，系统自动推断 struct student stuArr[] = { {1001, \u0026#34;张三\u0026#34;, 1, 18, \u0026#34;1栋1单元\u0026#34;, 12, 30, 2000}, {1002, \u0026#34;李四\u0026#34;, 1, 20, \u0026#34;2栋2单元\u0026#34;, 11, 15, 1998}, {1003, \u0026#34;王五\u0026#34;, 1, 22, \u0026#34;3栋3单元\u0026#34;, 10, 15, 1996} }; 使用 # stuArr[1].age++; // 年龄+1 printf(\u0026#34;name = %s\\n\u0026#34;, stuArr[1].name); // name = 李四 printf(\u0026#34;age = %d\\n\u0026#34;, stuArr[1].age); // age = 21 10.2.2 结构体指针 # [!tip] 结构体指针\n指向结构体变量所占据的内存起始地址 也可指向结构体数组中的元素 struct student stu; // 结构体变量 struct student *ps; // 结构体指针 ps = \u0026amp;stu; strcpy(stu.name, \u0026#34;小虎\u0026#34;); stu.age = 16; 通过指针访问成员的两种方式 # 方式 写法 说明 方式1 (*ps).num *ps 要加括号，因为 . 优先级高于 * 方式2 ps-\u0026gt;num -\u0026gt; 指向结构体成员运算符，优先级最高 (*ps).num = 1008; // 第一种 ps-\u0026gt;sex = 1; // 第二种（推荐） printf(\u0026#34;name: %s\\n\u0026#34;, ps-\u0026gt;name); // name: 小虎 printf(\u0026#34;age = %d\\n\u0026#34;, ps-\u0026gt;age); // age = 16 printf(\u0026#34;num = %d\\n\u0026#34;, ps-\u0026gt;num); // num = 1008 结构体指针与数组 # struct student stuArr[3] = { ... }; struct student *ps; ps = stuArr; // 数组名作为首地址 for(int i = 0; i \u0026lt; 3; i++) { printf(\u0026#34;name = %s\\n\u0026#34;, ps-\u0026gt;name); printf(\u0026#34;age = %d\\n\u0026#34;, ps-\u0026gt;age); printf(\u0026#34;num = %d\\n\u0026#34;, ps-\u0026gt;num); ps++; // 跳过一个数组元素所占的字节数 } int ilen = sizeof(struct student); printf(\u0026#34;ilen = %d\\n\u0026#34;, ilen); // 如 224 指针自增运算 # ps = stuArr; printf(\u0026#34;%d\\n\u0026#34;, (++ps)-\u0026gt;num); // 1002，先++后用 ps = stuArr; printf(\u0026#34;%d\\n\u0026#34;, (ps++)-\u0026gt;num); // 1001，先用后++ ! 结构体指针只能指向同类型的结构体变量或数组元素，不能指向成员 // 错误写法 ps = \u0026amp;stu.num; // 不能指向结构体变量的成员 ps = \u0026amp;stuArr[0].num; // 不能指向数组元素的成员 10.2.3 用指向结构体的指针作为函数参数 # [!tip] 传参方式对比\n方式 效果 效率 传结构体指针 可修改原数据 高（只传地址） 传结构体变量 不能修改原数据 低（复制全部成员） // 方式1: 传指针（推荐） void func1(struct student *pd) { pd-\u0026gt;age = 118; // 修改原数据 } int main() { struct student *ps = stuArr; func1(ps); // 传指针 printf(\u0026#34;stuArr[0].age = %d\\n\u0026#34;, stuArr[0].age); // 118，已修改 return 0; } // 方式2: 传整个结构体变量（不推荐） void func1(struct student d) { d.age = 118; // 只修改形参副本，不影响原数据 } int main() { stuArr[0].age = 12; func1(stuArr[0]); // 传整个变量 printf(\u0026#34;stuArr[0].age = %d\\n\u0026#34;, stuArr[0].age); // 12，未修改 return 0; } ! 传整个结构体变量开销大（所有成员数据全部复制），建议用指针 10.3 共用体、枚举类型与 typedef # @ 共用体枚举typedef图 ![[白板/P31 共用体枚举typedef.canvas|P31 共用体枚举typedef]] 10.3.1 共用体 # [!tip] 共用体（联合）\n@ 共用体: 几种不同类型的变量存放到同一段内存单元 成员共用同一段内存，会互相覆盖 ! 共用体大小 = 占用内存最大的成员的大小 union myun { int carnum; // 轿车编号，4字节 char cartype; // 轿车类型，1字节 char cname[60]; // 轿车名，60字节 } a, b, c; // 也可以分开定义 union myun a, b, c; // 也可以省略共用体名 union { int carnum; char cartype; char cname[60]; } a, b, c; 共用体 vs 结构体 # 对比项 结构体 共用体 内存大小 各成员大小之和（可能因对齐更多） 最大成员的大小 成员关系 各占不同内存，互不影响 共用同一段内存，互相覆盖 同时有效 所有成员同时有效 每一瞬间只有一个成员有效 共用体要点 # ! 每一瞬间只能有一个成员起作用，最后赋值的成员起作用 ! 共用体变量地址和其所有成员的地址都相同 ! 不能在定义时给所有成员初始化，只能初始化第一个成员 union myun a; a.carnum = 128924898; strcpy(a.cname, \u0026#34;小汽车\u0026#34;); // cname起作用，carnum的值已无意义 // 初始化第一个成员是允许的 union myun b = {12}; // 正确 // 不能初始化所有成员 // union myun a = {12, \u0026#39;A\u0026#39;, \u0026#34;小汽车\u0026#34;}; // 报错! 10.3.2 枚举类型 # [!tip] 枚举类型\n@ 枚举: 将值一一列举出来，用有意义的名字代替数字 枚举常量默认从 0 开始递增 enum color { // color 是枚举类型名 Red, // 0 Green, // 1 Blue, // 2 Yellow // 3 }; enum color mycolor1, mycolor2; // 定义枚举变量 mycolor1 = Red; // 赋值 // 也可以省略枚举类型名 enum {Red, Green, Blue, Yellow} mycolor1, mycolor2; 枚举要点 # % 枚举常量是常量，不是变量，不能对它们赋值 % 默认值从 0 开始递增 % 可以改变默认值 enum color { Red = 1, // 1 Green, // 2（前一个+1） Blue = 5, // 5 Yellow // 6（前一个+1） }; printf(\u0026#34;Red = %d\\n\u0026#34;, Red); // 1 printf(\u0026#34;Yellow = %d\\n\u0026#34;, Yellow); // 6 10.3.3 typedef 关键字 # [!tip] typedef\n@ typedef: 为已有的数据类型定义一个新的名字（别名） ! typedef 不创建新类型，只是给已有类型起别名 // 给 int 起别名 typedef int INTEGER; INTEGER a, b; // 等价于 int a, b; // 给结构体起别名（最常用） typedef struct student { int num; char name[100]; int age; } STU; STU s1, s2; // 不用写 struct student，直接用 STU // 给指针类型起别名 typedef int *PINT; PINT p1, p2; // 等价于 int *p1, *p2; // 给数组类型起别名 typedef int ARR[10]; ARR a, b; // 等价于 int a[10], b[10]; typedef 与 #define 的区别 # 对比项 typedef #define 处理时机 编译时 预处理时 类型检查 有 无 作用范围 受作用域限制 从定义到文件末尾 本质 类型别名 文本替换 // typedef 正确处理指针类型 typedef int *PINT; PINT a, b; // a 和 b 都是 int* // #define 可能出错 #define PINT int* PINT a, b; // a 是 int*，b 是 int！（宏展开为 int* a, b;） ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p9-%E7%BB%93%E6%9E%84%E4%BD%93%E4%B8%8E%E5%85%B1%E7%94%A8%E4%BD%93/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e10.1 结构体变量定义、引用与初始化\n    \u003cdiv id=\"101-结构体变量定义引用与初始化\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#101-%e7%bb%93%e6%9e%84%e4%bd%93%e5%8f%98%e9%87%8f%e5%ae%9a%e4%b9%89%e5%bc%95%e7%94%a8%e4%b8%8e%e5%88%9d%e5%a7%8b%e5%8c%96\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 结构体基础图\n\u003cul\u003e\n\u003cli\u003e![[白板/P29 结构体基础.canvas|P29 结构体基础]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e10.1.1 结构体简介\n    \u003cdiv id=\"1011-结构体简介\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1011-%e7%bb%93%e6%9e%84%e4%bd%93%e7%ae%80%e4%bb%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 结构体\u003c/p\u003e","title":"P9 结构体与共用体","type":"notes"},{"content":"C语言基础与进阶。\n","externalUrl":null,"permalink":"/notes/c-cpp/c/","section":"S","summary":"\u003cp\u003eC语言基础与进阶。\u003c/p\u003e","title":"C","type":"notes"},{"content":"C/C++ 编程语言学习笔记。\n","externalUrl":null,"permalink":"/notes/c-cpp/","section":"S","summary":"\u003cp\u003eC/C++ 编程语言学习笔记。\u003c/p\u003e","title":"C/C++","type":"notes"},{"content":"C语言基础与进阶。\n","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/","section":"S","summary":"\u003cp\u003eC语言基础与进阶。\u003c/p\u003e","title":"C语言程序设计","type":"notes"},{"content":" 知识点 # 11.1 位的概念和位运算符简介 # @ 位运算基础图 ![[白板/P32 位运算基础.canvas|P32 位运算基础]] 11.1.1 位的概念 # [!tip] 位的基本概念\n1 字节 = 8 个二进制位 最左边的位称为==最高位==，最右边的位称为==最低位== 每个二进制位的值是 0 或 1 unsigned int 占 4 字节 = 32 个二进制位，最大值 4294967295 类型 字节数 二进制位数 char 1 8 int 4 32 unsigned int 4 32 11.1.2 位运算符简介 # 位运算符 含义 运算类型 \u0026amp; 按位与 二目 | 按位或 二目 ^ 按位异或 二目 ~ 取反 单目 \u0026lt;\u0026lt; 左移 二目 \u0026gt;\u0026gt; 右移 二目 1. 按位与运算符 \u0026amp; # [!tip] 按位与\n两个相应位都为 1，结果为 1，否则为 0 公式: 0\u0026amp;0=0; 0\u0026amp;1=0; 1\u0026amp;0=0; 1\u0026amp;1=1 unsigned int tempvalue = 38 \u0026amp; 22; // 6 // 计算过程（二进制）: // 100110 (38) // 010110 (22) // ------ (\u0026amp;) // 000110 (6) 2. 按位或运算符 | # [!tip] 按位或\n两个相应位有一个为 1，结果为 1，否则为 0 公式: 0|0=0; 0|1=1; 1|0=1; 1|1=1 unsigned int tempvalue = 38 | 22; // 54 // 计算过程（二进制）: // 100110 (38) // 010110 (22) // ------ (|) // 110110 (54) 3. 按位异或运算符 ^ # [!tip] 按位异或\n两个相应位相同为 0，不同为 1 ==都一样出 0，不一样出 1== 公式: 0^0=0; 0^1=1; 1^0=1; 1^1=0 unsigned int tempvalue = 38 ^ 22; // 49 // 计算过程（二进制）: // 100110 (38) // 010110 (22) // ------ (^) // 110000 (49) ! 与 1 异或→翻转，与 0 异或→保持不变 // 低4位翻转，高4位保持不变 // 01111010 // 00001111 // -------- (^) // 01110101 4. 取反运算符 ~ # [!tip] 取反\n单目运算符，0 变 1，1 变 0 unsigned int tempvalue = ~38; // 4294967257 // 计算过程: // 00000000,00000000,00000000,00100110 (38) // ------------------------------------ (~) // 11111111,11111111,11111111,11011001 (4294967257) 5. 左移运算符 \u0026lt;\u0026lt; # [!tip] 左移\n二进制位左移若干位，右侧补 0 ==每左移一位相当于乘以 2== unsigned int tempvalue = 15 \u0026lt;\u0026lt; 1; // 30 // 01111 → 11110 unsigned int tempvalue = 15 \u0026lt;\u0026lt; 2; // 60 // 001111 → 111100 6. 右移运算符 \u0026gt;\u0026gt; # [!tip] 右移\n二进制位右移若干位，超出的位被舍弃，左侧补 0 ==每右移一位相当于除以 2== unsigned int tempvalue = 15 \u0026gt;\u0026gt; 1; // 7 // 1111 → 0111 unsigned int tempvalue = 15 \u0026gt;\u0026gt; 2; // 3 // 1111 → 0011 复合赋值运算符 # 复合运算符 等价写法 a \u0026amp;= b a = a \u0026amp; b a |= b a = a | b a ^= b a = a ^ b a \u0026lt;\u0026lt;= b a = a \u0026lt;\u0026lt; b a \u0026gt;\u0026gt;= b a = a \u0026gt;\u0026gt; b int a = 35; int b = 22; a \u0026amp;= b; // a = a \u0026amp; b = 2 // 100011 (35) // 010110 (22) // ------ (\u0026amp;) // 000010 (2) 11.2 位运算的具体应用 # @ 位运算应用图 ![[白板/P33 位运算应用.canvas|P33 位运算应用]] 实际应用: 每日任务标记 # [!tip] 位运算实际用途\n用一个 unsigned int 变量记录多达 32 种状态 每个二进制位代表一个任务：0 = 未完成，1 = 已完成 通过按位与判断任务是否完成 通过按位或标记任务完成 核心宏定义 # #define BIT(n) (1 \u0026lt;\u0026lt; (n)) // 实用价值极高 // BIT(0) = 1, BIT(1) = 2, BIT(2) = 4, BIT(3) = 8 // BIT(4) = 16, BIT(5) = 32, BIT(6) = 64, BIT(7) = 128 // BIT(8) = 256, BIT(9) = 512 BIT(n) 十进制 二进制 左移位数 BIT(0) 1 1 左移 0 位 BIT(1) 2 10 左移 1 位 BIT(2) 4 100 左移 2 位 BIT(3) 8 1000 左移 3 位 BIT(4) 16 10000 左移 4 位 BIT(5) 32 100000 左移 5 位 BIT(6) 64 1000000 左移 6 位 BIT(7) 128 10000000 左移 7 位 完整实现代码 # // 定义10个任务的枚举 enum ETask { ETask1 = BIT(0), // 1 = 1 ETask2 = BIT(1), // 2 = 10 ETask3 = BIT(2), // 4 = 100 ETask4 = BIT(3), // 8 = 1000 ETask5 = BIT(4), // 16 = 10000 ETask6 = BIT(5), // 32 = 100000 ETask7 = BIT(6), // 64 = 1000000 ETask8 = BIT(7), // 128 = 10000000 ETask9 = BIT(8), // 256 = 100000000 ETask10 = BIT(9) // 512 = 1000000000 }; unsigned int task = 0; // 初始化为0，所有任务未完成 // 判断任务7是否完成 if (task \u0026amp; ETask7) { // 按位与，不为0表示做过 printf(\u0026#34;任务7已经做过了\\n\u0026#34;); } else { printf(\u0026#34;任务7还没做过，现在做任务7\\n\u0026#34;); // 标记任务7完成（按位或） task = task | ETask7; // 将第7位标记为1 } // 再次判断 if (task \u0026amp; ETask7) { printf(\u0026#34;任务7已经做过了，可以保存到数据库\\n\u0026#34;); } 位运算操作总结 # 操作 运算符 说明 判断某位是否为 1 task \u0026amp; BIT(n) 结果不为 0 表示该位为 1 将某位置为 1 task |= BIT(n) 按位或，标记任务完成 将某位置为 0 task \u0026amp;= ~BIT(n) 按位与取反，清除标记 翻转某位 task ^= BIT(n) 按位异或，0↔1 切换 ! 一个 unsigned int 变量可记录 32 种状态，节省大量存储空间 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p10-%E4%BD%8D%E8%BF%90%E7%AE%97/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e11.1 位的概念和位运算符简介\n    \u003cdiv id=\"111-位的概念和位运算符简介\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#111-%e4%bd%8d%e7%9a%84%e6%a6%82%e5%bf%b5%e5%92%8c%e4%bd%8d%e8%bf%90%e7%ae%97%e7%ac%a6%e7%ae%80%e4%bb%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 位运算基础图\n\u003cul\u003e\n\u003cli\u003e![[白板/P32 位运算基础.canvas|P32 位运算基础]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e11.1.1 位的概念\n    \u003cdiv id=\"1111-位的概念\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1111-%e4%bd%8d%e7%9a%84%e6%a6%82%e5%bf%b5\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 位的基本概念\u003c/p\u003e","title":"P10 位运算","type":"notes"},{"content":" 知识点 # 12.1 文件简介及文本、二进制文件区别 # @ 文件基础图 ![[白板/P34 文件基础.canvas|P34 文件基础]] 12.1.1 文件简介 # [!tip] 文件\n@ 文件: 保存在硬盘、U盘等存储介质上的数据 操作系统以文件为单位对数据进行管理 文件可以看成字符序列 12.1.2 文本文件和二进制文件区别 # [!tip] 文本文件 vs 二进制文件\n@ 文本文件（ASCII 文件）: 每个字节存放一个 ASCII 码，代表一个字符，人类能看懂 @ 二进制文件: 把内存中的数据按存储形式原样输出到磁盘，人类可能看不懂 对比项 文本文件 二进制文件 存储方式 每个 ASCII 码占 1 字节 按内存存储形式原样保存 可读性 人类能看懂 人类可能看不懂（乱码） 存储空间 较大（如 10000 占 5 字节） 较小（如 10000 占 2 字节） 转换开销 需要二进制↔ASCII 转换 无需转换 ! 对于计算机来讲，==没有文本文件和二进制文件之分==，都是二进制数据 ! 文本文件可以理解为\u0026quot;人类能看懂的二进制文件\u0026quot; 大端存储与小端存储 # [!tip] 字节序\n@ 小端存储: 低字节存低地址，高字节存高地址 @ 大端存储: 低字节存高地址，高字节存低地址 与 CPU 体系结构有关 // 10000 = 0x2710 // 小端存储: 10 27（低地址→高地址） // 大端存储: 27 10（低地址→高地址） 12.2 文件的打开、关闭、读写与实战操练 # @ 文件操作图 ![[白板/P35 文件操作.canvas|P35 文件操作]] 12.2.1 文件的打开 # [!tip] fopen 函数\n一般形式: FILE *fp = fopen(文件名, 文件使用方式); FILE 是一个结构体，fp 是指向 FILE 的指针变量（文件指针） ! 打开失败时返回 NULL FILE *fp; fp = fopen(\u0026#34;A1\u0026#34;, \u0026#34;r\u0026#34;); // 以只读方式打开 A1 文件 if(fp == NULL) { // 文件打开失败的处理代码 } 常用文件使用方式 # 文件使用方式 含义 \u0026quot;r\u0026quot; 只读，打开文本文件，文件必须存在 \u0026quot;w\u0026quot; 只写，打开文本文件，存在则覆盖，不存在则创建 \u0026quot;a\u0026quot; 追加，向文本文件末尾增加数据 \u0026quot;rb\u0026quot; 只读，打开二进制文件 \u0026quot;wb\u0026quot; 只写，打开二进制文件 \u0026quot;ab\u0026quot; 追加，向二进制文件末尾增加数据 \u0026quot;r+\u0026quot; 读写，打开文本文件 \u0026quot;w+\u0026quot; 读写，建立新的文本文件 规律记忆 # % 有 w → 往文件中写入 % 有 r → 从文件中读出 % 有 b → 二进制文件 % 有 a → 追加内容到文件末尾 % 有 + → 既能读又能写 文件位置指针 # 每个打开的文件都有一个当前位置指针 代表当前从文件的哪个位置开始读/写数据 每读出 1 字节数据，位置指针自动后移 1 字节 12.2.2 文件的关闭 # [!tip] fclose 函数\n一般形式: fclose(文件指针); ! 必须关闭文件，原因: 释放文件占用的内存资源 将缓冲区中的数据写入磁盘，避免数据丢失 if(fp != NULL) fclose(fp); ! 关闭后 fp 不能再使用 fclose() 返回 0 表示成功，非 0 表示失败 12.2.3 文件的读写 # fputc — 写一个字符 # [!tip] fputc\n调用形式: fputc(ch, fp); 将字符 ch 写到 fp 所指向的文件中 成功返回字符的 ASCII 码，失败返回 EOF FILE *fp; fp = fopen(\u0026#34;FTest.txt\u0026#34;, \u0026#34;w\u0026#34;); if(fp == NULL) { printf(\u0026#34;文件打开失败\u0026#34;); } else { char reco = fputc(\u0026#39;a\u0026#39;, fp); if(reco == EOF) { // 写失败的处理代码 } fputc(\u0026#39;d\u0026#39;, fp); fputc(\u0026#39;e\u0026#39;, fp); fclose(fp); } fgetc — 读一个字符 # [!tip] fgetc\n调用形式: char reco = fgetc(fp); 从 fp 所指向的文件中读入一个字符 成功返回读入的字符，失败或到文件末尾返回 EOF FILE *fp; fp = fopen(\u0026#34;FTest.txt\u0026#34;, \u0026#34;r\u0026#34;); if(fp == NULL) { printf(\u0026#34;文件打开失败\u0026#34;); } else { char reco = fgetc(fp); while(reco != EOF) { putchar(reco); // 在屏幕上输出 reco = fgetc(fp); // 继续读 } fclose(fp); } fgets — 读一行字符串 # [!tip] fgets\n调用形式: fgets(str, n, fp); 从 fp 指向的文件中读入 n-1 个字符到 str 中 遇到 \\n 或 EOF 提前结束 ! 自动在末尾加 \\0 fputs — 写一行字符串 # [!tip] fputs\n调用形式: fputs(str, fp); 将字符串 str 写到 fp 指向的文件中 12.2.4 文件读写实战操练 # // 读取配置文件 config.txt FILE *fp; fp = fopen(\u0026#34;config.txt\u0026#34;, \u0026#34;r\u0026#34;); if(fp == NULL) { printf(\u0026#34;文件打开失败\\n\u0026#34;); } else { char LineBuf[1024]; while(fgets(LineBuf, sizeof(LineBuf), fp) != NULL) { // 处理每一行数据 printf(\u0026#34;%s\u0026#34;, LineBuf); } fclose(fp); } 12.3 将结构体数据写入二进制文件和从二进制文件读出 # @ 结构体文件操作图 ![[白板/P36 结构体文件操作.canvas|P36 结构体文件操作]] 12.3.1 将结构体数据写入二进制文件 # [!tip] fwrite 函数\n一般形式: fwrite(buffer, size, count, fp); buffer: 要写入数据的地址 size: 要写入的字节数 count: 要写入多少个 size 字节的数据项 fp: 文件指针 返回值: 失败返回 0，成功返回 count #pragma pack(1) // 1字节对齐，避免跨平台问题 struct stu { char name[30]; int age; double score; }; #pragma pack() // 恢复默认对齐 struct stu student[2] = { {\u0026#34;张三\u0026#34;, 18, 80.5}, {\u0026#34;李四\u0026#34;, 20, 90.5} }; FILE *fp; fp = fopen(\u0026#34;structfile.bin\u0026#34;, \u0026#34;wb\u0026#34;); // 二进制写方式 if(fp != NULL) { fwrite(student, sizeof(struct stu), 2, fp); fclose(fp); } 结构体写入的坑点 # ! 结构体成员中不要出现指针类型，指针地址在程序再次运行时会失效 ! 结构体字节对齐问题：不同编译器/平台对齐方式不同 Windows VS 可能按 8 字节对齐 Linux GCC 可能按 4 字节对齐 解决方案: #pragma pack(1) 设置 1 字节对齐 12.3.2 从二进制文件中读出结构体数据 # [!tip] fread 函数\n一般形式: fread(buffer, size, count, fp); buffer: 读出数据存放的地址 size: 要读出的字节数 count: 要读出多少个 size 字节的数据项 fp: 文件指针 返回值: 失败返回 0，成功返回 count FILE *fp; fp = fopen(\u0026#34;structfile.bin\u0026#34;, \u0026#34;rb\u0026#34;); // 二进制读方式 if(fp != NULL) { struct stu studentnew[2]; int retresult = fread(studentnew, sizeof(struct stu), 2, fp); fclose(fp); } 12.3.3 文件使用方式中 \u0026quot;r\u0026quot; 与 \u0026quot;rb\u0026quot; 和 \u0026quot;w\u0026quot; 与 \u0026quot;wb\u0026quot; 的区别 # [!warning] b 标记的重要性\n! 写和读是配套操作，写用了 \u0026quot;b\u0026quot; 标记，读也必须用 \u0026quot;b\u0026quot; 标记 ! 不带 \u0026quot;b\u0026quot; 标记读文件时，\\r 可能被丢弃 ! 不带 \u0026quot;b\u0026quot; 标记写文件时，\\n 可能被自动替换为 \\r\\n（Windows 平台） ! 带 \u0026quot;b\u0026quot; 标记会原封不动地读/写，不做任何转换 操作 不带 b 带 b 读 可能缺失 \\r 原封不动读出 写 \\n 可能变成 \\r\\n 原封不动写入 ! 结论: 希望原封不动地读写文件内容，请在 fopen 中使用 \u0026quot;b\u0026quot; 标记 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p11-%E6%96%87%E4%BB%B6/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e12.1 文件简介及文本、二进制文件区别\n    \u003cdiv id=\"121-文件简介及文本二进制文件区别\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#121-%e6%96%87%e4%bb%b6%e7%ae%80%e4%bb%8b%e5%8f%8a%e6%96%87%e6%9c%ac%e4%ba%8c%e8%bf%9b%e5%88%b6%e6%96%87%e4%bb%b6%e5%8c%ba%e5%88%ab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 文件基础图\n\u003cul\u003e\n\u003cli\u003e![[白板/P34 文件基础.canvas|P34 文件基础]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e12.1.1 文件简介\n    \u003cdiv id=\"1211-文件简介\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1211-%e6%96%87%e4%bb%b6%e7%ae%80%e4%bb%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 文件\u003c/p\u003e","title":"P11 文件","type":"notes"},{"content":" 知识点 # 13.1 语言特性、工程构成与可移植性 # @ 语言特性图 ![[白板/P37 语言特性.canvas|P37 语言特性]] 13.1.1 语言特性：过程式、对象式程序设计 # [!tip] 面向过程式程序设计\n@ 面向过程: 从上到下，逐步求精，公用功能写成函数，需要结构体就定义结构体 代码按顺序一步一步解决问题 C++ 对过程式程序设计完全支持 [!tip] 基于对象的程序设计\n@ 基于对象: 把功能包到类中，定义类对象并通过该对象调用各种成员函数 C 中的\u0026quot;结构\u0026quot; → C++ 中的\u0026quot;类\u0026quot; C 中的\u0026quot;结构变量\u0026quot; → C++ 中的\u0026quot;对象\u0026quot; 类比结构更强大：类中不仅可以定义成员变量，还可以定义成员函数（方法） [!tip] 面向对象的程序设计\n@ 面向对象 = 基于对象 + 继承性 + 多态性 继承性: 子类继承父类的方法，并可增加自己的新成员函数 多态性: 父类和子类有同名成员函数，调用时根据具体情况决定调用哪个 优点：易维护、易扩展、模块化（通过访问级别限制访问） 13.1.2 C++程序和项目文件构成 # [!tip] 项目文件构成\n% 源文件: .cpp 后缀，也叫源文件 % 头文件: .h 后缀，公共定义放在头文件中（函数声明、类定义、#define 等） .hpp: 定义和实现都包含在一个文件里，减少编译次数 C++ 标准头文件不带扩展名，如 \u0026lt;cstdio\u0026gt; 代替 \u0026lt;stdio.h\u0026gt; 13.1.3 编译型语言概念与可移植性问题 # [!tip] 编译型 vs 解释型\n@ 编译型语言: 程序执行前需要专门编译过程，编译成二进制文件，执行时不需要重新翻译 @ 解释型语言: 不进行预先编译，执行时先解释再执行 C++ 属于编译型语言，执行速度快 ! 可移植性是针对源代码而言的，相同源代码在不同操作系统上能编译运行并实现相同功能 13.2 命名空间简介与基本输入/输出精解 # @ 命名空间图 ![[白板/P38 命名空间与IO.canvas|P38 命名空间与IO]] 13.2.1 命名空间简介 # [!tip] 命名空间\n@ 命名空间: 为防止名字冲突而引入的机制 每个命名空间都有自己的名字，不可以同名 不同命名空间中的同名函数互不影响 namespace NMZhangSan // 定义命名空间 { void radius() { printf(\u0026#34;NMZhangSan::radius 函数被执行\\n\u0026#34;); } } [!tip] 命名空间使用要点\n1 访问命名空间中的实体：命名空间名::实体名（:: 叫作用域运算符） 1 using namespace 命名空间名; 声明后可省略前缀 ! 同时 using namespace 声明两个含同名函数的命名空间会报错 解决方案：①不同时声明两个命名空间 ②不同命名空间函数不同名 ③调用时加前缀 13.2.2 基本输入/输出 # [!tip] C++ 输入/输出\n@ iostream 库: C++ 标准库中的输入/输出流库 #include \u0026lt;iostream\u0026gt; 包含头文件（不带扩展名） std::cout: 标准输出对象，向屏幕输出内容 \u0026lt;\u0026lt;: 输出运算符，将右侧内容写到 cout（屏幕）中 std::endl: 换行 + 刷新输出缓冲区 #include \u0026lt;iostream\u0026gt; using namespace std; int main() { int x = 3; cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34;的平方是\u0026#34; \u0026lt;\u0026lt; x * x \u0026lt;\u0026lt; endl; // 3的平方是9 return 0; } [!tip] std::endl 的两个作用\n1 输出换行符 \\n 1 刷新输出缓冲区（调用 flush，强制输出缓冲区中所有数据到屏幕） [!tip] 基本输入 cin\nstd::cin: 标准输入对象 \u0026gt;\u0026gt;: 输入运算符，从键盘读取数据 int a; cin \u0026gt;\u0026gt; a; // 从键盘读入一个整数 13.3 自动类型推断、头文件防卫与引用 # 13.3.1 自动类型推断 # [!tip] C++ 新标准初始化方式\nC++11 引入 {} 初始化：int abc{12}; {} 初始化更安全：int abc{3.5f}; 会编译报错（防止数据被误截断） () 初始化也可以：int abc(12); [!tip] auto 关键字\n@ auto: 变量的自动类型推断（C++11 新含义） 声明变量时根据初始值类型自动选择匹配类型 自动类型推断发生在编译期，不影响运行效率 ! 定义 auto 变量时必须给初始值 auto bvalue = true; // bool auto ch = \u0026#39;a\u0026#39;; // char auto dv = 12; // int auto ev = -5; // int 13.3.2 头文件防卫式声明 # [!tip] 头文件防卫式声明\n@ 防卫式声明: 使用 #ifndef、#define、#endif 防止头文件被多次 #include ! 每个 .h 头文件的 #ifndef 后面定义的名字都不一样 // head.h #ifndef HEAD #define HEAD int g_globalh1 = 8; #endif 13.3.3 引用 # [!tip] 引用\n@ 引用: 为变量起的另外一个名字（别名），用 \u0026amp; 符号表示 引用和原变量看成是同一个变量，不额外占用内存 ! 定义引用时必须初始化 ! 引用必须绑定到变量或对象上，不能绑定到常量 ! 引用类型必须与原变量类型相同 int value = 10; int \u0026amp;refval = value; // refval 是 value 的别名 refval = 13; // 等价于 value = 13 // 错误示例 int\u0026amp; refval2; // 错误！必须初始化 int\u0026amp; refval3 = 10; // 错误！不能绑定到常量 float\u0026amp; refbvalue = bvalue; // 错误！类型不同 [!tip] 引用 vs 取地址\nint \u0026amp;b = a; — 引用，\u0026amp; 在 = 左边 int *p = \u0026amp;a; — 取地址符，\u0026amp; 在 = 右边 [!tip] 引用作为函数形参\n引用形参可以直接影响外界实参的值 不存在参数值复制问题，效率更高 void func(int \u0026amp;ta, int \u0026amp;tb) // 引用类型形参 { ta = 4; // 直接影响 main() 中的 a tb = 5; // 直接影响 main() 中的 b } 13.3.4 常量 # [!tip] const 关键字\n@ const: 表示不变，定义变量时加 const 后值不可改变 编译器会检查 const 承诺 ! const 变量实际上可以通过特殊手段修改，但强烈建议不要这样做 const int var = 17; // 承诺值不变 // var = 18; // 编译报错 // 技术上可以修改（但不建议） int \u0026amp;var2 = (int\u0026amp;)var; var2 = 5; // var 的值确实变成了 5 [!tip] constexpr 关键字\n@ constexpr（C++11）: 在编译时求值的常量，提升运行时性能 constexpr 函数中代码必须尽可能简单 ! constexpr 函数中不能定义未初始化变量 ! constexpr 函数中不能有 printf 等语句 constexpr 自带 inline 属性 constexpr int var1 = 1; constexpr int func1(int abc) // constexpr 函数 { return abc * 16; } constexpr int var2 = 11 * func1(12); // 编译时求值 13.4 范围 for、new 内存动态分配与 nullptr # 13.4.1 范围 for 语句 # [!tip] 范围 for 语句\n@ 范围 for（C++11）: 用于遍历一个序列 使用 auto 自动推断元素类型 使用 auto \u0026amp; 引用方式避免数据复制，提高效率 int v[] = {12, 13, 14, 16, 18}; for (auto x : v) // 复制元素到 x cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; endl; for (auto \u0026amp;x : v) // 引用方式，避免复制 cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; endl; 13.4.2 动态内存分配问题 # [!tip] C++ 内存 5 个区域\n% 栈: 函数内局部变量，编译器自动分配和释放 % 堆: 程序员用 malloc/new 申请，free/delete 释放 % 全局/静态存储区: 全局变量和静态变量，程序结束时释放 % 常量存储区: 存放常量，不允许修改 % 程序代码区: 保存程序代码 对比项 栈 堆 空间大小 有限（系统规定） 理论上不超出物理内存 分配速度 快 相对较慢 控制权 程序员控制不了 程序员自由决定 分配方式 自动 new/malloc 手动 [!tip] malloc 和 free\nC 语言中的函数，成对使用，从堆中分配和释放内存 malloc 返回 void * 类型，分配失败返回 NULL ! 使用完必须 free，否则内存泄漏 int *p = NULL; p = (int *)malloc(10 * sizeof(int)); // 分配40字节 if (p != NULL) { *p = 5; cout \u0026lt;\u0026lt; *p \u0026lt;\u0026lt; endl; free(p); // 千万不要忘记！ } [!tip] new 和 delete\nC++ 中的运算符（不是函数），成对使用 new 不但分配内存，还会做初始化工作 delete 不但释放内存，还会做清理工作 ! 在 C++ 中，不要再使用 malloc/free，而是使用 new/delete int *myint = new int; // 分配一个 int 空间 *myint = 8; delete myint; // 释放 int *myint2 = new int(18); // 分配并初始化为 18 delete myint2; int *a = new int[100]; // 分配大小为 100 的数组 delete[] a; // ! 释放数组要用 delete[] [!tip] malloc/free vs new/delete\nnew/delete 做了 malloc/free 同样的事情，还做了更多 new 会额外做初始化，delete 会额外做清理 ! 有 new 必有 delete，有 malloc 必有 free ! free/delete 不要重复调用 13.4.3 nullptr # [!tip] nullptr\n@ nullptr（C++11）: 代表空指针的新关键字 NULL 实际就是 0，类型是 int nullptr 类型是 std::nullptr_t ! NULL 和 nullptr 类型不同，在函数重载时会导致调用不同函数 ! 对于指针初始化，能用 nullptr 的全部用 nullptr void myfunc(void *ptmp) { printf(\u0026#34;void *\\n\u0026#34;); } void myfunc(int tmpvalue) { printf(\u0026#34;int\\n\u0026#34;); } myfunc(NULL); // 调用 myfunc(int) myfunc(nullptr); // 调用 myfunc(void *) 13.5 结构、类与组织 # 13.5.1 结构回顾 # [!tip] C++ 中的结构\nC++ 中结构可以省略 struct 关键字 C++ 中的结构不仅可以有成员变量，还可以有成员函数 成员函数用 对象名.成员函数名(实参列表) 调用 struct student { int number; // 成员变量 char name[100]; // 成员变量 void func() // 成员函数 { number++; } }; student student1; student1.number = 1001; student1.func(); // 调用成员函数 [!tip] 结构体作为函数参数\n值传递：效率低（参数值复制） 引用传递：效率高，形参改变影响实参 指针传递：效率高，通过 -\u0026gt; 访问成员 13.5.2 public 和 private 权限修饰符 # [!tip] 权限修饰符\n@ public: 公有，成员可以被外界访问（外部接口） @ private: 私有，成员只能被类内部定义的成员函数使用 struct 默认所有成员为 public class 默认所有成员为 private 判断成员权限：沿该成员往上方看，先看到哪个修饰符就属于哪个 struct student { public: int number; // public private: char name[100]; // private public: void func() // public { number++; // 成员函数可以访问 private 成员 } }; 13.5.3 类简介 # [!tip] 类与结构的区别\n类用 class 定义，结构用 struct 定义 class 默认访问级别为 private，struct 默认为 public class 默认继承为 private，struct 默认继承为 public 如果都明确写上访问级别，struct 和 class 没什么区别 [!tip] 类的核心要点\nC++ 标准库包含大量丰富的类和函数 ! 在 C++ 中，无论实现什么功能，都应设法通过写一个或多个类来达到目的 类是 C++ 语言中最核心的部件 13.5.4 类的组织 # [!tip] 类的代码组织\n1 类的定义代码放到 .h 头文件中（头文件名与类名相同） 1 类的实现代码（成员函数实现）放到 .cpp 文件中 1 其他文件使用类时，#include \u0026quot;类名.h\u0026quot; 包含头文件 13.6 函数新特性、inline 内联函数与 const 详解 # 13.6.1 函数回顾与后置返回类型 # [!tip] 后置返回类型\n@ 前置返回类型: 返回类型在函数声明开头（传统写法） @ 后置返回类型（C++11）: 返回类型写在参数列表之后，用 -\u0026gt; 开始 前面放 auto 关键字 // 前置返回类型 int func(int a, int b); // 后置返回类型 auto func(int a, int b) -\u0026gt; int; 13.6.2 inline 内联函数 # [!tip] inline 内联函数\n@ 内联函数: 在编译阶段将函数调用替换为函数本体，提升执行性能 inline 只是给编译器的建议，编译器可以不采纳 内联函数定义放在头文件中 ! 内联函数体要尽可能短小 ! 复杂的循环、分支、递归不应出现在内联函数中 constexpr 函数自带 inline 属性 [!tip] 内联函数优缺点\n$ 优点：避免函数调用开销（压栈、出栈），增加效率 $ 缺点：代码膨胀，多处调用导致多处重复代码 13.6.3 函数特殊写法总结 # [!tip] 函数特殊写法\n返回 void 的函数可以作为另一个 void 函数的返回值：return funcA(); ! 不要返回局部变量的指针或引用（局部变量函数结束后被销毁） C++ 中更习惯使用引用类型形参取代指针类型形参 函数重载：同名函数形参类型或数量应有明显区别 ! const 关键字在比较同名函数时会被忽略，不能仅靠 const 区分重载 13.6.4 const char *、char const * 与 char * const 三者的区别 # [!tip] 指针与 const 三种写法\n@ const char *p: 指向常量的指针，*p 不能改（指向的内容不能改），p 能改（指向能改） @ char const *p: 与 const char *p 完全等价 @ char * const p: 常量指针，p 不能改（指向不能改），*p 能改（指向的内容能改） 记忆技巧：const 在 * 左边修饰内容，const 在 * 右边修饰指针 写法 指向能改 内容能改 含义 const char *p 能 不能 指向常量字符 char const *p 能 不能 同上 char * const p 不能 能 常量指针 13.6.5 函数形参中带 const # [!tip] 函数形参使用 const\n! 防止无意中修改形参值导致实参值被修改 const 形参可以接收更多类型的实参（常量引用和普通引用都能接收） 建议形参使用常量引用 const 类型 \u0026amp; void fs(const student \u0026amp;stu) // 常量引用形参 { // stu.num = 1010; // 错误！不能修改 } void func2(const int \u0026amp;a) {} func2(156); // 可以传递常量 13.7 string 类型 # @ string 类型图 ![[白板/P39 string类型.canvas|P39 string类型]] 13.7.1 总述 # [!tip] string 类型\n@ string: 标准库中的类型，代表可变长字符串 位于 std 命名空间中 #include \u0026lt;string\u0026gt; 包含头文件 尽量使用标准库提供的功能，不要重复开发 13.7.2 定义和初始化 string 对象 # string s1; // 默认初始化，空串 string s2 = \u0026#34;I love China\u0026#34;; // 复制初始化 string s3(\u0026#34;I love China\u0026#34;); // 直接初始化 string s4 = s2; // 拷贝初始化 string s5(6, \u0026#39;a\u0026#39;); // 6个字符\u0026#39;a\u0026#39;组成的字符串 13.7.3 string 对象上的常用操作 # [!tip] string 常用操作\n操作 说明 示例 s.empty() 判断是否为空 if (s1.empty()) s.size() / s.length() 返回字符数（字节数） s2.size() s[n] 返回第 n 个字符（从 0 开始） s3[4] s1 + s2 字符串连接 s6 = s4 + s5 s1 = s2 赋值 s7 = s8 s1 == s2 判断相等（大小写敏感） if (s9 == s10) s1 != s2 判断不等 if (s9 != s10) s.c_str() 返回 C 风格字符串指针 const char *p = s.c_str() string s3 = \u0026#34;I love China\u0026#34;; s3[4] = \u0026#39;w\u0026#39;; // 修改字符 cout \u0026lt;\u0026lt; s3 \u0026lt;\u0026lt; endl; // I wove China // c_str() 与 C 语言兼容 const char *p = s10.c_str(); char str[100]; strcpy_s(str, sizeof(str), p); [!tip] string 读写与字面值相加\ncin \u0026gt;\u0026gt; s1 读入字符串（遇空格停止） ! 两个字符串字面值不能直接相加：\u0026quot;abc\u0026quot; + \u0026quot;def\u0026quot; 语法错误 正确写法：\u0026quot;abc\u0026quot; + s1 + \u0026quot;def\u0026quot;（中间夹一个 string 对象） [!tip] 范围 for 遍历 string\nstring s1 = \u0026#34;I love China\u0026#34;; for (auto \u0026amp;c : s1) // 引用方式，可修改 c = toupper(c); // 小写转大写 cout \u0026lt;\u0026lt; s1 \u0026lt;\u0026lt; endl; // I LOVE CHINA 13.8 vector 类型 # @ vector 类型图 ![[白板/P40 vector类型.canvas|P40 vector类型]] 13.8.1 vector 类型简介 # [!tip] vector 类型\n@ vector: 标准库中的容器/动态数组类型 #include \u0026lt;vector\u0026gt; 包含头文件 vector\u0026lt;int\u0026gt; vjihe; 定义存放 int 的容器 vector 是类模板，\u0026lt;int\u0026gt; 提供元素类型信息 ! vector 不能装引用（引用不是对象） vector\u0026lt;int\u0026gt; vjihe; // 存放 int vector\u0026lt;string\u0026gt; mystr; // 存放 string vector\u0026lt;student\u0026gt; studlist; // 存放自定义结构 // vector\u0026lt;int\u0026amp;\u0026gt; vjihe3; // 错误！不能装引用 13.8.2 定义和初始化 vector 对象 # vector\u0026lt;string\u0026gt; mystr; // 空 vector vector\u0026lt;string\u0026gt; mystr2(mystr); // 复制元素 vector\u0026lt;string\u0026gt; mystr3 = mystr; // 复制元素 vector\u0026lt;string\u0026gt; def{\u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, \u0026#34;ccc\u0026#34;}; // 初始化列表 vector\u0026lt;int\u0026gt; ijihe(15, -200); // 15个元素，每个值为 -200 vector\u0026lt;int\u0026gt; ijihe2(20); // 20个元素，每个值为 0 [!tip] () vs {} 初始化\n() 一般表示元素数量 {} 一般表示元素内容 vector\u0026lt;int\u0026gt; i1(10); — 10个元素，每个为 0 vector\u0026lt;int\u0026gt; i2{10}; — 1个元素，值为 10 13.8.3 vector 对象上的操作 # [!tip] vector 常用操作\n操作 说明 v.empty() 判断是否为空 v.push_back(val) 向末尾增加一个元素 v.size() 返回元素个数 v.clear() 移除所有元素 v[n] 返回第 n 个元素（从 0 开始） v1 = v2 赋值，用 v2 内容取代 v1 v1 == v2 判断相等 v1 != v2 判断不等 vector\u0026lt;int\u0026gt; ivec; ivec.push_back(1); ivec.push_back(2); cout \u0026lt;\u0026lt; ivec.size() \u0026lt;\u0026lt; endl; // 2 // 范围 for 遍历 for (auto \u0026amp;vecitem : vecvalue) vecitem *= 2; // 元素值翻倍 [!tip] 范围 for 中不要改变 vector 容量\n! 在范围 for 循环中，不要增加或删除 vector 中的元素 增删元素会导致序列结束位置改变，输出混乱 ! 切记：在 for 语句中，不要改变 vector 的容量 13.9 迭代器精彩演绎、失效分析及弥补、实战 # 13.9.1 迭代器简介 # [!tip] 迭代器\n@ 迭代器: 遍历容器内元素的数据类型，类似指针 C++ 中一般采用迭代器来访问容器元素，而非下标 所有容器都支持迭代器操作 13.9.2 容器的迭代器类型 # vector\u0026lt;int\u0026gt; iv{100, 200, 300}; vector\u0026lt;int\u0026gt;::iterator iter; // 定义迭代器 13.9.3 迭代器 begin/end、反向迭代器 rbegin/rend # [!tip] 迭代器操作\nbegin(): 返回指向容器第一个元素的迭代器 end(): 返回指向容器末端元素后面的迭代器（哨兵） 容器为空时，begin() 和 end() 返回的迭代器相同 rbegin(): 反向迭代器，指向最后一个元素 rend(): 反向迭代器，指向第一个元素的前面 // 正向遍历 vector\u0026lt;int\u0026gt; iv{100, 200, 300}; for (vector\u0026lt;int\u0026gt;::iterator iter = iv.begin(); iter != iv.end(); ++iter) cout \u0026lt;\u0026lt; *iter \u0026lt;\u0026lt; endl; // 100 200 300 // 反向遍历 for (vector\u0026lt;int\u0026gt;::reverse_iterator riter = iv.rbegin(); riter != iv.rend(); ++riter) cout \u0026lt;\u0026lt; *riter \u0026lt;\u0026lt; endl; // 300 200 100 13.9.4 迭代器运算符 # 运算符 说明 *iter 返回迭代器指向元素的引用 ++iter 指向下一个元素 --iter 指向上一个元素 iter1 == iter2 判断两个迭代器是否相等 iter-\u0026gt;member 访问结构成员（等价于 (*iter).member） 13.9.5 const_iterator 迭代器 # [!tip] const_iterator\n@ const_iterator: 只读迭代器，只能读元素，不能修改 迭代器本身可以改变（指向下一个元素），但指向的元素值不能改 常量容器必须使用 const_iterator cbegin()/cend()（C++11）: 始终返回 const_iterator const vector\u0026lt;int\u0026gt; iv{100, 200, 300}; vector\u0026lt;int\u0026gt;::const_iterator iter; for (iter = iv.begin(); iter != iv.end(); ++iter) // *iter = 4; // 错误！不能修改 cout \u0026lt;\u0026lt; *iter \u0026lt;\u0026lt; endl; // 可以读 13.9.6 迭代器失效 # [!tip] 迭代器失效\n! 任何能改变 vector 对象容量的操作（如 push_back）都会使当前迭代器失效 在使用迭代器的循环中，千万不要改变 vector 的容量 ! 插入一个元素后应立刻 break，重新获取 begin/end ! 删除元素时，erase 返回下一个元素位置 // 安全的删除方式 while (!iv.empty()) { auto iter = iv.cbegin(); iv.erase(iter); } // 安全的连续插入方式 auto beg = vecvalue.begin(); int icount = 0; while (beg != vecvalue.end()) { beg = vecvalue.insert(beg, icount + 80); // insert 返回结果要保存 icount++; if (icount \u0026gt; 10) break; ++beg; } 13.10 类型转换 # 13.10.1 类型转换种类 # [!tip] C++ 四种类型转换运算符\n运算符 用途 检查时机 static_cast 相关类型转换 编译时 dynamic_cast 父类转子类（运行时类型识别） 运行时 const_cast 去除 const 属性 编译时 reinterpret_cast 无关类型转换（重新解释） 编译时 13.10.2 static_cast 详解 # [!tip] static_cast\n用于相关类型转换（整型↔实型、子类→父类、void * ↔ 其他指针） 不能用于指针类型之间的转换（如 int * → double *） double f = 100.34f; int i2 = static_cast\u0026lt;int\u0026gt;(f); // 100, C++ 风格 // void * 与其他指针转换 int i = 10; int *p = \u0026amp;i; void *q = static_cast\u0026lt;void*\u0026gt;(p); int *db = static_cast\u0026lt;int*\u0026gt;(q); 13.10.3 const_cast 详解 # [!tip] const_cast\n唯一能去掉 const 属性的转换 ! 如果原来是常量，强行去掉 const 并写入是未定义行为 ! 使用 const_cast 总是意味着设计缺陷 const int ai = 90; const int *pai = \u0026amp;ai; int *pai2 = const_cast\u0026lt;int*\u0026gt;(pai); // 去掉 const // *pai2 = 120; // 未定义行为！不要这样做 13.10.4 reinterpret_cast 详解 # [!tip] reinterpret_cast\n用于无关类型转换，重新解释内存内容 ! 非常危险的类型转换，安全性很差 不建议轻易使用 13.10.5 总结 # [!tip] 类型转换总结\n! 强制类型转换一般不建议使用，会干扰系统正常类型检查 写 C++ 程序时不要使用老式 C 风格类型转换，全部用新风格 static_cast 和 reinterpret_cast 能很好地取代 C 风格类型转换 使用 reinterpret_cast 非常危险，使用 const_cast 总是意味着设计缺陷 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p12-c++%E5%9F%BA%E6%9C%AC%E8%AF%AD%E8%A8%80/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e13.1 语言特性、工程构成与可移植性\n    \u003cdiv id=\"131-语言特性工程构成与可移植性\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#131-%e8%af%ad%e8%a8%80%e7%89%b9%e6%80%a7%e5%b7%a5%e7%a8%8b%e6%9e%84%e6%88%90%e4%b8%8e%e5%8f%af%e7%a7%bb%e6%a4%8d%e6%80%a7\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 语言特性图\n\u003cul\u003e\n\u003cli\u003e![[白板/P37 语言特性.canvas|P37 语言特性]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e13.1.1 语言特性：过程式、对象式程序设计\n    \u003cdiv id=\"1311-语言特性过程式对象式程序设计\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1311-%e8%af%ad%e8%a8%80%e7%89%b9%e6%80%a7%e8%bf%87%e7%a8%8b%e5%bc%8f%e5%af%b9%e8%b1%a1%e5%bc%8f%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 面向过程式程序设计\u003c/p\u003e","title":"P12 C++基本语言","type":"notes"},{"content":" 知识点 # 14.1 成员函数、对象复制与私有成员 # @ 类基础图 ![[白板/P41 类基础.canvas|P41 类基础]] 14.1.1 总述 # [!tip] 类的概念\n@ 类: 一种自定义的数据类型，也就是一个新类型 类与类之间不是彼此孤立的，一个类可以派生出子类 设计类要站在多个角度考虑： 1 设计和实现者角度：理顺数据存储布局，定义必要的成员变量和成员函数 2 使用者角度：提供可访问的接口，隐藏内部实现细节 3 父类设计角度：抽取公共特性放到父类中 14.1.2 类基础 # [!tip] 类基础\n@ 类: 一个用户自己定义的数据类型，可以想象成一个名字空间，包着一堆内容（成员函数、成员变量） % 类的构成最常见的是成员变量和成员函数两种 访问成员的方式： 用对象访问：对象名.成员名 用指针访问：指针名-\u0026gt;成员名 public 修饰符：提供类的访问接口，暴露给外界 private 修饰符：提供实现细节，不暴露给外界 struct 是成员默认为 public 的 class class 成员默认是 private 的 struct student { int number; char name[100]; void func() {}; }; class A { public: // ...公共成员 private: // ...私有成员 }; // struct A{...} 等价于 class A{public: ...}; 14.1.3 成员函数 # [!tip] 成员函数\n@ 成员函数: 属于类的函数，与类有关联关系 将函数设计为类的成员函数，两者就有关联关系 成员函数可以在类定义内部直接实现（定义），也可以声明和实现分开 类定义放在 .h 头文件中，成员函数实现放在 .cpp 源文件中 在 .cpp 中使用 类名::函数名 的作用域运算符表示函数属于哪个类 类定义可以在多个 .cpp 文件中用 #include 重复包含，这是被允许的 // Time.h - 类定义 class Time { public: int Hour; int Minute; int Second; void initTime(int tmphour, int tmpmin, int tmpsec); // 声明 }; // Time.cpp - 成员函数实现 void Time::initTime(int tmphour, int tmpmin, int tmpsec) { Hour = tmphour; Minute = tmpmin; Second = tmpsec; } 14.1.4 对象的复制 # [!tip] 对象的复制\n@ 对象复制: 定义一个新对象时，用另外一个老对象里面的内容进行初始化 复制后每个对象都有不同的地址，成员变量的值都相等 可以使用 =、()、{} 等运算符进行对象复制 默认情况下，类对象的复制是每个成员变量逐个复制 可以通过定义\u0026quot;赋值运算符\u0026quot;来控制对象的复制行为 Time myTime2 = myTime; // 对象复制 Time myTime3(myTime); // 对象复制 Time myTime4{myTime}; // 对象复制 Time myTime5 = {myTime}; // 对象复制 Time myTime6; myTime6 = myTime5; // 通过赋值操作来复制对象 14.1.5 私有成员 # [!tip] 私有成员\n@ 私有成员: 类的私有成员变量和私有成员函数都只能在类的成员函数内调用，外界无法直接调用 私有成员的设置目的是不暴露接口，只供类内部的其他成员函数使用 在类定义内部，private 和 public 修饰符修饰其下面的所有成员，直到遇到其他修饰符 class 定义时默认所有成员为 private 一个类的定义中可以出现多个 public、多个 private 14.2 构造函数详解、explicit 与初始化列表 # @ 构造与析构图 ![[白板/P42 构造与析构.canvas|P42 构造与析构]] 14.2.1 称呼上的统一 # [!tip] 成员函数的称呼\n@ 成员函数的定义: 在 class 定义内部将成员函数完整写出（包括所有实现代码） @ 成员函数的声明: 在 class 定义内部只写出函数声明（一般在 .h 文件中） @ 成员函数的实现: 在 class 定义外部写出函数体代码（一般在 .cpp 文件中） 14.2.2 构造函数 # [!tip] 构造函数\n@ 构造函数: 名字与类名相同，在创建类对象时被系统自动调用的特殊成员函数 构造函数的目的（存在的意义）就是==初始化类对象的数据成员== ! 构造函数无返回值，函数头什么也不写（不是 void） ! 不可以手工调用构造函数 构造函数应该声明为 public，否则无法直接创建该类的对象 构造函数如果有参数，创建对象时也要指定相应参数 // 声明 public: Time(int tmphour, int tmpmin, int tmpsec); // 实现 Time::Time(int tmphour, int tmpmin, int tmpsec) { Hour = tmphour; Minute = tmpmin; Second = tmpsec; } // 创建对象 Time myTime(12, 13, 52); // 调用带参构造函数 Time myTime2{12, 13, 52}; // 调用带参构造函数 14.2.3 多个构造函数 # [!tip] 多个构造函数\n一个类中可以同时存在多个构造函数，为对象创建提供多种方法 多个构造函数之间必须在参数数量或参数类型上有所不同 无参构造函数也叫默认构造函数 Time myTime10 = Time(); // 调用无参构造函数 Time myTime12; // 调用无参构造函数，注意写法只有对象名 Time myTime13 = Time{}; // 调用无参构造函数 Time myTime14{}; // 调用无参构造函数 14.2.4 函数默认参数 # [!tip] 函数默认参数\n@ 默认参数: 如果生成对象时不给参数传值，该参数就使用默认值 默认参数一般放在函数声明中（.h 文件中），不放在函数实现中 ! 默认参数都必须出现在非默认参数的右侧，一旦开始指定默认值，右侧所有参数都必须指定 Time(int tmphour, int tmpmin = 59, int tmpsec = 12); // 可以 Time(int tmphour, int tmpmin = 59, int tmpsec); // 不可以 14.2.5 隐式转换和 explicit # [!tip] 隐式转换与 explicit\n@ 隐式转换: 编译系统将一种数据类型自动转换为另一种类型（如将数字转换为类对象） 单参数的构造函数会带来隐式转换问题 @ explicit: 在构造函数声明前加 explicit，禁止隐式转换，只能用于初始化和显式类型转换 ! 建议：单参数的构造函数都声明为 explicit，除非有特别原因 explicit 也可以用于无参或多个参数的构造函数 explicit Time(int tmphour); // 禁止隐式转换 Time myTime100 = Time(16); // 正常，显式转换 Time myTime101 = 16; // 报错，隐式转换被禁止 func(16); // 报错，隐式转换被禁止 func(Time(16)); // 正常，显式转换 14.2.6 构造函数初始化列表 # [!tip] 构造函数初始化列表\n@ 初始化列表: 在构造函数定义中，用括号逗号式写法初始化成员变量，位于函数体之前执行 提倡优先使用初始化列表的原因： 1 写法更专业 2 对于类类型的成员变量，使用初始化列表比赋值语句效率更高（少调用了一次构造函数等） ! 成员变量的初始化顺序依据类定义中的定义顺序（从上到下），而非初始化列表的书写顺序 ! 避免在初始化列表中让某个成员变量的值依赖其他成员变量 // 初始化列表写法 Time::Time(int tmphour, int tmpmin, int tmpsec) : Hour(tmphour), Minute(tmpmin), Second(tmpsec) // 初始化列表 { // 函数体 } // 函数体内赋值写法 Time::Time(int tmphour, int tmpmin, int tmpsec) { Hour = tmphour; // 赋值，不是初始化 Minute = tmpmin; } 14.3 inline、const、mutable、this 与 static # 14.3.1 在类定义中实现成员函数 inline # [!tip] 内联成员函数\n直接在类定义中实现的成员函数会被当作 inline 内联函数来处理 内联函数系统将尝试用函数体内的代码直接取代函数调用代码，以提高运行效率 ! 内联函数只是对编译器的建议，能否 inline 成功取决于编译器 成员函数的定义体尽量写得简单，以增加被 inline 成功的概率 14.3.2 成员函数末尾的 const # [!tip] 常量成员函数\n@ 常量成员函数: 在成员函数末尾增加 const，告诉系统该函数不会修改对象里面的任何成员变量 声明和实现分开时，声明和实现中都要加 const const 对象只能调用 const 成员函数 非 const 对象既可以调用 const 成员函数，也可以调用非 const 成员函数 ! 普通函数（非成员函数）末尾不能加 const，编译都无法通过 void noone() const { Hour += 10; // 错误！常量成员函数不可以修改成员变量 } const Time abc; // 定义 const 对象 abc.addhour(12); // 不可以，非 const 成员函数 abc.noone(); // 可以，const 成员函数 14.3.3 mutable # [!tip] mutable 修饰符\n@ mutable: 与 const 正好是反义词，用于突破 const 的限制 一个成员变量一旦被 mutable 修饰，就表示==永远处于可变状态==，即使是在以 const 结尾的成员函数中 使用场景：在 const 成员函数中确实需要修改某个成员变量，但又不想去掉函数末尾的 const mutable int myHour; // mutable 成员变量 void noone() const { myHour += 3; // 可以修改，因为 myHour 是 mutable 的 } 14.3.4 返回自身对象的引用——this # [!tip] this 指针\n@ this: 用在成员函数中的一个隐藏的函数参数，表示指向本对象的指针 *this 表示调用该成员函数的那个对象（本对象） this 是一个指针常量（不是常量指针），总是指向对象本身，不可以让 this 再指向其他地方 在普通成员函数中，this 是 类名 * const 类型 在 const 成员函数中，this 是 const 类名 * const 类型 ! this 指针只能在成员函数中使用，全局函数、静态函数都不能使用 返回 *this 可以实现函数串联调用 Time\u0026amp; Time::rtnhour(int tmphour) { Hour += tmphour; return *this; // 返回对象自身 } // 串联调用 mytime.rtnhour(3).rtnminute(4); // 先调用 rtnhour，再调用 rtnminute 14.3.5 static 成员 # [!tip] 静态成员\n@ 静态成员变量: 不属于某个对象，而是属于整个类的成员变量 所有该类的对象共享同一个副本 可以用 类名::成员变量名 或 对象名.成员变量名 的方式访问 ! 静态成员变量必须在类外某个 .cpp 源文件中定义（分配内存），定义时无须用 static @ 静态成员函数: 不隶属于某个对象，而是隶属于整个类 调用方式：类名::成员函数名(...) ! 静态成员函数中只能操作 static 成员变量，不能操作普通成员变量 class Time { public: static int mystatic; // 声明静态成员变量 static void stafunc(int testvalue); // 声明静态成员函数 }; int Time::mystatic = 5; // 在 .cpp 中定义静态成员变量 // 使用 cout \u0026lt;\u0026lt; Time::mystatic \u0026lt;\u0026lt; endl; // 用类名访问 mytimel.mystatic = 12; // 用对象名访问 Time::stafunc(1288); // 用类名调用静态成员函数 14.4 delete 与 default、类内初始化、默认构造函数 # 14.4.1 类相关非成员函数 # [!tip] 类相关非成员函数\n有些功能函数和类有关系但不应该定义在类里面 定义可以放在 .cpp 中，声明放在 .h 中类定义的后面 形参用引用类型，避免对象复制产生的效率损失 14.4.2 类内初始值 # [!tip] 类内初始值\nC++11 新标准中，可以为成员变量提供类内初始值 创建对象时，这个初始值用来初始化该成员变量 如果使用构造函数初始化列表或在构造函数中给值，该值会覆盖掉初始值 class Time { public: int Second{0}; // 类内初始值 int Minute = 0; // 类内初始值（两种写法都可以） }; 14.4.3 const 成员变量的初始化 # [!tip] const 成员变量初始化\n! 类的 const 成员变量==只能使用初始化列表来初始化==，不能在构造函数内部赋值 构造函数完成初始化之后，const 成员变量才真正具备 const 属性 ! 构造函数不能声明成 const 的，因为构造函数需要进行写值操作 const int testvalue; // const 成员变量 Time::Time(int tmphour, int tmpmin, int tmpsec) : Hour(tmphour), Minute(tmpmin), testvalue(18) // 必须用初始化列表 { testvalue = 6; // 不可以！ } 14.4.4 默认构造函数 # [!tip] 默认构造函数\n@ 默认构造函数: 没有参数的构造函数 如果一个类中没有构造函数，也能成功生成对象 编译器只有在满足一定情形下才会生成\u0026quot;合成的默认构造函数\u0026quot; ! 一旦程序员自己写了构造函数，编译器就不会生成\u0026quot;合成的默认构造函数\u0026quot; 必须通过初始化列表初始化的情况： 1 const 成员变量 2 类类型成员变量（该类没有默认构造函数） 14.4.5 = default; 和 = delete; # [!tip] = default 和 = delete\n@ = default;: 让编译器为函数自动生成函数体（等价于空函数体），适用于特殊函数（默认构造函数、析构函数等） 放在类声明中则具备 inline 特性，放在类定义外则不具备 @ = delete;: 显式禁用某个函数 用 = delete 可以禁止编译器生成\u0026quot;合成的默认构造函数\u0026quot; class Time2 { public: Time2() = default; // 编译器自动生成默认构造函数 Time2() = delete; // 禁用默认构造函数 }; 14.5 拷贝构造函数 # [!tip] 拷贝构造函数\n@ 拷贝构造函数: 第一个参数是所属类类型引用，若有额外参数则都有默认值的构造函数 拷贝构造函数会在一定时机被系统自动调用 一般第一个参数带 const 修饰 ! 一个类中只能有一个拷贝构造函数 拷贝构造函数一般不声明为 explicit 如果定义了自己的拷贝构造函数，就取代了编译器合成的拷贝构造函数，必须自己给成员逐个赋值 // 声明 Time(const Time\u0026amp; tmptime, int a = 0); // 实现 Time::Time(const Time\u0026amp; tmptime, int a) { Hour = tmptime.Hour; Minute = tmptime.Minute; Second = tmptime.Second; } [!tip] 调用拷贝构造函数的时机\n1 用一个对象初始化另一个对象：Time myTime2 = myTime; 2 将一个对象作为实参传递给一个非引用类型的形参 3 从一个函数中返回一个对象 ! 以下不调用拷贝构造函数：myTime6 = myTime5;（这是赋值操作，调用拷贝赋值运算符） 14.6 重载运算符、拷贝赋值运算符与析构函数 # 14.6.1 重载运算符 # [!tip] 重载运算符\n@ 重载运算符: 以运算符名为成员函数名来写成员函数，实现运算符应用于类对象时的功能 本质上是函数，函数名是 operator 关键字后面接运算符 具有返回类型和参数列表 如果不自己重载 =，某些情况下编译器会帮助重载赋值运算符 ! 如果不自己重载 ==，编译器不会帮助重载，编译一定报错 // 声明 bool operator==(Time\u0026amp; t); // 实现 bool Time::operator==(Time\u0026amp; t) { if (Hour == t.Hour) return true; return false; } 14.6.2 拷贝赋值运算符 # [!tip] 拷贝赋值运算符\n@ 拷贝赋值运算符: 给对象赋值时系统调用的运算符重载函数 左侧对象就是 operator= 里的 this 对象 形参用 const 类型，防止误改源对象 返回值通常是指向左侧运算符对象的引用（return *this;） 如果想禁止对象间赋值，用 private 修饰赋值运算符重载声明 // 声明 Time\u0026amp; operator=(const Time\u0026amp;); // 实现 Time\u0026amp; Time::operator=(const Time\u0026amp; tmpTime) { Hour = tmpTime.Hour; Minute = tmpTime.Minute; Second = tmpTime.Second; return *this; } 14.6.3 析构函数 # [!tip] 析构函数\n@ 析构函数: 与构造函数正好相反，对象销毁时系统调用的成员函数 名字由波浪线 ~ 连接类名构成，没有返回值，不接受任何参数 ! 一个给定的类只有唯一一个析构函数（不能被重载） 如果不写自己的析构函数，编译器可能会生成\u0026quot;默认析构函数\u0026quot; ! 在构造函数中 new 出来的内存，必须在析构函数中 delete 释放，否则内存泄漏 // 声明 ~Time(); // 实现 Time::~Time() { // 释放资源 } 14.6.4 几个话题 # [!tip] 构造函数的成员初始化\n构造函数做的事分两部分：函数体之前（初始化列表）和函数体之中 成员变量初始化顺序按照类中定义的顺序，而非初始化列表的书写顺序 ! 对于类类型成员变量，能放在初始化列表中初始化的，千万不要放在函数体里赋值 [!tip] 析构函数的成员销毁\n析构函数做的事也分两部分：函数体之中和函数体之后 成员变量不是在析构函数体里销毁的，而是函数体执行完成后由系统隐含销毁 成员变量初始化顺序：先定义的先初始化；销毁顺序：先定义的后销毁 ! new 分配的内存必须自己 delete 释放，系统不会帮忙 [!tip] new 对象和 delete 对象\nnew 创建对象时系统调用构造函数 ! 自己 new 出来的对象，自己必须 delete 释放，否则内存泄漏 delete 时系统调用析构函数 ! new 和 delete 必须配对使用 14.7 子类、调用顺序、访问等级与函数遮蔽 # @ 继承与多态图 ![[白板/P43 继承与多态.canvas|P43 继承与多态]] 14.7.1 子类概念 # [!tip] 继承\n@ 继承: 子类从父类那里继承成员变量和成员函数，是面向对象程序设计的核心思想之一 子类比父类有更多的成员变量和成员函数，一般比父类更庞大 定义子类的一般形式：class 子类名 : 继承方式 父类名 class Men : public Human { public: Men(); }; 14.7.2 子类对象定义时调用构造函数的顺序 # [!tip] 构造函数调用顺序\n! 定义子类对象时，先执行父类构造函数的函数体，再执行子类构造函数的函数体 销毁时顺序相反：先执行子类析构函数，再执行父类析构函数 14.7.3 访问等级（public、protected 与 private） # [!tip] 三种访问权限\n@ public: 可以被任意实体所访问 @ protected: 只允许本类或者子类的成员函数来访问 @ private: 只允许本类的成员函数访问 [!tip] 继承方式与访问权限\n父类中的访问权限 子类继承方式 子类得到的访问权限 public public 继承 public protected public 继承 protected private public 继承 子类无权访问 public protected 继承 protected protected protected 继承 protected private protected 继承 子类无权访问 public private 继承 private protected private 继承 private private private 继承 子类无权访问 ! 父类中的 private 成员不受继承方式的影响，子类永远无权访问 public 继承：父类所有成员在子类中的访问权限都不发生改变 protected 继承：将父类中 public 成员变为子类的 protected 成员 private 继承：父类所有成员在子类中的访问权限变为 private 14.7.4 函数遮蔽 # [!tip] 函数遮蔽\n@ 函数遮蔽: 子类会遮蔽父类中的同名函数，不论此函数的返回值、参数 只要子类中有一个和父类同名的成员函数，通过子类对象完全无法调用父类中的同名函数 调用父类同名函数的方法： 1 在子类成员函数中使用 父类名::成员函数名(...) 2 在 main 中使用 子类对象名.父类名::成员函数名(...) 3 使用 using 声明让父类同名函数在子类中可见 // using 声明 public: using Human::samenamefunc; // 让父类的同名函数在子类中可见 // 调用 men.samenamefunc(1); // 执行子类的函数 men.samenamefunc(); // 执行父类的不带参数的函数 14.8 父类指针、虚/纯虚函数、多态性与析构函数 # 14.8.1 父类指针与子类指针 # [!tip] 父类指针与子类指针\n父类指针可以 new 一个子类对象：Human *phuman = new Men; ! 子类指针不能 new 一个父类对象：Men *pmen = new Human;（报错） 父类指针只能调用父类的成员函数，无法调用子类特有的成员函数 14.8.2 虚函数 # [!tip] 虚函数\n@ 虚函数: 在父类成员函数声明前增加 virtual 关键字，使父类指针可以调用子类的同名同参成员函数 virtual 关键字必须加在父类的成员函数声明中 子类中是否加 virtual 没有强制要求，但建议加上 ! 子类的虚函数形参必须和父类的完全一致，否则被认为是不同的函数 @ override: C++11 关键字，用在子类虚函数声明末尾，编译器会检查是否正确覆盖了父类虚函数 @ final: 用在虚函数声明末尾，任何子类中尝试覆盖该函数的操作都将引发错误 // 父类 class Human { public: virtual void eat(); // 虚函数声明 }; // 子类 class Men : public Human { public: virtual void eat() override; // override 检查是否正确覆盖 }; // 使用 Human *phuman = new Men; phuman-\u0026gt;eat(); // 调用 Men 类的 eat 函数（动态绑定） [!tip] 动态绑定\n@ 动态绑定: 程序运行时才能知道调用了哪个子类的虚函数 虚函数必须写定义部分，否则编译出错 如果用普通对象调用虚函数，则不需要动态绑定，编译时就能确定 14.8.3 多态性 # [!tip] 多态性\n@ 多态性: 针对虚函数说的，非虚函数不存在多态 体现在具有继承关系的父类和子类之间，子类重新定义（覆盖/重写）父类的虚函数 通过父类指针，到程序运行时期才能找到动态绑定的对象，从而调用正确的虚函数 系统内部通过查\u0026quot;虚函数表\u0026quot;找到虚函数入口地址 14.8.4 纯虚函数与抽象类 # [!tip] 纯虚函数与抽象类\n@ 纯虚函数: 在父类中声明的虚函数，没有函数体，在函数原型后面加 = 0 @ 抽象类: 含有纯虚函数的类，不能用来生成对象，主要当作父类用来生成子类 ! 子类中必须实现父类的纯虚函数，否则该子类也是抽象类，无法创建对象 抽象类的主要用途：统一管理子类，建立规范 class Human2 { public: virtual void eat() = 0; // 纯虚函数 }; class Human2_sub : public Human2 { public: virtual void eat() { // 子类必须实现纯虚函数 cout \u0026lt;\u0026lt; \u0026#34;Human2_sub::eat()\u0026#34; \u0026lt;\u0026lt; endl; } }; 14.8.5 父类的析构函数一般写成虚函数 # [!tip] 虚析构函数\n! 父类指针 new 子类对象后 delete 时，如果父类析构函数不是虚函数，==只会调用父类的析构函数==，不会调用子类的析构函数，导致内存泄漏 将父类析构函数声明为 virtual 后，delete 父类指针时会先调用子类析构函数，再调用父类析构函数 ! 如果一个类要做父类，务必把析构函数写成 virtual 析构函数 虚函数会增加内存和执行效率上的开销（虚函数表） class Human { public: virtual ~Human(); // 虚析构函数 }; // delete 父类指针时正确调用顺序 Human *phuman = new Men; delete phuman; // 先调用 Men 析构函数，再调用 Human 析构函数 14.9 友元函数、友元类与友元成员函数 # 14.9.1 友元函数 # [!tip] 友元函数\n@ 友元函数: 一个函数，通过声明为某个类的友元，可以访问该类的所有成员（包括 private、protected、public） 友元函数声明不受 public、protected、private 的限制 用 friend 关键字声明 class Men { friend void func(const Men\u0026amp; tmpmen); // 友元函数声明 private: void funcmen2() const; }; 14.9.2 友元类 # [!tip] 友元类\n@ 友元类: 如果类 B 是类 A 的友元类，B 可以在其成员函数中访问类 A 的所有成员 ! 友元关系不能被子类继承 ! 友元关系是单向的（B 是 A 的友元，不代表 A 是 B 的友元） ! 友元关系没有传递性（B 是 A 的友元，C 是 B 的友元，不代表 C 是 A 的友元） class A { friend class B; // 友元类声明 private: int data; }; 14.9.3 友元成员函数 # [!tip] 友元成员函数\n@ 友元成员函数: 只让另一个类中的某些成员函数成为本类的友元函数 比让整个类成为友元类更精细 写法需要注意代码组织结构，需要前向声明 // A.h class A { friend void B::callBAF(int x, A\u0026amp; a); // 声明 B 的成员函数为友元 private: int data; }; // B.h class A; // 前向声明 class B { public: void callBAF(int x, A\u0026amp; a); }; [!tip] 友元的优缺点\n优点：允许特定情况下某些非成员函数访问类的 protected 或 private 成员 缺点：破坏了类的封装性，降低了类的可靠性和可维护性 面向对象三大特性：封装性、继承性、多态性 14.10 RTTI、dynamic_cast、typeid、type-info 与虚函数表 # 14.10.1 RTTI 是什么 # [!tip] RTTI\n@ RTTI（Run Time Type Identification）：运行时类型识别 通过运行时类型识别，程序能够使用父类的指针或引用来检查所指对象的实际子类类型 ! RTTI 的两个运算符要正常工作，父类中至少要有一个虚函数 14.10.2 dynamic_cast 运算符 # [!tip] dynamic_cast\n@ dynamic_cast: 能将父类的指针或者引用安全地转换为子类的指针或者引用 转换成功说明指针实际上是要转换到的那个类型 对于指针类型：转换失败返回 nullptr 对于引用类型：转换失败抛出 std::bad_cast 异常 Human* phuman = new Men; Men* pmen = dynamic_cast\u0026lt;Men*\u0026gt;(phuman); if (pmen != nullptr) { cout \u0026lt;\u0026lt; \u0026#34;phuman 实际指向一个 Men 类型对象\u0026#34; \u0026lt;\u0026lt; endl; } // 引用类型 try { Men\u0026amp; ifmen = dynamic_cast\u0026lt;Men\u0026amp;\u0026gt;(myhuman_y); } catch (bad_cast\u0026amp;) { cout \u0026lt;\u0026lt; \u0026#34;转换失败\u0026#34; \u0026lt;\u0026lt; endl; } 14.10.3 typeid 运算符 # [!tip] typeid 运算符\n@ typeid: 返回一个 type_info 类型的常量对象引用，获取对象的类型信息 两种形式：typeid(类型) 和 typeid(表达式) .name() 成员函数获取类型名字信息 ! 只有当父类含有虚函数时，typeid 才会对表达式求值（返回动态类型） 如果父类没有虚函数，typeid 返回表达式定义时的类型（静态类型） Human* phuman = new Men; cout \u0026lt;\u0026lt; typeid(*phuman).name() \u0026lt;\u0026lt; endl; // class Men（父类有虚函数时） // 比较两个指针是否指向同一种类型 if (typeid(*phuman) == typeid(*phuman2)) { cout \u0026lt;\u0026lt; \u0026#34;指向同一种类型对象\u0026#34; \u0026lt;\u0026lt; endl; } 14.10.4 type_info 类 # [!tip] type_info 类\nname() 成员函数：获取类型名字信息 == 和 != 运算符：比较两个 type_info 对象是否表示同一种类型 14.10.5 RTTI 与虚函数表 # [!tip] 虚函数表\n如果类含有虚函数，编译器会产生一个虚函数表 虚函数表里每个表项是一个指针，指向虚函数的入口地址 有些编译器在虚函数表中存放 type_info 对象信息 14.11 基类与派生类关系的详细再探讨 # 14.11.1 派生类对象模型简介 # [!tip] 派生类对象模型\n派生类对象包含多个子对象： 1 派生类自己定义的成员变量、成员函数的子对象 2 派生类所继承的基类的子对象 基类指针可以 new 派生类对象，因为派生类对象含有基类部分 14.11.2 派生类构造函数 # [!tip] 派生类构造函数\n派生类不能直接初始化从基类继承的成员，而是使用基类的构造函数来初始化基类部分 通过派生类构造函数的初始化列表向基类构造函数传递参数 格式：类名(实参列表) class B : public A { public: B(int i, int j, int k) : A(i), m_valuec(k) {}; // 初始化列表中调用基类构造函数 }; 14.11.3 既当父类又当子类 # [!tip] 继承链\n一个类可以既是某个类的父类，同时又是另外一个类的子类 派生类会包含直接基类的成员以及每个间接基类的成员 14.11.4 不想当基类的类 # [!tip] final 类\nC++11 中 final 关键字加在类名后面，该类不可以作为基类 class AA final {}; // 不可以作为基类 class BB : public AA {}; // 编译报错 14.11.5 静态类型与动态类型 # [!tip] 静态类型与动态类型\n@ 静态类型: 变量声明时的类型，编译时已知 @ 动态类型: 指针或引用所代表的内存中对象的类型，运行时才知道 只有基类指针或引用才存在静态类型和动态类型不一致的情况 14.11.6 派生类向基类的隐式类型转换 # [!tip] 隐式类型转换\n编译器会隐式执行派生类到基类的转换（因为派生类对象包含基类部分） ! 不存在从基类到派生类的自动类型转换 如果基类有虚函数，可以用 dynamic_cast 安全转换 14.11.7 父类、子类之间的复制与赋值 # [!tip] 父子类复制与赋值\n用派生类对象为基类对象初始化或赋值时，只有该派生类对象的==基类部分==会被复制或赋值 派生类部分将被忽略掉 14.12 左值、右值、左值引用、右值引用与 move # 14.12.1 左值和右值 # [!tip] 左值与右值\n@ 左值: 能用在赋值语句等号左侧的内容（代表一个地址） @ 右值: 不能作为左值的值（不能出现在赋值语句等号左侧） 一个左值可能同时具有左值属性和右值属性 左值在等号右侧时用的是值（右值属性），在等号左侧时用的是地址（左值属性） 14.12.2 引用分类 # [!tip] 引用的三种形式\n% 左值引用（绑定到左值）：int\u0026amp;，引用希望改变值的对象 % const 引用（常量引用）：const int\u0026amp;，引用不希望改变值的对象，可以绑定到右值 % 右值引用（绑定到右值）：int\u0026amp;\u0026amp;，C++11 新概念，绑定到即将销毁/临时的对象 14.12.3 左值引用 # [!tip] 左值引用\n引用必须初始化（绑定一个对象） 左值引用不能绑定到右值上 const 引用可以绑定到右值上（比较特殊） int a = 1; int\u0026amp; b = a; // 可以，绑定到左值 int\u0026amp; c = 1; // 不可以，不能绑定到右值 const int\u0026amp; c = 1; // 可以，const 引用可以绑定到右值 14.12.4 右值引用 # [!tip] 右值引用\n@ 右值引用: 必须绑定到右值的引用，用 \u0026amp;\u0026amp; 获得 右值引用主要是用来绑定到那些\u0026quot;即将销毁/临时的对象\u0026quot;上 引入目的是==提高程序运行效率==，把复制对象变成移动对象 ! 所有变量都要看成左值（有地址） ! 任何函数里的形参都是左值 ! 临时对象都是右值 int\u0026amp;\u0026amp; refrightvalue = 3; // 绑定到一个右值 string\u0026amp;\u0026amp; r5{\u0026#34;I love China!\u0026#34;}; // 绑定临时变量 int i = 10; int\u0026amp;\u0026amp; ri5 = i * 100; // 可以，乘法结果是右值 int\u0026amp; ri3 = i * 100; // 不可以，左值引用不能绑右值 14.12.5 std::move 函数 # [!tip] std::move\n@ std::move: 把一个左值强制转换成一个右值，本身并没有做任何移动操作 move 函数名起得比较糟糕，它并没有移动能力 ! string def = std::move(st); 会触发 string 类的移动构造函数，st 变为空 ! string\u0026amp;\u0026amp; def = std::move(st); 不会触发移动构造函数，只是一个绑定动作 调用 move 后，不应再对原对象的值做任何假设 int j = 10; int\u0026amp;\u0026amp; ri20 = std::move(j); // 将左值转换为右值 fff(std::move(j)); // 传递给右值引用形参 string st = \u0026#34;I love China!\u0026#34;; string def = std::move(st); // 触发移动构造函数，st 变为空 14.13 临时对象深入探讨、解析与提高性能手段 # 14.13.1 临时对象的概念 # [!tip] 临时对象\n@ 临时对象: 系统产生的、程序员看不见的对象 临时对象的产生和销毁都有成本，会影响程序执行性能和效率 编写代码的原则：产生的临时对象越少越好 临时对象就是一种右值 14.13.2 产生临时对象的情况和解决方案 # [!tip] 以传值方式给函数传递参数\n以传值方式传递对象参数会调用拷贝构造函数创建副本 解决方案：将形参改为引用类型 [!tip] 类型转换生成的临时对象\n隐式类型转换以保证函数调用成功时，编译器会产生临时对象 sum = 1000; 会先创建临时对象再赋值 解决方案：直接用 CTempValue sum = 1000; 定义时初始化 ! C++ 只会为 const 引用产生临时对象，不会为非 const 引用产生临时对象 [!tip] 函数返回对象的时候\n函数返回一个对象时会产生临时对象 如果有变量接收返回值，临时对象直接构造在该变量预留空间中（编译器优化） 优化方案：return CTempValue(ts.val1 * 2, ts.val2 * 2); 直接返回构造的对象 [!tip] 类外的运算符重载之中的优化\n运算符重载函数中返回局部对象也会产生临时对象 优化方案：return mynum(tmpnum1.num1 + tmpnum2.num1, ...); 直接返回构造的对象 14.14 对象移动、移动构造函数与移动赋值运算符 # 14.14.1 对象移动的概念 # [!tip] 对象移动\n@ 对象移动: 把一个不想用了的对象 A 中的一些有用数据提取出来，在构造新对象 B 时直接使用 移动指的是把一块内存地址中数据的==所有者==从原来的标记为新的，而非搬移数据 移动效率比复制高得多 14.14.2 移动构造函数和移动赋值运算符概念 # [!tip] 移动构造函数\n@ 移动构造函数: 第一个参数是右值引用（\u0026amp;\u0026amp;）的构造函数 与拷贝构造函数的区别：形参是右值引用而非 const 左值引用 移动构造函数中应该完成： 1 让新对象接管源对象的资源 2 斩断源对象与资源的关联（置 nullptr） 3 确保源对象处于可被销毁的状态 ! 不抛出异常的移动构造函数应加上 noexcept A(A\u0026amp;\u0026amp; tmpa) noexcept : m_pb(tmpa.m_pb) { tmpa.m_pb = nullptr; // 斩断源对象与内存的关联 } [!tip] 移动赋值运算符\n@ 移动赋值运算符: 形参为右值引用的赋值运算符重载 同样应加上 noexcept 需要处理自赋值情况 A\u0026amp; operator=(A\u0026amp;\u0026amp; src) noexcept { if (this != \u0026amp;src) { delete m_pb; // 释放自己原来的内存 m_pb = src.m_pb; // 对方的内存直接靠过来 src.m_pb = nullptr; // 斩断源对象与内存的关联 } return *this; } 14.14.3 移动构造函数范例 # [!tip] 移动构造函数调用时机\n系统发现调用移动构造函数更合适时（如生成临时对象的情形），会自动调用 使用 std::move 将左值转为右值后，可以强制调用移动构造函数 ! A\u0026amp;\u0026amp; a2 = std::move(a); 不产生新对象，不调用移动构造函数，只是绑定 14.14.4 移动赋值运算符范例 # [!tip] 移动赋值运算符调用\nA a = getA(); // 移动构造 A a2; // 普通构造 a2 = a; // 拷贝赋值运算符 a2 = std::move(a); // 移动赋值运算符 14.14.5 合成的移动操作 # [!tip] 合成的移动操作\n! 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数（三者之一），编译器就不会为它合成移动构造函数和移动赋值运算符 如果类中没有移动构造函数和移动赋值运算符，系统会调用拷贝构造函数和拷贝赋值运算符代替 只有一个类没定义任何自己版本的拷贝构造函数、拷贝赋值运算符、析构函数，且每个非静态成员都可以移动时，编译器才会合成移动操作 14.14.6 总结 # [!tip] 移动操作总结\n1 在有必要的情况下，应考虑给类添加移动构造函数和移动赋值运算符 2 不抛出异常的移动构造函数、移动赋值运算符都应加上 noexcept 3 程序员有责任使被移动数据的对象处于可被释放的状态 4 如果类中没有移动操作，系统会调用拷贝操作代替 14.15 继承的构造函数、多重继承、类型转换与虚继承 # 14.15.1 继承的构造函数 # [!tip] 继承的构造函数\nC++11 中，派生类能够重用其直接基类定义的构造函数 使用 using 父类名::父类名; 继承父类的构造函数 编译器会把父类的每个构造函数都生成一个与之对应的子类构造函数（函数体为空） ! 一个类只继承其直接基类的构造函数，不能继承间接基类 class B : public A { public: using A::A; // 继承 A 的构造函数 }; 14.15.2 多重继承 # [!tip] 多重继承\n@ 多重继承: 从多个父类产生出子类，子类继承了所有父类的内容 多重继承容易出现二义性问题（同名函数不明确调用哪个） 解决方法：添加作用域明确调用，或在派生类中定义新版本 ! 能用单一继承解决的问题就不要使用多重继承 [!tip] 多重继承中的构造与析构\n构造函数执行顺序：按派生列表中基类的出现顺序 析构函数执行顺序：与构造顺序相反 每个类的构造函数初始化列表负责初始化它的直接基类 14.15.3 类型转换 # [!tip] 多重继承中的类型转换\n基类指针可以指向派生类对象（编译器隐式执行派生类到基类的转换） 多重继承中，每个基类指针都可以指向派生类对象 14.15.4 虚基类与虚继承 # [!tip] 虚继承\n@ 虚基类: 无论在继承体系中出现多少次，派生类中都只会包含唯一一个共享的该类子内容 @ 虚继承: 在继承方式前加 virtual 关键字 ! 所有从虚基类而来的派生类都要虚继承该类，一个都不能少 虚基类的初始化工作由最底层的派生类来做 虚基类子内容会被最先初始化（不管在继承体系中的位置） class A : virtual public Grand {...}; // 虚继承 class A2 : public virtual Grand {...}; // 虚继承（顺序可互换） class C : public A, public A2, public B { public: C(int i, int j, int k) : A(i), A2(j), B(j), Grand(i), m_valuec(k) {}; // 虚基类 Grand 由最底层派生类 C 来初始化 }; 14.16 类型转换构造函数、运算符与类成员指针 # 14.16.1 类型转换构造函数 # [!tip] 类型转换构造函数\n@ 类型转换构造函数: 只有一个形参（非本类 const 引用）的构造函数，可以将其他类型数据转换成该类类型对象 可以用 explicit 禁止隐式类型转换 class TestInt { public: explicit TestInt(int x = 0) : m_i(x) {} // 类型转换构造函数 int m_i; }; TestInt ti = 12; // 隐式类型转换（无 explicit 时） TestInt ti = TestInt(12); // 显式类型转换 14.16.2 类型转换运算符 # [!tip] 类型转换运算符\n@ 类型转换运算符: 将一个类类型对象转成某个其他类型数据的特殊成员函数 一般形式：operator 类型名() const; 没有形参，不能指定返回类型，但会返回对应类型的值 必须定义为类的成员函数 ! 请谨慎使用，建议只在类类型与目标类型之间存在明显关系时才使用 operator int() const { return m_i; // 将本类对象转成 int 类型 } int k = ti2 + 5; // 隐式调用 operator int() int k2 = ti2.operator int() + 5; // 显式调用 [!tip] 显式的类型转换运算符\n加 explicit 关键字后，不能隐式转换 需要使用 static_cast 进行显式转换 explicit operator int() const { return m_i; } int k = static_cast\u0026lt;int\u0026gt;(ti2) + 5; // 显式转换 14.16.3 类型转换的二义性问题 # [!tip] 类型转换二义性\n! 类类型转换建议少用，容易产生二义性问题 如果类中定义了多个类型转换运算符（如 operator int() 和 operator double()），可能出现二义性 如果两个类都有从 int 的类型转换构造函数，重载函数调用时也可能二义性 14.16.4 类成员函数指针 # [!tip] 类成员函数指针\n普通成员函数指针：void (类名::*指针变量名)(参数列表) 获取地址：\u0026amp;类名::成员函数名 ! 必须绑定到类对象才能调用 静态成员函数指针：void (*指针变量名)(参数列表)，不需要绑定对象 void (CT::*myfpointpt)(int) = \u0026amp;CT::ptfunc; // 普通成员函数指针 (ct.*myfpointpt)(100); // 对象调用 (pct-\u0026gt;*myfpointpt)(200); // 指针调用 void (*myfpointstatic)(int) = \u0026amp;CT::staticfunc; // 静态成员函数指针 myfpointstatic(100); // 直接调用 14.16.5 类成员变量指针 # [!tip] 类成员变量指针\n普通成员变量指针：类型 类名::*指针变量名 获取地址：\u0026amp;类名::成员变量名 普通成员变量指针存储的是偏移量，不是真正的内存地址 静态成员变量指针：存储的是真正的内存地址 int CT::*ap = \u0026amp;CT::m_a; // 普通成员变量指针（偏移量） ct.*ap = 189; // 通过指针修改成员变量 int* stcp = \u0026amp;CT::m_stca; // 静态成员变量指针（真正地址） *stcp = 196; ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p13-%E7%B1%BB/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e14.1 成员函数、对象复制与私有成员\n    \u003cdiv id=\"141-成员函数对象复制与私有成员\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#141-%e6%88%90%e5%91%98%e5%87%bd%e6%95%b0%e5%af%b9%e8%b1%a1%e5%a4%8d%e5%88%b6%e4%b8%8e%e7%a7%81%e6%9c%89%e6%88%90%e5%91%98\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 类基础图\n\u003cul\u003e\n\u003cli\u003e![[白板/P41 类基础.canvas|P41 类基础]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e14.1.1 总述\n    \u003cdiv id=\"1411-总述\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1411-%e6%80%bb%e8%bf%b0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 类的概念\u003c/p\u003e","title":"P13 类","type":"notes"},{"content":" 知识点 # 15.1 模板概念与函数模板的定义、调用 # @ 函数模板图 ![[白板/P44 函数模板.canvas|P44 函数模板]] 15.1.1 模板概念 # [!tip] 模板与泛型\n@ 泛型编程: 以独立于任何特定类型的方式编写代码 @ 模板: 泛型编程的基础，是创建类或者函数的蓝图或者公式 模板支持将==类型作为参数==的程序设计方式 模板一般分为函数模板和类模板 通过给模板提供足够的信息，让蓝图转变为具体的类或函数，这种转变发生在==编译时== 15.1.2 函数模板的定义 # [!tip] 函数模板定义\n@ 函数模板: 以 template 关键字开头，后面跟尖括号中的模板参数列表 模板参数用 typename 或 class 修饰（两者等价，与类定义的 class 无关） 多个模板参数用逗号分隔 模板参数列表中的 T 是类型参数，具体代表什么类型由编译器根据调用推断 template \u0026lt;typename T\u0026gt; T funcadd(T a, T b) { T addhe = a + b; return addhe; } 15.1.3 函数模板的调用 # [!tip] 函数模板调用\n调用时编译器根据实参推断模板参数类型 推断成功后，编译器实例化一个特定版本的函数 ! 如果实参类型不一致（如 int 和 float），编译器无法推断，报错 可以用 \u0026lt; \u0026gt; 显式指定模板参数 int he = funcadd(3, 4); // 推断 T 为 int float he2 = funcadd(3.1f, 1.2f); // 推断 T 为 float int he3 = funcadd\u0026lt;int\u0026gt;(3, 4); // 显式指定 15.1.4 非类型模板参数 # [!tip] 非类型模板参数\n@ 非类型模板参数: 表示一个值而非类型，用传统类型名指定（如 int size） ! 非类型模板参数必须是==常量表达式==，值在编译时确定 可以与类型模板参数混合使用 显式指定模板参数时，以 \u0026lt; \u0026gt; 中提供的类型为准 函数模板的 inline 写在模板参数列表之后 template \u0026lt;int a, int b\u0026gt; int funcaddv2() { int addhe = a + b; return addhe; } int result = funcaddv2\u0026lt;12, 13\u0026gt;(); // 通过 \u0026lt;\u0026gt; 传递常量 template \u0026lt;typename T, int a, int b\u0026gt; int funcaddv3(T c) { int addhe = (int)c + a + b; return addhe; } int result = funcaddv3\u0026lt;int, 11, 12\u0026gt;(13); // 混合使用 [!tip] 模板参数分类\n分类 说明 值来源 类型模板参数 typename/class 修饰 编译器推断或用户提供 非类型模板参数 传统类型名修饰 用户提供（常量表达式） 15.2 类模板概念与类模板的定义、使用 # @ 类模板图 ![[白板/P45 类模板.canvas|P45 类模板]] 15.2.1 类模板概念 # [!tip] 类模板概念\n@ 类模板: 产生类的模具，通过给定的模板参数生成具体的类 ! 编译器==不能为类模板推断模板参数==，必须用 \u0026lt; \u0026gt; 提供额外信息 类模板名不是类型名，实例化后的才是（如 myvector\u0026lt;int\u0026gt; 才是类型名） 每种类型编译器都会生成一个不同的类 15.2.2 类模板的定义 # [!tip] 类模板定义\n类模板的所有信息（声明和实现）都必须写在一个 .h 文件中 因为实例化具体类时必须有类模板的全部信息 其他源文件只需 #include 这个 .h 文件即可 // myvector.h template \u0026lt;typename T\u0026gt; class myvector { public: typedef T* iterator; public: myvector(); // 构造函数声明 myvector\u0026amp; operator=(const myvector\u0026amp;); // 赋值运算符 public: iterator mybegin(); iterator myend(); }; 15.2.3 类模板的成员函数 # [!tip] 类模板成员函数\n在类模板定义内部实现的成员函数被隐式声明为内联函数 在类模板定义外部实现时： 1 必须以 template 开头，后接模板参数列表 2 类名后用 \u0026lt; \u0026gt; 列出所有模板参数名 ! 只有被使用到的成员函数才会被实例化 // 类模板外部实现 template \u0026lt;typename T\u0026gt; void myvector\u0026lt;T\u0026gt;::myfunc() { /* ... */ } template \u0026lt;typename T\u0026gt; myvector\u0026lt;T\u0026gt;::myvector() { /* ... */ } 15.2.4 类模板名字的使用 # [!tip] 类模板名字使用规则\n在类模板定义内部，可以直接使用类模板名，不需要跟模板参数 编译器会假定 myvector 等价于 myvector\u0026lt;T\u0026gt; ! 在类模板定义之外，必须带上模板参数 // 类内部 myvector\u0026amp; operator=(const myvector\u0026amp;); // 可以 // 类外部 template \u0026lt;typename T\u0026gt; myvector\u0026lt;T\u0026gt;\u0026amp; myvector\u0026lt;T\u0026gt;::operator=(const myvector\u0026lt;T\u0026gt;\u0026amp;) { return *this; } 15.2.5 非类型模板参数的使用 # [!tip] 类模板非类型模板参数\n非类型模板参数可以给默认值 使用默认值时用空的 \u0026lt; \u0026gt;（不能省略） ! 浮点型和类类型==不能==作为非类型模板参数 template \u0026lt;typename T, int size = 10\u0026gt; class myarray { private: T arr[size]; }; myarray\u0026lt;int, 100\u0026gt; tmparr; // 指定 size=100 myarray\u0026lt;int\u0026gt; tmparr; // 使用默认 size=10 // 类外部实现 template \u0026lt;typename T, int size\u0026gt; void myarray\u0026lt;T, size\u0026gt;::myfunc() { /* ... */ } 15.3 使用 typename 的场合、函数模板、默认模板参数与趣味写法分析 # 15.3.1 typename 的使用场合 # [!tip] typename 两种用法\n% 用法一：在模板定义里，表明其后的模板参数是类型参数 template \u0026lt;typename T\u0026gt; 中的 typename 可替换为 class % 用法二：用 typename 标明这是一个类型（类型成员） ! 在类模板定义之外，访问依赖于模板参数的类型成员时，必须加 typename 默认情况下 C++ 假定通过 :: 访问的是静态成员变量而非类型 ! 此处 typename 不能用 class 替换 // 用法二：标明类型成员 template \u0026lt;typename T\u0026gt; typename myvector\u0026lt;T\u0026gt;::myiterator myvector\u0026lt;T\u0026gt;::mybegin() { /* ... */ } // 另一个范例 template \u0026lt;typename T\u0026gt; typename T::size_type getlength(const T\u0026amp; c) { if (c.empty()) return 0; return c.size(); } 15.3.2 函数指针作为其他函数的参数 # [!tip] 函数指针作参数\n使用 typedef 定义函数指针类型 将函数指针类型作为函数形参类型 typedef int(*FuncType)(int, int); void testfunc(int i, int j, FuncType funcpoint) { int result = funcpoint(i, j); cout \u0026lt;\u0026lt; result \u0026lt;\u0026lt; endl; } testfunc(3, 4, mf); // mf 是函数名，代表函数首地址 15.3.3 函数模板趣味用法举例 # [!tip] 可调用对象与函数模板\n@ 可调用对象: 重载了 () 运算符的类的对象，可以像函数一样调用 同一个函数模板，根据传递参数不同，可推断出不同类型： 函数指针类型 可调用对象类型 ! 可调用对象必须重载 () 运算符，且参数和返回值类型需匹配 template \u0026lt;typename T, typename F\u0026gt; void testfunc(const T\u0026amp; i, const T\u0026amp; j, F funcpoint) { cout \u0026lt;\u0026lt; funcpoint(i, j) \u0026lt;\u0026lt; endl; } // 可调用对象类 class tc { public: int operator()(int v1, int v2) const { return v1 + v2; } }; tc tcobj; testfunc(3, 4, tcobj); // 推断 F 为 tc 类类型 testfunc(3, 4, tc()); // 传递临时对象，节省拷贝构造 15.3.4 默认模板参数 # [!tip] 默认模板参数\n类模板和函数模板都可以有默认模板参数 ! 从有默认值的参数开始，后面的所有参数都得有默认值 类模板使用默认值时用空的 \u0026lt; \u0026gt;（不能省略） C++11 新标准允许为函数模板提供默认模板参数 // 类模板默认参数 template \u0026lt;typename T = string, int size = 5\u0026gt; class myarray { /* ... */ }; myarray\u0026lt;\u0026gt; abc; // 完全使用默认值 myarray\u0026lt;int\u0026gt; def; // 只提供第一个参数 // 函数模板默认参数 template \u0026lt;typename T, typename F = tc\u0026gt; void testfunc(const T\u0026amp; i, const T\u0026amp; j, F funcpoint = F()) { cout \u0026lt;\u0026lt; funcpoint(i, j) \u0026lt;\u0026lt; endl; } testfunc(3, 4); // 使用默认模板参数和默认函数参数 15.4 成员函数模板、模板显式实例化与声明 # 15.4.1 普通类的成员函数模板 # [!tip] 普通类的成员函数模板\n普通类的成员函数本身可以是一个函数模板 ! 成员函数模板==不可以是虚函数== class A { public: template \u0026lt;typename T\u0026gt; void myft(T tmpt) { cout \u0026lt;\u0026lt; tmpt \u0026lt;\u0026lt; endl; } }; A a; a.myft(3); // 自动推断 T 为 int 15.4.2 类模板的成员函数模板 # [!tip] 类模板的成员函数模板\n类模板和其成员函数模板有各自独立的模板参数 在类模板外部实现时，先写类模板参数列表，再写成员函数模板参数列表 构造函数也可以引入自己的模板参数，与类的模板参数无关 template \u0026lt;typename C\u0026gt; class A { public: template \u0026lt;typename T2\u0026gt; A(T2 v1, T2 v2); // 构造函数模板 template \u0026lt;typename T\u0026gt; void myft(T tmpt); }; // 类外部实现 template \u0026lt;typename C\u0026gt; // 类模板参数 template \u0026lt;typename T2\u0026gt; // 构造函数模板参数 A\u0026lt;C\u0026gt;::A(T2 v1, T2 v2) { /* ... */ } 15.4.3 模板显式实例化与声明 # [!tip] 模板显式实例化\n多个 .cpp 文件独立编译时，可能重复实例化相同模板，增加编译开销 @ 实例化定义: template A\u0026lt;float\u0026gt;; — 让编译器实例化，只写在一个 .cpp 中 @ 实例化声明: extern template A\u0026lt;float\u0026gt;; — 告诉编译器其他文件已实例化，不重复生成 函数模板同理：template void myfunc(int\u0026amp;, int\u0026amp;); ! 实例化定义只有一个，实例化声明可以有多个 ! 显式实例化会把类模板所有成员函数都实例化出来 // test.cpp 中（实例化定义） template A\u0026lt;float\u0026gt;; template void myfunc(int\u0026amp;, int\u0026amp;); // MyProject.cpp 中（实例化声明） extern template A\u0026lt;float\u0026gt;; extern template void myfunc(int\u0026amp;, int\u0026amp;); 15.5 using 定义模板别名与显式指定模板参数 # 15.5.1 using 定义模板别名 # [!tip] using 定义模板别名\ntypedef 只能给固定类型起别名，无法定义类型不固定的别名 C++11 的 using 可以定义别名模板（类型相关的模板） using 包含了 typedef 的所有功能，语法顺序相反 别名模板既不是类模板也不是函数模板，是一种新的模板形式 // typedef 定义固定类型别名 typedef std::map\u0026lt;std::string, int\u0026gt; map_s_i; // using 定义固定类型别名（语法顺序相反） using map_s_i = std::map\u0026lt;std::string, int\u0026gt;; // using 定义模板别名（typedef 做不到） template \u0026lt;typename T\u0026gt; using str_map_t = std::map\u0026lt;std::string, T\u0026gt;; str_map_t\u0026lt;int\u0026gt; map1; // 等价于 std::map\u0026lt;std::string, int\u0026gt; str_map_t\u0026lt;string\u0026gt; map2; // 等价于 std::map\u0026lt;std::string, string\u0026gt; // using 定义函数指针类型 using FunType = int(*)(int, int); // 比 typedef 更直观 // using 定义函数指针模板别名 template \u0026lt;typename T\u0026gt; using myfunc_t = int(*)(T, T); 15.5.2 显式指定模板参数 # [!tip] 显式指定模板参数\n显式模板实参从左到右按顺序与模板参数匹配 能推导出来的可以省略，但省略后后面的也得省略 ! 无法推断的模板参数必须显式提供 template \u0026lt;typename T1, typename T2, typename T3\u0026gt; T1 sum(T2 i, T3 j) { T1 result = i + j; return result; } auto result = sum\u0026lt;double, double, double\u0026gt;(2000000000, 2000000000); // 三个都指定 auto result = sum\u0026lt;double\u0026gt;(2000000000, 2000000000); // 只指定 T1 15.6 模板全特化与偏特化（局部特化） # @ 模板特化图 ![[白板/P46 模板特化.canvas|P46 模板特化]] 15.6.1 类模板特化 # [!tip] 泛化与特化\n@ 泛化: 模板可以指定任意模板参数，这就是泛化 @ 特化: 针对某种独特类型做单独设计和代码编写 ! 先有泛化版本才能有特化版本 [!tip] 类模板全特化\n@ 全特化: 所有类型模板参数都用具体类型代替 格式：template \u0026lt;\u0026gt; struct TC\u0026lt;int, int\u0026gt; { /* ... */ }; 特化版本具有==优先被选择权== 可以有任意多个全特化版本 也可以只特化某个成员函数 // 泛化版本 template \u0026lt;typename T, typename U\u0026gt; struct TC { TC() { cout \u0026lt;\u0026lt; \u0026#34;TC泛化版本构造函数\u0026#34; \u0026lt;\u0026lt; endl; } void functest() { cout \u0026lt;\u0026lt; \u0026#34;TC泛化版本\u0026#34; \u0026lt;\u0026lt; endl; } }; // 全特化版本 template \u0026lt;\u0026gt; struct TC\u0026lt;int, int\u0026gt; { TC() { cout \u0026lt;\u0026lt; \u0026#34;TC\u0026lt;int,int\u0026gt;特化版本构造函数\u0026#34; \u0026lt;\u0026lt; endl; } void functest() { cout \u0026lt;\u0026lt; \u0026#34;TC\u0026lt;int,int\u0026gt;特化版本\u0026#34; \u0026lt;\u0026lt; endl; } }; // 特化成员函数 template \u0026lt;\u0026gt; void TC\u0026lt;double, double\u0026gt;::functest() { cout \u0026lt;\u0026lt; \u0026#34;TC\u0026lt;double,double\u0026gt;的functest()特化版本\u0026#34; \u0026lt;\u0026lt; endl; } [!tip] 类模板偏特化（局部特化）\n% 模板参数数量上的偏特化: 绑定部分模板参数，留下部分 % 模板参数范围上的偏特化: 将类型范围缩小（如 T → const T、T*、T\u0026amp;、T\u0026amp;\u0026amp;） 偏特化完了本质上还是一个模板 // 数量上的偏特化 template \u0026lt;typename T, typename U, typename W\u0026gt; struct TCP { /* 泛化 */ }; template \u0026lt;typename U\u0026gt; struct TCP\u0026lt;int, U, double\u0026gt; { /* 偏特化：绑定第1、3个参数 */ }; // 范围上的偏特化 template \u0026lt;typename T\u0026gt; struct TCF { /* 泛化 */ }; template \u0026lt;typename T\u0026gt; struct TCF\u0026lt;const T\u0026gt; { /* const 特化 */ }; template \u0026lt;typename T\u0026gt; struct TCF\u0026lt;T*\u0026gt; { /* 指针特化 */ }; template \u0026lt;typename T\u0026gt; struct TCF\u0026lt;T\u0026amp;\u0026gt; { /* 左值引用特化 */ }; template \u0026lt;typename T\u0026gt; struct TCF\u0026lt;T\u0026amp;\u0026amp;\u0026gt; { /* 右值引用特化 */ }; 15.6.2 函数模板特化 # [!tip] 函数模板全特化\n格式：template \u0026lt;\u0026gt; void tfunc\u0026lt;int, double\u0026gt;(int\u0026amp;, double\u0026amp;) { /* ... */ } 全特化等价于实例化一个函数模板，不等价于函数重载 ! 编译器选择优先级：普通函数 \u0026gt; 函数模板特化版本 \u0026gt; 函数模板泛化版本 ! 函数模板==不能偏特化==，只有类模板才能偏特化 // 泛化版本 template \u0026lt;typename T, typename U\u0026gt; void tfunc(T\u0026amp; tmprv, U\u0026amp; tmprv2) { cout \u0026lt;\u0026lt; \u0026#34;tfunc泛化版本\u0026#34; \u0026lt;\u0026lt; endl; } // 全特化版本 template \u0026lt;\u0026gt; void tfunc\u0026lt;int, double\u0026gt;(int\u0026amp; tmprv, double\u0026amp; tmprv2) { cout \u0026lt;\u0026lt; \u0026#34;tfunc\u0026lt;int,double\u0026gt;特化版本\u0026#34; \u0026lt;\u0026lt; endl; } 15.6.3 模板特化版本放置位置建议 # [!tip] 特化版本放置位置\n模板特化版本应和模板泛化版本放在同一个 .h 文件中 特化版本一般放在泛化版本的后面 15.7 可变参模板与模板模板参数 # @ 可变参模板图 ![[白板/P47 可变参模板.canvas|P47 可变参模板]] 15.7.1 可变参函数模板 # [!tip] 可变参函数模板\n@ 可变参模板: C++11 引入，允许模板定义中含有 0 到多个模板参数 用 ...（省略号）表示可变参 typename... T — 可变参类型（一包类型） T... args — 可变形参（一包形参） sizeof...(args) 或 sizeof...(T) — 获取参数个数 template \u0026lt;typename... T\u0026gt; void myfunct1(T... args) { cout \u0026lt;\u0026lt; sizeof...(args) \u0026lt;\u0026lt; endl; // 参数个数 } myfunct1(); // 0 myfunct1(10, 20); // 2 myfunct1(10, 25.8, \u0026#34;abc\u0026#34;, 68); // 4，类型各不相同 [!tip] 参数包展开\n通过递归函数方式展开参数包 需要两个函数： 1 参数包展开函数（函数模板）：一个单独参数 + 一包参数 2 递归终止函数（普通函数）：0 个参数的同名函数 每次调用剥离一个参数，直到参数包为空时调用终止函数 // 递归终止函数 void myfunct2() { cout \u0026lt;\u0026lt; \u0026#34;参数包展开时执行了递归终止函数\u0026#34; \u0026lt;\u0026lt; endl; } // 参数包展开函数 template \u0026lt;typename T, typename... U\u0026gt; void myfunct2(const T\u0026amp; firstarg, const U\u0026amp;... otherargs) { cout \u0026lt;\u0026lt; \u0026#34;收到的参数值为\u0026#34; \u0026lt;\u0026lt; firstarg \u0026lt;\u0026lt; endl; myfunct2(otherargs...); // 递归调用 } myfunct2(10, \u0026#34;abc\u0026#34;, 12.7); // 输出: 10 → abc → 12.7 → 递归终止函数 15.7.2 可变参类模板 # [!tip] 可变参类模板\n参数包展开方式与函数模板不同 % 方式一：通过递归继承方式展开 % 方式二：通过递归组合方式展开 % 方式三：通过 tuple 和递归调用展开 [!tip] 递归继承方式\n偏特化版本继承参数包剩余部分 每次继承拆分出一个参数 需要一个 0 个模板参数的特化版本作为终止 所有子部分共享同一个 this 地址 // 主模板定义（泛化版本） template \u0026lt;typename... Args\u0026gt; class myclasst { public: myclasst() { printf(\u0026#34;泛化版本\\n\u0026#34;); } }; // 特殊特化版本（0个参数，递归终止） template \u0026lt;\u0026gt; class myclasst\u0026lt;\u0026gt; { public: myclasst() { printf(\u0026#34;特殊特化版本\\n\u0026#34;); } }; // 偏特化版本（递归继承） template \u0026lt;typename First, typename... Others\u0026gt; class myclasst\u0026lt;First, Others...\u0026gt; : private myclasst\u0026lt;Others...\u0026gt; { public: First m_i; myclasst(First parf, Others... paro) : m_i(parf), myclasst\u0026lt;Others...\u0026gt;(paro...) {} }; [!tip] 递归组合方式\n将继承改为成员变量（组合关系） 产生多个不同的对象（this 地址不同） [!tip] tuple 与递归调用方式\n使用 std::tuple 存储参数 通过计数器从 0 递增，用 get\u0026lt;计数器\u0026gt;(tuple) 提取每个参数 需要偏特化版本作为递归终止条件 tuple\u0026lt;float, int, int\u0026gt; mytuple(12.5f, 100, 52); cout \u0026lt;\u0026lt; get\u0026lt;0\u0026gt;(mytuple) \u0026lt;\u0026lt; endl; // 12.5 cout \u0026lt;\u0026lt; get\u0026lt;1\u0026gt;(mytuple) \u0026lt;\u0026lt; endl; // 100 cout \u0026lt;\u0026lt; get\u0026lt;2\u0026gt;(mytuple) \u0026lt;\u0026lt; endl; // 52 15.7.3 模板模板参数 # [!tip] 模板模板参数\n@ 模板模板参数: 模板参数本身又是一个模板 写法：template\u0026lt;class\u0026gt; class Container 也可以写成 template\u0026lt;typename W\u0026gt; typename Container（W 可省略） ! 不能用 typename Container 代替模板模板参数 使用 using 定义模板别名解决容器分配器参数不匹配问题 template \u0026lt;typename T, template\u0026lt;class\u0026gt; class Container\u0026gt; class myclass { public: Container\u0026lt;T\u0026gt; myc; // Container 作为类模板使用 }; // 解决 vector 分配器参数不匹配 template \u0026lt;typename T\u0026gt; using MYVec = vector\u0026lt;T, allocator\u0026lt;T\u0026gt;\u0026gt;; myclass\u0026lt;int, MYVec\u0026gt; myvecobj; // 编译通过 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p14-%E6%A8%A1%E6%9D%BF%E4%B8%8E%E6%B3%9B%E5%9E%8B/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e15.1 模板概念与函数模板的定义、调用\n    \u003cdiv id=\"151-模板概念与函数模板的定义调用\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#151-%e6%a8%a1%e6%9d%bf%e6%a6%82%e5%bf%b5%e4%b8%8e%e5%87%bd%e6%95%b0%e6%a8%a1%e6%9d%bf%e7%9a%84%e5%ae%9a%e4%b9%89%e8%b0%83%e7%94%a8\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 函数模板图\n\u003cul\u003e\n\u003cli\u003e![[白板/P44 函数模板.canvas|P44 函数模板]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e15.1.1 模板概念\n    \u003cdiv id=\"1511-模板概念\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1511-%e6%a8%a1%e6%9d%bf%e6%a6%82%e5%bf%b5\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 模板与泛型\u003c/p\u003e","title":"P14 模板与泛型","type":"notes"},{"content":" 知识点 # 16.1 直接内存管理(new/delete)、创建新工程与观察内存泄漏 # @ new与delete图 ![[白板/P48 new与delete.canvas|P48 new与delete]] 16.1.1 直接内存管理(new/delete) # [!tip] 动态内存分配\n@ 动态分配: 由程序员自己用 new 为对象分配内存（new 返回该对象的指针），用 delete 释放内存 new 出来的对象在==堆==上分配内存，必须自己 delete 释放，否则内存泄漏 局部变量在==栈==上分配，离开作用域自动释放 静态变量（static）在==静态内存==上，程序结束时释放 [!tip] new 初始化方式\n% 直接初始化: int *p = new int(100); — 值为 100 % 值初始化: int *p = new int(); — 值被初始化为 0 % 默认初始化: int *p = new int; — 值未定义（不确定） % 类类型: new 会调用构造函数初始化，值初始化对自定义类无额外意义 % const 对象: const int *pci = new const int(200); // new 与 auto 配合 string *mystr2 = new string(5, \u0026#39;a\u0026#39;); auto mystr3 = new auto(mystr2); // mystr3 被推断为 string** 类型 // vector 初始化 vector\u0026lt;int\u0026gt; *pointv = new vector\u0026lt;int\u0026gt;{1, 2, 3, 4, 5}; [!tip] new/delete 使用注意事项\n! new 和 delete 必须==成对使用==，有 new 必须有 delete ! 同一块内存只能 delete ==一次==，多次 delete 会报异常 ! delete 后的内存==不能再使用==，否则运行异常 ! delete 后建议将指针设为 nullptr，避免悬空指针 ! 两个指针指向同一块内存时，delete 一个后另一个也不能再操作 const 对象可以被 delete 16.1.2 创建新工程与观察内存泄漏 # [!tip] MFC 应用检测内存泄漏\n@ MFC 应用: 微软基础类库框架，程序退出时能报告内存泄漏 在 OnInitDialog 函数中写入测试代码 用 F5 调试运行，退出后在\u0026quot;输出\u0026quot;窗口查看内存泄漏信息 \u0026ldquo;Detected memory leaks!\u0026rdquo; 表示出现内存泄漏，会给出泄漏字节数 16.2 new/delete 探秘、智能指针总述与 shared_ptr 基础 # 16.2.1 new/delete 探秘 # [!tip] new/delete 本质\n@ new/delete: 都是==关键字（运算符）==，不是函数 new 做两件事: ① 分配内存 ② 调用构造函数初始化 delete 做两件事: ① 调用析构函数 ② 释放内存 new 通过调用 operator new() 分配内存 delete 通过调用 operator delete() 释放内存 [!tip] new/delete 与 malloc/free 的区别\n! 最明显区别: new/delete 会==调用构造/析构函数==，malloc/free 不会 new 相比 malloc 不但分配内存，还做初始化 delete 相比 free 不但释放内存，还做清理工作 [!tip] 数组的 new[]/delete[]\n! new[] 和 delete[] 必须==配套使用== new[] 分配类对象数组时，会==多分配 4 字节==保存数组大小，供 delete[] 知道调用几次析构函数 内置类型（如 int）new[] 不会多分配 4 字节 ! 有自定义析构函数的类，用 delete（而非 delete[]）释放数组会==报异常== 无自定义析构函数的类或内置类型，delete 和 delete[] 效果一样（但不推荐混用） // 正确用法 int *p = new int[2]; delete[] p; // 规范写法 A *pA = new A[2](); delete[] pA; // 必须用 delete[]，调用 2 次析构函数 // 错误用法 A *pA = new A[2](); delete pA; // 异常！只调用 1 次析构，且释放地址错乱 16.2.2 智能指针总述 # [!tip] 智能指针概念\n@ 智能指针: 对==裸指针==进行包装，能够==自动释放所指向的对象== 智能指针让程序员不用再担心 delete，程序更健壮 优先选择智能指针，裸指针能做的智能指针都能做 [!tip] 四种智能指针\n% std::shared_ptr: 共享式指针，多个指针指向同一对象，最后一个指针销毁时释放对象 % std::unique_ptr: 独占式指针，同一时刻只有一个指针指向该对象（所有权可移交） % std::weak_ptr: 辅助 shared_ptr 工作，不控制对象生存期 % std::auto_ptr: C++98 已有，已被 unique_ptr 取代，==不要再使用== 16.2.3 shared_ptr 基础 # [!tip] shared_ptr 初始化\n@ shared_ptr: 共享式智能指针，采用==引用计数==管理对象生存期 多个 shared_ptr 指向同一对象，最后一个不需要时才释放 shared_ptr 是 explicit 的，==不能隐式转换== // 常规初始化 shared_ptr\u0026lt;int\u0026gt; pi(new int(100)); // 正确：直接初始化 // shared_ptr\u0026lt;int\u0026gt; pi2 = new int(100); // 错误：不能隐式转换 // make_shared（推荐） shared_ptr\u0026lt;int\u0026gt; p2 = make_shared\u0026lt;int\u0026gt;(100); // 指向值为 100 的 int shared_ptr\u0026lt;string\u0026gt; p3 = make_shared\u0026lt;string\u0026gt;(5,\u0026#39;a\u0026#39;); // 5 个 \u0026#39;a\u0026#39; auto p5 = make_shared\u0026lt;string\u0026gt;(5, \u0026#39;a\u0026#39;); // auto 简写 // 裸指针初始化（不推荐） int *pi = new int; shared_ptr\u0026lt;int\u0026gt; p1(pi); // 不推荐 shared_ptr\u0026lt;int\u0026gt; p1(new int); // 稍好，直接传递 new [!tip] make_shared 优势\n@ make_shared: 标准库函数模板，==最安全和高效==的分配方式 在堆中分配并初始化对象，返回 shared_ptr 编译器只分配==一次内存==（对象+控制块），而 shared_ptr\u0026lt;T\u0026gt;(new T) 至少分配两次 ! 使用 make_shared 则==无法自定义删除器== 16.3 shared_ptr 常用操作、计数与自定义删除器等 # @ shared_ptr图 ![[白板/P49 shared_ptr.canvas|P49 shared_ptr]] 16.3.1 shared_ptr 引用计数的增加和减少 # [!tip] 引用计数增加的情况\n1 用一个 shared_ptr 初始化另一个 shared_ptr 1 将 shared_ptr 作为实参传递给函数（值传递） 1 函数返回 shared_ptr（有变量接收时） [!tip] 引用计数减少的情况\n1 给 shared_ptr 赋新值，指向新对象 1 局部 shared_ptr 离开其作用域 1 当引用计数变为 ==0== 时，自动释放所管理的对象 16.3.2 shared_ptr 指针常用操作 # [!tip] 常用成员函数\n函数 说明 use_count() 返回指向同一对象的智能指针数量（主要用于调试） unique() 是否独占指向的对象（引用计数为 1 返回 true） reset() 不带参：释放对象并置空；带参：指向新对象 get() 返回保存的裸指针（小心使用，不要 delete） swap() 交换两个智能指针所指向的对象 *p 解引用，获得指向的对象 = nullptr 引用计数减 1，若为 0 则释放对象，指针置空 // reset 用法 shared_ptr\u0026lt;int\u0026gt; pi(new int(100)); pi.reset(); // 释放对象，pi 置空 pi.reset(new int(1)); // 指向新内存 // get 用法 shared_ptr\u0026lt;int\u0026gt; myp(new int(100)); int *p = myp.get(); // 获取裸指针 // delete p; // 绝对不可以！ // 智能指针作为判断条件 if (ps1) { /* ps1 指向一个对象 */ } [!tip] 自定义删除器\n[!tip] 删除器与数组\n@ 删除器: 默认用 delete 释放对象，可自定义删除逻辑 shared_ptr 指定删除器：在参数中添加删除器函数名或 lambda ! shared_ptr 管理动态数组时，默认删除器==不支持数组==，需自定义或用 T[] // 自定义删除器（函数） void myDeleter(int *p) { delete p; } shared_ptr\u0026lt;int\u0026gt; p(new int(12345), myDeleter); // 自定义删除器（lambda） shared_ptr\u0026lt;int\u0026gt; p2(new int(12345), [](int *p) { delete p; }); // 管理数组方式1：自定义删除器 shared_ptr\u0026lt;A\u0026gt; pA(new A[10], [](A *p) { delete[] p; }); // 管理数组方式2：尖括号加 []（推荐） shared_ptr\u0026lt;A[]\u0026gt; pA(new A[10]); // 推荐！可直接用 pA[0], pA[1] 等 // 管理数组方式3：default_delete shared_ptr\u0026lt;A\u0026gt; pA(new A[10], std::default_delete\u0026lt;A[]\u0026gt;()); [!tip] 删除器与类型\n! 两个 shared_ptr 删除器不同，只要指向类型相同，就属于==同一类型== 这意味着可以放入同一容器中 make_shared 无法自定义删除器 16.4 weak_ptr 简介、weak_ptr 常用操作与尺寸问题 # @ weak_ptr图 ![[白板/P50 weak_ptr.canvas|P50 weak_ptr]] 16.4.1 weak_ptr 简介 # [!tip] weak_ptr 概念\n@ weak_ptr: 弱引用智能指针，指向 shared_ptr 管理的对象，但==不控制对象生存期== 绑定到 shared_ptr 不会改变引用计数（弱引用不影响强引用计数） weak_ptr 构造和析构不会增加/减少所指向对象的引用计数 不能直接访问对象，必须通过 lock() 获取 shared_ptr auto pi = make_shared\u0026lt;int\u0026gt;(100); weak_ptr\u0026lt;int\u0026gt; piw(pi); // piw 弱共享 pi，强引用计数不变 // *piw; // 错误！不能直接访问 auto pi2 = piw.lock(); // 返回 shared_ptr，强引用计数 +1 if (pi2 != nullptr) cout \u0026lt;\u0026lt; \u0026#34;所指对象存在\u0026#34; \u0026lt;\u0026lt; endl; 16.4.2 weak_ptr 常用操作 # [!tip] weak_ptr 常用成员函数\n函数 说明 use_count() 获取与该弱指针共享对象的 shared_ptr 数量（强引用计数） expired() 是否过期（use_count() 为 0 返回 true） reset() 将弱引用指针置空，弱引用计数减 1 lock() 检查对象是否存在，存在返回 shared_ptr，不存在返回空 auto pi = make_shared\u0026lt;int\u0026gt;(100); weak_ptr\u0026lt;int\u0026gt; piw(pi); int isc = piw.use_count(); // 1 pi.reset(); if (piw.expired()) // true，对象已释放 cout \u0026lt;\u0026lt; \u0026#34;piw 已过期\u0026#34; \u0026lt;\u0026lt; endl; auto p2 = piw.lock(); // 返回空 shared_ptr 16.4.3 尺寸问题 # [!tip] 智能指针尺寸\nshared_ptr 和 weak_ptr 尺寸都是裸指针的 ==2 倍==（x86 下为 8 字节） 包含两个裸指针： 1 指向所管理对象的指针 1 指向==控制块==的指针 控制块包含：强引用计数、弱引用计数、自定义删除器指针等 控制块由第一个指向某对象的 shared_ptr 创建 16.5 shared_ptr 使用场景、陷阱、性能分析与使用建议 # 16.5.1 shared_ptr 使用场景 # shared_ptr\u0026lt;int\u0026gt; createO(int value) { return make_shared\u0026lt;int\u0026gt;(value); // 返回 shared_ptr } void myfunc(int value) { shared_ptr\u0026lt;int\u0026gt; ptmp = createO(10); return; // ptmp 离开作用域，内存自动释放 } 16.5.2 shared_ptr 使用陷阱分析 # [!tip] 陷阱1：慎用裸指针\n! 把裸指针绑定到 shared_ptr 后，==不应再使用裸指针==访问该内存 ! 不要用裸指针初始化==多个== shared_ptr，否则会产生多个独立引用计数，导致重复释放 // 错误：裸指针初始化多个 shared_ptr int *pi = new int; shared_ptr\u0026lt;int\u0026gt; p1(pi); shared_ptr\u0026lt;int\u0026gt; p2(pi); // p1、p2 各自引用计数为 1，释放时重复 delete！ // 正确 shared_ptr\u0026lt;int\u0026gt; p1(new int); // 直接传递 new [!tip] 陷阱2：慎用 get 返回的指针\n! get() 返回的指针==不能 delete== ! 不能用 get() 返回的指针初始化另一个 shared_ptr shared_ptr\u0026lt;int\u0026gt; myp(new int(100)); int *p = myp.get(); // delete p; // 绝对不可以！ // shared_ptr\u0026lt;int\u0026gt; myp2(p); // 绝对不可以！ shared_ptr\u0026lt;int\u0026gt; myp2(myp); // 正确：共享引用 [!tip] 陷阱3：用 enable_shared_from_this 返回 this\n! 在类成员函数中直接用 this 创建 shared_ptr 会导致==独立引用计数== 解决：继承 std::enable_shared_from_this\u0026lt;类名\u0026gt;，使用 shared_from_this() class CT : public std::enable_shared_from_this\u0026lt;CT\u0026gt; { public: shared_ptr\u0026lt;CT\u0026gt; getself() { // return shared_ptr\u0026lt;CT\u0026gt;(this); // 错误！ return shared_from_this(); // 正确 } }; [!tip] 陷阱4：避免循环引用\n! 两个类互相持有对方的 shared_ptr，会导致==循环引用==，引用计数永远不为 0，内存泄漏 解决：将其中一个改为 weak_ptr class CA; class CB; class CA { public: shared_ptr\u0026lt;CB\u0026gt; m_pbs; // 强引用 CB ~CA() { cout \u0026lt;\u0026lt; \u0026#34;~A()\u0026#34; \u0026lt;\u0026lt; endl; } }; class CB { public: weak_ptr\u0026lt;CA\u0026gt; m_pas; // 改为弱引用！ ~CB() { cout \u0026lt;\u0026lt; \u0026#34;~B()\u0026#34; \u0026lt;\u0026lt; endl; } }; 16.5.3 性能说明 # [!tip] shared_ptr 性能\n尺寸为裸指针的 2 倍（8 字节 x86），包含对象指针和控制块指针 控制块创建时机： 1 make_shared 创建时 1 用裸指针创建 shared_ptr 时 指定删除器不影响 shared_ptr 大小 移动语义: 移动不会增加引用计数，复制会增加 shared_ptr\u0026lt;int\u0026gt; p1(new int(100)); shared_ptr\u0026lt;int\u0026gt; p2(std::move(p1)); // 移动构造，p1 置空，p2 引用计数为 1 16.5.4 补充说明和使用建议 # [!tip] 使用建议\n优先使用 make_shared 构造智能指针（更高效，只分配一次内存） 不要 new shared_ptr\u0026lt;T\u0026gt; 不要 memcpy shared_ptr\u0026lt;T\u0026gt; 不确定的用法要慎重分析 16.6 unique_ptr 简介与常用操作 # @ unique_ptr图 ![[白板/P51 unique_ptr.canvas|P51 unique_ptr]] 16.6.1 unique_ptr 简介 # [!tip] unique_ptr 概念\n@ unique_ptr: 独占式智能指针，同一时刻==只能有一个==指针指向该对象 销毁时自动释放所指向的对象 ==优先选择== unique_ptr，不能满足需求时再考虑 shared_ptr // 常规初始化 unique_ptr\u0026lt;int\u0026gt; pi; // 空指针 unique_ptr\u0026lt;int\u0026gt; pi2(new int(105)); // 绑定 new 返回的指针 // make_unique（C++14） unique_ptr\u0026lt;int\u0026gt; p1 = make_unique\u0026lt;int\u0026gt;(100); auto p2 = make_unique\u0026lt;int\u0026gt;(200); 16.6.2 unique_ptr 常用操作 # [!tip] unique_ptr 不支持的操作\n! 不支持==复制==：unique_ptr\u0026lt;T\u0026gt; p2(p1) — 编译错误 ! 不支持==赋值==：p2 = p1 — 编译错误 支持移动：unique_ptr\u0026lt;T\u0026gt; p2(std::move(p1)) [!tip] unique_ptr 常用成员函数\n函数 说明 release() 放弃控制权，返回裸指针，智能指针置空 reset() 不带参：释放对象并置空；带参：指向新对象 get() 返回保存的裸指针（不要 delete） swap() 交换两个智能指针所指向的对象 *p 解引用（数组不支持） = nullptr 释放对象并置空 // release 用法 unique_ptr\u0026lt;string\u0026gt; ps1(new string(\u0026#34;hello\u0026#34;)); unique_ptr\u0026lt;string\u0026gt; ps2(ps1.release()); // ps1 置空，ps2 接管 // 注意：release 后必须处理返回的裸指针 string *tempp = ps2.release(); delete tempp; // 手动释放 // reset 用法 unique_ptr\u0026lt;string\u0026gt; prs(new string(\u0026#34;hello\u0026#34;)); prs.reset(); // 释放并置空 prs.reset(new string(\u0026#34;world\u0026#34;)); // 指向新对象 [!tip] unique_ptr 管理数组\n尖括号中加 [] 即可管理数组，支持 [] 下标访问 ! 有析构函数的类，\u0026lt;\u0026gt; 中必须写 T[]，否则报异常 unique_ptr\u0026lt;int[]\u0026gt; ptrarray(new int[10]); ptrarray[0] = 12; // 支持下标访问 unique_ptr\u0026lt;A[]\u0026gt; ptrarray(new A[10]); // 正确 // unique_ptr\u0026lt;A\u0026gt; ptrarray(new A[10]); // 异常！ [!tip] 转换为 shared_ptr\nunique_ptr 可以转换为 shared_ptr（通过移动语义） 转换后 unique_ptr 置空，shared_ptr 引用计数为 1 系统会为 shared_ptr 创建控制块 unique_ptr\u0026lt;string\u0026gt; ps(new string(\u0026#34;hello\u0026#34;)); shared_ptr\u0026lt;string\u0026gt; ps2(std::move(ps)); // ps 置空，ps2 引用计数为 1 16.7 返回 unique_ptr、删除器与尺寸问题 # 16.7.1 返回 unique_ptr # [!tip] 函数返回 unique_ptr\nunique_ptr 不支持复制，但==从函数返回==是例外 返回局部 unique_ptr 时，编译器会调用移动构造函数 unique_ptr\u0026lt;string\u0026gt; tuniqp() { return unique_ptr\u0026lt;string\u0026gt;(new string(\u0026#34;hello\u0026#34;)); // 或者 // unique_ptr\u0026lt;string\u0026gt; pr(new string(\u0026#34;hello\u0026#34;)); // return pr; // 也可以 } unique_ptr\u0026lt;string\u0026gt; ps; ps = tuniqp(); // 可以接收 16.7.2 删除器 # [!tip] unique_ptr 删除器\n删除器类型写在尖括号中：unique_ptr\u0026lt;对象类型, 删除器类型\u0026gt; ! 删除器类型是 unique_ptr ==类型的一部分==，不同于 shared_ptr 删除器不同则 unique_ptr 类型不同，不能放入同一容器 // 函数指针类型作为删除器 typedef void(*fp)(string*); unique_ptr\u0026lt;string, fp\u0026gt; ps1(new string(\u0026#34;hello\u0026#34;), mydeleter); // using 定义函数指针类型 using fp2 = void(*)(string*); unique_ptr\u0026lt;string, fp2\u0026gt; ps2(new string(\u0026#34;hello\u0026#34;), mydeleter); // decltype 推导 auto mydella = [](string *pdel) { delete pdel; }; unique_ptr\u0026lt;string, decltype(mydella)\u0026gt; ps5(new string(\u0026#34;hello\u0026#34;), mydella); 16.7.3 尺寸问题 # [!tip] unique_ptr 尺寸\n通常情况下，unique_ptr 尺寸与==裸指针相同==（x86 下 4 字节） 如果删除器是 lambda 表达式：尺寸==不变== 如果删除器是函数指针：尺寸==变大==（x86 下 8 字节） 与 shared_ptr 不同：shared_ptr 不管什么删除器，大小始终是裸指针 2 倍 16.8 智能指针总结 # [!tip] 智能指针设计思想\n核心目的：==自动释放内存==，防止内存泄漏 智能指针在对象离开作用域时自动调用析构函数释放内存 [!tip] auto_ptr 为何被废弃\nauto_ptr 复制时会默默转移所有权，原指针变空，后续使用会崩溃 unique_ptr 复制时直接编译报错，更安全 unique_ptr 通过移动语义显式转移所有权 [!tip] 智能指针选择\n优先考虑 unique_ptr（独占式，开销小） 需要多个指针指向同一对象时选择 shared_ptr weak_ptr 辅助 shared_ptr 解决循环引用问题 不要再使用 auto_ptr ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p15-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e16.1 直接内存管理(new/delete)、创建新工程与观察内存泄漏\n    \u003cdiv id=\"161-直接内存管理newdelete创建新工程与观察内存泄漏\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#161-%e7%9b%b4%e6%8e%a5%e5%86%85%e5%ad%98%e7%ae%a1%e7%90%86newdelete%e5%88%9b%e5%bb%ba%e6%96%b0%e5%b7%a5%e7%a8%8b%e4%b8%8e%e8%a7%82%e5%af%9f%e5%86%85%e5%ad%98%e6%b3%84%e6%bc%8f\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ new与delete图\n\u003cul\u003e\n\u003cli\u003e![[白板/P48 new与delete.canvas|P48 new与delete]]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e16.1.1 直接内存管理(new/delete)\n    \u003cdiv id=\"1611-直接内存管理newdelete\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1611-%e7%9b%b4%e6%8e%a5%e5%86%85%e5%ad%98%e7%ae%a1%e7%90%86newdelete\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 动态内存分配\u003c/p\u003e","title":"P15 智能指针","type":"notes"},{"content":" 知识点 # 17.1 基本概念和实现 # @ 线程基础图 ![[白板/P52 线程基础.canvas|线程基础]] 17.1.1 并发、进程、线程的基本概念和综述 # [!tip] 并发\n并发: ==两个或更多任务（独立的活动）同时发生（进行）==\n一个程序同时执行多个独立的任务 单核 CPU 通过任务切换实现并发假象（上下文切换） 多核 CPU 可真正实现并行执行（硬件并发） 使用并发的主要原因：提高整体运行性能 [!tip] 可执行程序\n可执行程序: ==磁盘上的一个文件（程序）==\nWindows 下扩展名为 .exe Linux 下有可执行权限（-rwxrw-r-- 中的 x） [!tip] 进程\n进程: ==运行起来了的可执行程序==\n运行一个可执行程序就创建了一个进程 多次运行可创建多个进程 [!tip] 线程\n线程: ==一条代码的执行通路（道路）==\n每个进程都有一个唯一的主线程，随进程自动启动 主线程从 main 函数开始执行，到 return 0; 结束 可通过代码创建其他线程（子线程），代表新的执行通路 线程不是越多越好：每个线程需独立堆栈空间（约 1MB），上下文切换也有开销 创建线程数量一般不建议超过 200-300 个 ! 主线程执行完毕 = 整个进程结束，子线程会被操作系统强制终止 ! 想保持子线程运行，必须让主线程保持运行 17.1.2 并发的实现方法 # [!tip] 并发实现方式\n多进程并发: ==通过多个进程实现并发，每个进程做一件事==\n进程间通信：管道、文件、消息队列、共享内存（同机）；socket（不同机） 进程间通信较复杂（数据保护问题） 多线程并发: ==在单个进程中创建多个线程实现并发==\n线程共享地址空间（共享内存）、全局变量、指针、引用 开销远小于多进程 带来数据一致性问题 % 多线程并发优点：启动速度更快、更轻量级、系统资源开销更少、执行速度更快 % 多线程并发缺点：使用难度大，需小心处理数据一致性问题 17.1.3 C++11 新标准线程库 # [!tip] C++11 线程库\nC++11 线程库: ==C++语言本身增加的多线程支持，与具体操作系统平台无关==\n极大增加程序可移植性（跨平台） Windows 下开发的代码可直接在 Linux 上编译 以往需用 CreateThread（Windows）或 pthread_create（Linux），不可跨平台 17.2 线程启动、结束与创建线程写法 # @ 线程创建图 ![[白板/P53 线程创建.canvas|线程创建]] 17.2.1 范例演示线程运行的开始和结束 # [!tip] std::thread\nstd::thread: ==C++标准库中用来创建线程的类==\n构造函数接受一个可调用对象作为参数 代码一执行，新线程创建并立即开始执行初始函数 #include \u0026lt;thread\u0026gt; void myprint() { cout \u0026lt;\u0026lt; \u0026#34;我的线程开始执行了\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;我的线程执行完毕了\u0026#34; \u0026lt;\u0026lt; endl; } int main() { std::thread mytobj(myprint); // 创建线程 mytobj.join(); // 等待线程执行完毕 cout \u0026lt;\u0026lt; \u0026#34;main主函数执行结束!\u0026#34; \u0026lt;\u0026lt; endl; return 0; } [!tip] join\njoin(): ==阻塞主线程，等待子线程执行完毕==\n主线程阻塞在 join 行，直到子线程执行完毕 书写良好的程序应确保主线程等待所有子线程执行完毕后再退出 [!tip] detach\ndetach(): ==分离主线程与子线程，主线程不必等待子线程==\n主线程与子线程失去关联，子线程驻留后台运行 子线程被 C++ 运行时库接管，执行完后由运行时库清理资源 分离后的线程称为守护线程 一旦调用 detach，不可再调用 join，否则程序异常 ! detach 会导致程序员失去对线程的控制，多数实际项目中 join 更常用 [!tip] joinable\njoinable(): ==判断是否可以成功使用 join 或 detach==\n创建线程后、未调用 join/detach 前，返回 true 调用 join 或 detach 后，返回 false if (mytobj.joinable()) { mytobj.join(); // 安全调用 } 17.2.2 其他创建线程的写法 # [!tip] 用类创建线程\n类对象创建线程: ==通过重载 operator() 的类对象作为可调用对象==\nclass TA { public: void operator()() { cout \u0026lt;\u0026lt; \u0026#34;TA::operator()开始执行了\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;TA::operator()执行结束了\u0026#34; \u0026lt;\u0026lt; endl; } }; TA ta; std::thread mytobj3(ta); // ta 不能是临时对象 mytobj3.join(); ! 类与 detach 结合使用时，类中不应包含引用或指针成员变量（主线程退出后引用/指针失效） ! 类对象会被复制到子线程中（拷贝构造函数执行），但引用/指针成员仍指向主线程内存 [!tip] 用 lambda 表达式创建线程\nauto mylambdathread = [] { cout \u0026lt;\u0026lt; \u0026#34;我的线程开始执行了\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;我的线程执行完毕了\u0026#34; \u0026lt;\u0026lt; endl; }; std::thread mytobj4(mylambdathread); mytobj4.join(); 17.3 线程传参详解、detach 坑与成员函数作为线程函数 # 17.3.1 传递临时对象作为线程参数 # [!tip] 线程传参要点\n线程传参: ==传递简单类型参数建议值传递，传递类对象建议构建临时对象==\n! 陷阱 1：detach 时不要传递引用、指针（主线程退出后内存不安全） ! 陷阱 2：隐式类型转换可能在子线程中进行（主线程退出后才构造对象，不安全） [!tip] 临时对象解决方案\n安全传参: ==在创建线程时构造临时对象，确保形参在主线程退出前构造完毕==\n临时对象在主线程中构造（通过线程 id 可验证） 线程入口函数的类类型形参应使用引用（避免多构造一次临时对象） // 安全写法：显式构造临时对象 std::thread mytobj(myprint, mvar, string(mybuf)); // 不安全写法：依赖隐式转换 std::thread mytobj(myprint, mvar, mybuf); // mybuf 可能在线程中才转换 ! 总结：传递 int 等简单类型用值传递；传递类对象避免隐式转换，在创建线程行构建临时对象；建议不使用 detach，只使用 join 17.3.2 临时对象作为线程参数继续讲 # [!tip] 线程 id\n线程 id: ==每个线程对应的唯一数字标识==\n通过 std::this_thread::get_id() 获取 不同线程的 id 必然不同 [!tip] 临时对象构造时机\n关键结论: ==给线程入口函数传递类类型对象时，使用临时对象可确保形参在主线程退出前创建完毕==\n不构造临时对象：对象可能在子线程中构造（不安全） 构造临时对象：对象在主线程中构造（安全） 形参使用引用可避免多一次拷贝构造 17.3.3 传递类对象与智能指针作为线程参数 # [!tip] std::ref\nstd::ref: ==明确告诉编译器传递一个能影响原始参数的引用==\n使用 std::ref 后不涉及产生临时对象 线程入口函数形参可去掉 const 修饰 修改形参会影响到实参 A myobj(10); std::thread mytobj(myprint2, std::ref(myobj)); // 真正传递引用 [!tip] 智能指针传参\nunique_ptr 传参: ==使用 std::move 将 unique_ptr 转移到线程==\nunique_ptr\u0026lt;int\u0026gt; myp(new int(100)); std::thread mytobj(myprint3, std::move(myp)); // 所有权转移 mytobj.join(); // 必须用 join，不能用 detach 17.3.4 用成员函数作为线程入口函数 # class A { public: void thread_work(int num) { cout \u0026lt;\u0026lt; \u0026#34;子线程thread_work执行\u0026#34; \u0026lt;\u0026lt; endl; } }; A myobj(10); // 成员函数指针 + 对象 + 参数 std::thread mytobj(\u0026amp;A::thread_work, myobj, 15); mytobj.join(); // 使用 std::ref 避免拷贝 std::thread mytobj(\u0026amp;A::thread_work, std::ref(myobj), 15); // 或使用对象地址 std::thread mytobj(\u0026amp;A::thread_work, \u0026amp;myobj, 15); 17.4 创建多个线程、数据共享问题分析与案例代码 # 17.4.1 创建和等待多个线程 # vector\u0026lt;std::thread\u0026gt; mythreads; for (int i = 0; i \u0026lt; 5; i++) { mythreads.push_back(std::thread(myprint, i)); // 创建并开始执行 } for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter) { iter-\u0026gt;join(); // 等待所有线程返回 } ! 多个线程之间的执行顺序是乱的（与操作系统调度机制有关） $ 将 thread 对象放到容器中管理，方便一次性创建和管理大量线程 17.4.2 数据共享问题分析 # % 只读数据：多个线程同时读，安全 % 有读有写：必须小心处理，否则数据不一致 ! 写操作是多步骤的，必须保证原子性（中间不能被截断） 17.4.3 共享数据的保护实战范例 # [!tip] 共享数据保护\n核心思路: ==当某线程操作共享数据时，用锁锁住，其他线程必须等待==\n互斥量（mutex）是最通用的保护共享数据的机制 17.5 互斥量的概念、用法、死锁演示与解决详解 # @ 互斥量与死锁图 ![[白板/P54 互斥量与死锁.canvas|互斥量与死锁]] 17.5.1 互斥量的基本概念 # [!tip] 互斥量\n互斥量（mutex）: ==一把锁，同一时间只有一个线程可以加锁成功==\n多个线程调用 lock 尝试加锁，只有一个成功 未加锁成功的线程卡在 lock 行不断尝试 加锁成功线程执行完共享数据操作后必须 unlock 保护范围不要多也不要少 17.5.2 互斥量的用法 # [!tip] lock 与 unlock\nlock()/unlock(): ==成对使用，有 lock 必然要有 unlock==\n调用 1 次 lock 必须调用 1 次 unlock 多个出口（return）时每个出口都要 unlock my_mutex.lock(); // 操作共享数据 my_mutex.unlock(); [!tip] std::lock_guard\nstd::lock_guard: ==类模板，自动管理 mutex 的 lock/unlock==\n构造函数中调用 mutex 的 lock 析构函数中调用 mutex 的 unlock 超出作用域自动解锁，防止忘记 unlock 不如单独使用 mutex 灵活（无法随时 unlock） { std::lock_guard\u0026lt;std::mutex\u0026gt; sbguard(my_mutex); // 操作共享数据 } // 离开作用域，自动 unlock 17.5.3 死锁 # [!tip] 死锁\n死锁: ==至少两个互斥量才能产生，线程互相等待对方解锁==\n线程 A 持有锁 1 等待锁 2，线程 B 持有锁 2 等待锁 1 两个线程都卡住，程序无法继续执行 [!tip] 死锁的一般解决方案\n解决死锁: ==确保多个互斥量上锁的先后顺序相同==\n两个线程对锁的 lock 顺序必须一致 unlock 顺序建议谁后 lock 谁先 unlock [!tip] std::lock 函数模板\nstd::lock: ==一次锁住两个或两个以上的互斥量，不存在死锁风险==\n要么全部锁住，要么全部没锁住 锁住一个后若第二个失败，会释放第一个重新尝试 std::lock(my_mutex1, my_mutex2); // 同时锁住两个 // 操作共享数据 my_mutex2.unlock(); my_mutex1.unlock(); [!tip] std::adopt_lock\nstd::adopt_lock: ==标记互斥量已经被 lock 过了，lock_guard 不需要再 lock==\n配合 std::lock 使用，lock_guard 只负责 unlock std::lock(my_mutex1, my_mutex2); std::lock_guard\u0026lt;std::mutex\u0026gt; sbguard1(my_mutex1, std::adopt_lock); std::lock_guard\u0026lt;std::mutex\u0026gt; sbguard2(my_mutex2, std::adopt_lock); // 操作共享数据，离开作用域自动 unlock 17.6 unique_lock 详解 # 17.6.1 unique_lock 取代 lock_guard # [!tip] unique_lock\nstd::unique_lock: ==比 lock_guard 更灵活的互斥量管理类模板==\n可完全取代 lock_guard 更灵活但效率略差、内存占用稍多 构造函数中 lock，析构函数中 unlock std::unique_lock\u0026lt;std::mutex\u0026gt; sbguard1(my_mutex); // 替代 lock_guard 17.6.2 unique_lock 的第二个参数 # [!tip] std::adopt_lock\nstd::adopt_lock: ==表示互斥量已经被 lock 过了，不需要 unique_lock 构造函数再 lock==\n前提：开发者需要先把互斥量 lock 上 my_mutex.lock(); std::unique_lock\u0026lt;std::mutex\u0026gt; sbguard1(my_mutex, std::adopt_lock); [!tip] std::try_to_lock\nstd::try_to_lock: ==尝试 lock，失败也立即返回，不会阻塞==\n前提：不能自己先 lock 互斥量 通过 owns_lock() 判断是否拿到锁 std::unique_lock\u0026lt;std::mutex\u0026gt; sbguard1(my_mutex, std::try_to_lock); if (sbguard1.owns_lock()) { // 拿到锁，操作共享数据 } else { // 没拿到锁，做别的事 } [!tip] std::defer_lock\nstd::defer_lock: ==初始化 unique_lock 但不加锁，后续手动 lock==\n前提：不能自己先 lock 互斥量 提供灵活性，可调用 unique_lock 的成员函数 std::unique_lock\u0026lt;std::mutex\u0026gt; sbguard1(my_mutex, std::defer_lock); sbguard1.lock(); // 手动加锁 17.6.3 unique_lock 的成员函数 # [!tip] unique_lock 成员函数\nlock(): ==给互斥量加锁，无法加锁则阻塞等待==\nunlock(): ==给互斥量解锁，可随时调用==\n体现了 unique_lock 比 lock_guard 灵活的地方 try_lock(): ==尝试加锁，成功返回 true，失败返回 false，不阻塞==\nrelease(): ==返回所管理的 mutex 对象指针，并释放所有权==\n不同于 unlock（只解锁不解除关联） 调用 release 后，程序员有责任负责解锁 ! 锁的粒度：锁住的代码越少（粒度越细），效率越高；锁住的代码越多（粒度越粗），效率越低 ! 选择合适的粒度是高级程序员能力的体现 17.6.4 unique_lock 所有权的传递 # [!tip] unique_lock 所有权\n所有权: ==unique_lock 与 mutex 的绑定关系，可移动不可复制==\n类似 unique_ptr 的所有权传递 通过 std::move 转移所有权 std::unique_lock\u0026lt;std::mutex\u0026gt; sbguard1(my_mutex); std::unique_lock\u0026lt;std::mutex\u0026gt; sbguard10(std::move(sbguard1)); // 移动所有权 $ 返回 unique_lock 类型也是一种用法（返回局部对象会触发移动构造） 17.7 单例设计模式共享数据分析、解决与 call_once # 17.7.1 设计模式简单谈 # [!tip] 设计模式\n设计模式: ==开发程序的一些代码写法，使程序更灵活==\n优点：增加或减少功能不会牵一发动全身 缺点：代码晦涩，管理复杂 应活学活用，不要生搬硬套 17.7.2 单例设计模式 # [!tip] 单例模式\n单例模式: ==某个特殊的类只能创建一个对象==\n构造函数用 private 修饰 通过静态成员函数 GetInstance() 获取唯一对象 使用内部类 CGarhuishou 自动释放单例对象内存 class MyCAS { private: MyCAS() {} // 私有构造函数 static MyCAS* m_instance; public: static MyCAS* GetInstance() { if (m_instance == NULL) { m_instance = new MyCAS(); static CGarhuishou cl; // 程序退出时自动释放 } return m_instance; } class CGarhuishou { // 内部类，用于释放对象 public: ~CGarhuishou() { if (MyCAS::m_instance) { delete MyCAS::m_instance; MyCAS::m_instance = NULL; } } }; }; MyCAS* MyCAS::m_instance = NULL; // 静态成员初始化 17.7.3 单例设计模式共享数据问题分析、解决 # [!tip] 双重检查锁定\n双重检查锁定: ==外层 if 提高效率，内层 if 加锁后确保安全==\nstatic MyCAS* GetInstance() { if (m_instance == NULL) { // 第一次检查（提高效率） std::unique_lock\u0026lt;std::mutex\u0026gt; mymutex(resource_mutex); if (m_instance == NULL) { // 第二次检查（确保安全） m_instance = new MyCAS(); static CGarhuishou cl; } } return m_instance; } ! 外层 if 成立不代表 m_instance 一定没被 new 过（可能另一个线程正在 new） 17.7.4 std::call_once # [!tip] std::call_once\nstd::call_once: ==保证函数只被调用一次（即使多线程环境下）==\n配合 std::once_flag 标记使用 效率比互斥量消耗资源更少 可替代双重检查锁定解决单例初始化问题 std::once_flag g_flag; static void CreateInstance() { m_instance = new MyCAS(); static CGarhuishou cl; } static MyCAS* GetInstance() { if (m_instance == NULL) { // 提高效率 std::call_once(g_flag, CreateInstance); } return m_instance; } \u0026amp; 强烈建议在主线程中、子线程创建之前完成单例类对象的初始化 17.8 condition_variable、wait、notify_one 与 notify_all # @ 条件变量图 ![[白板/P55 条件变量.canvas|条件变量]] 17.8.1 条件变量 std::condition_variable、wait 与 notify_one # [!tip] 条件变量\nstd::condition_variable: ==用于等待一个条件达成，需与互斥量配合工作==\n线程 A 等待条件满足（如消息队列有数据） 线程 B 使条件满足后通知线程 A [!tip] wait\nwait(): ==阻塞等待条件满足==\n有第二个参数（lambda 表达式）：返回 true 则继续，返回 false 则解锁互斥量并阻塞 无第二个参数：等同于 lambda 返回 false 被唤醒后重新获取互斥量并加锁，再判断 lambda 表达式 [!tip] notify_one\nnotify_one(): ==唤醒一个等待在 wait 的线程==\n唤醒哪个线程不确定 必须把互斥量解锁，另一个线程的 wait 才能继续正常工作 // 等待线程 std::unique_lock\u0026lt;std::mutex\u0026gt; sbguardl(my_mutex); my_cond.wait(sbguardl, [this] { if (!msgRecvQueue.empty()) return true; return false; }); // 流程走下来意味着队列必然有数据 // 通知线程 std::unique_lock\u0026lt;std::mutex\u0026gt; sbguardl(my_mutex); msgRecvQueue.push_back(i); my_cond.notify_one(); // 唤醒等待线程 17.8.2 上述代码深入思考 # ! wait 被唤醒后，队列中可能有多条数据 ! notify_one 唤醒时，如果线程不在 wait 行，则 notify_one 无效 ! 必须透彻理解多线程函数的工作流程后再使用 17.8.3 notify_all # [!tip] notify_all\nnotify_all(): ==通知所有处于 wait 状态的线程==\n适用于多个不同线程做不同事情但等待同一条件变量的场景 被唤醒的线程仍需竞争获取锁 17.9 async、future、packaged_task 与 promise # 17.9.1 std::async 和 std::future 创建后台任务并返回值 # [!tip] std::async\nstd::async: ==启动一个异步任务，返回 std::future 对象==\n可能创建新线程，也可能不创建（取决于参数） future 对象中包含线程入口函数的返回结果 通过 get() 获取结果（阻塞等待） std::future\u0026lt;int\u0026gt; result = std::async(mythread); cout \u0026lt;\u0026lt; result.get() \u0026lt;\u0026lt; endl; // 阻塞等待，只能 get 一次 [!tip] std::async 额外参数\nstd::launch::deferred: ==延迟执行，调用 wait/get 时才执行（不创建新线程）==\nstd::launch::async: ==强制创建新线程立即执行==\nstd::launch::async \\| std::launch::deferred: ==系统自行决定是否创建新线程==（默认行为）\n[!tip] std::async 与 std::thread 的区别\n特性 std::thread std::async 创建线程 直接创建 可能创建也可能不创建 返回值 不易获取 通过 future 获取 资源紧张 创建失败报异常 不报异常，改为同步执行 17.9.2 std::packaged_task # [!tip] std::packaged_task\nstd::packaged_task: ==把可调用对象包装起来，方便作为线程入口函数调用==\n模板参数是可调用对象的类型签名 通过 get_future() 获取 future 对象 本身也是可调用对象，可直接调用 std::packaged_task\u0026lt;int(int)\u0026gt; mypt(mythread); std::thread t1(std::ref(mypt), 1); t1.join(); std::future\u0026lt;int\u0026gt; result = mypt.get_future(); cout \u0026lt;\u0026lt; result.get() \u0026lt;\u0026lt; endl; 17.9.3 std::promise # [!tip] std::promise\nstd::promise: ==在某个线程中为其赋值，在其他线程中取出使用==\n通过 set_value() 保存值 通过 get_future() 绑定 future 获取值 实现线程间数据传递 void mythread(std::promise\u0026lt;int\u0026gt;\u0026amp; tapp, int calc) { calc++; calc *= 10; tapp.set_value(calc); // 保存结果 } std::promise\u0026lt;int\u0026gt; myprom; std::thread t1(mythread, std::ref(myprom), 180); t1.join(); std::future\u0026lt;int\u0026gt; ful = myprom.get_future(); auto result = ful.get(); // 获取结果 17.10 future 其他成员函数、shared_future 与 atomic # 17.10.1 std::future 的其他成员函数 # [!tip] wait_for\nwait_for(): ==等待一段时间，返回 std::future_status 枚举==\nstd::future_status::timeout：超时，线程还没执行完 std::future_status::ready：线程成功返回 std::future_status::deferred：线程被延迟执行 std::future_status status = result.wait_for(std::chrono::seconds(1)); if (status == std::future_status::timeout) { /* 超时 */ } else if (status == std::future_status::ready) { /* 完成 */ } else if (status == std::future_status::deferred) { /* 延迟 */ } 17.10.2 续谈 std::async 的不确定性问题 # [!tip] 判断异步任务是否被推迟\n方法: ==使用 wait_for(std::chrono::seconds(0)) 判断==\n返回 deferred：任务被推迟 返回 ready：任务已完成 返回 timeout：任务正在执行中 17.10.3 std::shared_future # [!tip] std::shared_future\nstd::shared_future: ==共享式 future，get() 返回数据的复制而非移动==\nfuture 的 get() 是移动语义，只能调用一次 shared_future 的 get() 是复制语义，可调用多次 适用于多个线程都需要获取结果的场景 std::shared_future\u0026lt;int\u0026gt; result_s(std::move(result)); // 从 future 转移 // 或 result_s = result.share(); auto val1 = result_s.get(); // 可多次 get auto val2 = result_s.get(); 17.10.4 原子操作 std::atomic # [!tip] 原子操作\nstd::atomic: ==不可分割的操作，不会被打断，无需互斥量（无锁）==\n针对一个变量而非代码段 效率比互斥量更高 ++、--、+=、-=、\u0026amp;=、\\|=、^= 等简单运算是原子的 atm = atm + 1; 不是原子操作 std::atomic\u0026lt;int\u0026gt; g_mycout(0); void mythread() { for (int i = 0; i \u0026lt; 10000000; i++) { g_mycout++; // 原子操作，不会被打断 } } [!tip] atomic 的限制\n不允许拷贝构造（auto atm2 = atm; 编译报错） 不允许复制赋值（atm3 = atm; 编译报错） load()：以原子方式读 atomic 对象的值 store()：以原子方式写入内容 17.11 Windows 临界区与其他各种 mutex 互斥量 # 17.11.1 Windows 临界区 # [!tip] Windows 临界区\n临界区: ==Windows 平台下与 mutex 功能类似的同步机制==\nEnterCriticalSection：进入临界区（加锁） LeaveCriticalSection：离开临界区（解锁） InitializeCriticalSection：初始化 DeleteCriticalSection：释放 17.11.2 多次进入临界区试验 # ! Windows 临界区：同一线程可多次进入，但 Enter 和 Leave 次数必须相同 ! C++11 mutex：同一线程连续两次 lock 会报异常 17.11.3 自动析构技术 # [!tip] RAII 技术\nRAII（Resource Acquisition Is Initialization）: ==在构造函数中初始化资源，析构函数中释放资源==\n智能指针、容器等都用到了这种技术 可自定义类实现类似 lock_guard 的功能 17.11.4 recursive_mutex 递归的独占互斥量 # [!tip] std::recursive_mutex\nstd::recursive_mutex: ==允许同一个线程多次 lock 同一个互斥量==\n解决同一线程中函数嵌套调用导致多次 lock 的问题 比普通 mutex 消耗更多、效率差一些 lock 多少次就要 unlock 多少次 17.11.5 带超时的互斥量 # [!tip] std::timed_mutex\nstd::timed_mutex: ==带超时功能的独占互斥量==\ntry_lock_for()：等待一段时间获取锁，超时返回 false try_lock_until()：等待到某个时间点获取锁，超时返回 false std::recursive_timed_mutex: ==带超时功能的递归独占互斥量==\n结合 recursive_mutex 和 timed_mutex 的特性 std::timed_mutex my_mutex; std::chrono::milliseconds timeout(100); if (my_mutex.try_lock_for(timeout)) { // 拿到锁 my_mutex.unlock(); } else { // 超时未拿到锁 } 17.12 补充知识、线程池浅谈、数量谈与总结 # 17.12.1 知识点补充 # [!tip] 虚假唤醒\n虚假唤醒: ==wait 被唤醒但条件不满足（如消息队列无数据）==\n产生原因：push_back 一条数据后调用多次 notify_one，或 notify_all 唤醒多个线程 解决方案：wait 的第二个参数（lambda 表达式）中用 if 判断条件是否真正满足 [!tip] atomic 进一步理解\ncout \u0026lt;\u0026lt; atm \u0026lt;\u0026lt; endl; 读 atm 是原子操作，但整行语句不是 atm = atm + 1; 不是原子操作（atm.load() + atm.store()） atm += 1; 是原子操作 17.12.2 浅谈线程池 # [!tip] 线程池\n线程池: ==预先创建一定数量的线程，统一管理调度，循环再利用==\n程序启动时一次性创建好线程 任务到来时从池中取出空闲线程处理 处理完后线程不销毁，继续等待下次任务 优点：避免动态创建线程的资源消耗，提高效率，程序更健壮稳定 17.12.3 线程创建数量谈 # ! 线程创建数量极限约 2000 个 ! 建议一个进程中的线程数量尽量不超过 500 个，200 个以内为佳 \u0026amp; 采用 IOCP 等技术时，建议线程数等于 CPU 数量或 CPU 数量 × 2 \u0026amp; 根据业务需要确定线程数，实践是检验真理的最好标准 17.12.4 C++11 多线程总结 # [!tip] C++11 多线程总结\nC++11 引入多线程开发接口，实现跨平台统一开发 降低学习成本，提高程序可移植性 可与具体操作系统平台线程开发接口结合使用，优势互补 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p16-%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%A4%9A%E7%BA%BF%E7%A8%8B/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e17.1 基本概念和实现\n    \u003cdiv id=\"171-基本概念和实现\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#171-%e5%9f%ba%e6%9c%ac%e6%a6%82%e5%bf%b5%e5%92%8c%e5%ae%9e%e7%8e%b0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 线程基础图 ![[白板/P52 线程基础.canvas|线程基础]]\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e17.1.1 并发、进程、线程的基本概念和综述\n    \u003cdiv id=\"1711-并发进程线程的基本概念和综述\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1711-%e5%b9%b6%e5%8f%91%e8%bf%9b%e7%a8%8b%e7%ba%bf%e7%a8%8b%e7%9a%84%e5%9f%ba%e6%9c%ac%e6%a6%82%e5%bf%b5%e5%92%8c%e7%bb%bc%e8%bf%b0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 并发\u003c/p\u003e","title":"P16 并发与多线程","type":"notes"},{"content":" 知识点 # 18.1 new、delete 的进一步认识 # @ new与delete深入图 ![[白板/P56 new与delete深入.canvas|new与delete深入]] 18.1.1 总述与回顾 # [!tip] 回顾\nnew/delete 的基本使用已在 13.4.2 节和 16.2 节讲解 本章从高级角度深入探讨内存相关知识 理解内存高级话题有助于理解模板中的内存分配等概念 实战中常遇到内存池等概念 18.1.2 从 new 说起 # new 类对象加与不加括号的差别 # [!tip] new类对象的括号差异\nnew A() vs new A：\n空类：无区别 有成员变量无构造函数：new A() 成员变量初始化为 0，new A 成员变量为随机值 有构造函数：两者相同，初始化工作交给构造函数 简单类型：\nnew int：初值为随机值 new int()：初值为 0 new int(100)：初值为 100 new 做了什么事 # [!tip] new关键字的调用关系\nnew 关键字主要做了两件事：\n调用 operator new() 分配内存 调用类的构造函数 完整调用关系：\nA *pa = new A(); // 操作符 operator new(); // 函数 malloc(); // C风格函数分配内存 A::A(); // 有构造函数就调用构造函数 ! operator new 是一个函数，可以被直接调用：operator new(12); [!tip] delete关键字的调用关系\ndelete 关键字的调用关系（注意顺序）：\ndelete pa; A::~A(); // 先调用析构函数 operator delete(); // 函数 free(); // C风格函数释放内存 new 与 malloc 的区别 # [!tip] new与malloc区别（面试必答）\nnew 是关键字/操作符，malloc 是函数 new 不但分配内存，还会调用类的构造函数 new 可以将成员变量初始化为 0，malloc 没这个能力 delete 不但释放内存，还会调用析构函数；free 只释放内存 ! 一般提倡使用 new/delete，不提倡使用 malloc/free（C编程风格） 18.2 new 内存分配细节探秘与重载类内 operator new、delete # @ 内存分配细节图 ![[白板/P56 new与delete深入.canvas|new与delete深入]] 18.2.1 new 内存分配细节探秘 # [!tip] malloc分配内存的真相\n分配 10 字节内存，实际分配远不止 10 字节 额外内存用于记录：分配字节数、调试信息、边界调整填充、回收内存标记等 在分配的内存首地址前 12 字节位置记录了分配出去的字节数 free 释放内存时影响的范围远大于分配的字节数 ==程序员拿到的地址是 malloc 分配的整块内存中间的某个位置==\n[!tip] 内存分配与释放的合并\n释放内存时，free 函数负责将临近的空闲内存块合并 合并空闲块、登记空闲块大小、设置标记等都是 free 的职责 频繁分配小块内存浪费更加严重 18.2.2 重载类中的 operator new 和 operator delete 操作符 # [!tip] 重载类内operator new/delete\n在类中重载 operator new 和 operator delete 可以取代系统的同名函数 重载后的函数负责分配和释放内存 operator new 应为 static 函数（new 对象时还没对象存在） class A { public: static void *operator new(size_t size); static void operator delete(void *phead); }; void *A::operator new(size_t size) { cout \u0026lt;\u0026lt; \u0026#34;A::operator new被调用了\u0026#34; \u0026lt;\u0026lt; endl; A *ppoint = (A *)malloc(size); return ppoint; } void A::operator delete(void *phead) { cout \u0026lt;\u0026lt; \u0026#34;A::operator delete被调用了\u0026#34; \u0026lt;\u0026lt; endl; free(phead); } ! 调用 ::new A() 使用全局 new，不调用类内重载的 operator new，但仍会调用构造函数 18.2.3 重载类中的 operator new[] 和 operator delete[] 操作符 # [!tip] 数组操作符重载\nnew A[3]() 调用的是 operator new[]，不是 operator new\noperator new[] 和 operator delete[] 只调用 1 次\n构造函数和析构函数分别调用 3 次（数组大小）\n对象数组分配时，额外 4 字节记录数组大小\nmalloc 返回的首地址与程序员拿到的指针相差 4 字节\n[!tip] 对象数组内存概貌\n| 记录字节数(4B) | 调试信息(~几十B) | 数组大小(4B) | 3个对象(3B) | 边界填充 | 回收标记(4B) | ↑ 程序员拿到的指针 多出的 4 字节记录数组元素个数，delete[] 时据此知道调用多少次析构函数 18.3 内存池概念、代码实现和详细分析 # @ 内存池图 ![[白板/P57 内存池.canvas|内存池]] 18.3.1 内存池的概念和实现原理简介 # [!tip] 内存池概念\n内存池: ==用 malloc 申请一大块内存，分配时从大块中一点点分配，用完再申请一大块==\n解决的主要问题：减少 malloc 调用次数，减少内存浪费 提高运行效率是顺带的，malloc 本身执行速度极快 实现原理：一次申请一大块，小块分配，用完再申请 18.3.2 针对一个类的内存池实现演示代码 # [!tip] 内存池实现核心思路\n通过重载类内 operator new/operator delete 实现内存池 operator new：从空闲链表中取出一块返回；链表为空时一次性 malloc 多块 operator delete：不调用 free，而是将内存块链回空闲链表 class A { public: static void *operator new(size_t size); static void operator delete(void *phead); static int m_iCount; // 分配计数 static int m_iMallocCount; // malloc次数 private: A *next; // 空闲链表指针 static A *m_FreePosi; // 指向可分配内存的首地址 static int m_sTrunkCount; // 一次分配多少块 }; [!tip] 内存池分配流程\n第一次 new：m_FreePosi 为 nullptr，malloc 一大块（m_sTrunkCount 块），用 next 指针链成链表 后续 new：从 m_FreePosi 取出一块，m_FreePosi 指向下一块 链表用完：再次 malloc 一大块，链入空闲链表 delete：将释放的内存块链回空闲链表头部，m_FreePosi 指向它 ! 内存池的 operator delete 并不真正归还内存给系统（不调用 free），而是链回空闲链表 ! m_sTrunkCount 不宜设置太大，否则首次分配浪费；几十之间较合适 [!tip] 内存池性能对比\n方式 500万次分配用时 malloc 次数 内存池（每次5块） ~500-800ms 100万次 内存池（每次500块） ~300-500ms 1万次 原生 malloc ~1000-1500ms 500万次 malloc 速度本身极快，内存池提升效率有限 内存池主要价值在于==减少内存浪费== 18.3.3 内存池代码后续说明 # % 每 5 个分配的内存地址是挨着的（间隔 4 字节），说明内存池在发挥作用 ! 内存池代码不完善：释放时没有真正 delete，只是链回空闲链表 ! 程序退出时建议真正释放内存池占用的内存 18.4 嵌入式指针概念及范例、内存池改进版 # @ 嵌入式指针图 ![[白板/P57 内存池.canvas|内存池]] 18.4.1 嵌入式指针 # [!tip] 嵌入式指针（embedded pointer）\n嵌入式指针: ==借用对象所占内存空间的前 4 字节来保存空闲链表指针，省去额外的 next 成员变量==\n前提条件：类的 sizeof 必须 \u0026gt;= 4 字节 工作原理：空闲时前 4 字节保存链表指针；分配出去后被对象数据覆盖，无影响 常用于内存池代码实现中 [!tip] 嵌入式指针实现方式\nclass TestEP { public: int m_i; int m_j; private: struct obj { struct obj *next; // 嵌入式指针 }; }; struct obj 定义在类内部（private），限制外部使用 next 指针借用对象前 4 字节，空闲时链住空闲块 18.4.2 内存池代码的改进 # [!tip] 独立内存池类 myallocator\n将内存池逻辑提取为独立的 myallocator 类 使用嵌入式指针替代 next 成员变量 任何类只需声明静态 myallocator 成员即可使用 class myallocator { public: void *allocate(size_t size); // 分配内存接口 void deallocate(void *phead); // 释放内存接口 private: struct obj { struct obj *next; }; // 嵌入式指针 int m_sTrunkCount = 5; obj *m_FreePosi = nullptr; }; [!tip] 使用宏简化内存池声明\n#define DECLARE_POOL_ALLOC() \\ public: \\ static void *operator new(size_t size) { return myalloc.allocate(size); } \\ static void operator delete(void *phead) { return myalloc.deallocate(phead); } \\ static myallocator myalloc; #define IMPLEMENT_POOL_ALLOC(classname) \\ myallocator classname::myalloc; // 使用 class A { DECLARE_POOL_ALLOC(); public: int m_i; int m_j; }; IMPLEMENT_POOL_ALLOC(A) 18.5 重载全局 new/delete、定位 new 及重载 # @ 定位new图 ![[白板/P58 定位new.canvas|定位new]] 18.5.1 重载全局 operator new 和 operator delete 操作符 # [!tip] 重载全局operator new/delete\nvoid *operator new(size_t size) { return malloc(size); } void *operator new[](size_t size) { return malloc(size); } void operator delete(void *phead) { free(phead); } void operator delete[](void *phead) { free(phead); } 必须放在全局空间，不能放在自定义命名空间 影响面太广，==很少在实际项目中使用== 类内重载会覆盖全局重载 18.5.2 定位 new（placement new） # [!tip] 定位new（placement new）\n定位new: ==在已经分配好的原始内存中初始化一个对象（调用构造函数），不额外分配内存==\n不分配内存，内存必须事先分配好 只初始化对象（调用构造函数） 没有 placement delete [!tip] 定位new用法\n// 格式：new (地址) 类类型(参数) // 1. 先分配内存 void *mymemPoint = (void *)new char[sizeof(PLA)]; // 2. 定位new：在已分配内存上构造对象 PLA *pmyAobj1 = new(mymemPoint) PLA(); // 无参构造 PLA *pmyAobj2 = new(mymemPoint2) PLA(12); // 带参构造 // 3. 手动调用析构函数 pmyAobj1-\u0026gt;~PLA(); // 4. 释放原始内存 delete[](void *)pmyAobj1; [!tip] 定位new的调用关系\nPLA *pa = new(分配好的内存首地址) PLA(); // 定位new操作符 operator new(); // 函数，不调用malloc PLA::PLA(); // 调用构造函数 定位new的 operator new 不调用 malloc，不分配内存 可以在类中重载定位new的 operator new，参数比传统 new 多一个 [!tip] 重载定位new的operator new\n// 在类PLA中重载 void *operator new(size_t size, void *phead) { // 不要分配内存 return phead; // 返回传入的内存地址 } 18.5.3 多种版本的 operator new 重载 # [!tip] 多版本operator new重载\n可以重载多个版本的 operator new，只要参数不同 第一个参数固定为 size_t（对象的 sizeof 值） 其他参数通过 new 时指定 // 类中重载 void *operator new(size_t size, int typ1, int typ2) { return NULL; } void operator delete(void *phead, int typ1, int typ2) { return; } // 调用 PLA *pla = new(1234, 56) PLA(); 这种用法不会调用类的构造函数 主要目的是扩大见闻，不建议常规使用 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p17-%E5%86%85%E5%AD%98%E9%AB%98%E7%BA%A7%E8%AF%9D%E9%A2%98/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e18.1 new、delete 的进一步认识\n    \u003cdiv id=\"181-newdelete-的进一步认识\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#181-newdelete-%e7%9a%84%e8%bf%9b%e4%b8%80%e6%ad%a5%e8%ae%a4%e8%af%86\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ new与delete深入图 ![[白板/P56 new与delete深入.canvas|new与delete深入]]\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e18.1.1 总述与回顾\n    \u003cdiv id=\"1811-总述与回顾\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1811-%e6%80%bb%e8%bf%b0%e4%b8%8e%e5%9b%9e%e9%a1%be\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 回顾\u003c/p\u003e","title":"P17 内存高级话题","type":"notes"},{"content":" 知识点 # 19.1 STL 总述、发展史、组成与数据结构谈 # @ STL基础概念图 ![[白板/P59 STL组成结构.canvas|STL组成结构]] 19.1.1 几个概念与推荐书籍 # [!tip] STL相关概念\nC++标准库：安装C++编译器后自带，提供各种可复用功能（如 vector） 标准模板库（STL）：C++标准库的核心组成部分，包含在标准库中 泛型编程：使用模板为主要编程手段，STL就是用泛型编程方式编写的库 \u0026amp; 推荐书籍：《C++标准库》（字典式查阅）、《STL源码剖析》（侯捷） 19.1.2 算法和数据结构谈 # [!tip] 数据结构基础\n栈：后进先出（LIFO），如上公交车 队列：先进先出（FIFO），如排队买饭 链表：每个节点有数据部分和 next 指针，多个节点串在一起 树、图、散列表：了解基本概念即可 ==STL内部使用树、哈希表等数据结构，但程序员不需要关心这些==\n! 90%的开发者不需要深入学习红黑树左旋右旋等细节 19.1.3 STL 发展史和各个版本 # [!tip] STL版本\nHP STL：惠普STL，所有STL实现版本的始祖 SGI STL：Linux下的GNU C++（gcc、g++）使用 P.J. Plauger STL：Visual C++（Visual Studio）使用 19.1.4 标准库的使用说明 # [!tip] 标准库使用\n所有标识符定义在 std 命名空间内：using namespace std; 头文件无扩展名：#include \u0026lt;iostream\u0026gt;、#include \u0026lt;string\u0026gt; C语言头文件加 c 前缀：#include \u0026lt;cstdlib\u0026gt;（对应 \u0026lt;stdlib.h\u0026gt;） 19.1.5 STL 的组成部分 # [!tip] STL五大组成部分\n容器：保存数据，管理元素（如 vector、map、list） 迭代器：遍历/访问容器中的元素，类似指针 算法：STL提供的函数，如查找（find）、排序（sort）、复制（copy） 分配器：内存分配器，服务于容器，一般使用默认分配器 其他：适配器、仿函数（函数对象）等 19.2 容器分类与 array、vector 容器精解 # @ 容器分类图 ![[白板/P59 STL组成结构.canvas|STL组成结构]] 19.2.1 容器的分类 # [!tip] 容器三大分类\n顺序容器：元素放进去时排在哪里，就在哪里\narray、vector、deque、list、forward_list 关联容器：每个元素是键值对，通过 key 找 value 特别快\n内部用树（红黑树）实现 set、map、multiset、multimap 无序容器（C++11）：元素位置不重要，内部用哈希表实现\nunordered_set、unordered_multiset、unordered_map、unordered_multimap [!tip] 哈希表工作原理\n有多个篮子（桶），元素通过算法计算后挂到对应篮子上 元素个数 \u0026gt;= 篮子个数时，扩充篮子数量（如2倍），重新分配元素 同一篮子上挂的元素越多，查找效率越低 19.2.2 容器的说明和简单应用 # array 容器 # [!tip] array容器\n顺序容器，固定大小数组 空间连续，大小固定，不能增加 元素之间紧挨着（地址间隔等于元素大小） #include \u0026lt;array\u0026gt; array\u0026lt;string, 5\u0026gt; mystring = {\u0026#34;1\u0026#34;, \u0026#34;Love\u0026#34;, \u0026#34;China\u0026#34;}; cout \u0026lt;\u0026lt; mystring.size() \u0026lt;\u0026lt; endl; // 5 vector 容器 # [!tip] vector容器特点\n顺序容器，一端（尾端）开口 尾端插入/删除快，中间插入/删除慢（需移动元素） 内存空间连续 有 size()（元素数量）和 capacity()（空间容量） ! capacity \u0026gt;= size，空间增长时会重新分配内存，导致元素搬迁 [!tip] vector效率陷阱\n频繁插入元素时，capacity 增长导致：\n老元素析构 新内存中重新构造 效率极低 解决方案：预先预留空间\nvector\u0026lt;A\u0026gt; myveca; myveca.reserve(10); // 预留10个空间 [!tip] vector删除/插入元素\nerase()：删除元素，后续元素往前移动（析构一次） insert()：插入元素，后续元素往后移动（析构+重新构造） 19.3 容器的说明和简单应用例续 # 19.3.1 deque 和 stack # [!tip] deque容器\n双端队列（双向开口），动态数组 头部和尾部插入/删除都快 中间插入/删除效率低 ==分段连续内存==（每段内存连续，整体不连续） [!tip] stack容器\n栈/堆栈，后进先出（LIFO） 只有一端开口，只支持栈顶操作 不支持中间插入/删除（不同于 vector） deque 包含 stack 功能 19.3.2 queue # [!tip] queue容器\n普通队列，先进先出（FIFO） 元素从一端入，从另一端出 deque 也包含 queue 功能 19.3.3 list # [!tip] list容器\n双向链表，内存不连续 任意位置插入/删除非常快（只需改变指针） 查找效率不高（需沿链逐个查找） 不支持高效随机存取 [!tip] vector vs list（面试常考）\n特性 vector list 内存 连续 不连续 插入/删除 低效 高效 随机存取 高效 低效 内存不足时 重新分配+搬迁 无此问题 19.3.4 其他容器 # [!tip] forward_list容器\nC++11新增，单向链表 比 list 少一个方向的指针，节省4字节 只有 push_front，适合往前头插入 [!tip] map容器\n关联容器，内部用红黑树 每个元素是键值对（key/value） 通过 key 查找 value 极快 key 不允许重复（重复插入无效） key 可重复时用 multimap #include \u0026lt;map\u0026gt; map\u0026lt;int, string\u0026gt; mymap; mymap.insert(std::make_pair(1, \u0026#34;老王\u0026#34;)); mymap.insert(pair\u0026lt;int, string\u0026gt;(3, \u0026#34;老赵\u0026#34;)); auto iter = mymap.find(3); // 查找key为3的元素 [!tip] set容器\n关联容器，每个元素就是一个 value 元素值不允许重复 值可重复时用 multiset 插入时容器自动安排位置 [!tip] unordered_set等无序容器\nC++11新增，内部用哈希表 unordered_map、unordered_set：键/值不允许重复 unordered_multimap、unordered_multiset：允许重复 bucket_count()：篮子数量 元素超过篮子数量时，篮子数量会扩充 19.4 分配器简介、使用与工作原理说 # @ 分配器图 ![[白板/P59 STL组成结构.canvas|STL组成结构]] 19.4.1 分配器简介 # [!tip] 分配器概念\n内存分配器：服务于容器，为容器中的元素分配内存 默认分配器：std::allocator\u0026lt;T\u0026gt; vector\u0026lt;int\u0026gt; 等价于 vector\u0026lt;int, std::allocator\u0026lt;int\u0026gt;\u0026gt; ==引入分配器的主要目的：减少 malloc 调用次数，减少内存浪费==\n! 标准库的默认 allocator 底层可能直接调用 malloc，未使用内存池技术 19.4.2 分配器的使用 # [!tip] 分配器使用范例\n#include \u0026lt;memory\u0026gt; allocator\u0026lt;int\u0026gt; aalloc; int *p = aalloc.allocate(3); // 分配3个int的原始内存 int *q = p; *q = 1; q++; *q = 2; q++; *q = 3; aalloc.deallocate(p, 3); // 释放时需记住分配了多少 ! 分配器一般不直接使用，用 new/delete 更方便 19.4.3 其他的分配器与原理说 # [!tip] 内存池分配器原理\n使用多个内存池，不同编号对应不同大小内存块 1号针对8字节，2号针对16字节\u0026hellip; 申请7字节时，往8字节靠拢，从1号分配 一次分配一大块，小块分配给程序员 ==减少 malloc 调用，减少内存浪费== ! 分配器申请的内存很难真正归还给操作系统（除非整块全部回收） 19.5 迭代器的概念和分类 # @ 迭代器分类图 ![[白板/P60 迭代器与算法.canvas|迭代器与算法]] 19.5.1 迭代器基本概念 # [!tip] 迭代器概念\n迭代器：可遍历STL容器全部或部分元素的对象 行为类似于指针的对象 由容器提供，表现容器中的某一个位置 *iter 读取迭代器指向的内容 [!tip] 迭代器经典用法\nvector\u0026lt;int\u0026gt; iv {100, 200, 300}; for (vector\u0026lt;int\u0026gt;::iterator iter = iv.begin(); iter != iv.end(); ++iter) cout \u0026lt;\u0026lt; *iter \u0026lt;\u0026lt; endl; 19.5.2 迭代器的分类 # [!tip] 迭代器五大分类\n输出型迭代器（output_iterator_tag）：向前写入 输入型迭代器（input_iterator_tag）：向前读取一次 前向迭代器（forward_iterator_tag）：向前读取/写入 双向迭代器（bidirectional_iterator_tag）：向前和向后读取 随机访问迭代器（random_access_iterator_tag）：随机读取，可跳跃多个元素 [!tip] 迭代器分类继承关系\noutput_iterator_tag（最低） input_iterator_tag forward_iterator_tag bidirectional_iterator_tag random_access_iterator_tag（最高） [!tip] 各容器支持的迭代器类型\n迭代器类型 容器 随机访问 array、vector、deque、string、C风格数组 双向 list、set、multiset、map、multimap 前向 forward_list、unordered_*容器 输入 istream 输出 ostream ! stack、queue 不提供迭代器（后进先出/先进先出，不需要遍历） [!tip] 随机访问迭代器特点\n支持最多操作：iter[n]、iter+=n、iter-=n、iter+n、iter-n 可计算距离：iter1 - iter2 支持关系运算：iter1 \u0026lt; iter2、iter1 \u0026gt; iter2 支持随机访问的容器多数内存连续 19.6 算法简介、内部处理与使用范例 # @ 算法图 ![[白板/P60 迭代器与算法.canvas|迭代器与算法]] 19.6.1 算法简介 # [!tip] 算法概念\n算法：理解为函数模板，全局函数 不针对某个容器，对大部分容器都适用 参数一般是迭代器，表示元素区间 区间是==前闭后开== [begin(), end()) [!tip] 前闭后开区间的好处\n判断迭代器等于 end() 表示迭代结束 begin() == end() 表示空区间 ! 算法违背了面向对象封装性，但增加了灵活性 19.6.2 算法内部处理 # [!tip] 算法内部根据迭代器类型优化\n算法内部拿到迭代器分类，不同种类有不同处理 随机访问迭代器：直接加数字跳转，效率高 前向迭代器：不能执行向后读取操作 ==从效率角度考虑不同迭代器的处理方式== 19.6.3 一些典型算法使用范例 # [!tip] for_each算法\n#include \u0026lt;algorithm\u0026gt; void myfunc(int i) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; endl; } vector\u0026lt;int\u0026gt; myvector {10, 20, 30, 40, 50}; for_each(myvector.begin(), myvector.end(), myfunc); 第三个参数是可调用对象（函数、lambda、重载 operator() 的类） [!tip] find算法\nvector\u0026lt;int\u0026gt;::iterator finditer = find(myvector.begin(), myvector.end(), 400); if (finditer != myvector.end()) cout \u0026lt;\u0026lt; \u0026#34;找到\u0026#34; \u0026lt;\u0026lt; endl; 返回迭代器，指向第一个匹配元素 ! 容器有同名成员函数时，优先使用成员函数（如 map::find） [!tip] find_if算法\nauto result = find_if(myvector2.begin(), myvector2.end(), [](int val) { return val \u0026gt; 15; }); 第三个参数是可调用对象，返回 true 时停止遍历 [!tip] sort算法\nvector\u0026lt;int\u0026gt; myvector3 {50, 15, 80, 30, 46}; sort(myvector3.begin(), myvector3.end()); // 默认从小到大 sort(myvector3.begin(), myvector3.end(), greater\u0026lt;int\u0026gt;()); // 从大到小 只适用于随机访问迭代器 list 不支持 sort 算法，用 list::sort 成员函数 关联容器不适合排序（位置由容器内部算法决定） 19.7 函数对象回顾、系统函数对象与范例 # @ 函数对象图 ![[白板/P60 迭代器与算法.canvas|迭代器与算法]] 19.7.1 函数对象/仿函数回顾 # [!tip] 函数对象形式\n函数：void func(int x) {...} 函数对象：重载 operator() 的类对象 class A { public: bool operator()(int i, int j) { return i \u0026gt; j; } }; lambda表达式：也是一种可调用对象 ==调用方式统一：名字(参数列表)==\n19.7.2 标准库中定义的函数对象 # [!tip] 标准库函数对象分类\n算术运算类：negate、plus、minus、multiplies、divides、modulus 关系运算类：equal_to、not_equal_to、less、greater、less_equal、greater_equal 逻辑运算类：logical_not、logical_and、logical_or 位运算类：bit_and、bit_or、bit_xor 使用前包含：#include \u0026lt;functional\u0026gt;\n[!tip] 函数对象使用范例\ncout \u0026lt;\u0026lt; plus\u0026lt;int\u0026gt;()(4, 5) \u0026lt;\u0026lt; endl; // 9 plus\u0026lt;int\u0026gt;()：生成临时对象 (4, 5)：调用 operator()，传入两个参数 19.7.3 标准库中定义的函数对象范例 # [!tip] 使用标准库函数对象排序\nsort(myvector3.begin(), myvector3.end(), greater\u0026lt;int\u0026gt;()); // 从大到小 sort(myvector3.begin(), myvector3.end(), less\u0026lt;int\u0026gt;()); // 从小到大 19.8 适配器概念、分类、范例与总结 # @ 适配器图 ![[白板/P59 STL组成结构.canvas|STL组成结构]] 19.8.1 适配器基本概念 # [!tip] 适配器概念\n适配器：类似于转接头，把既有东西进行适当改造 增加或减少一点东西，成为适配器 分为：容器适配器、算法适配器、迭代器适配器 19.8.2 容器适配器 # [!tip] 容器适配器\nstack：后进先出，是对 deque 的改造（减少一端） queue：先进先出，是对 deque 的改造（减少一端） stack 和 queue 归类为容器适配器 19.8.3 算法适配器 # [!tip] 算法适配器（绑定器）\nbind：C++11的绑定器，替代老的 bind1st、bind2nd 可以把函数对象的某些参数绑定固定值 #include \u0026lt;functional\u0026gt; auto bf = bind(less\u0026lt;int\u0026gt;(), 40, placeholders::_1); bf(19); // 等价于 less\u0026lt;int\u0026gt;()(40, 19)，即 40 \u0026lt; 19 placeholders::_1：表示调用时传入的第一个参数 [!tip] bind配合count_if使用\nint cishu = count_if(myvector.begin(), myvector.end(), bind(less\u0026lt;int\u0026gt;(), 40, placeholders::_1)); // 统计大于40的元素 19.8.4 迭代器适配器 # [!tip] 迭代器适配器\n反向迭代器（reverse_iterator）：反向遍历容器 vector\u0026lt;int\u0026gt; iv = {100, 200, 300}; for (vector\u0026lt;int\u0026gt;::reverse_iterator riter = iv.rbegin(); riter != iv.rend(); riter++) cout \u0026lt;\u0026lt; *riter \u0026lt;\u0026lt; endl; // 输出 300, 200, 100 19.8.5 总结 # [!tip] STL组成结构总结\nSTL组成 ├── 容器（顺序、关联、无序） ├── 迭代器（输出、输入、前向、双向、随机访问） ├── 算法（for_each、find、find_if、sort等） ├── 分配器（std::allocator\u0026lt;T\u0026gt;） └── 其他 ├── 函数对象（标准库提供：plus、less等） ├── 适配器 │ ├── 容器适配器（stack、queue） │ ├── 算法适配器（bind） │ └── 迭代器适配器（reverse_iterator） ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p18-stl%E6%A0%87%E5%87%86%E6%A8%A1%E6%9D%BF%E5%BA%93%E5%A4%A7%E5%B1%80%E8%A7%82/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e19.1 STL 总述、发展史、组成与数据结构谈\n    \u003cdiv id=\"191-stl-总述发展史组成与数据结构谈\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#191-stl-%e6%80%bb%e8%bf%b0%e5%8f%91%e5%b1%95%e5%8f%b2%e7%bb%84%e6%88%90%e4%b8%8e%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e8%b0%88\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ STL基础概念图 ![[白板/P59 STL组成结构.canvas|STL组成结构]]\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e19.1.1 几个概念与推荐书籍\n    \u003cdiv id=\"1911-几个概念与推荐书籍\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#1911-%e5%87%a0%e4%b8%aa%e6%a6%82%e5%bf%b5%e4%b8%8e%e6%8e%a8%e8%8d%90%e4%b9%a6%e7%b1%8d\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] STL相关概念\u003c/p\u003e","title":"P18 STL标准模板库大局观","type":"notes"},{"content":" 知识点 # 20.1 函数调用运算符与 function 类模板 # @ 函数对象图 ![[白板/P61 函数对象与可调用对象.canvas|函数对象与可调用对象]] 20.1.1 函数调用运算符 # [!tip] 函数调用运算符\n( ) 是函数调用运算符 在类中重载 operator()，可以像函数一样使用该类的对象 ==重载了 ( ) 的类对象就是可调用对象（函数对象）== class biggerthanzero { public: int operator()(int value) const { if (value \u0026lt; 0) return 0; return value; } }; biggerthanzero obj; int result = obj(-5); // 调用 operator()，返回 0 20.1.2 不同调用对象的相同调用形式 # [!tip] 调用形式相同\n函数和重载 operator() 的类对象，如果形参和返回值相同，则\u0026quot;调用形式相同\u0026quot; 一种调用形式对应一个函数类型 int(int) 代表一个函数类型：接收一个 int 参数，返回一个 int 值 20.1.3 标准库 function 类型简介 # [!tip] std::function 类模板\nC++11 标准库中的类模板，用于包装可调用对象 function\u0026lt;int(int)\u0026gt; 可以包装任何接收 int 返回 int 的可调用对象 ==统一了不同可调用对象的类型== #include \u0026lt;functional\u0026gt; function\u0026lt;int(int)\u0026gt; f1 = echovalue; // 函数指针 function\u0026lt;int(int)\u0026gt; f2 = obj; // 类对象 function\u0026lt;int(int)\u0026gt; f3 = biggerthanzero(); // 临时对象 ! 函数是重载的时，无法直接包装进 function，需用函数指针解决二义性 20.2 万能引用 # @ 万能引用图 ![[白板/P62 万能引用与类型推断.canvas|万能引用与类型推断]] 20.2.1 类型区别基本概念 # [!tip] 类型模板参数 T\nT 是类型模板参数，有类型 tmprv 是函数形参，也有类型 ==T 的类型不仅取决于实参，还取决于形参的类型== 20.2.2 universal reference 基本认识 # [!tip] 万能引用（universal reference）\n又名转发引用（forwarding reference）、未定义引用 ==万能引用是一种类型，长得像右值引用 T\u0026amp;\u0026amp;== 必须同时满足两个条件： 必须是函数模板 必须发生模板类型推断，形参为 T\u0026amp;\u0026amp; [!tip] 万能引用 vs 右值引用\n特性 右值引用 万能引用 形式 int\u0026amp;\u0026amp; T\u0026amp;\u0026amp;（模板中） 绑定左值 不可以 可以 绑定右值 可以 可以 类型推断 无 有 template \u0026lt;typename T\u0026gt; void myfunc(T\u0026amp;\u0026amp; tmprv) { // 万能引用 tmprv = 12; } int i = 100; myfunc(i); // 左值，tmprv 为 int\u0026amp; myfunc(std::move(i)); // 右值，tmprv 为 int\u0026amp;\u0026amp; ! 判断题： void func(int\u0026amp;\u0026amp; param) → 右值引用（不是函数模板） template\u0026lt;typename T\u0026gt; void func(T\u0026amp;\u0026amp; tmpvalue) → 万能引用 template\u0026lt;typename T\u0026gt; void func(std::vector\u0026lt;T\u0026gt;\u0026amp;\u0026amp; param) → 右值引用（T和\u0026amp;\u0026amp;不挨着） 20.2.3 万能引用资格的剥夺与辨认 # [!tip] 万能引用资格剥夺\nconst 修饰词会剥夺万能引用资格，退化为右值引用 template\u0026lt;typename T\u0026gt; void myfunc(const T\u0026amp;\u0026amp; tmprv) → 右值引用 [!tip] 万能引用辨认\n类模板的成员函数 void testfunc(T\u0026amp;\u0026amp; x) → 右值引用（无类型推断） 成员函数模板 template\u0026lt;typename T2\u0026gt; void testfunc2(T2\u0026amp;\u0026amp; x) → 万能引用（有类型推断） 20.3 理解函数模板类型推断与查看类型推断结果 # @ 类型推断图 ![[白板/P62 万能引用与类型推断.canvas|万能引用与类型推断]] 20.3.1 如何查看类型推断结果 # [!tip] Boost库 typeindex\n#include \u0026lt;boost/type_index.hpp\u0026gt; using boost::typeindex::type_id_with_cvr; cout \u0026lt;\u0026lt; \u0026#34;T = \u0026#34; \u0026lt;\u0026lt; type_id_with_cvr\u0026lt;T\u0026gt;().pretty_name() \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;tmprv = \u0026#34; \u0026lt;\u0026lt; type_id_with_cvr\u0026lt;decltype(tmprv)\u0026gt;().pretty_name() \u0026lt;\u0026lt; endl; 20.3.2 理解函数模板类型推断 # [!tip] 指针或引用类型（非万能引用）\nT\u0026amp; tmprv： 实参是引用 → 引用部分被忽略，T 不推导为引用 实参是 const → const 属性会成为 T 的组成部分 const T\u0026amp; tmprv：T 中的 const 消失（形参已有 const） T* tmprv：实参的 const 会带到 T 类型中 [!tip] 万能引用类型推断\n实参类型 T 类型 tmprv 类型 左值 int i int\u0026amp; int\u0026amp; 左值 const int j int const \u0026amp; int const \u0026amp; 右值 100 int int\u0026amp;\u0026amp; [!tip] 传值方式\n引用类型实参 → 引用部分被忽略 const 类型实参 → const 部分被忽略（产生新副本） 指针的常量性被忽略，指向内容的常量性被保留 [!tip] 数组作为实参\nT tmprv → 数组退化为指针 T\u0026amp; tmprv → T 推导为数组类型（含尺寸） template\u0026lt;typename T, unsigned L\u0026gt; void myfunc(T(\u0026amp;tmprv)[L]) → 可获取数组长度 [!tip] 函数名作为实参\nT tmprv → 函数指针 T\u0026amp; tmprv → 函数引用 20.4 引用折叠、转发、完美转发与 forward # @ 引用折叠图 ![[白板/P63 引用折叠与完美转发.canvas|引用折叠与完美转发]] 20.4.1 引用折叠规则 # [!tip] 引用折叠规则\n编译器内部遇到超过两个 \u0026amp; 时进行合并 ==如果任意一个引用为左值引用，结果就为左值引用（左值引用会传染）== 组合 折叠结果 \u0026amp; + \u0026amp; \u0026amp;（左值引用） \u0026amp; + \u0026amp;\u0026amp; \u0026amp;（左值引用） \u0026amp;\u0026amp; + \u0026amp; \u0026amp;（左值引用） \u0026amp;\u0026amp; + \u0026amp;\u0026amp; \u0026amp;\u0026amp;（右值引用） ! 程序中不能直接写引用的引用（如 int\u0026amp; \u0026amp;byy），编译报错 20.4.2 转发与完美转发 # [!tip] 转发问题\n函数模板接收参数后转发给其他函数 普通引用形参无法保存实参的左值/右值信息 ==万能引用可以保存实参的所有类型信息== [!tip] 完美转发问题\n右值实参传递到函数模板后变成左值（形参本身是左值） 再转发给需要右值引用的函数时会失败 需要保持原始实参的左值/右值性 20.4.3 std::forward # [!tip] std::forward 完美转发\nC++11 标准库函数，专门为转发而存在 ==保持原始实参的左值或右值性== 只对原来是右值的情况有用（强制把左值转成右值） template\u0026lt;typename F, typename T1, typename T2\u0026gt; void myFuncTemp(F f, T1\u0026amp;\u0026amp; t1, T2\u0026amp;\u0026amp; t2) { f(std::forward\u0026lt;T1\u0026gt;(t1), std::forward\u0026lt;T2\u0026gt;(t2)); } [!tip] std::forward 工作原理\nstd::forward\u0026lt;T\u0026gt;(t) 从 T 中取出类型信息 T 是 int → 转成右值 T 是 int\u0026amp; → 保持左值 20.4.4 std::move 和 std::forward 的区别 # [!tip] move vs forward\n特性 std::move std::forward 转换方式 无条件转右值 有条件转换 参数 一个普通参数 模板类型参数 + 普通参数 用途 为移动操作做准备 参数转发 后接内容 左值 万能引用 20.5 理解 auto 类型推断与 auto 应用场合 # @ auto类型推断图 ![[白板/P62 万能引用与类型推断.canvas|万能引用与类型推断]] 20.5.1 auto 类型常规推断 # [!tip] auto 类型推断规则\nauto 相当于函数模板中的类型模板参数 T 发生在编译期，定义变量时必须立即初始化 [!tip] 传值方式（非指针、非引用）\n==会抛弃引用、const 等限定符== auto x = 27; → x 为 int const auto x2 = x; → x2 为 const int auto xy2 = xy;（xy 为 const int\u0026amp;） → xy2 为 int [!tip] 指针或引用类型（非万能引用）\n==不会抛弃 const，但会抛弃引用== auto\u0026amp; xy3 = xy;（xy 为 const int\u0026amp;） → xy3 为 const int\u0026amp;，auto 为 const int auto* xp2 = \u0026amp;x; → xp2 为 int* [!tip] 万能引用类型\nauto\u0026amp;\u0026amp; wnyy0 = 222; → 右值，wnyy0 为 int\u0026amp;\u0026amp; auto\u0026amp;\u0026amp; wnyy1 = x; → 左值，发生引用折叠，wnyy1 为 int\u0026amp; 20.5.2 auto 类型针对数组和函数的推断 # [!tip] 数组推断\nauto myarr = mystr; → myarr 为 char const*（指针） auto\u0026amp; myarr2 = mystr; → myarr2 为 char const(\u0026amp;)[14]（数组引用） [!tip] 函数推断\nauto tmpf = myfunc3; → 函数指针 auto\u0026amp; tmpf2 = myfunc3; → 函数引用 20.5.3 auto 类型 std::initializer_list 的特殊推断 # [!tip] initializer_list 特殊推断\nauto x3 = {30}; → x3 为 std::initializer_list\u0026lt;int\u0026gt; auto x4{30}; → x4 为 int（无等号） ==只有 auto 有这种特殊推断，函数模板没有== 20.5.4 auto 不适用场合 # ! 普通成员变量不能是 auto 类型 ! static const auto 成员可以使用（必须在类内初始化） 20.5.5 auto 适用场合 # [!tip] auto 适用场合\n迭代器类型简化：for (auto iter = mymap.begin(); ...) 无法确定类型时：auto value = T::testr(); 20.6 详解 decltype 含义与 decltype 主要用途 # @ decltype图 ![[白板/P64 decltype与类型萃取.canvas|decltype与类型萃取]] 20.6.1 decltype 含义和举例 # [!tip] decltype 特点\n返回操作数的数据类型 发生在编译期，不真正计算表达式的值 ==不会抛弃 const、引用等属性== [!tip] decltype 后是变量\ndecltype(i) j2 = 15;（i 为 const int） → j2 为 const int decltype(iy) j3 = j2;（iy 为 const int\u0026amp;） → j3 为 const int\u0026amp; [!tip] decltype 后是表达式\ndecltype(i + 1) j; → j 为 int decltype(*pi) k3 = i; → k3 为 int\u0026amp;（能做左值的表达式返回引用） decltype((i)) iy3 = i; → iy3 为 int\u0026amp;（双层括号变成表达式） [!tip] decltype 后是函数\ndecltype(testf()) tmpv = 1; → tmpv 为 int（函数返回类型） decltype(testf) tmpv2; → tmpv2 为函数类型 20.6.2 decltype 主要用途 # [!tip] 应付可变类型\ntemplate\u0026lt;typename T\u0026gt; class CTTMP { public: decltype(T().begin()) iter; // 自动推断迭代器类型 }; [!tip] 返回类型后置语法\nauto add(int i, int k) -\u0026gt; decltype(i + k) { return i + k; } [!tip] decltype(auto) 用法（C++14）\n用于函数返回类型：保持引用属性 用于变量声明：捡回 auto 丢掉的 const、引用 template\u0026lt;typename T\u0026gt; decltype(auto) mydouble(T\u0026amp; vl) { vl *= 2; return vl; // 返回引用 } 20.7 可调用对象、std::function 与 std::bind # @ 可调用对象图 ![[白板/P61 函数对象与可调用对象.canvas|函数对象与可调用对象]] 20.7.1 可调用对象 # [!tip] 可调用对象分类\n函数指针：void(*paf)(int) = myfunc; 仿函数：重载 operator() 的类对象 可转换为函数指针的类对象：类型转换运算符 类成员函数指针：void(TC::*myfpoint)(int) = \u0026amp;TC::ptfunc; 20.7.2 std::function 可调用对象包装器 # [!tip] std::function 用法\n#include \u0026lt;functional\u0026gt; std::function\u0026lt;void(int)\u0026gt; f1 = myfunc; // 普通函数 std::function\u0026lt;int(int)\u0026gt; fs2 = TC::stcfunc; // 静态成员函数 std::function\u0026lt;void(int)\u0026gt; f3 = tc3; // 仿函数 ! std::function 不能包装类成员函数指针 20.7.3 std::bind 绑定器 # [!tip] std::bind 用法\n#include \u0026lt;functional\u0026gt; auto bf1 = std::bind(myfunc1, 10, 20, 30); // 绑定所有参数 bf1(); // 直接调用 auto bf2 = std::bind(myfunc1, placeholders::_1, placeholders::_2, 30); bf2(5, 15); // 部分参数在调用时指定 [!tip] std::bind 特性\n预先绑定的参数通过值传递 placeholders 传递的参数通过引用传递 可以绑定成员函数和成员变量 CQ cq; auto bf5 = std::bind(\u0026amp;CQ::myfunpt, \u0026amp;cq, placeholders::_1, placeholders::_2); bf5(10, 20); // 调用成员函数 std::function\u0026lt;int\u0026amp;(void)\u0026gt; bf7 = std::bind(\u0026amp;CQ::m_a, \u0026amp;cq); bf7() = 60; // 修改成员变量 20.8 lambda 表达式与 for_each、find_if 简介 # @ lambda表达式图 ![[白板/P61 函数对象与可调用对象.canvas|函数对象与可调用对象]] 20.8.1 用法简介 # [!tip] lambda 表达式一般形式\n[捕获列表](参数列表) -\u0026gt; 返回类型 { 函数体; };\n返回类型必须后置，可省略（编译器自动推导） 无参数时可省略 ( ) 捕获列表和函数体不能省略 auto f = [](int a) -\u0026gt; int { return a + 1; }; cout \u0026lt;\u0026lt; f(1) \u0026lt;\u0026lt; endl; // 2 auto f2 = [] { return 2; }; // 无参数，返回类型省略 20.8.2 捕获列表 # [!tip] 捕获列表规则\n捕获方式 含义 [] 不捕获任何变量 [\u0026amp;] 按引用捕获所有外部变量 [=] 按值捕获所有外部变量 [this] 捕获 this 指针 [变量名] 按值捕获指定变量 [\u0026amp;变量名] 按引用捕获指定变量 [=, \u0026amp;变量名] 默认按值，指定按引用 [\u0026amp;, 变量名] 默认按引用，指定按值 20.8.3 lambda 表达式延迟调用易出错细节 # [!tip] 按值捕获的陷阱\nint x = 5; auto f = [=] { return x; }; x = 10; cout \u0026lt;\u0026lt; f() \u0026lt;\u0026lt; endl; // 5，不是 10 ==按值捕获时，变量值在 lambda 定义时已复制== 想即时访问外部变量，用引用捕获 [\u0026amp;] 20.8.4 lambda 表达式中的 mutable # [!tip] mutable 关键字\n按值捕获的变量默认不能修改 mutable 允许修改按值捕获的变量副本 使用 mutable 时，( ) 不能省略 auto f = [=]() mutable { x = 6; return x; }; 20.8.5 lambda 表达式的类型和存储 # [!tip] 闭包类型\nlambda 表达式的类型称为闭包类型（Closure Type） 每个 lambda 触发编译器生成一个独一无二的类类型 可以用 std::function 和 std::bind 保存 [!tip] 捕获列表为空的 lambda\n可转换为普通函数指针 using functype = int(*)(int); functype fp = [](int tv) { return tv; }; 20.8.6 lambda 表达式再演示 # [!tip] for_each 中的 lambda\nvector\u0026lt;int\u0026gt; myvector {10, 20, 30, 40, 50}; int isum = 0; for_each(myvector.begin(), myvector.end(), [\u0026amp;isum](int val) { isum += val; }); [!tip] find_if 中的 lambda\nauto result = find_if(myvector.begin(), myvector.end(), [](int val) { return val \u0026gt; 15; }); 20.9 lambda 表达式捕获模式的陷阱分析 # 20.9.1 捕获列表中的 \u0026amp; # [!tip] 引用悬空问题\n按引用捕获会导致 lambda 包含绑定到局部变量的引用 局部变量超出作用域后，引用失效 ==产生未定义行为== 20.9.2 形参列表可以使用 auto（C++14） # [!tip] auto 形参\ngv.push_back([\u0026amp;, auto tv](int tv) { if (tv % tmpvalue == 0) return true; return false; }); 20.9.3 成员变量的捕获问题 # [!tip] 成员变量不能被捕获\n捕获只针对非静态局部变量 [=] 捕获的是 this 指针，不是成员变量 ==lambda 的生命周期依赖于对象的生命周期== [!tip] 解决方案\n将成员变量复制到局部变量 auto tmpvalueCopy = m_tmpvalue; gv.push_back([tmpvalueCopy](auto tv) { ... }); 20.9.4 广义 lambda 捕获（C++14） # [!tip] 广义捕获\ngv.push_back([tmpvalue = m_tmpvalue](auto tv) { // tmpvalue 是副本 }); 20.9.5 静态局部变量 # [!tip] 静态变量不捕获但可使用\n静态局部变量不能被捕获，但可在 lambda 中使用 ==类似按引用捕获的效果== 不同 lambda 在不同时机输出结果不同 20.10 可变参数函数、initializer_list 与省略号形参 # 20.10.1 可变参数函数 # [!tip] 可变参数函数\n参数数量不固定的函数 所有实参类型相同时用 initializer_list 类型不同时用省略号形参（C时代产物） 20.10.2 initializer_list（初始化列表） # [!tip] initializer_list 特点\nC++11 新类型，类模板 元素永远是常量值，不能改变 begin()、end() 遍历，size() 获取元素个数 复制和赋值不复制元素（共享元素） #include \u0026lt;initializer_list\u0026gt; void printvalue(initializer_list\u0026lt;string\u0026gt; tmpstr) { for (auto\u0026amp; tmpitem : tmpstr) { cout \u0026lt;\u0026lt; tmpitem \u0026lt;\u0026lt; endl; } } printvalue({\u0026#34;aa\u0026#34;, \u0026#34;bb\u0026#34;, \u0026#34;cc\u0026#34;}); [!tip] 统一初始化\nC++11 引入统一初始化（Uniform Initialization） 用 {} 初始化各种类型 背后由 std::initializer_list 支持 20.10.3 省略号形参 # [!tip] 省略号形参\nC时代产物，C++中用得较少 至少有一个有效形参 只能出现在形参列表最后 不进行类型检查 #include \u0026lt;cstdarg\u0026gt; double average(int num, ...) { va_list valist; va_start(valist, num); double sum = 0; for (int i = 0; i \u0026lt; num; ++i) { sum += va_arg(valist, int); } va_end(valist); return sum / num; } ! 不建议在 C++ 中使用省略号形参 20.11 萃取技术概念与范例 # @ 类型萃取图 ![[白板/P64 decltype与类型萃取.canvas|decltype与类型萃取]] 20.11.1 类型萃取简介 # [!tip] 类型萃取（type traits）\n泛型编程技术，在 STL 实现源码中用得多 编译期间的基于模板的接口 用于查询或修改类型的属性 C++11 标准库提供很多类型萃取接口（类模板） 20.11.2 类型萃取范例 # [!tip] 常用萃取接口\nis_void\u0026lt;T\u0026gt;::value：是否是 void is_class\u0026lt;T\u0026gt;::value：是否是类 is_object\u0026lt;T\u0026gt;::value：是否是对象类型 is_pod\u0026lt;T\u0026gt;::value：是否是 POD 类型 is_default_constructible\u0026lt;T\u0026gt;::value：是否有默认构造函数 is_copy_constructible\u0026lt;T\u0026gt;::value：是否有拷贝构造函数 is_move_constructible\u0026lt;T\u0026gt;::value：是否有移动构造函数 has_virtual_destructor\u0026lt;T\u0026gt;::value：是否有虚析构函数 #include \u0026lt;type_traits\u0026gt; cout \u0026lt;\u0026lt; is_void\u0026lt;int\u0026gt;::value \u0026lt;\u0026lt; endl; // 0 cout \u0026lt;\u0026lt; is_class\u0026lt;string\u0026gt;::value \u0026lt;\u0026lt; endl; // 1 cout \u0026lt;\u0026lt; is_pod\u0026lt;int\u0026gt;::value \u0026lt;\u0026lt; endl; // 1 20.11.3 迭代器萃取简介 # [!tip] iterator_traits\n给定迭代器，萃取出迭代器的种类 用于算法内部根据迭代器类型优化处理 20.11.4 总结 # [!tip] 萃取技术总结\n属于泛型编程技术 STL 实现源码中用得多 实际工作中应用较少（业务逻辑编码为主） 能实现一些意想不到的效果 ","externalUrl":null,"permalink":"/notes/c-cpp/c/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/p19-%E9%AB%98%E7%BA%A7%E8%AF%9D%E9%A2%98%E4%B8%8E%E6%96%B0%E6%A0%87%E5%87%86/","section":"S","summary":"\u003ch1 class=\"relative group\"\u003e知识点\n    \u003cdiv id=\"知识点\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%9f%a5%e8%af%86%e7%82%b9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e20.1 函数调用运算符与 function 类模板\n    \u003cdiv id=\"201-函数调用运算符与-function-类模板\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#201-%e5%87%bd%e6%95%b0%e8%b0%83%e7%94%a8%e8%bf%90%e7%ae%97%e7%ac%a6%e4%b8%8e-function-%e7%b1%bb%e6%a8%a1%e6%9d%bf\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e@ 函数对象图 ![[白板/P61 函数对象与可调用对象.canvas|函数对象与可调用对象]]\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch3 class=\"relative group\"\u003e20.1.1 函数调用运算符\n    \u003cdiv id=\"2011-函数调用运算符\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#2011-%e5%87%bd%e6%95%b0%e8%b0%83%e7%94%a8%e8%bf%90%e7%ae%97%e7%ac%a6\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!tip] 函数调用运算符\u003c/p\u003e","title":"P19 高级话题与新标准","type":"notes"},{"content":"C++ 现代编程指南。\n","externalUrl":null,"permalink":"/notes/c-cpp/cpp/","section":"S","summary":"\u003cp\u003eC++ 现代编程指南。\u003c/p\u003e","title":"C++","type":"notes"},{"content":"这是MCU相关的技术笔记分类。\n","externalUrl":null,"permalink":"/notes/mcu/","section":"S","summary":"\u003cp\u003e这是MCU相关的技术笔记分类。\u003c/p\u003e","title":"MCU","type":"notes"},{"content":"这是RTOS相关的技术笔记分类。\n","externalUrl":null,"permalink":"/notes/rtos/","section":"S","summary":"\u003cp\u003e这是RTOS相关的技术笔记分类。\u003c/p\u003e","title":"RTOS","type":"notes"},{"content":"数据结构与算法学习笔记。\n","externalUrl":null,"permalink":"/notes/algorithms/","section":"S","summary":"\u003cp\u003e数据结构与算法学习笔记。\u003c/p\u003e","title":"数据结构与算法","type":"notes"},{"content":"","externalUrl":null,"permalink":"/en/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/tags/auto/","section":"Tags","summary":"","title":"Auto","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/","section":"Blowfish","summary":"","title":"Blowfish","type":"page"},{"content":"","externalUrl":null,"permalink":"/series/c-%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E7%AC%94%E8%AE%B0/","section":"Series","summary":"","title":"C 语言程序设计笔记","type":"series"},{"content":"","externalUrl":null,"permalink":"/categories/c/c++/","section":"Categories","summary":"","title":"C/C++","type":"categories"},{"content":"","externalUrl":null,"permalink":"/tags/c++/","section":"Tags","summary":"","title":"C++","type":"tags"},{"content":"","externalUrl":null,"permalink":"/series/c++-%E6%96%B0%E7%BB%8F%E5%85%B8%E7%AC%94%E8%AE%B0/","section":"Series","summary":"","title":"C++ 新经典笔记","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/c++11/","section":"Tags","summary":"","title":"C++11","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/c++%E6%96%B0%E7%BB%8F%E5%85%B8/","section":"Tags","summary":"","title":"C++新经典","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/tags/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/","section":"Tags","summary":"","title":"C语言程序设计","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/decltype/","section":"Tags","summary":"","title":"Decltype","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/fopen/","section":"Tags","summary":"","title":"Fopen","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/fread/","section":"Tags","summary":"","title":"Fread","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/fwrite/","section":"Tags","summary":"","title":"Fwrite","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/lambda/","section":"Tags","summary":"","title":"Lambda","type":"tags"},{"content":"这是技术笔记的主页，包含所有技术笔记的分类。\n","externalUrl":null,"permalink":"/notes/","section":"S","summary":"\u003cp\u003e这是技术笔记的主页，包含所有技术笔记的分类。\u003c/p\u003e","title":"S","type":"notes"},{"content":"","externalUrl":null,"permalink":"/en/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/shared_ptr/","section":"Tags","summary":"","title":"Shared_ptr","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/stl/","section":"Tags","summary":"","title":"STL","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/string/","section":"Tags","summary":"","title":"String","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/typedef/","section":"Tags","summary":"","title":"Typedef","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/unique_ptr/","section":"Tags","summary":"","title":"Unique_ptr","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/vector/","section":"Tags","summary":"","title":"Vector","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/weak_ptr/","section":"Tags","summary":"","title":"Weak_ptr","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E4%BD%8D%E8%BF%90%E7%AE%97/","section":"Tags","summary":"","title":"位运算","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%85%B1%E7%94%A8%E4%BD%93/","section":"Tags","summary":"","title":"共用体","type":"tags"},{"content":" 关于我 # 个人简介 # 我是一名大三学生，主修电子工程专业，专注于嵌入式系统开发方向。自大学以来，我就对嵌入式系统产生了浓厚的兴趣，并投入大量时间学习和实践相关技术。\n教育背景 # 大学: XX大学 专业: 电子工程 年级: 大三 预计毕业时间: 2025年6月 技能概览 # 编程语言 # C/C++ (熟练) Python (基础) JavaScript (基础) 嵌入式平台 # STM32系列微控制器 ESP32系列微控制器 TI MSP430系列微控制器 实时操作系统 # FreeRTOS RT-Thread 开发工具 # Keil MDK STM32CubeIDE PlatformIO VS Code Git 硬件设计 # 电路设计基础 PCB设计基础 传感器应用 通信协议 (UART, SPI, I2C, USB, WiFi, Bluetooth) 项目经验 # 智能家居控制系统 # 基于STM32和ESP32的智能家居控制系统，实现了温湿度监测、灯光控制和远程控制功能。\n低功耗环境监测器 # 基于MSP430的超低功耗环境监测设备，可连续工作数月，支持温度、湿度、气压监测。\n物联网网关 # 基于ESP32的物联网网关，支持多种通信协议，可连接多种传感器并上传数据到云端。\n职业目标 # 我的职业目标是成为一名专业的嵌入式软件工程师，在物联网、智能硬件或汽车电子领域发展。我希望能够在实际项目中应用所学知识，不断学习新技术，提升自己的专业能力。\n联系方式 # 邮箱: your-email@example.com GitHub: github.com/yourusername LinkedIn: linkedin.com/in/yourname ","externalUrl":null,"permalink":"/about/","section":"网站主页","summary":"\u003ch1 class=\"relative group\"\u003e关于我\n    \u003cdiv id=\"关于我\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e5%85%b3%e4%ba%8e%e6%88%91\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\n\u003ch2 class=\"relative group\"\u003e个人简介\n    \u003cdiv id=\"个人简介\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e4%b8%aa%e4%ba%ba%e7%ae%80%e4%bb%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cp\u003e我是一名大三学生，主修电子工程专业，专注于嵌入式系统开发方向。自大学以来，我就对嵌入式系统产生了浓厚的兴趣，并投入大量时间学习和实践相关技术。\u003c/p\u003e","title":"关于我","type":"page"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%86%85%E5%AD%98%E6%B1%A0/","section":"Tags","summary":"","title":"内存池","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/","section":"Tags","summary":"","title":"内存管理","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%87%BD%E6%95%B0/","section":"Tags","summary":"","title":"函数","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88/","section":"Tags","summary":"","title":"函数指针","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%87%BD%E6%95%B0%E6%A8%A1%E6%9D%BF/","section":"Tags","summary":"","title":"函数模板","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%8F%98%E9%87%8F%E4%BD%9C%E7%94%A8%E5%9F%9F/","section":"Tags","summary":"","title":"变量作用域","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4/","section":"Tags","summary":"","title":"命名空间","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%9C%B0%E5%9D%80/","section":"Tags","summary":"","title":"地址","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%9F%BA%E7%A1%80/","section":"Tags","summary":"","title":"基础","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%A4%9A%E6%80%81/","section":"Tags","summary":"","title":"多态","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/","section":"Tags","summary":"","title":"多线程","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"Tags","summary":"","title":"字符串","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%AD%97%E7%AC%A6%E6%95%B0%E7%BB%84/","section":"Tags","summary":"","title":"字符数组","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%AE%8F%E5%AE%9A%E4%B9%89/","section":"Tags","summary":"","title":"宏定义","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%AE%B9%E5%99%A8/","section":"Tags","summary":"","title":"容器","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%B9%B6%E5%8F%91/","section":"Tags","summary":"","title":"并发","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%BC%95%E7%94%A8/","section":"Tags","summary":"","title":"引用","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E5%BE%AA%E7%8E%AF/","section":"Tags","summary":"","title":"循环","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%8C%87%E9%92%88/","section":"Tags","summary":"","title":"指针","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%8C%89%E4%BD%8D%E4%B8%8E/","section":"Tags","summary":"","title":"按位与","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%8C%89%E4%BD%8D%E6%88%96/","section":"Tags","summary":"","title":"按位或","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%8E%A7%E5%88%B6%E8%AF%AD%E5%8F%A5/","section":"Tags","summary":"","title":"控制语句","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%95%B0%E7%BB%84/","section":"Tags","summary":"","title":"数组","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%95%B0%E7%BB%84%E6%8C%87%E9%92%88/","section":"Tags","summary":"","title":"数组指针","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C/","section":"Tags","summary":"","title":"文件操作","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/","section":"Tags","summary":"","title":"智能指针","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%9D%A1%E4%BB%B6%E5%88%A4%E6%96%AD/","section":"Tags","summary":"","title":"条件判断","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%9D%A1%E4%BB%B6%E7%BC%96%E8%AF%91/","section":"Tags","summary":"","title":"条件编译","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0/","section":"Tags","summary":"","title":"构造函数","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0/","section":"Tags","summary":"","title":"析构函数","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%9E%9A%E4%B8%BE/","section":"Tags","summary":"","title":"枚举","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%A8%A1%E6%9D%BF/","section":"Tags","summary":"","title":"模板","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%B3%9B%E5%9E%8B%E7%BC%96%E7%A8%8B/","section":"Tags","summary":"","title":"泛型编程","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%89%B9%E5%8C%96/","section":"Tags","summary":"","title":"特化","type":"tags"},{"content":" 示例页面 # 这是网站的示例页面区域。\n","externalUrl":null,"permalink":"/examples/","section":"示例","summary":"\u003ch1 class=\"relative group\"\u003e示例页面\n    \u003cdiv id=\"示例页面\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e7%a4%ba%e4%be%8b%e9%a1%b5%e9%9d%a2\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\u003cp\u003e这是网站的示例页面区域。\u003c/p\u003e","title":"示例","type":"examples"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%A7%BB%E4%BD%8D/","section":"Tags","summary":"","title":"移位","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%AE%97%E6%B3%95/","section":"Tags","summary":"","title":"算法","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%B1%BB/","section":"Tags","summary":"","title":"类","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%B1%BB%E6%A8%A1%E6%9D%BF/","section":"Tags","summary":"","title":"类模板","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%BB%93%E6%9E%84%E4%BD%93/","section":"Tags","summary":"","title":"结构体","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%BB%A7%E6%89%BF/","section":"Tags","summary":"","title":"继承","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E7%BC%96%E8%AF%91%E9%A2%84%E5%A4%84%E7%90%86/","section":"Tags","summary":"","title":"编译预处理","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA/","section":"Tags","summary":"","title":"输入输出","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E8%BF%AD%E4%BB%A3%E5%99%A8/","section":"Tags","summary":"","title":"迭代器","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E9%80%92%E5%BD%92/","section":"Tags","summary":"","title":"递归","type":"tags"},{"content":"","externalUrl":null,"permalink":"/tags/%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97/","section":"Tags","summary":"","title":"逻辑运算","type":"tags"},{"content":" 隐私政策 # 最后更新时间：2025年11月7日\n引言 # 本隐私政策描述了当您访问本网站时，我们如何收集、使用和共享您的个人信息。\n我们收集的信息 # 当您访问本网站时，我们会自动收集有关您的设备的某些信息，包括有关您的Web浏览器、IP地址、时区以及安装在您设备上的某些Cookie的信息。此外，当您浏览本网站时，我们会收集有关您查看的各个网页或产品的信息，哪些网站或搜索词将您引荐到本网站，以及有关您如何与本网站互动的信息。我们将这种自动收集的信息称为\u0026quot;设备信息\u0026quot;。\n我们使用以下技术收集设备信息：\nCookies：放置在您的设备或计算机上的数据文件，通常包含匿名的唯一标识符。有关Cookie的更多信息以及如何禁用Cookie，请访问http://www.allaboutcookies.org。 日志文件：跟踪在本网站上发生的操作，并收集数据，包括您的IP地址、浏览器类型、互联网服务提供商、引荐/退出页面以及日期/时间戳。 网络信标、标签和像素：用于记录有关您如何浏览本网站的电子文件。 此外，当您通过本网站进行购买或尝试进行购买时，我们会从您那里收集某些信息，包括您的姓名、账单地址、送货地址、付款信息（包括信用卡号码）、电子邮件地址和电话号码。我们将此信息称为\u0026quot;订单信息\u0026quot;。\n在本隐私政策中，当我们谈论\u0026quot;个人信息\u0026quot;时，我们指的是设备信息和订单信息。\n我们如何使用您的个人信息 # 我们收集的订单信息通常用于履行通过本网站下达的订单（包括处理您的付款信息、安排发货以及向您提供发票和/或订单确认）。此外，我们使用此订单信息：\n与您沟通； 筛选我们的订单以防止潜在风险或欺诈；以及 与您分享的偏好一致，为您提供有关我们的产品或服务的信息或广告。 我们使用收集的设备信息来帮助我们筛选潜在风险和欺诈（特别是您的IP地址），并更普遍地改进和优化我们的网站（例如，通过生成有关客户如何浏览和与网站互动的分析，以及评估我们营销和广告活动的成功）。\n共享您的个人信息 # 我们将您的个人信息与第三方共享，以帮助我们使用您的个人信息，如上所述。例如，我们使用Google Analytics来帮助我们了解客户如何使用本网站\u0026ndash;您可以在此处阅读更多关于Google如何使用您的个人信息的信息：https://www.google.com/intl/en/policies/privacy/。您也可以在此处选择退出Google Analytics：https://tools.google.com/dlpage/gaoptout。\n最后，我们还可能共享您的个人信息以遵守适用的法律法规、响应我们收到的传票、搜查令或其他合法请求，或以其他方式保护我们的权利。\n不跟踪 # 请注意，当我们在您的浏览器中看到\u0026quot;不跟踪\u0026quot;信号时，我们不会改变我们收集和使用数据的做法。\n您的权利 # 如果您是欧洲居民，您有权访问我们持有的有关您的个人信息，并要求更正、更新或删除您的个人信息。如果您希望行使此权利，请通过以下联系方式与我们联系。\n此外，如果您是欧洲居民，我们注意到我们正在处理您的信息，以便履行我们可能与您签订的合同（例如，如果您通过本网站下单），或者以其他方式追求我们在上述列出的合法商业利益。此外，请注意，您的信息将被传输到欧洲以外的地区，包括加拿大和美国。\n数据保留 # 当您通过本网站下单时，我们将保留您的订单信息以备记录，除非您要求我们删除此信息。\n变更 # 我们可能会不时更新此隐私政策，以反映例如我们实践的变化或其他运营、法律或监管原因。\n联系我们 # 如需了解更多关于我们隐私实践的信息，如果您有任何问题，或如果您想提出投诉，请通过developer@example.com与我们联系。\n","externalUrl":null,"permalink":"/privacy-policy/","section":"网站主页","summary":"\u003ch1 class=\"relative group\"\u003e隐私政策\n    \u003cdiv id=\"隐私政策\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%9a%90%e7%a7%81%e6%94%bf%e7%ad%96\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\u003cp\u003e最后更新时间：2025年11月7日\u003c/p\u003e","title":"隐私政策","type":"page"},{"content":"","externalUrl":null,"permalink":"/tags/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/","section":"Tags","summary":"","title":"面向对象","type":"tags"},{"content":" 404 - 页面未找到 # 抱歉，您访问的页面不存在。\n您可能想要查看：\n首页 项目展示 技术文章 关于我 ","externalUrl":null,"permalink":"/404/","section":"网站主页","summary":"\u003ch1 class=\"relative group\"\u003e404 - 页面未找到\n    \u003cdiv id=\"404---页面未找到\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#404---%e9%a1%b5%e9%9d%a2%e6%9c%aa%e6%89%be%e5%88%b0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\u003cp\u003e抱歉，您访问的页面不存在。\u003c/p\u003e","title":"页面未找到","type":"page"},{"content":" 项目展示 # 以下是我在嵌入式系统开发领域的一些项目实践。\n智能家居控制系统 # 基于STM32和ESP32的智能家居控制系统，实现了温湿度监测、灯光控制和远程控制功能。\n技术栈: STM32, ESP32, FreeRTOS, MQTT\n项目亮点:\n实现了本地和远程控制的无缝切换 采用低功耗设计，延长设备使用寿命 支持多种传感器接入 查看项目详情\n低功耗环境监测器 # 基于MSP430的超低功耗环境监测设备，可连续工作数月，支持温度、湿度、气压监测。\n技术栈: MSP430, 传感器技术, 低功耗设计\n项目亮点:\n超低功耗设计，一颗电池可使用数月 高精度传感器数据采集 支持数据本地存储和远程传输 查看项目详情\n物联网网关 # 基于ESP32的物联网网关，支持多种通信协议，可连接多种传感器并上传数据到云端。\n技术栈: ESP32, WiFi, TCP/IP, 云平台集成\n项目亮点:\n支持多种通信协议的设备接入 稳定的数据传输机制 云端数据可视化展示 查看项目详情\n","externalUrl":null,"permalink":"/projects/","section":"网站主页","summary":"\u003ch1 class=\"relative group\"\u003e项目展示\n    \u003cdiv id=\"项目展示\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#%e9%a1%b9%e7%9b%ae%e5%b1%95%e7%a4%ba\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\u003cp\u003e以下是我在嵌入式系统开发领域的一些项目实践。\u003c/p\u003e","title":"项目展示","type":"page"},{"content":"","externalUrl":null,"permalink":"/tags/%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7/","section":"Tags","summary":"","title":"高级特性","type":"tags"}]