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

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 void f(int a) < a = 10; >int main()
Enter fullscreen mode

Exit fullscreen mode

which just prints 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()
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()
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

  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 .
  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)
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 int main()
Enter fullscreen mode

Exit fullscreen mode

It prints (ignore the compiler warning)

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 * i s 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 #include 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

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

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

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

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

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 == , =, > if they both are pointers of the same type, and of the same array or same aggregate object. Otherwise, it is undefined behavior.

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.*

typedef struct some_struct < int p; int q; >some_struct; some_struct a = ; 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
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

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] = ; 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 int main() < int arr[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] = .

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 gets rbp-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[])
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] = ; 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

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

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] = ; 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] = , , >; 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 = ; // 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);