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
112: Exact-Width Integers with stdint.h
One of the most frustrating things about C is that the language doesn't actually tell you how big an int is. It just says it's "at least 16 bits." Depending on whether you're compiling for an Arduino, a 32-bit ARM chip, or a 64-bit Windows machine, your int might be 2 bytes or 4 bytes. In a vacuum, that doesn't matter. But the moment you start reading data from a file or a network socket, it's a disaster.
I've spent way too many hours debugging "ghost" bugs caused by platform-dependent integer sizes. To avoid this, we use stdint.h. It gives us exact-width types—types that are guaranteed to be the same size regardless of where the code is running.
Defining a Binary Sensor Packet
Let's imagine we're writing a driver for a piece of hardware—say, a digital temperature sensor. The sensor sends us a "packet" of 7 bytes. The first two bytes are the device ID, the next four are a timestamp, and the last byte is the temperature reading. If we get the sizes wrong, we'll read the wrong bytes, and our temperature will look like a random, massive number.
Here is how I'd start sketching out the structure for this packet:
#include <stdio.h>
#include <stdint.h>
typedef struct {
uint16_t device_id; // Exactly 16 bits
uint32_t timestamp; // Exactly 32 bits
uint8_t reading; // Exactly 8 bits
} SensorPacket;
Notice I used uint (unsigned integer) followed by the bit count. uint16_t is exactly 16 bits. This is my safety blanket. I know exactly how many bytes I'm dealing with.
The Trap of Platform-Dependent Types
Now, let's say I'm feeling lazy. I might think, "A timestamp is just a number, I'll just use a standard unsigned int." I've made this mistake plenty of times in my early career.
// This is the "danger" version
typedef struct {
uint16_t device_id;
unsigned int timestamp; // Danger! Size varies by platform
uint8_t reading;
} SensorPacket;
On my development machine, unsigned int is 4 bytes. Everything looks great. But then I push this code to a small 16-bit microcontroller. Suddenly, unsigned int becomes 2 bytes. The reading byte is now shifted 2 positions to the left in memory. When the program tries to read the temperature, it reads part of the timestamp instead. The code compiles perfectly, but the data is garbage.
Locking Down the Widths
To fix this, I have to be disciplined. I go back to uint32_t. By using the _t types from stdint.h, I'm telling the compiler: "I don't care what the native word size of this CPU is; I need exactly 32 bits here."
Let's put it all together into a small program that simulates receiving this binary data. I'll use a byte array to represent the raw data coming off the wire and cast it to our struct to see if we've aligned things correctly.
#include <stdio.h>
#include <stdint.h>>
typedef struct {
uint16_t device_id;
uint32_t timestamp;
uint8_t reading;
} SensorPacket;
int main() {
// Simulating 7 bytes of raw data from a sensor
// ID: 0x0102, Timestamp: 0x0000000A, Reading: 0x19 (25C)
uint8_t raw_data[] = {0x02, 0x01, 0x0A, 0x00, 0x00, 0x00, 0x19};
// Point our struct pointer to the raw data
SensorPacket *packet = (SensorPacket *)raw_data;
printf("Device ID: %u\n", packet->device_id);
printf("Timestamp: %u\n", packet->timestamp);
printf("Temperature: %u C\n", packet->reading);
return 0;
}
One quick side note: in a real-world production environment, you'd also have to worry about "padding" and "endianness" (the order of bytes), but for now, the most important habit you can form is refusing to use int or long when the specific bit-width of your data matters. If it's for a counter that will never exceed 255, use uint8_t. If it's for a memory address, use uintptr_t. Be explicit.
📋 Practical Task
Exercise: Building a Network Packet Header Parser
You are tasked with writing a program that parses a custom network header. The header is exactly 8 bytes long and consists of the following fields:
- Version: 8 bits (unsigned)
- Payload Length: 16 bits (unsigned)
- Sequence Number: 32 bits (unsigned)
- Checksum: 8 bits (unsigned)
Write a program that defines a struct using the correct stdint.h types to represent this header. Then, create a uint8_t` array containing the following hexadecimal values: {0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x05, 0xAF}. Cast this array to your struct and print each field to the console to verify the data was parsed correctly.
There are no comments for now.