DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for (Almost) Everything You Need To Know About Pointers in C
Aniket Bhattacharyea
Aniket Bhattacharyea

Posted on

(Almost) Everything You Need To Know About Pointers in C

When I was first starting out with C, pointers were something that confused me the most. I was scared of pointers and could never understand how to use them.

No, I didn't say that! In fact, pointers have always been intuitive to me. But most of the students starting to learn C are put off by the idea of pointers. It is one of those areas of C which are not explained properly to students. resulting in many misconceptions about them.

In this huge post, I have compiled almost everything that is fundamental to pointers. Of course, it is a huge topic, and it's not possible to cover the entirety of it in one post, but once you know these fundamentals, you'll be able to use them more efficiently, and hopefully will be able to tackle pointers in a program.

Let's start.

Beginning With Pointer Sorcery

One fine afternoon, you are lying on your couch and thinking about the year 2038 problem and the end of the universe, and suddenly your friend calls you and asks "Hey, I want to come over and contemplate our existence, but I do not know where your house is!"

You say, "No problem buddy. I'll give you a copy of my home."

Of course, you'd never say that. Instead, you will give him your address so that he can come over. Now, you could make him a copy of your home if you're generous enough, but it takes time and defeats the purpose of your friend coming over. He wants to come to your house, not a copy.

Now think in terms of programming. At the time when C was created, memory was scarce, and being efficient was not only needed but vital. For this reason, you'd have to be really careful while dealing with memory. You'd really not like to make unnecessary copies of something.

Another case you can consider is of having "side effect" of a function. Consider this simple program.

#include <stdio.h>
void f(int a) { a = 10; }
int main() {
    int a = 5;
    printf("%d\n", a);
    f(a);
    printf("%d\n", a);
}
Enter fullscreen mode Exit fullscreen mode

which just prints

5
5
Enter fullscreen mode Exit fullscreen mode

Even though you are calling the function f with the variable a as a parameter, and f is changing the value of a. the change doesn't show up in the original value of a, because when you are calling the function f, you are passing a copy of a, not a itself. In other terms, you are giving your friend a copy of your house.

This is desired in most cases. You don't really want your functions to accidentally change any variable where it's not supposed to. But sometimes, you actually want the function to change a variable. You have already seen such a function that can change the actual parameter.

scanf("%d", &n);
Enter fullscreen mode Exit fullscreen mode

How does scanf change the value of n? The answer is through pointers.

Also, take a look at this classic example of swap-

void swap(int a, int b) {
    int t = a;
    a = b;
    b = t;
}
int main() {
    int a = 5, b =10;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
}
Enter fullscreen mode Exit fullscreen mode

It works, except it doesn't.

swap does swap the variables, but since you are making a copy of a, and b, the change doesn't show up outside the function. But we want the function to be able to change the actual variables. So we need to have some kind of way to pass the actual a and b . But in C, there is no way you can pass "actual" variables. (which is not the case in C++).

One way you might end up doing is to make a and b global

int a = 5, b = 10;
void swap() {
    int t = a;
    a = b;
    b = t;
}
int main() {
    swap();
    printf("a = %d, b = %d\n", a, b);
}
Enter fullscreen mode Exit fullscreen mode

And it now works, because swap now can access a and b, but having global variables is a real bad idea.

The way? Give swap the addresses of a and b. If it has addresses of a and b , it can change them directly.

Pointers are nothing but variables that hold the address of another variable.

Now, where does this address come from? We know how bits and bytes work. The RAM of the computer can be thought of as a mess, a really long one, with lots of rooms one after another, and each byte is a room. How does the computer know which room to put data in? It gives a number to each room, and that number is the address.

When I write

char a;
Enter fullscreen mode Exit fullscreen mode

I tell the compiler "Buddy, reserve one room in the mess, and call it a" . Why one room? Because the size of char is 1 byte. (Note that C's definition of a byte is basically the sizeof char , which in some rare cases might not be actually 1 byte in the machine, however, it is always 1 byte in C)

If I write

int b;
Enter fullscreen mode Exit fullscreen mode

I tell the compiler to reserve the number of rooms necessary for int and call it b.

Side rant: People coming from Turbo C, and being told size of int is 2 bytes, it's not necessarily so, and probably not so in any modern computer. The C standard guarantees at least 2 bytes for int and on my machine sizeof(int) is 4, so we will stick to that for the rest of this post.

Now that our b has 4 rooms, it will stay in the rooms starting from the first one. So that when we say "address of b", we actually mean "address of the starting or ending byte of b". (See big endian and little endian. For this tutorial, let's assume it's the ending byte because it is so on my machine)

In order to get the address of b and store it, we need to use a pointer variable. Just like any other variable, a pointer also has a type, defined by the type of the thing it points to. The syntax is type_of_the_thing_it_points_to *name

char *pa;
Enter fullscreen mode Exit fullscreen mode

Note that the asterisk need not be adjoined to the variable name. Any of these is valid -

char* pa;
char *pa;
char * pa;
Enter fullscreen mode Exit fullscreen mode

We will prefer the 2nd syntax. We will see in a short while why.

Let's first see how to assign a value to a pointer. In order to make a pointer point to a variable, we have to store the address of the variable in the pointer. The syntax for getting the address of a variable is &variable_name.

char a;
char *pa = &a; // pa now contains the address of a

printf("%p", pa); // %p is the format specifier to print a pointer
Enter fullscreen mode Exit fullscreen mode

If you run this program, you will see something like 0x7ffc2fc4ff27. That is the value of the pointer, which is the address of the variable a (this is in hexadecimal). This value is not fixed. If you run the program again, the value will likely change, because a will be stored somewhere else.

One thing you might have noticed. Although we are declaring it as *pa, the * is not used when printing the pointer. In fact, * is not a part of the name of the pointer. The name of the pointer is just pa. The * is instead used to get the value of whatever thing the pointer is pointing to (known as dereferencing).

char a = 'a';
char *pa = &a;

printf("%p\n", pa); // prints the value of pa
printf("%c", *pa); // prints the value of a
Enter fullscreen mode Exit fullscreen mode

So, quickly revise -

  1. pa is the value of the pointer, which is the address of a.
  2. *pa is the value of the thing pa is pointing to, in this case a.

One more time.

  1. pointer_name is the value of the pointer itself.
  2. *pointer_name is the value of the thing the pointer points to.

Now, this should be clear.

char a = 'a', b = 'b';
char * pa = &a; // pa points to a
*pa = 'c'; // change the value of whatever pa is pointing to, in this case a
printf("%c", a); //prints c
pa = &b; // change the pointer itself. pa now points to b
*pa = 'd' // change the value of whatever pa is pointing to, in this case b
printf("%c", a); //prints c, because a is unchanged as pa is no more pointing to a
printf("%c", b); //prints d
Enter fullscreen mode Exit fullscreen mode

Now we can rewrite the swap function as follows -

void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}
Enter fullscreen mode Exit fullscreen mode

And call it with the addresses swap(&a, &b). This works and the change shows up outside the function too. Because once you have the address of a variable, you know where it lives in memory so you can freely change it.

You might have a valid question. Since all pointers are just addresses, which are basically numbers, why is the type of the thing it points to necessary? Why do we distinguish between char* and int* although both of them are just some numbers?

The answer is clear. When you dereference a pointer, the compiler needs to know what data type is the object. Remember that address of a variable is just the address of the ending byte of the variable. In order to read the variable, the compiler needs to know its type so that it knows how many bytes to read.

Consider this program

#include <stdio.h>

int main() {
    int a = 1101214537;
    char *pa = &a;
    printf("%c", *pa);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

It prints (ignore the compiler warning)

I
Enter fullscreen mode Exit fullscreen mode

What happened here?

If you represent 1101214537 in binary it is 01000001 10100011 00110011 01001001 . So &a which is the address of a points to the byte in memory that contains the last byte of the number, which is 01001001. When I dereference pa, the compiler sees that it points to char so it reads only one byte at that address, giving the value 01001001 which 73, the ASCII for I. This is why the type is absolutely and you should not mix and match types unless you are absolutely sure of what you are doing. (We'll see a few examples)

Remember we told that we will prefer int *pa rather than int* pa although they are the same? The reason is to safeguard against the following common misconception. Can you find the difference?

int a, b; // a and b both are int
int* pa, pb; // whoopsie! pb is not a pointer
Enter fullscreen mode Exit fullscreen mode

If you are a beginner, you will assume that since int a, b makes both a and b as int , then int* pa, pb will make both pa and pb as int*. But it doesn't. The reason is * "binds" to the variable name, not the type name. If instead, you'd have written

int *pa, pb;
Enter fullscreen mode Exit fullscreen mode

you'd rightly conclude pa is a pointer to int, and pb is just int. Hence I prefer to write the * with the variable name, however, there are compelling reasons for the other style as well, and if you are careful enough, you can use the other style as well.

NULL and void pointer

These two are a special types of pointers in C. The Null pointer is used to denote that the pointer doesn't point to a valid memory location.

int *pa = NULL;
char *pb = NULL;
Enter fullscreen mode Exit fullscreen mode

We use Null pointer in various ways, for example, to denote failure, or mark the end of a list of unknown size, etc. Dereferencing a Null pointer is undefined behavior and your program will likely crash.

Note that the Null pointer is not the same as pointer to memory address 0, although it's very likely to be so. There are exceptions, for example in small embedded devices where address 0 might be a valid location.

Void pointer is one more interesting pointer in C. Basically void pointer "throws away" the type of a pointer. It is a general-purpose pointer that can hold any type of pointer and can be cast to any type of pointer. The following are all valid -

int a;
char b;
float c;
void *p = &a;
p = &b;
p = &c;
Enter fullscreen mode Exit fullscreen mode

But you can't dereference a void * because it doesn't have a type. Trying to dereference a void * will give you an error. However, you can cast it to anything you want and then dereference it, although it's not a very good idea and it violates the type aliasing rules.

int a = 65;
void *p = &a;
char *c = (char *) p;
printf("%c\n", *c);
Enter fullscreen mode Exit fullscreen mode

Here we're removing the type of &a through p and casting it to a char *. Essentially a is getting read as a char and this prints A.

Be careful during casting. You should use void pointers only if you are absolutely sure of what you're doing.

int a = 65;
void *p = &a;
int (*f)(int) = (int (*)(int)) p; // cast as a function pointer (discussed later)
f(2); // Segmentation fault
Enter fullscreen mode Exit fullscreen mode

Sometimes you'll see char * used as a generic pointer as well. This is because void * was not present in old versions of C, and some practice remains, or maybe the code needs to do pointer arithmetic on that pointer.

Generally void *is used in places where you expect to work with pointers to multiple types. As an example, consider the famous memcpy function which copies a block of memory. Here is the signature of memcpy -

void * memcpy ( void * destination, const void * source, size_t num );
Enter fullscreen mode Exit fullscreen mode

As you see, it accepts void *, which means it works with any type of pointers. As for an example (copied from cplusplus) -

/* memcpy example */
#include <stdio.h>
#include <string.h>

struct {
  char name[40];
  int age;
} person, person_copy;

int main ()
{
  char myname[] = "Pierre de Fermat";

  /* using memcpy to copy string: */
  memcpy ( person.name, myname, strlen(myname)+1 );
  person.age = 46;

  /* using memcpy to copy structure: */
  memcpy ( &person_copy, &person, sizeof(person) );

  printf ("person_copy: %s, %d \n", person_copy.name, person_copy.age );

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

In line 15, we invoked memcpy with char * and in line 19, we invoked memcpy with a pointer to structure, and they both work.

Pointer arithmetic

Since pointers are just like other variables, you'd expect that we should be able to do arithmetic with them. We can, but there's a catch. First of all, we are only allowed these 2 operations -

  1. Addition (and hence subtraction) of an integer constant to a pointer.
  2. Subtraction of two pointers of the same type.

Let's see them one by one

int a;
int *pa = &a;

printf("pa = %p\n", pa);
printf("pa + 1 = %p\n", pa + 1);
printf("pa - 1 = %p\n", pa - 1);
Enter fullscreen mode Exit fullscreen mode

This prints

pa = 0x7ffdd7eeee64
pa + 1 = 0x7ffdd7eeee68
pa - 1 = 0x7ffdd7eeee60
Enter fullscreen mode Exit fullscreen mode

Strangely, it seems pa+1 increments the pointer by 4, and not by 1. The reason lies in the datatype of the thing it points to, in this case, int. Remember that a pointer must always point to something. When you increment the pointer by 1, it points to the next thing.

In this case, pa points to an int. Where is the next int? After 4 bytes of course, because the size of int is 4 bytes.

Similarly pa-1 points to the previous int which lies 4 bytes before.

By the same logic, pa+2 points to the int 2 places after a that is 4 * 2 = 8 bytes after a, and pa+n points to the integer n places after a which is 4n bytes after a.

An observant reader might have noticed that things are looking almost like an array, and he/she is not wrong completely. In a few minutes, we shall explore the idea of array using pointers. Before let's talk about the subtraction of pointers.

int a;
int *pa = &a;
int *pb = pa + 2;
printf("pa = %p\n", pa);
printf("pb = %p\n", pb);
printf("pb - pa = %ld\n", pb - pa);
printf("pa - pb = %ld\n", pa - pb);
Enter fullscreen mode Exit fullscreen mode

This prints

pa = 0x7ffec09d685c
pb = 0x7ffec09d6864
pb - pa = 2
pa - pb = -2  
Enter fullscreen mode Exit fullscreen mode

Similar to the previous case, although the difference between pa and pb is of 8 bytes as numbers, as pointers the difference is 2. The negative sign of pa-pb implies that pb points after pa.

To quickly summarise -

  1. If I have some_data_type *p, then pa + n increments the pointer by n * sizeof(some_data_type) bytes.
  2. If I have some_data_type *p, *q then p - q is equal to the difference in bytes divided by sizeof(some_data_type)

Let's consider what happens if we mix indirection and prefix or postfix increment/decrement operators. Can you guess what each of these does? I have omitted the data types so that you can't guess ;-). Assume p points to int

x = *p++;
x = ++*p;
x = *++p;
Enter fullscreen mode Exit fullscreen mode

In order to answer, you have to remember the precedence -

  1. Postfix ++ and -- have higher precedence than *
  2. Prefix ++ and -- have the same precedence as *.

Since the * operator is itself a prefix, you'll never have a problem with prefix increment or decrement. You can tell just by the order of the operator. For the postfix operator, remember that postfix works first, then indirection.

So *p++ is same as *(p++). So, the value of p will be used in the expression, then p will be incremented. So x gets the value of *p and p becomes p+1, so that the type of x ought to be int too.

int a = 5;
int *p = &a;
int x;
printf("Before:\n");
printf("a = %d\n", a);
printf("p = %p\n", p);
x = *p++;
printf("After\n");
printf("a = %d\n", a);
printf("p = %p\n", p);
printf("x = %d\n", x);
Enter fullscreen mode Exit fullscreen mode

This prints

Before:
a = 5
p = 0x7ffe82ae9eb0
After
a = 5
p = 0x7ffe82ae9eb4
x = 5
Enter fullscreen mode Exit fullscreen mode

++*p will probably not arise confusion. This is the same as ++ (*p). So, first p is dereferenced, and then ++ is applied. So whatever p was pointing to gets incremented by 1 and then it is assigned to x, and p is unchanged. So the type of x is again int.

int a = 5;
int *p = &a;
int x;
printf("Before:\n");
printf("a = %d\n", a);
printf("p = %p\n", p);
x = ++*p;
printf("After\n");
printf("a = %d\n", a);
printf("p = %p\n", p);
printf("x = %d\n", x);
Enter fullscreen mode Exit fullscreen mode

This prints

Before:
a = 5
p = 0x7fff1484e210
After
a = 6
p = 0x7fff1484e210
x = 6
Enter fullscreen mode Exit fullscreen mode

And finally * ++p is same as * (++p). So, first p gets incremented by 1, and then it is dereferenced. So x gets the value of whatever is this incremented pointer pointing to.

int a = 5;
int *p = &a;
int x;
printf("Before:\n");
printf("a = %d\n", a);
printf("p = %p\n", p);
x = *++p;
printf("After\n");
printf("a = %d\n", a);
printf("p = %p\n", p);
printf("x = %d\n", x);
Enter fullscreen mode Exit fullscreen mode

This prints

Before:
a = 5
p = 0x7ffd4bad9c90
After
a = 5
p = 0x7ffd4bad9c94
x = 32765
Enter fullscreen mode Exit fullscreen mode

We can also compare pointers using relational operators like ==, <= etc. but there's a catch. You can only compare two pointers using <=, <, >=, > if they both are pointers of the same type, and of the same array or same aggregate object. Otherwise, it is undefined behavior.

Quoting C11 -

When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. If two pointers to object types both point to the same object, or both point one past the last element of the same array object, they compare equal. If the objects pointed to are members of the same aggregate object, pointers to structure members declared later compare greater than pointers to members declared earlier in the structure, and pointers to array elements with larger subscript values compare greater than pointers to elements of the same array with lower subscript values. All pointers to members of the same union object compare equal. If the expression P points to an element of an array object and the expression Q points to the last element of the same array object, the pointer expression Q+1 compares greater than P. *In all other cases, the behavior is undefined.*

Take a look -

typedef struct some_struct {
    int p;
    int q;
} some_struct;

some_struct a = {1, 2}; 
int *p = &a.p;
int *q = &a.q;
if(p > q) puts("Hi\n");
else puts("Bye\n");
Enter fullscreen mode Exit fullscreen mode

This prints Bye. Well first of all this comparison is valid since p and q are both pointers to int and also they both point to elements of the same struct. Since q was declared later in some_struct , q compares greater to p

For equality, the restriction is a bit slack. You can compare any two pointers as long as they have the same type, or one of them is a null pointer or void pointer. And they compare equal if they point to the same object, or if both are null (doesn't matter if types don't match), or if both are pointing to members of the same union.

Let's demonstrate the last point.

typedef union some_union {
    int p;
    int q;
} some_union;

some_union a;
int *p = &a.p;
int *q = &a.q;

if(p == q) {
    puts("Equal\n");
} else {
    puts("Not equal\n");
}
Enter fullscreen mode Exit fullscreen mode

This prints Equal although p and q point to different things, they are within the same union.

Since pointers are just numbers, can you put any integer in them? The answer is yes, but be careful of what you put. In fact, be careful when you dereference it. If you try to dereference an invalid address, your program will likely segfault and crash.

int *x = (int *) 1;
printf("%d\n", *x);
Enter fullscreen mode Exit fullscreen mode

This instantly segfaults.

Admitted to Hogwarts School Of Pointer Magic

Pointers and Arrays

Let's now move to some advanced sorcery - array and pointers.

We know that an array stores its elements contiguously in memory. Which means the elements are stored in order one after another. So if we have int arr[10], we know arr[1] lies right after arr[0], arr[2] lies right after arr[1] and so on. So if I have a pointer to arr[0] and I increment it by 1, it should point to arr[1]. If I increment it by 1 again, it should point to arr[2].

In fact, there are so many similarities between arrays and pointers, that we can talk about the equivalence of arrays and pointers.

Word of caution! This does not mean arrays and pointers are the same and you can use one in place of another. This misconception is quite common and ends up being harmful. Arrays and pointers are very different things, but pointer arithmetic and array indexing are equivalent.

For starters, the name of an array "decays" into a pointer to the first element. What do I mean by that? Consider this code -

int arr[10];
int *pa = &(arr[0]); // pointer to the first element
int *pb = arr; // What

printf("pa = %p\n", pa);
printf("pb = %p\n", pb);
Enter fullscreen mode Exit fullscreen mode

This prints

pa = 0x7ffef7706bb0
pb = 0x7ffef7706bb0
Enter fullscreen mode Exit fullscreen mode

But aren't we mixing up datatypes in the case of pb? pb is a pointer to int and arr is an array of int!

Turns out that arr is converted to a pointer to the first element. So that arr and &(arr[0]) is equivalent.

Quick note: indexing operator [] has higher precendence than * so that * arr[0] is same as * (arr[0])

Let's do even more funny stuff -

int arr[3] = {1, 2, 3};
int *pa = arr;


printf("arr[1] = %d\n", arr[1]); // 2nd element using array indexing
printf("*(pa + 1) = %d\n", *(pa + 1)); // 2nd element using pointer arithmetic
printf("pa[1] = %d\n", pa[1]); // What
printf("*(arr + 1) = %d\n", *(arr + 1)); // Whatt
printf("1[arr] = %d\n", 1[arr]); // Whattt
Enter fullscreen mode Exit fullscreen mode

The first printf is ok. arr[1] means the 2nd element of arr.

We just reasoned about the 2nd line. pa points to the first element of arr. So pa+1 will point to the next int in memory, which is arr[1] because array elements are stored contiguously.

But in the 3rd and 4th lines, aren't we mixing up array and pointer syntax? Well, turns out that arr[i] is just the same as *(arr + i) and this is (almost) what happens internally when you write arr[i].

Similarly *(pa + i) is the same as pa[i]. Pointer arithmetic works both on arrays and pointers. Similarly, array indexing works on both pointers and arrays.

And for the last part, arr[1] is the same as *(arr + 1) which is the same as *(1 + arr) which should be the same as 1[arr]. This is one of those weird quirks of C.

Does this mean you can mix and match pointers and arrays? The answer is a big fat no. The reason is although arr[i] and pa[i] give you the same result, i. e. the 2nd element of arr, the way they reach there is quite different.

Consider the code

#include<stdio.h>
int main() {
    int arr[3] = {1, 2, 3};
    int *pa = arr;
    int a = arr[1];
    int b = pa[1];
}
Enter fullscreen mode Exit fullscreen mode

Let's look at the assembly code generated by the compiler. I used Compiler Explorer. Don't worry if you can't read assembly. We'll go together.

We are interested in lines 5 and 6. Here's the related assembly for int arr[3] = {1, 2, 3}.

mov     DWORD PTR [rbp-28], 1
mov     DWORD PTR [rbp-24], 2
mov     DWORD PTR [rbp-20], 3
Enter fullscreen mode Exit fullscreen mode

In case you are seeing assembly for the first time, rbp is the base pointer register that holds the memory address of the base of the current stack frame. Don't worry about what that means. For now think of rbp as a pointer variable, which points to some location in memory.

Here the contents of arr is being put in memory. For example, consider the first line. The mov instruction puts the value 1 somewhere in memory. The DWORD PTR tells that it is of size 32 bit or 4 bytes as it is an int. The syntax [rbp - 28] means the content of the memory location at the address rbp-28. Remember that rbp is like a pointer. So it is the same as doing * (rbp - 28).

Putting everything together, we see that the first line puts the value 1 in the memory address pointed by rbp-28. The next value should be stored right after it, i. e. after 4 bytes. Which should be pointed by rbp-24 and indeed that is where 2 is stored. And finally, 3 is stored in the memory address pointed by rbp-20.

So, we see that the address of the first element is rbp-28. So we'd expect this should be reflected in the line int *pa = arr;. And indeed it is -

lea     rax, [rbp-28]
mov     QWORD PTR [rbp-8], rax
Enter fullscreen mode Exit fullscreen mode

lea means load effective address which calculates rbp-28 and stores the address in rax rather than fetching the content of the memory address Β rbp-28 and storing the content. In other words, it just copies the address of the first element in rax register and then in the memory location rbp-8 which is our pa.

Now let's look at int a = arr[1]

mov     eax, DWORD PTR [rbp-24]
mov     DWORD PTR [rbp-12], eax
Enter fullscreen mode Exit fullscreen mode

So here first the content of rbp-24 is loaded into eax and then stored in rbp-12 which is our a. The interesting thing to notice is that the compiler knows the first element of arr is at rbp-28 so when you write arr[1] it directly offsets the base address by and getsrbp-24`. This happens in compile time.

Now let's look at int b = pa[1];

mov     rax, QWORD PTR [rbp-8]
mov     eax, DWORD PTR [rax+4]
mov     DWORD PTR [rbp-16], eax
Enter fullscreen mode Exit fullscreen mode

Here we see first the value stored at rbp-8 is moved to rax. Remember this was our pa variable? So first the value stored at pa is read. Then it is offset by 1, so we get rax + 4 and we read the value at rax+4 and store it to eax. Finally, we store the value from eax to rbp-16 which is the b variable.

The noticeable difference is that it takes one extra instruction in case of pointer. Because array address is fixed, when you write arr, the compiler knows what you're talking about. But a pointer value can be changed. So when you write pa, the value of pa needs to be read first and then it can be used.

Now suppose something like this. You have two files. One contains a global array like

int arr[3] = { 1, 2, 3 };
Enter fullscreen mode Exit fullscreen mode

And in another file, you get carried away by the equivalence of array and pointer and write

extern int *arr;
Enter fullscreen mode Exit fullscreen mode

In other words, you have declared arr as a pointer but defined as an array. What will happen if you write int a = arr[1]?

The answer is - something catastrophic. Let's see why.

Let's assume the array elements are stored just like before -

mov     DWORD PTR [rbp-28], 1
mov     DWORD PTR [rbp-24], 2
mov     DWORD PTR [rbp-20], 3
Enter fullscreen mode Exit fullscreen mode

But in our second file, we are doing arr[1]. So it will do something like

mov     rax, QWORD PTR [rbp-28]
mov     eax, DWORD PTR [rax+4]
mov     DWORD PTR [rbp-16], eax
Enter fullscreen mode Exit fullscreen mode

Can you see the problem? We are reading the content at rbp-28, but the content is 1, the first element of the array. So, essentially we are reading the content of memory address 1+4=5 which is an invalid location!

Bottom line: Don't mix and match.

Another difference is that a pointer name is a variable, but an array name is not. So you can do pa++ and pa=arr but you cannot do arr=pa and arr++

But, there is a case where arrays and pointers are the same. That is in function parameters -

void f(int *pa, int arr[]) {
    int a = pa[1];
    int b = arr[1];
}
Enter fullscreen mode Exit fullscreen mode

What is the difference between arr and pa? There is no difference

int a = pa[1]
mov     rax, QWORD PTR [rbp-24]
mov     eax, DWORD PTR [rax+4]
mov     DWORD PTR [rbp-4], eax

int b = arr[1]
mov     rax, QWORD PTR [rbp-32]
mov     eax, DWORD PTR [rax+4]
mov     DWORD PTR [rbp-8], eax
Enter fullscreen mode Exit fullscreen mode

The compiler treats arr and pa both as pointers, and that's about the only case you can be certain that using pointer in place of array works.

Technically, this is an illustration of an array-like syntax being used to declare pointers, rather than an example of pointers and arrays being the same.

Since pointers are like any other variable, you can have a pointer to pointers too.

int **pa;
Enter fullscreen mode Exit fullscreen mode

Here pa is a pointer to pointer to int. So, *pa will give you a pointer to int, and finally **pa will give you an int

int a;
int *pa = &a; // pointer to int
int **ppa = &pa; // pointer to pointer to int
Enter fullscreen mode Exit fullscreen mode

You can have pointers to array too. But before that, remember [] has higher precedence than *

int (*pa)[3]; // pointer to array of 3 elements
int *pa[3]; // 3 element array of pointer to int
Enter fullscreen mode Exit fullscreen mode

What's the difference between pointer to an array and normal pointer? Consider

int arr[3] = {1, 2, 3};
int (*pa)[3] = &arr;
int *pb = arr;

printf("arr = %p\n", arr);
printf("pa = %p\n", pa);
printf("pb = %p\n", pb);
Enter fullscreen mode Exit fullscreen mode

This prints

arr = 0x7fff2632cc84
pa = 0x7fff2632cc84
pb = 0x7fff2632cc84
Enter fullscreen mode Exit fullscreen mode

So, essentially they all point to the same location. And we already know arr is the same as a pointer to the first element. Now we see that &arr also contains the location of the first element.

Although pa and pb point to the same location, what they point to is very different. pb is a pointer to [int] so it points to a [int] which is the first element of arr whereas pa is a pointer to [array of 3 elements] so it points to an [array of 3 elements] i. e. the whole arr.

This is evident when you try to do arithmetic -

printf("pa + 1 = %p\n", pa + 1);
printf("pb + 1 = %p\n", pb + 1);
Enter fullscreen mode Exit fullscreen mode

This prints

pa + 1 = 0x7fff2632cc90
pb + 1 = 0x7fff2632cc88
Enter fullscreen mode Exit fullscreen mode

pb is a pointer to int. So pb+1 points to the next int 4 bytes after. Whereas pa is a pointer to array of 3 int. So pa+1 will point to the next array of 3 int which is 3 * 4 = 12 bytes after, and indeed, pa+1 is 12 bytes after pa. ( 0x7fff2632cc90 - 0x7fff2632cc84 = 12, these are in hexadecimal in case you're confused).

You can use a pointer to array just like a normal variable. Just remember the precedence -

int arr[3] = {1, 2, 3};
int (*pa)[3] = &arr;

int a = *pa[1]; // Wrong
int b = (*pa)[1]; // Correct
Enter fullscreen mode Exit fullscreen mode

The easiest way to remember is "Declaration follows usage." So the usage of a pointer will look like the way it was defined. Since we defined pa as (*pa)[], its usage will also look the same.

One common mistake that students do, with the fact that arrays decay down to pointers in function parameters is working with multidimensional arrays.

If you have something like

int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
f(arr);
Enter fullscreen mode Exit fullscreen mode

you might think since array names decay to pointers in function parameter, an array of array should decay to a pointer to pointer. So you might write the declaration of f as

void f(int **m) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this is wrong and will give a warning (but will compile)

main.c:22:8: warning: passing argument 1 of β€˜f’ from incompatible pointer type [-Wincompatible-pointer-types]                                                   
main.c:11:5: note: expected β€˜int **’ but argument is of type β€˜int (*)[4]’
Enter fullscreen mode Exit fullscreen mode

What happened here? It's easy.

If an array of [int] decays down to a pointer to [int], what should an array of [array of int] decay down to? Of course a pointer to [array of int]. Remember that the size is also part of arrays type. So, in our case, arr is an array of [4 element array of int]. So, it decays down to pointer to [4 element array of int].

So you should write

void f(int (*m)[4]) {

}
Enter fullscreen mode Exit fullscreen mode

Or, you can just take an array of array

void f(int m[][4]){
}
Enter fullscreen mode Exit fullscreen mode

Note that only the size of the rightmost column is required in the formal parameters list.

Pointers and Structures and Unions

Now we move on to struct and union. We can have pointers to them too.

struct some_struct {
    int p;
    int q;
}

struct some_struct a;
struct some_struct *pa = &a;
Enter fullscreen mode Exit fullscreen mode

Or if you prefer a typedef

typedef struct some_struct {
    int p;
    int q;
} some_struct;

some_struct a;
some_struct *pa = &a;
Enter fullscreen mode Exit fullscreen mode

An interesting situation occurs when you want to access members of struct using pointer. Suppose you want to access the member p through pa. You might do

int k = *pa.p;
Enter fullscreen mode Exit fullscreen mode

Except, this doesn't do what you expect. The operator . has higher precedence than * so *pa.p is same as *(pa.p). So instead of dereferencing pa and then accessing the member p, you end up accessing the member p and then dereferencing it. But pa doesn't have a member p. So, it gives a compiler error.

Instead, you want to write this

int k = (*pa).p;
Enter fullscreen mode Exit fullscreen mode

Which works the way you want. But writing this is tedious, and turns out that we write this so much that they have a special operator ->

int k = pa -> p;
Enter fullscreen mode Exit fullscreen mode

pa -> p is same as (*pa).p but looks neat and clean.

The case of unions is a little bit involved. Quoting cppreference -

A pointer to a union can be cast to a pointer to each of its members (if a union has bit field members, the pointer to a union can be cast to the pointer to the bit field's underlying type). Likewise, a pointer to any member of a union can be cast to a pointer to the enclosing union.

What it means is that, if you have a pointer to a union, you can cast it to any of its members, and vice versa. Take a look

typedef union some_union {
        int p;
        char q;
    } some_union;

some_union a = {1}; // Initialize a with p = 1
some_union *pa = &a;
printf("%d\n", pa -> p); // Access p through pointer to a

int * pb = (int *) pa; // cast pa to point to p directly
printf("%d\n", *pb);
Enter fullscreen mode Exit fullscreen mode

This prints

1
1
Enter fullscreen mode Exit fullscreen mode

Here, I could cast the pointer to a to an int* and it automatically pointed to the member p. Similarly, if I had cast it to char* it would point to q.

Conversely, if I had a pointer to p, I could cast it to a pointer to some_union and it would point to a

int *pc = &(a.p);
some_union *pd = (some_union *)pc;
a.q = 'a';
printf("%c\n", pd -> q);
Enter fullscreen mode Exit fullscreen mode

This prints a as expected.

Ministry of Pointer Magic

Pointers and Function

It is possible to have pointers to functions too. Remember that the return type, the number of parameters it takes, and the type of each parameter - these 3 are parts of the type of a function. Hence you must provide these during pointer declaration. Also worth noting () has higher precedence than *

int *f(); // a function that returns a pointer to int
int (*f)(); // a pointer to a function that takes no argument and returns an int
int (*f)(int); // a pointer to a function that takes an int and returns an int
Enter fullscreen mode Exit fullscreen mode

A pointer to function can be used just like other pointers -

int f(int a) {
    return a+1;
}

int main()
{
    int (*fp)(int) = &f;
    printf("%d\n", (*fp)(1));
    // printf("%d\n", *fp(1)); Wrong~ Won't cpmpile
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

This prints 2 as you'd expect.

Remember I talked about declaration follows usage? Well, turns out that in the case of pointer to functions, that rule can be ignored. For example, this works

printf("%d\n", (**fp)(1));
Enter fullscreen mode Exit fullscreen mode

And so does this

printf("%d\n", (*********fp)(1));
Enter fullscreen mode Exit fullscreen mode

And weirdly enough, this too

printf("%d\n", fp(1));
Enter fullscreen mode Exit fullscreen mode

So, in the case of functions, not only you can dereference as many times as you want, you can drop the dereferencing altogether and just use the pointer as if it were a function itself. Another one of those C quirks.

Finally, you can get wild with pointers and arrays and function like

char *(*(*foo)[5])(int); // foo is a pointer to array of 5 elements of pointer to a function that takes an int and returns a pointer to char
int *(*foo)(int *, int (*[4])()); // foo is a pointer to function (that takes a pointer to int and a 4 element array of pointer to functions that return int) and returns a pointer to int
Enter fullscreen mode Exit fullscreen mode

You get the idea. Yes, it can get pretty messy, but once you know the syntax, and you have cdecl, you can easily breeze through them (or read my article)

As for how you can use a function pointer. here's an example of a simple calculator

#include <stdio.h>

float add(float a, float b) { return a+b; }
float sub(float a, float b) { return a-b; }
float mul(float a, float b) { return a+b; }
float divide(float a, float b) { return a/b; }


int main()
{
    float (*arr[4])(float, float) = { add, sub, mul, divide };
    int n;
    float a, b;
    printf("Enter two numbers: ");
    scanf("%f%f", &a, &b);
    printf("Enter 1 for addition, 2 for subtraction, 3 for multiplication, 4 for division: ");
    scanf("%d", &n);
    printf("%f", arr[n-1](a, b));

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

We are storing all 4 operations in an array and when the user enters a number, we call the corresponding operation.

Qualified types

Each type in C can be qualified by using qualifiers. In particular we have 3 - const, volatile, and restrict. Here we will look at const and restrict.

Adding const to a type effectively marks it read-only, so that attempting to change the value will result in a compiler error.

const int p = 1;
p = 2; // error: assignment of read-only variable β€˜p’
Enter fullscreen mode Exit fullscreen mode

Turns out, int const and const int both are valid. Now if I throw pointers into the party, I get some fun stuff

const int *p;
int * const p;
const int * const p;
Enter fullscreen mode Exit fullscreen mode

Can you guess which one is what?

To untangle this, we will remember some_data_type *p declares p to be a pointer to some_data_type

Hence, const int *p can be thought of as (const int) *p. So that p is a pointer to a const int. It means, whatever p is pointing to is a const int and you cannot change that. However, p itself is not const and can be changed.

const int a = 1;
const int b = 2;
const int *p = &a;

p = &b; // works. You can change p
// *p = 3; error: assignment of read-only location β€˜*p’
Enter fullscreen mode Exit fullscreen mode

For the second one compare it with int const p which declares p as a read only int. So, int * const p should declare p as read-only int*. This means the pointer itself is const and you can't change p , but you can change what p is pointing to.

int a = 1, b = 2;
int * const p = &a;

*p = 2; // Works. you can change *p
// p = &b; error: assignment of read-only location β€˜p’
Enter fullscreen mode Exit fullscreen mode

And finally, const int * const p declares that both p and *p are read-only. So you can neither change p nor *p.

const int a = 1, b = 2;
const int * const p = &a;
// *p = 2; error: assignment of read-only location β€˜*p’
// p = &b; error: assignment of read-only location β€˜p’
Enter fullscreen mode Exit fullscreen mode

Now let's look at the restrict keyword. The restrict keyword, when applied to a pointer p, tells the compiler that as long as p is in scope, only p and pointers directly or indirectly derived from it (e. g. p+1 ) will access the thing it's pointing to.

Confused? Let's see an example.

void f(int *p, int *q, int *v) {
    *p += *v;
    *q += *v;
}
Enter fullscreen mode Exit fullscreen mode

Here is the assembly generated after enabling optimization -

mov     eax, DWORD PTR [rdx]
add     DWORD PTR [rdi], eax
mov     eax, DWORD PTR [rdx]
add     DWORD PTR [rsi], eax
ret
Enter fullscreen mode Exit fullscreen mode

The problem in this function is, p, q and v might point to the same location. So that when you do *p += *v, it might happen that *v also gets changed because p and v were pointing to the same location.

This is why *v is first loaded into eax by mov Β  Β  eax, DWORD PTR [rdx]. Then it is added to *p. Again, we have to load *v because at this point, we are not sure if *v has changed or not.

Now if I update the function as follows -

void g(int *restrict p, int *restrict q, int *restrict v) {
    *p += *v;
    *q += *v;
}
Enter fullscreen mode Exit fullscreen mode

the compiler is free to assume that p, q and v all point to different locations, and can load *v only once, and indeed it does

mov     eax, DWORD PTR [rdx]
add     DWORD PTR [rdi], eax
add     DWORD PTR [rsi], eax
Enter fullscreen mode Exit fullscreen mode

Note that it is up to the programmer to guarantee that the pointers do not overlap. In case they do, it is undefined behavior.

You can read more about restricthere.

That's probably enough for one post. To quickly recap, you have learned -

  1. What pointers are.
  2. How to declare and dereference pointers.
  3. Pointer arithmetic.
  4. Pointer comparison.
  5. Pointer to array and array of pointers.
  6. Pointers and arrays are not the same.
  7. Pointer arithmetic and array indexing are equivalent.
  8. Array in function parameter decay to a pointer.
  9. Pointers to a multidimensional array.
  10. Pointers to structures and unions.
  11. Pointer to functions.
  12. Pointer to wild exotic types.
  13. const and restrict with pointers.

Note that most of these hold true in C++ also, although you have minor changes, and some new stuff like smart pointers.

Hopefully, you learned something new in this post. Subscribe to my blog for more.

Top comments (0)

Find what you were looking for? Sign up so you can:

Β 
🌚 Enable dark mode
πŸ”  Change your default font
πŸ“š Adjust your experience level to see more relevant content