Today, C code relies on type qualifiers such as const and volatile to tell the compiler how it may treat objects in memory. These qualifiers leave the type’s size and representation unchanged, but they attach rules to every read and write and influence which optimizations are allowed. Those rules guide storage placement, control which operations are legal in source code, and limit or permit the transformations a compiler performs during optimization.
Const Qualifier in C
Most C codebases lean on const to mark data that should not change through a particular name or pointer. The qualifier wraps extra rules around an object. Any attempt to modify an object that was defined with a const-qualified type through a casted pointer steps outside the rules of the language, even if the compiler accepts that cast. Compilers also treat const as a strong signal that data can live in read-only storage, so a linker may place such objects in a read-only segment when that matches the target platform.
Read Only Objects in Memory
Marking an object const at file scope tells the compiler that writes through that name are not allowed, and also gives it freedom to place the bytes in a read-only section of the binary. That change affects both the type system and the generated machine code. The type carries the read-only rule during compilation, while the binary marks the underlying storage as read only where the platform and toolchain make that possible.
That declaration sets up max_clients as an integer with static storage duration whose value is fixed from C code perspective. Any line that tries to assign a new value such as max_clients = 200; violates the type rules, so a conforming compiler must issue a diagnostic.
Undefined behavior enters the picture when code removes const from a pointer type and then writes through it to an object that was originally defined as const. In that case the rules treat the object as read only, so the store is not covered by the standard even if it appears to work on a given build. The generated code may still perform the write on some runs, yet the compiler is free to rely on the const qualifier for optimization in ways that break that code.
That store can appear to work on systems where value happens to sit in writable memory, yet the compiler is allowed to make assumptions based on const that break such code in subtle ways. Optimizations may remove reads, keep stale values in registers, or even merge separate variables.
Static storage pairs with const in a natural way. Objects with static storage that carry const are initialized either by an explicit initializer at the point of definition or, if none is written, by zero initialization, which keeps their value well defined for the entire run, like:
Both lines set up objects that never change through those names, and both values are available from the first line of main onward.
Const data is handy for tables and other aggregates that stay fixed. One common style uses a table of bytes or integers that encode configuration in a way that is cheap to read:
Code that computes checksums can read from crc8_table whenever needed. No function is allowed to modify that array through its declared name, and toolchains often place the bytes in a read-only section.
Read-only strings fit the same idea. Constant error messages or status labels can be in a const char array, which keeps accidental modification out of reach.
Concatenation or logging code can use error_prefix as a source of characters without worrying that another part of the codebase will append to it or change the text.
Local variables with const also play a part. Loops may have bounds that never change inside the function body, or intermediate values that must stay fixed after computation. Marking such values with const gives both documentation for people reading the code and extra checks from the compiler.
The name scale never changes, which matches its use as a fixed factor, and any accidental assignment like scale = 1.0; must trigger a compile time diagnostic.
Const in Function Parameters
Function parameters benefit from const in a slightly different way. When a parameter points to data but should not write through that pointer, it can mark the pointed-to object as read only. That keeps the function honest and documents the contract for callers at the same time.
This header tells a reader that count_matches inspects size integers starting at data and leaves them unchanged. Inside the function body, index expressions such as data[i] are valid on the right side of assignments, while any attempt to assign through data[i] conflicts with the const rule and triggers a diagnostic from the compiler.
The function reads from data as much as it needs, and the compiler checks that no store reaches those array elements.
Strings passed through pointers follow the same idea. Functions that read characters but never edit them should accept const char * parameters, like this:
text and prefix are treated as read-only sequences inside count_prefix. The function is free to walk through the bytes and compare them, but not to store into those locations.
Const also affects the pointer variable itself when placed after the * in a declaration. That style fixes the pointer value while leaving the pointed-to data writable.
The declaration spells out that buffer always points to the same location during the call, yet the integers stored at that location can change. Any attempt to assign a new address such as buffer = buffer + 4; conflicts with the const qualifier on the pointer itself.
C syntax allows these placements to combine, and that sometimes shows up in higher level helper functions. A helper that walks an array without modifying it can take a const int * parameter, while a helper that treats a pointer as a cursor but must not lose the original address can take an int * const parameter. Choosing between those two forms depends on what the function is allowed to change and what must stay fixed while the call runs.
Volatile Qualifier in C
Compilers treat the volatile qualifier as a rule that accesses to a volatile object are observable side effects. That means the compiler cannot remove those reads or writes, and it cannot assume repeated reads produce the same value. This matters for device registers, memory shared with interrupt handlers, and other low level cases where reads and writes must stay as real accesses.
Volatile with Hardware Registers
Many embedded targets map device control and status registers directly into the processor address space. Reads and writes to those addresses talk to hardware, not ordinary RAM. When such registers are exposed through C code, the volatile qualifier tells the compiler that every access must be preserved and that the value may change between two lines of code even if there is no visible write in C.
The definition of GPIO_STATUS and GPIO_CONTROL uses pointers to volatile data cast from fixed addresses, then dereferences them to create lvalue expressions. The read from GPIO_STATUS must become a load from the hardware register every time the function executes, and the store to GPIO_CONTROL must become a write, even when no C code ever reads the value back.
Polling loops for status flags rely heavily on this rule. Compilers are free to keep non-volatile values in registers across loop iterations, which would break a status-polling loop. volatile prevents that and keeps each read as a genuine access.
The loop must test ADC_STATUS on every pass, because the hardware can set or clear ADC_READY_BIT between iterations. The compiler cannot fold the read out of the loop or keep a cached copy in a register, since the type marks that register as volatile.
Memory mapped registers are sometimes grouped into structs to make field layout easier to read and to keep related registers under one base address.
Each member of UartRegs is declared volatile, so a read or write to a field such as UART0->STATUS generates an actual access to the mapped address. The struct form does not change the semantics of volatile and mainly affects how the code expresses register layout.
Shared Data in Concurrent Code
Shared variables in multithreaded C code bring a different concern. On a single thread that talks to hardware, volatile handles access preservation. In a multithreaded setting, the ordering of reads and writes between threads also matters, and this area is where plain volatile falls short.
Traditional code sometimes used volatile for flags watched by one thread and written by another. That pattern relied on compilers translating each read and write into an actual memory access, hoping that would keep threads in sync. The C11 memory model and modern practice around concurrency have made it clear that such code has undefined behavior in subtle ways, because volatile does not add the happens-before relationships needed between threads.
The two functions access stop_flag from different threads without atomics or locks, which is a data race in C11 and later. A data race has undefined behavior, so the loop is not required to ever observe the write, and the compiler is allowed to make transformations that break the intent. volatile does not make cross thread communication defined, and it does not provide atomic read modify write updates.
C11 introduced _Atomic types and library functions that combine access preservation with defined ordering across threads. Those facilities are designed for shared state across threads. volatile still fits memory mapped I/O and, for signal handlers, a flag declared as volatile sig_atomic_t.
The _Atomic qualifier and matching functions such as atomic_load_explicit and atomic_store_explicit supply both the per-access semantics and the cross-thread ordering rules that concurrent code needs. volatile remains useful for memory that can change outside the C execution model, while atomics cover updates that must be shared between threads in a portable way.
Some platforms still combine volatile with atomics or with special intrinsics when shared variables also reflect hardware state or must interact with signal handlers. The central idea is that volatile on its own does not provide atomic updates or ordering, it just keeps the compiler from discarding or reordering accesses in ways that ignore external changes.
Combining const with volatile
C allows const and volatile to appear on the same type, and they apply to the same base type when written side by side. An object declared as const volatile unsigned int is treated as a value that can change outside the current C code but must not be written through that particular type.
The pointer above refers to a register that can change between two consecutive reads, and yet the current source promises not to write through status_reg. Any attempt to assign through that pointer conflicts with the const rule and must trigger a compile time diagnostic, while the volatile property still forces the compiler to fetch the register on each read.
Read-only status registers are natural candidates for const volatile. Some devices expose a status word that software polls, but that software never sets bits in that word. Writes either have no effect or are not permitted by the hardware interface.
The access to TIMER_STATUS must always hit the hardware register, yet the code treats the register as read only. Combining const with volatile states both facts in the type system.
Pointers and pointees can carry these qualifiers in many combinations, which are read from right to left around the *. That reading rule lets code builders express subtle interface contracts. A helper may need a pointer that stays fixed but refers to volatile data, or it may accept a pointer to const volatile data that it reads but never updates. Take this for example:
The parameter list states that monitor_values will read from an array of unsigned int values that can change outside the routine and that the routine itself will not write through the pointer. Any caller that passes a pointer to memory mapped registers or to state updated by another execution context can rely on the function signature to preserve that contract.
Conclusion
const and volatile sit where C types meet the optimizer, telling the compiler which writes are forbidden and which reads must still touch memory on every access. The const qualifier keeps data fixed through a given name or pointer so that code and hardware treat those bytes as read only, while volatile stops the compiler from caching or discarding accesses to locations that can change outside normal control flow. Placing these qualifiers carefully in declarations turns the type system into a map of how values move between ordinary variables, memory mapped registers, and shared state, so the compiled result stays aligned with the low level behavior you intend.




![const double pi = 3.141592653589793; static const char app_name[] = "Telemetry Collector"; const double pi = 3.141592653589793; static const char app_name[] = "Telemetry Collector";](https://substackcdn.com/image/fetch/$s_!NDFO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11657d04-8f00-464c-b27a-d59426e0c378_1664x109.png)
![static const unsigned char crc8_table[256] = { /* precomputed values */ }; static const unsigned char crc8_table[256] = { /* precomputed values */ };](https://substackcdn.com/image/fetch/$s_!4C8n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0680fe9-995e-480c-9376-9a55e5e793db_1656x164.png)
![static const char error_prefix[] = "ERROR: "; static const char error_prefix[] = "ERROR: ";](https://substackcdn.com/image/fetch/$s_!uPY5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01f026b1-d426-4770-aaeb-5cbf7eec1ebe_1667x54.png)
![void process_samples(double *samples, size_t count) { const double scale = 0.5; size_t i = 0; while (i < count) { samples[i] = samples[i] * scale; ++i; } } void process_samples(double *samples, size_t count) { const double scale = 0.5; size_t i = 0; while (i < count) { samples[i] = samples[i] * scale; ++i; } }](https://substackcdn.com/image/fetch/$s_!M8zY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dd54482-b7f4-491f-9d14-657f455b3129_1665x505.png)

![size_t count_matches(const int *data, size_t size, int value) { size_t matches = 0; size_t i = 0; while (i < size) { if (data[i] == value) { ++matches; } ++i; } return matches; } size_t count_matches(const int *data, size_t size, int value) { size_t matches = 0; size_t i = 0; while (i < size) { if (data[i] == value) { ++matches; } ++i; } return matches; }](https://substackcdn.com/image/fetch/$s_!WOol!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7579bea-28f1-45df-ae0b-2500d22c76d1_1635x727.png)
![int count_prefix(const char *text, const char *prefix) { size_t i = 0; while (prefix[i] != '\0' && text[i] == prefix[i]) { ++i; } return prefix[i] == '\0'; } int count_prefix(const char *text, const char *prefix) { size_t i = 0; while (prefix[i] != '\0' && text[i] == prefix[i]) { ++i; } return prefix[i] == '\0'; }](https://substackcdn.com/image/fetch/$s_!zX0U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126e493e-8865-4774-a357-70101c86cbfb_1650x507.png)
![void walk_buffer(int * const buffer, size_t size) { size_t i = 0; while (i < size) { buffer[i] = buffer[i] + 1; ++i; } } void walk_buffer(int * const buffer, size_t size) { size_t i = 0; while (i < size) { buffer[i] = buffer[i] + 1; ++i; } }](https://substackcdn.com/image/fetch/$s_!q1IK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f20d904-2b90-4d1c-b17e-ee69d17f614b_1658x456.png)







![void monitor_values(const volatile unsigned int *values, unsigned int count) { unsigned int i = 0; while (i < count) { unsigned int v = values[i]; /* log or process v */ ++i; } } void monitor_values(const volatile unsigned int *values, unsigned int count) { unsigned int i = 0; while (i < count) { unsigned int v = values[i]; /* log or process v */ ++i; } }](https://substackcdn.com/image/fetch/$s_!KNAQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e375b1-f607-4549-b90b-63f424a30085_1661x560.png)