Pointer movement and array boundaries
A position can be valid without holding an element
An index tells us which array element to select. A pointer can identify that same element, and moving the pointer changes the selected position without changing the stored integers. The important boundary is that a pointer may reach one position beyond the final element, although there is no element there to read or write.
In this lesson you will match indexed and pointer-based scans, distinguish movement from a store, classify boundary operations, and find an actual array's element count using sizeof. You will also explain why the same size calculation cannot recover a caller's array length inside an array-parameter helper.
Prerequisites: initialized arrays, valid indices, finite loops and accumulators, pointer targets and copied pointer values, and the previous lesson's array arguments, adjusted parameters and explicit length contracts. C11 is our teaching convention, not a version mandated by the GATE syllabus. Every array here has a positive constant size and initialized int elements. All arithmetic on int stays within −32767 through 32767. Complete programs are independent. Classification-only text is never an instruction to compile or execute an invalid operation.
We reason with positions in one live array, not invented numeric addresses. No int byte size, pointer width, common size for all pointer types or physical placement of separate objects is assumed. Two-dimensional arrays, allocation and casts are outside this lesson.
Count steps in elements
Start with int marks[4] = {5, 2, 8, 1}; and int *p = marks;. In this initializer, marks supplies a pointer to its first element. Record p → marks[0].
- Evaluating
p + 1produces a pointer tomarks[1]. By itself, this expression does not replace the value inp - The assignment
p = p + 1;saves that new position inp. No element value changes - The separate statement
p++;advances the pointer by one element too. We keep movement separate from reads and stores - With
p → marks[1],p + 2identifiesmarks[3], andp - 1identifiesmarks[0]
The integer being added counts elements of the pointed-to type. Here the type is int *, so each step is one int element. Do not multiply the step by an assumed byte size. Starting at marks[1], an offset of 2 reaches index 3, irrespective of how many bytes an int occupies on the implementation.
A useful symbolic record is (array identity, position). For example, (marks, 1) means the pointer to element 1 of this particular array. The record is our reasoning notation, not a C data structure or an integer representation of a pointer.
Let an actual array a have N elements. In this lesson, a known pointer into that live array is represented by position j, allowing the special end position N. For an integer offset k, forming p + k requires the resulting position j + k to stay from 0 through N, inclusive. Reading or writing through the result needs the stricter range 0 through N - 1. Check formation before asking whether an element access follows. Going beyond the permitted positions is already invalid even if you never dereference the result.
Brackets and dereference choose the same element
For a valid index i into an actual array a, a[i] and *(a + i) select the same element. The parentheses matter: first form the pointer to position i, then follow it. Either spelling can read that element or select it for an assignment.
For a pointer p, p[i] means *(p + i). Its index is relative to the current position of p. If p → marks[1], then p[0] selects marks[1], and p[2] selects marks[3]. It does not select marks[2] merely because the subscript says 2. The resulting target still has to be a valid element of the same array. Equivalent spellings do not make an out-of-bounds access valid.
Keep these three effects separate when p points to a readable, writable initialized integer:
| Expression or statement | What it does |
|---|---|
p = p + 1; | Moves the pointer, subject to the formation bounds |
*p = *p + 1; | Increases the selected integer; the pointer stays in place |
*(p + 1) | Selects the next element for a read here, if that element exists; p stays in place |
The value expression *p + 1 adds 1 to the current integer. It is not the same expression as *(p + 1). For p → marks[1], they yield 3 and 8 respectively. Neither expression alone stores its result.
One past is a stopping position
For the four-element marks, marks + 4 is a valid one-past pointer. It identifies the boundary after marks[3]; it does not identify a fifth integer and does not extend the array. We write it as marks + 4 (end), rather than drawing an arrow to a fictitious element.
You may store this pointer, compare a traversal pointer with it for equality or inequality, and move back from it to an element. In particular, (marks + 4) - 1 identifies marks[3]. You must not evaluate a dereference of the end pointer to read or write an integer. Forming marks + 5 or marks - 1 is outside this array's permitted movement range as well. A presumed neighbouring object would not fix that.
Our forward scan starts at position 0, checks p != end before reading, and advances by exactly one after each body. Those facts together establish that every body has an element. The condition alone is not a general pointer-validity test. If the starting pointer, end pointer or movement rule is wrong, an inequality test does not repair it. We use no ordering comparison between unrelated pointers.
Worked example 1 Two scans of the same four elements
Predict both totals and the final equality result. The indexed scan finishes before the pointer scan starts; neither scan changes marks.
#include <stdio.h>
int main(void)
{
int marks[4] = {5, 2, 8, 1};
int indexed_total = 0;
for (int i = 0; i < 4; i++) {
indexed_total += marks[i];
}
int pointer_total = 0;
int *p = marks;
int *end = marks + 4;
while (p != end) {
pointer_total += *p;
p = p + 1;
}
printf("%d %d %d\n", indexed_total, pointer_total, p == end);
return 0;
}The table aligns equivalent checkpoints in the two separate scans. The position after the last body is allowed even though it cannot supply another integer.
| Elements already processed | Next index in indexed scan | Next position in pointer scan | Total in each scan |
|---|---|---|---|
| 0 | 0 | marks + 0 | 0 |
| 1 | 1 | marks + 1 | 5 |
| 2 | 2 | marks + 2 | 7 |
| 3 | 3 | marks + 3 | 15 |
| 4 | 4 | marks + 4 | 16 |
Before each successful test, the next position is an element. The body reads 5, 2, 8 and 1 in order. In each scan, the total at its checkpoint is the sum of exactly the already processed prefix. The final update reaches index 4 or pointer position 4. The next test fails, so there is no fifth read. Each scan has four bodies and five loop-condition tests.
The output is 16 16 1, followed by a newline. Equality produces the int value 1 when true, so %d matches its type. Both totals are also int. The array remains {5, 2, 8, 1}.
If you omit the final value 1, you probably stopped at the last element rather than after it. If you try to add a fifth value, you treated the stopping position as a readable element. If moving p changed marks, you confused pointer assignment with a store through *p.
Sizes need their own type
sizeof asks for a size in C bytes, using the operand's type. It is an operator, not a function call. For our fixed arrays and ordinary integer or pointer operands, its operand is not evaluated. We do not use variable-length arrays, whose rules need separate treatment.
Introduce size_t before storing these results. It is an unsigned integer type provided by <stddef.h> and is the result type of sizeof. Use %zu to print a size_t value. Do not assume it is int, or use %d for it. Our counts are small nonnegative values; keep a size_t loop index and its size_t length together. Unsigned counts are not a place for a −1 not-found marker. Before subtracting 1 from a count, establish that it is positive.
For an actual array declaration int values[5] = {4, 7, 1, 6, 2}; in main, sizeof values measures the entire array, while sizeof values[0] measures one int element. The array does not convert to a pointer as the operand of this sizeof. If one element occupies S C bytes, the whole array occupies 5 * S; the ratio is 5. We need no numeric value for S.
Thus size_t count = sizeof values / sizeof values[0]; records 5. The ratio is an element count, not a byte count. It describes this actual array where that array type is available. It is not a general “length of whatever this name points at” operation.
Worked example 2 Count the actual array and pass a chosen length
This helper uses the previous lesson's array-parameter spelling, now with a size_t length. The parameter values is adjusted to int *. For this example's contract, the caller supplies the first element of a live array of at most five initialized integers, each from 0 through 20, and length is from zero through its actual element count. The helper reads that prefix, changes no elements and returns an int total from 0 through 100. Even for length zero, our contract supplies the real array's first element; it does not introduce null-pointer calls.
#include <stddef.h>
#include <stdio.h>
int sum_prefix(int values[], size_t length)
{
int total = 0;
for (size_t i = 0; i < length; i++) {
total += values[i];
}
return total;
}
int main(void)
{
int values[5] = {4, 7, 1, 6, 2};
size_t count = sizeof values / sizeof values[0];
size_t selected = 3;
int prefix_total = sum_prefix(values, selected);
int full_total = sum_prefix(values, count);
printf("%zu %zu %d %d\n", count, selected, prefix_total, full_total);
return 0;
}| Call | Supplied length | Visited indices | Running totals | Returned total |
|---|---|---|---|---|
sum_prefix(values, selected) | 3 | 0, 1, 2 | 4, 11, 12 | 12 |
sum_prefix(values, count) | 5 | 0, 1, 2, 3, 4 | 4, 11, 12, 18, 20 | 20 |
The first call selects only three of the five existing elements. The second selects all five. Every body uses i < length, and the caller's supplied length also satisfies the capacity bound. The two totals start independently at 0. Nothing stores through values, so the caller's array stays unchanged. Output: 5 3 12 20, followed by a newline.
Now ask what sizeof values would mean in each scope:
- In
main,valuesnames an actual five-element array, so its size covers all five elements - Inside
sum_prefix,valuesis a pointer parameter, so its size would be the size of thatint *parameter type. Writing brackets in the parameter declaration does not preserve the caller's array type - Inside that helper,
sizeof values[0]would still measure anint. Dividing a pointer size by an element size has no general connection to the supplied length or capacity
These are facts about the types, not a claim that the two byte sizes must differ. A numeric coincidence on one machine proves no length rule. A pointer parameter does not carry an automatic array-length field. Replacing length by that ratio would discard the explicit contract. The defined program uses no such replacement.
A distance between positions also has a type
Subtracting two suitable pointers in the same live array can report a signed distance in elements. One or both pointers may be at that array's one-past position. The result must fit in ptrdiff_t, a signed integer type declared in <stddef.h>. Use %td to print it. Our distances are only −5 through 5 and fit. Do not store or print an arbitrary pointer difference as though its type were necessarily int or size_t.
For positions j and k, the result of the first pointer minus the second is j - k. The order matters: end minus start is positive, start minus end is negative, and a position minus itself is zero. This counts element steps, not bytes. We do not subtract pointers from separate arrays, even if both arrays have equal values or were declared next to each other. This lesson does not use pointer subtraction to discover whether two unknown pointers belong to the same array.
Practice before opening the solutions
These four original transfer tasks are unscored. State the type and bounds facts as well as the numeric answers. Treat the two text-fenced sets as classification-only material; never compile or execute their invalid operations.
Practice 1 Classify the positions and measure the span
Assume a live initialized array int a[4] = {9, 3, 7, 2};. Classify each expression independently: valid element pointer, valid end pointer, valid integer read, or undefined operation. They are not consecutive statements.
a + 0 a + 4 *(a + 3) *(a + 4) a + 5 a - 1
Then give the output of the safe complete program in the solution. Before looking, reason about the last value, end - first, and first - end, with first = a and end = a + 4. Which type and print format are needed for the two distances?
Practice 2 Move one pointer and change two elements
Trace the current positions separately from the array values. Explain what q[0] selects after q moves.
#include <stdio.h>
int main(void)
{
int a[5] = {2, 5, 8, 4, 1};
int *p = a + 1;
int *q = p;
p = p + 2;
*p = *q + 3;
q = q + 1;
q[0] = *p - 2;
printf("%d %d %d %d %d | %d %d\n",
a[0], a[1], a[2], a[3], a[4], *p, *q);
return 0;
}Practice 3 Repair a backward scan
The goal is to sum all three elements, visiting them from last to first. This unsafe fragment is for classification only.
int values[3] = {3, 4, 6};
int *p = values + 3;
int total = 0;
while (p != values) {
total += *p;
p = p - 1;
}Identify the first invalid operation. Repair only the order of the two body statements, then give the visit order, running totals and final pointer position. Explain why the repaired body never forms a before-first pointer. Would the repaired loop read anything if it started with p = values instead?
Practice 4 Find the final value without inventing a length
A helper's declaration is int final_value(int data[], size_t length). Its contract requires a pointer to the first element of a live initialized int array and a selected length of at least 1 and no greater than the actual count. It returns the last selected integer and changes no elements.
For int data[5] = {2, 4, 6, 8, 9}; in main, complete the call using a count computed from the actual array. Explain why replacing the explicit length inside the helper by sizeof data / sizeof data[0] is unjustified. Does an observed pointer size on one machine prove the element count? Explain why the positive-length condition is needed for data[length - 1].
Full solutions and wrong-turn feedback
Practice 1 solution
a + 0is a valid pointer toa[0]a + 4is a valid one-past pointer; it cannot supply an integer read or write*(a + 3)is a valid read of the last element and yields 2*(a + 4)attempts an evaluated one-past dereference: undefined behavior, with no prescribed integer resulta + 5is already invalid pointer formation: undefined behavior even without a later dereferencea - 1is also invalid pointer formation: there is no permitted before-first position
The safe program below uses only the allowed endpoints and moves back before reading. forward and backward are ptrdiff_t, whereas the element read is int.
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int a[4] = {9, 3, 7, 2};
int *first = a;
int *end = a + 4;
int *last = end - 1;
ptrdiff_t forward = end - first;
ptrdiff_t backward = first - end;
printf("%d %td %td\n", *last, forward, backward);
return 0;
}Positions are 0 for first, 4 for end and 3 for last. The differences are 4 - 0 = 4 and 0 - 4 = -4. Output: 2 4 -4, followed by a newline. The array is unchanged.
A distance of 3 confuses the last index with the end position. Multiplying 4 by an assumed byte size answers a different question. Treating a + 5 as harmless because it is not read misses that pointer formation has its own bounds. No test run can give a required answer for the invalid expressions.
Practice 2 solution
| After this step | Array values | Position of p | Position of q |
|---|---|---|---|
| All declarations | {2, 5, 8, 4, 1} | 1 | 1 |
p = p + 2 | {2, 5, 8, 4, 1} | 3 | 1 |
*p = *q + 3 | {2, 5, 8, 8, 1} | 3 | 1 |
q = q + 1 | {2, 5, 8, 8, 1} | 3 | 2 |
q[0] = *p - 2 | {2, 5, 6, 8, 1} | 3 | 2 |
The first movement changes only p; copied pointer values are independent. The first store reads 5 through q and writes 8 to a[3] through p. Then q moves to position 2. Its subscript 0 is relative to that position, so q[0] selects a[2], not a[0]. The last store reads the current 8 at a[3], subtracts 2 and stores 6 at a[2].
All pointer positions used for reads and writes are 1, 2 or 3, within the five-element array. Output: 2 5 6 8 1 | 8 6, followed by a newline. Predicting a change to a[0] treats q as though it still named the array's beginning. Moving q together with p would instead confuse a copied pointer value with a permanent link.
Practice 3 solution
Forming values + 3 is valid. The first loop test is true, but its original first body statement tries to read through that end pointer. That read has undefined behavior. C does not prescribe a total for the unsafe fragment.
Move backward before reading:
#include <stdio.h>
int main(void)
{
int values[3] = {3, 4, 6};
int *p = values + 3;
int total = 0;
while (p != values) {
p = p - 1;
total += *p;
}
printf("%d %d\n", total, p == values);
return 0;
}| Body number | Position before body | Position read after movement | Value read | Total after body |
|---|---|---|---|---|
| 1 | 3 | 2 | 6 | 6 |
| 2 | 2 | 1 | 4 | 10 |
| 3 | 1 | 0 | 3 | 13 |
At each successful test, the current position is from 1 through 3. Moving back produces a readable position from 0 through 2. After the third body, p equals values; the next test fails before another decrement. The loop therefore never forms position −1. It has three bodies and four tests. Output: 13 1, followed by a newline, with the array unchanged.
If the repaired loop starts at values, its first test fails: no body, no movement, no read, total 0. The real three-element array still exists. Merely moving the original starting pointer to the last element without reconsidering the condition would skip the first element. Repair the relationship among starting position, test, movement and read, rather than guessing from a plausible total.
Practice 4 solution
#include <stddef.h>
#include <stdio.h>
int final_value(int data[], size_t length)
{
return data[length - 1];
}
int main(void)
{
int data[5] = {2, 4, 6, 8, 9};
size_t length = sizeof data / sizeof data[0];
int answer = final_value(data, length);
printf("%zu %d\n", length, answer);
return 0;
}In main, the ratio is 5 * S / S = 5, where S denotes sizeof data[0]. The call passes the pointer to the first element and the copied count 5. Inside the helper, length - 1 is 4, a valid index, and the initialized value there is 9. Output: 5 9, followed by a newline. Both the array and the caller's count remain unchanged.
Inside the helper, data has adjusted type int *. Its size measures that pointer type, while sizeof data[0] measures int. Their ratio does not discover the selected length or the array capacity. Equal pointer sizes for two particular calls would not make their arrays equally long; the same helper could be called with a suitable three-element array as well. We claim nothing about all pointer types having the same size.
The contract's length >= 1 establishes a final selected element and makes subtracting 1 appropriate. A zero count violates this helper's contract; do not evaluate an element access to test what happens. For a positive count within capacity, the index is from 0 through capacity minus 1. A supplied length of 3 for this same array would select index 2 and return 6. The size ratio in the caller gives its capacity; a chosen shorter length still has to be passed deliberately.
Before moving on
You should be able to justify every formed position before predicting an output, and say whether each statement moves a pointer or changes an integer. You should also know whether a name denotes an actual array or an adjusted pointer parameter at the exact sizeof expression. This lesson stays with one-dimensional fixed arrays; it does not provide a general test for an arbitrary pointer's validity.
Semantic fact checks: WG14 N1570, clauses 6.3.2.1, 6.5.2.1, 6.5.3.4, 6.5.6, 6.7.6.3, 7.19 and 7.21.6.1. Explanations, examples, traces and practice are original. This attribution identifies verification sources; it is not a claim of recovered historical source accounting or global rights clearance.
Quick reference
- Record the array identity and current pointer position separately from element values
- Adding 1 to an
int *advances oneintelement, without assuming a byte size - Evaluating
p + kalone leavespunchanged; assignment or incrementingpcan update its stored pointer value - For valid targets,
a[i]selects*(a + i);p[i]is relative top's current position - Formation allows positions 0 through the count; an element read or write requires 0 through count minus 1
- One past is a valid stopping position, never an extra readable or writable element
- Prove the start, test and step of a traversal together; the comparison alone proves no safety
- Same-array pointer differences count signed element steps; use
ptrdiff_tand%td sizeofyieldssize_t; use%zuand check positivity before subtracting 1 from a count- The array-size ratio gives a count only for an actual array; an adjusted pointer parameter still needs an explicit length
Notes for this lesson
Sign in to keep your progress. Sign in