Recommended resources
The freely available resources this lesson will direct you to.
Hello! In the previous lesson, p + 1 moved a recursive function’s pointer to the next array element, while p[0] and p[1] read two adjacent values. Now we will make those operations precise. You will learn to calculate both the address an expression designates and the value it produces, including when a pointer moves, an array is indexed, or a two-dimensional array is involved.
An address is not the value at that address
Consider an integer variable and a pointer to it:
int x = 12;
int *p = &x;
x contains 12. The expression &x produces the address of x, and p stores that address. The expression *p follows the stored address and accesses x; its value is therefore 12. If you execute *p = 18;, x becomes 18, but p still holds the same address. By contrast, assigning another valid address to p changes which object it designates.
A pointer is itself an object with its own address. The supplied diagram makes this distinction visible:

In the diagram, p == ptr is true because their stored pointer values are equal. &p == &ptr is false because the pointer objects occupy different locations. Meanwhile, *p and *ptr both have the integer value 3. Do not infer a pointer’s size from the distance between the two pointer objects: the diagram does not specify their sizes or require them to be adjacent.
Read the pointer fundamentals in Cornell’s Arrays & Pointers. The examples distinguish assigning to a pointer from assigning through a pointer, which is the first decision to make in a trace.
Read the pointer fundamentals in Cornell’s Arrays & Pointers to distinguish an object's address, a pointer's stored value, and the object reached by dereferencing it.
In “Pointers,” begin under “Pointer Types and Reference-Of” with address and dereference. Continue through “Everything Has an Address, Including Pointers” and “Pointers as References, and Dereferencing.” Stop after the example that assigns through a pointer for the second time. Focus on which object changes at each assignment.
For GATE tracing, a compact notation helps: record a pointer as “p holds address 3000; *p is 3.” This prevents the common mistake of answering an address question with an element value—or the reverse.
Pointer arithmetic counts elements, not bytes
Suppose the diagram represents this array and sizeof(int) is 4 bytes:
int a[5] = {3, 5, 6, 7, 9};
int *p = a;
Assume a[0] begins at byte address 3000. An array’s elements are contiguous, so a[1] begins at 3004 and a[2] at 3008. Since p has type int *, adding 2 to p advances two integers, not two bytes.
For an array whose first element begins at byte address , whose element size is bytes, and whose index is :
Here, and , so p + 2 has address 3008. That expression produces a pointer; *(p + 2) follows it and produces the integer 6.
| Expression | Kind of result | Result in this example |
|---|---|---|
p |
Address of a[0] |
3000 |
p + 2 |
Address of a[2] |
3008 |
*(p + 2) |
Integer stored in a[2] |
6 |
&a[2] |
Address of a[2] |
3008 |
a[2] |
Integer stored in a[2] |
6 |
Watch Neso Academy’s Pointer Arithmetic (Addition) for the visual movement between elements, then the corresponding byte-address calculation.
Watch Neso Academy’s Pointer Arithmetic (Addition) to connect an index change with its scaled change in byte address.
In element movement, follow how adding an integer changes the targeted array index. In byte addresses, follow the four-byte integer example; notice why adding 1 to an int * does not add just 1 to its numerical address.
The scale comes from the pointed-to type. If an int * points into an integer array, advancing it by one adds sizeof(int) bytes to the address. If a char * points into a character array, advancing it by one adds sizeof(char), which is one byte. A pointer’s own size is not the scale factor.
Array indexing, pointer values, and pointer objects
C defines subscripting in terms of pointer arithmetic:
The same rule applies when the left operand is a pointer: p[i] means *(p + i). Its index is measured from wherever p currently points. If p = a + 2, then p[0] is a[2], and p[1] is a[3]. This is precisely why the recursive call in the previous lesson could use the same expressions p[0] and p[1] while examining a new pair on each invocation.
Read Cornell’s array discussion for the contiguous layout behind indexing:
Return to Cornell’s Arrays & Pointers for its visualizable base-address calculation.
In “Arrays are Mostly Just Pointers,” start at array addresses. Continue into “Treat an Array as a Pointer to the First Element,” stopping after the comparison with the first element's address. Pay particular attention to the element-size term in the address calculation. The next paragraph uses a convenient pointer analogy; keep the array-versus-pointer distinction below in mind when reading it.
The distinction is that an array object is not a pointer variable. In most expressions, the array expression a is converted to a pointer to its first element, so either of these initializations works:
int *p = a;
int *q = &a[0];
Both pointers initially have the same value. But p is a modifiable pointer object; a is an array object. You may write p = a + 1 or ++p, but not a = p or ++a. Neso Academy’s Using Array Name as a Pointer demonstrates that difference:
Watch the array-name distinction in Neso Academy’s Using Array Name as a Pointer so that “acts like a pointer” does not become “is a pointer variable.”
In array versus pointer, focus on why computing the expression a + 1 is permitted but trying to update a with a++ is not. The video then contrasts that with advancing a separate pointer variable.
There are contexts where an array expression does not convert to a pointer. Two especially useful ones are sizeof a, which gives the size of the whole array, and &a, which gives a pointer to the whole array. With our five-element example and four-byte integers, sizeof a is 20 bytes; sizeof p is the size of a pointer on the given implementation, not 20. Although a and &a begin at the same numerical address, their types differ: a + 1 advances one integer, whereas &a + 1 advances past the entire five-integer array.
When an array is passed to a function parameter declared int values[], that parameter is adjusted to int *values. Inside the function, sizeof values therefore measures a pointer, not the caller’s whole array. Pass the element count separately when the function needs it.
Advancing, subtracting, and stopping at the boundary
Arithmetic can produce a new pointer without changing the original one:
int *p = a;
int *q = p + 3; /* q points to a[3]; p still points to a[0] */
An increment does update its pointer variable: ++p moves p to the next element. Parentheses and operator precedence determine whether an increment moves a pointer or changes an element:
int *p = a; /* a[0] is 3, a[1] is 5 */
int first = *p++; /* first is 3; p now points to a[1] */
int old = (*p)++; /* old is 5; a[1] becomes 6; p does not move */
int next = *++p; /* p moves to a[2]; next is 6 */
*p++ is parsed as *(p++): the value is read at the old position, then p advances. (*p)++ increments the integer at the current position. *++p advances the pointer first. Keeping these operations in separate statements makes their order clear; do not assume a safe left-to-right order for multiple pointer updates buried in one expression.
Two pointers into the same array can also be subtracted. The answer is a difference in elements, not bytes:
int *p = &a[1];
int *q = &a[4];
q - p /* 3 elements */
Although the byte addresses differ by 12 under our assumptions, q - p is 3. Its C result type is ptrdiff_t. Ordered comparisons such as p < q are likewise meaningful when the pointers designate positions in the same array (including its one-past position); do not use numerical-address intuition to order pointers into unrelated objects.
For a five-element array, a + 5 is a valid one-past-the-end pointer. It is useful as a stopping position, but there is no a[5] object to read:
for (int *p = a; p != a + 5; ++p) {
/* *p is valid while this loop body runs */
}
Dereferencing a + 5 is invalid. Forming pointers farther outside the permitted array range is not a legitimate shortcut for a trace. This boundary rule explains the base case in the previous lesson: it stopped before trying to read a nonexistent element after the last one.
Two-dimensional arrays: first identify the step size
A two-dimensional C array is an array of rows, with each row itself an array. For example:
int grid[2][3] = {
{10, 20, 30},
{40, 50, 60}
};
Suppose grid[0][0] starts at address 2000 and an int occupies four bytes. Each row occupies bytes. Thus grid[1][0] starts at 2012, and grid[1][2] starts at 2020. In general, for columns:
The key is to inspect what one pointer step traverses. In most expressions, grid becomes a pointer to its first row, of type int (*)[3]. Consequently, grid + 1 advances by one complete three-integer row. Once you select a row, grid[1] generally converts to a pointer to that row’s first integer, and grid[1] + 2 advances by two integers. These relationships give:
grid[1][2] /* value: 60 */
*(*(grid + 1) + 2) /* same value: 60 */
&grid[1][2] /* address: 2020 */
Although the address of grid[0][0] is also 2000, grid is not an int **. An int ** expects to point to a pointer object that stores an integer address; grid contains rows of integers, not row-pointer objects. For address questions involving multiple dimensions, write down the pointed-to type before applying a byte offset.
A complete address-and-value trace
Here is a GATE-style worked trace—not a previous-year question—using the five-element array from the diagram. Assume its base address is 3000 and sizeof(int) == 4.
int a[5] = {3, 5, 6, 7, 9};
int *p = a;
int *ptr = a;
ptr += 2;
int first = *p++;
*ptr = *p + 4;
--ptr;
int second = (*ptr)++;
int gap = ptr - p;
int third = *++p;
Track pointer positions and changed elements after each complete statement:
| Statement completed | p points to |
ptr points to |
Important result |
|---|---|---|---|
| Initializations | a[0] (3000) |
a[0] (3000) |
Array is {3, 5, 6, 7, 9} |
ptr += 2 |
a[0] |
a[2] (3008) |
No element changes |
first = *p++ |
a[1] (3004) |
a[2] |
first is 3 |
*ptr = *p + 4 |
a[1] |
a[2] |
a[2] becomes 5 + 4 = 9 |
--ptr |
a[1] |
a[1] |
No element changes |
second = (*ptr)++ |
a[1] |
a[1] |
second is 5; a[1] becomes 6 |
gap = ptr - p |
a[1] |
a[1] |
gap is 0 elements |
third = *++p |
a[2] (3008) |
a[1] (3004) |
third is 9 |
The final array is {3, 6, 9, 7, 9}. Notice that p and ptr are independent pointer variables even when they happen to hold the same address. Also, changing a[2] changes the value subsequently read there; it does not change its address.
An incorrect result here usually has a specific cause. If first is reported as 5, the old-position read in *p++ was missed. If second is reported as 6, the old value returned by (*ptr)++ was missed. If gap is reported as a byte count, pointer subtraction was treated as ordinary address subtraction rather than a difference in elements. Locate that first mistaken interpretation before continuing the trace; later numbers otherwise inherit it.
Key takeaways
For every pointer expression, identify its type, its current position, and whether the expression asks for an address or dereferences it for a value. Pointer arithmetic moves in units of the pointed-to type; subscripting is dereferenced pointer arithmetic; and an array can provide its first element’s address without being a modifiable pointer variable. Stay within the array’s permitted range, remembering that a one-past pointer may be formed but not dereferenced.
The next lesson applies these same rules to character arrays and C strings, where moving one character at a time is straightforward but recognizing the string’s terminator becomes essential.