diff --git a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md index 568b3e23b..57ec50994 100644 --- a/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md +++ b/documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 深入理解 C 语言的作用域规则、存储类别和链接性,掌握 static 的三种用法 difficulty: beginner order: 8 platform: host prerequisites: -- 控制流:让程序学会选择和重复 + - 控制流:让程序学会选择和重复 reading_time_minutes: 20 tags: -- host -- cpp-modern -- beginner -- 入门 -- 基础 + - host + - cpp-modern + - beginner + - 入门 + - 基础 title: 作用域与存储类别 --- + # 作用域与存储类别 如果你写过超过两个源文件的项目,大概率已经踩过这样的坑:两个文件里都定义了一个叫 `count` 的全局变量,编译的时候链接器一脸懵逼地告诉你 `multiple definition`。或者更隐蔽的情况——你在某个 `.c` 文件里定义了一个辅助函数,结果别的文件不小心也调用了它,后来你改了那个函数的实现,调用方毫无预警地崩了。 @@ -261,13 +262,13 @@ extern int kValue = 42; // 千万别这么干! 这三者的关系可以用一个表格来总结: -| 声明位置 | 关键字 | 链接性 | 作用域 | 生命周期 | -| --- | --- | --- | --- | --- | -| 函数内 | (无) | 无 | 块 | 自动 | -| 函数内 | `static` | 无 | 块 | 静态 | -| 函数外 | (无) | 外部 | 文件 | 静态 | -| 函数外 | `static` | 内部 | 文件 | 静态 | -| 函数外 | `extern` | (取决于首次声明) | 文件 | 静态 | +| 声明位置 | 关键字 | 链接性 | 作用域 | 生命周期 | +| -------- | -------- | ------------------ | ------ | -------- | +| 函数内 | (无) | 无 | 块 | 自动 | +| 函数内 | `static` | 无 | 块 | 静态 | +| 函数外 | (无) | 外部 | 文件 | 静态 | +| 函数外 | `static` | 内部 | 文件 | 静态 | +| 函数外 | `extern` | (取决于首次声明) | 文件 | 静态 | 这个表格值得多看几眼——注意函数外的 `static` 改变的是链接性(从外部变成内部),而不是作用域或生命周期。 @@ -455,6 +456,58 @@ void counter_reset(void); 请自行实现 `counter.c`。 +### 练习 1 参考答案 + +main.c + +```c +#include +#include "counter.h" + +int main(void) { + printf("%d\n",counter_get()); //输出应当是0 + counter_increment(); + printf("%d\n",counter_get()); //输出应当是1 + counter_increment(); + printf("%d\n",counter_get()); //输出应当是2 + counter_reset(); + printf("%d\n",counter_get()); //输出应当是0 + return 0; +} +``` + +counter.h + +```c +#ifndef MODERNCPP_PRE8_1_COUNTER_H +#define MODERNCPP_PRE8_1_COUNTER_H + +void counter_increment(void); +int counter_get(void); +void counter_reset(void); + +#endif //MODERNCPP_PRE8_1_COUNTER_H +``` + +counter.c + +```c +#include "counter.h" + +static int counter = 0; +void counter_increment(void) { + counter++; +} + +void counter_reset(void) { + counter = 0; +} + +int counter_get(void) { + return counter; +} +``` + ### 练习 2:多文件符号可见性 创建三个文件 `a.c`、`b.c`、`main.c`。要求: @@ -471,11 +524,104 @@ void counter_reset(void); // 各 .c 文件的实现留给你 ``` +### 练习 2 参考答案 + +main.c + +```c +#include +#include "a.h" +#include "b.h" + +int main(void) { + + a_greet(); // 调用 a 模块的公共函数,触发它内部的 helper_a + printf("%d\n",kSharedValue); //输出应当是0 + set_kSharedValue(100); // 内部会调用 b 模块自己的 helper_a + printf("%d\n",kSharedValue); //输出应当是100 + + return 0; +} +``` + +a.h + +```c +#ifndef MODERNCPP_PRE8_2_A_H +#define MODERNCPP_PRE8_2_A_H + +extern int kSharedValue; +void a_greet(void); + +#endif //MODERNCPP_PRE8_2_A_H +``` + +b.h + +```c +#ifndef MODERNCPP_PRE8_2_B_H +#define MODERNCPP_PRE8_2_B_H + +void set_kSharedValue(int value); + +#endif //MODERNCPP_PRE8_2_B_H +``` + +a.c + +```c +#include +#include "a.h" + +int kSharedValue = 0; +static void helper_a(void) { + printf("need help?\n"); +} + +// a.c 暴露的公共函数,内部调用文件私有的 helper_a +void a_greet(void) { + helper_a(); +} +``` + +b.c + +```c +#include +#include "b.h" +#include "a.h" + + +static void helper_a(void) { + printf("need help?\n"); +} +void set_kSharedValue(int value) { + helper_a(); + kSharedValue = value; +} +``` + ### 练习 3:延迟初始化 用 `static` 局部变量实现一个 `get_config` 函数:第一次调用时执行初始化(打印 "Initializing..." 并设置默认值),后续调用直接返回已初始化的值,不再重新初始化。 ```c +typedef struct { + int max_connections; //建议设为5 + int timeout_ms; //建议设为500 + const char* server_name; //建议设为localhost +} Config; + +const Config* get_config(void); +``` + +> 提示:`static` 局部变量只在第一次进入函数时被初始化——正好可以用来实现"只初始化一次"的语义。 + +### 练习 3 参考答案 + +```c +#include + typedef struct { int max_connections; int timeout_ms; @@ -483,9 +629,27 @@ typedef struct { } Config; const Config* get_config(void); -``` -> 提示:`static` 局部变量只在第一次进入函数时被初始化——正好可以用来实现"只初始化一次"的语义。 +int main(void) { + get_config(); //应当输出"Initializing..." + printf("%d %d %s\n",get_config()->max_connections, get_config()->timeout_ms, get_config()->server_name); //应当输出"5 500 localhost" + return 0; +} + +const Config* get_config(void) { + // 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; +} +``` ## 参考资源 diff --git a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md index 0481d0123..2ee6b314c 100644 --- a/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md +++ b/documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md @@ -1,22 +1,23 @@ --- chapter: 1 cpp_standard: -- 11 + - 11 description: 从零开始理解 C 语言指针——内存模型直觉、声明初始化、取地址与解引用运算符、指针的加减运算和距离计算 difficulty: beginner order: 9 platform: host prerequisites: -- 数据类型基础:整数与内存 -- 运算符基础:让数据动起来 + - 数据类型基础:整数与内存 + - 运算符基础:让数据动起来 reading_time_minutes: 10 tags: -- host -- cpp-modern -- beginner -- 入门 + - host + - cpp-modern + - beginner + - 入门 title: 指针入门:地址的世界 --- + # 指针入门:地址的世界 指针大概是 C 语言里名声最响也最容易劝退新手的特性了。如果你之前接触过 Python 或 Java,可能习惯了"变量就是对象本身"的思维——变量里存的就是数据。但到了 C 这边,多了一个关键概念:每个变量都住在内存的某个位置,这个位置有一个编号(地址)。指针就是用来存储和操作这些地址的变量。 @@ -35,7 +36,7 @@ title: 指针入门:地址的世界 我们接下来的所有实验都在这个环境下进行: -- 平台:Linux x86\_64(WSL2 也可以) +- 平台:Linux x86_64(WSL2 也可以) - 编译器:GCC 13+ 或 Clang 17+ - 编译选项:`-Wall -Wextra -std=c17` @@ -268,6 +269,38 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**( 写一个程序,声明三个不同类型的变量(`int`、`double`、`char`),打印它们的值、地址和 `sizeof` 结果。观察地址之间的间隔是否符合各类型的大小。 +### 练习 1 参考答案 + +```c +#include + +int main(void) { + + int value_int = 0; + double value_double = 0.0; + char value_char = '0'; + + printf("(int) value:%d address:%p size:%zu\n", value_int, (void*)&value_int, sizeof(int)); + printf("(double) value:%.2f address:%p size:%zu\n", value_double, (void*)&value_double, sizeof(double)); + printf("(char) value:%d address:%p size:%zu\n", value_char, (void*)&value_char, sizeof(char)); + return 0; +} + +``` + +输出结果可能为(地址每次运行都会变): + +```text +(int) value:0 address:0x7ffd8cecdbec size:4 +(double) value:0.00 address:0x7ffd8cecdbf0 size:8 +(char) value:48 address:0x7ffd8cecdbeb size:1 +``` + +两点值得留意: + +- `char` 的 `value` 显示成 `48`,因为 `'0'` 的 ASCII 码就是 48。C 语言里 `char` 本质上是一个小整数,用 `%d` 打印看到的就是它的整数值(想直接看到字符 `'0'`,把格式符换成 `%c` 即可)。 +- 三个地址的间隔并不等于各自的类型大小。按声明顺序是 `int`(4) → `double`(8) → `char`(1),但实际地址排列成了 `char` → `int` → `double`,相邻差值也不是 4、8、1。原因有两个:编译器会为了内存对齐给局部变量重排位置、插入填充字节;而且栈布局根本不保证按声明顺序排列变量。所以"地址间隔正好等于类型大小"这个直觉,在真实编译器里通常不成立——这正是这道题想让你亲眼看到的。 + ### 练习 2:指针遍历数组 用指针算术遍历一个 `int` 数组并打印所有元素。要求不使用 `[]` 运算符,只用指针加减和解引用: @@ -279,6 +312,38 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**( void print_int_array(const int* data, size_t count); ``` +### 练习 2 参考答案 + +```c +#include +#include // size_t + +void print_int_array(const int* data, size_t count); + +int main(void) { + int arr[] = {1, 2, 3, 4, 5}; + print_int_array(arr, 5); + return 0; +} + +void print_int_array(const int* data, size_t count) { + for (size_t i = 0; i < count;i++) { + printf("data[%zu] = %d\n", i, *(data + i)); + } +} + +``` + +输出结果应当为: + +```text +data[0] = 1 +data[1] = 2 +data[2] = 3 +data[3] = 4 +data[4] = 5 +``` + ## 参考资源 - [cppreference: 指针声明](https://en.cppreference.com/w/c/language/pointer)