C Programming

Module 3· 7 min read· 10 Questions·completed

3. Pointers, Pointer Arithmetic & Array Decay

Memory model, pointer scaling, array decay rules (arr vs &arr), 2D subscript equivalence, complex declarations via Right-Left rule, and string literal pitfalls

GATE CS & UGC NET JRF Core Subject Exam Weightage: 3–4 Questions every year on pointer arithmetic scaling, arr vs &arr type differences, 2D array dereferencing (*(*(p + i) + j)), and decoding complex type declarations.


1. Technical Jargon & The Pointer Memory Model#

A pointer is not a mysterious entity; it is a variable whose stored value is a memory address pointing into the process's virtual address space.

Diagram / Text
Variable: x (int)             Pointer: p (int*)
Address:  0x1000              Address:  0x2000
Value:    42                  Value:    0x1000 (Stores address of x)
          ┌────────┐                    ┌────────────┐
0x100042   │ <————————————————─ │   0x10000x2000
          └────────┘                    └────────────┘
  1. Dereferencing (*): Accessing the data stored at the memory location pointed to by the address.
  2. Address-Of (&): Retrieving the physical RAM address of an l-value object.
  3. Pointer Scaling: In pointer arithmetic, adding an integer kk does not add kk bytes; it adds k×sizeof(p)k \times \text{sizeof}(*p) bytes.
  4. ptrdiff_t: The signed integer type returned when subtracting two pointers of the same type.
  5. Array Decay: The automatic implicit conversion of an array name into a pointer to its first element in most expression contexts.
  6. Generic Pointer (void*): A pointer with no associated data type. Can hold the address of any object, but cannot be directly dereferenced or used in standard pointer arithmetic without a type cast.
  7. Dangling Pointer: A pointer holding the address of memory that has already been deallocated or gone out of scope.
  8. Wild Pointer: An uninitialized pointer holding arbitrary garbage memory addresses.

2. Pointer Arithmetic & Scaling#

2.1 The Pointer Arithmetic Rule#

Given pointer T *p pointing to base address Addr:

Address(p+k)=Addr+k×sizeof(T)\text{Address}(p + k) = \text{Addr} + k \times \text{sizeof}(T) Address(pk)=Addrk×sizeof(T)\text{Address}(p - k) = \text{Addr} - k \times \text{sizeof}(T)

C
int *pi = (int *)1000;    // Assume sizeof(int) = 4
double *pd = (double *)1000; // Assume sizeof(double) = 8

pi + 2  ==>  1000 + (2 * 4) = 1008
pd + 2  ==>  1000 + (2 * 8) = 1016

2.2 Pointer Subtraction (p2 - p1)#

Subtracting two pointers that point to elements of the same array:

p2p1=Numeric Address(p2)Numeric Address(p1)sizeof(T)p_2 - p_1 = \frac{\text{Numeric Address}(p_2) - \text{Numeric Address}(p_1)}{\text{sizeof}(T)}

The result is the number of elements between them, not bytes!

Warning

Pointer Subtraction Precondition: Subtracting two pointers that do NOT point into the same array (or one past the end) is UNDEFINED BEHAVIOR in ISO C.


3. Array Decay Rules: arr vs. &arr#

In C, an array name decays to a pointer to its first element in almost all expressions:

C
int a[5] = {10, 20, 30, 40, 50};

The 3 Exceptions Where Array Does NOT Decay:#

  1. sizeof(a): Returns total size of the array in bytes (5×4=205 \times 4 = 20), NOT pointer size!
  2. &a: Yields a pointer to the entire array of type int (*)[5], NOT int*.
  3. String literal initializing an array: char s[] = "hello"; copies the characters into stack storage.

The Big GATE Trap: What is the difference between a, &a[0], and &a?#

ExpressionNumeric Address ValueC Data TypeWhat does + 1 advance by?
a0x1000int* (pointer to int)4 bytes (sizeof(int))
&a[0]0x1000int* (pointer to int)4 bytes (sizeof(int))
&a0x1000int (*)[5] (pointer to array of 5 ints)20 bytes (5 * sizeof(int))
C
#include <stdio.h>

int main(void) {
    int a[5] = {1, 2, 3, 4, 5};

    printf("a     = %p, a + 1     = %p\n", (void*)a, (void*)(a + 1));
    // a + 1 advances by 4 bytes (next integer)

    printf("&a    = %p, &a + 1    = %p\n", (void*)&a, (void*)(&a + 1));
    // &a + 1 advances by 20 bytes (skips the ENTIRE array of 5 integers!)

    // Classic GATE Exam Trick:
    int *ptr = (int *)(&a + 1);
    printf("%d\n", *(ptr - 1)); // Points to last element! Prints 5!
    return 0;
}

4. Multi-Dimensional Arrays & Subscript Equivalence#

4.1 Subscript Equivalence Principle#

In C, array subscript notation is syntactic sugar for pointer arithmetic:

a[i](a+i)(i+a)i[a]a[i] \equiv *(a + i) \equiv *(i + a) \equiv i[a]

For 2D Arrays:

a[i][j]((a+i)+j)a[i][j] \equiv *(*(a + i) + j)

Note

Why is i[a] valid C? Because addition is commutative: a[i] is defined as *(a + i), which is identical to *(i + a), which by definition equals i[a]! 3[arr] is completely legal and accesses arr[3].

4.2 Pointer to Array vs. Array of Pointers#

C
// 1. Pointer to an Array of 5 Integers:
int (*p)[5]; // 'p' is ONE pointer. sizeof(p) is 8 bytes (on 64-bit).
             // p + 1 skips 5 * 4 = 20 bytes.

// 2. Array of 5 Pointers to Integer:
int *p[5];   // 'p' is an ARRAY of 5 pointers.
             // sizeof(p) is 5 * 8 = 40 bytes (on 64-bit).

5. Decoding Complex Declarations: The Right-Left / Spiral Rule#

In GATE and UGC NET, you will encounter declarations like int *(*(*foo)())[10]. You decode any complex C declaration using the Right-Left (Clockwise Spiral) Rule:

Diagram / Text
1. Start at the identifier name.
2. Look RIGHT: If you see '()', it's a function; if you see '[]', it's an array.
3. Look LEFT: If you see '*', it's a pointer to...
4. When parentheses are encountered, resolve everything inside them before moving outside.
Diagram / Text
Example: void (*bsd_signal(int sig, void (*func)(int)))(int);

1. Start at identifier: bsd_signal
2. Look Right: (int sig, void (*func)(int))  --> is a function taking an int and a function pointer...
3. Look Left: *                            --> returning a pointer to...
4. Move Outside Right: (int)               --> a function taking an int...
5. Move Outside Left: void                 --> returning void.

Common Complex Declarations Reference Table:#

DeclarationHuman Readable Meaning
int *p[10]Array of 10 pointers to int
int (*p)[10]Pointer to an array of 10 ints
int (*p)(void)Pointer to a function taking void and returning int
int *(*p)(void)Pointer to a function taking void and returning pointer to int
int (*p[10])(int)Array of 10 pointers to functions taking int and returning int
int (*(*p)[10])(void)Pointer to an array of 10 function pointers returning int

6. String Literals: Read-Only Segment vs. Stack Arrays#

C
// Case 1: Pointer to String Literal
char *str1 = "Hello";
str1[0] = 'M'; // UNDEFINED BEHAVIOR! Segfault on modern OS (stored in read-only .rodata)

// Case 2: Character Array initialized with String Literal
char str2[] = "Hello";
str2[0] = 'M'; // FULLY LEGAL! Array is allocated on the Stack and mutable ("Mello")

7. Best Practices & Defensive Pointer Habits#

  1. Initialize All Pointers Immediately: If a pointer has no target yet, assign NULL: int *p = NULL;.
  2. Zero Out Freed Pointers: After calling free(p), immediately set p = NULL; to prevent accidental dangling pointer dereferencing.
  3. Use sizeof(*ptr) in Allocation: Write p = malloc(n * sizeof(*p)); instead of malloc(n * sizeof(int));. If the type of p changes, the allocation remains automatically correct!
  4. Const Correctness for Read-Only Buffers: If a function only reads a buffer, mark parameter as const int *buf.

Practice Quiz

Test your understanding with step-by-step solutions

Practice Quiz

10 questions · 90s per question

Each question has a 90-second time limit. Unanswered questions will be auto-submitted when time runs out.