DEV Community

Cover image for Pointers In C - Definition, Notation, Types and Arithmetic
Srijan
Srijan

Posted on • Edited on

Pointers In C - Definition, Notation, Types and Arithmetic

Pointers are arguably the most difficult feature of C to understand. But, it is one of the features which make C an excellent language. In a series of articles, we will take a closer look at pointers and how they can be used in C programming.


Topics -

0. What exactly are pointers?
1. Pointer Notation and Definition
2. Some Special Pointers
3. Pointer Arithmetic


0. What exactly are pointers?

Before we go to the definition of pointers, let us understand what happens when we write

int digit = 42;

A block of memory is reserved by the compiler to hold an int value. The name of this block is digit and the value stored in this block is 42. Now, to remember the block, it is assigned with an address or a location number(say, 24650).

The value of the location number is not important for us, as it is a random value. However, we can access this address using & (ampersand) or address of operator like below.

printf("The address of digit = %d.",&digit);
 /* prints "The address of digit = 24650. */

Now, we can get the value of the variable digit from its address using another operator * (asterisk), called indirection or dereferencing or value at address operator.

printf("The value of digit = %d.", *(&digit);
 /* prints "The value of digit = 42. */

1. Pointer Definition and Notation

The address of digit can be stored in another variable known as a pointer variable. The syntax for storing a variable's address to a pointer is :

dataType *pointerVariableName = &variableName;

For our digit variable, this can be written as :

int *addressOfDigit = &digit;

or

int *addressOfDigit;
addressOfDigit= &digit;

This can be read as - A pointer to int (integer) addressOfDigit stores the address of(&) digit variable.

Few points to understand -

1.dataType - We need to tell the computer what is the data type of the variable whose address we are going to store. Here, int was the data type of digit.

It does not mean that addressOfDigit will store a value of type int.

An integer pointer (like addressOfDigit) can only store the address of variables of integer type.

int variable1;
int variable2;
char variable3;
int *addressOfVariables;

Here, we can assign the address of variable1 and variable2 to the integer pointer addressOfVariables but not to variable3 since it is of type char. We will need a character pointer variable to store its address.

2.* - A pointer variable is a special variable in the sense that it is used to store an address of another variable. To differentiate it from other variables that do not store address, we use *, as a symbol in the declaration.

Now, we can use our addressOfDigit pointer variable to print the address and the value of digit as below :

printf("The address of digit = %d.", addressOfDigit);
 /* prints "The address of digit = 24650." */
printf("The value of digit = %d.", *addressOfDigit);
 /*prints "The value of digit = 42. */

Here, *addressOfDigit is to be read as the value at the address stored in addressOfDigit.

Notice, we used %d as the format identifier for addressOfDigit. Well, this not completely correct. The correct identifier to be used would be %p.

Using %p, the address is displayed in hexadecimal value. But the memory address can be displayed in integers as well as octal values. Still, since it is not an entirely correct way, a warning is shown.

int num = 5;
int *p = #
printf("Address using %%p = %p",p);
printf("Address using %%d = %d",p);
printf("Address using %%o = %o",p);

The output according to compiler I'm using is -

Address using %p = 000000000061FE00
Address using %d = 6422016
Address using %o = 30377000

This is the warning shown when you use %d -

warning: format '%d' expects argument of type 'int', but argument 2 has type 'int *'

2. Some Special Pointers

1. Wild Pointer

char *alphabetAddress; /* uninitialised or wild pointer */
char alphabet = "a";
alphabetAddress = &alphabet; /* now, not a wild pointer */

When we defined our character pointer alphabetAddress, we did not initialize it. Such pointers are known as wild pointers. They store a garbage value i.e. memory address of a byte that we don't know is reserved or not (remember int digit = 42;, we reserved a memory address when we declared it).

Suppose we dereference a wild pointer and assign a value to the memory address it is pointing at. This will lead to unexpected behaviour since we will write data at a memory block that may be free or reserved.

2. Null Pointer

Now, to ensure we do not have a wild pointer, we can initialize a pointer with a NULL value, making it a null pointer.

char *alphabetAddress = NULL /* Null pointer */ 

A null pointer points at nothing, or at a memory address that users can not access.

3. Void Pointer

A void pointer can be used to point at a variable of any data type. It can be reused to point at any data type we want to. It is declared as

void *pointerVariableName = NULL;

Since they are very general in nature, they are also known as generic pointers.

With their flexibility, void pointers also bring some constraints. Void pointers cannot be dereferenced as any other pointer. Appropriate typecasting is necessary.

void *pointer = NULL;
int number = 54;
char alphabet = "z";
pointer = &number;
printf("The value of number = ", *pointer); /* Compilation Error */
/* Correct Method */
printf("The value of number = ", *(int *)pointer); /* prints "The value at number = 54" */
pointer = &alphabet;
printf("The value of alphabet = ", *pointer); /* Compilation Error */
printf("The value of alphabet = ", *(char *)pointer); /* prints "The value at alphabet = z */

Similarly, void pointers need to be typecasted for performing arithmetic operations.

Void pointers are of great use in C. Library functions malloc() and calloc() which dynamically allocate memory, return void pointers. qsort(), an inbuilt sorting function in C, has a function as its argument which itself takes void pointers as its argument.

4. Dangling Pointer

A dangling pointer points to a memory address which used to hold a variable. Since the address it points at is no longer reserved, using it will lead to unexpected results.

main(){
  int *ptr;
  ptr = (int *)malloc(sizeof(int));
  *ptr = 1;
  printf("%d",*ptr); /* prints 1 */
  free(ptr); /* deallocation */
  *ptr = 5;
  printf("%d",*ptr); /* may or may not print 5 */
}

Though the memory has been deallocated by free(ptr), the pointer to integer ptr still points to that unreserved memory address.

3. Pointer Arithmetic

We know by now that pointers are not like any other variable. They do not store any value but the address of memory blocks. Hence, it is quite clear that not all arithmetic operations would be valid with them. Would multiplying or dividing two pointers (having address) make sense?

Pointers have few but immensely useful valid operations -

1.you can assign the value of one pointer to another only if they are of the same type (unless typecasted or one of them being void *.

int ManU = 1;
int *addressOfManU = &ManU;
int *anotherAddressOfManU = NULL;
anotherAddressOfManU = addressOfManU; /* Valid */
double *wrongAddressOfManU = addressOfManU; /* Invalid */

2.you can only add or subtract integers to pointers.

int myArray = {3,6,9,12,15};
int *pointerToMyArray = &myArray[0];
pointerToMyArray += 3; /* Valid */
pointerToMyArray *= 3; /* Invalid */

When you add (or subtract) an integer (say n) to a pointer, you are not actually adding (or subtracting) the integer literally. You are adding (or subtracting) n-times the size of the data type of the variable the pointer is pointing to.

int number = 5;
 /* Suppose the address of number is 100 */
int *ptr = &number;
int newAddress = ptr + 3;
 /* Same as ptr + 3 * sizeof(int) */

The value stored in newAddress will not be 103, rather 112.

3.subtraction and comparison of pointers is valid only if both are members of the same array.

int myArray = {3,6,9,12,15};
int sixthMultiple = 18;
int *pointer1 = &myArray[0];
int *pointer2 = &myArray[1];
int *pointer6 = &sixthMuliple;
 /* Valid Expressions */
if(pointer1 == pointer2)
pointer2 - pointer1;
 /* Invalid Expressions
if(pointer1 == pointer6)
pointer2 - pointer6

The subtraction of pointers results in the number of elements separating them.

4.you can assign or compare a pointer with NULL.

The only exception to above rules is that the address of the first memory block after the last element of an array follows pointer arithmetic.

Pointer and arrays exist together. There is a reason why the most valid manipulations of pointers can only be done with arrays. We will discuss the above rules with arrays in the next article.


P.S. - Try to find what will be the value stored in pointerToMyArray for valid operations in 1 and 2, if the address of myArray[0] is 100.

The edits suggested by @pentacular have been included. Thanks to him/her.

Top comments (3)

Collapse
 
pentacular profile image
pentacular

An address is simply a pointer value, and you'll find that replacing 'address of' with 'pointer to', and 'address' with 'pointer value' will simplify things a lot.

you can assign the value of one pointer to another only if they are of the same type

This is not true -- you can assign where there is an implicit conversion (such as from void *), or by using a cast.

When you add an integer (say n) to a pointer, you are not actually adding the integer literally. You are adding n-times the size of the data type of the variable the pointer is pointing to.

This is a clumsy way to think about it.

A pointer is an index into an array.

When you add one to a pointer, you get a pointer to the next item in that array.

When you subtract two pointers, you get the number of items between those two pointers.

Variables are effectively stored in arrays of length one.
Which is why { int i; &i + 1 } is well defined but { int i; &i + 2; } is not.

Since the address it points at is no longer reserved, it causes illegal memory access.

This is not quite true.

A pointer that does not have a null pointer value or points into or one beyond the end of a block of allocated memory has an undefined value, and using that value has undefined behavior.

If you are very lucky your implementation will produce an illegal memory access error.

If you are unlucky it will appear to work perfectly until, one day, it doesn't. :)

Collapse
 
its_srijan profile image
Srijan • Edited

A big thank you for pointing out the mistakes.

An address is simply a pointer value, and you'll find that replacing 'address of' with 'pointer to', and 'address' with 'pointer value' will simplify things a lot.

I wanted to keep this as basic as possible and I think keeping 'address' would remind them what pointers store.

This is not true -- you can assign where there is an implicit conversion (such as from void *), or by using a cast.

I missed this. My bad.

This is a clumsy way to think about it.

Though your explanation on the same is quite well put, I found this way they would never confuse as I did.

A pointer that does not have a null pointer value or points into or one beyond the end of a block of allocated memory has an undefined value, and using that value has undefined behavior.

I meant "illegal memory access" in the sense it would not give a consistent result. But yeah, should have used different words.

I have made the changes. Thanks again.

Collapse
 
pentacular profile image
pentacular

Regarding pointers and addresses :)

I'd say that a pointer variable stores a pointer type value (just like any other variable).

Which may or may not be a pointer to an object.

The address of an object is a pointer value to that object.

So sometimes a pointer variable stores a pointer value that is the address of an object. :)