CodingBison

C uses pointers to refer to variables by pointing to their address in memory. If we have a variable x of type int, then we can use a pointer to refer to the address of x. When using pointers, we need to remember three basic syntax rules. First, we can define pointers using "*" keyword. Second, we can refer to the address of a variable by using "&" keyword. Lastly, we can use the "*" operator to retrieve value stored at a given address.

Let us start with a simple example. Here, ptr_to_x is a pointer that points to the address of x. The value of ptr_to_x is, in fact, the memory address of x.

int x = 100;
int *ptr_to_x; /* this is a pointer */

ptr_to_x = &x; /* ptr_to_x points to the memory address of x */

Since x is of the (storage) type int, ptr_to_x starts at memory location, &x, and then it automatically reads sizeof(int) bytes, starting at this location for finding the value stored at that location.

The following figure shows an example of address and value stored for both of these variables, x and ptr_to_x. As we can see from this figure, the value stored at the pointer (ptr_to_x) is the address of x.

------------------                      ------------------
| Value: 0x28ff000 | -----------------> |    Value: 100    |
------------------                      ------------------
Name: ptr_to_x                             Name: x

Figure: A pointer stores an address

The above figure also shows that the pointer (ptr_to_x) has a storage of its own. The storage size of the pointer depends upon the system. In practice, on 32 bit processors (e.g. Celeron, Pentium I, Pentinum II, Pentium III etc), a pointer would need 4 bytes of storage and on 64 bit systems (e.g. Intel Core i3, Core i5, Core i7, etc), a pointer would need 8 bytes of storage. We can use the sizeof() function to find out the storage size for a pointer. Thus, in the above example, sizeof(ptr_to_x) would tell the storage size of pointer ptr_to_x.

Let us now see another example, where we use the "*" keyword to access value stored at an address. Then, the example uses the "*" keyword to swap values between two variables. The example also shows that we can have multiple pointers point to the same value.

#include <stdio.h>

int main () {
int x, y, temp;
int *ptr_to_x, *ptr_to_x1, *ptr_to_x2;

y = 200;
x = 100;
ptr_to_x = &x;
printf("Value of x: %d and *ptr_to_x: %d\n", x, *ptr_to_x);
printf("Address of x: %p and ptr_to_x: %p\n", &x, ptr_to_x);

/* Swapping value of x and y */
printf("Before swapping, x: %d  y: %d\n", x, y);
temp = y;
y = *ptr_to_x;
*ptr_to_x = temp;
printf("After  swapping, x: %d  y: %d\n", x, y);

/* Making multiple pointers point to the same value */
ptr_to_x1 = &x;
ptr_to_x2 = &x;
return 0;
}

Note that we pass the "%p" formatting option to printf() function to print the address pointed by a pointer. Here is the output of the above program:

Value of x: 100 and *ptr_to_x: 100
Address of x: 0xbfd44178 and ptr_to_x: 0xbfd44178
Before swapping, x: 100  y: 200
After  swapping, x: 200  y: 100

Since a pointer points to the memory location of a variable and since pointers themselves are variables as well, it is also possible to point to the memory location of a pointer itself. We can use the keyword "**" to achieve that. In the following code, ptr_to_ptr_to_x is a pointer that itself point to another pointer, ptr_to_x! Accordingly, we can retrieve the value of x from ptr_to_ptr_to_x using "**ptr_to_ptr_to_x".

int x;
int *ptr_to_x;
int **ptr_to_ptr_to_x;

/* ptr_to_x points to the memory address of x */
ptr_to_x = &x;

/* ptr_to_ptr_to_x points to the memory address of ptr_to_x */
ptr_to_ptr_to_ x = &ptr_to_x;

When a pointer does not point to any address, its value is a symbolic NULL, which represents an integer value of 0. The compiler would complain bitterly, if we assign a non-zero integer value to a pointers; a value of 0 (or NULL) is the only exception. This special value is used very often for conditional expressions. Thus, if we know that a pointer does not point to anything, then the following expressions would evaluate to True: "if (ptr_to_x == NULL)" or "if (!ptr_to_x)". On the other hand, if it does, then the opposite expressions would evaluate to True: "if (ptr_to_x != NULL)" or "if (ptr_to_x)".

Constant Pointers

C allows us to make the value pointed by a pointer constant. Once the constant value is declared constant, we cannot change it. For this purpose, we can use the "const" keyword. Further, we can use the same keyword to restrict the pointer's address as well. In other words, the "const" keyword allows us to constraint not only the value pointed by the pointer, but also its address.

In fact, there are three possible cases of this "constantness": (a) the value pointed to by the pointer is constant, (b) the address of the pointer is constant, and (c) both value and address of the pointer are constant.

In the first case, we constrain the value pointed by the pointer. The statement, "const int *x" means that the value pointed by the pointer is a constant and hence whatever is stored at that value cannot be changed. Trying to assign the value of the pointer to a new value would surely throw the compiler into a rage. However, the compiler would happily comply, if we reassign the pointer to a new address!

int main () {
int varA = 10, varB = 20;
const int* x = &varA;

*x = 20;        /* Compiler would throw an error */
x = &varB;
return 0;
}

In the second case, we constrain the address of the pointer and with that, we cannot change the address of the pointer. However, changing the value of the pointer would be okay. Thus, in the following code, "x = &varB" would lead to a compiler error, but "*x = 20" would not.

int main () {
int varA = 10, varB = 20;
int *const x = &varA;

*x = 20;
x = &varB; /* Compiler would throw an error */
return 0;
}

In the third case, we crank it up a notch and constraint both the pointer and its value! In the following code, the compiler would not allow the last two assignment statements.

int main () {
int varA = 10, varB = 20;
const int *const x = &varA;

*x = 20;   /* Compiler would throw an error */
x = &varB; /* Compiler would throw an error */
return 0;
}

If it gets confusing as to what location of "const" results in what constraint, then there is a simple trick! If the "const" qualifier is near the "*", then it constrains the pointer (e.g. in "int *const x"), else it constrains the value (e.g. in "const int *x").

Besides qualifying variable, we can also use the "const" keyword when passing pointers (or for that matter values) to a function. Using the "const" keyword, we can specify that the called function should not modify the value of the passed pointer or the address pointed by the pointer, or both.

So, why do we need to provide such constant pointers or values to function calls? Well, it is possible that the called function might be making a copy of the value of the passed pointer to another pointer and may not need to modify the value pointed to by the passed pointer. Using a const qualifier explicitly, the caller can explicitly tell the called function that, by design, it is not supposed to modify the passed value. C allows us to restrict the called function from modifying the original value by using a "const" qualifier. Here is the signature of one such function that constraints the value of x_copy_from from being modified.

int copy_the_value(int *x_copy_to, const int *x_copy_from);

It is a good idea to use the "const" keyword, whenever it is applicable. When writing new functions, if the design dictates that the called function should not modify the passed pointer (or its value), then we should not hesitate for a second in making the parameter constant. If you do not believe me, then simply look for "man string.h" in Google (or on your terminal) and you would see that a lot of string and memory functions already use this keyword.