Skip to content

[中文版本](/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.

TypeDescriptionMemory (4-byte int = 0x12345678)
Little-endianLeast significant byte first78 56 34 12
Big-endianMost significant byte first12 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 short
  • ntohl– 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:

TypeTypical 32-bitTypical 64-bit
char1 byte1 byte
short2 bytes2 bytes
int4 bytes4 bytes
long4 bytes8 bytes
long long8 bytes8 bytes
void*4 bytes8 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 assumesizeof(struct) is the same across systems.

Step 9. Compiler and OS Differences ​

Be careful with:

  • Path separators (/ vs\\)
  • Newline conventions (\n vs\r\n)
  • #include <unistd.h>(POSIX only)
  • system() commands (OS-specific)
  • Thread APIs (pthread vs Windows threads)
  • Socket APIs ( vs<winsock2.h>)

Use conditional compilation:

#ifdef _WIN32
#include <winsock2.h>
#else
#include <arpa/inet.h>
#endif

Step 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 ​

  1. Write a program that detects and prints system endianness.
  2. Serialize a struct to a binary file, then deserialize it on another system.
  3. Usehtonl andntohl to ensure data stays consistent.
  4. Compile your code with both GCC and Clang.
  5. 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.

本站总访问 — 次