C — Modern C (C11/C17/C23)
C99 (ISO/IEC 9899:1999) was a major update to C that added inline functions, designated initializers, compound literals, variable-length arrays, and the _Bool type. Many C99 features are now ubiquitous.
| 1 | #include <stdio.h> |
| 2 | #include <stdbool.h> |
| 3 | |
| 4 | /* C99: inline functions */ |
| 5 | inline int square(int x) { |
| 6 | return x * x; |
| 7 | } |
| 8 | |
| 9 | /* C99: designated initializers */ |
| 10 | struct Config { |
| 11 | int width; |
| 12 | int height; |
| 13 | bool fullscreen; |
| 14 | const char *title; |
| 15 | }; |
| 16 | |
| 17 | /* C99: compound literals */ |
| 18 | void print_coords(void) { |
| 19 | /* Create temporary array on the stack */ |
| 20 | int *coords = (int[]){10, 20, 30}; |
| 21 | for (int i = 0; i < 3; i++) { |
| 22 | printf("coord[%d] = %d\n", i, coords[i]); |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | int main(void) { |
| 27 | /* C99: mixed declarations and code */ |
| 28 | printf("square(5) = %d\n", square(5)); |
| 29 | int x = 10; /* Declaration after code — C99 allows this */ |
| 30 | |
| 31 | /* C99: designated initializers — order doesn't matter */ |
| 32 | struct Config cfg = { |
| 33 | .title = "My App", |
| 34 | .width = 1920, |
| 35 | .height = 1080, |
| 36 | .fullscreen = true |
| 37 | }; |
| 38 | printf("%s: %dx%d %s\n", |
| 39 | cfg.title, cfg.width, cfg.height, |
| 40 | cfg.fullscreen ? "fullscreen" : "windowed"); |
| 41 | |
| 42 | /* C99: compound literal for struct */ |
| 43 | struct Config *temp = &(struct Config){ |
| 44 | .width = 800, .height = 600, .fullscreen = false, .title = "Temp" |
| 45 | }; |
| 46 | printf("temp: %dx%d\n", temp->width, temp->height); |
| 47 | |
| 48 | /* C99: variable-length arrays */ |
| 49 | int n = 5; |
| 50 | int vla[n]; |
| 51 | for (int i = 0; i < n; i++) { |
| 52 | vla[i] = i * 10; |
| 53 | } |
| 54 | |
| 55 | /* C99: __VA_ARGS__ in variadic macros */ |
| 56 | #define LOG(fmt, ...) printf("LOG: " fmt "\n", __VA_ARGS__) |
| 57 | LOG("value: %d, name: %s", 42, "test"); |
| 58 | |
| 59 | /* C99: __func__ predefined identifier */ |
| 60 | printf("Current function: %s\n", __func__); |
| 61 | |
| 62 | return 0; |
| 63 | } |
note
malloc for dynamic arrays.C99 added __VA_ARGS__ for variadic macros, the __func__ predefined identifier, and single-line // comments.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | /* Variadic macro with __VA_ARGS__ */ |
| 4 | #define debug_print(fmt, ...) \ |
| 5 | fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) |
| 6 | |
| 7 | /* __func__ — function name as string literal */ |
| 8 | void example_function(void) { |
| 9 | printf("Entering %s\n", __func__); |
| 10 | } |
| 11 | |
| 12 | /* Token pasting with ## */ |
| 13 | #define MAKE_PAIR(type, name) \ |
| 14 | type name##_first; \ |
| 15 | type name##_second |
| 16 | |
| 17 | /* Stringification */ |
| 18 | #define STRINGIFY(x) #x |
| 19 | #define TOSTRING(x) STRINGIFY(x) |
| 20 | |
| 21 | int main(void) { |
| 22 | debug_print("Starting program"); |
| 23 | debug_print("Value: %d", 42); |
| 24 | |
| 25 | example_function(); |
| 26 | |
| 27 | /* Token pasting */ |
| 28 | MAKE_PAIR(int, point); |
| 29 | point_first = 10; |
| 30 | point_second = 20; |
| 31 | printf("point: (%d, %d)\n", point_first, point_second); |
| 32 | |
| 33 | /* Stringification */ |
| 34 | printf("Compiled: %s %s\n", __DATE__, __TIME__); |
| 35 | printf("Line: %d\n", __LINE__); |
| 36 | |
| 37 | return 0; |
| 38 | } |
C11 added _Alignas for specifying alignment and _Alignof for querying it. The <stdalign.h> header provides nicer macros.
| 1 | #include <stdio.h> |
| 2 | #include <stdalign.h> |
| 3 | |
| 4 | /* Cache-line aligned structure */ |
| 5 | struct alignas(64) CacheLine { |
| 6 | volatile int data[16]; |
| 7 | }; |
| 8 | |
| 9 | /* SIMD-aligned array */ |
| 10 | alignas(32) float simd_data[8] = {0}; |
| 11 | |
| 12 | /* Aligned struct with specific alignment */ |
| 13 | struct alignas(16) AlignedData { |
| 14 | double x, y, z, w; |
| 15 | }; |
| 16 | |
| 17 | int main(void) { |
| 18 | printf("alignof(char): %zu\n", alignof(char)); |
| 19 | printf("alignof(int): %zu\n", alignof(int)); |
| 20 | printf("alignof(double): %zu\n", alignof(double)); |
| 21 | printf("alignof(ptr): %zu\n", alignof(void *)); |
| 22 | printf("alignof(CacheLine): %zu\n", alignof(struct CacheLine)); |
| 23 | printf("sizeof(CacheLine): %zu\n", sizeof(struct CacheLine)); |
| 24 | printf("alignof(AlignedData): %zu\n", alignof(struct AlignedData)); |
| 25 | |
| 26 | /* Dynamic aligned allocation (C11) */ |
| 27 | void *p = NULL; |
| 28 | size_t alignment = 64; |
| 29 | size_t size = 256; |
| 30 | int err = aligned_alloc(alignment, size, &p); |
| 31 | if (err == 0 && p) { |
| 32 | printf("Allocated %zu bytes at %p (aligned to %zu)\n", |
| 33 | size, p, alignment); |
| 34 | free(p); |
| 35 | } |
| 36 | |
| 37 | return 0; |
| 38 | } |
_Generic provides compile-time type dispatch. It evaluates to one of several expressions based on the type of a controlling expression — like a type-safe switch statement.
| 1 | #include <stdio.h> |
| 2 | #include <math.h> |
| 3 | #include <string.h> |
| 4 | |
| 5 | /* Type-safe print macro */ |
| 6 | #define print(x) _Generic((x), \ |
| 7 | int: printf("%d", x), \ |
| 8 | long: printf("%ld", x), \ |
| 9 | long long: printf("%lld", x), \ |
| 10 | unsigned: printf("%u", x), \ |
| 11 | double: printf("%f", x), \ |
| 12 | float: printf("%f", (double)x), \ |
| 13 | char *: printf("%s", x), \ |
| 14 | const char *: printf("%s", x), \ |
| 15 | default: printf("(unknown)") \ |
| 16 | ) |
| 17 | |
| 18 | /* Type-safe absolute value */ |
| 19 | #define ABS(x) _Generic((x), \ |
| 20 | int: abs(x), \ |
| 21 | long: labs(x), \ |
| 22 | long long: llabs(x), \ |
| 23 | double: fabs(x), \ |
| 24 | float: fabsf(x) \ |
| 25 | ) |
| 26 | |
| 27 | /* Type-safe comparison */ |
| 28 | #define MAX(a, b) _Generic((a), \ |
| 29 | int: ((a) > (b) ? (a) : (b)), \ |
| 30 | double: fmax((double)(a), (double)(b)), \ |
| 31 | float: fmaxf((float)(a), (float)(b)), \ |
| 32 | default: ((a) > (b) ? (a) : (b)) \ |
| 33 | ) |
| 34 | |
| 35 | int main(void) { |
| 36 | print(42); |
| 37 | printf("\n"); |
| 38 | print(3.14); |
| 39 | printf("\n"); |
| 40 | print("hello"); |
| 41 | printf("\n"); |
| 42 | |
| 43 | printf("ABS(-10) = %d\n", ABS(-10)); |
| 44 | printf("ABS(-3.14) = %f\n", ABS(-3.14)); |
| 45 | |
| 46 | printf("MAX(3, 7) = %d\n", MAX(3, 7)); |
| 47 | printf("MAX(3.14, 2.71) = %f\n", MAX(3.14, 2.71)); |
| 48 | |
| 49 | return 0; |
| 50 | } |
_Static_assert performs compile-time assertions with a descriptive error message. _Noreturn tells the compiler a function never returns (like exit()).
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <stdnoreturn.h> |
| 4 | #include <stdint.h> |
| 5 | |
| 6 | /* Compile-time assertions */ |
| 7 | _Static_assert(sizeof(int) >= 4, |
| 8 | "int must be at least 32 bits"); |
| 9 | _Static_assert(sizeof(void *) == 8 || sizeof(void *) == 4, |
| 10 | "pointer must be 32 or 64 bits"); |
| 11 | |
| 12 | /* _Noreturn: function never returns */ |
| 13 | _Noreturn void fatal_error(const char *msg) { |
| 14 | fprintf(stderr, "FATAL: %s\n", msg); |
| 15 | exit(EXIT_FAILURE); |
| 16 | /* No return statement needed */ |
| 17 | } |
| 18 | |
| 19 | /* _Noreturn with _Generic */ |
| 20 | _Noreturn void abort_with_type(int code) { |
| 21 | fprintf(stderr, "Aborting with code %d\n", code); |
| 22 | exit(code); |
| 23 | } |
| 24 | |
| 25 | /* Static assert in struct */ |
| 26 | struct Packet { |
| 27 | uint32_t header; |
| 28 | uint8_t payload[64]; |
| 29 | uint32_t checksum; |
| 30 | }; |
| 31 | _Static_assert(sizeof(struct Packet) == 72, |
| 32 | "Packet must be 72 bytes for wire format"); |
| 33 | |
| 34 | int main(void) { |
| 35 | printf("Packet size: %zu\n", sizeof(struct Packet)); |
| 36 | |
| 37 | /* Use _Noreturn function */ |
| 38 | if (sizeof(int) < 8) { |
| 39 | fatal_error("int too small"); |
| 40 | } |
| 41 | |
| 42 | printf("All checks passed\n"); |
| 43 | return 0; |
| 44 | } |
C11 allows unnamed (anonymous) struct and union members. This enables overlapping access to the same memory through different field names — useful for register access and protocol parsing.
| 1 | #include <stdio.h> |
| 2 | #include <stdint.h> |
| 3 | |
| 4 | /* Hardware register layout with overlapping fields */ |
| 5 | struct StatusReg { |
| 6 | uint32_t ready : 1; |
| 7 | uint32_t error : 1; |
| 8 | uint32_t mode : 3; |
| 9 | uint32_t reserved: 27; |
| 10 | }; |
| 11 | |
| 12 | /* Anonymous union for register access */ |
| 13 | struct Device { |
| 14 | uint32_t raw; |
| 15 | struct { |
| 16 | uint32_t tx_enable : 1; |
| 17 | uint32_t rx_enable : 1; |
| 18 | uint32_t speed : 3; |
| 19 | uint32_t duplex : 1; |
| 20 | uint32_t reserved : 26; |
| 21 | }; /* Anonymous struct member */ |
| 22 | }; |
| 23 | |
| 24 | /* Network packet with anonymous union */ |
| 25 | struct PacketHeader { |
| 26 | uint16_t version; |
| 27 | uint16_t length; |
| 28 | union { |
| 29 | uint32_t raw_flags; |
| 30 | struct { |
| 31 | uint32_t syn : 1; |
| 32 | uint32_t ack : 1; |
| 33 | uint32_t fin : 1; |
| 34 | uint32_t rst : 1; |
| 35 | uint32_t padding : 28; |
| 36 | }; /* Anonymous struct */ |
| 37 | }; |
| 38 | }; |
| 39 | |
| 40 | int main(void) { |
| 41 | /* Access device through raw or structured fields */ |
| 42 | struct Device dev = {.raw = 0}; |
| 43 | dev.tx_enable = 1; |
| 44 | dev.speed = 3; |
| 45 | dev.duplex = 1; |
| 46 | printf("Device raw: 0x%08X\n", dev.raw); |
| 47 | |
| 48 | /* Access packet flags through raw or structured */ |
| 49 | struct PacketHeader pkt = { |
| 50 | .version = 1, |
| 51 | .length = 20, |
| 52 | .syn = 1 |
| 53 | }; |
| 54 | printf("Flags raw: 0x%08X\n", pkt.raw_flags); |
| 55 | printf("SYN=%d ACK=%d FIN=%d\n", pkt.syn, pkt.ack, pkt.fin); |
| 56 | |
| 57 | return 0; |
| 58 | } |
C11 provides standardized atomic operations via <stdatomic.h>. Atomics guarantee data-race-free concurrent access without locks, using memory ordering semantics.
| 1 | #include <stdio.h> |
| 2 | #include <stdatomic.h> |
| 3 | |
| 4 | static atomic_int g_counter = ATOMIC_VAR_INIT(0); |
| 5 | static atomic_flag g_lock = ATOMIC_FLAG_INIT; |
| 6 | |
| 7 | /* Spinlock using atomic_flag */ |
| 8 | void spinlock_lock(void) { |
| 9 | while (atomic_flag_test_and_set(&g_lock)) { |
| 10 | /* Busy wait — spin until lock is acquired */ |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | void spinlock_unlock(void) { |
| 15 | atomic_flag_clear(&g_lock); |
| 16 | } |
| 17 | |
| 18 | /* Atomic operations demo */ |
| 19 | void atomic_demo(void) { |
| 20 | atomic_int val = ATOMIC_VAR_INIT(0); |
| 21 | |
| 22 | /* Store with release semantics */ |
| 23 | atomic_store_explicit(&val, 42, memory_order_release); |
| 24 | |
| 25 | /* Load with acquire semantics */ |
| 26 | int loaded = atomic_load_explicit(&val, memory_order_acquire); |
| 27 | printf("Loaded: %d\n", loaded); |
| 28 | |
| 29 | /* Fetch-and-add */ |
| 30 | int old = atomic_fetch_add(&val, 8); |
| 31 | printf("Old: %d, New: %d\n", old, atomic_load(&val)); |
| 32 | |
| 33 | /* Compare-and-swap (weak — may spuriously fail) */ |
| 34 | int expected = 50; |
| 35 | int desired = 100; |
| 36 | bool ok = atomic_compare_exchange_weak(&val, &expected, desired); |
| 37 | printf("CAS: %s, expected=%d\n", ok ? "ok" : "fail", expected); |
| 38 | |
| 39 | /* Exchange */ |
| 40 | int prev = atomic_exchange(&val, 200); |
| 41 | printf("Exchanged: prev=%d, now=%d\n", prev, atomic_load(&val)); |
| 42 | } |
| 43 | |
| 44 | int main(void) { |
| 45 | atomic_demo(); |
| 46 | |
| 47 | /* Thread-safe counter increment */ |
| 48 | for (int i = 0; i < 1000; i++) { |
| 49 | atomic_fetch_add(&g_counter, 1); |
| 50 | } |
| 51 | printf("Counter: %d\n", atomic_load(&g_counter)); |
| 52 | |
| 53 | return 0; |
| 54 | } |
C11 standardized threading via <threads.h> — but it is optional and not widely implemented. Most production code uses pthreads (POSIX) or platform-specific APIs. Here is the standard API for reference.
| 1 | #include <stdio.h> |
| 2 | #include <threads.h> |
| 3 | |
| 4 | /* Thread function */ |
| 5 | int thread_func(void *arg) { |
| 6 | int id = *(int *)arg; |
| 7 | printf("Thread %d started\n", id); |
| 8 | |
| 9 | /* Simulate work */ |
| 10 | for (int i = 0; i < 3; i++) { |
| 11 | printf("Thread %d: step %d\n", id, i); |
| 12 | } |
| 13 | |
| 14 | printf("Thread %d finished\n", id); |
| 15 | return id * 10; /* Return value */ |
| 16 | } |
| 17 | |
| 18 | /* Mutex-protected shared data */ |
| 19 | mtx_t g_mutex; |
| 20 | int g_shared = 0; |
| 21 | |
| 22 | int mutex_thread(void *arg) { |
| 23 | int id = *(int *)arg; |
| 24 | for (int i = 0; i < 5; i++) { |
| 25 | mtx_lock(&g_mutex); |
| 26 | g_shared++; |
| 27 | printf("Thread %d: shared = %d\n", id, g_shared); |
| 28 | mtx_unlock(&g_mutex); |
| 29 | } |
| 30 | return 0; |
| 31 | } |
| 32 | |
| 33 | int main(void) { |
| 34 | /* Create and join threads */ |
| 35 | thrd_t threads[3]; |
| 36 | int ids[3] = {1, 2, 3}; |
| 37 | |
| 38 | for (int i = 0; i < 3; i++) { |
| 39 | thrd_create(&threads[i], thread_func, &ids[i]); |
| 40 | } |
| 41 | |
| 42 | for (int i = 0; i < 3; i++) { |
| 43 | int res; |
| 44 | thrd_join(threads[i], &res); |
| 45 | printf("Thread %d returned %d\n", i + 1, res); |
| 46 | } |
| 47 | |
| 48 | /* Mutex example */ |
| 49 | mtx_init(&g_mutex, mtx_plain); |
| 50 | thrd_t t[2]; |
| 51 | int tids[2] = {10, 20}; |
| 52 | for (int i = 0; i < 2; i++) { |
| 53 | thrd_create(&t[i], mutex_thread, &tids[i]); |
| 54 | } |
| 55 | for (int i = 0; i < 2; i++) { |
| 56 | thrd_join(t[i], NULL); |
| 57 | } |
| 58 | mtx_destroy(&g_mutex); |
| 59 | printf("Final shared: %d\n", g_shared); |
| 60 | |
| 61 | return 0; |
| 62 | } |
note
-pthread flag. The C11 threads API is optional — check if your platform supports it with #ifdef __STDC_VERSION__ and __STDC_VERSION__ >= 201112L.C17 (ISO/IEC 9899:2018) is essentially C11 with defect reports resolved. It removed the optional Annex K (bounds-checking functions) from the mandatory spec. The key change was clarifying C11 ambiguities.
| 1 | #include <stdio.h> |
| 2 | #include <stdalign.h> |
| 3 | #include <stdatomic.h> |
| 4 | #include <stdbool.h> |
| 5 | #include <stdnoreturn.h> |
| 6 | |
| 7 | /* |
| 8 | * C17 differences from C11: |
| 9 | * - Defect reports resolved (DR 4xx, DR 5xx) |
| 10 | * - Annex K (gets_s, strcpy_s, etc.) removed from mandatory spec |
| 11 | * - Clarifications on _Generic, _Alignas, and _Atomic |
| 12 | * - No new keywords or features |
| 13 | * |
| 14 | * Compile with: gcc -std=c17 -Wall -Wextra |
| 15 | * Or use: gcc -std=c18 (same standard, different name) |
| 16 | */ |
| 17 | |
| 18 | /* All C11 features still work in C17 */ |
| 19 | struct alignas(16) Data { |
| 20 | atomic_int counter; |
| 21 | _Atomic int atomic_counter; |
| 22 | }; |
| 23 | |
| 24 | _Noreturn void shutdown(void) { |
| 25 | printf("Shutting down...\n"); |
| 26 | _Exit(0); |
| 27 | } |
| 28 | |
| 29 | _Static_assert(sizeof(int) >= 4, "int too small"); |
| 30 | |
| 31 | int main(void) { |
| 32 | printf("C version: %ld\n", __STDC_VERSION__); |
| 33 | |
| 34 | /* C17/201710L, C11/201112L, C99/199901L */ |
| 35 | #if __STDC_VERSION__ >= 201710L |
| 36 | printf("Compiled as C17 or later\n"); |
| 37 | #elif __STDC_VERSION__ >= 201112L |
| 38 | printf("Compiled as C11\n"); |
| 39 | #elif __STDC_VERSION__ >= 199901L |
| 40 | printf("Compiled as C99\n"); |
| 41 | #else |
| 42 | printf("Compiled as C89/C90\n"); |
| 43 | #endif |
| 44 | |
| 45 | return 0; |
| 46 | } |
info
-std=c17 for the latest stable standard until C23 support matures.C23 adds nullptr and nullptr_t — a type-safe null pointer constant that replaces NULL. Unlike NULL, nullptr cannot be confused with integer zero.
| 1 | #include <stdio.h> |
| 2 | #include <stddef.h> |
| 3 | |
| 4 | void func_int(int x) { |
| 5 | printf("func_int: %d\n", x); |
| 6 | } |
| 7 | |
| 8 | void func_ptr(int *p) { |
| 9 | if (p == nullptr) { /* C23: type-safe null check */ |
| 10 | printf("null pointer\n"); |
| 11 | } else { |
| 12 | printf("value: %d\n", *p); |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | /* C23: nullptr_t type */ |
| 17 | void generic_null(nullptr_t n) { |
| 18 | printf("nullptr_t value: %d\n", n == nullptr); |
| 19 | } |
| 20 | |
| 21 | int main(void) { |
| 22 | func_ptr(nullptr); /* C23: clear intent */ |
| 23 | generic_null(nullptr); |
| 24 | |
| 25 | /* C23: nullptr works in comparisons */ |
| 26 | int *p = nullptr; |
| 27 | if (p == nullptr) { |
| 28 | printf("p is null\n"); |
| 29 | } |
| 30 | |
| 31 | /* C23: nullptr_t converts to any pointer type */ |
| 32 | char *cp = nullptr; |
| 33 | double *dp = nullptr; |
| 34 | printf("cp == nullptr: %d\n", cp == nullptr); |
| 35 | printf("dp == nullptr: %d\n", dp == nullptr); |
| 36 | |
| 37 | return 0; |
| 38 | } |
C23 adds typeof and typeof_unqual for querying types at compile time, plus constexpr for true compile-time constants.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | /* C23: typeof for variable declarations */ |
| 4 | #define SWAP(a, b) do { \ |
| 5 | typeof(a) temp = a; \ |
| 6 | a = b; \ |
| 7 | b = temp; \ |
| 8 | } while(0) |
| 9 | |
| 10 | /* C23: constexpr for compile-time constants */ |
| 11 | constexpr int ARRAY_SIZE = 10; |
| 12 | constexpr double PI = 3.14159265358979; |
| 13 | |
| 14 | /* C23: typeof in macros */ |
| 15 | #define PRINT_TYPE(x) \ |
| 16 | do { \ |
| 17 | typeof(x) val = x; \ |
| 18 | printf("value: %g, size: %zu bytes\n", \ |
| 19 | (double)val, sizeof(val)); \ |
| 20 | } while(0) |
| 21 | |
| 22 | int main(void) { |
| 23 | int a = 10, b = 20; |
| 24 | SWAP(a, b); |
| 25 | printf("a=%d, b=%d\n", a, b); |
| 26 | |
| 27 | double x = 3.14, y = 2.71; |
| 28 | SWAP(x, y); |
| 29 | printf("x=%f, y=%f\n", x, y); |
| 30 | |
| 31 | /* C23: typeof_unqual strips qualifiers */ |
| 32 | const int ci = 42; |
| 33 | typeof_unqual(ci) mutable = ci; /* mutable is just int */ |
| 34 | mutable = 100; |
| 35 | printf("mutable: %d\n", mutable); |
| 36 | |
| 37 | /* Compile-time sized array */ |
| 38 | int arr[ARRAY_SIZE]; |
| 39 | for (int i = 0; i < ARRAY_SIZE; i++) { |
| 40 | arr[i] = i * i; |
| 41 | } |
| 42 | |
| 43 | PRINT_TYPE(42); |
| 44 | PRINT_TYPE(3.14); |
| 45 | PRINT_TYPE(100LL); |
| 46 | |
| 47 | return 0; |
| 48 | } |
C23 adds standard attributes from C++11: [[nodiscard]], [[maybe_unused]], [[deprecated]], and new keywords like unreachable(), auto (type deduction), and improved enum support.
| 1 | #include <stdio.h> |
| 2 | #include <stddef.h> |
| 3 | |
| 4 | /* C23: [[nodiscard]] — compiler warns if return value ignored */ |
| 5 | [[nodiscard]] int critical_operation(void) { |
| 6 | return 42; |
| 7 | } |
| 8 | |
| 9 | /* C23: [[maybe_unused]] — suppress unused warnings */ |
| 10 | [[maybe_unused]] void helper(void) { |
| 11 | int x = 5; |
| 12 | (void)x; |
| 13 | } |
| 14 | |
| 15 | /* C23: [[deprecated]] — compiler warns on use */ |
| 16 | [[deprecated("Use new_func instead")]] |
| 17 | void old_func(void) { |
| 18 | printf("old_func\n"); |
| 19 | } |
| 20 | |
| 21 | void new_func(void) { |
| 22 | printf("new_func\n"); |
| 23 | } |
| 24 | |
| 25 | /* C23: auto for type deduction */ |
| 26 | auto get_value(void) { |
| 27 | return 42; /* auto deduces int */ |
| 28 | } |
| 29 | |
| 30 | /* C23: nullptr */ |
| 31 | [[nodiscard]] int *safe_get(void) { |
| 32 | return nullptr; |
| 33 | } |
| 34 | |
| 35 | /* C23: unreachable() — mark code as dead */ |
| 36 | void process(int flag) { |
| 37 | if (flag == 1) { |
| 38 | printf("case 1\n"); |
| 39 | } else if (flag == 2) { |
| 40 | printf("case 2\n"); |
| 41 | } else { |
| 42 | unreachable(); /* Compiler knows this is dead code */ |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | int main(void) { |
| 47 | /* [[nodiscard]] */ |
| 48 | int result = critical_operation(); |
| 49 | printf("Result: %d\n", result); |
| 50 | |
| 51 | /* [[deprecated]] */ |
| 52 | /* old_func(); — compiler warning */ |
| 53 | new_func(); |
| 54 | |
| 55 | /* auto deduction */ |
| 56 | auto val = get_value(); |
| 57 | printf("val: %d\n", val); |
| 58 | |
| 59 | /* unreachable */ |
| 60 | process(1); |
| 61 | |
| 62 | return 0; |
| 63 | } |
C23 adds #embed to include binary data directly in source code, #warning for compile-time warnings, #assert for preprocessor assertions, and #elifdef/#elifndef for cleaner conditional compilation.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | /* C23: #embed for binary data */ |
| 4 | /* |
| 5 | * Suppose you have a file "icon.bin": |
| 6 | * #embed "icon.bin" |
| 7 | * expands to comma-separated integer values. |
| 8 | * |
| 9 | * Usage: |
| 10 | * const unsigned char icon[] = { |
| 11 | * #embed "icon.bin" |
| 12 | * }; |
| 13 | */ |
| 14 | |
| 15 | /* C23: #warning */ |
| 16 | #if __STDC_VERSION__ < 202311L |
| 17 | #warning "Compile with -std=c23 for full C23 support" |
| 18 | #endif |
| 19 | |
| 20 | /* C23: #assert */ |
| 21 | #define MAX_BUFFER 1024 |
| 22 | #if MAX_BUFFER < 256 |
| 23 | #error "MAX_BUFFER must be at least 256" |
| 24 | #endif |
| 25 | |
| 26 | /* C23: #elifdef / #elifndef */ |
| 27 | #define FEATURE_A |
| 28 | /* #define FEATURE_B */ |
| 29 | |
| 30 | #if defined(FEATURE_A) |
| 31 | printf("Feature A enabled\n"); |
| 32 | #elifdef FEATURE_B |
| 33 | printf("Feature B enabled\n"); |
| 34 | #else |
| 35 | printf("No features enabled\n"); |
| 36 | #endif |
| 37 | |
| 38 | /* C23: digit separators */ |
| 39 | int million = 1'000'000; |
| 40 | long hex_val = 0xFF'FF'FF'FF; |
| 41 | int binary = 0b1010'0101'1111'0000; |
| 42 | |
| 43 | int main(void) { |
| 44 | printf("million: %d\n", million); |
| 45 | printf("hex: 0x%lX\n", hex_val); |
| 46 | printf("binary: %d\n", binary); |
| 47 | |
| 48 | return 0; |
| 49 | } |
C23 introduces _BitInt(N) for integers of any width from 1 to the implementation maximum. This is useful for bit-precise protocols, hardware registers, and cryptographic operations.
| 1 | #include <stdio.h> |
| 2 | #include <stdint.h> |
| 3 | |
| 4 | /* C23: _BitInt for precise widths */ |
| 5 | _BitInt(12) twelve_bit = 4095; /* 12-bit integer */ |
| 6 | _BitInt(24) twenty_four = 0xFFFFFF; /* 24-bit integer */ |
| 7 | _BitInt(64) sixty_four = 9223372036854775807LL; |
| 8 | |
| 9 | /* Unsigned variant */ |
| 10 | unsigned _BitInt(8) u8 = 255; |
| 11 | unsigned _BitInt(16) u16 = 65535; |
| 12 | unsigned _BitInt(32) u32 = 4294967295U; |
| 13 | |
| 14 | /* Useful for bit-field-like structures without the overhead */ |
| 15 | struct NetworkPacket { |
| 16 | unsigned _BitInt(4) version; |
| 17 | unsigned _BitInt(12) length; |
| 18 | unsigned _BitInt(16) flags; |
| 19 | unsigned _BitInt(32) sequence; |
| 20 | }; |
| 21 | |
| 22 | int main(void) { |
| 23 | printf("_BitInt(12) max: %d\n", twelve_bit); |
| 24 | printf("_BitInt(24) max: %d\n", twenty_four); |
| 25 | |
| 26 | struct NetworkPacket pkt = { |
| 27 | .version = 4, |
| 28 | .length = 1500, |
| 29 | .flags = 0x01, |
| 30 | .sequence = 12345 |
| 31 | }; |
| 32 | printf("Packet: v=%d len=%d flags=0x%X seq=%u\n", |
| 33 | (int)pkt.version, (int)pkt.length, |
| 34 | (unsigned)pkt.flags, (unsigned)pkt.sequence); |
| 35 | |
| 36 | /* Arithmetic works normally */ |
| 37 | _BitInt(8) a = 200, b = 100; |
| 38 | _BitInt(8) sum = a + b; /* Wraps at 256 */ |
| 39 | printf("200 + 100 = %d (mod 256)\n", (int)sum); |
| 40 | |
| 41 | return 0; |
| 42 | } |
warning
_BitInt support varies by compiler. GCC and Clang support it; MSVC support is evolving. Check __STDC_VERSION__ >= 202311L and compiler-specific macros.C23 allows specifying the underlying type of enums (like C++ enum class), enables forward declarations, and makes enums work with typeof.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | /* C23: enum with explicit underlying type */ |
| 4 | enum Color : unsigned char { |
| 5 | RED = 0, |
| 6 | GREEN = 1, |
| 7 | BLUE = 2 |
| 8 | }; |
| 9 | |
| 10 | /* C23: forward declaration of enums */ |
| 11 | enum Status : int; /* Forward declaration */ |
| 12 | enum Status { |
| 13 | OK = 0, |
| 14 | ERROR = -1, |
| 15 | PENDING = 1 |
| 16 | }; |
| 17 | |
| 18 | /* C23: typeof works with enum values */ |
| 19 | typeof(RED) current_color = GREEN; |
| 20 | |
| 21 | int main(void) { |
| 22 | enum Color c = BLUE; |
| 23 | printf("Color value: %d\n", c); |
| 24 | printf("Color size: %zu bytes\n", sizeof(enum Color)); |
| 25 | |
| 26 | enum Status s = OK; |
| 27 | printf("Status value: %d\n", s); |
| 28 | |
| 29 | current_color = RED; |
| 30 | printf("current_color: %d\n", current_color); |
| 31 | |
| 32 | /* Enum in switch */ |
| 33 | switch (c) { |
| 34 | case RED: printf("Red\n"); break; |
| 35 | case GREEN: printf("Green\n"); break; |
| 36 | case BLUE: printf("Blue\n"); break; |
| 37 | } |
| 38 | |
| 39 | return 0; |
| 40 | } |
Modern C features enable cleaner, safer patterns. Here are practical examples combining C11/C23 features for real-world code.
| 1 | #include <stdio.h> |
| 2 | #include <stdatomic.h> |
| 3 | #include <stdbool.h> |
| 4 | #include <threads.h> |
| 5 | |
| 6 | /* Pattern 1: Type-safe container using _Generic */ |
| 7 | #define container_of(ptr, type, member) \ |
| 8 | ((type *)((char *)(ptr) - offsetof(type, member))) |
| 9 | |
| 10 | #define print_container(ptr, type) _Generic((ptr), \ |
| 11 | type *: printf("Container value: %d\n", (ptr)->value) \ |
| 12 | ) |
| 13 | |
| 14 | /* Pattern 2: Designated initializer for config */ |
| 15 | struct ServerConfig { |
| 16 | const char *host; |
| 17 | int port; |
| 18 | bool use_tls; |
| 19 | int max_connections; |
| 20 | }; |
| 21 | |
| 22 | struct ServerConfig default_config(void) { |
| 23 | return (struct ServerConfig){ |
| 24 | .host = "localhost", |
| 25 | .port = 8080, |
| 26 | .use_tls = false, |
| 27 | .max_connections = 100 |
| 28 | }; |
| 29 | } |
| 30 | |
| 31 | /* Pattern 3: Atomic flag for once-init */ |
| 32 | static atomic_flag g_initialized = ATOMIC_FLAG_INIT; |
| 33 | |
| 34 | void init_once(void) { |
| 35 | if (atomic_flag_test_and_set(&g_initialized)) { |
| 36 | return; /* Already initialized */ |
| 37 | } |
| 38 | printf("Initializing...\n"); |
| 39 | } |
| 40 | |
| 41 | /* Pattern 4: Thread-safe ring buffer */ |
| 42 | #define RING_SIZE 256 |
| 43 | |
| 44 | struct RingBuffer { |
| 45 | atomic_int head; |
| 46 | atomic_int tail; |
| 47 | int data[RING_SIZE]; |
| 48 | }; |
| 49 | |
| 50 | bool ring_push(struct RingBuffer *rb, int val) { |
| 51 | int head = atomic_load(&rb->head); |
| 52 | int next = (head + 1) % RING_SIZE; |
| 53 | if (next == atomic_load(&rb->tail)) { |
| 54 | return false; /* Full */ |
| 55 | } |
| 56 | rb->data[head] = val; |
| 57 | atomic_store(&rb->head, next); |
| 58 | return true; |
| 59 | } |
| 60 | |
| 61 | bool ring_pop(struct RingBuffer *rb, int *val) { |
| 62 | int tail = atomic_load(&rb->tail); |
| 63 | if (tail == atomic_load(&rb->head)) { |
| 64 | return false; /* Empty */ |
| 65 | } |
| 66 | *val = rb->data[tail]; |
| 67 | atomic_store(&rb->tail, (tail + 1) % RING_SIZE); |
| 68 | return true; |
| 69 | } |
| 70 | |
| 71 | int main(void) { |
| 72 | /* Pattern 1: Designated initializer */ |
| 73 | struct ServerConfig cfg = default_config(); |
| 74 | printf("Server: %s:%d\n", cfg.host, cfg.port); |
| 75 | |
| 76 | /* Pattern 2: Compound literal override */ |
| 77 | struct ServerConfig custom = { |
| 78 | .host = "0.0.0.0", |
| 79 | .port = 443, |
| 80 | .use_tls = true |
| 81 | }; |
| 82 | printf("Custom: %s:%d TLS=%d\n", |
| 83 | custom.host, custom.port, custom.use_tls); |
| 84 | |
| 85 | /* Pattern 3: Once-init */ |
| 86 | init_once(); |
| 87 | init_once(); /* No-op */ |
| 88 | |
| 89 | /* Pattern 4: Ring buffer */ |
| 90 | struct RingBuffer rb = {0}; |
| 91 | ring_push(&rb, 42); |
| 92 | ring_push(&rb, 99); |
| 93 | int val; |
| 94 | ring_pop(&rb, &val); |
| 95 | printf("Popped: %d\n", val); |
| 96 | |
| 97 | return 0; |
| 98 | } |
Different compilers support different C standards. Always check __STDC_VERSION__ and compile with explicit standard flags.
| Compiler | C17 Flag | C23 Flag | Notes |
|---|---|---|---|
| GCC | -std=c17 | -std=c23 | Full C23 support since GCC 13 |
| Clang | -std=c17 | -std=c23 | C23 since Clang 17 |
| MSVC | /std:c17 | /std:c23 | C23 in VS 2022 17.8+ |
| TCC | -std=c11 | N/A | Limited C23 support |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | /* Detect C standard version */ |
| 4 | void print_c_version(void) { |
| 5 | #if __STDC_VERSION__ >= 202311L |
| 6 | printf("C23 (ISO/IEC 9899:2024)\n"); |
| 7 | #elif __STDC_VERSION__ >= 201710L |
| 8 | printf("C17 (ISO/IEC 9899:2018)\n"); |
| 9 | #elif __STDC_VERSION__ >= 201112L |
| 10 | printf("C11 (ISO/IEC 9899:2011)\n"); |
| 11 | #elif __STDC_VERSION__ >= 199901L |
| 12 | printf("C99 (ISO/IEC 9899:1999)\n"); |
| 13 | #else |
| 14 | printf("C89/C90\n"); |
| 15 | #endif |
| 16 | } |
| 17 | |
| 18 | /* Detect compiler extensions */ |
| 19 | void print_extensions(void) { |
| 20 | #ifdef __GNUC__ |
| 21 | printf("GCC/Clang extensions available\n"); |
| 22 | #endif |
| 23 | #ifdef _MSC_VER |
| 24 | printf("MSVC extensions available\n"); |
| 25 | #endif |
| 26 | #ifdef __STDC_VERSION__ |
| 27 | printf("__STDC_VERSION__ = %ld\n", __STDC_VERSION__); |
| 28 | #else |
| 29 | printf("__STDC_VERSION__ not defined\n"); |
| 30 | #endif |
| 31 | } |
| 32 | |
| 33 | int main(void) { |
| 34 | print_c_version(); |
| 35 | print_extensions(); |
| 36 | return 0; |
| 37 | } |
best practice
-Wall -Wextra -Wpedantic to catch non-standard usage. Use -std=c17 as your baseline standard for production code.Migrating from C89 to modern C improves safety, readability, and performance. Here is a practical migration checklist.
| C89 Pattern | Modern Replacement | Standard |
|---|---|---|
| typedef enum { YES=1, NO=0 } bool; | bool from <stdbool.h> | C99 |
| /* block comments only */ | // single-line comments | C99 |
| int arr[3] = {1, 2, 3}; | .field = value designators | C99 |
| malloc + memset init | Compound literals | C99 |
| volatile int flag; | atomic_int flag; | C11 |
| pthread_create() | thrd_create() (if available) | C11 |
| #define MIN(a,b) ... | #define MIN(a,b) _Generic(...) | C11 |
| NULL for pointers | nullptr | C23 |
| 1 | #include <stdio.h> |
| 2 | #include <stdbool.h> |
| 3 | #include <stdatomic.h> |
| 4 | |
| 5 | /* C89 style (BAD): */ |
| 6 | /* typedef struct { int x; int y; } Point; */ |
| 7 | /* void init_point(Point *p) { p->x = 0; p->y = 0; } */ |
| 8 | |
| 9 | /* Modern C style (GOOD): */ |
| 10 | typedef struct { |
| 11 | int x; |
| 12 | int y; |
| 13 | } Point; |
| 14 | |
| 15 | Point point_new(int x, int y) { |
| 16 | return (Point){.x = x, .y = y}; |
| 17 | } |
| 18 | |
| 19 | /* C89: volatile for flags (WRONG for threads) */ |
| 20 | /* volatile int counter = 0; */ |
| 21 | |
| 22 | /* Modern: atomic for flags (CORRECT) */ |
| 23 | atomic_int counter = ATOMIC_VAR_INIT(0); |
| 24 | |
| 25 | int main(void) { |
| 26 | /* Modern: compound literals and designated initializers */ |
| 27 | Point p1 = point_new(10, 20); |
| 28 | Point p2 = {.x = 30, .y = 40}; |
| 29 | |
| 30 | printf("p1: (%d, %d)\n", p1.x, p1.y); |
| 31 | printf("p2: (%d, %d)\n", p2.x, p2.y); |
| 32 | |
| 33 | /* Modern: atomic operations */ |
| 34 | atomic_fetch_add(&counter, 1); |
| 35 | printf("Counter: %d\n", atomic_load(&counter)); |
| 36 | |
| 37 | /* Modern: nullptr (C23) */ |
| 38 | Point *pp = nullptr; |
| 39 | if (pp == nullptr) { |
| 40 | printf("pp is null\n"); |
| 41 | } |
| 42 | |
| 43 | return 0; |
| 44 | } |