From 06683ea94d81944d46b3e52cde431210d63261d0 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:00:23 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E5=AF=B9C=5Ftutorials=20=E7=AC=AC?= =?UTF-8?q?=E5=85=AD=E7=AB=A0=E7=9A=84=E7=8A=B6=E6=80=81=E6=9C=BA=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E4=BB=A3=E7=A0=81=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../c_tutorials/04-control-flow.md | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index 21823228f..77e94838d 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -326,19 +326,19 @@ error_hardware: ```c typedef enum { - kStateIdle, - kStateHeader, - kStatePayload, - kStateChecksum, - kStateDone, - kStateError + kStateIdle, // 空闲态:等待帧头 0xAA + kStateHeader, // 帧头态:准备接收长度 + kStatePayload, // 负载态:正在接收数据 + kStateChecksum, // 校验态:准备校验数据 + kStateDone, // 完成态:一帧解析成功 + kStateError // 错误态:数据不对(比如长度超限或校验失败) } ParseState; typedef struct { - ParseState state; - unsigned char payload[64]; - unsigned char payload_len; - unsigned char index; + ParseState state; // 记录当前状态 + unsigned char payload[64]; // 仓库:存放接收到的负载数据 + unsigned char payload_len; // 记录:这帧数据应该有多少个负载 + unsigned char index; // 计数器:当前已经收到了几个负载 } Parser; void parser_init(Parser* p) { @@ -350,42 +350,42 @@ void parser_init(Parser* p) { ParseState parser_feed(Parser* p, unsigned char byte) { switch (p->state) { case kStateIdle: - if (byte == 0xAA) { - p->state = kStateHeader; + if (byte == 0xAA) { // 看到帧头了吗? + p->state = kStateHeader; // 看到了,进入下一个状态(等长度) } break; case kStateHeader: - p->payload_len = byte; - if (p->payload_len > 64) { - p->state = kStateError; + p->payload_len = byte; // 把收到的这个字节当作长度存起来 + if (p->payload_len > 64) { // 长度太大了,仓库装不下怎么办? + p->state = kStateError; // 报错! } else { - p->index = 0; - p->state = kStatePayload; + p->index = 0; // 准备开始收数据,计数器清零 + p->state = kStatePayload; // 进入接收负载的状态 } break; case kStatePayload: - p->payload[p->index++] = byte; - if (p->index >= p->payload_len) { - p->state = kStateChecksum; + p->payload[p->index++] = byte; // 把字节存进仓库,并且计数器+1 + if (p->index >= p->payload_len) { // 收够了吗? + p->state = kStateChecksum; // 收够了,进入校验状态 } break; case kStateChecksum: { unsigned char calc = 0; for (int i = 0; i < p->payload_len; i++) { - calc ^= p->payload[i]; + calc ^= p->payload[i]; // 把收到的所有数据按位异或一遍 } - p->state = (calc == byte) ? kStateDone : kStateError; + p->state = (calc == byte) ? kStateDone : kStateError; // 算出来的和收到的校验码对比 break; } case kStateDone: case kStateError: - break; + break; // 什么都不做 } - return p->state; + return p->state; // 把当前状态告诉外面的人 } ``` @@ -402,17 +402,17 @@ int main(void) // 帧头 0xAA,长度 3,负载 {0x01, 0x02, 0x03},校验 0x00 unsigned char frame[] = {0xAA, 0x03, 0x01, 0x02, 0x03, 0x00}; for (int i = 0; i < (int)sizeof(frame); i++) { + // 把数组里的字节,一个一个喂给解析器 ParseState s = parser_feed(&p, frame[i]); printf("Byte 0x%02X → State %d\n", frame[i], s); + if (s == kStateDone) { + // 如果解析器说“完成”,就把收到的数据打印出来 printf("Frame OK, payload: "); - for (int j = 0; j < p.payload_len; j++) { - printf("0x%02X ", p.payload[j]); - } - printf("\n"); - break; + // ... + break; // 结束循环 } else if (s == kStateError) { - printf("Parse error at byte %d\n", i); + // 如果解析器报错,也停止 break; } } From bce6334e4697f884c7ca6c3eda77555b9d5240a6 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:04:13 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E5=AF=B9C=5Ftutorials=20=E7=AC=AC?= =?UTF-8?q?=E5=85=AD=E7=AB=A0=E7=9A=84=E7=8A=B6=E6=80=81=E6=9C=BA=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E4=BB=A3=E7=A0=81=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=EF=BC=88=E4=BF=AE=E6=AD=A3=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vol1-fundamentals/c_tutorials/04-control-flow.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index 77e94838d..2f224ea2c 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -402,17 +402,19 @@ int main(void) // 帧头 0xAA,长度 3,负载 {0x01, 0x02, 0x03},校验 0x00 unsigned char frame[] = {0xAA, 0x03, 0x01, 0x02, 0x03, 0x00}; for (int i = 0; i < (int)sizeof(frame); i++) { - // 把数组里的字节,一个一个喂给解析器 ParseState s = parser_feed(&p, frame[i]); printf("Byte 0x%02X → State %d\n", frame[i], s); - if (s == kStateDone) { // 如果解析器说“完成”,就把收到的数据打印出来 printf("Frame OK, payload: "); - // ... - break; // 结束循环 + for (int j = 0; j < p.payload_len; j++) { + printf("0x%02X ", p.payload[j]); + } + printf("\n"); + break; } else if (s == kStateError) { // 如果解析器报错,也停止 + printf("Parse error at byte %d\n", i); break; } } From 0a85376bbf94a515cacffbeb305139f5fe97d1c1 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 13 Jul 2026 19:08:51 +0800 Subject: [PATCH 03/15] docs(vol1): refine state machine comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 追加到 PR #104 的 review 修正,纯注释、零逻辑改动: - payload_len / index: 把「几个负载」改成「几个字节」, 代码实际含义如此,负载更像是完整的Payload,跟代码还是具备实际差距 - kStateHeader: 修正为「帧头已收完,接下来等长度字节」, - 顺手修了结构体注释的对齐(payload[64]) --- .../vol1-fundamentals/c_tutorials/04-control-flow.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index 2f224ea2c..24fe95639 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -327,7 +327,7 @@ error_hardware: ```c typedef enum { kStateIdle, // 空闲态:等待帧头 0xAA - kStateHeader, // 帧头态:准备接收长度 + kStateHeader, // 帧头态:帧头已收完,接下来等长度字节 kStatePayload, // 负载态:正在接收数据 kStateChecksum, // 校验态:准备校验数据 kStateDone, // 完成态:一帧解析成功 @@ -335,10 +335,10 @@ typedef enum { } ParseState; typedef struct { - ParseState state; // 记录当前状态 - unsigned char payload[64]; // 仓库:存放接收到的负载数据 - unsigned char payload_len; // 记录:这帧数据应该有多少个负载 - unsigned char index; // 计数器:当前已经收到了几个负载 + ParseState state; // 记录当前状态 + unsigned char payload[64]; // 仓库:存放接收到的负载数据 + unsigned char payload_len; // 记录:这帧要收多少个字节的负载 + unsigned char index; // 计数器:当前已经收到了几个字节 } Parser; void parser_init(Parser* p) { From d180606d7ff98ba08bfb1aeb6c04a0d067102a78 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:05:27 +0800 Subject: [PATCH 04/15] =?UTF-8?q?=E5=AF=B9C=5Ftutorials=20=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E7=AB=A0=E7=9A=84=E7=BB=83=E4=B9=A0=E9=A2=98=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E5=8F=82=E8=80=83=E7=AD=94=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added example solutions for exercises on data types and overflow in C. --- .../c_tutorials/02A-data-types-basics.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md b/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md index a868de482..1ad55b3fa 100644 --- a/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md @@ -284,6 +284,50 @@ uint8_t z{1000}; // C++ 编译错误!1000 超出 uint8_t 范围 提示:可以用一个宏来减少重复代码。 +###练习 1 参考答案 + +```c +#include +#include +//#include + +int main() { + // 基本类型 + printf("sizeof(char) = %zu bytes\n", sizeof(char)); + printf("sizeof(short) = %zu bytes\n", sizeof(short)); + printf("sizeof(int) = %zu bytes\n", sizeof(int)); + printf("sizeof(long) = %zu bytes\n", sizeof(long)); + printf("sizeof(long long) = %zu bytes\n", sizeof(long long)); + + // 定长整数类型 (需包含 ) + printf("sizeof(int8_t) = %zu bytes\n", sizeof(int8_t)); + printf("sizeof(uint8_t) = %zu bytes\n", sizeof(uint8_t)); + printf("sizeof(int32_t) = %zu bytes\n", sizeof(int32_t)); + printf("sizeof(uint32_t) = %zu bytes\n", sizeof(uint32_t)); + printf("sizeof(int64_t) = %zu bytes\n", sizeof(int64_t)); + + // size_t 类型 (需包含) + printf("sizeof(size_t) = %zu bytes\n", sizeof(size_t)); + + return 0; +} + +``` + +```终端输出 +sizeof(char) = 1 bytes +sizeof(short) = 2 bytes +sizeof(int) = 4 bytes +sizeof(long) = 4 bytes +sizeof(long long) = 8 bytes +sizeof(int8_t) = 1 bytes +sizeof(uint8_t) = 1 bytes +sizeof(int32_t) = 4 bytes +sizeof(uint32_t) = 4 bytes +sizeof(int64_t) = 8 bytes +sizeof(size_t) = 8 bytes +``` + ### 练习 2:溢出观察 分别对有符号 `int` 和无符号 `unsigned int` 做溢出实验: @@ -306,6 +350,28 @@ int main(void) 编译运行,观察两者的行为差异。然后加上 `-fsanitize=undefined` 选项重新编译,看看有什么变化。 +### 练习 2 参考答案 + +假设该文件名为overflow.c + +使用 gcc overflow.c -o overflow && ./overflow 编译运行后,你大概率会看到如下输出: + +```终端输出 +INT_MAX = 2147483647, INT_MAX + 1 = -2147483648 +UINT_MAX = 4294967295, UINT_MAX + 1 = 0 +``` + +使用 gcc -fsanitize=undefined overflow.c -o overflow_ubsan && ./overflow_ubsan 编译运行后,你会看到类似如下的输出: + +```终端输出 +INT_MAX = 2147483647, INT_MAX + 1 = -2147483648 +overflow.c:9:54: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' +UINT_MAX = 4294967295, UINT_MAX + 1 = 0 +``` +为什么? +实际上 C 标准其实并没有对带有符号的整数的溢出进行定义,也就是说,对INT_MAX进行+1这个操作严格意义上是一个未定义行为。 +(只不过溢出很好用,也是大部分编译器都默认支持译出的。) + ## 参考资源 - [cppreference: C 语言整型](https://en.cppreference.com/w/c/language/integer_constant) From bb7585e798925876a9c6c15d6fb783dd0a67d9e3 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:08:33 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E5=AF=B9C=5Ftutorials=20=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E7=AB=A0=E7=9A=84=E7=BB=83=E4=B9=A0=E9=A2=98=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E5=8F=82=E8=80=83=E7=AD=94=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../c_tutorials/02B-float-char-const-cast.md | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md index 1c34a3cb3..fa121e7ea 100644 --- a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md +++ b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md @@ -316,17 +316,41 @@ C++ 在类型系统上做了大量的安全加固,很多改进直接瞄准了 int main(void) { - double a = 0.1; - double b = 0.2; - double c = 0.3; - - printf("a + b == c? %s\n", (a + b == c) ? "yes" : "no"); - printf("a + b = %.20f\n", a + b); - printf("c = %.20f\n", c); + double a_double = 0.1; + double b_double = 0.2; + double c_double = 0.3; + float a_float = 0.1f; + float b_float = 0.2f; + float c_float = 0.3f; + float e_float = 0.3f; + float f_float = 0.4f; + float g_float = 0.7f; + + printf("a_double + b_double == c_double? %s\n", (a_double + b_double == c_double) ? "yes" : "no"); + printf("a_float + b_float == c_float? %s\n", (a_float + b_float == c_float) ? "yes" : "no"); + printf("e_float + f_float == g_float? %s\n", (e_float + f_float == g_float) ? "yes" : "no"); + printf("a_double + b_double = %.20f\n", 0.1 + 0.2); + printf("a_float + b_float = %.20f\n", 0.1f + 0.2f); + printf("e_float + f_float = %.20f\n", 0.3f + 0.4f); + printf("c_double = %.20f\n", c_double); + printf("c_float = %.20f\n", c_float); + printf("g_float = %.20f\n", g_float); return 0; } ``` +```终端输出 +a_double + b_double == c_double? no +a_float + b_float == c_float? yes +e_float + f_float == g_float? no +a_double + b_double = 0.30000000000000004441 //0.1 + 0.2 +a_float + b_float = 0.30000001192092895508 //0.1f + 0.2f +e_float + f_float = 0.70000004768371582031 //0.3f + 0.4f +c_double = 0.29999999999999998890 //0.3 +c_float = 0.30000001192092895508 //0.3f +g_float = 0.69999998807907104492 //0.7f +``` + 修改代码使用 epsilon 比较来得到正确的结果。 ### 练习 2:隐式转换陷阱 @@ -345,6 +369,30 @@ if (target < sizeof(values) / sizeof(values[0])) { 提示:`sizeof` 返回的是什么类型? + +### 练习 2 参考答案 + +```c +int values[] = {1, 2, 3, 4, 5}; +int target = -1; + +// bug 就在下面这行 +if (target < (int)sizeof(values) / (int)sizeof(values[0])) { + printf("target is in range\n"); +} +``` +或者 + +```c +int values[] = {1, 2, 3, 4, 5}; +int target = -1; + +// bug 就在下面这行 +if (target < (int)(sizeof(values) / sizeof(values[0]))) { + printf("target is in range\n"); +} +``` + ### 练习 3:const 实战 写一个函数,接收一个字符串,统计其中某个字符出现的次数。函数签名中正确使用 `const`: @@ -357,6 +405,23 @@ if (target < sizeof(values) / sizeof(values[0])) { size_t count_char(const char* str, char ch); ``` +### 练习 3 参考答案 + +```c +size_t count_char(const char* str, char ch) { + if (str == NULL) { // 警惕空指针 + return 0; + } + size_t count = 0; + for (;*str;str++) { + if (*str == ch) { + count++; + } + } + return count; +} +``` + ## 参考资源 - [cppreference: C 语言隐式转换](https://en.cppreference.com/w/c/language/conversion) From 238fabfde6fac7c34674bdbac3ce2a0535f7dbf5 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 15 Jul 2026 08:02:16 +0800 Subject: [PATCH 06/15] fix: lint issue --- .../c_tutorials/02A-data-types-basics.md | 13 ++++++++----- .../c_tutorials/02B-float-char-const-cast.md | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md b/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md index 1ad55b3fa..a0e3229ef 100644 --- a/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md @@ -284,7 +284,7 @@ uint8_t z{1000}; // C++ 编译错误!1000 超出 uint8_t 范围 提示:可以用一个宏来减少重复代码。 -###练习 1 参考答案 +### 练习 1 参考答案 ```c #include @@ -314,7 +314,7 @@ int main() { ``` -```终端输出 +```text sizeof(char) = 1 bytes sizeof(short) = 2 bytes sizeof(int) = 4 bytes @@ -328,6 +328,8 @@ sizeof(int64_t) = 8 bytes sizeof(size_t) = 8 bytes ``` +注意 `sizeof(long)` 这里是 4,但 `sizeof(size_t)` 已经是 8 了,说明这份输出来自 LLP64 环境(比如 64 位 Windows):这种模型下指针 8 字节,`long` 却只有 4。换到 64 位的 Linux 或 macOS(LP64),`long` 就是 8 字节。你在自己机器上看到 `sizeof(long) = 8`,程序没写错,是数据模型的差别。 + ### 练习 2:溢出观察 分别对有符号 `int` 和无符号 `unsigned int` 做溢出实验: @@ -356,21 +358,22 @@ int main(void) 使用 gcc overflow.c -o overflow && ./overflow 编译运行后,你大概率会看到如下输出: -```终端输出 +```text INT_MAX = 2147483647, INT_MAX + 1 = -2147483648 UINT_MAX = 4294967295, UINT_MAX + 1 = 0 ``` 使用 gcc -fsanitize=undefined overflow.c -o overflow_ubsan && ./overflow_ubsan 编译运行后,你会看到类似如下的输出: -```终端输出 +```text INT_MAX = 2147483647, INT_MAX + 1 = -2147483648 overflow.c:9:54: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' UINT_MAX = 4294967295, UINT_MAX + 1 = 0 ``` + 为什么? 实际上 C 标准其实并没有对带有符号的整数的溢出进行定义,也就是说,对INT_MAX进行+1这个操作严格意义上是一个未定义行为。 -(只不过溢出很好用,也是大部分编译器都默认支持译出的。) +(只不过溢出很好用,也是大部分编译器都默认支持溢出的。) ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md index fa121e7ea..23396f882 100644 --- a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md +++ b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md @@ -325,7 +325,7 @@ int main(void) float e_float = 0.3f; float f_float = 0.4f; float g_float = 0.7f; - + printf("a_double + b_double == c_double? %s\n", (a_double + b_double == c_double) ? "yes" : "no"); printf("a_float + b_float == c_float? %s\n", (a_float + b_float == c_float) ? "yes" : "no"); printf("e_float + f_float == g_float? %s\n", (e_float + f_float == g_float) ? "yes" : "no"); @@ -339,7 +339,7 @@ int main(void) } ``` -```终端输出 +```text a_double + b_double == c_double? no a_float + b_float == c_float? yes e_float + f_float == g_float? no @@ -381,6 +381,7 @@ if (target < (int)sizeof(values) / (int)sizeof(values[0])) { printf("target is in range\n"); } ``` + 或者 ```c From 992c7c410972f05c21cedf19bdeeeab13a791e69 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:52:47 +0800 Subject: [PATCH 07/15] =?UTF-8?q?=E5=AF=B9=20C=20=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=95=99=E7=A8=8B=E7=AC=AC=E5=9B=9B=E7=AB=A0?= =?UTF-8?q?=E7=AC=AC=E4=BA=94=E7=AB=A0=E6=8F=90=E4=BE=9B=E4=BA=86=E5=8F=82?= =?UTF-8?q?=E8=80=83=E7=AD=94=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../c_tutorials/03A-operators-basics.md | 59 +++++++++++-- .../c_tutorials/03B-bitwise-and-evaluation.md | 88 +++++++++++++++---- 2 files changed, 124 insertions(+), 23 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md b/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md index 3cd2b0f50..068f4912e 100644 --- a/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 掌握 C 语言的算术运算符、自增自减、关系与逻辑运算符、条件运算符和逗号运算符,理解短路求值和赋值运算符的用法 difficulty: beginner order: 4 platform: host prerequisites: -- 浮点、字符、const 与类型转换 + - 浮点、字符、const 与类型转换 reading_time_minutes: 9 tags: -- host -- cpp-modern -- beginner -- 入门 -- 基础 + - host + - cpp-modern + - beginner + - 入门 + - 基础 title: 运算符基础:让数据动起来 --- + # 运算符基础:让数据动起来 上一篇里我们把 C 语言的数据类型从里到外拆了一遍——整数怎么存、小数怎么存、字符怎么存。但光有数据还不够,我们还得让数据"动起来":做加减乘除、比较大小、判断真假。这些操作在 C 语言里由**运算符**来完成。 @@ -35,7 +36,7 @@ title: 运算符基础:让数据动起来 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -266,6 +267,16 @@ printf("%d\n", 7 % 2); printf("%d\n", -7 % 2); ``` +### 练习 1 参考答案 + +```text + 3 // 7 / 2 得 3.5 ,正整型向下取整,得3 +-3 // -7 / 2 得 -3.5,负整型向上取整,得3 +-3 // 其实我觉得不论是正整形还是负整形,都是向0取整。比如:7 / -2 得 -3.5 , 然后向0取整,得-3 + 1 // 7 % 2 得 1 (这应该不用过多做解释吧) +-1 // -7 % 2 等于 -(7 % 2) 等于 -(1),所以,得:-7 % 2 等于 -1 +``` + ### 练习 2:短路求值实战 写一个函数,安全地从数组中找到第一个大于指定值的元素。利用短路求值确保不越界: @@ -279,6 +290,38 @@ printf("%d\n", -7 % 2); int find_first_above(const int* arr, size_t len, int threshold); ``` +### 练习 2 参考答案 + +```c +#include + +int find_first_above(const int* arr, size_t len, int threshold) { + // 边界情况:如果数组为空指针或长度为0,直接返回 -1 + if (arr == NULL || len == 0) { + return -1; + } + + size_t i = 0; + + // 核心逻辑:利用短路求值防止越界 + // 1. 首先检查 i < len + // 2. 只有当 i < len 为真时,才会去访问 arr[i] (避免越界读取) + // 3. 如果 arr[i] <= threshold,循环继续 + while (i < len && arr[i] <= threshold) { + i++; + } + + // 循环退出有两个原因: + // 1. 找到了大于 threshold 的元素 (此时 i < len) + // 2. 遍历完了数组也没找到 (此时 i == len) + if (i < len) { + return (int)i; + } else { + return -1; + } +} +``` + ## 参考资源 - [cppreference: C 语言运算符优先级](https://en.cppreference.com/w/c/language/operator_precedence) diff --git a/documents/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md b/documents/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md index bc794ab94..26c378a22 100644 --- a/documents/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md +++ b/documents/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md @@ -1,21 +1,22 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 深入位运算的四大操作、移位注意事项、运算符优先级陷阱、求值顺序与序列点,理解未定义行为的本质 difficulty: beginner order: 5 platform: host prerequisites: -- 运算符基础:让数据动起来 + - 运算符基础:让数据动起来 reading_time_minutes: 10 tags: -- host -- cpp-modern -- beginner -- 入门 + - host + - cpp-modern + - beginner + - 入门 title: 位运算与求值顺序 --- + # 位运算与求值顺序 上一篇里我们把算术、关系、逻辑这些常用运算符过了一遍。现在我们来啃两块比较硬的骨头:位运算和求值顺序。位运算在一般的应用层编程中用得不多,但如果你以后要接触嵌入式开发或者底层系统编程,位运算就是你的日常工具——配置硬件寄存器、解析通信协议的位字段、实现标志位集合,全靠它。求值顺序和序列点则是理解"为什么有些代码在不同编译器上结果不一样"的关键。 @@ -34,7 +35,7 @@ title: 位运算与求值顺序 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -46,14 +47,14 @@ title: 位运算与求值顺序 C 提供了六种位运算符: -| 运算符 | 含义 | 简单理解 | -|--------|------|---------| -| `&` | 按位与 | 两个都是 1 才得 1 | -| `\|` | 按位或 | 有一个 1 就得 1 | -| `^` | 按位异或 | 不同得 1,相同得 0 | -| `~` | 按位取反 | 0 变 1,1 变 0 | -| `<<` | 左移 | 所有位向左移动,低位补 0 | -| `>>` | 右移 | 所有位向右移动,高位补 0(无符号数) | +| 运算符 | 含义 | 简单理解 | +| ------ | -------- | ------------------------------------ | +| `&` | 按位与 | 两个都是 1 才得 1 | +| `\|` | 按位或 | 有一个 1 就得 1 | +| `^` | 按位异或 | 不同得 1,相同得 0 | +| `~` | 按位取反 | 0 变 1,1 变 0 | +| `<<` | 左移 | 所有位向左移动,低位补 0 | +| `>>` | 右移 | 所有位向右移动,高位补 0(无符号数) | 我们用 8 位无符号数来演示,这样比较直观: @@ -312,6 +313,39 @@ uint32_t bit_toggle(uint32_t value, int n); uint32_t bit_extract(uint32_t value, int high, int low); ``` +### 练习 1 的参考答案 + +```c + +/// @brief 将 value 的第 n 位置为 1 +uint32_t bit_set(uint32_t value, int n) { + value = value | (1U << n); + return value; +} + +/// @brief 将 value 的第 n 位清零 +uint32_t bit_clear(uint32_t value, int n) { + value = value & (~(1U << n)); + return value; +} + +/// @brief 翻转 value 的第 n 位 +uint32_t bit_toggle(uint32_t value, int n) { + value = value ^ (1U << n); + return value; +} + +/// @brief 提取 value 的 [high:low] 位域(包含两端) +uint32_t bit_extract(uint32_t value, int high, int low) { + uint32_t width = high - low + 1; + value = value >> low; + uint64_t mask = (1ULL << width) - 1; + value = value & mask; + return value; +} + +``` + ### 练习 2:安全的移位 写一个函数,安全地执行左移操作,处理所有边界情况: @@ -325,6 +359,20 @@ uint32_t bit_extract(uint32_t value, int high, int low); uint32_t safe_shift_left(uint32_t val, int n, int bits); ``` +### 练习 2 的参考答案 + +```c +uint32_t safe_shift_left(uint32_t val, int n, int bits) { + if (bits <= 0 || bits > 32) { + return 0; + } + if (n < 0 || n >= bits) { + return 0; + } + return val << n; +} +``` + ### 练习 3:表达式分析 分析以下表达式的求值行为(不实际运行),标出每个是"明确定义"、"未指定行为"还是"未定义行为": @@ -337,6 +385,16 @@ int r3 = (a > b) ? a-- : b--; // ? printf("%d %d\n", a++, a++); // ? ``` +### 练习 3 的参考答案 + +```c +int a = 5, b = 3; +int r1 = a++ + b; // 明确定义 +int r2 = a++ + ++a; // UB +int r3 = (a > b) ? a-- : b--; // 明确定义 +printf("%d %d\n", a++, a++); // UB +``` + ## 参考资源 - [cppreference: C 运算符优先级](https://en.cppreference.com/w/c/language/operator_precedence) From b9615d411cf24b950b05c22aae237b1c58c4e84c Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:00:31 +0800 Subject: [PATCH 08/15] =?UTF-8?q?C=20=E8=AF=AD=E8=A8=80=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=95=99=E7=A8=8B=E7=AC=AC=E4=B8=89=E7=AB=A0=E7=BB=83=E4=B9=A0?= =?UTF-8?q?=E9=A2=98=E7=AD=94=E6=A1=88=20bug=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搞忘把“### 练习 1 参考答案”打上去了awa 其他.md格式修正是 VS Code 插件自动修改的格式 --- .../c_tutorials/02B-float-char-const-cast.md | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md index 23396f882..c24d2a4ca 100644 --- a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md +++ b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 掌握 C 语言的浮点类型与精度问题、字符存储与编码、const 限定符和隐式类型转换规则,理解 C++ 类型安全设计的动机 difficulty: beginner order: 3 platform: host prerequisites: -- 数据类型基础:整数与内存 + - 数据类型基础:整数与内存 reading_time_minutes: 12 tags: -- host -- cpp-modern -- beginner -- 入门 -- 基础 + - host + - cpp-modern + - beginner + - 入门 + - 基础 title: 浮点、字符、const 与类型转换 --- + # 浮点、字符、const 与类型转换 上一篇里我们把整数家族从里到外拆了一遍——整型层级、有符号无符号、固定宽度类型和 sizeof。但程序世界里不只有整数:商品价格需要小数,屏幕上的文字需要字符,变量声明后有时候需要保护它不被乱改,不同类型的数据混在一起运算时编译器到底怎么处理。这些就是我们今天要一块一块啃的内容。 @@ -35,7 +36,7 @@ title: 浮点、字符、const 与类型转换 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -45,11 +46,11 @@ title: 浮点、字符、const 与类型转换 C 语言提供了三种浮点类型,按精度从小到大排列: -| 类型 | 典型位数 | 有效数字 | 字面量写法 | -|------|---------|---------|-----------| -| `float` | 32 位(单精度) | 约 7 位 | `3.14f` | -| `double` | 64 位(双精度) | 约 15 位 | `3.14`(默认) | -| `long double` | 80 或 128 位 | 平台相关 | `3.14L` | +| 类型 | 典型位数 | 有效数字 | 字面量写法 | +| ------------- | --------------- | -------- | -------------- | +| `float` | 32 位(单精度) | 约 7 位 | `3.14f` | +| `double` | 64 位(双精度) | 约 15 位 | `3.14`(默认) | +| `long double` | 80 或 128 位 | 平台相关 | `3.14L` | `double` 是默认的浮点类型——你写 `3.14` 的时候,编译器就把它当作 `double` 处理。如果要用 `float`,记得加 `f` 后缀;要用 `long double`,加 `L` 后缀。 @@ -67,8 +68,8 @@ long double ld = 3.14L; // 后缀 L 表示 long double 十进制科学计数法把 `0.0123` 写成 `1.23 × 10⁻²`,一个尾数乘以 10 的幂。计算机干的事一样,只是底换成 2:`小数 = (±1) × 尾数 × 2^指数`。`float` 的 32 个位就照这三块切: -| 1 位符号 | 8 位指数 | 23 位尾数 | -|---|---|---| +| 1 位符号 | 8 位指数 | 23 位尾数 | +| -------- | ---------------------------- | ---------------------------- | | 正还是负 | 决定能表示多大、多小(范围) | 决定有多少位有效数字(精度) | 指数位管量级,尾数位管精细度,两个都有限。指数让你能写到 `10³⁸` 那么大、也能小到 `10⁻⁴⁵`;可尾数只有 23 位,有效数字大概就 7 位十进制,再多存不下。 @@ -339,6 +340,8 @@ int main(void) } ``` +### 练习 1 参考答案 + ```text a_double + b_double == c_double? no a_float + b_float == c_float? yes @@ -369,7 +372,6 @@ if (target < sizeof(values) / sizeof(values[0])) { 提示:`sizeof` 返回的是什么类型? - ### 练习 2 参考答案 ```c From a89113938fb84203c0d12f549267d19706970482 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:53:52 +0800 Subject: [PATCH 09/15] Update 02B-float-char-const-cast.md --- .../c_tutorials/02B-float-char-const-cast.md | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md index fe41979b7..4cbd5195d 100644 --- a/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md +++ b/documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md @@ -1,23 +1,22 @@ --- chapter: 1 cpp_standard: - - 11 +- 11 description: 掌握 C 语言的浮点类型与精度问题、字符存储与编码、const 限定符和隐式类型转换规则,理解 C++ 类型安全设计的动机 difficulty: beginner order: 3 platform: host prerequisites: - - 数据类型基础:整数与内存 +- 数据类型基础:整数与内存 reading_time_minutes: 12 tags: - - host - - cpp-modern - - beginner - - 入门 - - 基础 +- host +- cpp-modern +- beginner +- 入门 +- 基础 title: 浮点、字符、const 与类型转换 --- - # 浮点、字符、const 与类型转换 上一篇里我们把整数家族从里到外拆了一遍——整型层级、有符号无符号、固定宽度类型和 sizeof。但程序世界里不只有整数:商品价格需要小数,屏幕上的文字需要字符,变量声明后有时候需要保护它不被乱改,不同类型的数据混在一起运算时编译器到底怎么处理。这些就是我们今天要一块一块啃的内容。 @@ -36,7 +35,7 @@ title: 浮点、字符、const 与类型转换 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86_64(WSL2 也可以) +- 平台:Linux x86\_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -46,11 +45,11 @@ title: 浮点、字符、const 与类型转换 C 语言提供了三种浮点类型,按精度从小到大排列: -| 类型 | 典型位数 | 有效数字 | 字面量写法 | -| ------------- | --------------- | -------- | -------------- | -| `float` | 32 位(单精度) | 约 7 位 | `3.14f` | -| `double` | 64 位(双精度) | 约 15 位 | `3.14`(默认) | -| `long double` | 80 或 128 位 | 平台相关 | `3.14L` | +| 类型 | 典型位数 | 有效数字 | 字面量写法 | +|------|---------|---------|-----------| +| `float` | 32 位(单精度) | 约 7 位 | `3.14f` | +| `double` | 64 位(双精度) | 约 15 位 | `3.14`(默认) | +| `long double` | 80 或 128 位 | 平台相关 | `3.14L` | `double` 是默认的浮点类型——你写 `3.14` 的时候,编译器就把它当作 `double` 处理。如果要用 `float`,记得加 `f` 后缀;要用 `long double`,加 `L` 后缀。 @@ -68,8 +67,8 @@ long double ld = 3.14L; // 后缀 L 表示 long double 十进制科学计数法把 `0.0123` 写成 `1.23 × 10⁻²`,一个尾数乘以 10 的幂。计算机干的事一样,只是底换成 2:`小数 = (±1) × 尾数 × 2^指数`。`float` 的 32 个位就照这三块切: -| 1 位符号 | 8 位指数 | 23 位尾数 | -| -------- | ---------------------------- | ---------------------------- | +| 1 位符号 | 8 位指数 | 23 位尾数 | +|---|---|---| | 正还是负 | 决定能表示多大、多小(范围) | 决定有多少位有效数字(精度) | 指数位管量级,尾数位管精细度,两个都有限。指数让你能写到 `10³⁸` 那么大、也能小到 `10⁻⁴⁵`;可尾数只有 23 位,有效数字大概就 7 位十进制,再多存不下。 @@ -340,9 +339,6 @@ int main(void) } ``` -### 练习 1 参考答案 - - ```text a_double + b_double == c_double? no a_float + b_float == c_float? yes @@ -394,6 +390,7 @@ if (target < sizeof(values) / sizeof(values[0])) { 提示:`sizeof` 返回的是什么类型? + ### 练习 2 参考答案 ```c From a32f7c4cf605b0cba59d471a357dbad321f43065 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:12:16 +0800 Subject: [PATCH 10/15] =?UTF-8?q?=E5=AF=B9=20C=20=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=95=99=E7=A8=8B=E7=9A=84=E7=AC=AC=E5=85=AD?= =?UTF-8?q?=E7=AB=A0=E5=92=8C=E7=AC=AC=E4=B8=83=E7=AB=A0=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E5=8F=82=E8=80=83=E7=AD=94=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第六章的第三题难度有点高,这个答案我就不写了,交给你来写。 第二题也交给你了。 awa --- .../c_tutorials/04-control-flow.md | 41 +++++-- .../c_tutorials/05-function-basics.md | 113 ++++++++++++++++-- 2 files changed, 138 insertions(+), 16 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index 24fe95639..33f9115b3 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 掌握 C 语言的条件分支、循环、switch 穿透特性与状态机模式,理解 break/continue/goto 的正确用法 difficulty: beginner order: 6 platform: host prerequisites: -- 位运算与求值顺序 + - 位运算与求值顺序 reading_time_minutes: 11 tags: -- host -- cpp-modern -- beginner -- 入门 -- 基础 + - host + - cpp-modern + - beginner + - 入门 + - 基础 title: 控制流:让程序学会选择和重复 --- + # 控制流:让程序学会选择和重复 到目前为止我们写的程序都是从第一行一路跑到最后一行。但现实世界的逻辑不是这样的——"如果温度超过阈值就开风扇"、"重复读取传感器数据直到收到停止命令"。控制流语句就是干这个的:让程序根据条件选择不同的执行路径(分支),或者反复执行某段逻辑(循环)。 @@ -36,7 +37,7 @@ title: 控制流:让程序学会选择和重复 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -468,6 +469,30 @@ C++17 引入了 `if constexpr`,它在编译期评估条件,直接把不满 用 `switch` 实现一个函数,根据月份和是否闰年返回该月的天数。要求利用穿透特性合并同天数的月份。 +### 练习 1 参考答案 + +```c +int month_day(int year, int month) { + switch (month) { + case 1:case 3:case 5:case 7:case 8:case 10:case 12: + return 31; + case 4:case 6:case 9:case 11: + return 30; + case 2: + return is_leap_year(year) ? 29 : 28; + } +} + +bool is_leap_year(int year) { + if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) { + return true; + } + else { + return false; + } +} +``` + ### 练习 2:安全的矩阵搜索 在二维矩阵中查找目标值。找到后用两种方式跳出多层循环:一种用标志变量,一种用 `goto`。 diff --git a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md index 5de6cd745..27733c71a 100644 --- a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 理解 C 函数的声明定义调用机制、值传递本质、指针参数、返回值策略和递归原理,为 C++ 引用传递和函数重载打好基础 difficulty: beginner order: 7 platform: host prerequisites: -- 指针与数组、const 和空指针 + - 指针与数组、const 和空指针 reading_time_minutes: 10 tags: -- host -- cpp-modern -- beginner -- 入门 -- 基础 + - host + - cpp-modern + - beginner + - 入门 + - 基础 title: 函数基础与参数传递 --- + # 函数基础与参数传递 到现在为止我们写的代码都塞在 `main` 函数里。但现实世界的程序不会这样——一个项目动辄几万行代码,如果全挤在一个函数里,那基本没法维护。函数就是 C 语言模块化编程的基本单元:把一段逻辑封装起来,给它起个名字,需要的时候调用就行。 @@ -35,7 +36,7 @@ title: 函数基础与参数传递 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -324,6 +325,40 @@ typedef enum { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel; void log_message(LogLevel level, const char* format, ...); ``` +### 练习 1 参考答案 + +```c +void log_message(LogLevel level, const char* format, ...) { + const char* level_str; + switch (level) { + case LOG_DEBUG: + level_str = "DEBUG"; + break; + case LOG_INFO: + level_str = "INFO"; + break; + case LOG_WARN: + level_str = "WARN"; + break; + case LOG_ERROR: + level_str = "ERROR"; + break; + default: + level_str = "UNKNOWN"; + break; + } + printf("%s\n", level_str); + + va_list log; + va_start(log, format); + + vprintf(format, log); + printf("\n"); + + va_end(log); +} +``` + ### 练习 2:递归与迭代——二分查找 分别用递归和迭代实现二分查找,比较两者的性能和可读性: @@ -333,6 +368,49 @@ int binary_search_recursive(const int* arr, size_t len, int target); int binary_search_iterative(const int* arr, size_t len, int target); ``` +### 练习 2 参考答案 + +```c +int binary_search_recursive(const int* arr, size_t len, int target) { + if (len < 1) { + printf("%d is not found in index\n", target); + return -1; + } + size_t mid = (len - 1) / 2; + + if (arr[mid] == target) {return mid;} + if (arr[mid] < target) { + int res = binary_search_recursive(arr + mid + 1, len - mid - 1, target); + return (res == -1) ? -1 : (res + mid + 1); + } + if (arr[mid] > target) {return binary_search_recursive(arr, mid , target);} + return -1; +} + +int binary_search_iterative(const int* arr, size_t len, int target) { + if (len < 1) { + printf("%d is not found in index\n", target); + return -1; + } + size_t low = 0; + size_t high = len - 1; + size_t mid = low + (high - low) / 2; + while (low <= high) { + if (arr[mid] == target) {return mid;} + + if (arr[mid] < target) { + low = mid + 1; + } + else if (arr[mid] > target) { + high = mid - 1; + } + mid = low + (high - low) / 2; + } + printf("%d is not found in index\n", target); + return -1; +} +``` + ### 练习 3:多返回值实战 实现一个函数,同时计算数组的最大值和最小值: @@ -346,6 +424,25 @@ int binary_search_iterative(const int* arr, size_t len, int target); void find_min_max(const int* data, size_t len, int* min_out, int* max_out); ``` +### 练习 3 参考答案 + +```c +void find_min_max(const int* data, size_t len, int* min_out, int* max_out) { + if (data == NULL || min_out == NULL || max_out == NULL || len < 1) { + return; + } + *min_out = *max_out = data[0]; + for (size_t i = 1; i < len; i++) { + if (data[i] < *min_out) { + *min_out = data[i]; + } + if (data[i] > *max_out) { + *max_out = data[i]; + } + } +} +``` + ## 参考资源 - [cppreference: 函数声明](https://en.cppreference.com/w/c/language/function_declaration) From 56c11cf25bc021074399310b6346c6724e790e71 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sat, 18 Jul 2026 19:24:42 +0800 Subject: [PATCH 11/15] fix: submitted answers get possible sigfault and with implicit declarations --- .../c_tutorials/04-control-flow.md | 19 +++++------ .../c_tutorials/05-function-basics.md | 34 ++++++++----------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index 33f9115b3..8e191a146 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -472,23 +472,20 @@ C++17 引入了 `if constexpr`,它在编译期评估条件,直接把不满 ### 练习 1 参考答案 ```c +bool is_leap_year(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); +} + int month_day(int year, int month) { switch (month) { - case 1:case 3:case 5:case 7:case 8:case 10:case 12: + case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; - case 4:case 6:case 9:case 11: + case 4: case 6: case 9: case 11: return 30; case 2: return is_leap_year(year) ? 29 : 28; - } -} - -bool is_leap_year(int year) { - if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) { - return true; - } - else { - return false; + default: + return -1; } } ``` diff --git a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md index 27733c71a..f22130846 100644 --- a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md @@ -349,13 +349,13 @@ void log_message(LogLevel level, const char* format, ...) { } printf("%s\n", level_str); - va_list log; - va_start(log, format); + va_list args; + va_start(args, format); - vprintf(format, log); + vprintf(format, args); printf("\n"); - va_end(log); + va_end(args); } ``` @@ -381,30 +381,24 @@ int binary_search_recursive(const int* arr, size_t len, int target) { if (arr[mid] == target) {return mid;} if (arr[mid] < target) { int res = binary_search_recursive(arr + mid + 1, len - mid - 1, target); - return (res == -1) ? -1 : (res + mid + 1); + return (res == -1) ? -1 : (int)(res + mid + 1); } if (arr[mid] > target) {return binary_search_recursive(arr, mid , target);} return -1; } int binary_search_iterative(const int* arr, size_t len, int target) { - if (len < 1) { - printf("%d is not found in index\n", target); - return -1; - } - size_t low = 0; - size_t high = len - 1; - size_t mid = low + (high - low) / 2; - while (low <= high) { - if (arr[mid] == target) {return mid;} - - if (arr[mid] < target) { - low = mid + 1; + size_t lo = 0, hi = len; // 搜索区间 [lo, hi),左闭右开 + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + if (arr[mid] == target) { + return mid; } - else if (arr[mid] > target) { - high = mid - 1; + if (arr[mid] < target) { + lo = mid + 1; // 搜右半边,lo 只增不下溢 + } else { + hi = mid; // 搜左半边,hi 收敛到 mid,不下溢 } - mid = low + (high - low) / 2; } printf("%d is not found in index\n", target); return -1; From 4cc527da8850a6903f5ade29cb91f25dc50617a4 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:24:30 +0800 Subject: [PATCH 12/15] Update 06-scope-and-storage.md --- .../c_tutorials/06-scope-and-storage.md | 187 ++++++++++++++++-- 1 file changed, 171 insertions(+), 16 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md index 568b3e23b..ce38c6af5 100644 --- a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md +++ b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 深入理解 C 语言的作用域规则、存储类别和链接性,掌握 static 的三种用法 difficulty: beginner order: 8 platform: host prerequisites: -- 控制流:让程序学会选择和重复 + - 控制流:让程序学会选择和重复 reading_time_minutes: 20 tags: -- host -- cpp-modern -- beginner -- 入门 -- 基础 + - host + - cpp-modern + - beginner + - 入门 + - 基础 title: 作用域与存储类别 --- + # 作用域与存储类别 如果你写过超过两个源文件的项目,大概率已经踩过这样的坑:两个文件里都定义了一个叫 `count` 的全局变量,编译的时候链接器一脸懵逼地告诉你 `multiple definition`。或者更隐蔽的情况——你在某个 `.c` 文件里定义了一个辅助函数,结果别的文件不小心也调用了它,后来你改了那个函数的实现,调用方毫无预警地崩了。 @@ -261,13 +262,13 @@ extern int kValue = 42; // 千万别这么干! 这三者的关系可以用一个表格来总结: -| 声明位置 | 关键字 | 链接性 | 作用域 | 生命周期 | -| --- | --- | --- | --- | --- | -| 函数内 | (无) | 无 | 块 | 自动 | -| 函数内 | `static` | 无 | 块 | 静态 | -| 函数外 | (无) | 外部 | 文件 | 静态 | -| 函数外 | `static` | 内部 | 文件 | 静态 | -| 函数外 | `extern` | (取决于首次声明) | 文件 | 静态 | +| 声明位置 | 关键字 | 链接性 | 作用域 | 生命周期 | +| -------- | -------- | ------------------ | ------ | -------- | +| 函数内 | (无) | 无 | 块 | 自动 | +| 函数内 | `static` | 无 | 块 | 静态 | +| 函数外 | (无) | 外部 | 文件 | 静态 | +| 函数外 | `static` | 内部 | 文件 | 静态 | +| 函数外 | `extern` | (取决于首次声明) | 文件 | 静态 | 这个表格值得多看几眼——注意函数外的 `static` 改变的是链接性(从外部变成内部),而不是作用域或生命周期。 @@ -455,6 +456,58 @@ void counter_reset(void); 请自行实现 `counter.c`。 +### 练习 1 参考答案 + +main.c + +```c +#include +#include "counter.h" + +int main(void) { + printf("%d\n",counter_get()); //输出应当是0 + counter_increment(); + printf("%d\n",counter_get()); //输出应当是1 + counter_increment(); + printf("%d\n",counter_get()); //输出应当是2 + counter_reset(); + printf("%d\n",counter_get()); //输出应当是0 + return 0; +} +``` + +counter.h + +```c +#ifndef MODERNCPP_PRE8_1_COUNTER_H +#define MODERNCPP_PRE8_1_COUNTER_H + +void counter_increment(void); +int counter_get(void); +void counter_reset(void); + +#endif //MODERNCPP_PRE8_1_COUNTER_H +``` + +counter.c + +```c +#include "counter.h" + +static int counter = 0; +void counter_increment(void) { + counter++; +} + +void counter_reset(void) { + counter = 0; +} + +int counter_get(void) { + return counter; +} +``` + ### 练习 2:多文件符号可见性 创建三个文件 `a.c`、`b.c`、`main.c`。要求: @@ -471,11 +524,96 @@ void counter_reset(void); // 各 .c 文件的实现留给你 ``` +### 练习 2 参考答案 + +main.c + +```c +#include +#include "a.h" +#include "b.h" + +int main(void) { + + printf("%d\n",kSharedValue); //输出应当是0 + set_kSharedValue(100); + printf("%d\n",kSharedValue); //输出应当是100 + + return 0; +} +``` + +a.h + +```c +#ifndef MODERNCPP_PRE8_2_A_H +#define MODERNCPP_PRE8_2_A_H + +extern int kSharedValue; + +#endif //MODERNCPP_PRE8_2_A_H +``` + +b.h + +```c +#ifndef MODERNCPP_PRE8_2_B_H +#define MODERNCPP_PRE8_2_B_H + +void set_kSharedValue(int value); + +#endif //MODERNCPP_PRE8_2_B_H +``` + +a.c + +```c +#include +#include "a.h" + +int kSharedValue = 0; +static void helper_a(void) { + printf("need help?"); +} +``` + +b.c + +```c +#include +#include "b.h" +#include "a.h" + + +static void helper_a(void) { + printf("need help?"); +} +void set_kSharedValue(int value) { + kSharedValue = value; +} +``` + ### 练习 3:延迟初始化 用 `static` 局部变量实现一个 `get_config` 函数:第一次调用时执行初始化(打印 "Initializing..." 并设置默认值),后续调用直接返回已初始化的值,不再重新初始化。 ```c +typedef struct { + int max_connections; //建议设为5 + int timeout_ms; //建议设为500 + const char* server_name; //建议设为localhost +} Config; + +const Config* get_config(void); +``` + +> 提示:`static` 局部变量只在第一次进入函数时被初始化——正好可以用来实现"只初始化一次"的语义。 + +### 练习 3 参考答案 + +```c +#include + typedef struct { int max_connections; int timeout_ms; @@ -483,9 +621,26 @@ typedef struct { } Config; const Config* get_config(void); -``` -> 提示:`static` 局部变量只在第一次进入函数时被初始化——正好可以用来实现"只初始化一次"的语义。 +int main(void) { + get_config(); //应当输出"Initializing..." + printf("%d %d %s\n",get_config()->max_connections, get_config()->timeout_ms, get_config()->server_name); //应当输出"5 500 localhost" + return 0; +} + +const Config* get_config(void) { + static Config config; + static int initialized = 0; + if (!initialized) { + printf("Initializing...\n"); + config.max_connections = 5; + config.timeout_ms = 500; + config.server_name = "localhost"; + initialized = 1; + } + return &config; +} +``` ## 参考资源 From 382a9e423dc1f06f443dda71207f931bf4e6f5cb Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:29:48 +0800 Subject: [PATCH 13/15] Update 07A-pointer-essentials.md --- .../c_tutorials/07A-pointer-essentials.md | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index 0481d0123..541e6975d 100644 --- a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 从零开始理解 C 语言指针——内存模型直觉、声明初始化、取地址与解引用运算符、指针的加减运算和距离计算 difficulty: beginner order: 9 platform: host prerequisites: -- 数据类型基础:整数与内存 -- 运算符基础:让数据动起来 + - 数据类型基础:整数与内存 + - 运算符基础:让数据动起来 reading_time_minutes: 10 tags: -- host -- cpp-modern -- beginner -- 入门 + - host + - cpp-modern + - beginner + - 入门 title: 指针入门:地址的世界 --- + # 指针入门:地址的世界 指针大概是 C 语言里名声最响也最容易劝退新手的特性了。如果你之前接触过 Python 或 Java,可能习惯了"变量就是对象本身"的思维——变量里存的就是数据。但到了 C 这边,多了一个关键概念:每个变量都住在内存的某个位置,这个位置有一个编号(地址)。指针就是用来存储和操作这些地址的变量。 @@ -35,7 +36,7 @@ title: 指针入门:地址的世界 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -268,6 +269,33 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**( 写一个程序,声明三个不同类型的变量(`int`、`double`、`char`),打印它们的值、地址和 `sizeof` 结果。观察地址之间的间隔是否符合各类型的大小。 +### 练习 1 参考答案 + +```c +#include + +int main(void) { + + int value_int = 0; + double value_double = 0.0; + char value_char = '0'; + + printf("(int) value:%d address:%p size:%d\n", value_int, &value_int, sizeof(int)); + printf("(double) value:%.2f address:%p size:%d\n", value_double, &value_double, sizeof(double)); + printf("(char) value:%d address:%p size:%d\n", value_char, &value_char, sizeof(char)); + return 0; +} + +``` + +输出结果可能为: + +```text +(int) value:0 address:00000000005ffe8c size:4 +(double) value:0.00 address:00000000005ffe80 size:8 +(char) value:48 address:00000000005ffe7f size:1 +``` + ### 练习 2:指针遍历数组 用指针算术遍历一个 `int` 数组并打印所有元素。要求不使用 `[]` 运算符,只用指针加减和解引用: @@ -279,6 +307,37 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**( void print_int_array(const int* data, size_t count); ``` +### 练习 1 参考答案 + +```c +#include + +void print_int_array(const int* data, size_t count); + +int main(void) { + int arr[] = {1, 2, 3, 4, 5}; + print_int_array(arr, 5); + return 0; +} + +void print_int_array(const int* data, size_t count) { + for (size_t i = 0; i < count;i++) { + printf("data[%d] = %d\n", i, *(data + i)); + } +} + +``` + +输出结果应当为: + +```text +data[0] = 1 +data[1] = 2 +data[2] = 3 +data[3] = 4 +data[4] = 5 +``` + ## 参考资源 - [cppreference: 指针声明](https://en.cppreference.com/w/c/language/pointer) From 76c6b9f12df60215a721692cfd751f533e8a4935 Mon Sep 17 00:00:00 2001 From: owollz4 <122787810+owollz4@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:32:31 +0800 Subject: [PATCH 14/15] Update 07A-pointer-essentials.md --- .../vol1-fundamentals/c_tutorials/07A-pointer-essentials.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index 541e6975d..c72c02db3 100644 --- a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -307,7 +307,7 @@ int main(void) { void print_int_array(const int* data, size_t count); ``` -### 练习 1 参考答案 +### 练习 2 参考答案 ```c #include From ca5543f60cbdcf5aea2cb1cbd6ff2948c42646c9 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 20 Jul 2026 23:21:55 +0800 Subject: [PATCH 15/15] docs(vol1): fix -Wall -Wextra warnings in ch06/07A exercise answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 练习答案代码质量修正,纯格式/调用/注释调整,零逻辑改动。 07A-pointer-essentials.md: - 练习1: sizeof/size_t 改用 %zu(原 %d 触发 -Wformat 警告) - 练习1: %p 参数加 (void*) 强转(标准要求 void*,消除 UB) - 练习1: 输出示例换真实地址格式,补 char=48 与栈地址间隔两点说明 - 练习2: size_t 索引改用 %zu;补 #include 06-scope-and-storage.md: - 练习2: 让两个同名 static helper_a 各被模块公共函数调用(消除 unused-function 警告,顺便把"同名 static 互不干扰"演示得更透彻) - 练习3: config 改用 static 初始化器呼应题目提示,flag 只负责打印 全部 5 段经 -Wall -Wextra -std=c17 编译零警告、运行输出正确。 --- .../c_tutorials/06-scope-and-storage.md | 23 +++++++++++++------ .../c_tutorials/07A-pointer-essentials.md | 22 +++++++++++------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md index ce38c6af5..57ec50994 100644 --- a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md +++ b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md @@ -535,8 +535,9 @@ main.c int main(void) { + a_greet(); // 调用 a 模块的公共函数,触发它内部的 helper_a printf("%d\n",kSharedValue); //输出应当是0 - set_kSharedValue(100); + set_kSharedValue(100); // 内部会调用 b 模块自己的 helper_a printf("%d\n",kSharedValue); //输出应当是100 return 0; @@ -550,6 +551,7 @@ a.h #define MODERNCPP_PRE8_2_A_H extern int kSharedValue; +void a_greet(void); #endif //MODERNCPP_PRE8_2_A_H ``` @@ -573,7 +575,12 @@ a.c int kSharedValue = 0; static void helper_a(void) { - printf("need help?"); + printf("need help?\n"); +} + +// a.c 暴露的公共函数,内部调用文件私有的 helper_a +void a_greet(void) { + helper_a(); } ``` @@ -586,9 +593,10 @@ b.c static void helper_a(void) { - printf("need help?"); + printf("need help?\n"); } void set_kSharedValue(int value) { + helper_a(); kSharedValue = value; } ``` @@ -629,13 +637,14 @@ int main(void) { } const Config* get_config(void) { - static Config config; + // config 用 static 初始化器:程序加载时一次性初始化,正好呼应题目说的 + // "static 局部变量只在第一次进入函数时被初始化" + static Config config = {5, 500, "localhost"}; + // 但题目还要求第一次调用时打印 "Initializing..."——静态初始化器本身 + // 没有运行时钩子去打印,所以再用一个 static flag 控制只打印一次 static int initialized = 0; if (!initialized) { printf("Initializing...\n"); - config.max_connections = 5; - config.timeout_ms = 500; - config.server_name = "localhost"; initialized = 1; } return &config; diff --git a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index c72c02db3..2ee6b314c 100644 --- a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -280,22 +280,27 @@ int main(void) { double value_double = 0.0; char value_char = '0'; - printf("(int) value:%d address:%p size:%d\n", value_int, &value_int, sizeof(int)); - printf("(double) value:%.2f address:%p size:%d\n", value_double, &value_double, sizeof(double)); - printf("(char) value:%d address:%p size:%d\n", value_char, &value_char, sizeof(char)); + printf("(int) value:%d address:%p size:%zu\n", value_int, (void*)&value_int, sizeof(int)); + printf("(double) value:%.2f address:%p size:%zu\n", value_double, (void*)&value_double, sizeof(double)); + printf("(char) value:%d address:%p size:%zu\n", value_char, (void*)&value_char, sizeof(char)); return 0; } ``` -输出结果可能为: +输出结果可能为(地址每次运行都会变): ```text -(int) value:0 address:00000000005ffe8c size:4 -(double) value:0.00 address:00000000005ffe80 size:8 -(char) value:48 address:00000000005ffe7f size:1 +(int) value:0 address:0x7ffd8cecdbec size:4 +(double) value:0.00 address:0x7ffd8cecdbf0 size:8 +(char) value:48 address:0x7ffd8cecdbeb size:1 ``` +两点值得留意: + +- `char` 的 `value` 显示成 `48`,因为 `'0'` 的 ASCII 码就是 48。C 语言里 `char` 本质上是一个小整数,用 `%d` 打印看到的就是它的整数值(想直接看到字符 `'0'`,把格式符换成 `%c` 即可)。 +- 三个地址的间隔并不等于各自的类型大小。按声明顺序是 `int`(4) → `double`(8) → `char`(1),但实际地址排列成了 `char` → `int` → `double`,相邻差值也不是 4、8、1。原因有两个:编译器会为了内存对齐给局部变量重排位置、插入填充字节;而且栈布局根本不保证按声明顺序排列变量。所以"地址间隔正好等于类型大小"这个直觉,在真实编译器里通常不成立——这正是这道题想让你亲眼看到的。 + ### 练习 2:指针遍历数组 用指针算术遍历一个 `int` 数组并打印所有元素。要求不使用 `[]` 运算符,只用指针加减和解引用: @@ -311,6 +316,7 @@ void print_int_array(const int* data, size_t count); ```c #include +#include // size_t void print_int_array(const int* data, size_t count); @@ -322,7 +328,7 @@ int main(void) { void print_int_array(const int* data, size_t count) { for (size_t i = 0; i < count;i++) { - printf("data[%d] = %d\n", i, *(data + i)); + printf("data[%zu] = %d\n", i, *(data + i)); } }