Pointers in C/C++ |
||
---|---|---|
BasicsImportantThe codes in examples are executed on Linux (Ubuntu 16.04 64 bit) with g++ version 5.4.0 (thread model- Posix). Here, when we refer to 'memory', it means we are talking about only RAM, as program is executed here only. Before understanding pointers, first understand how memory is handled by C/C++ compiler. Let us see a loosely connected example. Consider these situations.
The post-men in first option are our normal variables defined in our code The post-man in second option is our pointer variable defined in our code. A pointer is a variable which stores the address of another variable of same-data-type, to access their values. |
VariablesVariables are locations in the computer's memory which can be accessed by their identifier (their name). This way, the program does not need to care about the physical address of the data in memory; it simply uses the identifier whenever it needs to refer to the variable. Or we can say that whenever we run the code, the memory is allotted to variables and their addresses are assigned to the identifiers. When a variable is initialized, the memory needed to store its value is allocated at a specific location in memory. Generally, C/C++ programs do not actively decide the exact memory addresses where its variables are stored. Fortunately, that task is left to the environment where the program is run - generally, an operating system that decides the particular memory locations on runtime. However, it may be useful for a program to be able to obtain the address of a variable during runtime in order to access data cells that are at a certain position relative to it. |
Address-of Operator(&)Pointers are special kind of variables which store address of another variable of same type. The address of any variable can be obtained by putting '&' (address-of operator) before it. Type the following C code:
Output
0x7fff35b1ee34 or 900853300 is the address of variable 'a'. (Value of address will vary every time you run the code.) Pointers have huge advantage as they store address of other variables. We will discuss about them later, first we should learn how to use them. For our ease, in rest of our examples and codes, we will use
|
Declaring a PointerGeneral Method: <data-type> *<identifier> Example:
As defined above- Pointers are special kind of variables which store address of another variable of same type. Here 'ptr1'; is a pointer variable of integer type. Hence it can store addresses of only integer variables. 'ptr2'; is a pointer variable of float ype. Hence it can store addresses of only float variables. 'ptr3'; is a pointer variable of character type. Hence it can store addresses of only character variables. |
Storing address in a PointerRun this code. (Note: the value of address will vary on every system and every-time you run code.)
Output
|
De-reference/ indirection operator (*)De-referencing means accessing value stored at particular address. This operator has two usages, to declare a pointer and to access the data pointed by the pointer, i.e. access the value, stored in variable, whose address is with the pointer. For example, in previous code, the variable 'a' has value 10. The address of 'a' is stored in 'ptr1'.
So
Output
|
Pointer pointing to a pointerOR Pointer to pointerORDouble pointer/ Triple pointerPointer to a pointer means, "A pointer which stores the address of another pointer". This may sound confusing, but take it in simple way. The concept of pointers is to store address of variables of same data type. As we defined, "Pointers are special kind of variables which store address of another variable of same type". But, Pointers are also variables. Though the value, inside them is an address, but this value can be changed. For example:
Output
Coming back to the topic. We understood the theoretical concept of pointer-to-pointer, now we will use this concept to implement double, triple pointer and so on. Let's have some variables. Note:
This is because, single-pointer: it has to point a data. Double-pointer: it has to point a pointer, which in points a data. Triple-pointer: it has to point a pointer, which again points another pointer, which points a data And this chain extends and goes same for multi-dimensional pointers.The de-reference/ indirection operator (*): to understand things more clearly, interpret it as: '*' - go to the address and access the value.
|
Size of PointerSince whole and soul purpose of pointers is to store address, they must contain an integral value. Hence their size is equal to that of 'int' or 'long', this depends on system and compiler. On my system (specs given in introduction), it is equal to 'long'.
char*, float*, int*, all are of same size and contain integral value. Though they are defined specifically as 'char', 'float', 'int' etc. This is done so that compiler should know that what kind of data it is going to point to. So, it becomes easy for it to de-reference it. This is all done to handle data properly. Let's look at the code:
Output
|
Pointer pointing to an arrayIn normal conditions too, arrays are implemented using concept, similar to pointers.
Here the 5-consecutive memory-blocks for 'int' type are allotted and address of 1st element is stored in variable 'arr'. Accessing first element will give access to other elements as well because their memory addresses are in consecutive order. Let us learn through various codes:
Output
Both are identical. Hence it is clear that identifier of array stores the address of 1st element of the array. Thus, if we store the address, stored in 'arr', into a pointer, it should also work well. Let's try:
Output
Note: We didn't used '&' while initializing pointer 'b'. It is because 'arr' itself is storing address of 1st element of the array. Hence, 'arr' is equivalent to 'b' but NOT equal to 'b'. This code does't works:
Compilation Error:
As said, 'arr' is equivalent to pointer 'b' (here, 'b' is referred to the pointer vaiable declared in previous working example.), but NOT equal to pointer 'b'. As once allotted, the address stored in 'arr' cannot be changed. |
Arithmetic operations on PointersConsider this code:
Output
Confused?Now it is obvious to think that there is something wrong. If the hypothetical address of first element of 'arr' is 8542, you will think: b = 8542 *b = 3 b + i = 8543 *b = <some-garbage-value> Right?? Wrong. Now this is where declaring the data-type of pointer comes into play. Basically: b + 1 ≠ 8543 The compiler knows 'b' is a pointer and it also knows that 'b' is an integer pointer. Hence it knows that integers are consist of 4 consecutive bytes (size of (b + 1) = 8542 + sizeof(int) In General: <pointer + 1> = <address-value-inside-pointer + sizeof(data-type-of-pointer)> In Linux, 64 bit, gcc/g++ compiler, size of
|
Some more ResultsThis code works:
Output
Though 'b' is a pointer, but above results conclude:
What will happen if we prefix &i with dereference operator? & or address-of operator returns the address of the variable to which it is added as prefix. And the work of dereferenc/ indirection operator is to go to the address and access the value. So, if we prefix &i with dereference/ indirection operator, then * will nullify the effect of address-of (&) operator.
Since b is a pointer and *(b + i) works same as b[i]. Thus, *(a + i) should also work. as 'arr' is equivalent to a pointer. This code works:
Output
This again confirms the similarity between pointer and array. As, though it is a normal array, the de-reference/indirection operator works perfectly on 'arr'. This is because 'arr' stores the address, and the work of dereference/ indirection operator is to go to the address stored in it and access the value. This code works:
Output
VER VERY IMPORTANT NOTE:'b' is pointing to element of the array 'arr'. Not the whole array. Not to be confused. So when you print *(b + i) or something equivalent. Do check that the address doesn't go out of range of array. It will not show error, but it will display some garbage value present at that unused memory. For example, for the above code if we try to print, arr[6] or *(b + 6), it will not show error, but it will show the value present at that address. |
Different types of Pointersint *ptr;
Output
Conclusion:
const int *ptr; or int const *ptr;
Compilation error
Conclusion:
int *const ptr;
Output
BUT!!
Compilation error
Conclusion:
const int *const ptr;
Compilation error
Conclusion:
|
Implementation of 2-D arrayOkay so till now we know that 2-D array consists of rows and columns. But do they exactly look like a matrix on the memory? We refer to consecutive blocks for 1-D array, matrix for 2-D array, cube for 3-D array. But then what for 4-D, 5-D and further dimensional array? Note: We "Refer". Actually, all different multi-dimensional arrays are stored in form of 1-D array, i.e. consecutive blocks of memory. That is something huge to digest, but this is how everything works. Let us consider this code first for better understanding.
Output
First let us take look a on the addresses of arr2[i][j]. They are all consecutive. Hence, we can conclude that a single chain of blocks (each of 4 bytes) is allotted to our 2-D array, same can be checked for 3-D or 4-D array.
So, what is happening? ⇴ When you access arr2[0][0], for you it is, 1st column of 1st row. But for C/C++ it is, 1st element of 1st memory group of 5 ('5' because we have defined number of columns to be five). ⇴ When you access arr2[1][3], for you its 4th element of 2nd row, but for C/C++ it is 4th element of 2nd memory group of 5. When it reads arr2[1], it jumps 5 blocks from 1st element, i.e. it reaches to 2nd memory group and then access its 4th element. Note: Do not get confused with row-major-form and column-major-form. That has to do something with how you have stored data. But memory is allotted in the same sequence depicted above. And this is also confirmed by the above code. As we said before, C/C++ is a language, not for your machine, but for your compiler. Observations:
Conclusion
We saw, 2-D array also work on the concept similar to pointers. Means they should implement arithmetic operations. They do, but not it that simple way.
Consider this code:
Output
Result: First we accessed the jth column of ith row, using *(*(arr2 + i) + j) while storing values. Then we accessed it using arr2[i][j] while printing. That means: arr2[i][j] is same as *(*(arr2 + i) + j) |
Pointer to a 2-D arrayNow we know from above examples that how 2-D array is implemented on our memory. So, we will try to access 2-D array with the help of a pointer. Note: This is different from what we saw just above. Because as we told, C/C++ is a language for your compiler. When you declare a 2-D array arr2[3][5], C/C++ knows that arr2 is the identifier of given 2-D array and the meaning of arr2[i] is to return the address of 1st element of ith row. But when we store the value of arr2 in a pointer, pointer does not know what is 2-D or 3-D array, it only knows to point data and how to access it. It does not know that for 2-D array, ptr[i] has to return the address of ith row. For pointer 2-D array is same as 1-D array, because it is saved in the same form in the memory, a consecutive sequence of integer bytes. Thus, ptr[i] is same as *(ptr + i). Thus it will go at that address and access its value. But, we know how 2-D array is saved in our memory (in form of 1-D array), we formulated this equation. For 3 x 5 2-D array, (i,j)th element will be *(ptr + (i * 5) + j) where 0 ≤ i < 3. For general: *(<pointer_name> + (i * no_of_cols) + j) where 0 ≤ i < no_of_rows and 0 ≤ i < no_of_cols.
Output
Though it shows warning for incompatible pointer assignment on some compilers. But it works well. Correct way:
|
ptr[0] | is same as | *(ptr + 0) | is same as | arr2[0] | contains address of arr2[0][0] |
ptr[i] | is same as | *(ptr + i) | is same as | arr2[i] | contains address of arr2[i][0] |
Now as we know that *(ptr + i) will return the address of 1st element of ith row, dereferencing it again will access the value at that place.
Thus, *(*(ptr+i)+j) = ptr[i][j] - same as- arr2[i][j], will access the (i,j)th element.
Note: There is no specific method. After knowing pointers and their working, we are just playing with one or the other method.
Dynamic AllocationAs, now we know how to handle pointers and how to deal with them. We can now make real use of them, that is Dynamic Allocation of memory. For example, the arrays (integer array, character array, structure array etc.) you define in your code, are static. Once defined, their size cannot be modified during run time. Suppose you made a program for general condition that inputs elements in an array. You define an empty array in your program with a fixed size. But what, if the fixed size is less than the number of elements you want to input in one of the test conditions? Will you again modify your code and re-compile it? So, here we take advantage of dynamic allocation. Through this we can ask the user that how much memory is needed, and then it will be allocated. C has 3 predefined functions for allocating memory, and free() for de-allocation, under "stdlib.h" header file. Note: here we will discuss the general working of these functions. For other exceptions and return values, read the documentation of your compiler. |
malloc()malloc()
void*We know how to declare pointer: <identifier> *<data-type>. |
calloc()calloc() It accepts the number of elements of array, and size of each element of the array as a parameter. It itself calculates the number of bytes to be allocated and rest working is same as of malloc. |
realloc()realloc() It re-allocates the memory, allocated by malloc/ calloc. It must be previously allocated by malloc(), calloc() or realloc() and not yet freed with a call to free or realloc. Otherwise, the results are undefined. The reallocation is done by either:
If there is not enough memory, the old memory block is not freed and NULL pointer is returned. |
free()To understand this, we should understand the memeory map of C program.
Now all other memory blocks all handled by C, except heap. Heap is dedicated to be handled by user. Memory is allocated and freed, during run time, on function calls, by C for static variables. But it's not same for heap. Once you allocate a memory, it is stored on heap, and will remain on heap till you free it or whole program is executed, because as we told, heap is dedicated to be manipulated by user.. For small programs, it is affordable but think of a situation where there is running a loop, and again and again malloc is called inside loop. If you will not free the memory after using it, a time will come when whole heap will be filled and no more space will be left. This is called as 'memory leak'. So, to avoid it, always free the memory after it is used. |
Implementing
|
Dynamic allocation of 1-D array
Output
|
Dynamic allocation of 1-D array using
|
Dynamic re-allocation of 1-D array using
|
Dynamic Allocation of 2-D arraymalloc() or calloc() malloc() calloc() The dynamically allocated 2-D array is very much different than static one. Implementing dynamic allocation of 2-D array or 3-D array etc., is more like playing with pointers. To do this it is required that you read everything taught above in this website. The dynamically allocated 2-D array is very much different than static one. Implementing dynamic allocation of 2-D array or 3-D array etc., is more like playing with pointers. To do this it is required that you read everything taught above in this website. Pseudo code:
What happened? First, we declared double pointer, **ptr2. In line 3, we allocated a 1-D array with 'm' elements and address of its first element is stored in ptr2. Now, the special thing is, each element of this 1-D array is an integer-pointer. Note: We wrote Now, in line 4, through loop, we created one 1-D array with 'n' integer elements in one iteration. And address of first element is stored in ptr2[i] or *(ptr2 + i). And we this for all possible values of i. Let us assume
|
Pointer Variable. | ||||||
Integer Variable. | ||||||
→ | ||||||
ptr2[0] | 1000 | 1004 | 1008 | 1012 | 1016 | |
→ | ||||||
ptr2[1] | 1020 | 1024 | 1028 | 1032 | 1036 | |
→ | ||||||
ptr2[2] | 1040 | 1044 | 1048 | 1052 | 1056 |
We know how 2-D stay is allocated on memory, so using pointers we tried to map something same.
Thus to access (i,j)th element, we first need to access ith row. Therefore, we do *(ptr2 + i).
Now to access jth column of ith row, we do *(*(ptr2 +i) + j).
Now we have seen earlier,
<name
>[i] is evaluated as: *(<name
> + i)
<name
>[i][j] is evaluated as: *(*(<name
> + i) + j).
Thus for above pseudo code, the (i,j)th element can be accessed through ptr2[i][j].
#include <stdio.h> #include <stdlib.h> int main() { int **ptr2 = NULL, i, j; int m = 3, n = 5; ptr2 = (int**)malloc(sizeof(int*)* m); if(ptr2 == NULL) { printf("Error: Could not allocate memory."); return 0; } for(i = 0 ; i < m; i++) ptr2[i] = (int*)malloc(sizeof(int)* n); for(i = 0 ; i < m; i++) printf( "\nAddress of ptr2[%d]: %u", i, &ptr2[i]); for(i = 0 ; i < m; i++) for(j = 0 ; j < n; j++) printf( "\n\tAddress of ptr2[%d][%d]: %u", i, j, &ptr2[i][j]); for(i = 0 ; i < m; i++) free(ptr2[i]); free(ptr2); return 0; }
Address of ptr2[0]: 25141264 Address of ptr2[1]: 25141272 Address of ptr2[2]: 25141280 Address of ptr2[0][0]: 25141296 Address of ptr2[0][1]: 25141300 Address of ptr2[0][2]: 25141304 Address of ptr2[0][3]: 25141308 Address of ptr2[0][4]: 25141312 Address of ptr2[1][0]: 25141328 Address of ptr2[1][1]: 25141332 Address of ptr2[1][2]: 25141336 Address of ptr2[1][3]: 25141340 Address of ptr2[1][4]: 25141344 Address of ptr2[2][0]: 25141360 Address of ptr2[2][1]: 25141364 Address of ptr2[2][2]: 25141368 Address of ptr2[2][3]: 25141372 Address of ptr2[2][4]: 25141376
Pointer Variable. | ||||||
Integer Variable. | ||||||
→ | ||||||
ptr2[0]25141264 | ptr2[0][0]25141296 | ptr2[0][1]25141300 | ptr2[0][2]25141304 | ptr2[0][3]25141308 | ptr2[0][4]25141312 | |
→ | ||||||
ptr2[1]25141272 | ptr2[1][0]25141328 | ptr2[1][1]25141332 | ptr2[1][2]25141336 | ptr2[1][3]25141340 | ptr2[1][4]25141344 | |
→ | ||||||
ptr2[2]25141280 | ptr2[2][0]25141360 | ptr2[2][1]25141364 | ptr2[2][2]25141368 | ptr2[2][3]25141372 | ptr2[2][4]25141376 | |
ptr2=(int**)malloc(sizeof(int*)*m), 3 int-pointer blocks are allotted, and address of first block, 25141264, is given to ptr2.
For i = 0, ptr2[0]=(int*)malloc(sizeof(int)*n), 5 int blocks are allotted and address of first block, 25141296, is given to ptr2[0]. And same goes for ptr2[1] and ptr[2].
Wild pointer and importnace of
|