C Programming
Chapter: 5.1 C Programming Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Answer all questions. Code must be written from memory; partial credit for correct reasoning. Where asked to explain out loud, write your reasoning in full sentences.
Question 1 — Compilation & memory layout (10 marks)
(a) From memory, list the four stages the file main.c passes through to become an executable, naming the tool for each stage and the artefact it produces (e.g. what a .i, .s, .o file is). (4)
(b) For the following global/local declarations, state in which memory segment each object lives (text, data, BSS, heap, or stack) and why: (6)
int g = 5; // A
static int h; // B
const char *msg = "hello"; // C: classify BOTH the pointer and "hello"
void f(void) {
int local; // D
int *p = malloc(40); // E: classify BOTH p and the 40 bytes
}Question 2 — Type sizes & bitwise derivation (10 marks)
(a) Explain out loud why sizeof(int) is not guaranteed by the C standard and how stdint.h solves the portability problem for a programmer who needs exactly 32 bits. (3)
(b) Given uint8_t x = 0xB3;, compute by hand, showing bits: (4)
x >> 4x & 0x0Fx | 0x0Cx ^ 0xFF
(c) Evaluate the expression 3 + 4 * 2 > 10 && 1 << 2 == 4 step by step using the precedence table, and give the final value. (3)
Question 3 — Pointers, arrays & decay (12 marks)
(a) Given int a[5] = {10,20,30,40,50}; and int *p = a;, give the value of each and justify: *(p+2), *(a+3), p[1], a[4] - *p, &a[2] - &a[0]. (5)
(b) Explain out loud the difference between int arr[10]; and int *ptr = malloc(10*sizeof(int)); in terms of sizeof, decay, and reassignability. (3)
(c) Write a from-scratch function int sum(const int *arr, size_t n) that returns the sum of n integers using pointer arithmetic only (no []). (4)
Question 4 — Dynamic memory & memory errors (12 marks)
(a) For each snippet, name the memory error and explain the consequence: (6)
// (i)
char *s = malloc(10); strcpy(s, "this string is too long");
// (ii)
int *p = malloc(sizeof(int)); free(p); *p = 5;
// (iii)
int *q = malloc(100); q = malloc(200);
// (iv)
int *r = malloc(4); free(r); free(r);(b) Explain out loud what Valgrind reports for a memory leak versus an invalid write, and which line of tooling output points you to the leak's allocation site. (3)
(c) Rewrite this dangerous string code safely using snprintf, and state why the original is unsafe: (3)
char buf[16];
sprintf(buf, "%s-%d", name, id);Question 5 — Structs, unions, function pointers (10 marks)
(a) Declare a struct Packet containing a uint8_t type field, a 3-bit flags bit-field, and a pointer to char. Then, given struct Packet *pp;, show the two syntaxes to access flags. (4)
(b) Explain out loud what a union of { int i; float f; } means at the memory level and why reading f after writing i is problematic. (3)
(c) Declare a function pointer cmp matching int compare(const void*, const void*), assign a function to it, and call it. (3)
Question 6 — Macros, UB & qualifiers (6 marks)
(a) Write a MAX(a,b) macro and give one concrete argument where it misbehaves compared to an inline function. (3)
(b) Give one example each of undefined behaviour caused by (i) signed integer overflow, (ii) reading uninitialised memory, and explain in one line why the compiler is allowed to do "anything." (3)
Answer keyMark scheme & solutions
Question 1 (10)
(a) (4 marks, 1 each)
- Preprocessor (
cpp) — expands#include,#define, macros → produces.i(translation unit). - Compiler (
cc1/gcc front-end) — translates C to assembly → produces.s. - Assembler (
as) — assembly to machine code → produces.o(object file, relocatable). - Linker (
ld) — resolves symbols, combines objects + libraries → executable.
(b) (6 marks, 1 each)
- A
int g = 5;→ data segment (initialised, non-zero global). - B
static int h;→ BSS (uninitialised/zero static; zero-filled at load, no disk cost). - C pointer
msg→ data (initialised global pointer); the string literal"hello"→ text/rodata (read-only). - D
local→ stack (automatic storage, lives inf's stack frame). - E
p→ stack (local variable); the 40 bytes frommalloc→ heap.
Why: segment is determined by storage duration + initialisation; literals are read-only constants.
Question 2 (10)
(a) (3) Standard only mandates minimum widths (int ≥ 16 bits) and sizeof(char)==1; actual size is platform/ABI-dependent (16/32/64). stdint.h gives exact-width types like int32_t/uint32_t that are typedef'd per-platform to guarantee exactly 32 bits, so code is portable.
(b) (4, 1 each) 0xB3 = 1011 0011
x >> 4=0000 1011=0x0B= 11x & 0x0F=0000 0011= 0x03 = 3x | 0x0C=1011 0011 | 0000 1100 = 1011 1111= 0xBF = 191x ^ 0xFF=0100 1100= 0x4C = 76
(c) (3) Precedence: * > << > > > == > &&.
4*2 = 8, so3+8 = 11;11 > 10→1.1<<2 = 4;4 == 4→1.1 && 1→ 1.
(Note: << (7) binds tighter than == (10), so 1 << 2 == 4 parses as (1<<2)==4.)
Question 3 (12)
(a) (5, 1 each)
*(p+2)= 30 (a[2])*(a+3)= 40 (a[3], a decays to &a[0])p[1]= 20 (equals *(p+1))a[4] - *p= 50 − 10 = 40&a[2] - &a[0]= 2 (pointer difference in elements, typeptrdiff_t)
(b) (3) sizeof(arr) = 40 (whole array, 10 ints); sizeof(ptr) = pointer size (8 on 64-bit). arr decays to &arr[0] in expressions but is not an lvalue you can reassign; ptr can be reassigned to point elsewhere. Array memory is fixed at declaration (stack) vs heap for malloc.
(c) (4)
int sum(const int *arr, size_t n) {
int total = 0;
const int *end = arr + n;
while (arr < end)
total += *arr++;
return total;
}Marks: signature/const (1), pointer traversal no [] (2), correct return (1).
Question 4 (12)
(a) (6, 1.5 each)
- (i) Buffer overflow — writing 24 bytes into a 10-byte buffer corrupts heap metadata/adjacent memory → crash or exploit.
- (ii) Use-after-free — dereferencing freed pointer; memory may be reallocated → corruption/UB.
- (iii) Memory leak — original 100-byte block's only pointer is overwritten, unreachable, never freed.
- (iv) Double free — freeing same block twice corrupts allocator's free list → UB/crash.
(b) (3) Leak: "definitely lost: N bytes in M blocks", with a stack trace showing the malloc call site (the allocation stack). Invalid write: "Invalid write of size k" at the offending line with the write's stack trace plus the block it belongs to. The allocation-site trace is what points to where the leaked memory was created.
(c) (3)
char buf[16];
snprintf(buf, sizeof buf, "%s-%d", name, id);Original unsafe: sprintf has no bound — if name is long the output exceeds 16 bytes → buffer overflow. snprintf truncates and always null-terminates within the given size.
Question 5 (10)
(a) (4)
struct Packet {
uint8_t type;
unsigned flags : 3;
char *data;
};Access via pointer (2): pp->flags and (*pp).flags. (struct decl 2)
(b) (3) A union stores all members in the same overlapping memory; its size = largest member. Writing i then reading f reinterprets the int's bit pattern as a float — the value is meaningless (type-punning), and in strict aliasing terms reading the inactive member is implementation-defined/UB territory.
(c) (3)
int (*cmp)(const void*, const void*) = compare;
int r = cmp(&x, &y); /* or (*cmp)(&x,&y) */Marks: correct declaration syntax (2), assignment+call (1).
Question 6 (6)
(a) (3)
#define MAX(a,b) ((a) > (b) ? (a) : (b))Misbehaves with side effects: MAX(i++, j) evaluates i++ twice. An inline function evaluates each argument once. (macro 1, example 2)
(b) (3)
- (i)
INT_MAX + 1— signed overflow is UB. - (ii)
int x; if (x) ...— reading indeterminate value. - Because UB has no defined behaviour in the standard, the compiler may assume it never happens and optimise accordingly (e.g. delete the check), producing "anything."
[
{"claim":"0xB3 >> 4 == 11, &0x0F==3, |0x0C==0xBF, ^0xFF==76", "code":"x=0xB3; result = (x>>4==11) and (x&0x0F==3) and (x|0x0C==0xBF) and (x^0xFF==76)"},
{"claim":"Q2c: 3+4*2>10 && (1<<2)==4 evaluates to 1(True)", "code":"v = ((3+4*2>10) and ((1<<2)==4)); result = (v==True)"},
{"claim":"Q3a pointer results: a[4]-a[0]=40 and &a[2]-&a[0]=2", "code":"a=[10,20,30,40,50]; result = (a[4]-a[0]==40) and (2-0==2)"},
{"claim":"Q1 sum of ints via traversal matches builtin sum", "code":"arr=[3,7,11,4]; total=0; \nfor e in arr: total+=e\nresult = (total==sum(arr))"}
]