我真的搞明白C了吗

指针和内存杂谈

Posted by moment on August 15, 2026

前言

我真的搞明白C了吗?
这是我在和胡文信讨论memcpy的时候想到的。内存我们自以为已经很清楚了(因为嵌入式中flash可以取指执行所以这里也算在内存里),栈,栈的栈顶指针,flash的分页,RAM的对齐,链接器的地址….但是内存地址,我们真的理解了吗?为什么有的0x08000000,有的0x00000000指的是同一个地方呢? Tsoding的评论区底下,也有非常多人不理解,为什么J语言中指针指向的内容长度为0,我们就可以进行空解引用?我已经做了很多指针的相关运用了,应当是很懂指针了才对,但是….我们真的懂吗,我们奉为圭臬的不能解引用空指针等思想,到底是不是和前文的main一样,是一种路径依赖或者说思维惯式呢?
因此我写了这篇文章,旨在以新的角度看待内存和指针,以实践的角度搞清楚。

内存

它连续吗?

指针

In Jai size of void is zero. Which means it is safe to dereference null because it reads 0 bytes from memory

tsoding发了这么一条推文。

什么是指针?

C与多线程

volatile

在计算机编程中,如果一个变量的值可以被当前执行线程以外的其他程序异步读取或修改,则称该变量为 volatile 变量 。
volatile 的值可能会因以下原因而自发改变:与其他线程共享值;与异步信号处理程序共享值;通过内存映射 I/O 访问硬件设备(通过读写内存来接收和发送来自外围设备的消息)。不同编程语言对这些用例的支持程度差异很大,即使它们都支持 volatile 关键字。volatile 特性会对函数调用约定以及变量的存储、访问和缓存方式产生影响。
在C/C++中,volatile 关键字的行为有时被描述为抑制编译器优化:

  1. 不移除已有的 volatile 读取和写入操作;
  2. 不添加新的 volatile 读取和写入操作;
  3. 不重新排列 volatile 读取和写入操作的顺序。
    乍一看,我们可能认为volatile就是原子操作的保证。但是事实并非如此。它只能实现:允许访问内存映射 I/O 设备,允许在 longjmp 过程中保留值,允许在信号处理程序和程序的其余部分之间共享 volatile sig_atomic_t 对象中的值。
    人们普遍误以为 volatile 关键字在 C 和 C++ 的多线程代码中很有用。事实上, volatile 关键字在 C 和 C++ 中从未作为任何多线程场景下的有效可移植工具发挥作用
    与 Java 和 C# 编程语言不同,C 和 C++ 中对 volatile 变量的操作并非原子操作,并且缺乏足够的 volatile 顺序保证(即内存屏障 )。大多数 C 和 C++ 编译器、链接器和运行时环境都无法提供必要的内存顺序保证,因此 volatile 关键字在任何多线程场景中都无法发挥作用。在 C11 和 C++11 标准出现之前,程序员只能依赖各个实现和平台(例如 POSIX 和 WIN32)提供的保证来编写多线程代码。而有了现代的 C11 和 C++11 标准,程序员可以使用诸如 std::atomic<T> 模板之类的新型可移植结构来编写可移植的多线程代码。
    一个常见的例子是指向外设的内存指针:
    //定义一个指针变量,使其等于某段地址强转为的指针,以便解引用。
    uint16_t *foo = (uint16_t*) 0x400100;
    while(*foo!=0){
     //do something
     }
    

    编译器会认为foo除了初始化之外没有任何东西能修改它,循环会被优化为一个常驻循环while(true); 这里,我们只需要加入volatile就能避免优化,这就是内存映射IO的用例。

内存屏障

内存屏障 (也称为内存栅栏 、 内存围栏或栅栏指令 )是一种屏障指令 ,它使中央处理器 (CPU) 或编译器强制执行屏障指令前后执行的内存操作的顺序约束。这通常意味着,在屏障指令之前执行的操作保证会在屏障指令之后执行的操作之前执行。
不同的指令集架构中,内存屏障的实现各不相同,可能之后会单独开一篇文章来讲。
内存屏障存在的原因是CPU的乱序执行:

乱序执行

在计算机工程中, 乱序执行 (或更正式地称为动态执行 )是一种指令调度范式,用于高性能中央处理器 (CPU) 中,以利用原本会被浪费的指令周期 。在这种范式中,处理器根据输入数据和执行单元的可用性来执行指令 , 而不是按照指令在程序中的原始顺序执行。 这样,处理器可以避免因等待前一条指令完成而处于空闲状态,同时可以处理能够立即独立运行的后续指令。
常见的例子如下:

线程 #1 核心 #1:

while (f == 0) {
    continue;
}
// Memory fence required here
printf("%d", x);

线程 #2 核心 #2:

x = 42;
// Memory fence required here
f = 1;

按常理,可能会预期打印语句总是打印数字“ 42 ”;然而,如果线程#2 的存储操作乱序执行,则 f 可能在 x 之前更新,因此打印语句可能会打印“ 0 ”。类似地,线程#1 的加载操作也可能乱序执行,并且 x 可能在 f 被检查之前被读取,因此打印语句同样可能会打印出意外的值。对于大多数程序而言,这两种情况都是不可接受的。必须在线程#2 将 f 赋值给 x 之前插入一个内存屏障,以确保 x 的新值在 f 的值发生变化时或之前对其他处理器可见。另一个重点是,在线程#1 访问 x 之前也必须插入一个内存屏障,以确保 x 的值不会在 f 的值发生变化之前被读取。

优先级反转

考虑两个优先级分别为高和低的任务 H 和 L ,它们都可以独占使用共享资源 R。 如果 H 在 L 获得 R 之后尝试获取它,那么 H 将被阻塞,直到 L 释放该资源。在一个设计良好的系统中,共享独占资源(本例中为 R )通常意味着 L 会及时释放 R, 以避免 H (优先级更高的任务)长时间被阻塞。然而,即使设计良好,也可能出现这样的情况:在 L 使用 R 期间 ,优先级为中等的第三个任务 M 变为可运行状态。此时,由于 M 的优先级高于 L ,它会抢占 L (因为 M 不依赖于 R ),导致 L 无法及时释放 R ,进而导致优先级最高的进程 H 无法运行(也就是说, H 遭受了由低优先级任务(例如 M )间接导致的意外阻塞)。