Programming in C

NET's Programming-in-C sub-item is broad: tokens and identifiers, sequence and subprogram control, arrays, structures and unions, strings, pointers, functions, file handling, command-line arguments and the preprocessor. The course's Programming and Data Structures chapter uses C for recursion, pointers and array traces; this chapter teaches the language itself, in full, the way NET asks it.

Tokens, Identifiers and Control Flow

  • C's tokens are keywords, identifiers, constants, string literals, operators and punctuators; an identifier's SCOPE (block, function, file/global) and STORAGE CLASS (auto, register, static, extern) are independent properties — static inside a function extends a local variable's LIFETIME to the whole program while leaving its scope local, which is the classic trap this sub-item tests.
  • Sequence control: if-else, switch (which falls through cases lacking a break — a classic trap), while, do-while (body runs at least once), for. Subprogram control: functions with call-by-value semantics for ordinary parameters (the callee gets a COPY; changes do not propagate back) — the ONLY way to affect a caller's variable is to pass its address explicitly and dereference the pointer inside the callee.

Arrays, Structures, Unions and Strings

A C array's name, in almost every expression context, DECAYS to a pointer to its first element — arr[i] is defined as *(arr + i), which is why array indexing and pointer arithmetic give identical results and sizeof(arr) (the whole array's byte size) differs sharply from sizeof(ptr) once arr has decayed into a function parameter (there it is just a pointer's own size). A structure groups named, independently-stored fields — its total size is generally at least the sum of its members' sizes, and often more, because of padding the compiler inserts for each member's own alignment requirement. A string in C is not a distinct type at all: it is a char array terminated by a '\0' (NUL) byte, so every string function (strlen, strcpy, strcat) scans until it finds that terminator, and forgetting to leave room for it is a standard buffer-overflow source.

⚠️ sizeof an array parameter is a classic trap
Inside the function it is declared in, int arr[10] has sizeof(arr) == 40 (on a 4-byte int). Once passed as a function PARAMETER, void f(int arr[10]) is silently rewritten by the compiler to void f(int *arr), and sizeof(arr) inside f gives the pointer's own size (commonly 8 on a 64-bit system) — the array's original length is simply gone unless passed separately.

Pointers and Functions

  • A pointer variable holds an address; *p dereferences it (accesses the pointed-to value), &x takes the address of x. Pointer arithmetic is scaled by the pointed-to type's size: p + 1 on an int* advances by sizeof(int) bytes, not by 1 byte — this is what makes array indexing via pointers correct regardless of element size.
  • A function pointer holds the address of a function's code and can be called through, assigned or passed as a callback — the mechanism behind qsort's comparator argument and, more generally, behind any C-style plug-in dispatch table.
  • Dynamic memory (malloc/calloc/realloc/free) is allocated on the heap and outlives the function that allocated it — the classic bugs are a memory leak (forgetting free), a dangling pointer (using memory after it is freed) and a double free (freeing the same block twice).

File Handling, Command-Line Arguments and the Preprocessor

  • File handling: fopen returns a FILE* (NULL on failure — always checked before use); fread/fwrite do binary I/O, fscanf/fprintf do formatted text I/O; fclose flushes buffered output and releases the handle, and forgetting it can lose buffered data that was never actually written to disk.
  • Command-line arguments arrive in main's parameters int argc, char *argv[] — argc is the count including the program name itself (so argv[0] is the program's own name, and the first real user argument is argv[1]).
  • Preprocessor directives (#define, #include, #ifdef/#ifndef, macros with parameters) run BEFORE compilation proper, doing pure textual substitution — a macro is not type-checked and not scoped like a function, which is why a parenthesised macro #define SQ(x) ((x)*(x)) needs those parentheses to avoid silently wrong expansions like SQ(a+b) without them.

Key takeaways

  • Storage class (auto/static/extern/register) and scope are independent — static inside a function extends lifetime to the whole program while keeping scope local.
  • C is strictly call-by-value; affecting a caller's variable requires passing its address and dereferencing inside the callee.
  • An array parameter silently becomes a pointer — sizeof gives the array's true size only inside the function it was declared in, never after being passed as a parameter.
  • A C string is a char array terminated by NUL, not a distinct type; forgetting room for the terminator is a standard buffer-overflow source.
  • The preprocessor does pure textual substitution before compilation — an unparenthesised macro parameter can silently produce a wrong expansion.

Practice questions (8)

Attempt each one before opening the answer. Every explanation names the tempting wrong option as well as the right one, because that is where marks are lost.

  1. A variable declared static inside a C function has:

    1. Local scope but a lifetime extending across the whole program
    2. Global scope and local lifetime
    3. Both global scope and global lifetime
    4. Neither scope nor lifetime beyond the statement it is declared in
    Show answer

    Answer: A — Local scope but a lifetime extending across the whole program

    static changes lifetime (the variable retains its value across calls, allocated once for the program's whole run) without changing scope, which stays confined to the function it is declared in.
  2. In C, to make a function change the value of a caller's variable, the function must:

    1. Receive the variable's address (a pointer) as a parameter and dereference it
    2. Simply declare the parameter with the same name
    3. Nothing special — C passes all variables by reference automatically
    4. Use a global variable exclusively
    Show answer

    Answer: A — Receive the variable's address (a pointer) as a parameter and dereference it

    C is strictly call-by-value; an ordinary parameter is a copy. Passing the variable's address explicitly and writing through the dereferenced pointer inside the callee is the only way to affect the caller's own storage.
  3. Given int arr[10]; declared and used within the SAME function, sizeof(arr) (with a 4-byte int) equals:

    1. 40
    2. 4
    3. 8
    4. 10
    Show answer

    Answer: A — 40

    Within its own declaring function, an array has not yet decayed to a pointer, so sizeof gives the array's TOTAL byte size: 10 elements × 4 bytes = 40. This changes to a pointer's own size (commonly 8) only once arr is passed as a function parameter.
  4. A union in C, compared to a structure with the same fields:

    1. Overlays all fields on the same memory, sized to its largest member
    2. Allocates separate memory for every field simultaneously, like a structure
    3. Cannot hold more than one type of field ever
    4. Is always exactly the same size as the equivalent structure
    Show answer

    Answer: A — Overlays all fields on the same memory, sized to its largest member

    A union's size is that of its largest member, since every field shares the same storage; a structure's size is at least the sum of all its members' sizes (plus any alignment padding).
  5. In C, a string is terminated by:

    1. A NUL byte ('\0')
    2. A newline character always
    3. A length field stored before the characters
    4. Nothing; C strings have no terminator
    Show answer

    Answer: A — A NUL byte ('\0')

    Unlike a length-prefixed string representation, C represents a string purely as a char array ending in the NUL byte, and every standard string function scans for that byte to know where the string ends.
  6. In main(int argc, char *argv[]), argv[0] holds:

    1. The program's own name/invocation path
    2. The first user-supplied argument
    3. Always NULL
    4. The total argument count
    Show answer

    Answer: A — The program's own name/invocation path

    argc counts the program's own name as one of the arguments, so argv[0] is that name/path and the first genuine user-supplied argument is argv[1] — a common off-by-one trap in loops that walk argv.
  7. Which of the following are classic dynamic-memory bugs in C? (More than one option may be correct.)

    1. Memory leak — forgetting to free allocated memory
    2. Dangling pointer — using memory after it has been freed
    3. Double free — freeing the same block twice
    4. Static typing — declaring a variable's type at compile time
    Show answer

    Answer: A — Memory leak — forgetting to free allocated memory; B — Dangling pointer — using memory after it has been freed; C — Double free — freeing the same block twice

    Memory leaks, dangling pointers and double frees are the three classic manual-memory-management bugs; static typing is an unrelated, ordinary and correct feature of C's type system, not a bug.
  8. The preprocessor directive used to conditionally compile code only if a macro is NOT already defined is #ifndef or, alternatively, #if !defined(...). Name the shorter of these two directive keywords (without the leading #).

    Numerical answer — type the value.

    Show answer

    Answer: ifndef

    #ifndef ("if not defined") is the standard directive used for header guards, preventing a header's contents from being included more than once in the same translation unit.