Menu
CoddyTech

Advanced C Interview Questions

Linkage, aliasing, generic code, variadic functions and the corners of the standard that cause real bugs.

11 questions
  1. What is linkage in C, and how do static and extern affect it?

    Linkage decides whether one name in different places means the same object or function. External linkage is shared across files (the default for functions and file-level variables), internal is one file (static), and locals have none.

    /* config.c */
    int max_users = 100;          /* external linkage, definition */
    static int retries = 3;       /* internal: invisible to other files */
    
    /* main.c */
    extern int max_users;         /* declaration, refers to config.c's object */
    

    Two files defining the same non-static global fail to link (GCC 10 and later default to -fno-common, so even uninitialized ones conflict).

  2. What is the strict aliasing rule?

    The compiler may assume pointers to different types do not alias: accessing an object through an lvalue of an incompatible type is undefined behavior, except through its signed or unsigned variant or a character type (char, signed char, unsigned char). The full list is in cppreference's strict aliasing section.

    float f = 1.0f;
    uint32_t bits = *(uint32_t *) &f;   /* undefined: float read through uint32_t */
    
    uint32_t ok;
    memcpy(&ok, &f, sizeof ok);         /* defined, and compiled to a single move */
    

    At -O2 GCC enables -fstrict-aliasing, so code that worked at -O0 can read stale values. Reinterpret bytes with memcpy or a union (allowed in C).

  3. How do you write type-generic code in C?

    With void * plus an element size and callbacks (as qsort does), with macros, or with C11's _Generic, which picks an expression at compile time from its argument's type.

    C
    #include <stdio.h>
    
    #define type_name(x) _Generic((x), \
        int: "int", \
        double: "double", \
        char *: "char *", \
        default: "other")
    
    static void print_int(int v) { printf("%d\n", v); }
    static void print_double(double v) { printf("%.2f\n", v); }
    static void print_str(const char *v) { printf("%s\n", v); }
    
    #define print_val(x) _Generic((x), \
        int: print_int, \
        double: print_double, \
        char *: print_str)(x)
    
    int main(void) {
        char name[] = "coddy";
        printf("%s %s %s\n", type_name(42), type_name(2.5), type_name(name));
        print_val(42);
        print_val(2.5);
        print_val(name);
        return 0;
    }

    It prints int double char *, then 42, 2.50 and coddy. print_val selects a function and then calls it, because every _Generic branch must compile for any argument.

  4. How do variadic functions like printf work, and how do you write one?

    Declare at least one named parameter (C23 also allows f(...)), then ..., and read the rest with va_start, va_arg and va_end. The function cannot know the count or types, so a count or format string must tell it.

    C
    #include <stdio.h>
    #include <stdarg.h>
    
    int sum(int count, ...) {
        va_list ap;
        va_start(ap, count);
        int total = 0;
        for (int i = 0; i < count; i++)
            total += va_arg(ap, int);
        va_end(ap);
        return total;
    }
    
    int main(void) {
        printf("%d\n", sum(3, 10, 20, 30));
        printf("%d\n", sum(5, 1, 2, 3, 4, 5));
        return 0;
    }

    It prints 60 and 15. Arguments are promoted, so va_arg(ap, char) or float is wrong (use int and double), and a wrong type is undefined behavior.

  5. In f() + g(), which function is called first?

    It is unspecified: either may run first, and the compiler need not document it or be consistent. Function arguments are the same. Only &&, ||, ?: and the comma operator fix the order. If f and g modify the same global it stays unspecified, since calls never interleave, but i++ + i++ is undefined behavior. When order matters, use separate statements.

  6. How do you detect integer overflow in C?

    Check before the operation: signed overflow is undefined behavior, so a check after it such as if (a + b < a) may be optimized away. Compare against <limits.h> limits, or use the GCC and Clang builtins.

    C
    #include <stdio.h>
    #include <limits.h>
    #include <stdbool.h>
    
    bool safe_add(int a, int b, int *out) {
        if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b))
            return false;
        *out = a + b;
        return true;
    }
    
    int main(void) {
        int r;
        printf("%s\n", safe_add(INT_MAX, 1, &r) ? "ok" : "overflow");
        printf("%s\n", safe_add(100, 23, &r) ? "ok" : "overflow");
        printf("builtin: %s\n", __builtin_add_overflow(INT_MAX, 1, &r) ? "overflow" : "ok");
        return 0;
    }

    It prints overflow, ok and builtin: overflow. C23 standardizes this as ckd_add in <stdckdint.h>.

    Practice it: Reverse the Digits, Factorial

  7. How does inline work in C, and how is it different from C++?

    In C99 and later, inline without extern in a header is only an inline definition: if the compiler emits a call, one .c file must provide the external definition with extern inline, or -O0 builds fail with "undefined reference". So headers use static inline.

    /* vec.h */
    static inline int clamp(int x, int lo, int hi) {
        return x < lo ? lo : x > hi ? hi : x;
    }
    

    C++ merges duplicate inline definitions at link time, so plain inline works there.

  8. What are setjmp and longjmp, and when would you use them?

    setjmp(env) saves the execution context and returns 0; a later longjmp(env, val) jumps back, and setjmp returns val. It is a non-local goto, the closest C has to exceptions.

    C
    #include <stdio.h>
    #include <setjmp.h>
    
    static jmp_buf on_error;
    static int error_code;
    
    void parse(int depth) {
        if (depth == 3) {
            error_code = 42;
            longjmp(on_error, 1);      /* abandon the whole call chain */
        }
        parse(depth + 1);
    }
    
    int main(void) {
        if (setjmp(on_error) == 0) {
            parse(0);
            printf("not reached\n");
        } else {
            printf("recovered with code %d\n", error_code);
        }
        return 0;
    }

    It prints recovered with code 42. setjmp is allowed only in a condition like this or as a statement, so int code = setjmp(env); is undefined behavior (see setjmp on cppreference). Nothing in between is cleaned up, and locals modified after setjmp must be volatile to have a defined value.

  9. Should you use #define or const for constants in C?

    Use enum for integer constants and const for other typed values. In C a const int is not a constant expression, so case n: and a file-scope static int arr[n]; are not valid C; that is where enum or #define is needed. Inside a function, int arr[n]; compiles, but as a variable length array. C23 adds constexpr for objects.

  10. How would you implement a growable array (like a C++ vector) in C?

    Keep a pointer, a length and a capacity, and when full realloc to double the capacity, so appends are amortized O(1); a fixed increment makes n appends O(n²).

    C
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
        int *data;
        size_t len, cap;
    } IntVec;
    
    int vec_push(IntVec *v, int x) {
        if (v->len == v->cap) {
            size_t cap = v->cap ? v->cap * 2 : 4;
            int *p = realloc(v->data, cap * sizeof *p);
            if (!p) return -1;            /* v->data is still valid */
            v->data = p;
            v->cap = cap;
        }
        v->data[v->len++] = x;
        return 0;
    }
    
    int main(void) {
        IntVec v = {0};
        for (int i = 1; i <= 10; i++) {
            vec_push(&v, i * i);
            printf("len %zu cap %zu\n", v.len, v.cap);
        }
        printf("last = %d\n", v.data[v.len - 1]);
        free(v.data);
        return 0;
    }

    The capacity goes 4, 8, 16 and the last element is 100. Pointers into data go stale after a growth.

  11. Write a binary search in C and point out the classic bug.

    Keep a range [lo, hi] that must contain the target and halve it, O(log n). The classic bug is (lo + hi) / 2, which overflows when lo + hi exceeds INT_MAX; write lo + (hi - lo) / 2.

    C
    #include <stdio.h>
    
    int binary_search(const int *a, int n, int target) {
        int lo = 0, hi = n - 1;
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            if (a[mid] == target) return mid;
            if (a[mid] < target) lo = mid + 1;
            else hi = mid - 1;
        }
        return -1;
    }
    
    int main(void) {
        int a[] = {2, 5, 8, 12, 16, 23, 38, 56};
        printf("%d %d %d\n", binary_search(a, 8, 23), binary_search(a, 8, 2), binary_search(a, 8, 7));
        return 0;
    }

    It prints 5 0 -1. See the binary search visualizer.

    Practice it: Binary Search, Search in Rotated Sorted Array, Square Root (Integer)

Coddy programming languages illustration

Learn to code with Coddy

Get started