C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
11: Type Qualifiers: const and volatile
I was looking through some old driver code yesterday and noticed a lot of keywords that usually get ignored in beginner tutorials. Specifically, const and volatile. On the surface, they seem simple—one stops you from changing things, and the other... well, I'm not entirely sure why we need it until you see the compiler try to "outsmart" you.
Protecting our configuration
Let's say we're writing a small program to manage a sensor. We have a hardware ID that should never, ever change while the program is running. My first instinct is just to use a variable, but that's risky. I might accidentally overwrite it in a different function.
int hardware_id = 0xAF42;
void reset_sensor() {
hardware_id = 0x0000; // Oops, I just wiped the ID.
}
The compiler doesn't care about my "intent" here; it just sees an integer. To fix this, I'll slap const on it. This tells the compiler, "This value is read-only."
const int hardware_id = 0xAF42;
void reset_sensor() {
hardware_id = 0x0000; // Now the compiler screams at me.
}
Now, if I try to compile this, I get a "assignment of read-only variable" error. That's exactly what I want. It's not just about preventing bugs; it's a signal to anyone reading my code that this value is a constant. It also lets the compiler potentially put this value in a read-only section of memory (like .rodata), which is a nice little efficiency win.
The compiler is being too clever
Now, things get weird. Let's imagine we're polling a status flag in memory. In a real scenario, this would be a memory-mapped I/O register that the hardware updates. Since we're on a PC, I'll simulate it with a global variable that I imagine is being changed by some other process or a hardware interrupt.
int status_flag = 0;
void wait_for_ready() {
while (status_flag == 0) {
// Do nothing, just wait for the hardware to set this to 1
}
printf("Sensor is ready!\n");
}
I ran this with optimizations turned on (-O2), and it hung forever. Even if I manually changed status_flag in a debugger, the loop didn't stop. Why? Because the compiler looked at that loop and thought: "Wait, status_flag isn't changed anywhere inside this loop. I'll just load the value into a CPU register once and check that register forever."
The compiler optimized my code into an infinite loop because it assumed the variable couldn't change "magically" from the outside. To stop this, I need volatile.
volatile int status_flag = 0;
void wait_for_ready() {
while (status_flag == 0) {
// Now the compiler is forced to re-read the actual memory address every time
}
printf("Sensor is ready!\n");
}
By adding volatile, I'm telling the compiler: "Don't assume you know the value of this variable. It can change at any moment, regardless of the code I've written here. Always fetch it from memory."
The read-only moving target
You might be wondering if you can use both. It sounds like a contradiction: a variable that is read-only (const) but also changes unpredictably (volatile). But think about a hardware status register. As a programmer, I am not allowed to write to it (so it's const), but the hardware does change its value (so it's volatile).
// A read-only status register at a specific memory address
const volatile int *status_reg = (int *)0x40001000;
void check_status() {
if (*status_reg & 0x01) {
printf("Data available!\n");
}
}
In this case, const prevents me from trying to write to the register (which would likely cause a segmentation fault or a hardware crash), and volatile ensures the compiler doesn't cache the register's value, allowing me to see the real-time state of the hardware.
📋 Practical Task
Fixing the Stuck Hardware Polling Loop
You are working on a driver for a simulated UART (Universal Asynchronous Receiver-Transmitter). The hardware sets a rx_buffer_full flag to 1 when data arrives. However, the current implementation is getting optimized by the compiler, causing the program to hang even when data is present.
Your Task: Modify the following code to ensure the compiler does not optimize away the polling loop, while also ensuring that the UART_BASE_ADDRESS cannot be accidentally modified by other parts of the program.
#include <stdio.h>
// Simulated memory address for the UART status register
int *UART_BASE_ADDRESS = (int *)0x40002000;
void read_uart() {
printf("Waiting for data...\n");
// This loop is being optimized into an infinite loop!
while (*UART_BASE_ADDRESS == 0) {
// Wait for hardware to set the bit to 1
}
printf("Data received!\n");
}
int main() {
read_uart();
return 0;
}
Apply the correct type qualifiers (const and/or volatile) to the UART_BASE_ADDRESS pointer declaration to fix the bug and protect the memory address.
There are no comments for now.