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 a0e3229ef..ac9691a0f 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 参考答案 +::: details 参考答案 ```c #include @@ -330,6 +330,8 @@ 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` 做溢出实验: @@ -352,7 +354,7 @@ int main(void) 编译运行,观察两者的行为差异。然后加上 `-fsanitize=undefined` 选项重新编译,看看有什么变化。 -### 练习 2 参考答案 +::: details 参考答案 假设该文件名为overflow.c @@ -375,6 +377,8 @@ UINT_MAX = 4294967295, UINT_MAX + 1 = 0 实际上 C 标准其实并没有对带有符号的整数的溢出进行定义,也就是说,对INT_MAX进行+1这个操作严格意义上是一个未定义行为。 (只不过溢出很好用,也是大部分编译器都默认支持溢出的。) +::: + ## 参考资源 - [cppreference: C 语言整型](https://en.cppreference.com/w/c/language/integer_constant) 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 4cbd5195d..31d1e5bbc 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 @@ -353,7 +353,7 @@ g_float = 0.69999998807907104492 //0.7f 修改代码使用 epsilon 比较来得到正确的结果。 -### 练习 1 参考答案 +::: details 参考答案 把 `==` 换成「差的绝对值小于一个很小的阈值(epsilon)」来判断: @@ -374,6 +374,8 @@ int float_equal(float a, float b) { 替换之后,`0.1 + 0.2` 与 `0.3` 的差约 `5.5e-17`,小于 `DBL_EPSILON`(约 `2.2e-16`),`double_equal(0.1 + 0.2, 0.3)` 就会返回真。要留意,绝对 epsilon 比较在数值数量级很大时会失效,工程里更稳妥的是相对误差比较,这里先用入门写法。 +::: + ### 练习 2:隐式转换陷阱 下面这段代码有一个隐藏的 bug,找出它并解释原因: @@ -391,7 +393,7 @@ if (target < sizeof(values) / sizeof(values[0])) { 提示:`sizeof` 返回的是什么类型? -### 练习 2 参考答案 +::: details 参考答案 ```c int values[] = {1, 2, 3, 4, 5}; @@ -415,6 +417,8 @@ if (target < (int)(sizeof(values) / sizeof(values[0]))) { } ``` +::: + ### 练习 3:const 实战 写一个函数,接收一个字符串,统计其中某个字符出现的次数。函数签名中正确使用 `const`: @@ -427,7 +431,7 @@ if (target < (int)(sizeof(values) / sizeof(values[0]))) { size_t count_char(const char* str, char ch); ``` -### 练习 3 参考答案 +::: details 参考答案 ```c size_t count_char(const char* str, char ch) { @@ -444,6 +448,8 @@ size_t count_char(const char* str, char ch) { } ``` +::: + ## 参考资源 - [cppreference: C 语言隐式转换](https://en.cppreference.com/w/c/language/conversion) diff --git a/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md b/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md index 806204358..b8eb3a244 100644 --- a/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md @@ -266,7 +266,7 @@ printf("%d\n", 7 % 2); printf("%d\n", -7 % 2); ``` -### 练习 1 参考答案 +::: details 参考答案 ```text 3 // 7 / 2 = 3.5,整数除法向零取整,砍掉小数部分 → 3 @@ -278,6 +278,8 @@ printf("%d\n", -7 % 2); 关键点:C99 起 `/` 和 `%` 都向零取整(truncation toward zero),正负数规则一致,直接丢弃小数部分。所以 `-7 / 2` 得 `-3` 而不是 `-4`,`-7 % 2` 的符号随被除数取负。 +::: + ### 练习 2:短路求值实战 写一个函数,安全地从数组中找到第一个大于指定值的元素。利用短路求值确保不越界: @@ -291,7 +293,7 @@ printf("%d\n", -7 % 2); int find_first_above(const int* arr, size_t len, int threshold); ``` -### 练习 2 参考答案 +::: details 参考答案 ```c #include @@ -316,6 +318,8 @@ int find_first_above(const int* arr, size_t len, int threshold) { 核心是 `while (i < len && arr[i] <= threshold)` 这一行:`&&` 短路求值,`i < len` 为假时根本不会去读 `arr[i]`,越界访问就被挡住了。 +::: + ## 参考资源 - [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 12acaf225..6f9bfe702 100644 --- a/documents/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md +++ b/documents/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md @@ -312,7 +312,7 @@ uint32_t bit_toggle(uint32_t value, int n); uint32_t bit_extract(uint32_t value, int high, int low); ``` -### 练习 1 参考答案 +::: details 参考答案 ```c /// @brief 将 value 的第 n 位置为 1 @@ -341,6 +341,8 @@ uint32_t bit_extract(uint32_t value, int high, int low) { `bit_extract` 里 mask 用 `1ULL` 是为了避开 `width == 32` 时 `1U << 32` 的移位溢出,这种细节在位域操作里很常见。 +::: + ### 练习 2:安全的移位 写一个函数,安全地执行左移操作,处理所有边界情况: @@ -354,7 +356,7 @@ uint32_t bit_extract(uint32_t value, int high, int low) { uint32_t safe_shift_left(uint32_t val, int n, int bits); ``` -### 练习 2 参考答案 +::: details 参考答案 ```c #include @@ -370,6 +372,8 @@ uint32_t safe_shift_left(uint32_t val, int n, int bits) { 只要保证 `n < bits` 且 `bits <= 32`,`val << n` 就不会触发移位溢出这类 UB。 +::: + ### 练习 3:表达式分析 分析以下表达式的求值行为(不实际运行),标出每个是"明确定义"、"未指定行为"还是"未定义行为": @@ -382,7 +386,7 @@ int r3 = (a > b) ? a-- : b--; // ? printf("%d %d\n", a++, a++); // ? ``` -### 练习 3 参考答案 +::: details 参考答案 ```c int a = 5, b = 3; @@ -394,6 +398,8 @@ printf("%d %d\n", a++, a++); // 未定义行为:函数实参之间没有序 容易踩的是 `r3`:看起来两边都在自减,但三目运算符只会对成立的那个分支求值,而且 `?:` 的第一个操作数之后有序列点,所以是安全的。`printf` 那行虽然写了逗号,但函数实参之间的逗号不是逗号运算符,没有序列点,两个 `a++` 之间无序列点 → UB。 +::: + ## 参考资源 - [cppreference: C 运算符优先级](https://en.cppreference.com/w/c/language/operator_precedence) diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index 8e191a146..bae088883 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -469,7 +469,7 @@ C++17 引入了 `if constexpr`,它在编译期评估条件,直接把不满 用 `switch` 实现一个函数,根据月份和是否闰年返回该月的天数。要求利用穿透特性合并同天数的月份。 -### 练习 1 参考答案 +::: details 参考答案 ```c bool is_leap_year(int year) { @@ -490,6 +490,8 @@ int month_day(int year, int month) { } ``` +::: + ### 练习 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 f22130846..4bdc4a47f 100644 --- a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md @@ -325,7 +325,7 @@ typedef enum { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel; void log_message(LogLevel level, const char* format, ...); ``` -### 练习 1 参考答案 +::: details 参考答案 ```c void log_message(LogLevel level, const char* format, ...) { @@ -359,6 +359,8 @@ void log_message(LogLevel level, const char* format, ...) { } ``` +::: + ### 练习 2:递归与迭代——二分查找 分别用递归和迭代实现二分查找,比较两者的性能和可读性: @@ -368,7 +370,7 @@ 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 参考答案 +::: details 参考答案 ```c int binary_search_recursive(const int* arr, size_t len, int target) { @@ -405,6 +407,8 @@ int binary_search_iterative(const int* arr, size_t len, int target) { } ``` +::: + ### 练习 3:多返回值实战 实现一个函数,同时计算数组的最大值和最小值: @@ -418,7 +422,7 @@ 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 参考答案 +::: details 参考答案 ```c void find_min_max(const int* data, size_t len, int* min_out, int* max_out) { @@ -437,6 +441,8 @@ void find_min_max(const int* data, size_t len, int* min_out, int* max_out) { } ``` +::: + ## 参考资源 - [cppreference: 函数声明](https://en.cppreference.com/w/c/language/function_declaration) 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 57ec50994..607954697 100644 --- a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md +++ b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md @@ -456,7 +456,7 @@ void counter_reset(void); 请自行实现 `counter.c`。 -### 练习 1 参考答案 +::: details 参考答案 main.c @@ -508,6 +508,8 @@ int counter_get(void) { } ``` +::: + ### 练习 2:多文件符号可见性 创建三个文件 `a.c`、`b.c`、`main.c`。要求: @@ -524,7 +526,7 @@ int counter_get(void) { // 各 .c 文件的实现留给你 ``` -### 练习 2 参考答案 +::: details 参考答案 main.c @@ -601,6 +603,8 @@ void set_kSharedValue(int value) { } ``` +::: + ### 练习 3:延迟初始化 用 `static` 局部变量实现一个 `get_config` 函数:第一次调用时执行初始化(打印 "Initializing..." 并设置默认值),后续调用直接返回已初始化的值,不再重新初始化。 @@ -617,7 +621,7 @@ const Config* get_config(void); > 提示:`static` 局部变量只在第一次进入函数时被初始化——正好可以用来实现"只初始化一次"的语义。 -### 练习 3 参考答案 +::: details 参考答案 ```c #include @@ -651,6 +655,8 @@ const Config* get_config(void) { } ``` +::: + ## 参考资源 - [存储类别说明符 - cppreference](https://en.cppreference.com/w/c/language/storage_duration) diff --git a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index 2ee6b314c..9eb735ef3 100644 --- a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -269,7 +269,7 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**( 写一个程序,声明三个不同类型的变量(`int`、`double`、`char`),打印它们的值、地址和 `sizeof` 结果。观察地址之间的间隔是否符合各类型的大小。 -### 练习 1 参考答案 +::: details 参考答案 ```c #include @@ -301,6 +301,8 @@ int main(void) { - `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` 数组并打印所有元素。要求不使用 `[]` 运算符,只用指针加减和解引用: @@ -312,7 +314,7 @@ int main(void) { void print_int_array(const int* data, size_t count); ``` -### 练习 2 参考答案 +::: details 参考答案 ```c #include @@ -344,6 +346,8 @@ data[3] = 4 data[4] = 5 ``` +::: + ## 参考资源 - [cppreference: 指针声明](https://en.cppreference.com/w/c/language/pointer) diff --git a/documents/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md b/documents/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md index b679d634d..d6d6c925b 100644 --- a/documents/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md +++ b/documents/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md @@ -265,7 +265,7 @@ std::unique_ptr p = std::make_unique(42); const int* linear_search(const int* data, size_t count, int target); ``` -### 练习 1 参考答案 +::: details 参考答案 ```c #include @@ -312,6 +312,8 @@ const int* linear_search(const int* data, size_t count, int target) { 地址本身不用记,关键是后面那个「第 3 个元素」——`result - arr` 算出来就是 `3`,这个结论是稳的。顺带一提:要是终端里中文显示成乱码,那是终端编码不是 UTF-8 的问题,跟代码无关,换 WSL2 或现代终端就好。 +::: + ### 练习 2:指针版数组反转 实现一个原地反转数组的函数,只使用指针算术(两个指针从两端向中间靠拢),不使用数组下标: @@ -323,7 +325,7 @@ const int* linear_search(const int* data, size_t count, int target) { void reverse_array(int* data, size_t count); ``` -### 练习 2 参考答案 +::: details 参考答案 ```c #include @@ -399,6 +401,8 @@ after reverse_array: 10 ``` +::: + ### 练习 3:const 练习 判断以下每个声明中,哪些操作是合法的,哪些会编译错误: @@ -415,7 +419,7 @@ const int* const p3 = &value; // px = &other; // 修改指针指向 ``` -### 练习 3 参考答案 +::: details 参考答案 ```c int value = 42, other = 100; @@ -436,6 +440,8 @@ const int* const p3 = &value; ``` +::: + ## 参考资源 - [cppreference: 指针声明](https://en.cppreference.com/w/c/language/pointer) diff --git a/documents/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md b/documents/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md index 7bbeba8c2..cbd6ab9a9 100644 --- a/documents/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md +++ b/documents/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md @@ -303,7 +303,7 @@ void free_matrix(int** matrix, int rows); void fill_matrix(int** matrix, int rows, int cols, int value); ``` -### 参考答案 +::: details 参考答案 ```c #include @@ -381,6 +381,8 @@ matrix 的类型是 int**,要解两次引用才摸到具体的 int: 提示:分配时先分配一个指针数组(`int**` 指向的那一维),然后对每一行分别 `malloc`。释放时顺序反过来——先释放每一行,再释放指针数组本身。 +::: + ## 参考资源 - [C 声明语法 - cppreference](https://en.cppreference.com/w/c/language/declarations) diff --git a/documents/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md b/documents/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md index 7f1ce8011..efeae0e90 100644 --- a/documents/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md +++ b/documents/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md @@ -325,7 +325,7 @@ int stack_pop(Stack* s, int* out); // 成功返回 0,空栈返回 -1 int stack_size(const Stack* s); ``` -### 参考答案 +::: details 参考答案 ```c // stack.h — 只暴露接口,不暴露内部结构 @@ -415,6 +415,8 @@ int stack_size(const Stack* s){ 提示:在 `.c` 文件里定义 `struct Stack` 的完整结构(可以用数组+栈顶索引实现),`.h` 文件只放前向声明和函数声明。 +::: + ## 参考资源 - [restrict 限定符 - cppreference](https://en.cppreference.com/w/c/language/restrict)