0x00000000001
0x00034AEEFC2
0xDEADBEEF
0x3A39AFE45
As we can see, these addresses all have some things in common.
Because they are hexadecimal numbers, we start them with the characters 0x
.
These characters tell us that the following number is represented in base 16, or hexadecimal.
Because the number is hexadecimal, it is composed of the following characters: 0 1 2 3 4 5 6 7 8 9 A B C D E F
.
int x = 44; #32 bit integer
int y = 13; #32 bit integer
long int ex = 4398; #64 bit integer
double dub = 188.1114; #64 bit float
Creating variables in our code on the stack is very simple and does not require any additional steps.
This is known as static memory allocation because we do not need to do anything to allocate memory for these variables.
Our program compiler takes care of this step for us because it knows based on the type of our variable how much memory it needs to allocate on this stack.
For example,
int x = 44;
is easy for the compiler to figure out.
It knows that the type is int, which needs 4 bytes of memory regardless of what the value is that we want to store.
We can even reassign this variable to a different value later and the amount of memory needed doesn't change.
int x = 43;
printf("%p \n",&x);
If we run this code, we'll find that the printf statement will print a value that looks like a memory address.
If you are confused by the printf statement, the following is a video explaining printf in C.
&
symobol means we want to access the location of the variable x.
We'll use this knowledge now to create a pointer.
Pointers are as simple as they sound; a pointer is a variable created to provide a reference to an address in memory we want to access.
This address then provides us access to something in memory we wish to work with more.
Let us consider an example of a pointer and break down it's components.
int x = 43;
int* y = &x;
On this first line, we create a variable in the same way we normally would.
In this line we assign the value of 43 to the int variable x.
Now lets say we want to be able to create a pointer to x.
To do this we say int*.
The * here tells our compiler that what we are trying to create is a pointer.
We then set our pointer variable y to be equal to a reference to x (&x).
The & here means we want to make a reference to the variable x.
What we are saying here is we want to create an int pointer y that is equal to the address of x.
This allows us to do two different things.
It allows us to access the value that has been assigned to the variable x through the reference.
It also assigns y a value of the address where our variable x has been stored.
Let us continue looking at our example from before:
int main(){
int x = 43;
int* y = &x;
printf("Value at y = %p \n", y);
printf("Value at x = %d \n", x);
printf("Value at &y = %d \n", *y);
return 0;
}
This code will print the following three lines when it is printed:
> Value at y = 0x3A39AFE45
> Value at x = 43
> Value at *y = 43
Here we see that our pointer y can be used for two things.
When we state *y this means we can access the VALUE that our pointer is pointing to.
When we do this, we get the value stored at the memory address our pointer is pointing to.
In this case it is the value assigned to the variable x, 43
.
When we state y by itself then we get the value assigned to y, which as it's a pointer, is the address in memory we're pointing to.
In this case, we get the value 0x3A39AFE45
because thats the address in memory where our variable x is located.
int main(){
int datum = 300;
int* ptr = &datum;
int ptrval = *ptr;
printf("%d \n",datum);
printf("%p \n",ptr);
printf("%d \n",*ptr);
printf("%d \n",ptrval);
return 0;
}
300
0xAEC43532 #(the address of datum)
300
00
int main(){
int val = 5;
int *pval = &val;
int **d_pval = &pval;
printf("Value of val = %d\n", val);
printf("Value of val using single pointer = %d\n", *pval);
printf("Value of var using double pointer = %d\n", **d_pval);
return 0;
}
>Value of val = 5
>Value of val using a single pointer = 5
>Value of val using a double pointer = 5
As we can see in this code we are defining three different variables.
val is a simple integer with value 5.
pval is a simple pointer much like what we've seen in section 2.
d_pval is a pointer that we have defined to point to our first pointer.
This is a pointer to a pointer or a double pointer.
Let's consider another example, this time in terms of what's happening in memory.
As we can see in the image below we have var which is a variable with value 10.
ptr1 is a regular pointer.
Its value is the memory address (location) of our variable var.
ptr2 is a double pointer.
Its value is the memory address (location) of our pointer ptr1.
int main(){
int a = 10;
char b = 'x';
// void pointer holds address of int 'a'
void *p = &a;
// void pointer reassigned to hold address of char 'b'
p = &b;
}
In this code, we define two variables a and b, with a being an int and b being a char.
We also have defined our void pointer, named p.
At first, we see that p has been assigned a reference to our variable a.
However, we can seamlessly reassign p to a reference to our variable b, despite the fact that a and b are different types of data.
This is great because we can access and reassign our pointer any time we want to anything we want.
Getting access to the address we are pointing to is easy.
However, getting access to the value that we are pointing at is tricky.
If we were to run the code below we would receive an error from the C compiler.
Relevant Links:
printf("%d", *p);
This is because C needs to know the type of the data we are referring to when we dereference the pointer to get the value we are pointing to.
Instead, when we need access to the value we are pointing to we need to use the following code wherein we specify the type we are referring to when we dereference the pointer.
printf("%d",*(int*)ptr);
int main(){
int arr[2] = {34,56};
void* parr = &arr;
printf("%d",*(int*)parr);
parr = parr+sizeof(int);
printf("%d",*(int*)parr);
return 0;
}
In this example, we see that inside of our function, we first define an array containing two integers, 34 and 56.
When we define a pointer to this array, we use a void pointer once more.
The void pointer starts out pointing to the 0th index of the array.
We can see this when we run the code and receive the following outputs.
>34
>56
The first print statement prints out 34 because our pointer initially references the address of the 0th index of our array.
Then we modify our pointer by adding the size of an integer.
This shifts the pointer to point to the next address in memory or the next element in the array.
We can do this for arrays of any other type. We can also use this to shift by larger amounts or structs.
We can even use this to iterate over arrays or other data structures.
We will see some of this in section 4 where we explore Malloc and heap memory allocation.
int main(){
int* p = (int*) malloc(400);
return 0;
}
Here we have define a pointer utilising malloc to allocate some memory on our heap.
Specifically we have allocated 400 bytes.
Since an integer is 4 bytes this is room for an array of 100 integers.
If we wanted to allocate a different number of integers or just speficy the number of integers we could instead do:
int main(){
int* p = (int*) malloc(200*sizeof(int));
return 0;
}
This code should work to allocate a section of memory.
However, sometimes it is possible we will have run out of memory on the heap that we can allocate.
When this happens our pointer will fail to be allocated.
We can check this with the following:
int main(){
int* p = (int*) malloc(200*sizeof(int));
if (p == NULL) {
printf("memory failed to allocate. \n");
}
return 0;
}
If our memory has been allocated correctly, we can now populate the array and access the array elements using our pointer and pointer arithmetic like we saw in the last section.
For example, lets set each of the elements in an array of size 200 to be equal to the numbers 1-200 in order.
int main(){
int i;
int* p = (int*) malloc(200*sizeof(int));
if (p == NULL) {
printf("memory failed to allocate. \n");
}
else{
for(i = 0; i < 200; i++){
p[i]=i+1;
}
}
//print the elements
for(i=0; i< 200; i++){
printf("%d, ",p[i]);
}
return 0;
}
In this code block we have allocated 800 bytes for 200 integers, populated the memory, and printed out the results to our terminal.
When we execute this code, we will receive the numbers 1 to 200 printed out.
int main(){
int i;
int* p = (int*) malloc(200*sizeof(int));
if (p == NULL) {
printf("memory failed to allocate. \n");
}
else{
for(i = 0; i < 200; i++){
p[i]=i+1;
}
}
//print the elements
for(i=0; i< 200; i++){
printf("%d, ",p[i]);
}
//dynamically deallocate memory
free(p);
p=NULL;
return 0;
}
Here the code is almost the same until the last two steps.
The first step is calling free() a function built into C.
free() takes in our pointer and frees the memory that has been allocated using the pointer.
Secondly, we set our pointer to be equal to NULL.
This means that we cannot try and access the memory after it has been deallocated which can be done and should be avoided.
This can cause severe issues including potential security issues.