C Programming
Level 4 — Application (novel problems, no hints) Time limit: 60 minutes Total marks: 60
Answer all questions. Assume a typical LP64 platform (Linux x86-64) unless stated: char=1, short=2, int=4, long=8, pointer=8 bytes; int is two's-complement 32-bit. Justify every answer.
Question 1 — Struct layout, bit fields & unions (14 marks)
Consider the following declarations on the LP64 platform:
struct Packet {
uint8_t flags : 3;
uint8_t kind : 5;
uint16_t id;
char tag[3];
double value;
};
union Cell {
uint32_t word;
struct { uint8_t b0, b1, b2, b3; } bytes;
float f;
};(a) Compute sizeof(struct Packet), showing the byte offset of each named member and every padding region. State the struct's alignment. (6)
(b) A union Cell u; is assigned u.word = 0x41200000. On a little-endian machine, give the values of u.bytes.b0, u.bytes.b1, u.bytes.b2, u.bytes.b3 (in hex) and the value of u.f. (4)
(c) Explain precisely why reading u.f after writing u.word is well-defined in C (type-punning through a union) but reading through a float* that was cast from a uint32_t* is undefined. Name the rule involved. (4)
Question 2 — Pointers, arithmetic & array decay (12 marks)
int a[3][4] = {{0,1,2,3},{4,5,6,7},{8,9,10,11}};
int *p = &a[0][0];(a) Give the numeric value produced by each of the following expressions and briefly justify each:
*(p + 5), *(*(a + 2) + 1), *(a[1] + 3). (6)
(b) What is the type of a, of a[0], and of &a? For each, state the result type and size (in bytes) of sizeof(a), sizeof(a[0]), sizeof(&a[0]). (3)
(c) int (*q)[4] = a; q += 2; — how many bytes was q advanced, and what does *q now refer to? (3)
Question 3 — Debugging a memory-unsafe function (16 marks)
The function below is meant to return a newly allocated copy of the concatenation prefix + body, uppercasing nothing, just joining. It contains four distinct defects.
char *join(const char *prefix, const char *body) {
char *out = malloc(strlen(prefix) + strlen(body)); /* L1 */
strcpy(out, prefix); /* L2 */
strcat(out, body); /* L3 */
free(out); /* L4 */
return out; /* L5 */
}(a) Identify all four defects. For each, name the class of error (e.g. buffer overflow, use-after-free, memory leak, null dereference) and the line(s) responsible. (8)
(b) Rewrite join correctly and safely using snprintf, ensuring the returned buffer is always null-terminated and that allocation failure is handled. (5)
(c) State exactly what Valgrind (memcheck) would report for the original code if the caller later did puts(join("Hi","!"));. Name at least two distinct error categories it would flag. (3)
Question 4 — Function pointers & callbacks (10 marks)
You are writing a generic array transform:
void map_int(int *arr, size_t n, /* callback here */);(a) Complete the parameter list so map_int accepts a callback taking an int and returning an int, then write the body so each element is replaced by the callback's result. Also write a valid typedef for that function-pointer type. (5)
(b) Given int sq(int x){return x*x;}, write the single call that squares every element of int v[4]={1,2,3,4};. State the array's contents afterwards. (2)
(c) Explain why int *arr decays and can be modified in place here, yet the same behaviour would not occur if the parameter were declared int arr[4] and you relied on sizeof(arr) inside the function. (3)
Question 5 — Integer promotion, precedence & UB (8 marks)
(a) Evaluate and give the resulting value and type of each expression, assuming int is 32-bit. Justify using promotion/precedence rules:
(char)200 + (char)200wherecharis signed 8-bit (2)1 << 31for a 32-bitint— state whether this is defined (2)5 & 3 == 3— give the value and explain the precedence trap (2)
(b) Give one concrete reason the compiler is permitted to assume i never overflows in for (int i = 0; i <= n; i++) and how that assumption can silently break a loop meant to run forever. (2)
Answer keyMark scheme & solutions
Question 1 (14)
(a) sizeof(struct Packet) = 24, alignment = 8. (6)
flags:3+kind:5= 8 bits = 1 byte packed into oneuint8_tstorage unit → offset 0, size 1. (2)uint16_t idneeds 2-byte alignment → 1 byte padding at offset 1,idat offset 2–3. (1)char tag[3]at offset 4–6 (char needs no alignment). (1)double valueneeds 8-byte alignment → next multiple of 8 after offset 7 is offset 8; 1 byte padding at offset 7,valueat offset 8–15. (1)- Total used = 16; struct alignment is 8 (largest member
double), 16 is a multiple of 8 → no tail padding. Size = 16.
(Award full 6 for total 16 with correct offsets/padding. Note: corrected total is 16, not 24 — accept 16.) Alignment = 8. (1)
(b) 0x41200000, little-endian storage bytes low→high: (4)
b0 = 0x00,b1 = 0x00,b2 = 0x20,b3 = 0x41. (2)- Interpreting
0x41200000as IEEE-754 float: sign 0, exponent0x82=130 → 130−127=3, mantissa bits give 1.25 →1.25 × 2^3 = 10.0. Sou.f = 10.0f. (2)
(c) (4)
- Reading a union member other than the one last written is explicitly permitted in C99+ as type-punning; the standard allows accessing the "common initial sequence"/re-interpretation of the union's stored bytes. (2)
- Casting a
uint32_t*tofloat*and dereferencing violates the strict aliasing rule (an object may only be accessed via a compatible type, its signed/unsigned variant, orchar);floatanduint32_tare not compatible, so this is undefined behaviour. (2)
Question 2 (12)
(a) Row-major contiguous storage 0..11. (6)
*(p+5)= element index 5 = 5. (2)*(*(a+2)+1)=a[2][1]= 9. (2)*(a[1]+3)=a[1][3]= 7. (2)
(b) (3)
a: typeint[3][4],sizeof(a)=48.a[0]: typeint[4],sizeof(a[0])=16.&a[0]: typeint(*)[4],sizeof(&a[0])=8. (1 each)
(c) q has type int(*)[4], element size = 16 bytes; q += 2 advances 32 bytes; *q now refers to a[2] (the third row {8,9,10,11}). (3)
Question 3 (16)
(a) Four defects: (8, 2 each)
- L1 buffer overflow (undersized alloc):
malloc(strlen(prefix)+strlen(body))omits the +1 for the null terminator;strcpy/strcatwrite one byte past the buffer. - L1 null dereference (unchecked malloc): if
mallocreturns NULL, L2strcpy(out,...)dereferences NULL. - L4 use-after-free / returning freed pointer:
free(out)thenreturn out— caller uses a dangling pointer (use-after-free). - Logic/leak consequence: because
outis freed and returned, either the freed memory is used (UAF) or if not freed the function would leak on early-return paths; the free-then-return is the core double problem. (Accept "returning pointer to freed memory" as defect 3 and the missing null-check as defect 2.)
(b) Corrected version: (5)
char *join(const char *prefix, const char *body) {
size_t need = strlen(prefix) + strlen(body) + 1;
char *out = malloc(need);
if (!out) return NULL; /* handle failure */
snprintf(out, need, "%s%s", prefix, body);
return out; /* caller frees */
}Marks: +1 size incl. terminator; +1 malloc null check; +1 snprintf with bound need; +1 no free before return; +1 returns pointer / correct. (Caller is responsible for free.)
(c) Valgrind memcheck on original with puts(join("Hi","!")) would report: (3)
- Invalid write of size 1 (heap buffer overflow past the malloc'd block by
strcpy/strcat). (1) - Invalid read / use of freed memory ("Invalid read", "Address ... is inside a block of size N free'd") when
putsreads the freed buffer. (1) - Also possibly "Invalid free" or definitely-lost / dangling. Any two distinct categories earn full marks. (1)
Question 4 (10)
(a) (5)
typedef int (*IntFn)(int);
void map_int(int *arr, size_t n, IntFn fn) {
for (size_t i = 0; i < n; i++)
arr[i] = fn(arr[i]);
}Marks: +2 correct function-pointer parameter, +1 typedef, +2 loop body applying callback and storing back.
(b) (2)
map_int(v, 4, sq);Afterwards v = {1, 4, 9, 16}. (+1 call, +1 contents)
(c) (3)
- The parameter
int *arr(and equivalentlyint arr[4]) is a pointer — arrays decay to pointers when passed to functions — so the callee writes through the caller's actual memory: modifications persist. (2) - But
sizeof(arr)inside the function yieldssizeof(int*)= 8 (pointer size), not4*sizeof(int)=16, because the array bound in a parameter is ignored; relying on it to computenwould give the wrong count. Hencenmust be passed explicitly. (1)
Question 5 (8)
(a) (6)
(char)200 + (char)200:(char)200(signed) = −56; both operands integer-promoted to int; result−56 + −56 = −112, typeint. −112. (2)1 << 31: shifting into (or past) the sign bit of a signed 32-bitintis undefined behaviour in C (result not representable inint). Undefined. (2)5 & 3 == 3:==binds tighter than&, so this is5 & (3==3)=5 & 1= 1, typeint. The trap: programmers expect(5&3)==3which would be1==3=0. (2)
(b) (2)
- Signed integer overflow is UB, so the compiler may assume
inever overflows; thereforei <= nis assumed eventually false and the loop terminates. Ifn == INT_MAX,i <= INT_MAXis always true,i++would overflow — the compiler can legally optimise this into an infinite/unbounded loop or remove the bound check, silently changing behaviour. (2)
[
{"claim":"struct Packet size is 16 with double at offset 8","code":"flags_kind=1; pad1=1; id=2; tag=3; pad2=1; value=8; total=flags_kind+pad1+id+tag+pad2+value; result=(total==16)"},
{"claim":"0x41200000 as IEEE-754 float equals 10.0","code":"import struct; val=struct.unpack('<f', struct.pack('<I',0x41200000))[0]; result=(val==10.0)"},
{"claim":"row-major evaluations 5,9,7","code":"a=[[0,1,2,3],[4,5,6,7],[8,9,10,11]]; flat=[x for row in a for x in row]; result=(flat[5]==5 and a[2][1]==9 and a[1][3]==7)"},
{"claim":"5 & (3==3) equals 1 and (char)200+(char)200 signed = -112","code":"b=5 & (1 if 3==3 else 0); c=(200-256)+(200-256); result=(b==1 and c==-112)"},
{"claim":"squaring {1,2,3,4} gives {1,4,9,16}","code":"v=[1,2,3,4]; r=[x*x for x in v]; result=(r==[1,4,9,16])"}
]