CoreHRx Jobs

Apply in one click, save jobs and take free mock tests.

Register free Log in
Hiring? Post jobs with CoreHRx
My account
user

View & update profile
Interview guide

C Programming Interview Questions and Answers: 60 Questions from Easy to Hard

This guide helps Indian freshers and experienced developers prepare for C programming interviews for software, embedded and systems roles, from a campus placement round to a senior firmware or Linux position. It has 60 questions in three levels. Easy covers data types, operators, pointers, arrays and strings. Medium covers memory management, structs, the preprocessor, file I/O and debugging. Hard covers undefined behaviour, volatile, atomics, linking, memory layout and performance. Each answer is written the way a strong candidate would say it aloud, and every code example was compiled with gcc -std=c11 -Wall -Wextra, with each stated output taken from a real run. Answer each question aloud before reading ours, then take the mock test to practise against the clock.

  • 60 questions, 3 levels
  • Updated
  • CoreHRx Jobs
  • 219 open C jobs

What C interviews cover

  • Core language: data types and their sizes, operators, integer promotions, control flow and functions, often tested through short predict-the-output programs.
  • Pointers, arrays and strings: pointer arithmetic, const pointers, array decay, null-terminated strings and the safe use of string.h functions.
  • Memory: stack versus heap, malloc and free, leaks, dangling pointers, structure padding, unions and bit-fields.
  • The preprocessor, storage classes and linkage, bitwise operations, and file I/O with stdio.h.
  • For embedded and systems roles: volatile, interrupts, fixed-width types, endianness, undefined behaviour, C11 atomics, and debugging with gdb and sanitizers.

How to prepare (freshers and experienced)

  • Freshers: write and run small programs every day with gcc -std=c11 -Wall -Wextra and read every warning. Many interview traps are things the compiler already warns about.
  • Freshers: practise string and array problems without library shortcuts, such as reversing a string or writing your own strlen and strcpy, and trace the pointers on paper.
  • Experienced developers: be ready to explain undefined behaviour, memory layout and alignment, linker errors, and how you found a real crash or leak.
  • Embedded candidates: revise volatile, interrupt handlers, register access with masks and shifts, ring buffers, and what the startup code does before main.
  • Everyone: build with -fsanitize=address,undefined and learn basic gdb (break, run, bt, print, watch). Showing a clear debugging workflow impresses interviewers.

Common mistakes and interview-day tips

  • Do not give a number for output questions built on undefined behaviour, such as i = i++ + 1. Say it is undefined and explain why; that is often what is being tested.
  • State your assumptions about sizes. Say "on 64-bit Linux with GCC an int is 4 bytes" instead of treating sizes as fixed by the language.
  • When you write code on a whiteboard, check every malloc for NULL, free what you allocate, and leave room for the terminating null character of every string.
  • Think aloud: explain the approach and the edge cases (empty input, NULL, maximum values) before you write code, then trace a small example.
  • Keep C and C++ apart. C has no references, no new or delete and no function overloading, so do not mix them into a C answer.

C interview questions: Easy questions (freshers)

20 questions

1. What are the basic data types in C, and what sizes do they have on 64-bit Linux with GCC?

C has char, the integer types short, int, long and long long (each signed or unsigned), the floating types float, double and long double, and _Bool since C99. The standard fixes only minimum ranges, so sizes depend on the platform. On 64-bit Linux with GCC (the LP64 model) char is 1 byte, short 2, int 4, long 8, long long 8, float 4, double 8, and every pointer 8. On 64-bit Windows long is only 4 bytes, so use int32_t or int64_t from stdint.h when the exact width matters, as in file formats.

2. What happens when an unsigned int goes below zero, and how is that different from signed overflow? Assume a 32-bit int, as on 64-bit Linux with GCC.

Unsigned arithmetic is defined to wrap around modulo 2 to the power N, where N is the number of bits. With a 32-bit unsigned int, 0 - 1 gives 4294967295, which is UINT_MAX, and adding 2 to that gives 1. Signed overflow is different: INT_MAX + 1 is undefined behaviour, so there is no guaranteed result and the optimiser may assume it never happens. A classic bug is a countdown loop such as for (unsigned i = n - 1; i >= 0; i--), which never ends because an unsigned value is always at least 0. -Wextra warns about it.

Example
#include <stdio.h>
#include <limits.h>

int main(void) {
    unsigned int u = 0;
    u = u - 1;               /* wraps to UINT_MAX */
    printf("%u\n", u);       /* 4294967295 */
    u = u + 2;
    printf("%u\n", u);       /* 1 */
    int s = INT_MAX;         /* s + 1 would be undefined */
    printf("%d\n", s);       /* 2147483647 */
    return 0;
}

3. What do the / and % operators give for negative integers in C11?

Since C99, integer division truncates toward zero, so -7 / 2 is -3, not -4. The remainder takes the sign of the dividend and always satisfies (a / b) * b + a % b == a, so -7 % 2 is -1 and 7 % -2 is 1. If either operand is floating point, / does real division: 7 / 2.0 is 3.5 while 7 / 2 is 3. A common bug is testing for odd numbers with n % 2 == 1, which fails for negative odd n because the remainder is -1; test n % 2 != 0 instead. Dividing by zero is undefined behaviour.

Example
#include <stdio.h>

int main(void) {
    printf("%d %d\n", 7 / 2, 7 % 2);     /* 3 1 */
    printf("%d %d\n", -7 / 2, -7 % 2);   /* -3 -1 */
    printf("%d %d\n", 7 / -2, 7 % -2);   /* -3 1 */
    printf("%.1f\n", 7 / 2.0);           /* 3.5 */
    return 0;
}

4. What is the difference between = and == in C, and why does if (x = 5) compile?

= assigns a value and == compares two values. if (x = 5) is legal because an assignment is an expression whose value is the value assigned, here 5, and any non-zero value counts as true. So the branch always runs and x is silently changed to 5. Compile with -Wall and GCC warns: suggest parentheses around assignment used as truth value. When you really mean to assign and test, as in while ((c = getchar()) != EOF), add the parentheses and write the comparison explicitly. Some teams write 5 == x so that a typo fails to compile, but the warning is the better safety net.

5. How does a switch statement work, and what is fall-through?

switch evaluates an integer expression once and jumps to the matching case label, or to default if none matches. Execution then continues downward until a break or the end of the switch, so a case without break falls through into the next one: with day = 2 the code prints Tue and then Wed. Fall-through is useful when several values share code, such as case 'a': case 'e': for vowels, but a missing break is a classic bug, and -Wextra turns on -Wimplicit-fallthrough to warn about it. Case labels must be integer constants, so you cannot switch on a string.

Example
#include <stdio.h>

int main(void) {
    int day = 2;
    switch (day) {
    case 1:
        printf("Mon ");
        break;
    case 2:
        printf("Tue ");      /* no break: falls through */
    case 3:
        printf("Wed ");
        break;
    default:
        printf("Other ");
    }
    printf("\n");            /* prints: Tue Wed */
    return 0;
}

6. What is the difference between while, do-while and for loops?

while checks its condition before each pass, so the body may run zero times. do-while checks after the body, so it always runs at least once, which suits menus and retry-until-valid input. for keeps the initialisation, condition and update on one line and is the natural choice for counting; since C99 the loop variable can be declared inside it, which keeps it local to the loop. If n is 0, while (n > 0) skips its body, but do { ... } while (n > 0) still runs once. Remember the semicolon after the do-while condition, and avoid changing the loop counter inside a for body.

7. Does C pass arguments by value or by reference? Why does a simple swap function fail?

C always passes arguments by value: the function receives copies, so swap_bad exchanges its own copies and the caller's x and y do not change. To let a function change a caller's variable, pass its address and work through the pointer, as swap does with *a and *b. That is still pass by value, but the value copied is an address, which is how C simulates pass by reference. Arrays look like an exception because an array argument decays to a pointer to its first element, so a function can modify the caller's array elements directly.

Example
#include <stdio.h>

void swap_bad(int a, int b) { int t = a; a = b; b = t; }

void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }

int main(void) {
    int x = 1, y = 2;
    swap_bad(x, y);
    printf("%d %d\n", x, y);   /* 1 2 */
    swap(&x, &y);
    printf("%d %d\n", x, y);   /* 2 1 */
    return 0;
}

8. What is a pointer, and what do the & and * operators do?

A pointer is a variable that stores the address of another object. The unary & operator gives the address of a variable, and the unary * operator, called dereferencing, reaches the object at that address. The pointer's type tells the compiler what is stored there and how many bytes to read, so an int * reads an int. If int *p = &marks, then *p = 85 changes marks itself. Pointers let functions modify the caller's data, build linked structures and work with dynamic memory. Always initialise a pointer, to a valid address or to NULL, before you use it.

9. What is the difference between a NULL pointer, a dangling pointer and a wild pointer?

A NULL pointer holds the null pointer value and points to no object. It is a valid value you can test with if (p == NULL), but dereferencing it is undefined behaviour and on Linux usually crashes. A dangling pointer once pointed to a valid object that no longer exists, for example after free() or after the function that owned a local variable has returned. A wild pointer was never initialised, so it holds garbage. Dangling and wild pointers are worse than NULL because the program may seem to work. Initialise every pointer and set it to NULL after free.

10. On 64-bit Linux with GCC, why does sizeof give a different result for an array in main and for the same array passed to a function?

In main, a is a real array of 10 ints, so sizeof a is 40 bytes and sizeof a / sizeof a[0] gives the element count, 10. When you pass an array to a function it decays to a pointer to its first element, and a parameter written as int arr[] is adjusted to int *arr. Inside show, sizeof arr is the size of a pointer, 8 bytes, and GCC even warns about it. That is why C functions that take arrays also take a length, as in void show(const int *arr, size_t n). The element-count trick works only where the array itself is in scope.

Example
#include <stdio.h>

void show(int arr[]) {             /* really int *arr */
    printf("%zu\n", sizeof arr);   /* 8: size of a pointer */
}

int main(void) {
    int a[10];
    printf("%zu\n", sizeof a);                 /* 40 */
    printf("%zu\n", sizeof a / sizeof a[0]);   /* 10 */
    show(a);
    return 0;
}

11. How are strings stored in C, and why do strlen and sizeof give different values for the same string?

A C string is an array of char that ends with a null character, '\0'. There is no stored length; string functions find the end by scanning for that terminator. strlen counts the characters before it: 4 for "Pune". sizeof is a compile-time operator that gives the size of the whole array: 5 for city, because the compiler added the '\0', and 20 for buf, whatever it currently holds. When you allocate space for a copy you need strlen(s) + 1 bytes. Forgetting the +1 for the terminator is one of the most common causes of buffer overflows in C.

Example
#include <stdio.h>
#include <string.h>

int main(void) {
    char city[] = "Pune";
    char buf[20] = "Pune";
    printf("%zu %zu\n", strlen(city), sizeof city);  /* 4 5 */
    printf("%zu %zu\n", strlen(buf), sizeof buf);    /* 4 20 */
    return 0;
}

12. Why can you not compare two strings with ==, and how should you compare them?

In an expression an array name becomes a pointer to its first element, so a == b compares two addresses. Two separate arrays never share an address, even with the same contents, so it gives 0, and GCC warns about comparing arrays. To compare characters, use strcmp from string.h: it returns 0 when the strings are equal, a negative value when the first sorts earlier and a positive value otherwise. Test strcmp(a, b) == 0, never just if (strcmp(a, b)), which is true when they differ. strncmp compares at most n characters.

Example
#include <stdio.h>
#include <string.h>

int main(void) {
    char a[] = "Delhi";
    char b[] = "Delhi";
    printf("%d\n", a == b);              /* 0: two arrays */
    printf("%d\n", strcmp(a, b) == 0);   /* 1: same text */
    printf("%d\n", strcmp("Agra", "Delhi") < 0);   /* 1 */
    return 0;
}

13. What is the difference between malloc, calloc, realloc and free?

All four come from stdlib.h and manage heap memory. malloc(n) returns n bytes whose contents are indeterminate, so read nothing before you write it. calloc(count, size) allocates an array and sets every byte to zero. realloc(p, n) resizes a block, possibly moving it, and keeps the old contents up to the smaller of the two sizes. free(p) releases a block, and free(NULL) is safe and does nothing. Every allocation can fail and return NULL, so check the result. Assign realloc to a temporary first, because p = realloc(p, n) loses the old block if it fails.

14. What is the difference between a struct and a union? Give their sizes on 64-bit Linux with GCC.

A struct gives every member its own storage, in declaration order, so its size is at least the sum of the members plus any padding. A union overlaps all members at the same address, so its size is that of the largest member, rounded up for alignment, and only the member last written holds a meaningful value. On 64-bit Linux with GCC the struct below takes 24 bytes, because of padding after roll and after grade, while the union takes 8, the size of a double. Unions save memory when a value is one of several types, usually with a tag saying which.

Example
#include <stdio.h>

struct student { int roll; double cgpa; char grade; };
union value { int i; double d; char c; };

int main(void) {
    printf("%zu\n", sizeof(struct student));   /* 24 */
    printf("%zu\n", sizeof(union value));      /* 8 */
    union value v;
    v.i = 42;                 /* now i is the live member */
    printf("%d\n", v.i);      /* 42 */
    return 0;
}

15. What is wrong with #define SQUARE(x) x * x, and how do you fix it?

The preprocessor substitutes text before compilation and knows nothing about precedence. SQUARE(2 + 3) becomes 2 + 3 * 2 + 3, which is 11, not 25, and 100 / SQUARE(5) becomes 100 / 5 * 5, which is 100. Put parentheses around each parameter and around the whole body: ((x) * (x)). Even then, an argument with side effects such as SQUARE(i++) is evaluated twice, which here is undefined behaviour. In modern C prefer a static inline function or a const variable: they are type-checked and evaluate arguments once. Keep macros for jobs functions cannot do.

Example
#include <stdio.h>

#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))

int main(void) {
    printf("%d\n", SQUARE_BAD(2 + 3));   /* 11 */
    printf("%d\n", SQUARE(2 + 3));       /* 25 */
    printf("%d\n", 100 / SQUARE_BAD(5)); /* 100 */
    printf("%d\n", 100 / SQUARE(5));     /* 4 */
    return 0;
}

16. What are the storage classes in C, and what does static do to a local variable?

C11 has the storage-class specifiers auto, register, static, extern and _Thread_local, plus typedef, which is grouped with them only for syntax. A local variable is automatic by default: created on each call and gone on return. Adding static gives it static storage duration, so it is initialised once, before the program starts, and keeps its value between calls while staying visible only inside the function. Here count grows 1, 2, 3 but temp restarts every call. A static local makes the function non-reentrant and unsafe across threads, so avoid it for shared state.

Example
#include <stdio.h>

int next_token(void) {
    static int count = 0;    /* initialised once, kept */
    int temp = 0;            /* automatic: new every call */
    temp++;
    count++;
    return count * 10 + temp;
}

int main(void) {
    for (int i = 0; i < 3; i++)
        printf("%d ", next_token());   /* 11 21 31 */
    printf("\n");
    return 0;
}

17. What do the bitwise operators do, and how do you set, clear, toggle and test a bit?

& gives 1 where both bits are 1, | where either is 1, ^ where they differ, ~ flips every bit, and << and >> shift left and right. Set bit n with x |= 1u << n, clear it with x &= ~(1u << n), toggle it with x ^= 1u << n, and test it with (x >> n) & 1u; n & 1 tells you whether n is odd. Use unsigned types: shifting a negative value left is undefined, right-shifting one is implementation-defined, and shifting by the type's width or more is undefined. Do not confuse & with &&, which is logical AND.

Example
#include <stdio.h>

int main(void) {
    unsigned int flags = 0x05;       /* 0101 */
    flags |= 1u << 1;                /* set bit 1:    0111 */
    flags &= ~(1u << 0);             /* clear bit 0:  0110 */
    flags ^= 1u << 3;                /* toggle bit 3: 1110 */
    printf("0x%X\n", flags);         /* 0xE */
    printf("%u\n", (flags >> 2) & 1u);   /* 1: bit 2 set */
    printf("%d\n", 7 & 1);           /* 1: 7 is odd */
    return 0;
}

18. How do you read a text file line by line in C?

Open it with fopen and a mode: "r" to read, "w" to create or truncate, "a" to append, with b added for binary. fopen returns NULL on failure, so check it and report the reason with perror. Then call fgets in a loop: it reads one line, at most sizeof line - 1 characters, keeps the newline, adds '\0', and returns NULL at end of file or on error. Finally call fclose, which flushes buffered output and releases the handle. A line longer than the buffer arrives in pieces, so code that parses records must handle that case.

Example
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("cities.txt", "r");
    if (fp == NULL) {
        perror("cities.txt");
        return 1;
    }
    char line[128];
    int n = 0;
    while (fgets(line, sizeof line, fp) != NULL)
        printf("%d: %s", ++n, line);
    fclose(fp);
    return 0;
}

19. What are the stages of turning a C source file into an executable?

There are four. The preprocessor handles #include, #define and #if and produces one expanded translation unit; gcc -E shows it. The compiler turns that into assembly for the target CPU, and this is where syntax and type errors appear; gcc -S stops here. The assembler converts the assembly into an object file, which gcc -c produces. The linker combines object files and libraries, resolves references between them and writes the executable. An undefined reference error therefore comes from the linker, usually a missing .c file or -l library on the command line.

20. What is recursion, and what must every recursive function have?

A recursive function solves a problem by calling itself on a smaller version of it. It needs a base case that returns without recursing, and every call must move closer to that base case; otherwise the calls never stop and the program crashes with a stack overflow. Each call gets its own stack frame with its own parameters and locals, so deep recursion uses a lot of memory. A factorial function returns 1 when n is 1 or less and n * factorial(n - 1) otherwise, so factorial(5) is 120. For simple chains like this a loop is faster, but recursion suits trees and backtracking.

Ready to check yourself? Take the free C mock test

C interview questions: Medium questions (1-3 years)

20 questions

21. How does pointer arithmetic work, and what does subtracting two pointers give? Byte counts are for 64-bit Linux with GCC.

Adding n to a pointer moves it by n elements, not n bytes: the compiler scales by the size of the pointed-to type. So p + 2 points to a[2], and *(p + 2) is exactly what a[2] means. Subtracting two pointers into the same array gives the number of elements between them as a ptrdiff_t, printed with %td: here q - p is 4 while the byte distance is 16. Arithmetic is defined only within one array, plus one past its end; subtracting pointers into different objects is undefined. Arithmetic on void * is a GCC extension, not standard C.

Example
#include <stdio.h>
#include <stddef.h>

int main(void) {
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;                     /* same as &a[0] */
    int *q = &a[4];
    printf("%d\n", *(p + 2));       /* 30, same as p[2] */
    ptrdiff_t gap = q - p;
    printf("%td\n", gap);           /* 4 elements */
    printf("%td\n", (char *)q - (char *)p);   /* 16 bytes */
    return 0;
}

22. What is the difference between const int *p, int *const p and const int *const p?

Read the declaration from right to left. const int *p is a pointer to a const int: you can point it elsewhere, but you cannot change the int through it. int *const p is a const pointer to an int: initialise it, because it can never be reassigned, and the int can be changed through it. const int *const p fixes both. The first form is the one for function parameters, as in size_t strlen(const char *s), because it promises the caller that the function will not modify the data. It restricts that access path only; the object itself may still change.

Example
#include <stdio.h>

int main(void) {
    int a = 1, b = 2;
    const int *p1 = &a;     /* pointer to const int */
    p1 = &b;                /* OK: the pointer can move */
    /* *p1 = 5;    error: read-only through p1 */

    int *const p2 = &a;     /* const pointer to int */
    *p2 = 5;                /* OK: the int can change */
    /* p2 = &b;    error: the pointer cannot move */

    const int *const p3 = &a;      /* neither can change */
    printf("%d %d %d\n", *p1, *p2, *p3);   /* 2 5 5 */
    return 0;
}

23. When do you need a pointer to a pointer? Show a function that allocates memory for the caller.

Because C passes everything by value, a function that must change the caller's pointer, not just the data it points to, needs the address of that pointer: a pointer to a char pointer. make_copy writes the new block's address into *out, so name in main receives it. With a plain char * parameter, the assignment would change only the local copy and the memory would leak. Pointers to pointers also appear in argv, in arrays of strings, and in linked-list code that removes the head node without a special case. Document who frees the memory: here the caller must call free.

Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int make_copy(const char *src, char **out) {
    char *p = malloc(strlen(src) + 1);
    if (p == NULL)
        return -1;
    strcpy(p, src);
    *out = p;                 /* changes the caller's pointer */
    return 0;
}

int main(void) {
    char *name = NULL;
    if (make_copy("Kavya", &name) == 0) {
        printf("%s\n", name);   /* Kavya */
        free(name);
    }
    return 0;
}

24. What is a function pointer? Show how qsort uses one.

A function pointer stores the address of a function, so code can decide at run time which function to call. int (*cmp)(const void *, const void *) reads as: cmp is a pointer to a function that takes two const void * and returns int. qsort from stdlib.h is generic and calls your comparator, which must return a negative, zero or positive value. Writing return x - y looks shorter but can overflow for large values of opposite sign, which is undefined, so compare instead. Function pointers also implement callbacks, command dispatch tables and driver interfaces in embedded code.

Example
#include <stdio.h>
#include <stdlib.h>

static int by_value(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);    /* no overflow */
}

int main(void) {
    int v[] = {42, -7, 19, 0, 3};
    size_t n = sizeof v / sizeof v[0];
    qsort(v, n, sizeof v[0], by_value);
    for (size_t i = 0; i < n; i++)
        printf("%d ", v[i]);     /* -7 0 3 19 42 */
    printf("\n");
    return 0;
}

25. How are two-dimensional arrays stored, and how do you pass one to a function?

C stores a 2D array in row-major order: all of row 0, then all of row 1, in one contiguous block. The array decays to a pointer to its first row, so the parameter must know the row length: int m[][COLS] and int (*m)[COLS] mean the same, and the number of rows is passed separately. A pointer to a pointer to int is a different type, so passing grid where one is expected is wrong and GCC reports incompatible pointer types. A VLA parameter such as int m[rows][cols] handles variable sizes, but VLAs are optional in C11, so some embedded compilers lack them.

Example
#include <stdio.h>

#define COLS 3

int sum(int rows, int m[][COLS]) {   /* = int (*m)[COLS] */
    int total = 0;
    for (int r = 0; r < rows; r++)
        for (int c = 0; c < COLS; c++)
            total += m[r][c];
    return total;
}

int main(void) {
    int grid[2][COLS] = {{1, 2, 3}, {4, 5, 6}};
    printf("%d\n", sum(2, grid));    /* 21 */
    return 0;
}

26. Why are gets and strcpy dangerous, and what should you use instead?

gets reads a line with no limit, so long input overflows the buffer; C11 dropped it. strcpy and strcat also write without knowing the target size. Prefer snprintf: it never writes more than the size given, always adds '\0' when the size is not zero, and returns the length it wanted, so a result of size or more means truncation, as with 9 here; GCC even warns when it can see that at compile time. Read lines with fgets. strncpy is not a safe strcpy: if the source is too long it leaves the buffer unterminated. C11's Annex K functions such as strcpy_s are optional, and glibc does not provide them.

Example
#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[8];
    const char *city = "Bengaluru";
    int need = snprintf(buf, sizeof buf, "%s", city);
    printf("%s %d\n", buf, need);     /* Bengalu 9 */

    char t[4];
    strncpy(t, city, sizeof t);       /* no '\0' added */
    t[sizeof t - 1] = '\0';           /* terminate by hand */
    printf("%s\n", t);                /* Ben */
    return 0;
}

27. What is a memory leak, how do you find one, and what is the safe way to call realloc?

A leak is heap memory the program can no longer free because it has lost every pointer to it; in a long-running service or firmware, leaks slowly exhaust memory. Find them with Valgrind (valgrind --leak-check=full ./app) or by building with -fsanitize=address, whose LeakSanitizer prints the allocation stack of each leaked block at exit on Linux. With realloc, never write p = realloc(p, n): on failure it returns NULL, the old block is still allocated, and you have just overwritten its only pointer. Keep the result in a temporary, check it, then assign, as grow does.

Example
#include <stdlib.h>

/* Doubles the capacity of a dynamic int array. */
int grow(int **arr, size_t *cap) {
    size_t new_cap = *cap ? *cap * 2 : 8;
    int *tmp = realloc(*arr, new_cap * sizeof **arr);
    if (tmp == NULL)
        return -1;        /* *arr is still valid: no leak */
    *arr = tmp;
    *cap = new_cap;
    return 0;
}

28. What is wrong with returning the address of a local variable, and how do you fix it?

A local variable has automatic storage: it lives in the function's stack frame, which is reused as soon as the function returns. Returning its address gives the caller a dangling pointer, and using it is undefined behaviour. GCC warns that the function returns the address of a local variable, and it may even compile the return as NULL. Fixes: allocate with malloc and make the caller free it, have the caller pass in a buffer and its size, which is the usual C style, or use a static buffer, which is not reentrant or thread-safe. Stack memory is fast and freed automatically.

Example
#include <stdio.h>
#include <stdlib.h>

char *label_bad(void) {
    char buf[16] = "Order 42";
    return buf;          /* buf dies here: dangling */
}

char *label_ok(void) {
    char *p = malloc(16);
    if (p != NULL)
        snprintf(p, 16, "Order %d", 42);
    return p;            /* the caller must free it */
}

int main(void) {
    char *s = label_ok();
    if (s != NULL)
        printf("%s\n", s);   /* Order 42 */
    free(s);
    return 0;
}

29. What is structure padding? On 64-bit Linux with GCC, how big is struct { char a; int b; char c; } and how can you shrink it?

CPUs access data fastest, and some only correctly, when each value sits at an address that is a multiple of its alignment, 4 for int on 64-bit Linux with GCC. So the compiler inserts padding. In struct loose, three bytes follow a so that b starts at offset 4, and three more follow c so that the size is a multiple of 4 and array elements stay aligned: 12 bytes in all. Ordering members from largest to smallest alignment, as in struct tight, cuts it to 8. Check layouts with offsetof, and never compare structs with memcmp, because padding bytes hold unspecified values.

Example
#include <stdio.h>
#include <stddef.h>

struct loose { char a; int b; char c; };
struct tight { int b; char a; char c; };

int main(void) {
    printf("%zu %zu\n", sizeof(struct loose),
           sizeof(struct tight));               /* 12 8 */
    printf("%zu %zu\n", offsetof(struct loose, b),
           offsetof(struct loose, c));          /* 4 8 */
    return 0;
}

30. What are bit-fields, and why are they risky for mapping hardware registers?

A bit-field gives a struct member an exact width in bits, such as unsigned int ready : 1, so several flags share one word. An unsigned bit-field wraps like unsigned arithmetic in that width: storing 16 in a 4-bit field gives 0. They are handy for packing flags, but the standard leaves most of the layout implementation-defined: the order of bits, whether a field may cross a storage unit, and whether a plain int field is signed. A bit-field struct laid over a device register or network header can break with another compiler or CPU, so use masks and shifts on a fixed-width integer there.

31. How do you use a tagged union to store values of different types safely?

Pair the union with an enum field, the tag, that records which member was last stored, and check the tag before every read. A struct keeps the two together, and C11 anonymous unions let you write v.i instead of v.u.i. A switch on the tag with no default lets -Wswitch, part of -Wall, warn when someone adds a new kind but forgets to handle it. The pattern appears in JSON parsers, interpreters and message queues. The usual bug is setting the member but not the tag, or reading the wrong member, which silently reinterprets the bytes instead of reporting an error.

Example
#include <stdio.h>

enum kind { K_INT, K_TEXT };

struct value {
    enum kind kind;          /* which member is live */
    union {                  /* anonymous union (C11) */
        int i;
        const char *text;
    };
};

void print(struct value v) {
    switch (v.kind) {
    case K_INT:  printf("int %d\n", v.i);     break;
    case K_TEXT: printf("text %s\n", v.text); break;
    }
}

int main(void) {
    print((struct value){ .kind = K_INT, .i = 7 });
    print((struct value){ .kind = K_TEXT, .text = "Chennai" });
    return 0;
}

32. What belongs in a header file, and why do you need include guards?

A header holds what other files need to use a module: type definitions, macros, function prototypes and extern declarations of shared variables. It should not define functions or variables, because every .c file that includes it would get its own copy and the linker would report multiple definitions; static inline functions are the usual exception. Include guards, the #ifndef, #define and #endif wrapper, make a second inclusion in the same translation unit expand to nothing, so types are not defined twice when headers include each other. #pragma once works in GCC but is not standard C.

Example
/* billing.h */
#ifndef BILLING_H
#define BILLING_H

#include <stddef.h>

typedef struct { int id; long paise; } invoice;

long invoice_total(const invoice *items, size_t n);
extern int billing_debug;    /* defined in billing.c */

#endif /* BILLING_H */

33. Why are multi-statement macros wrapped in do { ... } while (0)?

A macro that expands to several statements breaks as the body of an if without braces: only the first statement is guarded, so LOG_BAD's second printf runs anyway, and -Wall warns. Wrapping the body in do { ... } while (0) makes it exactly one statement that still needs its trailing semicolon, so it works with if and else and inside loops. Plain braces fail with if-else, because the semicolon after the closing brace ends the if and the else then has no matching if. If you do not need __FILE__ or __LINE__, a static inline function is better.

Example
#include <stdio.h>

#define LOG_BAD(msg) printf("[log] "); printf("%s\n", msg)

#define LOG(msg)                \
    do {                        \
        printf("[log] ");       \
        printf("%s\n", msg);    \
    } while (0)

int main(void) {
    int ok = 1;
    if (!ok)
        LOG_BAD("failed");   /* prints "failed" anyway */
    if (!ok)
        LOG("failed");       /* one statement: skipped */
    else
        LOG("done");         /* prints "[log] done" */
    return 0;
}

34. What do static and extern mean for global variables and functions across files?

At file scope, static gives a variable or function internal linkage: it is visible only inside that .c file, so two files can each have a private helper with the same name without a clash. Without static, a global has external linkage and can be shared. extern declares a name defined elsewhere: it creates no storage, and exactly one file must hold the definition. Put the extern declaration in a header so every file sees the same type. Making helpers static by default hides internals, avoids name collisions and lets the compiler inline them or drop unused ones.

35. How do you count the set bits in an integer and check whether it is a power of two?

x & (x - 1) clears the lowest set bit, because subtracting 1 flips that bit and every zero below it. Repeating it until x is 0 counts the set bits in as many steps as there are ones; this is Brian Kernighan's method. A power of two has exactly one set bit, so x & (x - 1) is 0 for it, but check x != 0 as well or 0 is wrongly accepted. Use unsigned fixed-width types so the subtraction is well defined. In production code GCC's __builtin_popcount can become a single instruction when the target has one, and C23 adds stdc_count_ones.

Example
#include <stdio.h>
#include <stdint.h>

int count_bits(uint32_t x) {
    int count = 0;
    while (x != 0) {
        x &= x - 1;          /* clear the lowest set bit */
        count++;
    }
    return count;
}

int is_pow2(uint32_t x) { return x != 0 && (x & (x - 1)) == 0; }

int main(void) {
    printf("%d %d\n", count_bits(0xF0), count_bits(255));  /* 4 8 */
    printf("%d %d %d\n",
           is_pow2(64), is_pow2(96), is_pow2(0));         /* 1 0 0 */
    return 0;
}

36. How do you write and read binary records with fwrite and fread, and what are the portability pitfalls?

Open the file in binary mode, "wb" or "rb". fwrite(ptr, size, count, fp) and fread(ptr, size, count, fp) copy raw bytes and return how many whole items were transferred, so always compare that with the count you asked for. On a stream opened for update, put fflush, fseek or rewind between writing and reading. Dumping structs is fast, but the file then depends on the compiler and CPU: padding, int sizes and byte order differ, so a file written on an x86 laptop may read wrongly on an ARM board. For data that moves between systems, write each field with a fixed width and byte order.

37. Why is while (!feof(fp)) the wrong way to loop over a file?

feof only reports that an earlier read already hit the end of the file; it does not predict that the next read will fail. So the loop body runs once more after the last character and processes a bogus value: the example counts 3 characters in a 2-byte file. Test the result of the read itself instead: fgetc returning EOF, fgets returning NULL, or fread returning fewer items. After the loop, use feof and ferror to tell end of file from a read error. Store fgetc's result in an int, not a char, because EOF is a negative int distinct from every unsigned char value.

Example
#include <stdio.h>

int main(void) {
    FILE *fp = tmpfile();
    if (fp == NULL)
        return 1;
    fputs("ab", fp);
    rewind(fp);

    int bad = 0;
    while (!feof(fp)) {             /* wrong */
        fgetc(fp);
        bad++;
    }
    rewind(fp);
    int c, n = 0;                   /* int, so it can hold EOF */
    while ((c = fgetc(fp)) != EOF)  /* right */
        n++;
    printf("%d %d\n", bad, n);      /* 3 2 */
    fclose(fp);
    return 0;
}

38. What is the difference between undefined, unspecified and implementation-defined behaviour? Give examples.

Implementation-defined behaviour must be chosen and documented by the compiler, such as the size of int or the result of right-shifting a negative number. Unspecified behaviour allows several outcomes and the compiler need not say which, such as the order in which the operands of f() + g() are evaluated. Undefined behaviour has no requirements at all: signed overflow, reading outside an array, dereferencing NULL, using freed memory, or modifying a variable twice without sequencing, as in i = i++ + 1. The program may crash, print nonsense, or work until the optimiser changes. Avoid UB entirely.

39. Your C program crashes with a segmentation fault. How do you find the cause?

A segmentation fault means the program touched memory it may not access: usually a NULL or dangling pointer, an index out of bounds, or a stack overflow from runaway recursion. Rebuild with -g -O0 so the debugger can map machine code back to source, run the program under gdb, and type bt when it stops to see the call stack and the failing line, then print the pointers involved. AddressSanitizer is often faster: built with -fsanitize=address, it stops at the first bad access and reports what kind it was and where the memory was allocated and freed. Fix the first error first.

40. Why does the comparison -1 < 1u evaluate to false in C?

When an int meets an unsigned int in an expression, the usual arithmetic conversions convert the int to unsigned, so -1 becomes UINT_MAX and -1 < 1u is false. The same trap hits size_t, the unsigned type of strlen and sizeof: strlen(s) - 4 > 0 is true for a 3-character string because the subtraction wraps to a huge value. Compile with -Wextra, which enables -Wsign-compare, keep sizes and indexes in size_t and signed quantities in signed types, and when you must compare the two, check the signed value for a negative first. Types narrower than int are promoted to int first.

Example
#include <stdio.h>
#include <string.h>

int main(void) {
    int balance = -1;
    unsigned int limit = 1;
    printf("%d\n", balance < limit);       /* 0 */
    printf("%d\n", balance < (int)limit);  /* 1 */
    char s[] = "abc";
    printf("%d\n", strlen(s) - 4 > 0);     /* 1: wrapped */
    return 0;
}

Ready to check yourself? Take the free C mock test

C interview questions: Hard questions (experienced)

20 questions

41. Why might an optimising compiler delete a check like if (x + 1 < x), and how do you detect signed overflow correctly?

Signed overflow is undefined, so the compiler may assume it never happens. Under that assumption x + 1 < x can never be true, and GCC at -O2 folds the check to false, so the guard silently disappears. Test the operands before the operation instead, as add_ok does with INT_MAX - b and INT_MIN - b, which cannot overflow. GCC and Clang also offer __builtin_add_overflow, and C23 standardises ckd_add in stdckdint.h. In testing, -fsanitize=undefined reports signed overflow at run time. -fwrapv makes it wrap, but that only hides the bug in code meant to be portable.

Example
#include <stdio.h>
#include <limits.h>

/* Wrong: x + 1 overflows first, so GCC -O2 returns 0 */
int overflows_bad(int x) { return x + 1 < x; }

/* Right: test before doing the operation */
int add_ok(int a, int b, int *out) {
    if ((b > 0 && a > INT_MAX - b) ||
        (b < 0 && a < INT_MIN - b))
        return 0;
    *out = a + b;
    return 1;
}

int main(void) {
    int r = 0;
    printf("%d\n", add_ok(INT_MAX, 1, &r));   /* 0 */
    if (add_ok(40, 2, &r))
        printf("%d\n", r);                    /* 42 */
    return 0;
}

42. What is the strict aliasing rule, and how do you read the bits of a float safely? Outputs are for 64-bit Linux with GCC, where float is IEEE 754 single precision.

The compiler may assume that pointers to incompatible types never refer to the same memory: an object may be accessed only through its own type, a compatible type, or a character type. So reading a float through a uint32_t * is undefined, and at -O2, where -fstrict-aliasing is on, GCC may reorder or cache loads and give wrong results. Copy the bytes with memcpy instead: it is fully defined, and GCC turns a small fixed-size memcpy into a plain register move. Reading another member of a union is also allowed in C, though not in C++. Casting a char buffer to a struct pointer breaks the same rule.

Example
#include <stdio.h>
#include <stdint.h>
#include <string.h>

uint32_t float_bits(float f) {
    uint32_t u;
    memcpy(&u, &f, sizeof u);   /* defined behaviour */
    return u;
    /* return *(uint32_t *)&f;     UB: strict aliasing */
}

int main(void) {
    printf("0x%08X\n", (unsigned)float_bits(1.0f));  /* 0x3F800000 */
    printf("0x%08X\n", (unsigned)float_bits(-2.5f)); /* 0xC0200000 */
    return 0;
}

43. What does volatile guarantee in embedded C, and what does it NOT guarantee?

volatile tells the compiler that an object can change, or that accessing it has effects, beyond the code it sees, so every read and write in the source really happens, in order relative to other volatile accesses. Memory-mapped registers and flags set by an interrupt handler need exactly that; without it the compiler may read UART0_STATUS once and spin forever. volatile does not make operations atomic, so count++ can still be split by an interrupt, and it does not order ordinary memory or work across cores. Between threads use C11 atomics; for multi-byte ISR data, disable interrupts briefly.

Example
#include <stdint.h>

#define UART0_STATUS (*(volatile uint32_t *)0x4000C018u)
#define TX_READY     (1u << 5)

static volatile uint8_t tick_flag;   /* set by a timer ISR */

void timer_isr(void) { tick_flag = 1; }

void wait_tick(void) {
    while (tick_flag == 0)
        ;                  /* re-read on every pass */
    tick_flag = 0;
}

void wait_uart(void) {
    while ((UART0_STATUS & TX_READY) == 0)
        ;                  /* each pass reads the register */
}

44. How do C11 atomics prevent data races, and when do you need memory_order_release and memory_order_acquire?

In C11 a data race, two threads touching the same non-atomic object with at least one write and no synchronisation, is undefined behaviour. An atomic type such as atomic_int makes each operation, like atomic_fetch_add, indivisible. Ordering is a separate question: a release store publishes every write made before it, and an acquire load that reads the stored value is guaranteed to see them all, so the reader prints 4250, never a stale total. memory_order_relaxed suits a plain counter; the default seq_cst is safest and slowest. threads.h is optional in C11; glibc has had it since 2.28.

Example
#include <stdio.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <threads.h>

static int order_total;              /* plain data */
static atomic_bool ready = false;

static int producer(void *arg) {
    (void)arg;
    order_total = 4250;              /* 1. write the data */
    atomic_store_explicit(&ready, true,
                          memory_order_release);  /* 2. publish */
    return 0;
}

int main(void) {
    thrd_t t;
    if (thrd_create(&t, producer, NULL) != thrd_success)
        return 1;
    while (!atomic_load_explicit(&ready, memory_order_acquire))
        ;                            /* wait for the flag */
    printf("%d\n", order_total);     /* always 4250 */
    thrd_join(t, NULL);
    return 0;
}

45. What does the restrict qualifier promise, and why does memcpy use it while memmove does not?

restrict on a pointer is the programmer's promise that, while the function runs, the object accessed through that pointer is not accessed through any other pointer. The compiler can then keep values in registers and vectorise loops without reloading memory after each store. memcpy's parameters are restrict-qualified, so calling it on overlapping regions is undefined; memmove must work even when source and destination overlap, as when shifting characters within one buffer. The compiler does not check the promise: a call such as add(a, a + 1, n) breaks it and can silently give wrong results.

Example
#include <stdio.h>
#include <string.h>

void add(int *restrict d, const int *restrict s, size_t n) {
    for (size_t i = 0; i < n; i++)
        d[i] += s[i];        /* free to vectorise */
}

int main(void) {
    char s[] = "abcdef";
    memmove(s + 2, s, 4);     /* overlap: needs memmove */
    printf("%s\n", s);        /* ababcd */
    int a[3] = {1, 2, 3}, b[3] = {10, 20, 30};
    add(a, b, 3);
    printf("%d %d %d\n", a[0], a[1], a[2]);   /* 11 22 33 */
    return 0;
}

46. Many embedded projects ban malloc. How would you write a simple arena allocator, and how do you keep its blocks aligned? Offsets are for 64-bit Linux with GCC.

An arena hands out memory from one large static buffer by bumping an offset, and frees everything at once by resetting it, which suits per-frame or per-request work. It is fast, deterministic and cannot fragment, which is why safety-critical firmware prefers it to malloc. Each block must start at a suitable alignment, so round the offset up to _Alignof(max_align_t), 16 on 64-bit Linux with GCC, with a mask; that works because alignments are powers of two. Check the remaining space without overflow, as n > a->size - start does. The trade-off: single blocks cannot be freed.

Example
#include <stdio.h>
#include <stddef.h>

typedef struct { unsigned char *base; size_t size, used; } arena;

void *arena_alloc(arena *a, size_t n) {
    size_t align = _Alignof(max_align_t);
    size_t start = (a->used + align - 1) & ~(align - 1);
    if (start > a->size || n > a->size - start)
        return NULL;                 /* out of space */
    a->used = start + n;
    return a->base + start;
}

int main(void) {
    static _Alignas(max_align_t) unsigned char pool[256];
    arena a = { pool, sizeof pool, 0 };
    char *name = arena_alloc(&a, 10);
    double *rate = arena_alloc(&a, sizeof *rate);
    printf("%zu %td\n", a.used, (char *)rate - name);   /* 24 16 */
    a.used = 0;                      /* free everything */
    return 0;
}

47. How does malloc work under the hood on Linux, and why do fragmentation and double free matter?

glibc's malloc keeps a small header before each block that records its size, which is why free needs only the pointer. Freed blocks go into bins and per-thread caches for reuse, and neighbouring free blocks are usually merged. Small requests are carved from the heap, which grows with brk, while large ones, from 128 KB by default, get their own mmap mapping that free returns to the OS at once. Fragmentation splits free memory into pieces too small to use, so a long-running service can grow without leaking. A double free or overflow corrupts those headers, and glibc aborts when it detects one.

48. What is a flexible array member, and why is it better than a separate pointer for variable-length records? Sizes are for 64-bit Linux with GCC.

Since C99 the last member of a struct can be an array with no size, such as unsigned char data[]. It adds nothing to sizeof, which is 8 bytes here on 64-bit Linux with GCC, and the header and payload come from one malloc(sizeof *p + n). Compared with a separate data pointer, that is one allocation and one free instead of two, and the payload sits right after the header, which is cache-friendly and easy to send as one buffer. Limits: such a struct cannot be an array element or a member of another struct, and assigning it copies only the header.

Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct packet {
    size_t len;
    unsigned char data[];     /* flexible array member */
};

int main(void) {
    const char *msg = "UPI OK";
    size_t n = strlen(msg);
    struct packet *p = malloc(sizeof *p + n);
    if (p == NULL)
        return 1;
    p->len = n;
    memcpy(p->data, msg, n);
    printf("%zu %zu %.*s\n", sizeof(struct packet), p->len,
           (int)p->len, (char *)p->data);   /* 8 6 UPI OK */
    free(p);
    return 0;
}

49. What are X-macros, and how do they keep an enum and its string table in sync?

An X-macro is a list macro that takes another macro as its parameter and applies it to every entry. You write the list of names once and expand it with different helper macros: AS_ENUM builds the enum constants with ## token pasting, and AS_TEXT builds the matching strings with the # stringizing operator. Adding a state to ORDER_STATES updates both, so the table cannot fall out of sync, and ST_COUNT gives the length for free. The technique is common for error codes, command tables and register maps. The cost is readability, so keep lists short and inspect the expansion with gcc -E.

Example
#include <stdio.h>

#define ORDER_STATES(X) \
    X(PLACED)           \
    X(PACKED)           \
    X(SHIPPED)          \
    X(DELIVERED)

#define AS_ENUM(name) ST_##name,
#define AS_TEXT(name) #name,

enum order_state { ORDER_STATES(AS_ENUM) ST_COUNT };
static const char *const names[] = { ORDER_STATES(AS_TEXT) };

int main(void) {
    printf("%d %s\n", ST_COUNT, names[ST_SHIPPED]);  /* 4 SHIPPED */
    return 0;
}

50. What is _Generic in C11, and how do you build a type-generic macro with it?

_Generic is a compile-time selection: it looks at the type of its controlling expression and picks the matching branch, so a macro can route to different code by type at no run-time cost; tgmath.h offers the same kind of type-generic sqrt. The expression is not evaluated, only its type is used, after lvalue conversion, so a string literal matches char *. Watch the promotions: c + 1 is an int even when c is a char. Types with no branch go to default, and without a default they fail to compile, which can reject wrong types on purpose. Typical uses are print, abs and min helpers.

Example
#include <stdio.h>

#define TYPE_NAME(x) _Generic((x),    \
    int: "int",                       \
    unsigned int: "unsigned int",     \
    double: "double",                 \
    char *: "char *",                 \
    default: "other")

int main(void) {
    char c = 'A';
    printf("%s\n", TYPE_NAME(42));       /* int */
    printf("%s\n", TYPE_NAME(42u));      /* unsigned int */
    printf("%s\n", TYPE_NAME(1.5));      /* double */
    printf("%s\n", TYPE_NAME("Pune"));   /* char * */
    printf("%s\n", TYPE_NAME(c + 1));    /* int */
    printf("%s\n", TYPE_NAME(1.5f));     /* other */
    return 0;
}

51. How do you make sure a struct used for a binary protocol or register map has the layout you expect?

Use fixed-width types from stdint.h, order the fields so each is naturally aligned, and lock the layout with C11 static_assert checks on sizeof and offsetof, so a compiler or flag change fails the build instead of corrupting data. GCC's __attribute__((packed)) removes padding, but it is not standard, it forces slower byte-wise access on CPUs without unaligned loads, and a pointer to a packed member can be misaligned, which can fault on such CPUs. Even a perfect layout does not fix byte order, so convert multi-byte fields with explicit shifts at the boundary.

Example
#include <assert.h>
#include <stddef.h>
#include <stdint.h>

struct frame_hdr {
    uint8_t  type;
    uint8_t  flags;
    uint16_t length;
    uint32_t seq;
};

static_assert(sizeof(struct frame_hdr) == 8, "size 8");
static_assert(offsetof(struct frame_hdr, seq) == 4, "seq at 4");

52. How do you serialise a 32-bit value in a fixed byte order regardless of the CPU's endianness?

Build the bytes with shifts on unsigned values, never by copying a struct or casting a pointer. Shifts work on values, not on memory layout, so put_be32 writes the same four bytes, 12 34 56 78, on a little-endian x86 PC and on a big-endian controller. Most network protocols use big-endian, also called network byte order. When decoding, cast each byte to uint32_t before shifting: an unsigned char is promoted to int, and shifting a byte of 0x80 or more left by 24 overflows int, which is undefined. At -O2 GCC recognises get_be32 and emits a load plus one byte-swap instruction.

Example
#include <stdio.h>
#include <stdint.h>

void put_be32(unsigned char *out, uint32_t v) {
    out[0] = (unsigned char)(v >> 24);
    out[1] = (unsigned char)(v >> 16);
    out[2] = (unsigned char)(v >> 8);
    out[3] = (unsigned char)v;
}

uint32_t get_be32(const unsigned char *in) {
    return (uint32_t)in[0] << 24 | (uint32_t)in[1] << 16 |
           (uint32_t)in[2] << 8  | (uint32_t)in[3];
}

int main(void) {
    unsigned char b[4];
    put_be32(b, 0x12345678u);
    printf("%02X %02X %02X %02X\n", b[0], b[1], b[2], b[3]);
    printf("0x%08X\n", (unsigned)get_be32(b));   /* 0x12345678 */
    return 0;
}

53. What is the difference between static and shared libraries, and why does the order of -l options matter?

A static library (.a) is an archive of object files; the linker copies the members you need into the executable, so it runs without the library, but each program has its own copy and needs relinking for a fix. A shared library (.so), built with -fPIC, is loaded at run time and shared between processes, so one update fixes every program, at the cost of a run-time dependency and versioning. GNU ld reads inputs left to right and takes from an archive only symbols still undefined at that point, as --as-needed (Ubuntu's default) also does for .so files, so put -lgst after main.c.

54. Describe the memory layout of a C program. What happens before main runs?

Machine code lives in .text, constants and string literals in .rodata, initialised globals and statics in .data, and zero or uninitialised ones in .bss, which takes no space in the file. Locals and return addresses live on the stack, and malloc memory on the heap. Before main, startup code runs: .bss is zero-filled (by the loader on Linux, by the reset handler on a microcontroller), on a microcontroller the initial values of .data are copied from flash to RAM, init functions run, and main is called. That is why globals start at zero, and why const tables can stay in flash.

55. A variable changes to a wrong value somewhere in a large C codebase. How do you track down memory corruption?

Catch the first bad write, not its later symptom. Rebuild with -fsanitize=address: it reports heap, stack and global buffer overflows and use-after-free at the faulting line, with the allocation and free stacks. -fsanitize=undefined adds checks such as signed overflow and misaligned access. If you know which variable is damaged, a gdb watchpoint (watch -l var) stops at the instruction that writes it. Valgrind needs no rebuild and also finds reads of uninitialised memory. Then check the usual suspects: off-by-one loops, no room for '\0' and unbounded sprintf.

56. Why can summing a 2D array column by column be several times slower than row by row?

C stores arrays row by row, and the CPU loads memory in cache lines, typically 64 bytes or eight doubles. Row order uses every byte of each line it loads, and the hardware prefetcher sees the steady stride and fetches ahead. Column order uses one double per line and jumps N * 8 bytes, 16 KB here, between accesses, so most loads miss the cache and wait for RAM. Same work, same big-O, very different speed. Keep the last index in the innermost loop, lay out data by how it is read together, for example a struct of arrays for hot fields, and measure with perf stat before and after.

Example
#define N 2048
static double m[N][N];

double sum_rows(void) {          /* cache-friendly */
    double s = 0;
    for (int r = 0; r < N; r++)
        for (int c = 0; c < N; c++)
            s += m[r][c];        /* consecutive addresses */
    return s;
}

double sum_cols(void) {          /* jumps N * 8 bytes */
    double s = 0;
    for (int c = 0; c < N; c++)
        for (int r = 0; r < N; r++)
            s += m[r][c];
    return s;
}

57. What may you safely do inside a signal handler in C?

Very little. A signal can arrive between any two instructions, even inside a printf or malloc that holds a lock or a half-updated buffer, so calling them from the handler can deadlock or corrupt state. Standard C only allows a handler to assign to a volatile sig_atomic_t object or use lock-free atomics, call signal for the same signal, or call abort, _Exit or quick_exit. POSIX adds a list of async-signal-safe functions such as write. The robust pattern is to set a flag in the handler and do the real work, logging and cleanup, in the main loop. On Linux prefer sigaction to signal.

Example
#include <signal.h>
#include <stdio.h>

static volatile sig_atomic_t stop_requested = 0;

static void on_sigint(int sig) {
    (void)sig;
    stop_requested = 1;       /* the only work done here */
}

int main(void) {
    signal(SIGINT, on_sigint);
    raise(SIGINT);            /* simulate Ctrl+C */
    if (stop_requested)
        printf("shutting down cleanly\n");
    return 0;
}

58. How do you handle errors and release resources cleanly in C, which has no exceptions?

Return an error code from every function that can fail, check it at every call, and release resources in the reverse order of acquisition. The cleanest way to do that in C is one exit path with goto labels: each failure jumps to the label that undoes only what has been acquired so far, so nothing leaks and nothing is freed twice. The Linux kernel uses this style throughout. Jump only forward, and initialise pointers to NULL so a shared cleanup can free them safely. Report details through errno or an error enum, and never ignore the return values of fopen, malloc and fread.

Example
#include <stdio.h>
#include <stdlib.h>

int load_report(const char *path) {
    int rc = -1;
    char *buf = NULL;
    FILE *fp = fopen(path, "rb");
    if (fp == NULL)
        goto out;
    buf = malloc(4096);
    if (buf == NULL)
        goto out_close;
    if (fread(buf, 1, 4096, fp) == 0)
        goto out_free;
    rc = 0;                   /* success */
out_free:
    free(buf);
out_close:
    fclose(fp);
out:
    return rc;
}

59. Design a ring buffer that passes bytes from a UART interrupt to the main loop. What makes it safe without a lock?

Use a single-producer, single-consumer design: the ISR is the only writer of head and the main loop the only writer of tail, so no index is written by both sides and no lock is needed. Keep head and tail as free-running unsigned counters: h - t is the fill level even after they wrap, full and empty are easy to tell apart, and a power-of-two size turns the modulo into a cheap mask. Write the byte before publishing the new head; C11 atomics guarantee that order. On a single-core MCU without atomics, volatile plus a compiler barrier is the classic substitute. Two producers need a lock.

Example
#include <stdatomic.h>
#include <stdint.h>

#define RB_SIZE 8u                 /* power of two */
static uint8_t buf[RB_SIZE];
static atomic_uint head, tail;     /* free-running counters */

int rb_put(uint8_t b) {            /* only the ISR calls this */
    unsigned h = head, t = tail;
    if (h - t == RB_SIZE)
        return 0;                  /* full: drop the byte */
    buf[h & (RB_SIZE - 1)] = b;
    head = h + 1;                  /* publish after writing */
    return 1;
}

int rb_get(uint8_t *b) {           /* only main calls this */
    unsigned t = tail, h = head;
    if (h == t)
        return 0;                  /* empty */
    *b = buf[t & (RB_SIZE - 1)];
    tail = t + 1;
    return 1;
}

60. How do you hide a struct's internals from the users of a C library?

Declare the struct in the header without its members: typedef struct wallet wallet. It is an incomplete type, so callers can hold and pass wallet pointers but cannot see the fields, declare one on the stack, or take its size. The library defines the struct in its .c file and offers functions to create, use and free it. This keeps the invariants under the library's control and lets you add fields without recompiling callers, so the ABI stays stable; SQLite's sqlite3 handle works this way. The cost is a heap allocation per object and a function call for each field access.

Example
/* wallet.h: users see only an incomplete type */
typedef struct wallet wallet;
wallet *wallet_new(long opening_paise);
int     wallet_debit(wallet *w, long paise);
long    wallet_balance(const wallet *w);
void    wallet_free(wallet *w);

/* wallet.c: only this file knows the layout */
#include <stdlib.h>
#include "wallet.h"

struct wallet { long paise; unsigned version; };

wallet *wallet_new(long opening_paise) {
    wallet *w = malloc(sizeof *w);
    if (w != NULL) {
        w->paise = opening_paise;
        w->version = 1;
    }
    return w;
}

Practice questions for November 2026

New every month

3 questions from the C mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.

  1. FresherData Types and Operators

    What does this code print?

    int r = 2 + 3 * 4 - 6 / 2;
    printf("%d\n", r);
    • 17
    • 7
    • 14
    • 11
    Show the answer

    Answer: 11. * and / bind tighter than + and -, so 3 * 4 = 12 and 6 / 2 = 3 are computed first. Then the additive operators run left to right: 2 + 12 - 3 = 11. Reading strictly left to right would give a wrong answer.

  2. Intermediate (1-5 years)Structures and Unions

    On 64-bit Linux with GCC, what does this code print?

    struct rec1 {
        char flag;
        int id;
        char grade;
    };
    struct rec2 {
        int id;
        char flag;
        char grade;
    };
    printf("%zu %zu\n", sizeof(struct rec1), sizeof(struct rec2));
    • 12 12
    • 8 12
    • 6 6
    • 12 8
    Show the answer

    Answer: 12 8. int needs 4-byte alignment. In rec1, 3 padding bytes follow flag and 3 more follow grade so the size is a multiple of 4: 12. In rec2 the two chars share the tail after id, giving 8. Ordering members by size saves memory.

  3. Experienced (5+ years)Bitwise Operations

    What does this code print?

    uint32_t x = 0x12345678;
    uint32_t y = (x >> 24) | ((x >> 8) & 0xFF00) |
                 ((x << 8) & 0xFF0000) | (x << 24);
    printf("%08X\n", (unsigned)y);
    • 78563412
    • 12345678
    • 87654321
    • 34127856
    Show the answer

    Answer: 78563412. Each term moves one byte to the mirror position: the top byte down, the bottom byte up, and the middle two swap. So 12 34 56 78 becomes 78 56 34 12. This is a byte swap (like ntohl on little-endian); GCC has __builtin_bswap32.

Take the full C mock test

Ready to apply?

There are 219 open jobs asking for C on CoreHRx right now. Create a free profile once and apply with it.