G27
Self-checkedTier 1 15 min readpds.pointers

Pointers and pointer arithmetic

A pointer is a variable that holds an address. Learn what &, * and p + 1 really do (p + 1 moves a whole element, not a byte), how *p++ and (*p)++ differ, and which pointer mistakes are undefined behaviour.

Why it exists

A variable name gives access to one object. Often a program needs to refer to an object indirectly: let a function change its caller's variable, walk along an array, build a list whose cells point to each other, ask the operating system for a block of memory whose size is not known until the program runs. All of these need a value that means "the object over there". That value is an address, and a variable that holds one is a pointer.

A pointer is also the machine's own way of reaching memory. The CPU's indirect addressing works exactly like *p: the instruction names a register, and the register holds the address of the data. C puts that mechanism in your hands, which is why it is both powerful and the source of most C bugs.

Prerequisites

Precise definitions

  • Pointer. An object whose value is the address of another object (or function). int *p; declares p as a pointer to int. The type int * records what kind of object p is supposed to point at.
  • Address-of &. &x is the address of x, of type int * if x is an int.
  • Dereference *. *p is the object that p points at. It can be read (y = *p) or written (*p = 5). Using * in a declaration (int *p) is a different use of the same symbol: it says "p is a pointer".
  • Pointer to pointer. int **pp holds the address of an int *. **pp follows two arrows. The animation below does this.
  • The null pointer. A null pointer is a valid value that points at no object. It is written with a null pointer constant: an integer constant expression with value 0 (such as 0) or that expression cast to void *. NULL is a macro for one of these. A variable that merely holds 0 (int z = 0; p = z;) is not a null pointer constant. Comparing a pointer with NULL is fine. Dereferencing a null pointer is undefined behaviour.
  • Size of a pointer. sizeof(int *) is the size of an address: 8 bytes on a typical 64-bit machine and 4 on a 32-bit one. On such platforms it does not depend on the pointee: int *, char * and double * are all the same size. (The C standard itself only requires that void * and char * match each other; the equal sizes are a property of common platforms, and GATE assumes them.) What differs is sizeof(*p), the size of the pointee.
  • Why the pointer type matters. The type tells the compiler (1) how many bytes to read or write for *p, and (2) how far p + 1 moves.
  • Pointer arithmetic (C11 6.5.6). For T *p:
    • p + n is the address address(p)+n×sizeof(T)\text{address}(p) + n \times \texttt{sizeof(T)}. It moves n elements, not n bytes. p++ and p-- likewise.
    • p - q (both pointing into the same array) is the number of elements between them, of type ptrdiff_t.
    • p + n is valid only if p points into an array (a single object counts as an array of one element) and the result is inside the array or exactly one past the last element. n may be negative. The one-past pointer may be compared and subtracted, but not dereferenced (forming &a[5] for a 5-element array is still allowed, because & and * cancel). Any other result is undefined behaviour, even if you never dereference it.
    • Relational comparison (<, <=, ...) is defined only between pointers into the same object (the same array, or members of the same struct). == and != work between pointers of compatible type, even into different arrays, though a one-past-the-end pointer may happen to equal the address of a neighbouring object.
    • Adding two pointers, or multiplying or dividing a pointer, is an error.
  • void *. A pointer with no type. It can hold any object pointer and converts to and from other object-pointer types without a cast in C (qualifiers such as const must be preserved), but it cannot be dereferenced. Standard C does not allow arithmetic on it (gcc allows it as an extension and treats the size as 1).
  • const and pointers. Read the declaration from the name outwards, right to left.
Declarationp is...*p = 1;p = &y;
int *ppointer to intallowedallowed
const int *p (same as int const *p)pointer to const interrorallowed
int *const pconst pointer to intallowederror
const int *const pconst pointer to const interrorerror
  • Bad pointers. A wild (uninitialised) pointer holds an indeterminate value. A dangling pointer holds the address of an object whose lifetime has ended (a returned local, a freed block); in C11 such a pointer's value is indeterminate, so even using it without dereferencing is not allowed. A null pointer is a perfectly good value, but it points at no object and must not be dereferenced. Dereferencing any of the three is undefined behaviour.

How it works

Following arrows

Step through this real run. Watch three things. First, q = p copies an address, so after it there are two arrows to the same x. Second, p = &y changes where p points and leaves x alone, while *p = ... changes the pointee. Third, pp is a pointer to the pointer p.

A pointer holds an address

Real run, real addresses

C source

1#include <stdio.h>
2
3int main(void) {
4 int x = 10, y = 20;
5 int *p = &x;
6 int *q = p;
7 *p = 11;
8 p = &y;
9 *p = *p + 1;
10 int **pp = &p;
11 **pp = 99;
12 *pp = &x;
13 q = NULL;
14 return 0;
15}

Stack

xint

10…d5fc

yint

20…d5f8

Amber = changed in this step. Hover a cell for its full address. Arrows show where a pointer points; the small tag under a cell names the pointers aimed at it.

Step 1

Two ordinary ints. Nothing points anywhere yet.

1 / 10
The pointer cell shows the object it points at. The tag under a cell names every pointer aimed at it. Produced by: real execution: every address and value is read from memory by the running program at each step. gcc (Debian 14.2.0-19) 14.2.0. gcc -O0 -o prog prog.c && setarch x86_64 -R ./prog (ASLR off so the numbers are stable)

The two assignments are the most confusing pair in the subject:

StatementWhat it changes
p = q;the pointer p: it now points where q points
*p = *q;the object p points at: it receives a copy of the value of the object q points at

Moving through an array

In the next run p walks along an array. Look at the address of p in the cell. Each p++ adds 4, because p points to int. q - p is 3 although the addresses differ by 12: the subtraction divides by the element size.

Pointer arithmetic moves in whole elements

Real run, real addresses

C source

1#include <stdio.h>
2
3int main(void) {
4 int a[5] = {10, 20, 30, 40, 50};
5 int *p = a;
6 p++;
7 int *q = p + 3;
8 int d = (int)(q - p);
9 p = q + 1;
10 p = p - 2;
11 *p = 0;
12 return 0;
13}

Stack

aint

10…d5e0
[0]
↑p
20…d5e4
[1]
30…d5e8
[2]
40…d5ec
[3]
50…d5f0
[4]

ppointer

a[0]…d5d8

Amber = changed in this step. Hover a cell for its full address. Arrows show where a pointer points; the small tag under a cell names the pointers aimed at it.

Step 1

An array name in an expression becomes a pointer to its first element, so p points at a[0].

1 / 7
p + 1 adds sizeof(*p) bytes, not 1. Watch the addresses step by 4 for an int pointer. Produced by: real execution: every address and value is read from memory by the running program at each step. gcc (Debian 14.2.0-19) 14.2.0. gcc -O0 -o prog prog.c && setarch x86_64 -R ./prog (ASLR off so the numbers are stable)

*p++, (*p)++, *++p

Postfix ++ binds tighter than unary *, and prefix ++ and * group right to left. So:

ExpressionParsed asValue of the expressionEffect
*p++*(p++)the old target, *p before the movep advances one element
(*p)++(*p)++the old value of *pthe pointee is incremented, p stays
*++p*(++p)the target after the movep advances one element
++*p++(*p)the new value of *pthe pointee is incremented

Worked examples

EX 1Address, dereference, and the same object seen twice

Easy
#include <stdio.h>
 
int main(void) {
    int x = 10;
    int *p = &x;
    *p = 11;
    printf("%d %d %d\n", x, *p, p == &x);
    return 0;
}

Output:

11 11 1

Reading it. *p = 11 writes into x itself, so x and *p are the same object and both read 11. p == &x compares two addresses and is true (1).

EX 2A pointer to a pointer

Medium
#include <stdio.h>
 
int main(void) {
    int x = 10, y = 20;
    int *p = &x;
    int **pp = &p;
    *pp = &y;
    **pp = *p + 1;
    printf("%d %d %d\n", x, y, *p);
    return 0;
}

Output:

10 21 21

Reading it. *pp is p itself, so *pp = &y re-aims p at y. Now **pp is *p, which is y. The right side reads *p, which is 20, adds 1, and **pp = 21 stores it into y. x was never touched.

EX 3Pointer difference counts elements

Medium
#include <stdio.h>
 
int main(void) {
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;
    p++;
    int *q = p + 3;
    printf("%d %td %d\n", *q, q - p, *(q - 1));
    return 0;
}

Output:

50 3 40

Reading it. After p++, p points at a[1]. p + 3 is a[4], which holds 50. q - p is 3 elements, even though the two addresses are 12 bytes apart. *(q - 1) is a[3], which holds 40.

EX 4*p++, (*p)++ and *++p in a row

GATE level
#include <stdio.h>
 
int main(void) {
    int a[4] = {10, 20, 30, 40};
    int *p = a;
    int x = *p++;
    int y = (*p)++;
    int z = *++p;
    printf("%d %d %d %d %td\n", x, y, z, a[1], p - a);
    return 0;
}

Output:

10 20 30 21 2

Step by step. Start with p at a[0].

  1. x = *p++: take the old target, a[0] = 10, then move p to a[1]. So x=10x = 10.
  2. y = (*p)++: the pointee is a[1] = 20. The expression yields 20 and then a[1] becomes 21. p does not move. So y=20y = 20.
  3. z = *++p: move p first, to a[2], then read it. So z=30z = 30.

At the end a[1] is 21 and p - a is 2.

EX 5sizeof of a pointer versus its target

GATE level
#include <stdio.h>
 
int main(void) {
    int *ip;
    char *cp;
    double *dp;
    printf("%zu %zu %zu %zu %zu %zu\n", sizeof ip, sizeof cp, sizeof dp,
           sizeof *ip, sizeof *cp, sizeof *dp);
    return 0;
}

Output (64-bit gcc):

8 8 8 4 1 8

Reading it. On this platform all pointers are the same size, 8 bytes. The sizes of the targets are 4, 1 and 8. Note that sizeof *ip does not dereference ip (the operand of sizeof is not evaluated), so the uninitialised pointer causes no problem here. This is a favourite check of whether you can separate "the pointer" from "what it points at".

EX 6Looking at the bytes of an int through a char pointer

GATE level
#include <stdio.h>
 
int main(void) {
    unsigned int x = 0x12345678;
    unsigned char *c = (unsigned char *)&x;
    printf("%x %x\n", c[0], c[3]);
    return 0;
}

Output on a little-endian machine such as x86:

78 12

Reading it. c points at the first (lowest-address) byte of x. On a little-endian machine the least significant byte, 0x78, is stored first, so c[0] is 0x78 and c[3] is 0x12. A big-endian machine would print 12 78. The order is a property of the machine, not of C, so a GATE question must state it (or ask which machine type produces a given output). Reading an object's bytes through unsigned char * is explicitly allowed by C.

Cheat box

Cheat box

  • &x address of x. *p the object p points at. p = q copies an address; *p = *q copies a value.
  • p + n moves n×sizeof(*p)n \times \texttt{sizeof(*p)} bytes. p - q is in elements. Only inside an array or one past its end.
  • Do not dereference: NULL, a wild pointer, a dangling pointer, or the one-past-the-end pointer.
  • *p++ →\to read old target, then advance p. (*p)++ →\to increment the pointee. *++p →\to advance, then read.
  • On typical platforms all pointers are the same size (8 bytes on 64-bit). sizeof *p is the pointee size.
  • const int *p: cannot change *p. int *const p: cannot change p.
  • int *p, q; makes p a pointer and q a plain int.
  • Byte order of an int seen through an unsigned char * is machine-dependent.

Traps and GATE patterns

Connections

Pointers and arrays are tied together: an array name decays to a pointer in most expressions, and a[i] is *(a + i). The arrays, strings and pointers page works through the consequences. Where a pointer variable and its target live is on the memory layout page. Dynamic memory is the main source of pointers whose target is not a named variable. A CPU's indirect addressing mode is the hardware version of *p.

PYQs

8 previous year questions from your book. I re-solved each one myself (8 by running or simulating, 0 by reasoning only); the book's key is shown next to my answer. All PYQs

GATE 2006 · Set 1 · MCQ · 1 markQ42 · page 26 of the book
Consider this C code to swap two integers and these
five statements:

void swap(int *px, int *py) {

*px = *px - *py

*py = *px + *py;

*px = *px - *py;

}

S1: will generate a compilation error

S2: may generate a segmentation fault at runtime
depending on the arguments passed

S3: correctly implements the swap procedure for all
input pointers referring to integers stored in memory
locations accessible to the process

S4: implements the swap procedure correctly for
some but not all valid input pointers

S5: may add or subtract integers and pointers.
  • (a)S1
  • (b)S2 and S3
  • (c)S2 and S4
  • (d)S2 and S5
GATE 2026 · Set 2 · MCQQ1 · page 39 of the book
Consider the following ANSI-C program.

#include <stdio.h>

int main() {

int *ptr, a, b, c;

a = 5;

b = 11;

c = 20;

ptr = &a;

*ptr = c;

ptr = &c;

a = *(&b);

c = *ptr - a;

printf("%d", c);

return 0;

}
The output of this program is __________. (answer
in integer)
GATE 2025 · Set 1 · MCQQ5 · page 40 of the book
Consider the following C program:

#include < stdio.h >
void foo(int *p, int x) {
    *p = x;
}
int main() {
    int *z;
    int a = 20, b = 25;
    z = &a;
    foo(z, b);
    printf("%d", a);
    return 0;
}

The output of the given C  program is__________  .
(Answer in integer)
GATE 2018 · MCQ · 2 marksQ13 · page 41 of the book
#include<stdio.h>
void fun1(char *s1, char *s2)  {
    char *tmp;
    tmp = s1;
     s1 = s2;
     s2 = tmp;
}
void fun2(char **s2, char **s2)  {
     char *tmp;
     tmp = *s1;
     *s1 = *s2;
     *s2 = tmp;
}
int main ()  {
     char *str1 = "Hi", *str2 = "Bye";
      fun1(str1, str2);
      printf("%s %s", str1, str2);
      fun2(&str1, &str2);
      printf("%s %s", str1, str2);
      return 0;
}

The output of the program above is
  • (a)Hi ByeBye Hi
  • (b)Hi Bye Hi Bye
  • (c)Bye Hi Hi Bye
  • (d)Bye Hi Bye Hi
GATE 2017 · MCQ · 1 markQ15 · page 42 of the book
Consider the following function implemented in
void printxy (int x, int y)   {
int *ptr;
x = 0;
ptr = &x;
y = *ptr;
*ptr = 1;
printf("%d,%d",x,y);
}
The output of invoking printxy(1, 1) is_____
  • (a)0, 0
  • (b)0 ,1
  • (c)1, 0
  • (d)1, 1
GATE 2016 · Set 1 · NAT · 1 markQ18 · page 43 of the book
#include<stdio.h>
void mystery(int *ptra, int *ptrb) {
int *temp;
temp = ptrb;
ptrb = ptra;
ptra = temp;
}
int main() {
int a=2016, b=0, c=4, d=42;
mystery(&a, &b);
if (a < c)
mystery(&c, &a);
mystery(&a, &d);
printf("%d\n", a);
}
The output of the program is ________.
GATE 2015 · Set 3 · NATQ21 · page 43 of the book
Consider the following C program.
# include<stdio.h>
int main( )
{
  static int a[] = {10, 20, 30, 40, 50};
  static int *p[] = {a, a+3, a+4, a+1, a+2};
  int **ptr = p;
ptr++;
printf("%d%d", ptr - p, **ptr};
}
The output of the program is _________
GATE 2014 · Set 1 · MCQ · 1 markQ22 · page 43 of the book
Consider the following program in C language:
#include <stdio.h>
main()
{
    int i;
    int *pi = &i;
scanf("%d", pi);
printf("%d\n", i+5);
}
Which one of the following statements is TRUE?
  • (a)Compilation fails.
  • (b)Execution results in a run-time error.
  • (c)On execution, the value printed is 5 more than the address of variable i.
  • (d)On execution, the value printed is 5 more than the integer value entered.

Self-check

Question 1

What is the type of q in int *p, q;, and what does sizeof(p) == sizeof(q) evaluate to on a 64-bit machine?

Show answer

q is a plain int. sizeof(p) is 8 and sizeof(q) is 4, so the comparison is false (0).

Question 2

A pointer holds the address 2000 on a machine where int is 4 bytes and double is 8 bytes. What is the address after + 3 if the pointer is (a) int *, (b) char *, (c) double *?

Show answer

(a) 2000+3×4=20122000 + 3 \times 4 = 2012. (b) 2000+3×1=20032000 + 3 \times 1 = 2003. (c) 2000+3×8=20242000 + 3 \times 8 = 2024.

Question 3

What does this print?

#include <stdio.h>
 
int main(void) {
    int a[4] = {1, 2, 3, 4};
    int *p = a + 1;
    printf("%d %d %d\n", *p + 1, *(p + 1), p[-1]);
    return 0;
}
Show answer

*p + 1 is the value 2 plus 1, which is 3. *(p + 1) is the next element, 3. p[-1] is *(p - 1), which is a[0] = 1. Negative indices through a pointer are fine as long as the result stays inside the array.

3 3 1

Question 4

Which of these are undefined behaviour? (a) *p where p is NULL. (b) Forming a + 5 for int a[5] and comparing it with a + 2. (c) Reading *(a + 5) for int a[5]. (d) int *p; *p = 1;.

Show answer

(a) undefined. (b) defined: a + 5 is one past the end, which may be formed and compared. (c) undefined: you may not dereference the one-past pointer. (d) undefined: p is uninitialised.

Question 5

Given const int *p = &x; and int y;, which of *p = 3; and p = &y; compile?

Show answer

p = &y; compiles (the pointer itself may change). *p = 3; is an error, because p points to a const int. To forbid p = &y you would need int *const p.

Revision card

One-screen revision

  • A pointer holds an address. & takes one, * follows one. p = q copies an arrow; *p = *q copies a value.
  • p + n moves n×sizeof(*p)n \times \texttt{sizeof(*p)} bytes. p - q counts elements. Valid only inside an array or one past it.
  • *p++: old target, then advance. (*p)++: increment the pointee. *++p: advance, then read.
  • On typical platforms all pointers are one size; sizeof *p is the pointee size.
  • const int *p guards the pointee. int *const p guards the pointer.
  • Undefined: dereferencing NULL, wild, dangling or one-past pointers; using a dangling pointer's value at all; < or - on pointers into different arrays.
  • int *p, q;: only p is a pointer.

References

  • ·Kernighan and Ritchie, The C Programming Language, 2nd ed., Sections 5.1 Pointers and Addresses, 5.2 Pointers and Function Arguments, 5.3 Pointers and Arrays, 5.4 Address Arithmetic, 5.6 Pointer Arrays, 5.10 Pointers vs. Multi-dimensional Arrays
  • ·ISO/IEC 9899:2011 (C11), Sections 6.2.4 Storage durations of objects, 6.2.5 Types (pointer types), 6.3.2.3 Pointers, 6.5.2.4 Postfix increment and decrement operators, 6.5.3.1 Prefix increment and decrement operators, 6.5.3.2 Address and indirection operators, 6.5.6 Additive operators, 6.5.8 Relational operators, 6.5.9 Equality operators, 6.7.3 Type qualifiers, 6.7.6.1 Pointer declarators

Verification

Both animations replay real runs of instrumented C programs: every address and pointer target is read from memory at each step, and the test suite checks the 4-byte step of p++, the 12-byte gap behind q - p == 3, and the double-indirection result. All C programs on this page are compiled with -Wall -Wextra -Werror and run by npm run verify:c. The byte-order example depends on a little-endian machine, which the page says. The textbook section numbers were written from memory and have NOT been checked against the books. The prose and the GATE-pattern claims have NOT yet had a human textbook check (the independent second pass is described at the end of these notes). SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand (long C programs were not re-read line by line: they are compiled and compared by npm run verify:c, and their printed outputs were checked against the prose). No defect found. Status raised to self-checked; verified needs a textbook or human check.

last verified: never