FirstHack Learn
Log in Sign up free
Lessons in this course 0/6 All courses Pointers and Memory in C

First year

Progress0 / 6 lessons
  1. 1. What an address really is
  2. 2. Declaring and dereferencing pointers
  3. 3. Pointers and arrays are not the same thing
  4. 4. Passing by pointer to change a caller variable
  5. 5. malloc, free and what a memory leak costs
  6. 6. Dangling pointers, double free and real crashes

Courses › Pointers and Memory in C

Pointers and arrays are not the same thing

Why arr and &arr[0] behave alike, where the illusion breaks, and what sizeof tells you.

10 min read · Lesson 3 of 6 · Free

Where the confusion comes from

Almost every C book says "an array name is a pointer". It is not. It decays into one in most expressions, which is close enough to fool you until the day it costs you marks.

Here is the truth in one line: an array is a block of memory. A pointer is a variable holding an address. In most expressions the array name is converted to the address of its first element. In three specific places, it is not.

Where they behave the same

C
#include <stdio.h>

int main(void)
{
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;          /* same as int *p = &a[0]; */

    printf("%d %d\n", a[2], p[2]);       /* 30 30 */
    printf("%d %d\n", *(a + 2), *(p + 2)); /* 30 30 */
    return 0;
}

a[2] is defined by the standard as (a + 2). That is why indexing works on pointers too, and why the strange-looking 2[a] also compiles and gives 30 — because (2 + a) is the same thing. Do not write that in real code, but if it appears in a paper, now you know.

Pointer arithmetic scales by type

C
#include <stdio.h>

int main(void)
{
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;

    printf("p     = %p\n", (void *)p);
    printf("p + 1 = %p\n", (void *)(p + 1));   /* 4 bytes further */
    printf("*(p+1)= %d\n", *(p + 1));          /* 20 */

    printf("&a[3] - &a[0] = %ld\n", (long)(&a[3] - &a[0]));  /* 3, not 12 */
    return 0;
}

p + 1 adds 1 * sizeof(int) bytes, not 1 byte. Subtracting two pointers gives the number of elements between them, not bytes. The compiler does the multiplication and division for you.

Where they are different: sizeof

C
#include <stdio.h>

int main(void)
{
    int a[5];
    int *p = a;

    printf("sizeof(a) = %zu\n", sizeof(a));   /* 20 */
    printf("sizeof(p) = %zu\n", sizeof(p));   /*  8 */
    return 0;
}

sizeof is the first place the illusion breaks. sizeof(a) is the size of the whole array — 5 ints, 20 bytes. sizeof(p) is the size of one pointer, 8 bytes on a 64-bit machine.

This gives you the array-length trick, which works only where the real array is visible:

C
#include <stdio.h>

int main(void)
{
    double d[7];
    size_t n = sizeof(d) / sizeof(d[0]);
    printf("%zu elements\n", n);   /* 7 */
    return 0;
}

Where they are different: &

&a is the second place. Its type is "pointer to array of 5 ints", so &a + 1 jumps 20 bytes, not 4:

C
#include <stdio.h>

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    printf("a       = %p\n", (void *)a);
    printf("&a[0]   = %p\n", (void *)&a[0]);   /* same number as a */
    printf("&a      = %p\n", (void *)&a);      /* same number again */
    printf("a + 1   = %p\n", (void *)(a + 1));   /* +4  */
    printf("&a + 1  = %p\n", (void *)(&a + 1));  /* +20 */
    return 0;
}

Three of these print the same number and mean different things. a is the address of the first int. &a is the address of the whole array. Same house number, different sized object.

Where they are different: assignment

C
#include <stdio.h>

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

    p = a;        /* fine: a pointer can be assigned */
    /* a = b; */  /* error: an array name is not assignable */
    /* a++; */    /* error, same reason */

    p++;          /* fine */
    printf("%d\n", *p);   /* 2 */
    return 0;
}

An array name is not a variable you can move. It is a fixed label on a fixed block. A pointer can be re-pointed all day.

⚠️

The most common consequence of the decay rule: sizeof inside a function that took an array parameter gives the pointer size, always. This is wrong and will silently loop the wrong number of times. Always pass the length as a separate argument.

C
#include <stdio.h>

/* WRONG */
void print_bad(int arr[])
{
    size_t n = sizeof(arr) / sizeof(arr[0]);   /* 8 / 4 = 2, always */
    size_t i;
    for (i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

/* RIGHT */
void print_good(const int *arr, size_t n)
{
    size_t i;
    for (i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    print_bad(a);              /* prints only 1 2 */
    print_good(a, 5);          /* prints 1 2 3 4 5 */
    return 0;
}

int arr[], int arr[5] and int *arr in a parameter list are all exactly the same thing: a pointer. The 5 is ignored by the compiler. Writing it is a comment, not a constraint.

Strings make it visible

C
#include <stdio.h>

int main(void)
{
    char arr[] = "hello";     /* 6 bytes on the stack, modifiable */
    char *ptr  = "hello";     /* pointer to read-only string data */

    arr[0] = 'H';             /* fine */
    /* ptr[0] = 'H'; */       /* undefined behaviour, usually a crash */

    printf("%s %s\n", arr, ptr);
    printf("%zu %zu\n", sizeof(arr), sizeof(ptr));   /* 6 8 */
    return 0;
}

char arr[] = "hello" copies the six bytes into your array. char *ptr = "hello" stores the address of a string literal, which usually lives in a read-only section of the executable. Writing through it crashes on most systems.

💡

If you want to modify a string, declare it as an array. If you only want to read it, const char * says so clearly and lets the compiler catch mistakes.

Quick summary

Question Array a Pointer p
What is it a block of memory a variable holding an address
sizeof size of the whole block 8
Can be assigned no yes
&a type pointer to whole array pointer to pointer
As a function parameter becomes a pointer stays a pointer