Dynamic Memory Allocation, Macros, Scoping and File Handling in C: Worked Examples

Trace four C concepts through code: grow an array without losing ownership, expand macros safely, resolve shadowed names, and process file records by return status.

KnowledgeGate Team

Exam prep & CS education

Updated 23 Aug 20266 min read

Dynamic memory allocation, macros, scoping and file handling often arrive in the same C syllabus, but they act at different stages. Macros transform tokens before compilation, scope decides which declaration a name denotes, allocation manages storage at runtime, and file handling moves data through streams. That mix is why one trace can feel like four unrelated puzzles. The C Programming by Yash Sir: Worked Examples & Traces maps the full language. For these four topics, track token expansion, lexical binding, storage ownership and stream status separately.

Put the Four Topics on One C Execution Map

Track the kind of state that belongs to each stage.

Topic

Stage

State to track

Typical failure

Macro

Preprocessing

Token expansion

Precedence or repeated evaluation

Scoping

Translation and execution

Name binding

Shadowing the intended declaration

DMA

Runtime

Owner, size and lifetime

Leak or dangling pointer

Files

Runtime

Stream, mode, position and return status

Bad read or truncation

Scope is name visibility. Storage duration is object lifetime. Ownership is the duty to release allocated storage. They are separate. In particular, static in a declaration can affect storage duration or linkage, but it does not mean static, or lexical, scoping.

Dynamic Memory Allocation: Allocate, Grow, Use and Free

malloc(n) obtains uninitialised allocated storage. calloc(count, size) allocates and zero-initialises the requested bytes. realloc(ptr, new_size) may grow the block in place or move it. free(ptr) ends that allocation's lifetime. Production code must also check that a count cannot overflow before multiplying it by an element size.

A temporary pointer preserves ownership whether realloc succeeds or fails:

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

int main(void) {
    int *scores = malloc(4 * sizeof *scores);
    if (scores == NULL) {
        return EXIT_FAILURE;
    }

    scores[0] = 12;
    scores[1] = 18;
    scores[2] = 25;
    scores[3] = 31;

    int *tmp = realloc(scores, 6 * sizeof *scores);
    if (tmp == NULL) {
        free(scores);
        return EXIT_FAILURE;
    }
    scores = tmp;
    scores[4] = 40;
    scores[5] = 47;

    int sum = 0;
    for (int i = 0; i < 6; ++i) {
        sum += scores[i];
    }
    printf("%d\n", sum);
    free(scores);
    return EXIT_SUCCESS;
}

The running sum is 12 + 18 = 30, 30 + 25 = 55, 55 + 31 = 86, 86 + 40 = 126, and 126 + 47 = 173. The new elements must be assigned before reading because realloc does not initialise the added region. Directly writing scores = realloc(scores, ...) can replace the only owner with NULL on failure, making the old allocation unreachable. The temporary pointer preserves that owner until success, then free(scores) runs exactly once.

Allocation trace: a four-cell scores array grows through a temporary pointer to six cells summing 173, then is freed.

Macros: Trace the Expansion Before Evaluating It

The unsafe and corrected definitions show the difference:

Unsafe macro

Corrected macro

#define BAD_SQUARE(x) x*x

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

Expansion comes before arithmetic. BAD_SQUARE(2+4) becomes 2+4*2+4; multiplication gives 2+8+4, so the result is 14. SQUARE(2+4) becomes ((2+4) * (2+4)); each sum is 6, so the result is 36.

Parentheses do not fix repeated side effects. SQUARE(i++) modifies i more than once without sequencing, so it has no valid numeric answer. Use side-effect-free arguments, or use static inline int square_int(int x) { return x * x; } when type checking and single evaluation matter. Object-like macros can name tokens such as #define BUFFER_CAPACITY 64, while #ifdef DEBUG selects code conditionally. A macro is token replacement, not a function, and it does not follow ordinary block scope.

Scoping and Storage Duration: Resolve Each Name Lexically

Standard C uses lexical, also called static, scope. Resolve a use of a name from where the function is defined, not from the function that called it.

#include <stdio.h>

int x = 5;

void show(void) {
    printf("%d\n", x);
}

int main(void) {
    int x = 20;
    show();
    {
        int x = 8;
        printf("%d\n", x);
    }
    printf("%d\n", x);
    return 0;
}

The output is 5, then 8, then 20, on separate lines. show denotes the file-scope x = 5; it cannot see its caller's x = 20. Under a hypothetical dynamic-scoping rule, the first line would instead be 20.

Now separate scope from lifetime. In static int calls = 0; inside a function, the name has block scope, but its object has static storage duration and retains its value between calls. A file-scope static name has internal linkage, which is a third axis. A macro has textual reach from its #define to a matching #undef or the end of the preprocessing translation unit. Variable-shadowing rules do not apply to it.

File Handling: Read Two Records and Write a Checked Summary

Assume records.txt contains these complete records:

id

name

marks

17

Asha

72

23

Ravi

81

The read operation, not an EOF guess, controls the loop:

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

int main(void) {
    FILE *in = fopen("records.txt", "r");
    if (in == NULL) {
        perror("records.txt");
        return EXIT_FAILURE;
    }

    int id, marks, count = 0, total = 0, scan_status;
    char name[32];
    while ((scan_status = fscanf(in, "%d %31s %d", &id, name, &marks)) == 3) {
        ++count;
        total += marks;
    }

    int read_failed = ferror(in);
    int malformed = !read_failed && scan_status != EOF;
    int input_close_failed = fclose(in) == EOF;
    if (read_failed || malformed || input_close_failed || count == 0) {
        return EXIT_FAILURE;
    }

    FILE *out = fopen("summary.txt", "w");
    if (out == NULL) {
        perror("summary.txt");
        return EXIT_FAILURE;
    }

    double average = (double) total / count;
    int write_failed = fprintf(out, "count=%d\naverage=%.2f\n",
                               count, average) < 0;
    if (fclose(out) == EOF) {
        write_failed = 1;
    }
    return write_failed ? EXIT_FAILURE : EXIT_SUCCESS;
}

After row one, the state is (count=1, total=72). After row two, it is (count=2, total=153) because 72 + 81 = 153. Therefore 153 / 2 = 76.5, printed as 76.50. summary.txt contains count=2 and average=76.50 on separate lines.

while (!feof(in)) is wrong because EOF becomes set only after a read attempt fails. Test fscanf instead, check ferror, and close the stream. Mode r reads, w writes after truncating existing content, and a writes at the end. Their binary forms are rb, wb and ab; text and binary streams need not behave identically on every platform.

Stream trace: two records read from records.txt with each fscanf checked, then count=2 and average=76.50 written to summary.txt.

Failure Patterns and Their Repairs

Failure

Repair

Overwrite the owner with direct realloc assignment

Store the result in a temporary pointer

Read uninitialised or freed storage

Initialise before reading and maintain one clear owner

Omit macro parentheses or pass i++

Parenthesise fully, use side-effect-free arguments, or call an inline function

Confuse scope with storage duration

Track visibility and lifetime separately

Use while (!feof(fp)), skip fopen checks, or choose the wrong mode

Drive the loop from the input result, check every open, and remember that w replaces while a appends

A double free and any dereference after free are invalid operations, not style problems. For files, a checked return status is part of the data flow.

How GATE and Interviews Test This Cluster

Common task shapes are predictable even though no frequency claim is needed: expand a macro and apply precedence, predict which declaration a name denotes, find a leak or dangling pointer, or count the records accepted by a stream loop. The MCQ, MSQ or NAT GATE question types guide helps place such traces in their answer formats, while GATE course and preparation guidance provides a wider study sequence.

Use this rapid check: the safe square of 2+4 is 36; show() prints 5; the six-element allocation sums to 173; and the file loop accepts 2 records with average 76.50. Before choosing an answer, write the intermediate expansion, binding, owner or return status.

The Short Version and the Next Practice Step

Own allocated memory: preserve its only owner until realloc succeeds. Expand macros: parenthesise replacements and avoid side effects. Bind names lexically: keep scope, linkage and storage duration separate. Check stream operations: drive every loop from the read result.

For a full lesson and practice sequence, use the C Language Course. Then use the wider GATE category for subject-level preparation. Re-run all four traces on paper without looking at the answers.