diff --git a/documents/en/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md b/documents/en/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md index aa455533a..3d6f0b14c 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md +++ b/documents/en/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md @@ -353,6 +353,8 @@ At this point, we have a clear understanding of the complete path of a C program ### Exercise 1: Multi-File Compilation Practice +**Difficulty: Basic** · multi-file staged compile and the symbol table + Build a multi-file project containing the following files: **utils.h**: @@ -373,6 +375,8 @@ Please complete the following: ### Exercise 2: `printf` Formatting Practice +**Difficulty: Basic** · practice printf width, precision, alignment + Without looking up resources, write the expected output of the following `printf` statements (then compile and run to verify): ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/02A-data-types-basics.md b/documents/en/vol1-fundamentals/c_tutorials/02A-data-types-basics.md index 43aae75e1..2894bbf49 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/02A-data-types-basics.md +++ b/documents/en/vol1-fundamentals/c_tutorials/02A-data-types-basics.md @@ -317,6 +317,8 @@ The next question arises: we've covered integers, but what about decimals? How a ### Exercise 1: Type Detector +**Difficulty: Basic** · use sizeof to map out each type's size + Write a program that prints the `sizeof` values of all the following types, and check against the standard to see if they meet the minimum guarantees: ```c @@ -330,6 +332,8 @@ Hint: You can use a macro to reduce repetitive code. ### Exercise 2: Overflow Observation +**Difficulty: Basic** · see signed overflow UB vs unsigned wraparound + Perform overflow experiments on signed `int8_t` and unsigned `uint8_t` respectively: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md b/documents/en/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md index 2036c3f01..0d655f3df 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md +++ b/documents/en/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md @@ -317,6 +317,8 @@ At this point, we have laid a solid foundation for C language data types. Next, ### Exercise 1: Floating-Point Precision Detective +**Difficulty: Basic** · compare floats with epsilon + Predict the output of the following code, then compile and run it to verify your prediction: ```c @@ -339,6 +341,8 @@ Modify the code to use epsilon comparison to obtain the correct result. ### Exercise 2: Implicit Conversion Pitfalls +**Difficulty: Basic** · the trap of mixing signed with size_t + The following code contains a hidden bug. Find it and explain the reason: ```c @@ -355,16 +359,38 @@ Hint: What type does `sizeof` return? ### Exercise 3: `const` in Practice -Write a function that accepts a string and counts the occurrences of a specific character. Use `const` correctly in the function signature: +**Difficulty: Basic** · what const protects, and how to pass arguments + +Read the code below and answer four questions: ```c -/// @brief 统计字符 ch 在字符串 str 中出现的次数 -/// @param str 不可修改的字符串 -/// @param ch 要查找的字符 -/// @return 出现次数 -size_t count_char(const char* str, char ch); +// sum promises not to modify what data points at +int sum(const int* data, size_t n); // (1) what does the const on the parameter promise? + +void f(void) { + const int limit = 100; // (2) what does const on a local variable do? + int arr[3] = {1, 2, 3}; + limit = 200; // (3) does this line compile? + sum(arr, 3); // (4) is passing int* to a const int* parameter legal? +} ``` +Now flip it around: if `sum`'s parameter were `int*` (no const), and the caller passed a `const int carr[3]`, what happens and why? + +::: details Reference answer + +(1) const is a read-only contract: it tells callers and the compiler that `sum` won't modify the array through `data`. + +(2) `limit` becomes a read-only variable that can't be reassigned, and the compiler can optimize based on it. + +(3) It does not compile. `limit` is read-only; assigning to it is an error. + +(4) Legal. Passing `int*` to a `const int*` parameter "tightens" the contract (writable to read-only), which is safe and implicit. + +The reverse: passing `const int*` to an `int*` parameter drops the const protection, so the compiler warns or errors. You'd need an explicit cast, which is dangerous: the function could modify data it wasn't supposed to. + +::: + ## References - [cppreference: Implicit conversions in C](https://en.cppreference.com/w/c/language/conversion) diff --git a/documents/en/vol1-fundamentals/c_tutorials/03A-operators-basics.md b/documents/en/vol1-fundamentals/c_tutorials/03A-operators-basics.md index 80d4c58ed..10738b870 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/03A-operators-basics.md +++ b/documents/en/vol1-fundamentals/c_tutorials/03A-operators-basics.md @@ -356,6 +356,8 @@ The next question is—we haven't covered bitwise operations yet. If you plan to ### Exercise 1: Integer Division Prediction +**Difficulty: Basic** · truncation toward zero and the sign of the remainder + Without actually running it, predict the value of the following expressions, then write a program to verify: ```c @@ -369,6 +371,8 @@ int a = 7, b = -4; ### Exercise 2: Short-Circuit Evaluation in Action +**Difficulty: Intermediate** · guard an array bound with short-circuit evaluation + Write a function that safely finds the first element in an array greater than a specified value. Use short-circuit evaluation to ensure no out-of-bounds access occurs: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md b/documents/en/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md index 02796def6..06f047d27 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md +++ b/documents/en/vol1-fundamentals/c_tutorials/03B-bitwise-and-evaluation.md @@ -286,6 +286,8 @@ At this point, we have covered all aspects of C operators. Next, we will learn a ### Exercise 1: Bit Manipulation Toolkit +**Difficulty: Intermediate** · set / clear / toggle / extract a bit field + Implement the following bit manipulation functions: ```c @@ -307,6 +309,8 @@ bool bit_check(uint32_t val, uint8_t n); ### Exercise 2: Safe Shift +**Difficulty: Basic** · boundary checks that fend off shift UB + Write a function to safely perform a left shift, handling all boundary cases: ```c @@ -320,6 +324,8 @@ bool safe_shift_left(uint32_t *result, uint32_t val, int n); ### Exercise 3: Expression Analysis +**Difficulty: Basic** · spot sequence points and undefined behavior + Analyze the evaluation behavior of the following expressions (without running them), marking each as "well-defined", "unspecified behavior", or "undefined behavior": ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/en/vol1-fundamentals/c_tutorials/04-control-flow.md index 46746bb8b..8e1b177e1 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/en/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -445,24 +445,93 @@ Next, we will learn about functions—how to organize code into reusable modules ### Exercise 1: Days in a Month +**Difficulty: Basic** · practice the fall-through of switch + Use `switch` to implement a function that returns the number of days in a month based on the month and whether it is a leap year. You are required to use the fall-through feature to merge months with the same number of days. ### Exercise 2: Safe Matrix Search +**Difficulty: Intermediate** · two ways to break out of nested loops + Search for a target value in a 2D matrix. Once found, break out of the multi-level loop in two ways: one using a flag variable, and one using `goto`. ```c // TODO: Implement search_matrix_flag and search_matrix_goto ``` -### Exercise 3: Waiting with Timeout +### Exercise 3: Hand-Writing a Protocol-Frame State Machine + +**Difficulty: Intermediate** · a state machine with switch and an explicit state variable + +The end of this chapter demonstrated a byte-driven serial protocol state machine (start byte `0xAA` -> payload length -> payload -> end byte `0x55`). Implement an equivalent parser yourself: feed received bytes one at a time to `frame_feed`, which internally uses `switch (state)` to transition between states and prints the payload once a full frame is received. + +```c +#include + +typedef enum { STATE_IDLE, STATE_LEN, STATE_PAYLOAD, STATE_DONE } FrameState; + +/// @brief Feed one byte at a time; returns 1 when a complete frame (with end byte) is received, 0 otherwise +int frame_feed(uint8_t byte); +``` + +Implement it with `switch` plus an explicit state variable, not a long if-else chain. One more thing to think about: if the "payload length" field sent by the peer is tampered to a value larger than your buffer, will your state machine blow up? How do you defend against it? -Implement a waiting function with a timeout mechanism to avoid deadlocks caused by naked `while` waiting: +::: details Reference answer ```c -// TODO: Implement wait_with_timeout +#include +#include + +#define MAX_PAYLOAD 16 + +typedef enum { STATE_IDLE, STATE_LEN, STATE_PAYLOAD, STATE_DONE } FrameState; + +static FrameState state = STATE_IDLE; +static uint8_t payload[MAX_PAYLOAD]; +static uint8_t payload_len = 0; +static uint8_t payload_idx = 0; + +int frame_feed(uint8_t byte) { + switch (state) { + case STATE_IDLE: + if (byte == 0xAA) { // only advance on the start byte + payload_idx = 0; + payload_len = 0; + state = STATE_LEN; + } + break; + case STATE_LEN: + // defense: the length may be tampered, clamp to the buffer cap to avoid an out-of-bounds write later + payload_len = (byte <= MAX_PAYLOAD) ? byte : MAX_PAYLOAD; + state = (payload_len == 0) ? STATE_DONE : STATE_PAYLOAD; + break; + case STATE_PAYLOAD: + payload[payload_idx++] = byte; + if (payload_idx >= payload_len) { + state = STATE_DONE; + } + break; + case STATE_DONE: + if (byte == 0x55) { // proper end byte + printf("Frame OK (%u bytes):", payload_len); + for (uint8_t i = 0; i < payload_len; i++) { + printf(" %02X", payload[i]); + } + printf("\n"); + state = STATE_IDLE; + return 1; + } + state = STATE_IDLE; // end byte never came: frame broken, go idle and wait for the next 0xAA + break; + } + return 0; +} ``` +The key is that every `case` explicitly states "what the next state is", which is what makes a state machine easier to read than a long if-else chain. Clamping the length in `STATE_LEN` is the most basic defense in protocol parsing: never trust a length field sent by the peer. + +::: + ## References - [cppreference: switch statement](https://en.cppreference.com/w/c/language/switch) diff --git a/documents/en/vol1-fundamentals/c_tutorials/05-function-basics.md b/documents/en/vol1-fundamentals/c_tutorials/05-function-basics.md index 2380f7500..9ab640a7f 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/05-function-basics.md +++ b/documents/en/vol1-fundamentals/c_tutorials/05-function-basics.md @@ -341,34 +341,56 @@ At this point, we have mastered the basic usage of functions. The next question ## Exercises -### Exercise 1: Variadic Log Function +### Exercise 1: Variadic Max -Implement a custom log function that supports log levels and formatted strings: +**Difficulty: Basic** · pull arguments one by one with va_arg + +Following the `average` example in this chapter, implement a variadic function that returns the maximum of all its integer arguments. The first argument `count` says how many integers follow: + +```c +/// @brief Return the maximum of count integers +/// @param count number of integer arguments that follow +/// @return the maximum; returns 0 if count is 0 +int max_int(int count, ...); +``` + +Usage: `max_int(3, 10, 25, 7)` should return `25`. + +**Challenge extension** (optional): if you want to implement `log_message(level, format, ...)` with real formatting, you need to forward the variadic arguments to the `printf` family, i.e. `vprintf`/`vfprintf` (not covered in this chapter). Look up `vprintf` on cppreference and then try it. + +::: details Reference answer ```c -#include #include -void log_message(const char *level, const char *fmt, ...) { - // TODO: Print timestamp and log level - // TODO: Use va_list to handle formatted string - printf("[%s] ", level); +int max_int(int count, ...) { + if (count <= 0) { + return 0; + } va_list args; - va_start(args, fmt); - vprintf(fmt, args); - va_end(args); - printf("\n"); -} + va_start(args, count); + + int result = va_arg(args, int); + for (int i = 1; i < count; i++) { + int next = va_arg(args, int); + if (next > result) { + result = next; + } + } -int main() { - log_message("INFO", "System started with code %d", 200); - log_message("ERROR", "Failed to open file: %s", "config.txt"); - return 0; + va_end(args); + return result; } ``` +Same structure as `average`, just "accumulate then divide" becomes "compare and keep the max". The `va_start` / `va_arg` / `va_end` trio is rehearsed once more here. + +::: + ### Exercise 2: Recursion vs. Iteration — Binary Search +**Difficulty: Intermediate** · two ways to write the same algorithm + Implement binary search using both recursion and iteration, and compare their performance and readability: ```c @@ -396,6 +418,8 @@ int main() { ### Exercise 3: Multiple Return Values in Practice +**Difficulty: Basic** · return multiple results through pointer parameters + Implement a function that calculates both the maximum and minimum values of an array: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/06-scope-and-storage.md b/documents/en/vol1-fundamentals/c_tutorials/06-scope-and-storage.md index 7738b0873..b529c26fb 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/06-scope-and-storage.md +++ b/documents/en/vol1-fundamentals/c_tutorials/06-scope-and-storage.md @@ -395,6 +395,8 @@ In actual projects, form a habit: **add `static` to all global variables and hel ### Exercise 1: Modular Counter +**Difficulty: Basic** · hide data with file-scope internal linkage + Design a simple module where the header file exposes only three functions: `counter_init`, `counter_inc`, and `counter_get`. Internally, use a `static` variable to maintain the count. External code must not be able to directly access or modify this counter variable. ```c @@ -413,6 +415,8 @@ Please implement `counter.c` yourself. ### Exercise 2: Multi-file Symbol Visibility +**Difficulty: Intermediate** · external linkage, internal linkage, and extern together + Create three files: `data.c`, `helper.c`, and `main.c`. Requirements: - `data.c` defines an external linkage global variable `g_sensor_data`, initial value `0`. @@ -448,28 +452,43 @@ int main(void) { } ``` -### Exercise 3: Lazy Initialization +### Exercise 3: Call Counter -Use a `static` local variable to implement a `get_config` function: on the first call, perform initialization (print "Initializing..." and set default values), subsequent calls directly return the initialized value without re-initializing. +**Difficulty: Basic** · keep state across calls with a static local variable + +Implement `call_count(void)`: each time it is called, it returns "which call this is". Use the property that a `static` local variable "does not get destroyed when the function returns". ```c -#include +/// @return which call this is (the first call returns 1) +int call_count(void); +``` -int *get_config(void) { - static int config = 0; - static int initialized = 0; +Hint: declare `static int n = 0;` inside the function, do `++n`, then return. Think some more: if you turned it into a plain local `int n = 0;` (dropping static), what would the result become, and why? - if (!initialized) { - printf("Initializing...\n"); - config = 42; // Load from EEPROM or something - initialized = 1; - } +::: details Reference answer - return &config; +```c +#include + +int call_count(void) { + static int n = 0; // initialized once; the value survives after the function returns + ++n; + return n; +} + +int main(void) { + printf("%d\n", call_count()); // 1 + printf("%d\n", call_count()); // 2 + printf("%d\n", call_count()); // 3 + return 0; } ``` -> Hint: `static` local variables are initialized only when entering the function for the first time—perfect for implementing "initialize once" semantics. +Drop `static` and `n` gets re-initialized to 0 on every entry, so `++n` returns 1 no matter how many times you call it: it loses the ability to "remember the last result". + +One common confusion worth clearing up: `static int n = 0;` is initialized when the program starts (not when `call_count` is first called), and this happens only once for the whole lifetime. Because it is initialized only once and the value is preserved afterwards, it works as a counter. + +::: ## Reference Resources diff --git a/documents/en/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/en/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index 331cef167..14986aca5 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/en/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -262,10 +262,14 @@ We have only laid the "foundation" for pointers so far. Next, we will tackle que ### Exercise 1: Addresses and Values +**Difficulty: Basic** · observe &, *, sizeof, and address spacing + Write a program that declares three variables of different types (`int`, `double`, `char`), prints their values, addresses, and the result of dereferencing their pointers. Observe if the spacing between addresses matches the size of each type. ### Exercise 2: Traversing Arrays with Pointers +**Difficulty: Intermediate** · walk an array with pointer arithmetic + Use pointer arithmetic to traverse an `int` array and print all elements. Do not use the `[]` operator; use only pointer addition and dereference: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md b/documents/en/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md index 3a5090239..b5d33ae2b 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md +++ b/documents/en/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md @@ -268,6 +268,8 @@ At this point, we have built a solid foundation in pointers. Next, we will learn ### Exercise 1: Linear Search with Pointers +**Difficulty: Basic** · pointer walk plus NULL on miss + Implement a linear search function that returns a pointer to the first occurrence of the target value in the array. If not found, return `NULL`. ```c @@ -279,6 +281,8 @@ int *find_int(int *arr, int size, int target) { ### Exercise 2: Array Reversal with Pointers +**Difficulty: Intermediate** · in-place reversal with two pointers + Implement a function that reverses an array in-place, using only pointer arithmetic (two pointers moving from both ends towards the middle), without using array subscripts: ```c @@ -289,6 +293,8 @@ void reverse(int *arr, int size) { ### Exercise 3: const Practice +**Difficulty: Basic** · the four const-and-pointer combinations + For each of the following declarations, determine which operations are legal and which will result in a compilation error: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md b/documents/en/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md index 795db2407..e1a6fc60c 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md +++ b/documents/en/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md @@ -288,6 +288,8 @@ The core logic of multi-level pointers is actually quite simple: each level stor ### Exercise: Allocation and Deallocation of Dynamic 2D Arrays +**Difficulty: Intermediate** · int** for a dynamic 2D array (mind rollback on a failed row) + Use multi-level pointers to implement the allocation, population, and deallocation of a dynamic two-dimensional array. Please implement the following three functions yourself: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md b/documents/en/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md index 84e72cb06..cbda7af3b 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md +++ b/documents/en/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md @@ -280,6 +280,8 @@ In this post, we looked at three independent but practical mechanisms. `restrict ### Exercise: Implement a Simple Opaque Pointer Module +**Difficulty: Intermediate** · opaque-pointer idiom for a stack module + Use the opaque pointer pattern to implement a simple Stack module. Requirements: - `stack.h`: Contains only the `struct Stack` forward declaration and function declarations. diff --git a/documents/en/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md b/documents/en/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md index 75355ebb4..2a2bd5c90 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md +++ b/documents/en/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md @@ -323,6 +323,8 @@ Function pointers are the core mechanism for implementing callbacks and the stra ### Exercise 1: Generic Sorting Interface +**Difficulty: Intermediate** · function pointer as the comparison strategy + Following the interface design of `qsort`, implement your own generic insertion sort function. Use it to sort an `int` array (ascending and descending) and a string array (lexicographical order): ```cpp @@ -331,12 +333,35 @@ void my_isort(void *base, size_t n, size_t size, int (*compar)(const void *, const void *)); ``` -### Exercise 2: Event Dispatch System Extension +### Exercise 2: Retry with a Max Attempt Count + +**Difficulty: Intermediate** · use a function pointer as a condition callback + +Implement `retry_until`: call the `check` function pointer repeatedly until it returns non-zero (success) or the max attempt count is reached. + +```c +/// @brief Call check repeatedly until it succeeds or max_attempts is reached +/// @param check the condition function; non-zero return means success +/// @param max_attempts maximum number of attempts +/// @return on success, which attempt succeeded (starting from 1); -1 if all attempts failed +int retry_until(int (*check)(void), int max_attempts); +``` + +Hint: `check` can simulate a peripheral that only becomes ready on the third try like this: + +```c +int device_ready(void) { + static int tried = 0; // the static local from Chapter 06, put to use here + return ++tried >= 3; +} +``` -Based on the event dispatch system in this chapter, support registering multiple callbacks for the same event (a callback chain) and support unregistering callbacks. Think about this: what happens if a handler in the chain modifies the linked list structure during execution? +Think about it: this pattern of turning the "condition to check" into a function pointer you pass in — what does it share with the `qsort` comparator and event dispatch in this chapter? ### Exercise 3: Simple Command-Line Calculator +**Difficulty: Intermediate** · table-driven dispatch with an array of function pointers + Use an array of function pointers to implement a command-line calculator supporting addition, subtraction, multiplication, division, and modulo operations. Select the corresponding function based on the user-inputted operator. ```cpp @@ -344,6 +369,14 @@ Use an array of function pointers to implement a command-line calculator support // double (*operations[])(double, double) = { ... }; ``` +### Exercise 4: Event Dispatch System Extension (Challenge, optional) + +**Difficulty: Challenge** · Optional, design a callback container, beginners can skip + +Based on the array-based event dispatch system in this chapter, extend it to support registering multiple callbacks for the same event, plus unregistering. Hint: you do not need a linked list — an array of function pointers can hold multiple callbacks, and unregistering can be done with a tombstone flag or a compaction move. + +Think about it: if one callback unregisters another callback while we are still iterating the callback array, what goes wrong? It is the same trap as "deleting from an array while iterating it". + ## References - [Function Pointer Declaration - cppreference](https://en.cppreference.com/w/c/language/pointer) diff --git a/documents/en/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md b/documents/en/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md index 1220c79cc..80a45af7a 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md +++ b/documents/en/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md @@ -371,6 +371,8 @@ The memory model of arrays is actually not complex—it's just a contiguous arra ### Exercise 1: Matrix Operations +**Difficulty: Intermediate** · 2D array transpose and multiply + Implement the following three functions to perform basic matrix operations. Matrices are represented using standard C two-dimensional arrays. Please implement matrix transposition and matrix multiplication yourself: ```c @@ -393,6 +395,8 @@ void print_matrix(int rows, int cols, int matrix[rows][cols]); ### Exercise 2: Compare VLA and malloc +**Difficulty: Intermediate** · trade-offs between VLA and malloc + Write a program that uses VLA and `malloc` respectively to allocate an integer array whose size is determined by user input, then compare the behavioral differences: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md b/documents/en/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md index 9f9c48cc1..6a03bccaf 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md +++ b/documents/en/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md @@ -317,38 +317,36 @@ A C string is simply a `\0`-terminated `char` array. Without the protection of t ### Exercise 1: Safe String Library -Implement a set of safe string manipulation functions where each function is aware of the destination buffer size and automatically handles truncation and termination: +**Difficulty: Intermediate** · wrap size-aware string operations + +Implement two safe string functions that each know the size of the destination buffer and handle truncation and termination automatically: ```c #include -/// @brief 安全地复制字符串到目标缓冲区 -/// @param dst 目标缓冲区 -/// @param src 源字符串 -/// @param dst_size 目标缓冲区总大小(含终止符) -/// @return 实际复制的字符数(不含终止符);如果 dst 为 NULL 返回 0 +/// @brief Safely copy a string into the destination buffer +/// @param dst destination buffer +/// @param src source string +/// @param dst_size total size of the destination buffer (including the terminator) +/// @return number of characters actually copied (excluding the terminator); 0 if dst is NULL size_t safe_str_copy(char* dst, const char* src, size_t dst_size); -/// @brief 安全地拼接字符串 -/// @param dst 目标缓冲区(已有内容) -/// @param src 要追加的字符串 -/// @param dst_size 目标缓冲区总大小(含终止符) -/// @return 拼接后字符串的总长度(不含终止符) +/// @brief Safely append a string +/// @param dst destination buffer (already holds some content) +/// @param src string to append +/// @param dst_size total size of the destination buffer (including the terminator) +/// @return total length of the concatenated string (excluding the terminator) size_t safe_str_cat(char* dst, const char* src, size_t dst_size); - -/// @brief 安全地格式化字符串 -/// @param dst 目标缓冲区 -/// @param dst_size 目标缓冲区总大小 -/// @param format 格式字符串 -/// @param ... 格式参数 -/// @return 实际写入的字符数(不含终止符) -size_t safe_str_format(char* dst, size_t dst_size, const char* format, ...); ``` -**Hint:** We can implement `safe_str_copy` based on `strncpy`, but we must ensure null termination. For `safe_str_cat`, we need to calculate the current length of the destination string first, then determine the remaining available space. We can implement `safe_str_format` directly using `vsnprintf`. +Hint: `safe_str_copy` can be based on `strncpy`, but `strncpy` does not write a terminator when src is too long, so you have to add one yourself. For `safe_str_cat`, first compute the current length of dst, then figure out the remaining space. + +**Challenge extension** (optional): add `safe_str_format(char* dst, size_t dst_size, const char* format, ...)`. It needs `vsnprintf` and the `` variadic mechanism (not covered here, and only briefly mentioned in the functions chapter). Look up `vsnprintf` on cppreference and then implement it. ### Exercise 2: String Splitting Function +**Difficulty: Basic** · walk a string with pointers, split on a delimiter + Implement a function that splits a string based on a delimiter: ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md b/documents/en/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md index 05ce40e1f..4d750560e 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md +++ b/documents/en/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md @@ -391,22 +391,91 @@ In this tutorial, we thoroughly dissected structures from "how to use them" to " ## Exercises -### Exercise: Design a Manually Aligned Communication Protocol Frame +### Exercise 1: Alignment Prediction and Verification -Please design a binary protocol frame structure for embedded device communication. Requirements are as follows: +**Difficulty: Basic** · first compute sizeof and offsetof by hand, then verify with code -1. **Frame Header**: 1-byte start flag `0xAA`, 1-byte frame type, 2-byte payload length, 4-byte timestamp. -2. **Payload**: Variable-length data (use a flexible array member). -3. **Frame Tail**: 2-byte CRC16 checksum. -4. Use `alignas(4)` to ensure the timestamp field is 4-byte aligned. -5. Use `__attribute__((packed))` to ensure the frame structure is compact (suitable for direct cast parsing of byte streams). -6. Write a function that uses `offsetof` to print the offset of each field to verify the layout. +Assume `int` has alignment 4 and `sizeof(int) == 4`. First compute the `offsetof` of every field and the `sizeof` of each struct below by hand, then write a program that prints them with `offsetof` and `sizeof` to check yourself: ```c -// TODO: Write your code here +#include +#include + +typedef struct { + char a; + int b; + char c; +} StructA; + +typedef struct { + int b; + char a; + char c; +} StructB; + +typedef struct { + char a; + char c; + int b; +} StructC; ``` -**Hint**: When using `alignas` inside a `packed` structure, be careful—`packed` removes automatic padding, but `alignas` can force a specific field's alignment. Think about this: in a packed structure, if the offset from the frame header to the timestamp is not a multiple of 4, how would you handle it? +Think about it: the three structs have exactly the same fields, only in a different order — why is `sizeof` different? Which one wastes the least space? + +::: details Reference answer + +Hand-computed results (`int` aligned to 4 bytes): + +| struct | a offset | b offset | c offset | sizeof | +|--------|----------|----------|----------|--------| +| StructA | 0 | 4 | 8 | 12 | +| StructB | 4 | 0 | 5 | 8 | +| StructC | 0 | 4 | 1 | 8 | + +In StructA, `a` is at 0, `b` needs 4-byte alignment so 3 padding bytes are inserted before it at offset 4, `c` is at 8, and the tail is padded to 12. Grouping the larger-alignment fields (`int`) and the small fields (`char`) each together reduces the middle padding. StructB and StructC are both 8 bytes, smaller than StructA's 12. + +::: + +### Exercise 2: packed vs Explicit Alignment + +**Difficulty: Intermediate** · compare default alignment, packed, and alignas + +For the same set of fields, define the struct in three ways — default alignment, `__attribute__((packed))`, and `alignas` — and print `sizeof` and each field's offset to see how the three layouts differ: + +```c +#include +#include +#include + +typedef struct { + uint8_t type; + uint32_t value; +} FrameNormal; + +typedef struct __attribute__((packed)) { + uint8_t type; + uint32_t value; +} FramePacked; + +typedef struct { + uint8_t type; + alignas(4) uint32_t value; +} FrameAligned; +``` + +Think about it: `FramePacked` saves the most space, so why is accessing the `value` field slower on some CPUs, or even crashing? When should you use packed, and when should you use explicit alignment? + +### Exercise 3: Communication Protocol Frame Design (Challenge, optional) + +**Difficulty: Challenge** · Optional, needs CRC and endianness, beginners can skip + +Design a binary protocol frame for embedded device communication: a header (start byte, frame type, payload length, timestamp), a variable-length payload (flexible array member), and a tail checksum. + +- Use `offsetof` to print each field's offset and verify the layout +- The tail checksum field can be a 2-byte placeholder for now, with a `// TODO: fill CRC16` comment; you do not have to implement the CRC algorithm yet +- Think about it: when devices with different endianness (big-endian, little-endian) talk to each other, how should multi-byte fields (like the timestamp) be handled? + +Hint: this chapter covered flexible array members, `alignas`, `__attribute__((packed))`, and `offsetof` — those are your tools. CRC algorithms and endianness conversion belong to the communication topic; here you only need to be aware of these two issues and leave a placeholder, no full implementation required. ## References diff --git a/documents/en/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md b/documents/en/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md index 286964a70..4e7a5e24f 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md +++ b/documents/en/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md @@ -373,6 +373,8 @@ In this post, we covered four C language features—unions, enums, bit-fields, a ### Exercise 1: IEEE 754 Float Decomposition +**Difficulty: Intermediate** · crack a float's bits with a union + Use a union to implement a tool that decomposes a `float` value into IEEE 754 format sign bit, exponent, and mantissa, and prints them out. ```c @@ -384,6 +386,8 @@ Use a union to implement a tool that decomposes a `float` value into IEEE 754 fo ### Exercise 2: 32-bit Hardware Control Register +**Difficulty: Basic** · map a register with bit-fields plus a union view + Use bit fields to define a 32-bit hardware control register struct, then write functions to manipulate it. ```c @@ -392,6 +396,8 @@ Use bit fields to define a 32-bit hardware control register struct, then write f ### Exercise 3: Simple Tagged Union +**Difficulty: Basic** · enum + union + tag-checked access + Use an enum and a union to implement a tagged union that can store an `int`, a `float`, or a string pointer. ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/14-dynamic-memory.md b/documents/en/vol1-fundamentals/c_tutorials/14-dynamic-memory.md index 9f131a479..0fa049101 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/14-dynamic-memory.md +++ b/documents/en/vol1-fundamentals/c_tutorials/14-dynamic-memory.md @@ -210,26 +210,133 @@ We started with memory layout, clarified the roles of the stack and heap, dissec ## Exercises -### Exercise 1: Fixed-Size Memory Pool Allocator +### Exercise 1: A Growing Array with realloc -Implement a simple fixed-size memory pool that slices fixed-size blocks from a large chunk of memory, supporting allocation and reclamation. +**Difficulty: Basic** · grow capacity with realloc, building on this chapter's realloc section -```cpp -// TODO: Implement allocate() and deallocate() -void* allocate(size_t size); -void deallocate(void* ptr); +Implement a simple dynamic array of ints: initial capacity 4, and each `push_back` doubles the capacity with `realloc` when full. + +```c +#include + +typedef struct { + int* data; + size_t size; + size_t capacity; +} IntVec; + +/// @brief initialize with capacity 4 +void intvec_init(IntVec* v); +/// @brief append to the tail; doubles capacity when full, returns -1 on failure +int intvec_push(IntVec* v, int value); +/// @brief release +void intvec_free(IntVec* v); ``` -Hint: Use a linked list to manage free blocks—the first few bytes of each free block store a pointer to the next free block. +Hint: `realloc(NULL, n)` is equivalent to `malloc(n)`, so even the first allocation can go through realloc. Note that `realloc` returns NULL on failure and does not free the old block — catch the return value in a temporary, confirm success, and only then overwrite the old pointer; otherwise you leak. -### Exercise 2: malloc/free Wrapper with Statistics +::: details Reference answer -Implement a wrapper layer for `malloc` and `free` that tracks all allocation and deallocation operations, printing a statistical report when the program exits. +```c +#include -```cpp -// TODO: Implement tracked_malloc() and tracked_free() -void* tracked_malloc(size_t size); -void tracked_free(void* ptr); +void intvec_init(IntVec* v) { + v->capacity = 4; + v->size = 0; + v->data = malloc(v->capacity * sizeof(int)); +} + +int intvec_push(IntVec* v, int value) { + if (v->size >= v->capacity) { + size_t new_cap = v->capacity * 2; + int* new_data = realloc(v->data, new_cap * sizeof(int)); + if (new_data == NULL) { + return -1; // grow failed; the old data is still valid, let the caller decide + } + v->data = new_data; + v->capacity = new_cap; + } + v->data[v->size++] = value; + return 0; +} + +void intvec_free(IntVec* v) { + free(v->data); + v->data = NULL; + v->size = v->capacity = 0; +} +``` + +The key is to store the `realloc` return value in `new_data` first and only assign it to `v->data` after success. If you wrote `v->data = realloc(v->data, ...)` directly, a failure would overwrite the original `v->data` with NULL, and that memory is lost forever — a leak. + +::: + +### Exercise 2: Memory Error Diagnosis + +**Difficulty: Intermediate** · identify the memory errors from this chapter with ASan or Valgrind + +The code below hides at least four memory errors. First read the code and guess what each issue is, then run it with `gcc -fsanitize=address` (or Valgrind) and record the error type and location reported by the tool: + +```c +#include + +int main(void) { + int* p = malloc(4 * sizeof(int)); + p[4] = 42; // (1) what is wrong here? + + int* q = malloc(sizeof(int)); + free(q); + *q = 100; // (2) and here? + + int* r = malloc(1024); + /* forgot to free(r) */ // (3) which category is this? + + free(p); + free(p); // (4) one more + return 0; +} +``` + +Requirement: for each one, name which kind of error from this chapter it is (out-of-bounds write, use-after-free, leak, double free, uninitialized read), and describe how the tool reports it. + +::: details Reference answer + +(1) `p[4]`: only 4 ints were allocated (indices 0–3), so `p[4]` is a heap-buffer-overflow write. ASan reports `heap-buffer-overflow`. + +(2) `*q = 100`: `q` was freed and then written — use-after-free. ASan reports `heap-use-after-free`. + +(3) `r` is never freed: a memory leak. Valgrind's `LEAK SUMMARY` lists it; ASan on most platforms checks for leaks by default (`detect_leaks=1`) and reports `Detected memory leaks` at exit. + +(4) `free(p)` twice: double free. ASan reports `attempting double-free`. + +These four cover exactly the typical memory errors from this chapter; the tool's error keyword lets you quickly tell which kind it is. + +::: + +### Exercise 3: Fixed-Size Memory Pool Allocator (Challenge, optional) + +**Difficulty: Challenge** · Optional, needs the free-list idiom; self-study required + +Implement a fixed-size memory pool: slice fixed-size blocks out of one large chunk and manage free blocks with a linked list — the first few bytes of each free block store a pointer to the next free block. Look up the "in-place linked list / free list" technique before writing it. + +```c +typedef struct MemoryPool MemoryPool; +MemoryPool* pool_create(size_t block_size, size_t block_count); +void* pool_alloc(MemoryPool* pool); +void pool_free(MemoryPool* pool, void* block); +void pool_destroy(MemoryPool* pool); +``` + +Think about it: compared to calling `malloc`/`free` directly, what does a memory pool buy you in an embedded or real-time system? Why can it do O(1) allocate/free with no fragmentation? + +### Exercise 4: malloc/free Wrapper with Statistics (Challenge, optional) + +**Difficulty: Challenge** · Optional, best done after Chapter 15 on the preprocessor + +Wrap `malloc`/`free` to record the file and line of each allocation, and print the still-unfreed list when the program exits. You will need the `__FILE__`/`__LINE__` macros (covered in Chapter 15) and `atexit` to register an exit hook. + +```c +#define TMALLOC(size) tracked_malloc((size), __FILE__, __LINE__) ``` -Hint: Use an array or linked list to record information for each allocation. `atexit` can register an exit hook. +Hint: use an array or linked list to record each allocation's address, size, and location; on `free`, match by address and mark it freed; register `mem_report` with `atexit` to print the remaining unfreed entries at exit. diff --git a/documents/en/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md b/documents/en/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md index 44dcc66ca..b4f6117c1 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md +++ b/documents/en/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md @@ -198,6 +198,8 @@ Although the preprocessor is primitive, it remains an indispensable glue for mul ### Exercise 1: Build a Multi-File Modular Project +**Difficulty: Basic** · .h/.c split plus packing a static library + ```c // math_utils.h #pragma once @@ -221,6 +223,8 @@ int main(void) { ### Exercise 2: Zero-Overhead DEBUG_LOG Macro +**Difficulty: Intermediate** · conditional compilation plus variadic macros + ```c // debug_log.h #pragma once diff --git a/documents/en/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md b/documents/en/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md index 54bba8ca8..71de68252 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md +++ b/documents/en/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md @@ -250,6 +250,8 @@ The core of file operations lies in `fopen` and `fread`/`fwrite`/`fgets`/`fputs` ### Exercise 1: Configuration File Parser +**Difficulty: Intermediate** · parse key=value line by line with fgets + Parse a configuration file in `.ini` format, ignoring `#` comments and empty lines. ```c @@ -263,6 +265,8 @@ Hint: Use `fgets` to read line by line, `strchr` to find the `=` position, and t ### Exercise 2: File Copy Tool +**Difficulty: Basic** · fread/fwrite with a progress bar + Specify source and destination files via command-line arguments, support binary file copying, and display progress. ```bash diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md index be3f79087..cd8ea5847 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md @@ -349,57 +349,43 @@ Many operations that require table lookups at runtime (CRC calculations, bit man ## Exercises -We leave the following exercises for you to tinker with—hands-on research, coding, and hardware verification are the true path to learning. +We leave the following exercises for you to tinker with. This chapter is mostly a theoretical overview, so the exercises are mainly conceptual analysis; the ones that need a board or QEMU are marked as optional challenges. -```c -/// @brief 练习 1:读取 IPSR 寄存器 -/// 使用 GCC 内嵌汇编读取 Cortex-M 的 IPSR 寄存器值 -/// 解释在正常运行和进入中断服务函数时读到的值有什么不同 -/// 提示:IPSR 是 xPSR 的一部分,可以用 MRS 指令读取 -uint32_t exercise_read_ipsr(void) -{ - // 练习: 用内嵌汇编读取 IPSR - return 0; -} -``` +### Exercise 1: The Role of the IPSR Register -```c -/// @brief 练习 2:触发并调试 HardFault -/// 对一个无效地址执行写操作,故意触发 HardFault -/// 然后在 HardFault Handler 中读取入栈的寄存器值 -/// 定位导致异常的指令地址 -/// 提示:HardFault Handler 的参数可以拿到栈帧指针 -void exercise_trigger_hardfault(void) -{ - // 练习: 写一个无效地址来触发 HardFault -} -``` +**Difficulty: Basic** · conceptual analysis, no code required + +IPSR is part of the Cortex-M program status register (xPSR). Answer: when is IPSR updated by hardware, and what does it record? Why does reading IPSR inside an interrupt service routine help you tell "which exception is currently being handled"? + +### Exercise 2: HardFault Debugging Flow + +**Difficulty: Intermediate** · describe the debugging flow in words, no board required + +When a program triggers a HardFault by accessing an illegal address, the hardware pushes the current registers onto the stack. Describe the whole flow from HardFault trigger to locating "the faulting instruction": which values on the stack do you need to read, and where does the pushed PC point? + +Hint: this chapter's exception-handling section covered that the stack-frame pointer the HardFault Handler receives points at `{R0, R1, R2, R3, R12, LR, PC, xPSR}`. + +### Exercise 3: Analyzing AAPCS Argument Passing + +**Difficulty: Intermediate** · needs the arm-none-eabi-gcc toolchain + +Write two functions: one taking 4 int arguments, the other 6. Disassemble and compare the call sequences with `arm-none-eabi-objdump -d`: the first 4 arguments go in R0–R3, but where do the 5th and 6th go? ```c -/// @brief 练习 3:分析 AAPCS 的参数传递 -/// 写两个函数:一个接受 4 个 int 参数,另一个接受 6 个 -/// 用 arm-none-eabi-objdump -d 反汇编对比调用序列 -/// 找出编译器如何分配 R4-R11 给局部变量 -int exercise_aapcs_4(int a, int b, int c, int d) -{ - // 练习: 添加局部变量和函数调用,使反汇编更有看头 - return 0; +int exercise_aapcs_4(int a, int b, int c, int d) { + return a + b + c + d; } -int exercise_aapcs_6(int a, int b, int c, int d, int e, int f) -{ - // 练习: 同上,对比反汇编结果 - return 0; +int exercise_aapcs_6(int a, int b, int c, int d, int e, int f) { + return a + b + c + d + e + f; } ``` -```c -/// @brief 练习 4(进阶):向量表重定位 -/// 阅读一个 Cortex-M 启动文件(如 startup_stm32f407xx.s) -/// 画出完整的向量表布局 -/// 然后修改链接脚本把向量表重定位到 RAM 中 -/// 实现运行时动态修改中断向量(Bootloader 开发的基础技能) -``` +### Exercise 4: Vector Table Relocation (Challenge, optional) + +**Difficulty: Challenge** · Optional, needs startup-file, linker-script, and bootloader background + +Read a Cortex-M startup file (e.g. startup_stm32f407xx.s) and draw the complete vector-table layout; then modify the linker script to relocate the vector table to RAM and enable runtime patching of interrupt vectors. This is the foundation of bootloader development. ## Reference Resources diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md index b718828a5..93b9601de 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md @@ -368,15 +368,15 @@ C++ standard library containers are also designed with caching factors in mind. ## Exercises -1. **Stride Experiment Verification**: Modify the stride test code from this article to shrink the array to 4 MB (which should fit into most CPUs' L3 cache, avoiding interference from main memory latency), and focus on the `per_access` column. Observe the change in single-access latency as the stride increases from one to 32. Think about it: why does `per_access` only start to rise significantly after the stride exceeds 16 (a cache line boundary)? Can the byte count corresponding to this inflection point be used to deduce the cache line size of your machine? +1. **Stride Experiment Verification** (Basic): Modify the stride test code from this article to shrink the array to 4 MB (which should fit into most CPUs' L3 cache, avoiding interference from main memory latency), and focus on the `per_access` column. Observe the change in single-access latency as the stride increases from one to 32. Think about it: why does `per_access` only start to rise significantly after the stride exceeds 16 (a cache line boundary)? Can the byte count corresponding to this inflection point be used to deduce the cache line size of your machine? -2. **Reproduce False Sharing**: Write a multi-threaded program (using pthreads or C++ ``) that creates two threads, each incrementing a different field in a shared struct one hundred million times. First, run it without alignment, then use `alignas(64)` to align the two fields to different cache lines and run it again. Compare the execution times. +2. **Reproduce False Sharing** (Intermediate): Write a multi-threaded program (using pthreads or C++ ``) that creates two threads, each incrementing a different field in a shared struct one hundred million times. First, run it without alignment, then use `alignas(64)` to align the two fields to different cache lines and run it again. Compare the execution times. -3. **Matrix Transpose Optimization**: Implement a square matrix transpose function. First, write a naive double-loop version, then try blocking—split the matrix into 32x32 small blocks and perform the transpose within each block. Compare the performance difference of the two versions on a large matrix (2048x2048). +3. **Matrix Transpose Optimization** (Intermediate): Implement a square matrix transpose function. First, write a naive double-loop version, then try blocking—split the matrix into 32x32 small blocks and perform the transpose within each block. Compare the performance difference of the two versions on a large matrix (2048x2048). -4. **AoS vs SoA Benchmark**: Define a particle struct containing `float x, y, z, r, g, b`, and create one hundred thousand particles. Implement "normalize all particle coordinates to the unit sphere" using both AoS and SoA layouts, and compare the execution times. +4. **AoS vs SoA Benchmark** (Basic): Define a particle struct containing `float x, y, z, r, g, b`, and create one hundred thousand particles. Implement "normalize all particle coordinates to the unit sphere" using both AoS and SoA layouts, and compare the execution times. -5. **Cache-Friendly Linked List**: Reference the design philosophy of the Linux kernel's `list_head` to implement an intrusive doubly linked list. Store the node data domain and the linked list pointer domain separately so that traversing the linked list pointers does not require loading the entire node data, thereby improving cache hit rates. +(The original also had a "Cache-Friendly Linked List" exercise; it was removed because it needs intrusive-container background and couples loosely with the cache topic. It fits better in the linked-list chapter, advanced_feature/06.) ## References diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md index f6f736973..f69f284ec 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md @@ -349,7 +349,7 @@ Several key C++ improvements are worth special mention. Brace initialization (`{ ## Practice Exercises -Here are a few practice problems. The code intentionally contains traps; please find and fix them. +Here are a few practice problems. The code intentionally contains traps; please find and fix them. All six are **Difficulty: Basic**, each targeting one trap covered in this chapter (greedy matching, precedence, assignment vs comparison, stray semicolon, integer overflow, synthesis). ```cpp // Exercise 1: Fix the precedence issue diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md index 3992a8df4..eb70dc641 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md @@ -560,6 +560,8 @@ In our C implementation, `shape_destroy()` uses the vtable to find the correct ` ### Exercise 1: Triangle Extension +**Difficulty: Basic** · add a shape following the vtable + Add a `Triangle` type to the graphics framework (represented by three side lengths): ```c @@ -576,6 +578,8 @@ Triangle* triangle_create(const char* name, int id, ### Exercise 2: Shape Sorting +**Difficulty: Intermediate** · qsort plus a function-pointer comparator + Add area sorting functionality to `ShapeManager`: ```c @@ -587,6 +591,8 @@ void shape_manager_sort_by_area(ShapeManager* mgr); ### Exercise 3: Opaque Pointer Counter +**Difficulty: Intermediate** · redo Counter with an opaque pointer + Refactor the `Counter` from step two into an opaque pointer version. The header file should only expose `typedef struct Counter Counter;` and the operation functions, while the implementation file hides the full definition. Please split the header and implementation files yourself, and provide a `counter_create()` function that returns a heap-allocated object. ## References diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md index 0ac1f7a7b..312d0ca25 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md @@ -506,6 +506,8 @@ The following exercises provide only the function signature and requirement desc ### Exercise 1: Implement resize +**Difficulty: Basic** · reserve plus fill with a default value + `reserve` only changes capacity, not size, whereas `resize` needs to change size. When the new size is greater than the old size, the extra positions should be filled with default values. ```c @@ -521,6 +523,8 @@ DynamicArrayStatus dynamic_array_resize( ### Exercise 2: Implement filter +**Difficulty: Basic** · predicate that returns a new array + Given a dynamic array and a filter predicate, return a newly created dynamic array containing only the elements that satisfy the condition. ```c @@ -534,6 +538,8 @@ DynamicArray* dynamic_array_filter( ### Exercise 3: Implement map transformation +**Difficulty: Intermediate** · transform with a possibly different element size + Given a dynamic array and a transformation function, we apply the transformation function to each element and store the results in a new array to return. ```c @@ -549,6 +555,8 @@ DynamicArray* dynamic_array_map( ### Exercise 4: Implementing Concatenation +**Difficulty: Basic** · concatenate two arrays of the same type + Concatenate two dynamic arrays of the same type into a new dynamic array. ```c diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md index da7f92d54..3b83d47d7 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md @@ -625,6 +625,8 @@ At this point, we have built a complete singly linked list from scratch. The nod ### Exercise 1: Reverse Linked List +**Difficulty: Basic** · three pointers, O(1) space + Implement a function to reverse a singly linked list in place. The space complexity must be O(1), and you cannot allocate new nodes. ```c @@ -637,6 +639,8 @@ void linked_list_reverse(LinkedList* list); ### Exercise 2: Merge Two Sorted Linked Lists +**Difficulty: Basic** · two-pointer merge + Given two linked lists sorted in ascending order, merge them into a new sorted linked list. ```c @@ -651,6 +655,8 @@ LinkedList* linked_list_merge_sorted(const LinkedList* a, const LinkedList* b); ### Exercise 3: Detect List Cycle +**Difficulty: Intermediate** · Floyd's tortoise and hare + Determine if a linked list contains a cycle (where a node's `next` pointer points to a node that has already appeared). ```c @@ -663,6 +669,8 @@ bool linked_list_has_cycle(const LinkedList* list); ### Exercise 4: Full API with Sentinel +**Difficulty: Intermediate** · full API with a sentinel node + Re-implement the full linked list API (`push_front`, `push_back`, `insert_at`, `remove`, `find`) using a sentinel node, and observe which special-case checks are eliminated by the sentinel node. ## Resources diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md index 4382750cd..42ab3d696 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md @@ -419,6 +419,8 @@ C++ improvements for embedded code focus on three areas: ### Exercise 1: Generic Ring Buffer +**Difficulty: Intermediate** · turn the uint8_t version into a generic void* version + Refactor the `uint8_t` ring buffer from the text into a generic version (using `void*` + element size): ```c @@ -436,21 +438,29 @@ bool ring_pop(generic_ring_t *ring, void *data); **Hint**: Use `memcpy` internally for generic byte copying. Change `head`/`tail` to absolute counts (don't worry about overflow), and calculate the actual index via `count % capacity`. -### Exercise 2: Portable UART Abstraction Layer +### Exercise 2: UART Abstraction Layer Interface Design + +**Difficulty: Intermediate** · design the interface only, do not implement the interrupt timing -Design a chip-independent abstraction layer interface for a UART peripheral. The driver needs two ring buffers (TX and RX). The application writes to the buffer first and then triggers the transmit interrupt; actual byte-by-byte transmission is completed in the ISR. +Following the peripheral-abstraction approach in this chapter, design a chip-independent UART abstraction-layer interface. You do not have to implement interrupt-driven byte-by-byte transmission — just define the interface clearly: which fields the driver struct needs (TX/RX buffers, state), the signatures of `init`/`write`/`read`, and "what write returns when the buffer is full". ```c -// uart.h -void uart_init(uint32_t baudrate); -void uart_send_byte(uint8_t data); -bool uart_receive_byte(uint8_t *data); -void UART_IRQHandler(void); +typedef struct { /* your design */ } UartDriver; + +void uart_init(UartDriver* uart, uint32_t baud); +size_t uart_write(UartDriver* uart, const uint8_t* data, size_t len); +size_t uart_read(UartDriver* uart, uint8_t* data, size_t len); ``` -### Exercise 3: Linker Script and Startup Code +Think about it: why does hiding the buffers and state inside the struct, exposing only function interfaces, keep upper-layer code from being tied to a specific chip? + +### Exercise 3: Reading a Linker Script + +**Difficulty: Basic** · explain an existing script, no need to write one from scratch + +Find an existing Cortex-M linker script (one was shown in this chapter's startup-flow section) and explain it section by section: which regions does `MEMORY` define? Why is the vector table placed at the start of Flash? What does `AT > FLASH` on the `.data` section mean? Why does the `.bss` section use `NOLOAD`? -Write a minimal linker script and startup code for an ARM Cortex-M4 (256K Flash, 64K SRAM). Requirements: define correct MEMORY regions, place the vector table at the start of Flash, handle `.data` section address separation, zero out `.bss`, and add a safe infinite loop after `main`. +Also think: to change Flash to 256K and SRAM to 64K in this script, which lines do you need to edit? ## Reference Resources diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md index dbb401041..71e13cc26 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md @@ -583,33 +583,52 @@ C++20 introduced the Modules system, aiming to fundamentally replace the header ## Exercises -### Exercise 1: Opaque Pointer String Hash Map +### Exercise 1: Opaque-Pointer Stack Module -Implement a simple string-to-integer map using opaque pointers to hide the internal implementation. Requirements: +**Difficulty: Intermediate** · build on this chapter's ring_buffer module, swap in a different data structure -```c -// inc/strmap.h -StrMap* strmap_create(void); -void strmap_destroy(StrMap* map); +Following the opaque-pointer ring_buffer in this chapter, implement an opaque-pointer stack module. The header exposes only `typedef struct Stack Stack;` and the interface functions; the internal struct lives in the .c file: -int strmap_put(StrMap* map, const char* key, int value); -int strmap_get(StrMap* map, const char* key, int* out_value); -void strmap_remove(StrMap* map, const char* key); +```c +// stack.h +typedef struct Stack Stack; +Stack* stack_create(size_t capacity); +void stack_destroy(Stack* s); +int stack_push(Stack* s, int value); // returns -1 when full +int stack_pop(Stack* s); // decide yourself what to do when empty +size_t stack_size(const Stack* s); ``` -**Hint:** Internally, you can use a simple array of linked lists (separate chaining) to implement the hash map. The hash function can use the classic `djb2` algorithm. Remember that all internal types and helper functions must be hidden in the `.c` file. +Think about it: why does the header only say `typedef struct Stack Stack;` instead of expanding the struct definition? Can a caller that gets the pointer do `s->top` to access a member? ### Exercise 2: Platform Abstraction Layer Practice -Write a platform abstraction layer for the hash map in Exercise 1 to replace the standard library's `malloc`/`free`. Requirements: +**Difficulty: Intermediate** · write two backends for the same interface + +Design a platform abstraction layer for the reusable module in this chapter (or the stack from Exercise 1) to replace direct calls to `malloc`/`free`: ```c -// inc/platform.h -void* plat_malloc(size_t size); -void plat_free(void* ptr); +// pal.h +void* pal_alloc(size_t size); +void pal_free(void* ptr); ``` -Please implement two versions: one using the standard library `malloc`/`free` (suitable for PC), and another using a static memory pool (suitable for embedded bare-metal environments). The hash map's `.c` file should allocate memory by including `platform.h` and calling `plat_malloc`, rather than directly calling `malloc`. +Implement two versions: one using the standard library `malloc`/`free` (suitable for PC), and one using a static memory pool (suitable for embedded bare metal). The module's .c file allocates memory by including `pal.h`. + +### Exercise 3: Opaque-Pointer String Hash Map (Challenge, optional) + +**Difficulty: Challenge** · Optional, needs separate-chaining hash-map background, beginners can skip + +Implement an opaque-pointer string-to-integer hash map (internally an array of linked lists with separate chaining, `djb2` as the hash function). Finish the linked-list chapter (advanced_feature/06) first, then come back to implement the chaining structure for collision resolution. + +```c +typedef struct HashMap HashMap; +HashMap* hashmap_create(size_t bucket_count); +void hashmap_destroy(HashMap* map); +int hashmap_insert(HashMap* map, const char* key, int value); +int hashmap_lookup(const HashMap* map, const char* key, int* out); +int hashmap_remove(HashMap* map, const char* key); +``` ## Reference Resources diff --git a/documents/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md b/documents/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md index c6cb196b3..6c2eb6047 100644 --- a/documents/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md +++ b/documents/vol1-fundamentals/c_tutorials/01-program-structure-and-compilation.md @@ -384,6 +384,8 @@ ODR(One Definition Rule)是 C++ 链接模型的核心规则:一个实体 ### 练习 1:多文件编译实战 +**难度:基础** · 多文件分步编译与符号表 + 构建一个多文件项目,包含以下文件: **utils.h**: @@ -407,6 +409,8 @@ void print_result(const char* label, int value); ### 练习 2:printf 格式化练习 +**难度:基础** · 练 printf 的宽度、精度、对齐 + 不查资料,写出以下 `printf` 语句的预期输出(然后再编译运行验证): ```c 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 ac9691a0f..914e8f672 100644 --- a/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/02A-data-types-basics.md @@ -273,6 +273,8 @@ uint8_t z{1000}; // C++ 编译错误!1000 超出 uint8_t 范围 ### 练习 1:类型探测器 +**难度:基础** · 用 sizeof 摸清各类型大小 + 编写一个程序,打印以下所有类型的 `sizeof` 值,并对照标准检查它们是否满足最低保证: ```c @@ -334,6 +336,8 @@ sizeof(size_t) = 8 bytes ### 练习 2:溢出观察 +**难度:基础** · 看有符号溢出 UB 与无符号回绕 + 分别对有符号 `int` 和无符号 `unsigned int` 做溢出实验: ```c 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 31d1e5bbc..c85f0dc44 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 @@ -309,6 +309,8 @@ C++ 在类型系统上做了大量的安全加固,很多改进直接瞄准了 ### 练习 1:浮点精度侦探 +**难度:基础** · 用 epsilon 判断浮点相等 + 预测以下代码的输出,然后编译运行验证你的预测: ```c @@ -378,6 +380,8 @@ int float_equal(float a, float b) { ### 练习 2:隐式转换陷阱 +**难度:基础** · 有符号数与 size_t 混用的坑 + 下面这段代码有一个隐藏的 bug,找出它并解释原因: ```c @@ -421,32 +425,35 @@ if (target < (int)(sizeof(values) / sizeof(values[0]))) { ### 练习 3:const 实战 -写一个函数,接收一个字符串,统计其中某个字符出现的次数。函数签名中正确使用 `const`: +**难度:基础** · 辨析 const 保护了什么、参数该怎么传 + +读下面这段代码,回答四个问题: ```c -/// @brief 统计字符 ch 在字符串 str 中出现的次数 -/// @param str 不可修改的字符串 -/// @param ch 要查找的字符 -/// @return 出现次数 -size_t count_char(const char* str, char ch); +// sum 承诺不改 data 指向的内容 +int sum(const int* data, size_t n); // (1) 参数加 const 是什么契约? + +void f(void) { + const int limit = 100; // (2) 局部变量加 const 有什么用? + int arr[3] = {1, 2, 3}; + limit = 200; // (3) 这行能编译吗? + sum(arr, 3); // (4) int* 传给 const int* 参数,合法吗? +} ``` +再反向想:如果 `sum` 的参数是 `int*`(没有 const),而调用者传一个 `const int carr[3]`,会发生什么?为什么? + ::: details 参考答案 -```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; -} -``` +(1) const 是只读契约:告诉调用者和编译器,`sum` 不会通过 `data` 改数组内容。 + +(2) `limit` 加 const 后变成只读变量,不能再赋值,编译器也能据此做优化。 + +(3) 不能编译。`limit` 是 read-only,给它赋值会直接报错。 + +(4) 合法。把 `int*` 传给 `const int*` 参数属于"加严"(从可写到只读),是安全的隐式转换。 + +反向:把 `const int*` 传给 `int*` 参数会丢掉 const 保护,编译器会警告甚至报错;如果非要做,得显式强转,但这很危险,函数内部可能改了本不该改的数据。 ::: diff --git a/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md b/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md index b8eb3a244..ebea43dfc 100644 --- a/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/03A-operators-basics.md @@ -256,6 +256,8 @@ C++ 在运算符方面做了两件重要的事情。第一件是引入了 `` 中的 `std::bits ### 练习 1:位操作工具集 +**难度:进阶** · 置位、清零、翻转、取位域 + 实现以下位操作函数: ```c @@ -345,6 +347,8 @@ uint32_t bit_extract(uint32_t value, int high, int low) { ### 练习 2:安全的移位 +**难度:基础** · 挡住移位 UB 的边界检查 + 写一个函数,安全地执行左移操作,处理所有边界情况: ```c @@ -376,6 +380,8 @@ uint32_t safe_shift_left(uint32_t val, int n, int bits) { ### 练习 3:表达式分析 +**难度:基础** · 判定序列点与未定义行为 + 分析以下表达式的求值行为(不实际运行),标出每个是"明确定义"、"未指定行为"还是"未定义行为": ```c diff --git a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md index bae088883..6e40c20aa 100644 --- a/documents/vol1-fundamentals/c_tutorials/04-control-flow.md +++ b/documents/vol1-fundamentals/c_tutorials/04-control-flow.md @@ -467,6 +467,8 @@ C++17 引入了 `if constexpr`,它在编译期评估条件,直接把不满 ### 练习 1:月份天数 +**难度:基础** · 练 switch 的穿透特性 + 用 `switch` 实现一个函数,根据月份和是否闰年返回该月的天数。要求利用穿透特性合并同天数的月份。 ::: details 参考答案 @@ -494,6 +496,8 @@ int month_day(int year, int month) { ### 练习 2:安全的矩阵搜索 +**难度:进阶** · 跳出多层循环的两种姿势 + 在二维矩阵中查找目标值。找到后用两种方式跳出多层循环:一种用标志变量,一种用 `goto`。 ```c @@ -506,18 +510,79 @@ typedef struct { SearchResult matrix_search(int** matrix, int rows, int cols, int target); ``` -### 练习 3:带超时的等待 +### 练习 3:手写一个协议帧解析状态机 -实现一个带超时机制的等待函数,避免裸 `while` 等待导致的死锁: +**难度:进阶** · 用 switch 加状态变量实现状态机 + +本篇末尾演示过一个逐字节驱动的串口协议状态机(起始符 `0xAA` → 载荷长度 → 载荷 → 结束符 `0x55`)。请你自己实现一个等价的解析器:把收到的字节逐个喂给 `frame_feed`,它内部用 `switch (state)` 在状态之间转移,收完一帧后把载荷打印出来。 ```c -/// @brief 等待某个条件满足或超时 -/// @param check 条件检查函数,返回非零表示条件满足 -/// @param timeout_ms 超时时间(毫秒) -/// @return 0 表示条件满足,-1 表示超时 -int wait_with_timeout(int (*check)(void), unsigned int timeout_ms); +#include + +typedef enum { STATE_IDLE, STATE_LEN, STATE_PAYLOAD, STATE_DONE } FrameState; + +/// @brief 逐字节喂入;收到完整一帧(含结束符)时返回 1,否则返回 0 +int frame_feed(uint8_t byte); ``` +要求用 `switch` 加一个显式的状态变量来实现,别用一长串 if-else。再想一件事:如果对端发来的"载荷长度"字段被篡改成一个超出缓冲区大小的值,你的状态机会被带崩吗?该怎么防? + +::: details 参考答案 + +```c +#include +#include + +#define MAX_PAYLOAD 16 + +typedef enum { STATE_IDLE, STATE_LEN, STATE_PAYLOAD, STATE_DONE } FrameState; + +static FrameState state = STATE_IDLE; +static uint8_t payload[MAX_PAYLOAD]; +static uint8_t payload_len = 0; +static uint8_t payload_idx = 0; + +int frame_feed(uint8_t byte) { + switch (state) { + case STATE_IDLE: + if (byte == 0xAA) { // 等到起始符才进入下一状态 + payload_idx = 0; + payload_len = 0; + state = STATE_LEN; + } + break; + case STATE_LEN: + // 防御:长度字段可能被篡改,钳到缓冲区上限,避免后面越界写 + payload_len = (byte <= MAX_PAYLOAD) ? byte : MAX_PAYLOAD; + state = (payload_len == 0) ? STATE_DONE : STATE_PAYLOAD; + break; + case STATE_PAYLOAD: + payload[payload_idx++] = byte; + if (payload_idx >= payload_len) { + state = STATE_DONE; + } + break; + case STATE_DONE: + if (byte == 0x55) { // 正常结束符 + printf("Frame OK (%u bytes):", payload_len); + for (uint8_t i = 0; i < payload_len; i++) { + printf(" %02X", payload[i]); + } + printf("\n"); + state = STATE_IDLE; + return 1; + } + state = STATE_IDLE; // 没等到结束符,帧出错,回空闲重新等 0xAA + break; + } + return 0; +} +``` + +关键在每个 `case` 里都明确写出"下一个状态是谁",这就是状态机读起来比一长串 if-else 清爽的地方。`STATE_LEN` 里对长度做钳制,是协议解析里最基本的防御:永远别直接信任对端发来的长度字段。 + +::: + ## 参考资源 - [cppreference: switch 语句](https://en.cppreference.com/w/c/language/switch) diff --git a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md index 4bdc4a47f..39f12389a 100644 --- a/documents/vol1-fundamentals/c_tutorials/05-function-basics.md +++ b/documents/vol1-fundamentals/c_tutorials/05-function-basics.md @@ -312,57 +312,56 @@ C++ 还支持**函数重载**——同名函数可以有不同参数列表,编 ## 练习 -### 练习 1:可变参数日志函数 +### 练习 1:可变参数求最大值 -实现一个自定义的日志函数,支持日志级别和格式化字符串: +**难度:基础** · 用 va_arg 逐个取参数 -```c -typedef enum { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel; +参照本篇 `average` 的写法,实现一个可变参数函数,返回所有整数参数里的最大值。第一个参数 `count` 给出后面有几个整数: -/// @brief 带级别的日志输出 -/// @param level 日志级别 -/// @param format 格式化字符串 -void log_message(LogLevel level, const char* format, ...); +```c +/// @brief 返回 count 个整数中的最大值 +/// @param count 后续整数参数的个数 +/// @return 最大值;count 为 0 返回 0 +int max_int(int count, ...); ``` +用法:`max_int(3, 10, 25, 7)` 应该返回 `25`。 + +**挑战扩展**(可选):如果想实现 `log_message(level, format, ...)` 这种带格式化的日志函数,需要把可变参数转交给 `printf` 家族——也就是 `vprintf`/`vfprintf`(本篇没讲),可以自查 cppreference 的 `vprintf` 后再动手。 + ::: details 参考答案 ```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); +#include +int max_int(int count, ...) { + if (count <= 0) { + return 0; + } va_list args; - va_start(args, format); + va_start(args, count); - vprintf(format, args); - printf("\n"); + int result = va_arg(args, int); + for (int i = 1; i < count; i++) { + int next = va_arg(args, int); + if (next > result) { + result = next; + } + } va_end(args); + return result; } ``` +和 `average` 的结构一模一样,只是把"累加再除"换成了"逐个比较取最大"。`va_start` / `va_arg` / `va_end` 三件套的用法在这里再过一遍。 + ::: ### 练习 2:递归与迭代——二分查找 +**难度:进阶** · 同一个算法的两种写法 + 分别用递归和迭代实现二分查找,比较两者的性能和可读性: ```c @@ -411,6 +410,8 @@ int binary_search_iterative(const int* arr, size_t len, int target) { ### 练习 3:多返回值实战 +**难度:基础** · 用指针参数带出多个结果 + 实现一个函数,同时计算数组的最大值和最小值: ```c 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 607954697..b4a3a3858 100644 --- a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md +++ b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md @@ -445,6 +445,8 @@ int Counter::count = 0; // 定义,在类外(C++17 可以用 inline static ### 练习 1:模块化计数器 +**难度:基础** · 用 static 文件级内部链接隐藏数据 + 设计一个简单的模块,头文件只暴露 `counter_increment`、`counter_get`、`counter_reset` 三个函数,内部用一个 `static` 变量维护计数。要求外部无法直接访问或修改这个计数器变量。 ```c @@ -512,6 +514,8 @@ int counter_get(void) { ### 练习 2:多文件符号可见性 +**难度:进阶** · 外部链接、内部链接、extern 综合题 + 创建三个文件 `a.c`、`b.c`、`main.c`。要求: - `a.c` 定义一个外部链接的全局变量 `int kSharedValue`,初始值为 `0` @@ -605,56 +609,42 @@ void set_kSharedValue(int value) { ::: -### 练习 3:延迟初始化 +### 练习 3:调用计数器 -用 `static` 局部变量实现一个 `get_config` 函数:第一次调用时执行初始化(打印 "Initializing..." 并设置默认值),后续调用直接返回已初始化的值,不再重新初始化。 +**难度:基础** · 用 static 局部变量保持函数调用之间的状态 -```c -typedef struct { - int max_connections; //建议设为5 - int timeout_ms; //建议设为500 - const char* server_name; //建议设为localhost -} Config; +实现一个 `call_count(void)`:每次被调用时返回"这是第几次调用"。利用 `static` 局部变量"值不会随函数返回而销毁"的特性。 -const Config* get_config(void); +```c +/// @return 这是第几次调用本函数(第一次调用返回 1) +int call_count(void); ``` -> 提示:`static` 局部变量只在第一次进入函数时被初始化——正好可以用来实现"只初始化一次"的语义。 +提示:在函数里声明 `static int n = 0;`,每次 `++n` 后返回。再想一下:如果把它换成普通局部变量 `int n = 0;`(去掉 static),结果会变成什么样?为什么? ::: details 参考答案 ```c #include -typedef struct { - int max_connections; - int timeout_ms; - const char* server_name; -} Config; - -const Config* get_config(void); +int call_count(void) { + static int n = 0; // 只初始化一次,值在函数返回后依然保留 + ++n; + return n; +} 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" + printf("%d\n", call_count()); // 1 + printf("%d\n", call_count()); // 2 + printf("%d\n", call_count()); // 3 return 0; } - -const Config* get_config(void) { - // config 用 static 初始化器:程序加载时一次性初始化,正好呼应题目说的 - // "static 局部变量只在第一次进入函数时被初始化" - static Config config = {5, 500, "localhost"}; - // 但题目还要求第一次调用时打印 "Initializing..."——静态初始化器本身 - // 没有运行时钩子去打印,所以再用一个 static flag 控制只打印一次 - static int initialized = 0; - if (!initialized) { - printf("Initializing...\n"); - initialized = 1; - } - return &config; -} ``` +去掉 `static` 的话,`n` 每次进函数都会重新初始化成 0,`++n` 后返回 1,于是不管调用多少次都只打印 1,丢了"记住上次结果"的能力。 + +顺便澄清一个容易混的点:`static int n = 0;` 的初始化发生在程序启动时(不是第一次调用 `call_count` 时),而且整个生命周期只发生这一次。正因为它只初始化一次、之后值一直保留,所以才能拿来当计数器用。 + ::: ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index 9eb735ef3..fb007236a 100644 --- a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -267,6 +267,8 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**( ### 练习 1:地址与值 +**难度:基础** · 观察 &、*、sizeof 和地址间隔 + 写一个程序,声明三个不同类型的变量(`int`、`double`、`char`),打印它们的值、地址和 `sizeof` 结果。观察地址之间的间隔是否符合各类型的大小。 ::: details 参考答案 @@ -305,6 +307,8 @@ int main(void) { ### 练习 2:指针遍历数组 +**难度:进阶** · 指针算术遍历数组 + 用指针算术遍历一个 `int` 数组并打印所有元素。要求不使用 `[]` 运算符,只用指针加减和解引用: ```c 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 d6d6c925b..3714cb5b6 100644 --- a/documents/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md +++ b/documents/vol1-fundamentals/c_tutorials/07B-pointers-arrays-const.md @@ -254,6 +254,8 @@ std::unique_ptr p = std::make_unique(42); ### 练习 1:指针版线性搜索 +**难度:基础** · 指针遍历加 NULL 返回 + 实现一个线性搜索函数,返回目标值在数组中首次出现的指针。如果未找到,返回 `NULL`。 ```c @@ -316,6 +318,8 @@ const int* linear_search(const int* data, size_t count, int target) { ### 练习 2:指针版数组反转 +**难度:进阶** · 双指针原地反转 + 实现一个原地反转数组的函数,只使用指针算术(两个指针从两端向中间靠拢),不使用数组下标: ```c @@ -405,6 +409,8 @@ after reverse_array: ### 练习 3:const 练习 +**难度:基础** · const 与指针的四种组合 + 判断以下每个声明中,哪些操作是合法的,哪些会编译错误: ```c 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 cbd6ab9a9..75976ab35 100644 --- a/documents/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md +++ b/documents/vol1-fundamentals/c_tutorials/08A-multi-level-pointers.md @@ -281,6 +281,8 @@ auto matrix = std::make_unique(rows * cols); ### 练习:动态二维数组的分配与释放 +**难度:进阶** · int** 管理动态二维数组(留意某行 malloc 失败时如何回滚释放) + 用多级指针实现一个动态二维数组的分配、填充和释放。请自行实现以下三个函数: ```c 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 efeae0e90..7cf7ad98b 100644 --- a/documents/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md +++ b/documents/vol1-fundamentals/c_tutorials/08B-restrict-incomplete-types.md @@ -312,6 +312,8 @@ C++ 标准一直没有引入 `restrict`。C++ 的类语义和引用让指针别 ### 练习:实现一个简单的 opaque pointer 模块 +**难度:进阶** · 不透明指针模式实现栈模块 + 用 opaque pointer 模式实现一个简单的栈(Stack)模块。要求: ```c diff --git a/documents/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md b/documents/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md index 45fb4dd46..d273d8131 100644 --- a/documents/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md +++ b/documents/vol1-fundamentals/c_tutorials/09-function-pointers-and-callbacks.md @@ -288,6 +288,8 @@ C++ 在这个方向上做了多层次的改进,从最基础的函数对象到 ### 练习 1:通用排序接口 +**难度:进阶** · 用函数指针做比较策略 + 参照 `qsort` 的接口设计,实现一个自己的通用插入排序函数,并用它分别对 `int` 数组(升序和降序)和一个字符串数组(按字典序)进行排序: ```c @@ -295,12 +297,35 @@ void insertion_sort(void* base, size_t nmemb, size_t size, int (*compar)(const void*, const void*)); ``` -### 练习 2:事件分发系统扩展 +### 练习 2:带最大次数的重试 + +**难度:进阶** · 用函数指针做条件回调 + +实现一个 `retry_until`:反复调用 `check` 函数指针,直到它返回非零(成功)或达到最大尝试次数。 + +```c +/// @brief 反复调用 check,直到成功或达到最大尝试次数 +/// @param check 条件函数,返回非零表示成功 +/// @param max_attempts 最大尝试次数 +/// @return 成功时返回第几次尝试(从 1 开始);全部失败返回 -1 +int retry_until(int (*check)(void), int max_attempts); +``` + +提示:`check` 可以这么模拟一个"第三次才就绪的外设": + +```c +int device_ready(void) { + static int tried = 0; // 第 06 章讲过的 static 局部变量,这里正好用上 + return ++tried >= 3; +} +``` -基于本篇的事件分发系统,支持同一个事件注册多个回调(回调链)并支持注销回调。思考:如果回调链中某个 handler 执行时修改了链表结构,会发生什么? +想一想:这种把"判断条件"做成函数指针传进来的写法,和本篇的 `qsort` 比较器、事件分发有什么共同点? ### 练习 3:简单的命令行计算器 +**难度:进阶** · 用函数指针数组做表驱动分发 + 使用函数指针数组实现一个命令行计算器,支持加减乘除和取模运算,通过用户输入的操作符选择对应的函数。 ```c @@ -308,6 +333,14 @@ typedef int (*BinaryOp)(int, int); // 请自行设计映射表和主循环 ``` +### 练习 4:事件分发系统扩展(挑战·可选) + +**难度:挑战** · 可选,需要设计回调容器,新手可跳过 + +基于本篇的数组版事件分发系统,扩展成支持同一个事件注册多个回调,并支持注销回调。提示:不必上链表,可以用**函数指针数组**存多个回调,注销时用标记删除或紧凑移动。 + +想一想:如果在遍历回调数组的过程中,某个回调又去注销了另一个回调,会出什么问题?这和"边遍历数组边删元素"是同一个坑。 + ## 参考资源 - [函数指针声明 - cppreference](https://en.cppreference.com/w/c/language/pointer) diff --git a/documents/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md b/documents/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md index 1e9a26abd..eb14bf977 100644 --- a/documents/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md +++ b/documents/vol1-fundamentals/c_tutorials/10-arrays-deep-dive.md @@ -423,6 +423,8 @@ int main() { ### 练习 1:矩阵运算 +**难度:进阶** · 二维数组转置与乘法 + 实现以下三个函数,完成基本的矩阵操作。矩阵使用普通的 C 二维数组表示,请自行实现矩阵转置和矩阵乘法: ```c @@ -461,6 +463,8 @@ void matrix_print(int rows, int cols, const int mat[rows][cols]); ### 练习 2:对比 VLA 与 malloc +**难度:进阶** · VLA 与 malloc 的取舍 + 编写一个程序,分别用 VLA 和 `malloc` 分配一个大小由用户输入决定的整型数组,然后对比两者的行为差异: ```c diff --git a/documents/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md b/documents/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md index 30969ff4e..8b6a0ef4e 100644 --- a/documents/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md +++ b/documents/vol1-fundamentals/c_tutorials/11-c-strings-and-buffer-safety.md @@ -309,7 +309,9 @@ C 字符串就是一个以 `\0` 终止的 `char` 数组,没有类型系统的 ### 练习 1:安全字符串库 -实现一组安全的字符串操作函数,让每个函数都知道目标缓冲区的大小,自动处理截断和终止: +**难度:进阶** · 封装带缓冲区大小感知的字符串操作 + +实现两个安全字符串函数,让它们都知道目标缓冲区的大小,自动处理截断和终止: ```c #include @@ -327,20 +329,16 @@ size_t safe_str_copy(char* dst, const char* src, size_t dst_size); /// @param dst_size 目标缓冲区总大小(含终止符) /// @return 拼接后字符串的总长度(不含终止符) size_t safe_str_cat(char* dst, const char* src, size_t dst_size); - -/// @brief 安全地格式化字符串 -/// @param dst 目标缓冲区 -/// @param dst_size 目标缓冲区总大小 -/// @param format 格式字符串 -/// @param ... 格式参数 -/// @return 实际写入的字符数(不含终止符) -size_t safe_str_format(char* dst, size_t dst_size, const char* format, ...); ``` -提示:`safe_str_copy` 可以基于 `strncpy` 实现,但必须保证终止;`safe_str_cat` 需要先算出目标字符串当前长度,再计算剩余可用空间;`safe_str_format` 直接用 `vsnprintf` 实现即可。 +提示:`safe_str_copy` 可以基于 `strncpy` 实现,但 `strncpy` 在 src 过长时不会自动写终止符,得自己补;`safe_str_cat` 要先算出 dst 当前长度,再算剩余可用空间。 + +**挑战扩展**(可选):再加一个 `safe_str_format(char* dst, size_t dst_size, const char* format, ...)`。它需要用到 `vsnprintf` 和 `` 的可变参数机制(本篇没讲,函数篇也只是带过),请自查 cppreference 的 `vsnprintf` 后再实现。 ### 练习 2:字符串分割函数 +**难度:基础** · 用指针遍历字符串,按分隔符切片 + 实现一个将字符串按分隔符切分的函数: ```c diff --git a/documents/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md b/documents/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md index 7a4a4b9a1..899d7b7c6 100644 --- a/documents/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md +++ b/documents/vol1-fundamentals/c_tutorials/12-struct-and-memory-alignment.md @@ -460,42 +460,91 @@ using AlignedStorage = std::aligned_storage_t -#include #include -// 练习: 定义 Frame 结构体 -// typedef struct __attribute__((packed)) { -// ... -// } Frame; +typedef struct { + char a; + int b; + char c; +} StructA; + +typedef struct { + int b; + char a; + char c; +} StructB; + +typedef struct { + char a; + char c; + int b; +} StructC; +``` -// 练习: 实现 print_frame_layout() 函数 -// 使用 offsetof 打印每个字段的偏移量 +想一下:三个结构体字段完全相同、只是顺序不同,`sizeof` 为什么不一样?哪个最省空间? -// 练习: 实现 create_frame() 函数 -// 分配内存并填充帧数据(含柔性数组成员) +::: details 参考答案 -int main(void) { - print_frame_layout(); +手算结果(`int` 4 字节对齐): - // 练习: 创建一个测试帧并验证偏移 - return 0; -} +| 结构体 | a 偏移 | b 偏移 | c 偏移 | sizeof | +|--------|--------|--------|--------|--------| +| StructA | 0 | 4 | 8 | 12 | +| StructB | 4 | 0 | 5 | 8 | +| StructC | 0 | 4 | 1 | 8 | + +StructA 里 `a` 在 0、`b` 要 4 字节对齐所以前面补 3 字节填到偏移 4、`c` 在 8、尾部再补到 12。把大对齐的字段(`int`)和小字段(`char`)各自集中放,能减少中间填充。StructB 和 StructC 都是 8 字节,比 StructA 的 12 字节省。 + +::: + +### 练习 2:packed 与显式对齐对比 + +**难度:进阶** · 对比默认对齐、packed、_Alignas 三种布局 + +对同一组字段,分别用默认对齐、`__attribute__((packed))`、`_Alignas` 三种方式定义结构体,打印 `sizeof` 和各字段偏移,看三种布局有什么不同: + +```c +#include +#include +#include + +typedef struct { + uint8_t type; + uint32_t value; +} FrameNormal; + +typedef struct __attribute__((packed)) { + uint8_t type; + uint32_t value; +} FramePacked; + +typedef struct { + uint8_t type; + _Alignas(4) uint32_t value; +} FrameAligned; ``` -提示:在 packed 结构体中使用 `alignas` 需要注意——packed 会取消自动填充,但 `alignas` 可以强制某个字段的对齐。思考一下:在 packed 结构体中,如果帧头到时间戳之间恰好不是 4 的倍数偏移,你该怎么处理? +想一想:`FramePacked` 最省空间,但为什么在有些 CPU 上访问 `value` 字段反而更慢、甚至直接崩?什么场景该用 packed,什么场景该用显式对齐? + +### 练习 3:通信协议帧设计(挑战·可选) + +**难度:挑战** · 可选,需要了解 CRC 与字节序,新手可跳过 + +设计一个用于嵌入式设备通信的二进制协议帧结构:帧头(起始符、帧类型、载荷长度、时间戳)、变长载荷(柔性数组成员)、帧尾校验。 + +- 用 `offsetof` 打印每个字段偏移,验证布局符合预期 +- 帧尾的校验字段可以先预留 2 字节占位、写一句 `// TODO: 填 CRC16`,不必现在就实现 CRC 算法 +- 想一下:不同字节序(大端、小端)的设备通信时,多字节字段(比如时间戳)该怎么处理? + +提示:本篇讲过柔性数组成员、`_Alignas`、`__attribute__((packed))`、`offsetof`,这些都是你的工具。CRC 算法和字节序转换属于通信专题,这里只要求你意识到这两个问题、留好占位,不要求完整实现。 ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md b/documents/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md index 59929fff5..068fb5c68 100644 --- a/documents/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md +++ b/documents/vol1-fundamentals/c_tutorials/13-union-enum-bitfield-typedef.md @@ -349,6 +349,8 @@ using EventHandler = void (*)(int); // 比 typedef 更直观 ### 练习 1:IEEE 754 浮点数分解 +**难度:进阶** · 用联合体拆开浮点的位 + 用联合体实现一个工具,把一个 `float` 值分解成 IEEE 754 格式的符号位、指数和尾数,并打印出来。 ```c @@ -370,6 +372,8 @@ int main(void) { ### 练习 2:32 位硬件控制寄存器 +**难度:基础** · 位域加 union 映射寄存器 + 用位域定义一个 32 位硬件控制寄存器结构体,然后编写函数对其进行操作。 ```c @@ -402,6 +406,8 @@ int main(void) { ### 练习 3:简单的 tagged union +**难度:基础** · enum 加 union 加 tag 检查 + 用枚举和联合体实现一个可以存储 `int`、`float` 或字符串指针的 tagged union。 ```c diff --git a/documents/vol1-fundamentals/c_tutorials/14-dynamic-memory.md b/documents/vol1-fundamentals/c_tutorials/14-dynamic-memory.md index 44d2ac11d..f5e48e9ec 100644 --- a/documents/vol1-fundamentals/c_tutorials/14-dynamic-memory.md +++ b/documents/vol1-fundamentals/c_tutorials/14-dynamic-memory.md @@ -219,67 +219,133 @@ gcc -fsanitize=address -g -o demo demo.c ## 练习 -### 练习 1:固定大小内存池分配器 +### 练习 1:用 realloc 实现动态增长数组 -实现一个简单的固定大小内存池,从大块内存中切分固定大小的块,支持分配和回收。 +**难度:基础** · 用 realloc 扩容,承接本篇 realloc 讲解 + +实现一个简单的动态整数数组:初始容量 4,每次 `push_back` 在满时用 `realloc` 把容量翻倍。 ```c #include -#include + +typedef struct { + int* data; + size_t size; + size_t capacity; +} IntVec; + +/// @brief 初始化,初始容量 4 +void intvec_init(IntVec* v); +/// @brief 尾部追加;满时扩容翻倍,失败返回 -1 +int intvec_push(IntVec* v, int value); +/// @brief 释放 +void intvec_free(IntVec* v); +``` + +提示:`realloc(NULL, n)` 等价于 `malloc(n)`,所以第一次分配也能走 realloc。注意 `realloc` 失败时返回 NULL 且不会释放旧块——拿一个临时变量接住返回值,确认成功后再覆盖原指针,否则会泄漏。 + +::: details 参考答案 + +```c #include -typedef struct MemoryPool MemoryPool; +void intvec_init(IntVec* v) { + v->capacity = 4; + v->size = 0; + v->data = malloc(v->capacity * sizeof(int)); +} -/// @brief 创建一个固定大小内存池 -/// @param block_size 每个块的大小(字节) -/// @param block_count 块的数量 -/// @return 指向内存池的指针,失败返回 NULL -MemoryPool* pool_create(size_t block_size, size_t block_count); +int intvec_push(IntVec* v, int value) { + if (v->size >= v->capacity) { + size_t new_cap = v->capacity * 2; + int* new_data = realloc(v->data, new_cap * sizeof(int)); + if (new_data == NULL) { + return -1; // 扩容失败,旧 data 仍然有效,调用者决定怎么办 + } + v->data = new_data; + v->capacity = new_cap; + } + v->data[v->size++] = value; + return 0; +} + +void intvec_free(IntVec* v) { + free(v->data); + v->data = NULL; + v->size = v->capacity = 0; +} +``` + +关键是 `realloc` 的返回值先存到 `new_data`、判断成功后再赋给 `v->data`。要是直接写 `v->data = realloc(v->data, ...)`,一旦失败原来的 `v->data` 就被 NULL 覆盖,那块内存再也找不回来,就是泄漏。 -/// @brief 从内存池中分配一个块 -void* pool_alloc(MemoryPool* pool); +::: -/// @brief 将块归还给内存池 -void pool_free(MemoryPool* pool, void* block); +### 练习 2:内存错误诊断 -/// @brief 销毁内存池,释放所有内存 -void pool_destroy(MemoryPool* pool); +**难度:进阶** · 用 ASan 或 Valgrind 识别本篇讲过的内存错误 + +下面这段代码藏着至少四种内存错误。先读代码猜每一处有什么问题,再用 `gcc -fsanitize=address`(或 Valgrind)跑一遍,把工具报出的错误类型和位置记下来: + +```c +#include int main(void) { - // 练习: 创建一个 64 字节/块、共 64 块的内存池 - // 练习: 分配几个块,写入数据,然后释放 - // 练习: 销毁内存池 + int* p = malloc(4 * sizeof(int)); + p[4] = 42; // (1) 这里有什么问题? + + int* q = malloc(sizeof(int)); + free(q); + *q = 100; // (2) 这里呢? + + int* r = malloc(1024); + /* 忘了 free(r) */ // (3) 这又是哪一类? + + free(p); + free(p); // (4) 再来一个 return 0; } ``` -提示:用链表管理空闲块——每个空闲块的前几个字节存储指向下一个空闲块的指针。 +要求:对每一处,写出它属于本篇讲的哪一类错误(越界写、释放后使用、泄漏、双重释放、未初始化读取),并说明工具是怎么报的。 + +::: details 参考答案 + +(1) `p[4]`:只分配了 4 个 `int`(下标 0–3),`p[4]` 是堆缓冲区越界写。ASan 报 `heap-buffer-overflow`。 + +(2) `*q = 100`:`q` 已经 free 又去写,是释放后使用。ASan 报 `heap-use-after-free`。 + +(3) `r` 没 free:内存泄漏。Valgrind 的 `LEAK SUMMARY` 会列出来;ASan 在多数平台上默认也做泄漏检查(`detect_leaks=1`),退出时报 `Detected memory leaks`。 -### 练习 2:带统计的 malloc/free 包装器 +(4) `free(p)` 两次:双重释放。ASan 报 `attempting double-free`。 -实现一个对 `malloc` 和 `free` 的包装层,跟踪所有分配和释放操作,程序退出时打印统计报告。 +这四类正好对应本篇讲的那几种典型内存错误,工具的报错关键词能帮你快速定位是哪一类。 + +::: + +### 练习 3:固定大小内存池分配器(挑战·可选) + +**难度:挑战** · 可选,需要自学空闲链表(free list)惯用法 + +实现一个固定大小内存池:从一块大内存里切出固定大小的块,用链表管理空闲块——每个空闲块的前几个字节存指向下一个空闲块的指针。建议先查资料弄懂"in-place 链表 / free list"是怎么回事再来写。 ```c -#include -#include +typedef struct MemoryPool MemoryPool; +MemoryPool* pool_create(size_t block_size, size_t block_count); +void* pool_alloc(MemoryPool* pool); +void pool_free(MemoryPool* pool, void* block); +void pool_destroy(MemoryPool* pool); +``` -/// @brief 带统计的 malloc -void* tracked_malloc(size_t size, const char* file, int line); +想一下:内存池相比直接 `malloc`/`free`,在嵌入式或实时系统里有什么好处?为什么它能做到 O(1) 分配释放、且不产生碎片? -/// @brief 带统计的 free -void tracked_free(void* ptr); +### 练习 4:带统计的 malloc/free 包装器(挑战·可选) -/// @brief 打印内存统计报告 -void mem_report(void); +**难度:挑战** · 可选,建议学完第 15 章预处理器后再做 -#define TMALLOC(size) tracked_malloc((size), __FILE__, __LINE__) +包装 `malloc`/`free`,记录每次分配的文件名和行号,程序退出时打印还没释放的清单。需要用到 `__FILE__`/`__LINE__` 宏(第 15 章才讲)和 `atexit` 注册退出钩子。 -int main(void) { - // 练习: 用 TMALLOC 分配几块内存 - // 练习: 故意只释放其中一部分 - // 练习: 调用 mem_report() 查看哪些分配没有被释放 - return 0; -} +```c +#define TMALLOC(size) tracked_malloc((size), __FILE__, __LINE__) ``` -提示:用一个数组或链表记录每次分配的信息。`atexit(mem_report)` 可以注册退出钩子。 +提示:用一个数组或链表记录每次分配的地址、大小、位置;`free` 时按地址匹配并标记已释放;`atexit(mem_report)` 注册退出时打印剩余未释放项。 diff --git a/documents/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md b/documents/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md index 745e30220..4e3e368f1 100644 --- a/documents/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md +++ b/documents/vol1-fundamentals/c_tutorials/15-preprocessor-and-multifile.md @@ -191,6 +191,8 @@ gcc -o demo main.c -L. -lmath_utils ### 练习 1:构建多文件模块化项目 +**难度:基础** · .h/.c 分离加静态库打包 + ```c // math_utils.h #pragma once @@ -214,6 +216,8 @@ int main(void) { ### 练习 2:零开销的 DEBUG_LOG 宏 +**难度:进阶** · 条件编译加可变参数宏 + ```c // debug_log.h #pragma once diff --git a/documents/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md b/documents/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md index f7c3a07d7..ef34d2a6d 100644 --- a/documents/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md +++ b/documents/vol1-fundamentals/c_tutorials/16-file-io-and-stdlib.md @@ -266,6 +266,8 @@ std::string s = std::format("{} is {} years old", name, age); ### 练习 1:配置文件解析器 +**难度:进阶** · fgets 逐行解析 key=value + 解析 `key=value` 格式的配置文件,忽略 `#` 注释和空行。 ```c @@ -306,6 +308,8 @@ int main(int argc, char* argv[]) { ### 练习 2:文件复制工具 +**难度:基础** · fread/fwrite 加进度 + 通过命令行参数指定源文件和目标文件,支持二进制文件复制,显示进度。 ```c diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md index 98b197666..9a940d3d7 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/01-arm-architecture-fundamentals.md @@ -339,57 +339,43 @@ ARM 平台上的 C++ 对象内存布局遵循 AAPCS 的 ABI 规范:普通成 ## 练习题 -下面几道练习题留给你们自己折腾——动手查资料、写代码、上板验证,才是真正的学习路径。 +下面几道练习题留给你们自己折腾。本篇偏理论综述,所以练习以概念分析为主;需要上板或 QEMU 的实操题,标了挑战可选。 -```c -/// @brief 练习 1:读取 IPSR 寄存器 -/// 使用 GCC 内嵌汇编读取 Cortex-M 的 IPSR 寄存器值 -/// 解释在正常运行和进入中断服务函数时读到的值有什么不同 -/// 提示:IPSR 是 xPSR 的一部分,可以用 MRS 指令读取 -uint32_t exercise_read_ipsr(void) -{ - // 练习: 用内嵌汇编读取 IPSR - return 0; -} -``` +### 练习 1:IPSR 寄存器的作用 -```c -/// @brief 练习 2:触发并调试 HardFault -/// 对一个无效地址执行写操作,故意触发 HardFault -/// 然后在 HardFault Handler 中读取入栈的寄存器值 -/// 定位导致异常的指令地址 -/// 提示:HardFault Handler 的参数可以拿到栈帧指针 -void exercise_trigger_hardfault(void) -{ - // 练习: 写一个无效地址来触发 HardFault -} -``` +**难度:基础** · 概念分析,不需要写代码 + +IPSR 是 Cortex-M 程序状态寄存器(xPSR)的一部分。请回答:IPSR 在什么时机被硬件更新?它记录的是什么信息?为什么在中断服务函数里读 IPSR 能帮你判断"当前在处理哪个异常"? + +### 练习 2:HardFault 调试流程 + +**难度:进阶** · 文字描述调试思路,不要求上板 + +程序因为访问非法地址触发 HardFault 时,硬件会把当前寄存器压栈。请描述从 HardFault 触发到定位"出错的那条指令地址"的整个流程:你需要读哪些栈上的值?压栈的 PC 指向哪里? + +提示:本篇异常处理部分讲过,HardFault Handler 拿到的栈帧指针指向 `{R0, R1, R2, R3, R12, LR, PC, xPSR}`。 + +### 练习 3:分析 AAPCS 的参数传递 + +**难度:进阶** · 需要 arm-none-eabi-gcc 工具链 + +写两个函数:一个接受 4 个 int 参数,另一个接受 6 个。用 `arm-none-eabi-objdump -d` 反汇编对比调用序列,观察前 4 个参数走 R0–R3,第 5、6 个参数走哪里。 ```c -/// @brief 练习 3:分析 AAPCS 的参数传递 -/// 写两个函数:一个接受 4 个 int 参数,另一个接受 6 个 -/// 用 arm-none-eabi-objdump -d 反汇编对比调用序列 -/// 找出编译器如何分配 R4-R11 给局部变量 -int exercise_aapcs_4(int a, int b, int c, int d) -{ - // 练习: 添加局部变量和函数调用,使反汇编更有看头 - return 0; +int exercise_aapcs_4(int a, int b, int c, int d) { + return a + b + c + d; } -int exercise_aapcs_6(int a, int b, int c, int d, int e, int f) -{ - // 练习: 同上,对比反汇编结果 - return 0; +int exercise_aapcs_6(int a, int b, int c, int d, int e, int f) { + return a + b + c + d + e + f; } ``` -```c -/// @brief 练习 4(进阶):向量表重定位 -/// 阅读一个 Cortex-M 启动文件(如 startup_stm32f407xx.s) -/// 画出完整的向量表布局 -/// 然后修改链接脚本把向量表重定位到 RAM 中 -/// 实现运行时动态修改中断向量(Bootloader 开发的基础技能) -``` +### 练习 4:向量表重定位(挑战·可选) + +**难度:挑战** · 可选,需要启动文件、链接脚本与 Bootloader 前置知识 + +阅读一个 Cortex-M 启动文件(如 startup_stm32f407xx.s),画出完整的向量表布局;再修改链接脚本把向量表重定位到 RAM,实现运行时动态修改中断向量。这是 Bootloader 开发的基础。 ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md index 130c2f51d..9ac6053b5 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md @@ -360,15 +360,15 @@ C++ 标准库里的容器在设计时也考虑了缓存因素。`std::vector` ## 练习 -1. **步长实验验证**:修改本文的步长测试代码,把数组缩小到 4MB(基本能塞进大部分 CPU 的 L3,避免主存延迟的干扰),重点盯 `per_access` 那一列。观察步长从 1 涨到 32 时单次访问耗时的变化——思考:为什么步长突破 16(一条缓存行边界)之后,`per_access` 才开始明显往上抬?这个拐点对应的字节数,能反推出你机器的缓存行大小吗? +1. **步长实验验证**(基础):修改本文的步长测试代码,把数组缩小到 4MB(基本能塞进大部分 CPU 的 L3,避免主存延迟的干扰),重点盯 `per_access` 那一列。观察步长从 1 涨到 32 时单次访问耗时的变化——想一下:为什么步长突破 16(一条缓存行边界)之后,`per_access` 才开始明显往上抬?这个拐点对应的字节数,能反推出你机器的缓存行大小吗? -2. **伪共享复现**:写一个多线程程序(使用 pthread 或 C++ ``),创建两个线程各自累加一个共享结构体里的不同字段到一亿次。先不加对齐地跑一次,然后用 `alignas(64)` 把两个字段分别对齐到不同缓存行再跑一次,对比耗时。 +2. **伪共享复现**(进阶):写一个多线程程序(使用 pthread 或 C++ ``),创建两个线程各自累加一个共享结构体里的不同字段到一亿次。先不加对齐地跑一次,然后用 `alignas(64)` 把两个字段分别对齐到不同缓存行再跑一次,对比耗时。 -3. **矩阵转置优化**:实现一个方阵转置函数,先写朴素的双重循环版本,再尝试分块(blocking)——将矩阵分成 32x32 的小块,在块内做转置。对比两个版本在大矩阵(2048x2048)上的性能差异。 +3. **矩阵转置优化**(进阶):实现一个方阵转置函数,先写朴素的双重循环版本,再尝试分块(blocking)——将矩阵分成 32x32 的小块,在块内做转置。对比两个版本在大矩阵(2048x2048)上的性能差异。 -4. **AoS vs SoA benchmark**:定义一个包含 `float x, y, z, r, g, b` 的粒子结构体,创建十万个粒子。分别用 AoS 和 SoA 两种布局实现"将所有粒子的坐标归一化到单位球内",对比耗时。 +4. **AoS vs SoA benchmark**(基础):定义一个包含 `float x, y, z, r, g, b` 的粒子结构体,创建十万个粒子。分别用 AoS 和 SoA 两种布局实现"将所有粒子的坐标归一化到单位球内",对比耗时。 -5. **Cache 友好的链表**:参考 Linux 内核的 `list_head` 设计思路,实现一个侵入式双向链表,节点数据域和链表指针域分开存储,使得遍历链表指针时不需要加载整个节点数据,提升缓存命中率。 +(原版还有一题"Cache 友好的侵入式链表",因为它依赖侵入式容器的预备知识、和缓存主题耦合也松,挪走了,更适合放进进阶专题 06 的链表篇。) ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md index 13ce23428..77caec2bc 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/03-c-traps-and-pitfalls.md @@ -419,7 +419,7 @@ char* result = malloc(strlen(s) + strlen(t) + 1); // OK,+1 给 '\0' ## 练习题 -以下是几道练习题,代码中故意留有陷阱,请找出并修复它们。 +以下是几道练习题,代码中故意留有陷阱,请找出并修复它们。这六道题都是**难度:基础**,每题对应本篇讲过的一个陷阱点(贪婪匹配、优先级、赋值与比较、多余分号、整数溢出、综合)。 ```c /// @brief 练习 1:修复词法分析陷阱 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md index 0be6498bf..547415b2c 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md @@ -550,6 +550,8 @@ C++ 的 OOP 语法本质就是 C OOP 惯用法的语法糖。编译器把绑 vta ### 练习 1:三角形扩展 +**难度:基础** · 照 vtable 加一个形状 + 在图形框架中添加一个 `Triangle` 类型(用三边长度表示): ```c @@ -566,6 +568,8 @@ Triangle* triangle_create(const char* name, int id, ### 练习 2:图形排序 +**难度:进阶** · qsort 加函数指针比较 + 给 `ShapeManager` 添加按面积排序功能: ```c @@ -577,6 +581,8 @@ void shape_manager_sort_by_area(ShapeManager* mgr); ### 练习 3:不透明指针版计数器 +**难度:进阶** · 不透明指针重做 Counter + 把第二步的 `Counter` 改成不透明指针版本——头文件只暴露 `typedef struct Counter Counter;` 和操作函数,实现文件藏起完整定义。请自行拆分头文件和实现文件,并提供一个 `counter_create()` 返回堆分配的对象。 ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md index 4e58dd2d0..2cbdd38cc 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/05-handmade-dynamic-array.md @@ -498,6 +498,8 @@ size_t dynamic_array_find( ### 练习 1:实现 resize +**难度:基础** · reserve 加填充默认值 + `reserve` 只改变容量不改变 size,而 `resize` 需要改变 size。当新 size 大于旧 size 时,多出来的位置应该填充默认值。 ```c @@ -513,6 +515,8 @@ DynamicArrayStatus dynamic_array_resize( ### 练习 2:实现 filter +**难度:基础** · 谓词返回新数组 + 给定一个动态数组和一个过滤谓词,返回一个新创建的动态数组,只包含满足条件的元素。 ```c @@ -526,6 +530,8 @@ DynamicArray* dynamic_array_filter( ### 练习 3:实现 map 变换 +**难度:进阶** · 变换函数,输出元素大小可能不同 + 给定一个动态数组和一个变换函数,对每个元素应用变换函数,将结果存入新数组返回。 ```c @@ -541,6 +547,8 @@ DynamicArray* dynamic_array_map( ### 练习 4:实现拼接 +**难度:基础** · 合并两个同类型数组 + 将两个同类型的动态数组拼接成一个新的动态数组。 ```c diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md index 5b1f2915c..2ed6862d3 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md @@ -617,6 +617,8 @@ C++ 的迭代器模式把"遍历"这个操作抽象了出来。不管是链表 ### 练习 1:链表反转 +**难度:基础** · 三指针 O(1) 空间 + 实现一个函数,将单链表原地反转。要求空间复杂度 O(1),不能分配新节点。 ```c @@ -629,6 +631,8 @@ void linked_list_reverse(LinkedList* list); ### 练习 2:合并两个有序链表 +**难度:基础** · 双指针归并 + 给定两个按升序排列的链表,将它们合并为一个新的有序链表。 ```c @@ -643,6 +647,8 @@ LinkedList* linked_list_merge_sorted(const LinkedList* a, const LinkedList* b); ### 练习 3:检测链表环 +**难度:进阶** · Floyd 快慢指针 + 判断一个链表是否有环(某个节点的 `next` 指向了前面已经出现过的节点)。 ```c @@ -655,6 +661,8 @@ bool linked_list_has_cycle(const LinkedList* list); ### 练习 4:哨兵版完整 API +**难度:进阶** · 带哨兵节点的完整链表 + 用哨兵节点重新实现完整的链表 API(`push_front`、`push_back`、`insert_at`、`remove`、`find`),体会哨兵节点消除了哪些特判代码。 ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md index 17e4e8e79..e483ae85b 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/07-embedded-c-patterns.md @@ -470,6 +470,8 @@ C++ 对嵌入式代码的改进主要集中在三个方面: ### 练习 1:通用环形缓冲区 +**难度:进阶** · 把 uint8_t 版改成 void* 通用版 + 将文中的 `uint8_t` 环形缓冲区改造为通用版本(用 `void*` + 元素大小实现): ```c @@ -490,23 +492,29 @@ uint32_t ring_buffer_count(const RingBuffer* rb); 提示:内部用 `memcpy` 做通用字节拷贝,`head`/`tail` 改为绝对计数(`uint32_t` 不怕溢出),实际索引通过 `count % capacity` 计算。 -### 练习 2:可移植的 UART 抽象层 +### 练习 2:UART 抽象层接口设计 + +**难度:进阶** · 只设计接口,不实现中断时序 -为 UART 外设设计一套和具体芯片无关的抽象层接口。驱动内部需要两个环形缓冲区(发送和接收),`uart_write` 先写缓冲区再触发发送中断,实际逐字节发送在 ISR 中完成。 +参照本篇外设抽象层的思路,设计一套与具体芯片无关的 UART 抽象层接口。不要求实现中断驱动的逐字节发送,只要求把接口定义清楚:驱动结构体里需要哪些字段(收发缓冲区、状态)、`init`/`write`/`read` 三个接口的签名、以及"write 写满缓冲区时返回什么"。 ```c typedef struct { /* 你设计 */ } UartDriver; -void uart_init(UartDriver* uart, uint32_t baud, - uint8_t* tx_buffer, uint8_t* rx_buffer, size_t buffer_size); +void uart_init(UartDriver* uart, uint32_t baud); size_t uart_write(UartDriver* uart, const uint8_t* data, size_t len); size_t uart_read(UartDriver* uart, uint8_t* data, size_t len); -void uart_irq_handler(UartDriver* uart); // 在 ISR 中调用 ``` -### 练习 3:链接脚本与启动代码 +想一想:为什么把缓冲区和状态藏在结构体里、只对外暴露函数接口,能让上层代码不被具体芯片绑死? + +### 练习 3:读懂一份链接脚本 + +**难度:基础** · 读解现有脚本,不要求从零写 + +找一份现成的 Cortex-M 链接脚本(本篇启动流程一节给过示例),逐段解释:`MEMORY` 命令定义了哪些区域?向量表为什么放在 Flash 开头?`.data` 段的 `AT > FLASH` 是什么意思?`.bss` 段为什么用 `NOLOAD`? -写一个针对 ARM Cortex-M4(256K Flash, 64K SRAM)的最小链接脚本和启动代码。要求:定义正确的 MEMORY 区域、向量表放 Flash 开头、处理 `.data` 段地址分离、清零 `.bss`、在 `main` 后加安全死循环。 +再想一下:要把这份脚本里的 Flash 改成 256K、SRAM 改成 64K,你需要改哪几行? ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md index b31e0db28..8249997b8 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/08-reusable-c-code.md @@ -693,58 +693,53 @@ C++20 引入了 Modules 系统,旨在从根本上替代头文件的 `#include` ## 练习 -### 练习 1:不透明指针的字符串哈希表 +### 练习 1:不透明指针的栈模块 -实现一个简单的字符串-整数映射表,使用不透明指针隐藏内部实现。要求: +**难度:进阶** · 承接本篇 ring_buffer 模块,换个数据结构 -```c -// hashmap.h — 你需要编写的公开接口 -#ifndef HASHMAP_H -#define HASHMAP_H - -#include +参照本篇不透明指针 ring_buffer 的写法,实现一个不透明指针的栈模块。头文件只暴露 `typedef struct Stack Stack;` 和接口函数,内部结构藏在 .c 里: -typedef struct HashMap HashMap; +```c +// stack.h +typedef struct Stack Stack; +Stack* stack_create(size_t capacity); +void stack_destroy(Stack* s); +int stack_push(Stack* s, int value); // 满返回 -1 +int stack_pop(Stack* s); // 空时怎么办你自己定 +size_t stack_size(const Stack* s); +``` -HashMap* hashmap_create(size_t bucket_count); -void hashmap_destroy(HashMap* map); +想一想:为什么头文件里只写 `typedef struct Stack Stack;` 而不展开结构体定义?调用者拿到指针后,能直接 `s->top` 访问成员吗? -/// 插入键值对,如果 key 已存在则覆盖旧值 -/// @return 0 表示成功,非零表示失败 -int hashmap_insert(HashMap* map, const char* key, int value); +### 练习 2:平台抽象层实践 -/// 查找 key 对应的值,通过 out 返回 -/// @return 0 表示找到,非零表示不存在 -int hashmap_lookup(const HashMap* map, const char* key, int* out); +**难度:进阶** · 给同一接口写两个后端 -/// 删除指定 key -/// @return 0 表示成功删除,非零表示 key 不存在 -int hashmap_remove(HashMap* map, const char* key); +为本篇的可复用模块(或上面练习 1 的栈)设计一个平台抽象层,替换掉对 `malloc`/`free` 的直接依赖: -#endif // HASHMAP_H +```c +// pal.h +void* pal_alloc(size_t size); +void pal_free(void* ptr); ``` -提示:内部可以用一个简单的链表数组(拉链法)来实现哈希表。哈希函数可以用经典的 `djb2` 算法。记住所有内部类型和辅助函数都要藏在 `.c` 文件里。 +分别实现两个版本:一个用标准库 `malloc`/`free`(适合 PC),一个用静态内存池(适合嵌入式裸机)。模块的 .c 文件通过包含 `pal.h` 来分配内存。 -### 练习 2:平台抽象层实践 - -为上面练习 1 的哈希表写一个平台抽象层,替换掉标准库的 `malloc`/`free`。要求: - -```c -// pal.h — 平台抽象层接口 -#ifndef PAL_H -#define PAL_H +### 练习 3:不透明指针的字符串哈希表(挑战·可选) -#include +**难度:挑战** · 可选,需要自学拉链法哈希表,新手可跳过 -void* pal_alloc(size_t size); -void pal_free(void* ptr); +实现一个字符串→整数的不透明指针哈希表(内部用链表数组拉链法、`djb2` 哈希函数)。建议先学完进阶专题 06 的链表,再回来实现冲突处理的拉链结构。 -#endif // PAL_H +```c +typedef struct HashMap HashMap; +HashMap* hashmap_create(size_t bucket_count); +void hashmap_destroy(HashMap* map); +int hashmap_insert(HashMap* map, const char* key, int value); +int hashmap_lookup(const HashMap* map, const char* key, int* out); +int hashmap_remove(HashMap* map, const char* key); ``` -请分别实现两个版本:一个使用标准库 `malloc`/`free`(适合 PC),另一个使用静态内存池(适合嵌入式裸机环境)。哈希表的 `.c` 文件应该通过包含 `pal.h` 来分配内存,而不是直接调用 `malloc`。 - ## 参考资源 - [Opaque Pointer 模式 - Wikipedia](https://en.wikipedia.org/wiki/Opaque_pointer)