Appearance
[中文版本](/posts/c教程/zh-CN/09-可移植与现代 C/082-Portability and Endianness)
Portability means your C program behaves the same way everywhere, on Linux, Windows, ARM, x86, or even a tiny microcontroller. Writing portable code is one of the hardest and most important skills in systems programming.
This section helps you understand the biggest low-level trap of all: endianness, and how to write code that runs safely across architectures.
Step 1. What Is Portability?
A portable C program is one that:
- Compiles cleanly with different compilers.
- Runs correctly on 32-bit, 64-bit, and embedded systems.
- Does not assume details of CPU, OS, or compiler behavior.
Portability depends on respecting what the C standard guarantees, and avoiding assumptions that might be true only on your machine.
Step 2. Why Portability Matters
You might write a C program on macOS (little-endian x86_64) and later need to run it on:
- A Raspberry Pi (ARM, also little-endian)
- A big-endian PowerPC router
- An embedded MIPS controller
If your program reads or writes binary data, it must handle endianness, or the same file may be misread on another architecture.
Step 3. Understanding Endianness
Endianness defines how bytes of multibyte values are stored in memory.
| Type | Description | Memory (4-byte int = 0x12345678) |
|---|---|---|
| Little-endian | Least significant byte first | 78 56 34 12 |
| Big-endian | Most significant byte first | 12 34 56 78 |
Intel and ARM (in most modes) are little-endian. Many older CPUs (PowerPC, SPARC) are big-endian.
C does not define the byte order, it depends on the platform.
Step 4. Checking Endianness at Runtime
#include <stdio.h>
int main(void) {
unsigned int x = 0x12345678;
unsigned char *p = (unsigned char *)&x;
if (*p == 0x78)
printf("Little-endian\n");
else
printf("Big-endian\n");
}Explanation: The pointerp reads the lowest memory byte. If it contains the least significant byte (0x78), it’s little-endian.
Step 5. Converting Between Endiannesses
Use standard POSIX functions to handle conversions safely:
#include <arpa/inet.h> // or <winsock2.h> on Windows
#include <stdint.h>
#include <stdio.h>
int main(void) {
uint32_t x = 0x12345678;
uint32_t y = htonl(x); // Host to Network Long (big-endian)
printf("0x%x -> 0x%x\n", x, y);
}Functions:
htons– host to network short (16-bit)htonl– host to network long (32-bit)ntohs– network to host shortntohl– network to host long
Network byte order is always big-endian.
Step 6. Handling Portability in File Formats
If you serialize structs directly to disk:
fwrite(&header, sizeof(header), 1, file);you’re likely writing machine-dependent data:
- Endianness may differ.
- Padding and alignment may differ.
- Structure layout can vary by compiler.
Better approach: Write each field individually in a well-defined order:
uint32_t size_net = htonl(header.size);
fwrite(&size_net, sizeof(size_net), 1, file);Now, any machine can read your file by reversing the conversion (ntohl).
Step 7. Data Type Size Differences
Type sizes vary across systems:
| Type | Typical 32-bit | Typical 64-bit |
|---|---|---|
char | 1 byte | 1 byte |
short | 2 bytes | 2 bytes |
int | 4 bytes | 4 bytes |
long | 4 bytes | 8 bytes |
long long | 8 bytes | 8 bytes |
void* | 4 bytes | 8 bytes |
Use<stdint.h> types (int32_t,uint64_t, etc.) for predictable sizes.
Step 8. Alignment and Padding
The compiler may insert padding between structure fields for speed or alignment.
Example:
struct Example {
char a;
int b;
};On most systems:
sizeof(struct Example)= 8, not 5 (3 bytes of padding).
To make portable formats:
- Use
#pragma pack(1)(non-standard) or serialize field-by-field. - Never assume
sizeof(struct)is the same across systems.
Step 9. Compiler and OS Differences
Be careful with:
- Path separators (
/vs\\) - Newline conventions (
\nvs\r\n) #include <unistd.h>(POSIX only)system()commands (OS-specific)- Thread APIs (
pthreadvs Windows threads) - Socket APIs (
vs<winsock2.h>)
Use conditional compilation:
#ifdef _WIN32
#include <winsock2.h>
#else
#include <arpa/inet.h>
#endifStep 10. Tiny Code: Writing Portable Binary I/O
#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>
int main(void) {
FILE *f = fopen("num.bin", "wb");
uint32_t n = 0x12345678;
uint32_t net = htonl(n);
fwrite(&net, sizeof(net), 1, f);
fclose(f);
f = fopen("num.bin", "rb");
uint32_t read_net;
fread(&read_net, sizeof(read_net), 1, f);
fclose(f);
printf("Read back: 0x%x\n", ntohl(read_net));
}This program writes and reads a 32-bit integer in portable big-endian form, the same bytes on any machine.
Why It Matters
Portability ensures your software lives longer than your hardware. A portable program:
- Runs on different CPUs and OSes.
- Shares data safely across architectures.
- Builds trust in your code across teams and systems.
Portability is a kind of professionalism, future-proofing your code.
Try It Yourself
- Write a program that detects and prints system endianness.
- Serialize a struct to a binary file, then deserialize it on another system.
- Use
htonlandntohlto ensure data stays consistent. - Compile your code with both GCC and Clang.
- Test it on both 32-bit and 64-bit architectures.
Next, you’ll explore inline assembly and hardware access, the bridge between pure C and the underlying CPU instructions.
