Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Top 25 - Part 1
C

Top 25 - Part 1

Practice the first 15 questions from a curated set of the top 25 C programming interview questions.

1. What is the behavior of bitwise left shift x << n when x is a signed negative integer in C?

Answer: Undefined behavior (prior to C23) — the C standard does not define left-shifting a negative signed value.

In C standards before C23, the result of x << n is well-defined only when x is non-negative and the result fits in the type. Left-shifting a signed negative value violates that rule, so the behavior is undefined — the program may do anything.

(The C23 standard changed this: it now defines E1 << E2 as the value E1 × 2^E2, even for negative E1, as long as it’s representable. But for older standards and the classic interview answer: it’s UB.)

The reason is historical: signed integer representation was implementation-defined (two’s complement is universal today, but the standard had to cover sign-magnitude and ones’ complement). The interview answer: UB under pre-C23 rules; C23 makes it well-defined for representable results.

2. What does the restrict qualifier tell the compiler when applied to a pointer argument (int * restrict p)?

Answer: That p is the only way to access the object it points to within its scope, enabling aggressive optimization without aliasing worries.

restrict is a promise from the programmer to the compiler: during the lifetime of p, no other pointer in scope accesses the same memory. This is a contract for aliasing — the situation where two pointers refer to the same object, which forces the compiler to be conservative (re-reading from memory after every store).

void copy(int *restrict dst, const int *restrict src, int n) {
    for (int i = 0; i < n; ++i) dst[i] = src[i];
}

With restrict, the compiler can cache src[i] in a register, reorder loads/stores, and apply vectorization, because dst can’t be overwriting src.

The danger: you must uphold the promise. If you pass overlapping buffers anyway, the behavior is undefined. Note restrict is a C keyword (C99+), not C++ (C++ adopted it via __restrict extensions and C++23’s restrict for this). The interview answer: restrict promises no other pointer aliases the same memory in scope, letting the compiler optimize freely.

3. What is the output of the following pointer arithmetic code on a standard system?

int arr[] = {10, 20, 30, 40};
int *p = arr;
printf("%d", *(p + 2));

Output: 30.

Pointer arithmetic is scaled by the pointed-to type’s size. p + 2 advances by 2 * sizeof(int) — two elements, not two bytes. p starts at arr[0]; p + 2 points at arr[2], which is 30. *(p + 2) is equivalent to p[2] (or arr[2]). Output: 30.

4. What occurs when dereferencing a NULL pointer in C?

Answer: Undefined behavior — in practice, typically a crash / segmentation fault (SIGSEGV).

Dereferencing NULL (e.g., *ptr where ptr == NULL, or ptr->field) is undefined behavior per the C standard. On modern OSes with virtual memory, address 0 lies on an unmapped page, so the hardware raises a protection fault that the OS delivers as SIGSEGV, terminating the program.

C has no exceptions — nothing “catches” the fault like a C++ exception. Good practice is to check for NULL before dereferencing (especially on pointers returned by malloc, fopen, etc.). The interview answer: UB — almost always a SIGSEGV crash on OS-managed memory systems.

5. What structural padding issue arises in this struct on 64-bit systems?

struct Data {
    char a;
    double b;
    int c;
};

Answer: Padding bytes are inserted after a (7 bytes) and after c (4 bytes), making the struct 24 bytes with 8-byte alignment.

Members must sit at natural alignment boundaries — a double needs 8-byte alignment:

  • char a at offset 0 (1 byte), then 7 padding bytes.
  • double b at offset 8 (8 bytes, ends at 16).
  • int c at offset 16 (4 bytes, ends at 20).
  • The struct’s total size must be a multiple of its alignment (8, the strictest member), so 4 trailing padding bytes → 24 bytes total.

So sizeof(struct Data) is 24, not 1 + 8 + 4 = 13. The compiler never reorders members (order is guaranteed by the standard); it only inserts padding. Reordering members manually (biggest first) is the classic way to shrink struct sizes. The interview answer: 7 padding after a, 4 after c, struct size 24 with 8-byte alignment.

6. Which header file contains definitions for fixed-width integer types such as uint32_t and int64_t?

Answer: <stdint.h>.

C99 introduced <stdint.h> with portable, fixed-width integer typedefs: int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, plus intptr_t, uintptr_t, and the intN_t “at least N bits” variants. This gives you exact-size types regardless of platform (where plain int is 16 or 32 or 64 bits depending on the architecture).

(C++ uses <cstdint> for the same types.) The interview answer: <stdint.h> (C99) defines the fixed-width integer types.

7. What is the main danger of using gets() in C, leading to its deprecation and removal in C11?

Answer: It reads input without any buffer-length bound, enabling stack buffer overflows — it was removed in C11.

gets(buffer) reads characters until a newline, writing everything into the caller’s buffer with no size limit and no check. If input exceeds the buffer, it writes past the end — a buffer overflow that corrupts adjacent stack memory. Attackers exploit this for stack-smashing / code injection; it’s one of the most infamous unsafe functions in C history.

The standard finally removed gets entirely in C11 (deprecated in C99). The safe replacement is fgets(buffer, size, stdin), which caps input at size - 1 characters. The interview answer: unbounded reads into a fixed buffer → stack overflow vulnerabilities; use fgets instead.

8. What is the evaluation result of 5 / 2 in C?

Answer: 2.

When both operands are integers, / performs integer division: the result is the quotient with the fractional part truncated toward zero. 5 / 2 = 2 (the .5 is dropped).

If you want 2.5, at least one operand must be floating-point: 5.0 / 2, 5 / 2.0, or 5.0 / 2.0 all give 2.5. (Also watch truncation direction for negatives — C truncates toward zero since C99, so -5 / 2 is -2, not -3.) The interview answer: 2 — integer division truncates the fractional part.

9. What is the scope and lifetime of a local variable declared with static inside a function body?

Answer: Block scope (only the function can see it), but lifetime spans the whole program — it keeps its value between calls and is initialized once.

A local static variable combines two properties:

  • Scope: block/function scope — accessible only inside the function where it’s declared (same visibility rules as a normal local).
  • Lifetime: static storage duration — it lives for the entire program run, not just the function call. It’s initialized once (before the program starts), and retains its value across calls.
int next() {
    static int counter = 0;   // initialized once
    return counter++;
}

Each call to next() returns an increasing value — the counter survives between calls. That’s the classic use: persistent state without a global. The interview answer: block scope, program-long lifetime, initialized once, value preserved between calls.

10. What is the standard return value of main() to signal successful execution to the Operating System?

Answer: 0 (or EXIT_SUCCESS).

main() returns an int status to the OS. Returning 0 (or EXIT_SUCCESS from <stdlib.h>) signals success. A non-zero value signals failure or an error code — shells check this via $?.

EXIT_SUCCESS is usually 0 and EXIT_FAILURE is typically 1 (both defined in <stdlib.h>), giving you named constants instead of magic numbers. The interview answer: return 0 or EXIT_SUCCESS to signal successful termination.

11. What happens if you attempt to access an array index out of bounds (int a[5]; a[10] = 50;)?

Answer: Undefined behavior — the write goes into adjacent memory, potentially corrupting other data or crashing.

C performs no runtime bounds checking. a[10] on a 5-element array just computes the address &a[0] + 10*sizeof(int) and writes 50 there — into whatever memory happens to sit after the array (possibly other variables, a return address, or unmapped pages). Consequences are unpredictable:

  • Silently corrupting neighboring data (hard to debug).
  • Crashes when writing into invalid/unmapped memory.
  • Security vulnerabilities (buffer overflow attacks).

The compiler may warn at compile time for constant out-of-bounds indices, but for computed ones nothing catches it. Bounds checking is entirely the programmer’s job in C. The interview answer: UB — the write lands in adjacent memory, corrupting state or crashing.

12. What is the purpose of the #pragma once directive?

Answer: A header guard optimization — it prevents a header file from being included more than once in a single compilation unit.

#pragma once is a preprocessor directive (supported by virtually all modern compilers) that tells the compiler: “only include this file once per translation unit, no matter how many times it’s #included.”

It’s an alternative to the classic include guards:

#ifndef MY_HEADER_H
#define MY_HEADER_H
/* ... */
#endif

#pragma once achieves the same goal with less boilerplate and fewer typo risks (a mismatched #define name in the guard defeats it). Its trade-off: it’s not part of the ISO C standard (hence #pragma), so its support is a compiler-extension convention — though in practice every mainstream compiler supports it. The interview answer: a modern, concise header guard preventing multiple inclusion of a header in one compilation unit.

13. What is the output of the macro expansion SQUARE(1 + 2) defined as #define SQUARE(x) x * x?

Answer: 5 — the classic macro-precedence trap.

Macros are pure textual substitution with no expression-awareness. SQUARE(1 + 2) expands to:

1 + 2 * 1 + 2

Applying normal C operator precedence (* binds tighter than +):

1 + (2 * 1) + 2 = 1 + 2 + 2 = 5

The programmer expected (1 + 2) × (1 + 2) = 9, but got 5. The fix is to parenthesize the parameters in the macro definition:

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

(Full parenthesization also protects against issues when the result is used in further expressions.) The interview answer: 5 — macro substitution ignores precedence, giving 1 + 2 * 1 + 2.

14. What does typedef do in C?

Answer: It creates an alias (new name) for an existing type.

typedef doesn’t create a new kind of type — it introduces a synonym:

typedef unsigned long ulong;
typedef struct { int x, y; } Point;

ulong big = 42;      // unsigned long
Point p = {1, 2};    // the struct

Uses:

  • Readability — names like Point instead of struct {...}.
  • Portability — abstract platform-specific types (size_t, uint32_t) behind one name.
  • Reducing verbositytypedef struct Node { ... } Node; lets you write Node instead of struct Node in C.

Note: typedef names are part of the ordinary identifier namespace (unlike struct tags), so they follow normal scope rules. The interview answer: typedef defines an alias for an existing type.

15. What function is used to compare two null-terminated C strings byte-by-byte?

Answer: strcmp().

strcmp(s1, s2) compares two C strings lexicographically (byte-by-byte by character value):

  • Returns 0 if the strings are equal.
  • Returns negative if s1 < s2.
  • Returns positive if s1 > s2.
if (strcmp(str, "quit") == 0) { /* match */ }

Important: == on char* compares the addresses, not the contents — you must use strcmp for value comparison. (For non-null-terminated or binary data, memcmp; for length-bounded string compare, strncmp.) The interview answer: strcmp() — returns 0 on equality, negative/positive for ordering.

My Private Notes

Notes are auto-saved locally to this device.