Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
06683ea
对C_tutorials 第六章的状态机示例代码添加了注释
owollz4 Jul 13, 2026
bce6334
对C_tutorials 第六章的状态机示例代码添加了注释(修正版)
owollz4 Jul 13, 2026
0a85376
docs(vol1): refine state machine comments
Charliechen114514 Jul 13, 2026
764aa4a
Merge branch 'Awesome-Embedded-Learning-Studio:main' into main
owollz4 Jul 13, 2026
078728b
Merge branch 'Awesome-Embedded-Learning-Studio:main' into main
owollz4 Jul 14, 2026
d180606
对C_tutorials 第二章的练习题添加了参考答案
owollz4 Jul 14, 2026
bb7585e
对C_tutorials 第三章的练习题添加了参考答案
owollz4 Jul 14, 2026
238fabf
fix: lint issue
Charliechen114514 Jul 15, 2026
992c7c4
对 C 语言系统教程第四章第五章提供了参考答案
owollz4 Jul 15, 2026
b9615d4
C 语言系统教程第三章练习题答案 bug 修复
owollz4 Jul 15, 2026
f7370ad
Merge branch 'main' of https://github.com/owollz4/Tutorial_AwesomeMod…
owollz4 Jul 16, 2026
abb4af2
Merge branch 'main' of https://github.com/owollz4/Tutorial_AwesomeMod…
owollz4 Jul 18, 2026
a891139
Update 02B-float-char-const-cast.md
owollz4 Jul 18, 2026
a32f7c4
对 C 语言系统教程的第六章和第七章添加了参考答案
owollz4 Jul 18, 2026
56c11cf
fix: submitted answers get possible sigfault and with implicit declar…
Charliechen114514 Jul 18, 2026
f62fce1
Merge branch 'Awesome-Embedded-Learning-Studio:main' into main
owollz4 Jul 20, 2026
4cc527d
Update 06-scope-and-storage.md
owollz4 Jul 20, 2026
382a9e4
Update 07A-pointer-essentials.md
owollz4 Jul 20, 2026
76c6b9f
Update 07A-pointer-essentials.md
owollz4 Jul 20, 2026
ca5543f
docs(vol1): fix -Wall -Wextra warnings in ch06/07A exercise answers
Charliechen114514 Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 180 additions & 16 deletions documents/vol1-fundamentals/c_tutorials/06-scope-and-storage.md
Original file line number Diff line number Diff line change
@@ -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` 文件里定义了一个辅助函数,结果别的文件不小心也调用了它,后来你改了那个函数的实现,调用方毫无预警地崩了。
Expand Down Expand Up @@ -261,13 +262,13 @@ extern int kValue = 42; // 千万别这么干!

这三者的关系可以用一个表格来总结:

| 声明位置 | 关键字 | 链接性 | 作用域 | 生命周期 |
| --- | --- | --- | --- | --- |
| 函数内 | (无) | 无 | 块 | 自动 |
| 函数内 | `static` | 无 | 块 | 静态 |
| 函数外 | (无) | 外部 | 文件 | 静态 |
| 函数外 | `static` | 内部 | 文件 | 静态 |
| 函数外 | `extern` | (取决于首次声明) | 文件 | 静态 |
| 声明位置 | 关键字 | 链接性 | 作用域 | 生命周期 |
| -------- | -------- | ------------------ | ------ | -------- |
| 函数内 | (无) | 无 | 块 | 自动 |
| 函数内 | `static` | 无 | 块 | 静态 |
| 函数外 | (无) | 外部 | 文件 | 静态 |
| 函数外 | `static` | 内部 | 文件 | 静态 |
| 函数外 | `extern` | (取决于首次声明) | 文件 | 静态 |

这个表格值得多看几眼——注意函数外的 `static` 改变的是链接性(从外部变成内部),而不是作用域或生命周期。

Expand Down Expand Up @@ -455,6 +456,58 @@ void counter_reset(void);

请自行实现 `counter.c`。

### 练习 1 参考答案

main.c

```c
#include <stdio.h>
#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`。要求:
Expand All @@ -471,21 +524,132 @@ void counter_reset(void);
// 各 .c 文件的实现留给你
```

### 练习 2 参考答案

main.c

```c
#include <stdio.h>
#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 <stdio.h>
#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 <stdio.h>
#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 <stdio.h>

typedef struct {
int max_connections;
int timeout_ms;
const char* server_name;
} 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;
}
```

## 参考资源

Expand Down
81 changes: 73 additions & 8 deletions documents/vol1-fundamentals/c_tutorials/07A-pointer-essentials.md
Original file line number Diff line number Diff line change
@@ -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 这边,多了一个关键概念:每个变量都住在内存的某个位置,这个位置有一个编号(地址)。指针就是用来存储和操作这些地址的变量。
Expand All @@ -35,7 +36,7 @@ title: 指针入门:地址的世界

我们接下来的所有实验都在这个环境下进行:

- 平台:Linux x86\_64(WSL2 也可以)
- 平台:Linux x86_64(WSL2 也可以)
- 编译器:GCC 13+ 或 Clang 17+
- 编译选项:`-Wall -Wextra -std=c17`

Expand Down Expand Up @@ -268,6 +269,38 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**(

写一个程序,声明三个不同类型的变量(`int`、`double`、`char`),打印它们的值、地址和 `sizeof` 结果。观察地址之间的间隔是否符合各类型的大小。

### 练习 1 参考答案

```c
#include <stdio.h>

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` 数组并打印所有元素。要求不使用 `[]` 运算符,只用指针加减和解引用:
Expand All @@ -279,6 +312,38 @@ C++ 在指针的基础上做了两个关键的改进。第一个是**引用**(
void print_int_array(const int* data, size_t count);
```

### 练习 2 参考答案

```c
#include <stdio.h>
#include <stddef.h> // 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)
Expand Down
Loading