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

What an address really is

Memory as a numbered street, and why a variable has both a value and a location.

9 min read · Lesson 1 of 6 · Free

Memory is a numbered street

Think of RAM as a very long street of identical houses. Each house holds one byte. Each house has a number: 0, 1, 2, and so on up to billions. That number is an address.

When you declare int x = 65;, the compiler picks four free houses next to each other and writes 65 into them. From then on, the name x means "the four bytes starting at that house number".

So every variable has two facts attached to it:

  • its value — 65
  • its address — where those bytes live

x gives you the value. &x gives you the address. That is the whole idea. Everything else in this course is built on it.

Seeing the address

C
#include <stdio.h>

int main(void)
{
    int x = 65;
    char c = 'A';
    double d = 3.14;

    printf("x is %d and lives at %p\n", x, (void *)&x);
    printf("c is %c and lives at %p\n", c, (void *)&c);
    printf("d is %f and lives at %p\n", d, (void *)&d);
    return 0;
}

The %p specifier prints an address in hexadecimal, something like 0x7ffd4c2ab8ac. The (void *) cast is what %p officially expects; without it gcc warns.

Run this twice. The addresses change between runs. Modern operating systems deliberately place your program at a random spot in memory each time, as a security measure. So never memorise an address — care about the relationships between them.

Addresses are ordinary numbers

Add up the addresses of an array and you see the arithmetic clearly:

C
#include <stdio.h>

int main(void)
{
    int a[4] = {10, 20, 30, 40};
    int i;
    for (i = 0; i < 4; i++)
        printf("a[%d] = %2d at %p\n", i, a[i], (void *)&a[i]);
    return 0;
}

Each address is exactly 4 higher than the one before, because an int is 4 bytes. Change int to char and the gap becomes 1. Change it to double and it becomes 8. The type decides the step size.

Why a program needs addresses at all

Three reasons, and they cover almost all real C code.

1. To let a function change your variable. C copies arguments. A function that gets a copy cannot change the original. Give it the address instead and it can reach back and write to the real thing. This is why scanf("%d", &age) has that &.

2. To avoid copying big things. A struct holding a student record might be 200 bytes. Passing it by value copies 200 bytes on every call. Passing its address copies 8 bytes. For an array of 100,000 elements the difference is not a detail.

3. To use memory whose size you learn at runtime. You cannot write int a[n]; in older C when n comes from the user. You ask the operating system for a block at runtime and it hands you back an address. That is malloc, and lesson 5 is entirely about it.

The address-of operator

C
#include <stdio.h>

void show(int value, int *address)
{
    printf("value   = %d\n", value);
    printf("address = %p\n", (void *)address);
    printf("what is at that address = %d\n", *address);
}

int main(void)
{
    int marks = 91;
    show(marks, &marks);
    return 0;
}

Three operators appear here and it is worth naming them precisely:

  • &marks — "the address of marks"
  • int *address — "address is a variable that holds the address of an int"
  • *address — "the value stored at that address"

& and are opposites. &marks is just marks.

⚠️

The * symbol does two different jobs and this confuses everybody at first. In a declaration (int *p;) it means "p is a pointer". In an expression (*p = 5;) it means "go to the address in p". Same symbol, opposite side of the fence. Read declarations right to left: int *p is "p is a pointer to int".

What you cannot take the address of

C
#include <stdio.h>

int main(void)
{
    int x = 5;
    printf("%p\n", (void *)&x);     /* fine, x is in memory */
    /* printf("%p\n", (void *)&5); */  /* error: 5 is not stored anywhere */
    /* printf("%p\n", (void *)&(x + 1)); */  /* error: not a variable */
    return 0;
}

You can take the address of something that occupies memory — a variable, an array element, a struct field. You cannot take the address of a literal or the result of a calculation, because those live briefly in CPU registers and have no house number.

The picture to keep in your head

Name Address Bytes stored
x 0x7ffd...ac 65
c 0x7ffd...ab 'A' (65)
p 0x7ffd...a0 0x7ffd...ac

p is a normal variable in a normal house. What is written inside it happens to be another house number. That is all a pointer is: a variable whose value is an address.

💡

When a pointer program confuses you, draw this table on paper. Two columns for the address and the contents of every variable. Then walk the code line by line and edit the table. Nine out of ten pointer bugs are visible the moment the picture exists.

Sizes

A pointer holds an address, and on a 64-bit machine every address is 8 bytes wide, no matter what it points to:

C
#include <stdio.h>

int main(void)
{
    printf("%zu %zu %zu\n",
           sizeof(char *), sizeof(int *), sizeof(double *));   /* 8 8 8 */
    return 0;
}

sizeof(char) is 1, but sizeof(char *) is 8. The thing pointed at and the pointer itself are different sizes, and mixing them up is a common source of malloc mistakes later.

Next lesson: declaring pointers, dereferencing them, and the NULL value that stops your program from writing to house number zero.