Skip to main content
C advanced Lesson 23 of 23

C Interview Preparation: Top 30 Questions

Master the top 30 C interview questions with detailed answers and working code examples.

Q1: What is the difference between char *s and char s[]?

Both declare something to hold a string, but they differ in where the string lives and whether you can modify it. This distinction matters for both correctness and security.

char *s  = "hello";    /* pointer to a string literal stored in read-only memory */
char  s[] = "hello";   /* array: writable copy of the literal on the stack */

/* s[0] = 'H'; */   /* CRASH if using char *s — modifying read-only memory */
s[0] = 'H';          /* OK if using char s[] — modifies the writable copy */

char *s stores a pointer to the literal. char s[] copies the literal into a writable array. Use const char *s when you don’t need to modify it.


Q2: What is a dangling pointer?

A dangling pointer refers to memory that has already been freed or gone out of scope. Using it is undefined behavior — the memory may have been reallocated and now belongs to something else.

int *dangling(void) {
    int x = 42;
    return &x;   /* x is destroyed when function returns — caller gets garbage */
}

int *p = malloc(sizeof(int));
free(p);
*p = 5;   /* dangling — undefined behavior; allocator may have reused this memory */

Fix: set pointers to NULL after free, and never return pointers to local variables.


Q3: What is the output of this code?

Understanding pre- vs post-increment is a classic interview topic. The key is that a++ evaluates to the current value before incrementing, while ++b increments first then evaluates.

#include <stdio.h>
int main(void) {
    int a = 5, b = 10;
    printf("%d %d\n", a++, ++b);
    printf("%d %d\n", a, b);
    return 0;
}

Output:

5 11
6 11

a++ returns 5 (then a becomes 6). ++b increments to 11 then returns 11. After the first printf, a is 6, b is 11.


Q4: What is the difference between const int *p, int * const p, and const int * const p?

The position of const relative to * determines whether the pointer or the pointed-to value is read-only. Read declarations right-to-left: “p is a [const] pointer to [const] int.”

int x = 10, y = 20;

const int *p = &x;     /* pointer to const int: *p is read-only, p can be redirected */
p = &y;                /* OK — p can point elsewhere */
/* *p = 5; */          /* ERROR — cannot modify through p */

int * const q = &x;   /* const pointer to int: p is fixed, *q can be modified */
/* q = &y; */         /* ERROR — q cannot be redirected */
*q = 5;               /* OK — modifies x */

const int * const r = &x;  /* both are fixed */
/* *r = 5; */              /* ERROR */
/* r = &y; */              /* ERROR */

Q5: Implement strlen without using <string.h>.

Walking a pointer to the null terminator and subtracting the start pointer gives the length. This also demonstrates pointer arithmetic.

size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') p++;   /* advance until we find the null terminator */
    return (size_t)(p - s);   /* distance in elements, not bytes */
}

Q6: What is the difference between malloc and calloc?

Both allocate heap memory, but calloc zero-initializes it. The difference matters for data structures that rely on zero as a default value — a hash table’s bucket array must be all-NULL pointers before use.

/* malloc: allocates n bytes — contents are uninitialized (garbage) */
int *a = malloc(10 * sizeof(int));

/* calloc: allocates count*size bytes — all bytes set to zero */
int *b = calloc(10, sizeof(int));

/* calloc is equivalent to: */
int *c = malloc(10 * sizeof(int));
memset(c, 0, 10 * sizeof(int));

calloc is slightly slower (due to zeroing) but safer for data structures that rely on zero-initialization.


Q7: Swap two integers without a temporary variable.

The XOR swap is a classic trick that works by exploiting the properties of XOR. It must handle the case where a and b point to the same location — XOR-ing a value with itself produces zero, destroying the data.

void swap_xor(int *a, int *b) {
    if (a == b) return;   /* required: XOR swap destroys data if a == b */
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

/* Arithmetic swap — may overflow for large values */
void swap_arith(int *a, int *b) {
    *a = *a + *b;
    *b = *a - *b;
    *a = *a - *b;
}

In practice, always use a temporary — it’s clearer, safer, and compiles to the same code.


Q8: What is undefined behavior? Give three examples.

Undefined behavior (UB) means the C standard places no requirements on what the program does. The compiler may assume UB never happens and optimize accordingly, producing silent data corruption or security vulnerabilities that are extremely hard to debug.

/* 1. Signed integer overflow */
int x = INT_MAX;
x++;   /* UB — may wrap, may not, may do something entirely unexpected */

/* 2. Dereferencing a null or invalid pointer */
int *p = NULL;
*p = 5;   /* UB — usually SIGSEGV */

/* 3. Reading an uninitialized variable */
int n;
printf("%d\n", n);   /* UB — value is unpredictable */

/* 4. Out-of-bounds array access */
int arr[5];
arr[10] = 1;   /* UB — corrupts whatever memory is at that address */

/* 5. Use-after-free */
int *q = malloc(sizeof(int));
free(q);
*q = 3;   /* UB — may corrupt allocator metadata */

Q9: What does static mean inside a function vs at file scope?

static has two distinct meanings depending on context. Both involve storage in the data segment (not the stack), but they differ in scope.

void counter(void) {
    static int count = 0;   /* persists between calls — initialized only once */
    printf("%d\n", ++count);
}

static int file_private = 0;   /* internal linkage — not visible to other .c files */
static void helper(void) { }   /* same: hidden from other translation units */

Q10: Reverse a string in-place.

The two-pointer technique walks inward from both ends, swapping characters until the pointers meet in the middle. It works in O(n) time with O(1) extra space.

#include <string.h>

void reverse_string(char *s) {
    int left = 0, right = (int)strlen(s) - 1;
    while (left < right) {
        char tmp  = s[left];
        s[left++] = s[right];
        s[right--] = tmp;
    }
}

Q11: What is a memory leak and how do you detect it?

A memory leak occurs when heap memory is allocated but never freed. The process’s RSS (resident set size) grows over time until it is killed by the OOM killer or crashes.

Detection tools:

  • Valgrind: valgrind --leak-check=full ./prog
  • AddressSanitizer: compile with -fsanitize=address
  • LeakSanitizer: compile with -fsanitize=leak

Q12: Write a function to check if a number is a power of 2.

Powers of 2 have exactly one bit set in their binary representation. The trick n & (n-1) clears the lowest set bit — if the result is zero, there was only one bit set.

#include <stdbool.h>

bool is_power_of_two(unsigned int n) {
    return n > 0 && (n & (n - 1)) == 0;
}

/* Examples:
   8  = 00001000, 7  = 00000111, 8&7  = 0 — yes, it's a power of 2
   6  = 00000110, 5  = 00000101, 6&5  = 4 — no
   1  = 00000001, 0  = 00000000, 1&0  = 0 — yes (2^0)
*/

Q13: What is the difference between struct and union?

In a struct, each member occupies its own memory. In a union, all members share the same memory region — its size equals its largest member. Only one member holds a valid value at any time.

struct S { int i; double d; };   /* sizeof = 4 + 4(pad) + 8 = 16 */
union  U { int i; double d; };   /* sizeof = 8 (largest member only) */

/* In a struct, each member has its own storage — all are valid simultaneously */
/* In a union, all members overlap — writing one invalidates all others */

Q14: Implement atoi (string to integer).

This tests whether you handle edge cases: leading whitespace, optional sign, non-digit characters after the number.

int my_atoi(const char *s) {
    while (*s == ' ' || *s == '\t') s++;   /* skip leading whitespace */

    int sign = 1;
    if (*s == '-') { sign = -1; s++; }
    else if (*s == '+') { s++; }

    int result = 0;
    while (*s >= '0' && *s <= '9') {
        result = result * 10 + (*s - '0');   /* shift left in decimal and add digit */
        s++;
    }
    return sign * result;
}

Q15: What is function pointer syntax?

Function pointers store the address of a function for indirect calls. They enable callbacks, dispatch tables, and plugin architectures — and they are exactly what qsort and bsearch use for their comparators.

/* Declare a pointer to a function that takes int and returns int */
int (*fp)(int);

/* Assign it */
int square(int x) { return x * x; }
fp = square;

/* Call it */
int result = fp(5);   /* 25 */

/* typedef for readability — preferred in production code */
typedef int (*MathFunc)(int);
MathFunc ops[] = {square, /* ... */};

Q16: What is sizeof and what are its limitations?

sizeof gives the size in bytes at compile time. Its limitation is that it cannot determine the length of a dynamically allocated array or an array parameter that has decayed to a pointer.

int arr[10];
printf("%zu\n", sizeof(arr));     /* 40 — full array size */
printf("%zu\n", sizeof(int));     /* 4 */

void func(int arr[]) {
    printf("%zu\n", sizeof(arr));  /* 8 (pointer size) — array has decayed! */
}

sizeof cannot determine the length of a dynamically allocated array or a passed array parameter. Always pass the length separately.


Q17: What is the volatile keyword used for?

volatile tells the compiler that a variable may change at any time outside the normal program flow — for example, a hardware register, a variable modified by a signal handler, or shared memory. Without it, the compiler might cache the value in a register and miss updates.

volatile int sensor_value;   /* re-read from the actual memory location on every access */

/* Without volatile, the compiler may optimize this into an infinite loop
   by caching sensor_value in a register and never re-reading it */
while (sensor_value == 0) { }

Use volatile for memory-mapped hardware registers, shared variables modified by signal handlers, and variables shared with interrupt service routines.


Q18: Find the middle of a linked list in one pass.

The tortoise-and-hare technique uses two pointers moving at different speeds. When the fast pointer reaches the end, the slow pointer is at the middle. This avoids a two-pass solution that first counts the length.

typedef struct Node { int data; struct Node *next; } Node;

Node *find_middle(Node *head) {
    Node *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;          /* moves 1 step per iteration */
        fast = fast->next->next;    /* moves 2 steps per iteration */
    }
    return slow;   /* when fast reaches end, slow is at the middle */
}

Q19: What is a segmentation fault?

A segfault is SIGSEGV — a signal sent by the OS when a process accesses memory it is not allowed to access. It is the OS’s way of stopping a program that has gone off the rails.

Common causes:

  • Dereferencing NULL or an uninitialized pointer
  • Stack overflow (infinite recursion, huge local array)
  • Buffer overflow writing past array bounds
  • Use-after-free

Debug with: gcc -g -o prog prog.c && gdb ./prog then run and backtrace.


Q20: Detect a cycle in a linked list.

Floyd’s tortoise and hare algorithm detects cycles in O(n) time with O(1) space. If there is a cycle, the fast pointer will eventually lap the slow pointer and they will meet.

#include <stdbool.h>
typedef struct Node { int data; struct Node *next; } Node;

bool has_cycle(Node *head) {
    Node *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) return true;   /* they met — cycle detected */
    }
    return false;   /* fast reached NULL — no cycle */
}

Q21: What is the difference between ++i and i++?

Pre-increment returns the new value; post-increment returns the old value then increments. In a standalone statement they compile identically. The difference only matters when the expression’s value is used.

int i = 5;
int a = ++i;   /* pre-increment: i becomes 6, then a = 6 */
int b = i++;   /* post-increment: b = 6, then i becomes 7 */

Q22: What is #pragma pack and when would you use it?

#pragma pack disables the compiler’s normal padding between struct members. This is necessary when mapping a struct to a binary format defined by an external standard (network protocol, file format, hardware register) where exact byte layout is specified.

#pragma pack(push, 1)
struct NetworkHeader {
    uint8_t  type;      /* 1 byte */
    uint16_t length;    /* 2 bytes */
    uint32_t checksum;  /* 4 bytes */
};  /* exactly 7 bytes — no padding inserted */
#pragma pack(pop)

Use it for network protocols, binary file formats, or hardware registers where the exact byte layout is specified by an external standard. Unaligned access may be slower or fail on some architectures.


Q23: Write a macro to find the number of elements in an array.

This is one of the most useful utility macros in C. It only works on actual arrays — not pointers — so it is important to understand its limitation.

#define ARRAY_LEN(arr)  (sizeof(arr) / sizeof((arr)[0]))

int data[] = {1, 2, 3, 4, 5};
printf("%zu\n", ARRAY_LEN(data));   /* 5 */

/* Warning: does NOT work when the array has decayed to a pointer */
void func(int *arr) {
    ARRAY_LEN(arr);   /* wrong! gives sizeof(pointer)/sizeof(int) */
}

Q24: What are the rules for const correctness?

const correctness means consistently marking pointers as const when a function doesn’t need to modify the pointed-to data. It prevents accidental modifications and documents the function’s contract.

  • A const T * can point to both const T and T objects
  • A T * should not point to a const T (drops the const qualifier)
  • Mark function parameters as const T * when the function does not modify the data
  • Return const T * from functions that return a pointer to data that should not be modified by the caller
/* Correct: takes const pointer (won't modify), returns const pointer */
const char *find_char(const char *s, char c) {
    while (*s && *s != c) s++;
    return *s ? s : NULL;
}

Q25: What happens when you free(NULL)?

Nothing — it is a guaranteed no-op. The C standard explicitly states that free(NULL) has no effect. This means you can safely call free on a potentially-null pointer without a NULL check first.


Q26: What is the output?

Pointer arithmetic and negative indexing are tested here. p[-2] is valid and equivalent to *(p-2).

#include <stdio.h>
int main(void) {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr + 2;   /* p points to arr[2] = 30 */
    printf("%d %d %d\n", *p, *(p-1), *(p+1));
    printf("%d\n", p[-2]);
    return 0;
}

Output:

30 20 40
10

p points to arr[2] (30). p-1 is arr[1] (20). p+1 is arr[3] (40). p[-2] is *(p-2) = arr[0] (10).


Binary search requires a sorted array and reduces the search space in half with each step. The key detail is computing mid as lo + (hi - lo) / 2 rather than (lo + hi) / 2 to avoid integer overflow when lo and hi are large.

int binary_search(const int *arr, int n, int target) {
    int lo = 0, hi = n - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;   /* avoids overflow vs (lo+hi)/2 */
        if      (arr[mid] == target) return mid;
        else if (arr[mid] <  target) lo = mid + 1;
        else                         hi = mid - 1;
    }
    return -1;   /* not found */
}

Q28: What is extern "C" and when is it needed?

extern "C" is C++ syntax that prevents name mangling for the enclosed declarations, making them callable from C. It is used in header files that must work from both C and C++ code — common when writing a library with a C API.

#ifdef __cplusplus
extern "C" {
#endif

/* These declarations use C linkage — no name mangling */
void my_library_init(void);
int  my_library_process(const char *data, int len);

#ifdef __cplusplus
}
#endif

Q29: What is the difference between exit() and return in main?

Both terminate the program with a status code, but they differ in what cleanup they perform and when you’d choose each.

  • return from main flushes stdio buffers, calls atexit handlers, and then calls exit
  • exit() flushes stdio buffers and calls atexit handlers, but does not call C++ destructors for local objects
  • _exit() / _Exit() terminates immediately — no atexit, no buffer flush. Used in a child process after fork to avoid double-flushing the parent’s stdio buffers.

Q30: Explain the concept of “incomplete type” and give an example.

An incomplete type is a type whose size is not yet known — the declaration exists but the definition does not. The most important use is the opaque pointer idiom, which hides implementation details and enables stable ABIs.

/* mylib.h — public header: users see the type but not its internals */
typedef struct MyHandle MyHandle;   /* forward declaration — incomplete type */

MyHandle *myhandle_create(void);
void      myhandle_destroy(MyHandle *h);
int       myhandle_process(MyHandle *h, const char *data);

/* mylib.c — implementation: struct is fully defined here, hidden from users */
struct MyHandle {
    int   fd;
    char  buf[4096];
    int   flags;
};

MyHandle *myhandle_create(void) {
    MyHandle *h = calloc(1, sizeof(MyHandle));
    /* ... */
    return h;
}

Users of the library can hold and pass MyHandle * pointers but cannot access or depend on the internal fields. This is C’s equivalent of a private class — the ABI can change without recompiling user code.

Frequently Asked Questions

What topics are most commonly tested in C interviews?
Pointers and memory management, undefined behavior, bit manipulation, data structures (linked lists, trees), the difference between similar-looking constructs (e.g., const int* vs int* const), and system-level concepts like stack vs heap.
Should I memorize all these answers?
Understand them, don't memorize them. Interviewers probe whether you understand why something works, not just that it does. Be able to explain the reasoning and trade-offs.