C — Type System
C has a weak, static, nominal type system. Types are checked at compile time, there is no runtime type information, and the compiler permits many implicit conversions that can silently produce incorrect results. Understanding the type system is essential to writing correct, portable C code — and to avoiding the class of bugs that arise from implicit conversions and undefined behavior.
| 1 | #include <stdio.h> |
| 2 | #include <limits.h> |
| 3 | #include <float.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | /* Fundamental integer types — guaranteed minimum ranges */ |
| 7 | printf("char: %zu bytes (min %d, max %d)\n", |
| 8 | sizeof(char), CHAR_MIN, CHAR_MAX); |
| 9 | printf("short: %zu bytes (min %d, max %d)\n", |
| 10 | sizeof(short), SHRT_MIN, SHRT_MAX); |
| 11 | printf("int: %zu bytes (min %d, max %d)\n", |
| 12 | sizeof(int), INT_MIN, INT_MAX); |
| 13 | printf("long: %zu bytes (min %ld, max %ld)\n", |
| 14 | sizeof(long), LONG_MIN, LONG_MAX); |
| 15 | printf("long long: %zu bytes (min %lld, max %lld)\n", |
| 16 | sizeof(long long), LLONG_MIN, LLONG_MAX); |
| 17 | |
| 18 | /* Floating-point types */ |
| 19 | printf("float: %zu bytes (digits %d)\n", |
| 20 | sizeof(float), FLT_DIG); |
| 21 | printf("double: %zu bytes (digits %d)\n", |
| 22 | sizeof(double), DBL_DIG); |
| 23 | printf("long double: %zu bytes (digits %d)\n", |
| 24 | sizeof(long double), LDBL_DIG); |
| 25 | |
| 26 | return 0; |
| 27 | } |
note
int must hold at least −32767 to 32767, but on most modern platforms it is 32 bits. Always use <stdint.h> types when exact widths matter.The C standard defines five signed integer types, three floating-point types, and several qualifier-modified variants. Each has specific guarantees about range and precision.
| 1 | #include <stdio.h> |
| 2 | #include <stdint.h> |
| 3 | #include <inttypes.h> |
| 4 | #include <limits.h> |
| 5 | |
| 6 | int main(void) { |
| 7 | /* Exact-width integer types from stdint.h */ |
| 8 | int8_t a = -128; |
| 9 | int16_t b = -32768; |
| 10 | int32_t c = -2147483648; |
| 11 | int64_t d = -9223372036854775807LL - 1; |
| 12 | |
| 13 | uint8_t ua = 255; |
| 14 | uint16_t ub = 65535; |
| 15 | uint32_t uc = 4294967295U; |
| 16 | uint64_t ud = 18446744073709551615ULL; |
| 17 | |
| 18 | /* PRIx64 macro for portable printf of 64-bit values */ |
| 19 | printf("int64_t min: %" PRId64 "\n", d); |
| 20 | printf("uint64_t max: %" PRIu64 "\n", ud); |
| 21 | printf("hex: %" PRIx64 "\n", ud); |
| 22 | |
| 23 | /* Minimum-width types */ |
| 24 | int_least8_t la = -128; |
| 25 | int_least32_t lb = -2147483648; |
| 26 | int_fast64_t lc = 9223372036854775807LL; |
| 27 | |
| 28 | /* Pointer-width types */ |
| 29 | intptr_t ptr_val = (intptr_t)&a; |
| 30 | uintptr_t uptr_val = (uintptr_t)&a; |
| 31 | printf("pointer as hex: %#" PRIxPTR "\n", uptr_val); |
| 32 | |
| 33 | return 0; |
| 34 | } |
info
int32_t and uint64_t when you need guaranteed widths. Use int_fast64_t when you want the fastest type at least that wide. Use size_t for sizes and indices.C99 introduced _Bool, a native boolean type that stores 0 or 1. The <stdbool.h> header provides the bool, true, and false macros for readability.
| 1 | #include <stdio.h> |
| 2 | #include <stdbool.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | _Bool raw_bool = 1; /* Native C99 boolean */ |
| 6 | bool nice_bool = true; /* Macro from stdbool.h */ |
| 7 | |
| 8 | /* Any nonzero value converts to 1 (true) */ |
| 9 | _Bool b1 = 42; /* b1 == 1 */ |
| 10 | _Bool b2 = -1; /* b2 == 1 */ |
| 11 | _Bool b3 = 0.5; /* b3 == 1 */ |
| 12 | _Bool b4 = 0; /* b4 == 0 */ |
| 13 | |
| 14 | printf("42 -> _Bool: %d\n", b1); |
| 15 | printf("-1 -> _Bool: %d\n", b2); |
| 16 | printf("0.5 -> _Bool: %d\n", b3); |
| 17 | |
| 18 | /* _Bool does NOT flip like a logical NOT: |
| 19 | _Bool b5 = !0; // b5 == 1 |
| 20 | _Bool b6 = !42; // b6 == 0 (because !42 is 0) */ |
| 21 | |
| 22 | /* Use in conditions */ |
| 23 | if (nice_bool && b1) { |
| 24 | printf("Both are true\n"); |
| 25 | } |
| 26 | |
| 27 | return 0; |
| 28 | } |
C99 added native complex number support via _Complex. The <complex.h> header provides macros and functions for working with complex arithmetic.
| 1 | #include <stdio.h> |
| 2 | #include <complex.h> |
| 3 | #include <math.h> |
| 4 | |
| 5 | int main(void) { |
| 6 | /* Declare complex numbers */ |
| 7 | double complex z1 = 3.0 + 4.0 * I; |
| 8 | double complex z2 = 1.0 - 2.0 * I; |
| 9 | |
| 10 | /* Arithmetic */ |
| 11 | double complex sum = z1 + z2; |
| 12 | double complex product = z1 * z2; |
| 13 | |
| 14 | printf("z1 = %.1f + %.1fi\n", creal(z1), cimag(z1)); |
| 15 | printf("z2 = %.1f + %.1fi\n", creal(z2), cimag(z2)); |
| 16 | printf("sum = %.1f + %.1fi\n", creal(sum), cimag(sum)); |
| 17 | printf("product = %.1f + %.1fi\n", creal(product), cimag(product)); |
| 18 | |
| 19 | /* Magnitude and phase */ |
| 20 | double mag = cabs(z1); |
| 21 | double phase = carg(z1); |
| 22 | printf("|z1| = %.4f\n", mag); |
| 23 | printf("arg(z1) = %.4f rad\n", phase); |
| 24 | |
| 25 | /* Conjugate */ |
| 26 | double complex conj = conj(z1); |
| 27 | printf("conj(z1) = %.1f + %.1fi\n", creal(conj), cimag(conj)); |
| 28 | |
| 29 | /* Imaginary type (optional in C99) */ |
| 30 | double imaginary img = 3.0 * I; |
| 31 | printf("imaginary: %.1f + %.1fi\n", 0.0, cimag(img)); |
| 32 | |
| 33 | return 0; |
| 34 | } |
warning
_Complex. Check your compiler documentation.Unsigned integer arithmetic in C is defined to wrap around using modular arithmetic. There is no overflow — values wrap at UINT_MAX + 1. This is well-defined behavior, unlike signed overflow.
| 1 | #include <stdio.h> |
| 2 | #include <stdint.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | uint8_t u = 255; |
| 6 | printf("uint8_t max: %u\n", u); /* 255 */ |
| 7 | u += 1; |
| 8 | printf("after +1: %u\n", u); /* 0 — wraps around */ |
| 9 | |
| 10 | uint32_t big = 0; |
| 11 | big -= 1; |
| 12 | printf("uint32_t 0 - 1: %u\n", big); /* 4294967295 */ |
| 13 | |
| 14 | /* Modular division: no divide-by-zero undefined behavior |
| 15 | when divisor is 0 — result is undefined per standard, |
| 16 | but many implementations define it. Use explicit checks. */ |
| 17 | uint32_t x = 10; |
| 18 | uint32_t y = 0; |
| 19 | if (y != 0) { |
| 20 | printf("%u / %u = %u\n", x, y, x / y); |
| 21 | } else { |
| 22 | printf("Division by zero avoided\n"); |
| 23 | } |
| 24 | |
| 25 | /* Unsigned subtraction used as array index — dangerous */ |
| 26 | size_t len = 5; |
| 27 | size_t idx = 3; |
| 28 | printf("len - idx = %zu\n", len - idx); /* 2 */ |
| 29 | |
| 30 | idx = 10; |
| 31 | /* This wraps to a huge number! len - idx = SIZE_MAX - 4 */ |
| 32 | printf("len - idx (wrapped): %zu\n", len - idx); |
| 33 | |
| 34 | return 0; |
| 35 | } |
warning
Signed integer overflow is undefined behavior in C. The compiler may assume it never happens and optimize accordingly. This is one of the most common sources of subtle bugs in C programs.
| 1 | #include <stdio.h> |
| 2 | #include <limits.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | int x = INT_MAX; |
| 6 | |
| 7 | /* UNDEFINED BEHAVIOR — do NOT do this */ |
| 8 | /* int y = x + 1; */ |
| 9 | |
| 10 | /* Safe alternative: check before arithmetic */ |
| 11 | if (x < INT_MAX - 1) { |
| 12 | int y = x + 1; |
| 13 | printf("y = %d\n", y); |
| 14 | } else { |
| 15 | printf("Addition would overflow\n"); |
| 16 | } |
| 17 | |
| 18 | /* Compiler may optimize away entire branches based on UB. |
| 19 | Example: this loop may never terminate in optimized builds: */ |
| 20 | int counter = 0; |
| 21 | for (int i = 1; i > 0; i *= 2) { |
| 22 | counter++; |
| 23 | if (counter > 100) break; /* Safety valve */ |
| 24 | } |
| 25 | printf("Counter: %d\n", counter); |
| 26 | |
| 27 | /* Use saturating arithmetic when overflow is expected */ |
| 28 | int a = INT_MAX; |
| 29 | int b = 1; |
| 30 | int result = (a > INT_MAX - b) ? INT_MAX : a + b; |
| 31 | printf("Saturated: %d\n", result); |
| 32 | |
| 33 | return 0; |
| 34 | } |
best practice
if (a > MAX - b) before a + b.C performs two key implicit conversions: integer promotion (smaller types promoted to int in expressions) and usual arithmetic conversions (operands of different types reconciled to a common type).
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | /* Integer promotion: char/short promoted to int in expressions */ |
| 5 | char a = 100; |
| 6 | char b = 50; |
| 7 | int sum = a + b; /* a and b promoted to int before addition */ |
| 8 | printf("char + char = int: %d\n", sum); |
| 9 | |
| 10 | /* Without promotion, 100 + 50 = 150 fits in char, |
| 11 | but the result is always int */ |
| 12 | char c = a + b; /* Implicit narrowing — potential data loss */ |
| 13 | printf("char result: %d\n", c); |
| 14 | |
| 15 | /* Usual arithmetic conversions: when types differ */ |
| 16 | int i = 10; |
| 17 | double d = 3.14; |
| 18 | double result = i + d; /* i converted to double */ |
| 19 | printf("int + double = %.2f\n", result); |
| 20 | |
| 21 | /* Float vs int: int converted to float */ |
| 22 | float f = 2.0f; |
| 23 | int n = 3; |
| 24 | float fr = f + n; /* n converted to float */ |
| 25 | printf("float + int = %.2f\n", fr); |
| 26 | |
| 27 | /* long vs int: int converted to long */ |
| 28 | long l = 1000000L; |
| 29 | int m = 42; |
| 30 | long lr = l + m; /* m converted to long */ |
| 31 | printf("long + int = %ld\n", lr); |
| 32 | |
| 33 | /* Unsigned vs signed — dangerous! */ |
| 34 | unsigned int u = 3; |
| 35 | int s = -5; |
| 36 | /* s converted to unsigned — becomes huge positive number! */ |
| 37 | unsigned int dangerous = u + (unsigned int)s; |
| 38 | printf("3 + (-5) as unsigned = %u\n", dangerous); |
| 39 | |
| 40 | return 0; |
| 41 | } |
warning
Explicit casts tell the compiler to convert between types. They can silence warnings but do not make unsafe operations safe. Use them sparingly and only when the conversion is well-understood.
| 1 | #include <stdio.h> |
| 2 | #include <math.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | /* Truncation: double to int */ |
| 6 | double pi = 3.14159; |
| 7 | int truncated = (int)pi; |
| 8 | printf("(int)3.14159 = %d\n", truncated); /* 3 */ |
| 9 | |
| 10 | /* Rounding: use round() instead of casting */ |
| 11 | int rounded = (int)round(pi); |
| 12 | printf("rounded = %d\n", rounded); /* 3 */ |
| 13 | |
| 14 | /* Sign change */ |
| 15 | unsigned int big = 3000000000U; |
| 16 | int converted = (int)big; /* Implementation-defined behavior */ |
| 17 | printf("(int)3000000000 = %d\n", converted); |
| 18 | |
| 19 | /* Precision loss: double to float */ |
| 20 | double precise = 1.0000001; |
| 21 | float less_precise = (float)precise; |
| 22 | printf("double: %.10f\n", precise); |
| 23 | printf("float: %.10f\n", less_precise); |
| 24 | |
| 25 | /* Pointer casts: void* is implicitly convertible in C */ |
| 26 | int x = 42; |
| 27 | void *vp = &x; |
| 28 | int *ip = (int *)vp; /* Cast not strictly needed in C */ |
| 29 | int *ip2 = vp; /* Works in C (not C++) */ |
| 30 | printf("value: %d\n", *ip2); |
| 31 | |
| 32 | /* Casting between function pointers — undefined behavior */ |
| 33 | void (*fp)(int) = (void (*)(int))printf; |
| 34 | /* fp(42); — don't do this */ |
| 35 | |
| 36 | return 0; |
| 37 | } |
const declares a variable as read-only. The compiler enforces that the variable is not modified after initialization. However, const does not make a value truly constant — it can be circumvented via pointers.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | /* Basic const */ |
| 5 | const int x = 10; |
| 6 | /* x = 20; — compile error */ |
| 7 | |
| 8 | /* Const pointer: pointer to const data */ |
| 9 | const int *p1 = &x; |
| 10 | /* *p1 = 30; — can't modify data through p1 */ |
| 11 | p1 = NULL; /* OK — can change the pointer itself */ |
| 12 | |
| 13 | /* Const data pointer: const pointer to mutable data */ |
| 14 | int y = 20; |
| 15 | int *const p2 = &y; |
| 16 | *p2 = 30; /* OK — can modify data through p2 */ |
| 17 | /* p2 = NULL; — can't change the pointer */ |
| 18 | |
| 19 | /* Both const pointer and const data */ |
| 20 | const int *const p3 = &x; |
| 21 | /* *p3 = 40; — can't modify data */ |
| 22 | /* p3 = NULL; — can't change pointer */ |
| 23 | |
| 24 | /* Array of const pointers */ |
| 25 | const int *arr[3] = {&x, &y, NULL}; |
| 26 | |
| 27 | /* Casting away const — undefined behavior if you modify */ |
| 28 | const int z = 42; |
| 29 | int *mutable = (int *)&z; |
| 30 | /* *mutable = 99; — UNDEFINED BEHAVIOR */ |
| 31 | |
| 32 | /* Const in function parameters */ |
| 33 | printf("const param: %d\n", x); |
| 34 | |
| 35 | return 0; |
| 36 | } |
best practice
const whenever a variable should not be modified. It communicates intent, enables compiler optimizations, and prevents accidental modification.volatile tells the compiler that a variable may change at any time outside the program's control. Every read and write must occur exactly as written — no caching in registers, no reordering, no elision.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <stdatomic.h> |
| 4 | |
| 5 | /* Hardware register example (memory-mapped I/O) */ |
| 6 | #define STATUS_REG (*(volatile unsigned int *)0x40021000) |
| 7 | |
| 8 | void wait_for_ready(void) { |
| 9 | /* Compiler must re-read STATUS_REG each iteration */ |
| 10 | while ((STATUS_REG & 0x01) == 0) { |
| 11 | /* Empty loop — volatile prevents optimization */ ; |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | /* Signal handler — shared variable must be volatile */ |
| 16 | volatile sig_atomic_t g_signal_received = 0; |
| 17 | |
| 18 | void signal_handler(int sig) { |
| 19 | g_signal_received = sig; |
| 20 | } |
| 21 | |
| 22 | /* Counter that must not be optimized away */ |
| 23 | volatile int g_debug_counter = 0; |
| 24 | |
| 25 | int main(void) { |
| 26 | signal(SIGINT, signal_handler); |
| 27 | |
| 28 | /* Without volatile, compiler might optimize this to infinite loop */ |
| 29 | while (!g_signal_received) { |
| 30 | g_debug_counter++; |
| 31 | } |
| 32 | |
| 33 | printf("Signal %d received\n", g_signal_received); |
| 34 | printf("Debug counter: %d\n", g_debug_counter); |
| 35 | |
| 36 | return 0; |
| 37 | } |
warning
volatile for thread synchronization. Use _Atomic (C11) or mutexes instead. volatile does not provide atomicity or memory ordering guarantees between threads.restrict is a promise to the compiler that the pointer is the sole means of accessing the data it points to during its lifetime. This allows aggressive optimization — the compiler can assume no aliasing.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | |
| 5 | /* Without restrict: compiler must assume src and dst may overlap */ |
| 6 | void copy_slow(char *dst, const char *src, size_t n) { |
| 7 | for (size_t i = 0; i < n; i++) { |
| 8 | dst[i] = src[i]; /* Compiler reads each byte individually */ |
| 9 | } |
| 10 | } |
| 11 | |
| 12 | /* With restrict: compiler knows dst and src don't alias */ |
| 13 | void copy_fast(char *restrict dst, const char *restrict src, size_t n) { |
| 14 | /* Compiler can use memcpy-style optimizations */ |
| 15 | memcpy(dst, src, n); |
| 16 | } |
| 17 | |
| 18 | /* Real-world: vector addition */ |
| 19 | void vec_add(float *restrict result, |
| 20 | const float *restrict a, |
| 21 | const float *restrict b, |
| 22 | size_t n) { |
| 23 | for (size_t i = 0; i < n; i++) { |
| 24 | result[i] = a[i] + b[i]; |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | /* WRONG: using restrict when pointers alias */ |
| 29 | void bad_example(float *restrict x, float *restrict y) { |
| 30 | *x = 1.0f; |
| 31 | *y = 2.0f; |
| 32 | /* If x == y, result is undefined — restrict was violated */ |
| 33 | printf("%f\n", *x); /* Could print 1.0 or 2.0 */ |
| 34 | } |
| 35 | |
| 36 | int main(void) { |
| 37 | float a[4] = {1.0f, 2.0f, 3.0f, 4.0f}; |
| 38 | float b[4] = {5.0f, 6.0f, 7.0f, 8.0f}; |
| 39 | float r[4]; |
| 40 | |
| 41 | vec_add(r, a, b, 4); |
| 42 | for (int i = 0; i < 4; i++) { |
| 43 | printf("%.1f ", r[i]); |
| 44 | } |
| 45 | printf("\n"); |
| 46 | |
| 47 | return 0; |
| 48 | } |
C11 added _Alignas and _Alignof for explicit control over data alignment. This is critical for SIMD operations, cache-line alignment, and memory-mapped hardware access.
| 1 | #include <stdio.h> |
| 2 | #include <stdalign.h> |
| 3 | |
| 4 | /* Cache-line aligned structure (typically 64 bytes) */ |
| 5 | struct alignas(64) CacheLine { |
| 6 | int data[16]; |
| 7 | char padding[64 - sizeof(int) * 16]; |
| 8 | }; |
| 9 | |
| 10 | /* Aligned to 32-byte boundary for SIMD */ |
| 11 | alignas(32) float simd_array[8] = {1.0f, 2.0f, 3.0f, 4.0f, |
| 12 | 5.0f, 6.0f, 7.0f, 8.0f}; |
| 13 | |
| 14 | int main(void) { |
| 15 | printf("int alignment: %zu\n", _Alignof(int)); |
| 16 | printf("double alignment: %zu\n", _Alignof(double)); |
| 17 | printf("CacheLine size: %zu\n", sizeof(struct CacheLine)); |
| 18 | printf("CacheLine align: %zu\n", _Alignof(struct CacheLine)); |
| 19 | printf("simd_array addr: %p\n", (void *)simd_array); |
| 20 | printf("simd_array align: %zu\n", _Alignof(float[8])); |
| 21 | |
| 22 | /* Dynamically aligned allocation */ |
| 23 | void *p = NULL; |
| 24 | int err = aligned_alloc(64, 256, &p); |
| 25 | if (err == 0 && p) { |
| 26 | printf("Aligned block: %p\n", p); |
| 27 | free(p); |
| 28 | } |
| 29 | |
| 30 | return 0; |
| 31 | } |
C11 introduced _Atomic for thread-safe access to shared data without locks. The <stdatomic.h> header provides convenience macros and operations.
| 1 | #include <stdio.h> |
| 2 | #include <stdatomic.h> |
| 3 | |
| 4 | /* Atomic counter — safe to modify from multiple threads */ |
| 5 | static atomic_int g_counter = ATOMIC_VAR_INIT(0); |
| 6 | |
| 7 | /* Atomic flag — guaranteed lock-free */ |
| 8 | static atomic_flag g_flag = ATOMIC_FLAG_INIT; |
| 9 | |
| 10 | int main(void) { |
| 11 | /* Store and load */ |
| 12 | atomic_store(&g_counter, 42); |
| 13 | int val = atomic_load(&g_counter); |
| 14 | printf("Value: %d\n", val); |
| 15 | |
| 16 | /* Fetch-and-add */ |
| 17 | int old = atomic_fetch_add(&g_counter, 1); |
| 18 | printf("Old: %d, New: %d\n", old, atomic_load(&g_counter)); |
| 19 | |
| 20 | /* Compare-and-swap */ |
| 21 | int expected = 43; |
| 22 | int desired = 99; |
| 23 | bool success = atomic_compare_exchange_weak(&g_counter, |
| 24 | &expected, desired); |
| 25 | printf("CAS %s: expected=%d, current=%d\n", |
| 26 | success ? "succeeded" : "failed", |
| 27 | expected, atomic_load(&g_counter)); |
| 28 | |
| 29 | /* Atomic flag: test and set */ |
| 30 | if (!atomic_flag_test_and_set(&g_flag)) { |
| 31 | printf("Flag acquired\n"); |
| 32 | } else { |
| 33 | printf("Flag already set\n"); |
| 34 | } |
| 35 | |
| 36 | atomic_flag_clear(&g_flag); |
| 37 | |
| 38 | return 0; |
| 39 | } |
Type punning is accessing the same memory as a different type. Union-based punning is explicitly allowed in C. Pointer-based punning violates the strict aliasing rule and can cause undefined behavior.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | /* Union-based type punning (ALLOWED in C) */ |
| 5 | union FloatInt { |
| 6 | float f; |
| 7 | unsigned int u; |
| 8 | }; |
| 9 | |
| 10 | void print_float_bits(float f) { |
| 11 | union FloatInt fi = {.f = f}; |
| 12 | printf("float %f as hex: 0x%08X\n", f, fi.u); |
| 13 | |
| 14 | /* Extract sign, exponent, mantissa */ |
| 15 | unsigned int sign = (fi.u >> 31) & 1; |
| 16 | unsigned int exponent = (fi.u >> 23) & 0xFF; |
| 17 | unsigned int mantissa = fi.u & 0x7FFFFF; |
| 18 | printf(" sign=%u exponent=%u mantissa=0x%06X\n", |
| 19 | sign, exponent, mantissa); |
| 20 | } |
| 21 | |
| 22 | /* Pointer-based type punning — VIOLATES strict aliasing */ |
| 23 | void dangerous_punning(void) { |
| 24 | int i = 0x3F800000; /* IEEE 754 for 1.0f */ |
| 25 | /* float *fp = (float *)&i; — undefined behavior! */ |
| 26 | /* Use memcpy instead (compiler optimizes it away) */ |
| 27 | float f; |
| 28 | memcpy(&f, &i, sizeof(f)); |
| 29 | printf("memcpy pun: %f\n", f); |
| 30 | } |
| 31 | |
| 32 | /* Safe pattern: memcpy for type punning */ |
| 33 | float int_to_float(unsigned int bits) { |
| 34 | float f; |
| 35 | memcpy(&f, &bits, sizeof(f)); |
| 36 | return f; |
| 37 | } |
| 38 | |
| 39 | int main(void) { |
| 40 | print_float_bits(3.14f); |
| 41 | print_float_bits(-0.0f); |
| 42 | print_float_bits(1.0f); |
| 43 | |
| 44 | dangerous_punning(); |
| 45 | printf("safe pun: %f\n", int_to_float(0x3F800000)); |
| 46 | |
| 47 | return 0; |
| 48 | } |
The strict aliasing rule says that an object shall only be accessed by an lvalue of a compatible type, a qualified version thereof, or a character type. Violating this rule is undefined behavior and can cause miscompilation with optimizations enabled.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | /* VIOLATION: accessing int as float through pointer */ |
| 5 | int bad_example(int *ip) { |
| 6 | float *fp = (float *)ip; /* Strict aliasing violation! */ |
| 7 | return (int)(*fp); |
| 8 | } |
| 9 | |
| 10 | /* COMPLIANT: use union for type punning */ |
| 11 | union IntFloat { |
| 12 | int i; |
| 13 | float f; |
| 14 | }; |
| 15 | |
| 16 | int safe_example(int *ip) { |
| 17 | union IntFloat u; |
| 18 | u.i = *ip; |
| 19 | return (int)u.f; |
| 20 | } |
| 21 | |
| 22 | /* COMPLIANT: use memcpy for type punning */ |
| 23 | int memcpy_example(int *ip) { |
| 24 | float f; |
| 25 | memcpy(&f, ip, sizeof(f)); |
| 26 | return (int)f; |
| 27 | } |
| 28 | |
| 29 | /* Character type exception: char* can alias anything */ |
| 30 | void print_bytes(const void *ptr, size_t size) { |
| 31 | const unsigned char *bp = (const unsigned char *)ptr; |
| 32 | for (size_t i = 0; i < size; i++) { |
| 33 | printf("%02X ", bp[i]); |
| 34 | } |
| 35 | printf("\n"); |
| 36 | } |
| 37 | |
| 38 | int main(void) { |
| 39 | int i = 0x3F800000; |
| 40 | printf("union pun: %f\n", (float)i); |
| 41 | printf("memcpy pun: %f\n", |
| 42 | *(float *)(void *)&i); /* Still risky via pointer cast */ |
| 43 | |
| 44 | print_bytes(&i, sizeof(i)); |
| 45 | |
| 46 | return 0; |
| 47 | } |
info
-fno-strict-aliasing to disable strict aliasing optimizations if your code relies on type punning via pointers. Or better: use unions or memcpy._Generic selects an expression based on the type of a controlling expression — similar to pattern matching on types. It is evaluated at compile time with zero runtime cost.
| 1 | #include <stdio.h> |
| 2 | #include <math.h> |
| 3 | |
| 4 | /* Type-safe print macro using _Generic */ |
| 5 | #define print_val(x) _Generic((x), \ |
| 6 | int: printf("%d\n", x), \ |
| 7 | long: printf("%ld\n", x), \ |
| 8 | double: printf("%f\n", x), \ |
| 9 | float: printf("%f\n", (double)x), \ |
| 10 | char *: printf("%s\n", x), \ |
| 11 | default: printf("unknown type\n") \ |
| 12 | ) |
| 13 | |
| 14 | /* Type-safe absolute value */ |
| 15 | #define safe_abs(x) _Generic((x), \ |
| 16 | int: abs, \ |
| 17 | long: labs, \ |
| 18 | double: fabs, \ |
| 19 | float: fabsf \ |
| 20 | )(x) |
| 21 | |
| 22 | /* Type-safe minimum */ |
| 23 | #define xmin(a, b) _Generic((a), \ |
| 24 | int: ((a) < (b) ? (a) : (b)), \ |
| 25 | double: fmin((double)(a), (double)(b)), \ |
| 26 | float: fminf((float)(a), (float)(b)) \ |
| 27 | ) |
| 28 | |
| 29 | int main(void) { |
| 30 | print_val(42); |
| 31 | print_val(3.14); |
| 32 | print_val("hello"); |
| 33 | |
| 34 | printf("abs(-5) = %d\n", safe_abs(-5)); |
| 35 | printf("fabs(-3.14) = %f\n", safe_abs(-3.14)); |
| 36 | |
| 37 | int x = 10, y = 20; |
| 38 | printf("min(%d, %d) = %d\n", x, y, xmin(x, y)); |
| 39 | |
| 40 | return 0; |
| 41 | } |
Type-related bugs are among the most insidious in C. They often compile without warnings, produce correct results on one platform, and silently fail on another.
| Bug | Description | Fix |
|---|---|---|
| Signed overflow | Undefined behavior, compiler may optimize away | Check before arithmetic |
| Implicit sign conversion | Negative signed becomes huge unsigned | Match signedness of operands |
| Narrowing conversion | int to char truncates high bits | Check range before converting |
| Printf format mismatch | Wrong format specifier for type | Use PRI/SCN macros from inttypes.h |
| Strict aliasing violation | Accessing memory through wrong pointer type | Use union or memcpy |
| Array decay | sizeof(array) in function returns pointer size | Pass length separately |
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | #include <limits.h> |
| 4 | |
| 5 | /* Bug 1: size_t arithmetic underflow */ |
| 6 | int sum_array_bad(int *arr, size_t len) { |
| 7 | int total = 0; |
| 8 | for (size_t i = 0; i < len; i++) { |
| 9 | total += arr[i]; |
| 10 | } |
| 11 | return total; |
| 12 | } |
| 13 | |
| 14 | /* Bug 2: printf format mismatch */ |
| 15 | void bug_printf(void) { |
| 16 | long long big = 9000000000LL; |
| 17 | /* printf("%d", big); — wrong format, undefined behavior */ |
| 18 | printf("%lld\n", big); /* correct */ |
| 19 | } |
| 20 | |
| 21 | /* Bug 3: char sign is platform-dependent */ |
| 22 | void bug_char_sign(void) { |
| 23 | char c = 0xFF; |
| 24 | /* On platforms where char is unsigned: c == 255 */ |
| 25 | /* On platforms where char is signed: c == -1 */ |
| 26 | unsigned char uc = (unsigned char)c; |
| 27 | printf("char value: %d, unsigned: %u\n", c, uc); |
| 28 | } |
| 29 | |
| 30 | /* Bug 4: sizeof loses information in function parameters */ |
| 31 | void bug_sizeof_array(int arr[]) { |
| 32 | /* sizeof(arr) is sizeof(int*), NOT sizeof the array */ |
| 33 | printf("arr size in func: %zu\n", sizeof(arr)); |
| 34 | } |
| 35 | |
| 36 | int main(void) { |
| 37 | int data[] = {1, 2, 3, 4, 5}; |
| 38 | bug_sizeof_array(data); |
| 39 | printf("arr size in main: %zu\n", sizeof(data)); |
| 40 | return 0; |
| 41 | } |
best practice
-Wall -Wextra -Wconversion -Wsign-conversion to catch most type-related issues at compile time. Treat warnings as errors.