Code that compiles and runs correctly on your machine fails on a colleague’s — different compiler, different OS, different architecture. Most of the time, the bug was always there; a different platform just exposed it. Here are the portability traps that trip up real-world C code.
Integer Size Assumptions
#include <stdio.h>
#include <stdint.h>
int main() {
/* These sizes are NOT guaranteed by the C standard */
printf("int: %zun", sizeof(int)); /* usually 4, was 2 on 16-bit */
printf("long: %zun", sizeof(long)); /* 4 on Windows 64-bit, 8 on Linux 64-bit */
printf("ptr: %zun", sizeof(void*)); /* 4 on 32-bit, 8 on 64-bit */
return 0;
}long is the most common trap: it is 4 bytes on 64-bit Windows (LLP64 model) and 8 bytes on 64-bit Linux/macOS (LP64 model). Code that stores a 64-bit value in a long silently truncates it on Windows.
Fix: use <stdint.h> for exact-size types:
uint8_t, int8_t /* 8-bit types */
uint16_t, int16_t /* 16-bit types */
uint32_t, int32_t /* 32-bit types */
uint64_t, int64_t /* 64-bit types */
size_t /* pointer-sized unsigned — for sizes and array indices */
ptrdiff_t /* pointer-sized signed — for pointer arithmetic */
intptr_t /* integer large enough to hold a pointer */char Signedness
#include <stdio.h>
int main() {
char c = 200; /* 200 > CHAR_MAX (127) — what happens? */
/* On platforms where char is signed (x86 Linux, Windows): -56 */
/* On platforms where char is unsigned (ARM, some embedded): 200 */
if (c > 127) {
printf("unsigned charn");
} else {
printf("signed char (c = %d)n", c); /* -56 */
}
return 0;
}The signedness of plain char is implementation-defined. On x86 Linux and Windows it is signed; on ARM it is often unsigned. Code that checks char >= 0 or compares characters to values above 127 behaves differently between platforms.
Fix: be explicit about signedness for non-ASCII values:
unsigned char byte = 200; /* always unsigned, 0–255 */
signed char sc = -5; /* always signed */Endianness
#include <stdio.h>
#include <stdint.h>
#include <string.h>
int main() {
uint32_t value = 0x01020304;
uint8_t bytes[4];
memcpy(bytes, &value, 4);
printf("Byte order: %02X %02X %02X %02Xn",
bytes[0], bytes[1], bytes[2], bytes[3]);
/* Little-endian (x86): 04 03 02 01 */
/* Big-endian (SPARC, network): 01 02 03 04 */
return 0;
}x86/x64 is little-endian. Network protocols are big-endian (“network byte order”). ARM can be either. If you write a 32-bit integer to a file or send it over a socket without byte-order conversion, the receiver may misinterpret it.
#include <arpa/inet.h> /* POSIX */
uint32_t host_val = 42;
uint32_t net_val = htonl(host_val); /* host to network (big-endian) */
uint32_t back = ntohl(net_val); /* network to host */Path Separators
/* WRONG: backslash hardcoded */
const char *path = "data\config.txt"; /* works on Windows only */
/* BETTER: forward slash works on both Windows and POSIX */
const char *path = "data/config.txt"; /* Windows accepts / since Win32 */
/* BEST: use a macro */
#ifdef _WIN32
#define PATH_SEP "\"
#else
#define PATH_SEP "/"
#endif
const char *path = "data" PATH_SEP "config.txt";POSIX vs Windows API
#include <stdio.h>
/* POSIX-only: sleep(seconds), usleep(microseconds) */
#ifdef _WIN32
#include <windows.h>
#define sleep(s) Sleep((s) * 1000) /* Sleep takes milliseconds */
#else
#include <unistd.h>
#endif
int main() {
printf("Sleeping 1 secondn");
sleep(1);
return 0;
}Compiler Extensions — Avoid or Guard
/* GCC extension: __attribute__ */
void __attribute__((noreturn)) fatal(const char *msg) { ... }
/* Portable equivalent using C11 standard */
_Noreturn void fatal(const char *msg) { ... }
/* GCC and MSVC both support these C11 keywords */
/* __attribute__ is GCC/Clang only *//* Checking for compiler support */
#if defined(__GNUC__) || defined(__clang__)
#define PACKED __attribute__((packed))
#elif defined(_MSC_VER)
#define PACKED __pragma(pack(1))
#else
#define PACKED
#endifVariable-Length Arrays (VLAs) — C99, Optional in C11
void process(int n) {
int arr[n]; /* VLA — not supported in MSVC, optional in C11 */
/* ... */
}
/* Portable alternative */
void process_portable(int n) {
int *arr = malloc(n * sizeof(int));
if (!arr) return;
/* ... */
free(arr);
}MSVC does not support C99 VLAs, and C11 made them optional. Code that uses VLAs will fail to compile on Windows with MSVC. Use malloc for portable heap allocation. Test cross-platform behavior by compiling with both our gcc compiler online and checking against MSVC behavior patterns in our GCC flags guide.
TL;DR
- Never assume
sizeof(long) == 8— it is 4 on 64-bit Windows; useint64_t - Plain
charmay be signed or unsigned — useunsigned charfor byte values >127 - Handle endianness explicitly for binary file formats and network protocols
- Use forward slashes in paths — they work on both Windows and POSIX
- Avoid VLAs — use
mallocfor portable stack-replacement allocations - Guard compiler extensions with
#ifdef __GNUC__and provide MSVC alternatives - Test on at least two compilers — bugs that GCC hides often surface on Clang or MSVC