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 50 - Part 2
C

Top 50 - Part 2

Practice the middle 15 questions from a comprehensive set of 50 important C programming interview questions.

1. What is the default access modifier of global variables in C across multiple translation units if declared without static?

Answer: External linkage — the variable is visible program-wide, and other translation units can access it with extern.

A file-scope variable declared without static has external linkage by default:

// file1.c
int counter = 0;            // external linkage
// file2.c
extern int counter;         // refers to file1.c's counter
counter++;

That’s what makes a name usable across translation units. Adding static flips it to internal linkage, hiding the variable from other files. (Contrast C++: a const file-scope variable has internal linkage by default in C++, but not in C — in C, const globals are still external by default unless marked static.) The interview answer: external linkage by default; other translation units reach it via extern.

2. What is the purpose of header guards in C (#ifndef HEADER_H …)?

Answer: They prevent duplicate declaration errors when the same header is included more than once in one compilation unit.

Headers are included textually. If a.h includes c.h and b.h also includes c.h, then including both in a .c file would paste c.h’s contents twice — re-declaring structs, typedefs, enums, and macros → compilation errors. The guard prevents that:

// c.h
#ifndef C_H
#define C_H
struct Point { int x, y; };   // only seen once per TU
#endif

How it works: the first inclusion defines C_H and pastes the body; any later inclusion sees C_H already defined and skips the whole block. (#pragma once is the modern shorthand.) The interview answer: guards make the header idempotent, so multiple #includes of the same file cause no re-declaration errors.

3. What is the evaluation result of evaluating an expression containing sizeof(i++) where int i = 5;?

Answer: i remains 5sizeof is a compile-time operator that does not evaluate its operand (except for VLA operands).

sizeof(expr) only needs the type of the expression; it doesn’t run it. So sizeof(i++):

  • Computes the size of int (the type of i++), typically 4.
  • Never executes the incrementi stays 5.

The only exception: if the operand is a variable-length array (VLA), sizeof must evaluate parts of it (the size expression) at runtime. For everything else, no side effects occur. This is why sizeof(*ptr) is safe even with an uninitialized pointer. The interview answer: i stays 5 — sizeof doesn’t evaluate its operand (barring VLAs).

4. What is a Flexible Array Member in C structs introduced in C99?

Answer: An unsized array as the last member of a struct (e.g., int data[];) that lets you allocate a variable-length payload contiguous with the struct header.

A flexible array member is declared as the last struct member without a size:

struct Packet {
    int length;
    char data[];   // flexible array member
};

You allocate the struct and its payload in one contiguous block:

struct Packet *p = malloc(sizeof(struct Packet) + payload_bytes);
p->length = payload_bytes;
p->data[0] = ...;   // reach the inline payload

Rules: it must be the last member; there must be at least one other named member; the struct is sized as if the array were absent (with trailing padding for alignment). This is the standard way to build header+payload messages with a single allocation and good locality. The interview answer: a trailing unsized array (data[]) enabling contiguous struct-header + variable-payload allocation.

5. What does the string formatting specifier %p expect in printf()?

Answer: A pointer cast to void*, printed as a memory address in hexadecimal.

%p is the pointer-formatting specifier. The argument should be a pointer (properly cast to void* per the standard):

int x = 42;
printf("%p", (void *)&x);   // e.g., 0x7ffeefbff5c0

Output format is implementation-defined but universally hexadecimal with a 0x prefix. Notes: the argument type is a pointer, not a string or integer — passing the wrong type for %p is a format-string bug (undefined behavior). Also, %p prints an address, not the value at the address — use %d/%c/etc. for the pointed-to data. The interview answer: a void* pointer argument, printed as a hexadecimal memory address.

6. What happens when compiling an undefined variable reference in C without declaring it extern or local?

Answer: A compile error — “Undeclared identifier” — or a link error (“unresolved external symbol”) if the declaration exists but the definition doesn’t.

C requires every identifier to be declared before use. If you reference a variable that was never declared:

  • Compile time: “use of undeclared identifier” (or implicit-declaration error).

If you declare it (extern int counter;) but never define it anywhere (no int counter; in any translation unit), then the linker fails: “unresolved external symbol.”

extern int counter;   // declared but never defined anywhere
counter = 1;          // link error: unresolved external

So: missing declaration → compiler error; declaration without definition → linker error. The interview answer: a compile error (undeclared identifier) or a link error (unresolved external symbol) for a declared-but-never-defined variable.

7. What is the outcome of int *ptr = (int*)malloc(0);?

Answer: Implementation-defined — either NULL or a unique non-null pointer, and in both cases the result must still (or may) be passed to free().

Requesting 0 bytes from malloc is legal but the behavior is implementation-defined:

  • Some implementations return NULL (no allocation).
  • Others return a unique non-null pointer to a zero-size region.

free() on either result is safe: free(NULL) is a no-op, and the non-null pointer is a valid malloc result that must be freed to avoid a leak. So regardless of which the platform chooses, free(ptr) is correct. What you must not do is dereference the result as if it had room — a 0-byte region isn’t usable storage. The interview answer: returns NULL or a unique non-null pointer (implementation-defined); either can safely be passed to free.

8. What is the difference between structure field access operators . and ->?

Answer: . accesses a member directly on a struct instance; -> accesses a member through a pointer to a struct — and ptr->member is shorthand for (*ptr).member.

struct Point p;         // instance
p.x = 5;                // . on the instance

struct Point *pp = &p;
pp->x = 7;              // -> dereferences the pointer
(*pp).x = 7;            // equivalent, more verbose

So . needs the struct itself (or a reference to it); -> dereferences the pointer first, then accesses the member. Using . where you have a pointer (or -> where you have an instance) is a compile error. The arrow is just the idiomatic, more readable version of (*ptr).member. The interview answer: . on instances, -> on pointers (dereference + access).

9. What does the C library function system(“command”) execute?

Answer: It passes the string to the host environment’s command processor (/bin/sh or cmd.exe) for execution.

system("cmd") spawns the OS command shell to run the given command, waits for it to finish, and returns its exit status:

system("ls -l");       // runs in the shell, like typing it
system("mkdir /tmp/x");

Details:

  • The exact shell and behavior are platform-defined (POSIX: /bin/sh -c, Windows: cmd.exe /c).
  • The return value is the command’s termination status (interpretable via WEXITSTATUS), or -1 if the shell couldn’t run.
  • It’s blocking — the caller waits for completion.
  • Security caveat: the string is interpreted by the shell, so unsanitized user input in system() is a command-injection vulnerability — prefer direct function calls or exec-family APIs where possible.

The interview answer: it hands the string to the platform’s command shell (/bin/sh/cmd.exe) to execute, returning the exit status.

10. What is a “Dangling Pointer”?

Answer: A pointer that still holds an address after the memory it points to has been freed (or has gone out of scope) — dereferencing it is undefined behavior.

A dangling pointer is stale: it points to memory that no longer belongs to the program’s valid state. How pointers become dangling:

  • Heap: free(ptr) but ptr keeps the old address.
  • Stack: returning a pointer to a local variable — the frame pops and the memory is reused.
  • Reallocation: realloc moves the block; old pointers still hold the old address.
int *p = malloc(sizeof(int));
free(p);        // p now dangles
*p = 5;         // UB — use-after-free

Dereferencing a dangling pointer is undefined behavior (use-after-free). Prevention: set pointers to NULL after free, prefer RAII-style ownership in C++, and be careful with realloc/scope. The interview answer: a pointer referencing memory that has been deallocated; using it is UB.

11. What does the storage class register suggest to the compiler?

Answer: A hint that the variable is heavily accessed and should be kept in a CPU register — though modern compilers generally ignore it.

register tells the compiler “this variable is used often; put it in a register if you can.” In modern optimizing compilers, register allocation is fully automatic, so register is essentially ignored as a hint. Remaining semantic restrictions:

  • You cannot take the address (&var) of a register variable (since it may have no memory address).
  • It applies only to block-scope (and function-parameter) variables, not globals.

The keyword survives mostly for historical/portability reasons and is now largely vestigial in practice. The interview answer: a mostly-ignored hint that the variable should live in a register; taking its address is illegal.

12. What is the evaluation result of applying logical negation !5 in C?

Answer: 0.

C’s logical operators treat any non-zero value as true. ! (logical NOT) flips truth to false and false to truth:

  • !5 — 5 is non-zero (true) → NOT true → 0 (false).
  • !0 — 0 is false → 1 (true).

So !5 evaluates to the integer 0. (It’s a boolean result represented as int in C: always 0 or 1.) The interview answer: 0 — logical NOT of any non-zero value is false.

13. What is the cause of “Undefined Behavior” when using strcpy(dest, src) on overlapping memory strings?

Answer: The C standard explicitly leaves strcpy undefined for overlapping buffers — it copies sequentially, so a copy can overwrite source bytes before they’re read.

strcpy copies characters one by one from src to dest until the null terminator, with no intermediate buffering. If the regions overlap:

  • The destination’s early bytes can overwrite source bytes that haven’t been copied yet.
  • The copied result is corrupt (or worse), and the standard says the behavior is undefined — no guarantee at all.

Example that breaks: strcpy(s + 1, s) — shifting a string right by one — overwrites s[1] (the second char of source) before it’s read.

The fix: memmove for overlapping regions (it copies safely, as if via a temp buffer), or strcpy only for provably disjoint buffers. The interview answer: strcpy copies sequentially with no temporary buffering, so overlapping source/destination is undefined per the standard.

14. What is the purpose of standard library function qsort() in <stdlib.h>?

Answer: It sorts an array of arbitrary elements in place using a caller-supplied comparison callback.

qsort(base, num, size, compar):

  • base — pointer to the array start.
  • num — number of elements.
  • size — byte size of each element.
  • compar — function returning <0, 0, or >0 for the ordering of two elements.
int cmp(const void *a, const void *b) {
    return (*(int *)a) - (*(int *)b);
}
qsort(arr, n, sizeof(int), cmp);

It works on any element type because it only ever moves bytes of size length and asks compar for ordering. The comparator receives const void* pointers, which you cast to the element type. (Worst case is O(n log n); the name is historical — “quick sort.”) The interview answer: in-place array sort of arbitrary element types driven by a user-provided comparison callback.

15. What is the function of clock() in <time.h>?

Answer: It returns the processor (CPU) time consumed by the program since it started, as a clock_t value convertible to seconds via CLOCKS_PER_SEC.

clock() measures CPU time — the amount of processor time the process has used (across all threads), not wall-clock elapsed time. It differs from wall-clock when the program sleeps or waits for I/O (that time isn’t “processing”).

clock_t start = clock();
/* ... work ... */
double secs = (double)(clock() - start) / CLOCKS_PER_SEC;

If the value is (clock_t)(-1), the time is unavailable. For wall-clock time, you’d use time(), gettimeofday, or clock_gettime(CLOCK_MONOTONIC) instead. The interview answer: CPU time since process start (a clock_t), divided by CLOCKS_PER_SEC to get seconds.

My Private Notes

Notes are auto-saved locally to this device.