Recommended resources
The freely available resources this lesson will direct you to.
Welcome back. The last module gave you a way to keep applications moving while using interview evidence to guide preparation. This module turns to C interview skills. Today’s focus is not memorizing trick questions: it is learning to predict what a short program must do, what depends on the machine, and what cannot be inferred safely.
You’ll work through pointers and arrays, const, structure layout, and storage duration, then verify your predictions on your Linux VM. Allow about 40 minutes, including the short videos and a compiler run.
Pointers and arrays: track the type, not just the address
A pointer expression has a type that determines what dereferencing it accesses and how far pointer arithmetic moves. If p points to an element of an int array, p + 1 points to the next int; it does not mean “one byte later.” Pointer subtraction within the same array counts elements, not bytes.
An array is different from a pointer variable. In many expressions, an array expression is converted to a pointer to its first element:
int samples[4] = {10, 20, 30, 40};
int *p = samples; /* p points to samples[0] */
int third = *(p + 2); /* 30 */
The distinction matters precisely where that conversion does not occur. sizeof samples measures the entire array, while sizeof p measures a pointer variable. &samples has yet another type: pointer to an array of four ints, not pointer to an int. It starts at the same location as the first element, but adding one to it advances past the whole array.
Pointer declaration - cppreference.com
Read cppreference’s “Pointers to objects” subsection to check the types involved in address-taking, dereferencing, and array conversion.
Under “Explanation,” find “Pointers to objects.” Read address and arrays, beginning with the paragraph about the address-of operator and ending just before the paragraph about pointer comparisons. Pay particular attention to the different declarations using a and &a, and to the multidimensional-array example.
One more interview-critical adjustment occurs at a function boundary. In a parameter declaration, int values[4] is adjusted to int *values. The 4 does not give the function a four-element array object or an enforced length. Inside that function, sizeof values is the size of the pointer parameter. Pass the element count separately when the function needs it.
Watch this brief explanation of why pointer increments depend on the pointed-to type. Its four-byte int example describes that setup, not a size guaranteed by C on every machine.
Master Pointers in C: 10X Your C Coding!
In Master Pointers in C, Dave’s Garage illustrates how pointer arithmetic scales with the pointed-to type.
Watch pointer arithmetic. Focus on what adding one means for a character pointer versus an integer pointer; then apply the same rule to a pointer to an entire array.
const: identify what you cannot modify through this expression
Read a declaration from the identifier outward. These three declarations impose different restrictions:
int x = 5;
int y = 7;
const int *view = &x; /* view can move; *view is read-only */
int *const fixed = &x; /* fixed cannot move; *fixed is writable */
const int *const both = &x; /* neither operation is allowed through both */
view = &y is permitted; *view = 9 requires a diagnostic. For fixed, the reverse applies: *fixed = 9 is permitted, but fixed = &y requires a diagnostic. Here, “read-only” describes access through that expression. Because x itself is an ordinary int, changing x directly can change the value later observed through view. This is different from casting away const and attempting to modify an object that was declared const.
Const, const pointers, and pointers to const things (in C)
Jacob Sorber’s Const, const pointers, and pointers to const things (in C) contrasts the two places const can qualify a pointer declaration.
Watch pointer to const for reassignment versus writing through the pointer, then const pointer for the reverse. Finish with both qualifiers. For each declaration shown, identify the permitted and forbidden operations before the demonstration reveals them.
This distinction is useful in systems interfaces. A function accepting const struct Command *cmd promises not to modify the command through that parameter; it does not establish that no other code can change the object. That promise should not be mistaken for synchronization or immutability across the whole program.
Structure layout: declaration order is fixed; byte offsets are not
Consider a command record:
struct Command {
char opcode;
int blocks;
char status;
};
Its members appear in declaration order, and there is no padding before the first member. The compiler may place padding between members and after the last member to satisfy layout requirements. Thus sizeof(struct Command) need not equal the sum of its members’ sizes. The offsets and total size are properties to measure for your target, not numbers to infer solely from the source.
Struct declaration - cppreference.com
Read cppreference’s “Explanation” of structure member order and padding. It provides the language guarantees against which to check any diagram or machine-specific example.
In the “Explanation” section, read member layout: the paragraph beginning with increasing member addresses and ending with the minimum structure size. Focus on which claims are guaranteed and which say padding may occur.
On a common target with four-byte int objects and four-byte alignment for int, the offsets of opcode, blocks, and status might be 0, 4, and 8, with a total structure size of 12 bytes. Those are example results, not portable constants. Tail padding can make each element of a struct Command array suitably laid out for the next one. This is why manually summing field sizes is unreliable when predicting array strides or binary layouts.
Storage duration: ask when the object stops existing
Knowing an address is not enough to know whether it is still valid. For each object in a code trace, identify both what it points to and how long the pointed-to object exists.
An ordinary block-local variable has automatic storage duration: its lifetime ends when execution leaves that block. A block-local variable declared static has static storage duration: it retains its value across calls and is initialized before use. File-scope objects also have static storage duration; writing static at file scope additionally restricts the name’s linkage. These are separate questions—lifetime and visibility.
Memory obtained with malloc has allocated storage duration and remains allocated until it is freed. The pointer variable holding its address may have a much shorter lifetime. C also has thread storage duration for _Thread_local objects: each thread has its own instance for the duration of that thread. This is a lifetime distinction, not a substitute for deciding whether shared data needs synchronization.
For example, this function must not be used to obtain a lasting pointer:
int *bad_id(void)
{
int id = 7;
return &id; /* id's lifetime ends on return */
}
The address may look plausible, but using the returned pointer to access id is not valid. Conversely, a pointer to a block-local static int can remain valid after the function returns, although every call refers to the same object. An uninitialized ordinary local object also does not receive the zero initialization that static-duration objects receive.
Verify the model on your Linux VM
Before compiling, make a quick prediction for each printed value or size. You do not need to guess machine-specific byte counts: mark those “measure.” Save the following as c_model.c:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
struct Command {
char opcode;
int blocks;
char status;
};
static unsigned issued; /* static storage duration; initially zero */
static unsigned next_id(void)
{
static unsigned sequence; /* persists across calls */
++issued;
return ++sequence;
}
/* In a parameter declaration, values[4] is adjusted to int *values. */
static void inspect(int values[4])
{
int *p = values;
printf("parameter pointer size: %zu\n", sizeof p);
printf("third element: %d, element distance: %td\n",
*(p + 2), (p + 3) - p);
}
int main(void)
{
int values[4] = {10, 20, 30, 40};
int *p = values;
int (*whole)[4] = &values;
printf("array size: %zu, pointer size: %zu\n",
sizeof values, sizeof p);
printf("via pointer: %d, via pointer-to-array: %d\n",
*(p + 2), (*whole)[2]);
inspect(values);
int x = 5;
int y = 7;
const int *view = &x;
int *const fixed = &x;
view = &y;
*fixed = 11;
printf("view: %d, fixed: %d\n", *view, *fixed);
printf("offsets: %zu %zu %zu; struct size: %zu\n",
offsetof(struct Command, opcode),
offsetof(struct Command, blocks),
offsetof(struct Command, status),
sizeof(struct Command));
unsigned first = next_id();
unsigned second = next_id();
printf("IDs: %u %u; issued: %u\n", first, second, issued);
int *allocated = malloc(sizeof *allocated);
if (allocated == NULL)
return 1;
*allocated = 42;
printf("allocated value: %d\n", *allocated);
free(allocated);
allocated = NULL; /* avoids retaining this dangling pointer */
return 0;
}
Compile and run it with:
cc -std=c11 -Wall -Wextra -pedantic c_model.c -o c_model
./c_model
Check your predictions against the output. The two ways of reading the third array element agree, but p and whole have different types and different pointer-arithmetic strides. The view and fixed values show that qualifier placement controls operations through each pointer, not whether x can ever change. Finally, the two IDs differ because sequence persists between calls; issued starts at zero despite having no explicit initializer.
If a predicted structure size or pointer size differs from the output, revise the target-specific assumption, not C’s rules. offsetof and sizeof are the right tools for checking those assumptions.
Key takeaways
To predict C behavior, keep four questions separate: What is the expression’s type? Does an array convert to a pointer here? Which access does const restrict? Is the object still alive? For structures, add a fifth: Is this layout guaranteed, or am I assuming a particular target?
This lesson establishes the model for reading code accurately. Next, you’ll sharpen the boundary between defined behavior, undefined behavior, and behavior that depends on an implementation—especially where an apparently reasonable pointer or layout assumption fails.