title logo
title right side
12.08.2026 03:58:08 Lokal:
Serverstatus: (4.47/4.47/4.3264.86/1061 GByte  
Aussentemperatur : 18.2 °C  Luftdruck : 1026.01 hPas/mbar
Impressions: 4 
Visits heute: 239 

  Aktuelle Seite: Speicherklassen in C

Home  

In C bestimmen Speicherklassen (storage classes) vor allem, wo und wie lange eine Variable existiert und welche Sichtbarkeit bzw. Verknüpfung (Linkage) sie besitzt.

Die wichtigsten Schlüsselwörter sind:

  • auto
  • register
  • static
  • extern

Zusätzlich ist es wichtig, zwischen Speicherdauer (storage duration) und Sichtbarkeit/Linkage zu unterscheiden.


1. auto – die normale lokale Variable

auto ist für lokale Variablen die Standard-Speicherklasse.

void foo(void)
{
    auto int x = 10;
    int y = 20;       // identisch, auto ist implizit
}

x und y sind hier praktisch dasselbe.

Eigenschaften

  • lokal zur Funktion bzw. zum Block
  • wird beim Eintritt in den Block erzeugt
  • verschwindet beim Verlassen des Blocks
  • bei jedem erneuten Aufruf neu erzeugt
  • nicht automatisch initialisiert

Beispiel:

void counter(void)
{
    int count = 0;

    count++;

    printf("%d\n", count);
}

Bei:

counter();
counter();
counter();

kommt:

1
1
1

Denn count wird bei jedem Aufruf neu erzeugt und mit 0 initialisiert.

In modernem C schreibt man auto fast nie explizit. int count ist die übliche Schreibweise.


2. static innerhalb einer Funktion

Hier wird es interessant.

Eine lokale Variable mit static behält ihren Wert zwischen Funktionsaufrufen.

void counter(void)
{
    static int count = 0;

    count++;

    printf("%d\n", count);
}

Jetzt:

counter();
counter();
counter();

ergibt:

1
2
3

Obwohl count weiterhin nur innerhalb von counter() sichtbar ist, wird sie nicht bei jedem Aufruf neu erzeugt.

Das Entscheidende

                    sichtbar?       Lebensdauer
----------------------------------------------------
int count           nur in Funktion  bis Blockende
static int count    nur in Funktion  gesamtes Programm

Man kann sich static hier also als "lokale Variable mit dauerhaftem Gedächtnis" vorstellen.


Beispiel: Zustand einer Funktion

Das ist beispielsweise praktisch für eine Funktion, die wissen soll, wie oft sie aufgerufen wurde:

int get_next_id(void)
{
    static int id = 0;

    return ++id;
}

Dann:

printf("%d\n", get_next_id());
printf("%d\n", get_next_id());
printf("%d\n", get_next_id());

ergibt:

1
2
3

3. static auf Dateiebene

static hat außerhalb einer Funktion noch eine andere, sehr wichtige Bedeutung.

Betrachten wir:

/* motor.c */

static int speed = 100;

Diese Variable ist nur innerhalb von motor.c bekannt.

Sie kann also nicht von anderen Übersetzungseinheiten aus angesprochen werden.

Das Gleiche gilt für Funktionen:

static void calculate_speed(void)
{
    ...
}

calculate_speed() ist dann eine private Funktion dieses C-Moduls.

Das ist bei größeren C-Projekten extrem nützlich.


Beispiel

motor.c

#include "motor.h"

static int speed = 0;

static void update_pwm(void)
{
    ...
}

void motor_set_speed(int value)
{
    speed = value;
    update_pwm();
}

main.c

#include "motor.h"

int main(void)
{
    motor_set_speed(50);
}

main.c kann motor_set_speed() verwenden.

Aber:

speed = 100;

geht nicht.

Und:

update_pwm();

geht ebenfalls nicht.

Damit kann man ein Modul sauber kapseln.


4. extern – eine Variable befindet sich woanders

extern bedeutet im Wesentlichen:

Diese Variable existiert, aber ihre Definition befindet sich an einer anderen Stelle.

Beispielsweise:

config.c

int debug_level = 2;

Das ist die Definition.

In einer anderen Datei:

main.c

extern int debug_level;

int main(void)
{
    printf("%d\n", debug_level);
}

Hier ist debug_level nur deklariert.

Der Compiler bekommt damit die Information:

Es gibt irgendwo ein int debug_level.

Der Linker findet später die eigentliche Definition in config.c.


Noch besser: Header-Datei

Typischerweise macht man:

config.h

#ifndef CONFIG_H
#define CONFIG_H

extern int debug_level;

#endif

config.c

#include "config.h"

int debug_level = 2;

main.c

#include <stdio.h>
#include "config.h"

int main(void)
{
    printf("Debug-Level: %d\n", debug_level);
}

Das ist eine typische C-Projektstruktur.


5. register – bitte in Ruhe lassen

register ist historisch interessant.

register int i;

Damit konnte man dem Compiler früher einen Hinweis geben:

Diese Variable wird sehr häufig benutzt; leg sie möglichst in einem CPU-Register ab.

Früher war das durchaus relevant.

Heute optimieren moderne Compiler wesentlich besser:

gcc -O2
gcc -O3

Der Compiler entscheidet selbst, ob eine Variable in einem Register landet.

Deshalb schreibt man normalerweise einfach:

int i;

und überlässt die Optimierung dem Compiler.


6. Ein wichtiger Unterschied: Initialisierung

Hier lauert eine typische Falle.

Eine normale lokale Variable:

void foo(void)
{
    int x;

    printf("%d\n", x);   // FEHLER: undefinierter Wert
}

hat keinen definierten Anfangswert.

Eine static-Variable dagegen wird automatisch mit 0 initialisiert:

void foo(void)
{
    static int x;

    printf("%d\n", x);   // 0
}

Das gilt auch für globale Variablen:

int global;

global beginnt mit:

0

7. static wird automatisch initialisiert

Das gilt auch für komplexere Datentypen:

static int values[10];

Alle Elemente sind zunächst:

0 0 0 0 0 0 0 0 0 0

Auch:

static char buffer[1024];

ist vollständig mit '\0' initialisiert.

Das ist beispielsweise bei Puffern sehr praktisch.


8. Die vier Speicherklassen im Überblick

Speicherklasse Typischer Ort Lebensdauer Sichtbarkeit
auto lokal Block Block
register lokal Block Block
static lokal lokal gesamtes Programm Block
static global Datei gesamtes Programm nur diese Datei
extern global gesamtes Programm über Dateien hinweg

Dabei ist extern etwas anders als die anderen: Es beschreibt hauptsächlich eine Deklaration bzw. externe Verknüpfung, nicht einfach einen bestimmten Speicherort.


9. Besonders wichtig: static hat zwei Bedeutungen

Das ist wahrscheinlich die wichtigste Sache, die man sich merken sollte.

Innerhalb einer Funktion

void foo(void)
{
    static int x;
}

bedeutet:

x bleibt über Funktionsaufrufe hinweg erhalten.

Außerhalb einer Funktion

static int x;

bedeutet:

x ist nur in dieser Quelldatei sichtbar.

Und:

static void foo(void)
{
}

bedeutet:

foo() ist nur in dieser Quelldatei sichtbar.


10. Zusammenspiel in einem echten C-Modul

Nehmen wir beispielsweise ein kleines Motor-Modul:

motor.c

#include "motor.h"

static int current_speed = 0;

static void update_hardware(void)
{
    printf("PWM = %d\n", current_speed);
}

void motor_set_speed(int speed)
{
    current_speed = speed;
    update_hardware();
}

int motor_get_speed(void)
{
    return current_speed;
}

Hier sind:

static int current_speed;

und

static void update_hardware(void)

private Implementierungsdetails.

Nach außen gibt es nur:

void motor_set_speed(int speed);
int motor_get_speed(void);

Der Header könnte deshalb so aussehen:

#ifndef MOTOR_H
#define MOTOR_H

void motor_set_speed(int speed);
int motor_get_speed(void);

#endif

Das ist ein sehr schönes Beispiel dafür, wie man mit static in C eine Art Kapselung erreicht.


11. Speicherklasse ist nicht dasselbe wie Speicherort

Ein häufiger Denkfehler ist:

static bedeutet "liegt im RAM".

Das ist so nicht ganz richtig.

Die Speicherklasse beschreibt Eigenschaften wie Lebensdauer und Linkage. Der tatsächliche Speicherort kann beispielsweise sein:

Stack
Heap
Data Segment
BSS
Register
...

Zum Beispiel:

void foo(void)
{
    int a;
    static int b;
}

Typischerweise könnte das vereinfacht so aussehen:

Stack:
    a

statischer Speicher:
    b

a verschwindet nach dem Verlassen von foo().

b bleibt während der gesamten Programmlaufzeit erhalten.


12. Ein schönes Gesamtbeispiel

#include <stdio.h>

int global = 10;

static int private_global = 20;

void test(void)
{
    int normal = 0;
    static int persistent = 0;

    normal++;
    persistent++;

    printf("normal:     %d\n", normal);
    printf("persistent: %d\n", persistent);
}

int main(void)
{
    test();
    test();
    test();

    printf("global: %d\n", global);
    printf("private: %d\n", private_global);

    return 0;
}

Ausgabe:

normal:     1
persistent: 1
normal:     1
persistent: 2
normal:     1
persistent: 3

global: 10
private: 20

Der entscheidende Unterschied ist:

normal
  │
  ├── Aufruf 1 → 0 → 1 → zerstört
  ├── Aufruf 2 → 0 → 1 → zerstört
  └── Aufruf 3 → 0 → 1 → zerstört


persistent
  │
  ├── Aufruf 1 → 0 → 1
  ├── Aufruf 2 → 1 → 2
  └── Aufruf 3 → 2 → 3

Meine praktische Faustregel

Beim Programmieren in C brauchst du in der Praxis hauptsächlich diese drei Muster:

/* normale lokale Variable */
int value;
/* dauerhaft, aber nur innerhalb dieser Funktion */
static int value;
/* privat für dieses C-Modul */
static int value;
static void helper(void);

Und für Variablen, die zwischen mehreren .c-Dateien geteilt werden:

/* header.h */
extern int value;
/* source.c */
int value = 42;

register kannst du bei modernem C praktisch vergessen.

Ja — und genau hier wird C richtig interessant. static, extern, const und volatile beschreiben unterschiedliche Eigenschaften und können deshalb teilweise miteinander kombiniert werden.

Der wichtigste Punkt zuerst:

static und extern betreffen hauptsächlich Sichtbarkeit/Linkage bzw. Lebensdauer, während const und volatile den Zugriff auf ein Objekt beschreiben.

Man kann sich die vier Schlüsselwörter deshalb zunächst so merken:

Schlüsselwort Hauptbedeutung
static dauerhaft bzw. nur in dieser Übersetzungseinheit sichtbar
extern Definition befindet sich woanders
const über diesen Namen nicht verändern
volatile Wert kann sich außerhalb des normalen Programmflusses ändern

1. static const

Das ist eine sehr häufige Kombination.

static const int MAX_RETRIES = 5;

Hier kommen zwei Eigenschaften zusammen:

  • static → nur in dieser .c-Datei sichtbar
  • const → der Wert darf über diesen Namen nicht verändert werden

Zum Beispiel:

static const int MAX_RETRIES = 5;

void foo(void)
{
    printf("%d\n", MAX_RETRIES);

    MAX_RETRIES = 10;   // Fehler
}

Das ist ideal für Konstanten, die nur ein Modul benötigt.

Zum Beispiel:

/* motor.c */

static const int PWM_MAX = 255;
static const int PWM_MIN = 0;

Diese Werte sind für motor.c verfügbar, aber nicht für andere .c-Dateien.


2. extern const

Jetzt wird es etwas interessanter.

extern const int version;

Das bedeutet:

Es gibt irgendwo ein konstantes int namens version.

Beispielsweise:

config.c

const int version = 42;

main.c

extern const int version;

printf("%d\n", version);

main.c kann den Wert lesen:

printf("%d\n", version);

aber:

version = 43;

ist nicht erlaubt.


Aber Vorsicht bei const auf globaler Ebene

In C hat ein globales

const int version = 42;

standardmäßig interne Linkage.

Das bedeutet, dass diese Definition nicht einfach aus einer anderen .c-Datei über extern erreichbar ist.

Wenn du eine globale Konstante tatsächlich zwischen Dateien teilen möchtest, ist diese Variante sinnvoll:

config.h

extern const int version;

config.c

const int version = 42;

Hier gibt es allerdings eine wichtige C-Nuance: Die Definition in config.c muss tatsächlich externe Linkage haben. Bei const allein ist das in C anders als viele C++-Programmierer erwarten.

Die robuste Variante ist:

/* config.c */

extern const int version = 42;

oder, je nach Projektstruktur, eine nicht-const Definition mit entsprechendem API-Design.

Für gewöhnliche Modulkonstanten ist deshalb oft einfach:

static const int version = 42;

die bessere Lösung.


3. static volatile

Diese Kombination ist bei Mikrocontrollern besonders wichtig.

Zum Beispiel:

static volatile int flag;

bedeutet:

  • static → das Objekt bleibt während der gesamten Programmlaufzeit bestehen und ist bei Dateiebene nur innerhalb des Moduls sichtbar
  • volatile → der Compiler darf nicht davon ausgehen, dass sich der Wert zwischen zwei Zugriffen nicht verändert

Ein typisches Beispiel:

static volatile int ready = 0;

void interrupt_handler(void)
{
    ready = 1;
}

void wait_for_ready(void)
{
    while (!ready)
        ;
}

Hier könnte ein Interrupt ready verändern.

Ohne volatile könnte ein optimierender Compiler theoretisch annehmen:

ready wird innerhalb dieser Schleife nicht verändert.

Dann könnte er die Schleife beispielsweise effektiv zu einer Endlosschleife optimieren.

Mit:

volatile

wird jeder Zugriff tatsächlich als relevanter Zugriff behandelt.


4. volatile bedeutet NICHT "thread-safe"

Das ist extrem wichtig.

Viele C-Programmierer denken:

volatile int counter;

würde parallelen Zugriff sicher machen.

Nein.

volatile bedeutet nicht:

  • atomar
  • mutex
  • threadsicher
  • synchronisiert

Zum Beispiel:

volatile int counter;

counter++;

ist nicht automatisch atomar.

counter++ besteht vereinfacht aus:

lesen
addieren
schreiben

Ein anderer Thread kann dazwischenkommen.

Für echte Thread-Synchronisation benötigt man beispielsweise C11-Atomics:

#include <stdatomic.h>

atomic_int counter;

5. const volatile

Diese Kombination sieht zunächst widersprüchlich aus:

const volatile int status;

Ist der Wert nun konstant oder kann er sich ändern?

Beides.

const bedeutet:

Dein C-Programm darf den Wert nicht über diesen Zugriff verändern.

volatile bedeutet:

Der Wert kann sich außerhalb des normalen C-Programms verändern.

Das ist bei Hardware-Registern sehr typisch.

Beispielsweise könnte ein Mikrocontroller ein Statusregister besitzen:

#define STATUS (*(const volatile uint32_t *)0x40000000)

Dann:

uint32_t status = STATUS;

liest den Hardwarewert.

Aber:

STATUS = 123;

ist verboten.

Warum?

Weil das Register möglicherweise read-only ist, sich aber durch Hardware jederzeit ändern kann.


6. volatile const ist dasselbe

Diese beiden Schreibweisen sind identisch:

const volatile int status;

und:

volatile const int status;

Die Reihenfolge spielt hier keine Rolle.


7. static volatile bei Interrupts

Für deine Mikrocontroller-Projekte ist dieses Muster besonders relevant.

Stell dir vor, ein Interrupt setzt ein Flag:

static volatile bool event = false;

void timer_interrupt(void)
{
    event = true;
}

void main_loop(void)
{
    while (1)
    {
        if (event)
        {
            event = false;

            do_something();
        }
    }
}

static sorgt hier für die Kapselung:

event
 ↓
nur dieses C-Modul

volatile sagt dem Compiler:

event
 ↓
kann jederzeit außerhalb des normalen Programmflusses geändert werden

Das ist ein sehr typisches Embedded-C-Muster.


8. extern volatile

Auch das ist häufig.

Nehmen wir an, ein Interrupt-Handler befindet sich in interrupt.c.

interrupt.c

volatile int adc_ready = 0;

void ADC_IRQHandler(void)
{
    adc_ready = 1;
}

In einer anderen Datei:

main.c

extern volatile int adc_ready;

int main(void)
{
    while (!adc_ready)
        ;

    process_adc();
}

Hier bedeutet:

extern volatile int adc_ready;

gleichzeitig:

  • extern → Definition befindet sich in einer anderen Übersetzungseinheit
  • volatile → Wert kann sich unabhängig vom normalen Programmfluss ändern

9. extern volatile + Header

In einem richtigen Projekt würde ich das so organisieren:

adc.h

#ifndef ADC_H
#define ADC_H

extern volatile int adc_ready;

void ADC_IRQHandler(void);

#endif

adc.c

#include "adc.h"

volatile int adc_ready = 0;

void ADC_IRQHandler(void)
{
    adc_ready = 1;
}

main.c

#include "adc.h"

int main(void)
{
    while (!adc_ready)
        ;

    process_adc();
}

Das ist eine saubere Trennung zwischen Deklaration und Definition.


10. static und extern kann man nicht sinnvoll kombinieren

Das hier ist ein Widerspruch:

static extern int value;

Denn:

static → nur innerhalb dieser Übersetzungseinheit
extern  → Definition mit externer Linkage

Die beiden Linkage-Eigenschaften widersprechen sich.

Man sollte also normalerweise nicht schreiben:

static extern int value;

11. Kombinationen als Übersicht

Sehr nützlich ist diese Tabelle:

Deklaration Bedeutung
static int x; dauerhaft, intern sichtbar
static const int x = 10; private Konstante
static volatile int x; private Variable, kann extern verändert werden
static const volatile int x; private, read-only aus C-Sicht, extern veränderbar
extern int x; Variable wird woanders definiert
extern const int x; externe read-only-Variable
extern volatile int x; externe Variable, kann sich außerhalb des normalen Programmflusses ändern
extern const volatile int x; externe, read-only Hardware-/Statusvariable

12. Ein besonders schönes Embedded-Beispiel

Angenommen, wir haben ein UART-Modul.

uart.c

#include "uart.h"

static volatile bool rx_ready = false;

static const int BUFFER_SIZE = 128;

static char rx_buffer[BUFFER_SIZE];

void UART_IRQHandler(void)
{
    /*
     * Zeichen vom UART lesen
     */
    rx_ready = true;
}

bool uart_data_available(void)
{
    return rx_ready;
}

Hier haben wir gleich mehrere Speicherklassen bzw. Qualifier:

static volatile bool rx_ready;

bedeutet:

rx_ready gehört ausschließlich zu diesem Modul und kann jederzeit durch einen Interrupt verändert werden.

Und:

static const int BUFFER_SIZE = 128;

bedeutet:

BUFFER_SIZE ist eine unveränderliche Konstante, die ausschließlich dieses Modul benötigt.


13. Der wichtigste Denkansatz

Ich würde die vier Begriffe nicht als eine einzige Kategorie lernen.

Denke stattdessen in zwei Ebenen:

Ebene 1: Wer sieht die Variable?

static
extern

Also:

              Wer kann darauf zugreifen?
                       │
             ┌─────────┴─────────┐
             │                   │
          static              extern
             │                   │
        dieses Modul       andere Module möglich

Ebene 2: Wie darf auf den Wert zugegriffen werden?

const
volatile

Also:

              Zugriff auf den Wert
                       │
             ┌─────────┴─────────┐
             │                   │
           const              volatile
             │                   │
       nicht schreiben       kann sich unerwartet
       über diesen Namen     verändern

Und genau deshalb lassen sich die Qualifier kombinieren:

             Sichtbarkeit             Zugriff
                  │                     │
              static/extern          const/volatile
                  │                     │
                  └──────────┬──────────┘
                             │
                  static volatile
                  extern volatile
                  static const
                  static const volatile
                  extern const volatile

Das ist meiner Meinung nach die sauberste Art, sich das zu merken.

Das ist ein Bereich, bei dem man in C leicht eine falsche Vorstellung bekommt:

volatile sorgt nicht für Synchronisation. _Atomic sorgt nicht automatisch für Hardware-I/O. Memory Barriers sind wiederum noch eine dritte Sache.

Gerade bei deinen Mikrocontroller-Projekten mit Interrupts ist die Unterscheidung wichtig.


1. Drei völlig unterschiedliche Probleme

Stell dir vor, zwei Ausführungskontexte greifen auf dieselben Daten zu:

        Hauptprogramm
             │
             │
             ▼
        gemeinsame Daten
             ▲
             │
             │
          Interrupt

Es gibt dabei drei verschiedene Fragen:

A. Darf der Compiler den Zugriff wegoptimieren?

volatile

B. Ist ein Zugriff atomar und zwischen Threads korrekt synchronisiert?

_Atomic

C. In welcher Reihenfolge werden Speicherzugriffe für andere Ausführungskontexte sichtbar?

→ Memory Ordering / Memory Barriers

Diese drei Dinge werden häufig miteinander verwechselt.


2. Was volatile tatsächlich macht

Nehmen wir:

volatile int flag;

Wenn du schreibst:

while (!flag) {
}

muss der Compiler davon ausgehen, dass sich flag zwischen zwei Lesezugriffen verändern kann.

Ohne volatile könnte ein optimierender Compiler möglicherweise daraus machen:

int temp = flag;

while (!temp) {
}

Denn aus Sicht des normalen C-Programms gibt es keinen Grund anzunehmen, dass flag plötzlich von außen verändert wird.

Mit:

volatile int flag;

sind die Zugriffe auf das Objekt observable accesses und dürfen nicht einfach wie normale Zugriffe wegoptimiert werden.


3. Was volatile NICHT macht

volatile macht einen Zugriff nicht automatisch atomar.

Das hier:

volatile int counter;

counter++;

ist nicht zwingend eine unteilbare Operation.

Vereinfacht:

LOAD counter
ADD 1
STORE counter

Wenn zwei Ausführungskontexte gleichzeitig arbeiten:

A: LOAD counter       → 10
B: LOAD counter       → 10
A: ADD 1              → 11
B: ADD 1              → 11
A: STORE              → 11
B: STORE              → 11

Ergebnis:

11

obwohl man vielleicht 12 erwartet hätte.

volatile verhindert das nicht.


4. _Atomic löst ein anderes Problem

C11 bietet dafür:

#include <stdatomic.h>

atomic_int counter;

Dann:

atomic_fetch_add(&counter, 1);

ist eine atomare Operation.

Zwei Threads können gleichzeitig:

atomic_fetch_add(&counter, 1);

ausführen, ohne dass eine Erhöhung verloren geht.


5. volatile vs. _Atomic

Eine gute Merkhilfe:

volatile
    ↓
"Der Wert könnte sich außerhalb meines normalen
 Programmflusses verändern."

_Atomic
    ↓
"Mehrere Ausführungskontexte dürfen sicher
 auf dieses Objekt zugreifen."

Oder noch kürzer:

volatile → Sichtbarkeit gegenüber Compiler
atomic   → Synchronisation zwischen Ausführungskontexten

6. Ein klassisches Beispiel

Angenommen, ein Thread erzeugt Daten:

data = 42;
ready = 1;

Ein anderer Thread wartet:

while (!ready)
    ;

printf("%d\n", data);

Mit einfachen Variablen besteht hier ein Synchronisationsproblem.

Auch:

volatile int ready;

macht daraus noch keine saubere Thread-Synchronisation.


7. Mit C11-Atomics

Wir können schreiben:

#include <stdatomic.h>

int data;
atomic_bool ready = false;

Produzent:

data = 42;

atomic_store_explicit(
    &ready,
    true,
    memory_order_release
);

Konsument:

while (!atomic_load_explicit(
    &ready,
    memory_order_acquire
)) {
}

printf("%d\n", data);

Jetzt haben wir eine definierte Synchronisation.

Der entscheidende Punkt ist:

Produzent                         Konsument

data = 42
   │
   ▼
release store ───────────────► acquire load
                                  │
                                  ▼
                              data == 42

Der release-Store veröffentlicht die vorherigen Speicherzugriffe.

Der acquire-Load stellt sicher, dass die entsprechenden vorherigen Schreibvorgänge sichtbar werden.


8. memory_order_release

Bei:

atomic_store_explicit(
    &ready,
    true,
    memory_order_release
);

bedeutet release vereinfacht:

Alles, was ich vor diesem Store geschrieben habe, darf nicht einfach hinter den Store verschoben werden.

Also:

data = 42;
data2 = 123;

atomic_store_explicit(
    &ready,
    true,
    memory_order_release
);

Die Veröffentlichung von ready kommt logisch nach den Daten.


9. memory_order_acquire

Auf der anderen Seite:

atomic_load_explicit(
    &ready,
    memory_order_acquire
);

bedeutet vereinfacht:

Nach dem erfolgreichen Acquire dürfen nachfolgende Zugriffe nicht einfach vor diesen Zugriff verschoben werden.

Damit:

if (atomic_load_explicit(
        &ready,
        memory_order_acquire))
{
    use(data);
}

data korrekt nach der Synchronisation gelesen wird.


10. Memory Ordering ist nicht dasselbe wie Memory Barrier

Hier wird die Terminologie etwas kompliziert.

Eine Memory Barrier/Fence ist eine konkrete Synchronisationsoperation.

C11 bietet beispielsweise:

atomic_thread_fence(memory_order_release);

oder:

atomic_thread_fence(memory_order_acquire);

Atomare Operationen mit acquire/release enthalten aber bereits die notwendige Speicherordnung für den jeweiligen Synchronisationsfall.

Deshalb braucht man häufig keine explizite Fence.


11. Warum Memory Barriers überhaupt notwendig sind

Moderne Prozessoren und Compiler dürfen Anweisungen umordnen, wenn das beobachtbare Verhalten innerhalb eines einzelnen Threads gleich bleibt.

Beispielsweise könnte man sich vorstellen:

a = 1;
b = 2;

als:

a = 1
b = 2

oder intern:

b = 2
a = 1

Wenn ein einzelner Thread beide Werte nur danach liest, ist das normalerweise egal.

Bei zwei Ausführungskontexten kann es aber relevant werden.


12. Compiler-Reordering vs. CPU-Reordering

Es gibt sogar zwei verschiedene Ebenen:

C-Quellcode
    │
    ▼
 Compiler
    │
    │  kann Anweisungen umordnen
    ▼
Maschinencode
    │
    ▼
 CPU
    │
    │  kann Speicherzugriffe unterschiedlich
    │  sichtbar machen
    ▼
 Speicher

Eine korrekte Synchronisation muss gegebenenfalls beide Ebenen berücksichtigen.

Deshalb ist ein simples:

volatile

kein Ersatz für eine Speicherbarriere.


13. Warum das bei ARM besonders interessant ist

Bei modernen ARM-Prozessoren, also beispielsweise vielen Cortex-M- und Cortex-A-Systemen, gibt es ebenfalls definierte Speicherordnungen und entsprechende Hardwaremechanismen.

Eine C11-Atomic-Operation kann vom Compiler beispielsweise in entsprechende ARM-Instruktionen übersetzt werden.

Das ist einer der großen Vorteile von _Atomic:

Du formulierst:

atomic_store_explicit(
    &flag,
    true,
    memory_order_release
);

und der Compiler erzeugt die für die jeweilige Architektur erforderliche Implementierung.

Du musst normalerweise nicht selbst ARM-Assembler schreiben.


14. Interrupts sind ein Sonderfall

Jetzt kommen wir zu deinem wahrscheinlich wichtigsten Anwendungsfall.

Angenommen:

volatile bool event = false;

Interrupt:

void TIMER_IRQHandler(void)
{
    event = true;
}

Hauptprogramm:

while (1)
{
    if (event)
    {
        event = false;

        process_event();
    }
}

Das ist bei Mikrocontrollern ein sehr verbreitetes Muster.

Und volatile ist hier sinnvoll.

Warum?

Weil event durch den Interrupt verändert wird.


15. Aber ist das vollständig sicher?

Nicht unbedingt.

Es gibt mehrere Fragen.

Ist bool atomar?

Auf einer bestimmten MCU wahrscheinlich ja.

Aber C garantiert dir nicht pauschal für jedes Objekt und jede Plattform dieselbe Hardware-Eigenschaft.

Gibt es noch andere Daten?

Zum Beispiel:

volatile bool event;
int adc_value;

Interrupt:

void ADC_IRQHandler(void)
{
    adc_value = read_adc();
    event = true;
}

Hauptprogramm:

if (event)
{
    printf("%d\n", adc_value);
}

Jetzt haben wir ein Ordering-Problem.

Wir wollen:

adc_value schreiben
       ↓
event setzen
       ↓
Hauptprogramm sieht event
       ↓
Hauptprogramm liest adc_value

und nicht:

event setzen
       ↓
Hauptprogramm sieht event
       ↓
adc_value möglicherweise noch nicht korrekt sichtbar

Auf vielen kleinen MCUs funktioniert das einfache Muster in der Praxis aufgrund der konkreten Architektur und Compiler-Annahmen.

Aber wenn du es formal sauber und portabel machen willst, brauchst du eine geeignete Synchronisation.


16. _Atomic für ein Interrupt-Flag

C11 erlaubt:

#include <stdatomic.h>

static atomic_bool event = false;

ISR:

void TIMER_IRQHandler(void)
{
    atomic_store_explicit(
        &event,
        true,
        memory_order_release
    );
}

Hauptprogramm:

if (atomic_load_explicit(
        &event,
        memory_order_acquire))
{
    process_event();
}

Das ist semantisch wesentlich stärker als:

volatile bool event;

17. Aber: Atomics in einer ISR sind nicht automatisch harmlos

Das ist ein sehr wichtiger Embedded-Punkt.

C11 sagt dir nicht:

Jede Atomic-Operation ist garantiert interrupt-sicher.

Eine Atomic-Operation kann auf einer Plattform beispielsweise intern eine Bibliotheksfunktion aufrufen oder komplexere Synchronisation benötigen.

Für eine ISR möchte man normalerweise:

  • deterministische Laufzeit
  • keine Locks
  • keine Blockierung
  • möglichst wenige Instruktionen

Deshalb sollte man prüfen, ob der verwendete Atomic-Typ lock-free ist.

Zum Beispiel:

atomic_is_lock_free(&event)

kann Auskunft geben.

Noch besser ist die Dokumentation des konkreten Compilers und der MCU.


18. Ein typischer Embedded-Sonderfall: Hardware-Register

Hier ist volatile genau das richtige Werkzeug.

Beispielsweise:

#define GPIO_STATUS \
    (*(volatile uint32_t *)0x40000000u)

Dann:

while (!(GPIO_STATUS & 1))
    ;

Hier darf der Compiler nicht einfach einmal lesen und das Ergebnis wiederverwenden.

Denn die Hardware kann das Register jederzeit verändern.

_Atomic wäre hier nicht das eigentliche Konzept.

Denn wir wollen nicht sagen:

Mehrere Threads verändern dieses Objekt atomar.

Wir wollen sagen:

Dieses Objekt repräsentiert Hardware und jeder Zugriff ist relevant.


19. const volatile bei Hardware

Besonders schön ist:

#define STATUS \
    (*(const volatile uint32_t *)0x40000000u)

Das bedeutet:

const
  ↓
Programm darf nicht schreiben

volatile
  ↓
Hardware darf den Wert jederzeit verändern

Also beispielsweise:

uint32_t status = STATUS;

ist erlaubt.

Aber:

STATUS = 123;

nicht.

Das ist eine der wichtigsten praktischen Anwendungen von const volatile.


20. Memory Barrier bei Hardware

Es gibt noch einen weiteren Fall.

Angenommen, du schreibst:

DATA_REG = value;
CONTROL_REG = START;

Du willst möglicherweise sicherstellen:

DATA_REG schreiben
       ↓
CONTROL_REG schreiben
       ↓
Hardware startet

Hier kann eine Hardware-Memory-Barrier erforderlich sein.

Bei GCC/Clang findet man beispielsweise Architekturfunktionen oder Builtins wie:

__atomic_thread_fence(__ATOMIC_RELEASE);

Aber bei Embedded-Programmierung sollte man sich hier an die Vorgaben des konkreten MCU-Herstellers halten.

Denn es gibt einen Unterschied zwischen:

Compiler Barrier

und:

CPU/Hardware Memory Barrier

und:

C11 Atomic Memory Ordering

21. C11 Atomic Memory Orders

C11 stellt mehrere Memory Orders bereit:

memory_order_relaxed
memory_order_consume
memory_order_acquire
memory_order_release
memory_order_acq_rel
memory_order_seq_cst

In der Praxis sind besonders diese vier wichtig:

relaxed

atomic_fetch_add_explicit(
    &counter,
    1,
    memory_order_relaxed
);

Garantiert Atomizität, aber keine zusätzliche Reihenfolge anderer Speicherzugriffe.

Gut für:

Statistiken
Zähler
Referenzzähler in bestimmten Designs

release

Für den Produzenten:

atomic_store_explicit(
    &ready,
    true,
    memory_order_release
);

acquire

Für den Konsumenten:

atomic_load_explicit(
    &ready,
    memory_order_acquire
);

Das klassische Paar:

        PRODUCER
           │
       Daten schreiben
           │
           ▼
    release store
           │
           │
       Synchronisation
           │
           ▼
    acquire load
           │
           ▼
       Daten lesen
        CONSUMER

seq_cst

Die stärkste und einfachste Variante:

atomic_store(
    &ready,
    true
);

Wenn du keine Memory Order angibst, ist die Standardeinstellung:

memory_order_seq_cst

Das ist die strengste Ordnung.

Für den Einstieg ist das oft die beste Wahl.


22. Praktischer Vergleich

Nur volatile

volatile bool flag;

Gut für:

Hardware-Register
Interrupt-induzierte Änderungen
Memory-mapped I/O

Aber:

keine allgemeine Thread-Synchronisation
keine Atomizitätsgarantie
keine allgemeine Memory-Ordering-Garantie

_Atomic

atomic_bool flag;

Gut für:

Threads
Lock-free Kommunikation
atomare Zustandsänderungen
Synchronisation

volatile _Atomic

Das kann tatsächlich sinnvoll sein:

volatile atomic_int register_shadow;

Aber nur, wenn beide Eigenschaften gebraucht werden.

Das ist allerdings deutlich seltener.


23. Ein besonders wichtiges Beispiel: Daten + Flag

Angenommen:

struct sensor_data {
    int temperature;
    int humidity;
};

static struct sensor_data data;
static volatile bool ready;

ISR:

void SENSOR_IRQHandler(void)
{
    data.temperature = read_temperature();
    data.humidity = read_humidity();

    ready = true;
}

Hauptprogramm:

if (ready)
{
    printf(
        "%d %d\n",
        data.temperature,
        data.humidity
    );

    ready = false;
}

Das funktioniert auf vielen Embedded-Systemen in der Praxis.

Aber die saubere Synchronisationsidee ist:

                 ISR

temperature = ...
humidity    = ...
       │
       │
       ▼
   release
       │
       ▼
   ready = true
       │
       │
       ▼
========================
       │
       ▼
  acquire
       │
       ▼

             Hauptprogramm

       data.temperature
       data.humidity

Das Flag ist dabei nicht einfach nur ein Flag.

Es ist ein Synchronisationspunkt.


24. Was ich bei deinen Mikrocontroller-Projekten verwenden würde

Ich würde die Dinge gedanklich in drei Kategorien aufteilen:

Hardware-Register

volatile uint32_t *reg;

oder:

const volatile uint32_t *reg;

Einfaches Interrupt-Flag

Wenn der konkrete MCU/Compiler das garantiert und der Zugriff natürlich atomar ist:

static volatile bool event;

Das ist ein sehr typisches Embedded-Muster.


Komplexere Kommunikation zwischen ISR und Hauptprogramm/Threads

Wenn dein Compiler/Target C11-Atomics sinnvoll unterstützt:

static atomic_bool event;

mit geeigneter Memory Order.

Bei Datenstrukturen und mehreren gemeinsam genutzten Variablen sollte man die Synchronisation bewusst entwerfen und nicht einfach überall volatile davor schreiben.


25. Und noch eine wichtige Falle: volatile schützt nicht vor Race Conditions

Dieser Code:

volatile int counter;

void interrupt(void)
{
    counter++;
}

int main(void)
{
    counter++;
}

ist trotz volatile problematisch.

Denn beide machen:

lesen
ändern
schreiben

gleichzeitig.

Wenn der Zähler wirklich atomar erhöht werden soll, wäre beispielsweise:

atomic_int counter;

und:

atomic_fetch_add(&counter, 1);

das richtige Konzept — sofern diese Atomic-Operation auf deinem konkreten MCU in einer ISR geeignet ist.

Alternativ kann man im Embedded-Bereich einen kurzen kritischen Abschnitt verwenden:

disable_interrupts();

counter++;

enable_interrupts();

Das ist ebenfalls eine Form der Synchronisation, aber natürlich plattformspezifisch.


26. Die wichtigste Tabelle

Problem Werkzeug
Hardware kann Wert verändern volatile
Hardware-Register lesen volatile
Read-only Hardware-Register const volatile
Atomarer Zugriff _Atomic
Thread-Synchronisation _Atomic + Memory Order
Compiler darf Zugriff nicht entfernen volatile
Reihenfolge zwischen Threads Acquire/Release
CPU-Speicherbarriere Atomic Fence / CPU Barrier
Race Condition verhindern Atomics, Mutex oder kritischer Abschnitt
ISR verändert Flag häufig volatile, ggf. _Atomic
ISR verändert komplexe Daten Synchronisationskonzept erforderlich

Die vier Begriffe in einem Bild

                         GEMEINSAME DATEN
                               │
              ┌────────────────┼────────────────┐
              │                │                │
           volatile          _Atomic          Barrier
              │                │                │
              ▼                ▼                ▼
        "Zugriff ist       "Zugriff ist      "Diese
         tatsächlich        atomar und        Speicher-
         relevant"          kann             operationen
                            synchronisieren"  haben diese
                                             Ordnung"
              │                │                │
              └────────────────┴────────────────┘
                               │
                         const als Zusatz
                               │
                               ▼
                    "Über diesen Zugriff
                     nicht veränderbar"

Für Embedded C ist die vielleicht wichtigste Merkhilfe daher:

volatile ist für Dinge, die sich verändern können. _Atomic ist für Dinge, auf die mehrere Ausführungskontexte zugreifen. Eine Memory Barrier regelt die Reihenfolge dieser Zugriffe.

Und bei einem Memory-Mapped Hardware-Register ist volatile richtig, während bei einer Kommunikation zwischen zwei Threads _Atomic bzw. Mutex/Lock richtig ist. Ein volatile vor eine normale globale Variable zu setzen, um damit eine Race Condition zu "beheben", ist dagegen ein klassischer Fehler.