(Almost) Everything You Need To Know About Pointers in C
Know the fundamentals of pointers in C to better understand the quirks and enable writing efficient 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);
}
which just prints
5
5
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);
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);
}
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);
}
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;
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;
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;
Note that the asterisk need not be adjoined to the variable name. Any of these is valid -
char* pa;
char *pa;
char * pa;
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
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
So, quickly revise -
pa
is the value of the pointer, which is the address ofa
.*pa
is the value of the thingpa
is pointing to, in this casea
.
One more time.
pointer_name
is the value of the pointer itself.*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
Now we can rewrite the swap function as follows -
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
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;
}
It prints (ignore the compiler warning)
I
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
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;
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;
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;
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);
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
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 );
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;
}
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 -
- Addition (and hence subtraction) of an integer constant to a pointer.
- 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);
This prints
pa = 0x7ffdd7eeee64
pa + 1 = 0x7ffdd7eeee68
pa - 1 = 0x7ffdd7eeee60
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);
This prints
pa = 0x7ffec09d685c
pb = 0x7ffec09d6864
pb - pa = 2
pa - pb = -2
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 -
- If I have
some_data_type *p
, thenpa + n
increments the pointer byn * sizeof(some_data_type)
bytes. - If I have
some_data_type *p, *q
thenp - q
is equal to the difference in bytes divided bysizeof(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;
In order to answer, you have to remember the precedence -
- Postfix
++
and--
have higher precedence than*
- 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);
This prints
Before:
a = 5
p = 0x7ffe82ae9eb0
After
a = 5
p = 0x7ffe82ae9eb4
x = 5
++*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);
This prints
Before:
a = 5
p = 0x7fff1484e210
After
a = 6
p = 0x7fff1484e210
x = 6
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);
This prints
Before:
a = 5
p = 0x7ffd4bad9c90
After
a = 5
p = 0x7ffd4bad9c94
x = 32765
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 expressionQ
points to the last element of the same array object, the pointer expressionQ+1
compares greater thanP
. 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");
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");
}
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);
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);
This prints
pa = 0x7ffef7706bb0
pb = 0x7ffef7706bb0
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
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];
}
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
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
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
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
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 };
And in another file, you get carried away by the equivalence of array and pointer and write
extern int *arr;
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
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
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];
}
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
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;
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
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
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);
This prints
arr = 0x7fff2632cc84
pa = 0x7fff2632cc84
pb = 0x7fff2632cc84
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);
This prints
pa + 1 = 0x7fff2632cc90
pb + 1 = 0x7fff2632cc88
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
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);
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) {
...
}
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]’
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]) {
}
Or, you can just take an array of array
void f(int m[][4]){
}
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;
Or if you prefer a typedef
typedef struct some_struct {
int p;
int q;
} some_struct;
some_struct a;
some_struct *pa = &a;
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;
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;
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;
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);
This prints
1
1
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);
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
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;
}
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));
And so does this
printf("%d\n", (*********fp)(1));
And weirdly enough, this too
printf("%d\n", fp(1));
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
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;
}
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’
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;
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’
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’
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’
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;
}
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
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;
}
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
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 restrict
here.
That's probably enough for one post. To quickly recap, you have learned -
- What pointers are.
- How to declare and dereference pointers.
- Pointer arithmetic.
- Pointer comparison.
- Pointer to array and array of pointers.
- Pointers and arrays are not the same.
- Pointer arithmetic and array indexing are equivalent.
- Array in function parameter decay to a pointer.
- Pointers to a multidimensional array.
- Pointers to structures and unions.
- Pointer to functions.
- Pointer to wild exotic types.
const
andrestrict
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.