Pointers in C

Introduction

Imagine a post office as a computer's memory, where each memory location (box or mailbox) has a unique address. These addresses are similar to memory addresses in a computer's memory. Pointers in programming are like special mailboxes that hold the addresses of other mailboxes (memory locations). They are used to access and manipulate data indirectly and are essential for tasks like dynamic memory allocation and data structures. Just like in the post office, pointers provide a way to efficiently work with data without needing to know the actual memory addresses.

Data Types and Memory

Every time you declare a variable, the computer reserves enough memory for this address. The memory variable will now store the value of the variable. Depending on the type of the variable, the computer will reserve more or less memory. The size is generally defined in bytes (1 byte = 8 bits, each bit being 0 or 1). This depends on the computer you are using. Here are the sizes of the most common types on most 64-bit Linux machines:

  • char -> 1 byte

  • int -> 4 bytes

  • float -> 4 bytes

  • double -> 8 bytes

    To determine the size of those types on your computer, you can use the sizeof operator as shown below:

#include <stdio.h>

/**
 * main - using sizeof to dynamically determine the size of types char, int and float
 *
 * Return: Always 0.
 */
int main(void)
{ 
    printf("Size of type 'char' on my computer: %lu bytes\n", sizeof(char));
    printf("Size of type 'int' on my computer: %lu bytes\n", sizeof(int));
    printf("Size of type 'float' on my computer: %lu bytes\n", sizeof(float));
    printf("Size of type 'double' on my computer: %lu bytes\n", sizeof(double)); 
   return (0);
}

Output:

Size of type 'char' on my computer: 1 bytes
Size of type 'int' on my computer: 4 bytes
Size of type 'float' on my computer: 4 bytes
Size of type 'double' on my computer: 8 bytes

Every variable is a memory location and every memory location has its address defined which can be accessed using the ampersand (&) operator, which represents an address in memory.

#include <stdio.h>

/**
 * main - prints address of the defined varible
 *
 * Return: Always 0.
 */
 int main(void)
 {
     char c;
     int n;

     printf("Address of variable 'c': %p \n", &c);
     printf("Address of varible 'n': %p \n", &n);
     return (0);    
 }

Output:

Address of variable 'c': 0x7ffdb288179f
Address of variable 'n': 0x7ffdb2881798

Note: You can use %p to print addresses (the values of pointers) with printf

What is a pointer

A pointer is the address of a piece of data in memory. A pointer variable is a variable that stores the address of that piece of data. Like any other variable, it needs to be declared. The general form is: var_type *var;

  • The * shows that var is a pointer that points to var_type

  • The value of var will be a memory address holding the value of type var_type

Example 1:

int *ptr;

In this example ptr is the name of the variable, of type "pointer to an integer". Anything on the left of * will give you the type that the pointer points to.

Example 2:

/* ptr2 is a pointer to a char */
char *ptr2;

Since a pointer is a variable, the computer will reserve enough memory for it. On most 64-bit machines the size of a pointer is 8 bytes.

Example 3:

#include <stdio.h>

/**
 * main - printing the size, in bytes, of a pointer
 *
 * Return: Always 0.
 */
int main(void)
{
   int *p;

   printf("Size of pointer: %lu\n", sizeof(p));
   return (0);
}

Output:

Size of pointer: 8

Now let’s store the address of a variable into a pointer.

#include <stdio.h>

/**
 * main - storing the address of variable into a pointer
 *
 * Return: Always 0.
 */
int main(void)
{
  int n;
  int *p;

  n = 10;
  p = &n;
  printf("Address of 'n': %p\n", &n);
  printf("Value of 'p': %p\n", p);
  return (0);
}

Output:

Address of 'n': 0x7ffe64269a14
Value of 'p': 0x7ffe64269a14

In the above example variable p holds the value of the address of variable n. Therefore the value of n which is 10 can also be accessed on variable p

Note: A pointer can only point to a variable of the same type.

The following example is wrong:

int n;
char *p;

p = &n;

Dereferencing

The real power of pointers is that they can manipulate values stored at the memory address they point to. This is called dereferencing. To do this you can use the dereferencing operator *. Also known as indirection operator.

Why do we use a dereferencing pointer?

  • It can be used to access or manipulate the data stored at the memory location, which is pointed by the pointer.

  • Any operation applied to the dereferenced pointer will directly affect the value of the variable that it points to.

Example:

#include <stdio.h>

/**
 * main - derefencing pointers
 *
 * Return: Always 0.
 */
int main(void)
{
   int n;
   int *p;

   n = 10;
   p = &n;
   printf("Value of 'n': %d\n", n);
   printf("Address of 'n': %p\n", &n);
   printf("Value of 'p': %p\n", p);
   *p = 50;
   printf("Value of 'n': %d\n", n);
   return (0);
}

Output:

Value of 'n': 10
Address of 'n': 0x7fff65bed1b4
Value of 'p': 0x7fff65bed1b4
Value of 'n': 50

Let's walk through the above example:

  • int *p: * is used in declaration. p is a pointer to an integer. And so after dereferencing p is an integer.

  • p = &n: p takes the address of n. So p == n and *p == n.

  • *p = 50;: equivalent to n = 50, since p == &n. Now *p == 50 so n == 50.

This works exactly the same for other types:

#include <stdio.h>

/**
 * main - derefencing pointers, example with int and char types
 *
 * Return: Always 0.
 */
int main(void)
{
   int n;
   int *p;
   char c;
   char *pp;

   c = 'H';
   pp = &c;
   n = 10;
   p = &n;
   printf("Value of 'c': %d ('%c')\n", c, c);
   printf("Address of 'c': %p\n", &c);
   printf("Value of 'pp': %p\n", pp);
   printf("Value of 'n': %d\n", n);
   printf("Address of 'n': %p\n", &n);
   printf("Value of 'p': %p\n", p);
   *p = 50;
   *pp = 'o';
   printf("Value of 'n': %d\n", n);
   printf("Value of '*pp': %d\n", *pp);
   printf("Value of 'c': %d ('%c')\n", c, c);
   printf("Value of '*pp': %d ('%c')\n", *pp, *pp);
   return (0);
}

Output:

Value of 'c': 72 ('H')
Address of 'c': 0x7ffd5644eaab
Value of 'pp': 0x7ffd5644eaab
Value of 'n': 10
Address of 'n': 0x7ffd5644eaac
Value of 'p': 0x7ffd5644eaac
Value of 'n': 50
Value of '*pp': 111
Value of 'c': 111 ('o')
Value of '*pp': 111 ('o')

Note: The * has a different meaning depending on the context (declaring vs dereferencing pointers).

  • at declaration, it is used to declare a variable of type pointer to something. Example: int *n;

  • when used inside the code it is used to dereference pointers. Example *n = 10;

Conclusion

As we conclude, it's worth emphasizing that pointers are both a blessing and a potential source of pitfalls in C programming. With knowledge, care, and practice, programmers can harness the full potential of pointers while avoiding common pitfalls. This understanding is crucial for those looking to master C and write efficient, low-level code that is both powerful and reliable.

Happy coding!

References