Table of Contents

Pointer variable

Published

A pointer variable, often simply called a pointer, is a fundamental concept in programming, particularly in languages like C and C++. It's a special type of variable that holds a memory address rather than a direct value like an integer or a character. This capability is crucial for dynamic memory management, creating complex data structures, and implementing efficient algorithms. Pointers allow programs to indirectly access and manipulate data stored at specific memory locations, providing a powerful tool for developers, but also a source of complexity if not handled correctly.

1. What is a Pointer?

1.1 Definition of a Pointer

A pointer is essentially a variable that stores the memory address of another variable. Unlike regular variables, which hold values directly, a pointer holds the location in memory where another value is stored. This indirection allows for dynamic data manipulation and efficient resource management. The type of data a pointer points to is also part of its definition; for example, an integer pointer points to an integer location in memory, while a character pointer points to a character location. Pointers are essential in C and C++ for tasks such as dynamic memory allocation, creating linked lists, and implementing various data structures. Understanding pointers is crucial for mastering memory management and efficient resource utilization in these languages. The concept of a pointer is foundational for advanced programming techniques.

When a pointer is declared, it does not inherently point to a valid memory location. Initially, it contains a garbage value, which is an arbitrary memory address. Before using a pointer, it must be initialized to point to a valid memory location. This initialization can be done using the address-of operator (&) to get the memory address of an existing variable or by dynamically allocating memory using functions like new. Failure to initialize a pointer properly can lead to serious runtime errors, such as segmentation faults, due to the program attempting to access random memory locations.

The type of data a pointer points to is crucial because it determines how the data at the pointed address is interpreted, as well as the size of the data. For instance, an integer pointer will treat the memory location as containing an integer, while a character pointer will treat it as containing a character. This type information is used by the compiler to ensure that the operations performed on the data through the pointer are valid for the data type. Additionally, pointer arithmetic is based on the size of the pointed-to data type, which means that incrementing an integer pointer moves the pointer by the size of an integer in memory, while incrementing a character pointer moves it by one byte.

1.2 Declaring Pointers

Declaring a pointer involves specifying the data type it will point to, followed by an asterisk (*) and the pointer variable name. The syntax is type *ptr_name;. For instance, int *iPtr; declares iPtr as a pointer to an integer, and double *dPtr; declares dPtr as a pointer to a double. It's important to note that the asterisk is part of the declaration syntax and does not indicate dereferencing at this point. The asterisk is used to indicate that the variable being declared is a pointer variable, and it is associated with the variable name it precedes. A common practice is to include "p" or "ptr" as a prefix or suffix to the variable name to denote that it is a pointer.

int *iPtr;   // Declares iPtr as an integer pointer
double *dPtr; // Declares dPtr as a double pointer
char *cPtr;  // Declares cPtr as a char pointer

When declaring multiple pointers on the same line, each variable must be preceded by an asterisk. For example, int *p1, *p2, i; declares p1 and p2 as integer pointers, while i is declared as an integer, not a pointer. This is a common source of confusion for beginners. To explicitly declare multiple pointers, you must use int *p1, *p2;, where both p1 and p2 are declared as integer pointers. The position of the asterisk relative to the variable name in the declaration statement can vary, and all the following are valid declarations of a pointer, int *ptr;, int* ptr;, int * ptr;.

1.3 Initializing Pointers

Initializing pointers is crucial to prevent undefined behavior. Pointers should be initialized with a valid memory address or the null pointer (0 or NULL). The address-of operator & is used to obtain the memory address of a variable. For example, int num = 10; int *ptr = # declares an integer num with a value of 10, and ptr is declared as an integer pointer that stores the memory address of num. The address-of operator returns the memory address of a given variable. It is important to note that the address-of operator (&) is typically used to obtain the address of an existing variable (e.g. int *p = #). You cannot assign to &num directly (e.g. &num = somePointer; is invalid). In other words, & can’t be used in a way that tries to write to an address-of expression. The address stored in a pointer can be changed during the execution of the program, but the type of the variable that the pointer is pointing to cannot change.

A pointer can also be initialized with the result of dynamic memory allocation. This is done using the new operator, which allocates memory from the heap at run time. For example, int *ptr = new int; allocates memory for an integer and assigns the address of that memory to ptr. It is the programmer's responsibility to deallocate this memory using the delete operator when it is no longer needed to avoid memory leaks. The following example shows how to declare an integer variable, a pointer to an integer, and how to assign a value to the pointer by assigning the address of the variable to the pointer:

int number = 88;
int *pNumber = &number;

2. Pointer Operations

2.1 Dereferencing Pointers

Dereferencing a pointer means accessing the value stored at the memory address pointed to by the pointer. The dereference operator, denoted by an asterisk *, is used to access the value at the pointed-to address. For example, if ptr is a pointer to an integer variable, then *ptr returns the integer value stored at the memory address stored in ptr. It is important to note that ptr represents the memory address itself, whereas *ptr represents the value stored at that address. Dereferencing an uninitialized pointer results in undefined behavior, potentially causing program crashes or unexpected behavior. The asterisk symbol * has different meanings in a declaration statement and in an expression. When used in a declaration statement (e.g. int *pNumber), it denotes that the name that follows is a pointer variable. When used in an expression (e.g. *pNumber = 99), it refers to the value pointed to by the pointer variable.

The act of dereferencing is also called indirection. A variable references a value directly, whereas a pointer references a value indirectly through the memory address it stores. This indirection is a powerful feature of pointers that allows for manipulation of variables through their memory addresses. The dereference operator * can be used on both the right-hand side and left-hand side of an assignment statement, for instance, temp = *pNumber and *pNumber = 99. The following example demonstrates the dereferencing operation:

int number = 88;
int *pNumber = &number;
cout << *pNumber; // Prints the value 88
*pNumber = 99; // Changes the value to 99

2.2 Pointer Arithmetic

Pointer arithmetic is the manipulation of pointer addresses by adding or subtracting integer values. Pointer arithmetic is not performed in terms of bytes, but rather in terms of the size of the data type to which the pointer is pointing. For example, if ptr is a pointer to an integer, adding 1 to ptr does not simply increment the address by one byte, but increments it by the size of an integer (typically 4 bytes). This ensures that the pointer will point to the next valid memory location for an integer. Pointer arithmetic is only defined for arrays and contiguous memory locations. It is undefined behavior to perform pointer arithmetic on pointers that do not point to contiguous memory locations. The following illustrates how pointer arithmetic works:

int numbers[] = {11, 22, 33};
int *iPtr = numbers;
cout << iPtr; // Prints the address of the first element
cout << iPtr + 1; // Prints the address of the second element

Pointer arithmetic is particularly useful when working with arrays, where a pointer can be used to iterate through the array elements. When a pointer is incremented, it moves to the next element in the array, regardless of the size of each element. Similarly, when a pointer is decremented, it moves to the previous element in the array. This makes pointer arithmetic an efficient way to access elements in an array without needing to access them via their index. However, it’s essential to ensure that pointer arithmetic does not go out of the bounds of the array. In other words, it should not point to memory locations outside of the allocated array.

2.3 Null Pointers

A null pointer can be represented by nullptr (introduced in C++11) which is preferred over NULL or 0, as nullptr is a keyword that represents a null pointer constant of type std::nullptr_t and provides better type safety. Initializing a pointer to null indicates that it doesn't yet point to a valid memory address. It is a good programming practice to initialize pointers to null when they are declared, especially when there is no immediate valid memory address available for them to point to. Dereferencing a null pointer leads to a segmentation fault or an access violation, which is a common error that can cause the program to crash. The most common reason to initialize a pointer to null is to indicate that it does not point to a valid memory address. The following example declares a null pointer:

int *iPtr = 0; // Using 0
int *p = NULL; // Using NULL
int *ptr = nullptr; // Using nullptr

Null pointers are frequently used as a way to check whether a pointer points to a valid memory location before dereferencing it. For example, a function may return a null pointer to indicate that it failed to find the requested data or allocate memory. Before dereferencing such a pointer, it's crucial to check that it's not null to avoid runtime errors. This practice is a cornerstone of robust and reliable code development. The null pointer can also be used as the end-of-list marker in data structures such as linked lists. It helps to denote the end of the list, and the program can determine that the end of the list has been reached when a pointer is equal to null.

3. Pointers and Arrays

3.1 Arrays as Pointers

In C and C++, an array name can be treated as a pointer to the first element of the array. This means that the array name itself stores the memory address of the first element. This relationship allows for pointer arithmetic to be used to access array elements, and it is one of the reasons why C and C++ are efficient for array operations. Thus, numbers is equivalent to &numbers[0]. Using the array name as a pointer can be useful in passing arrays to functions, or for direct memory manipulation. The following example shows the equivalence between an array name and a pointer to the first element of the array:

int numbers[] = {11, 22, 33};
cout << numbers; // Prints the address of the first element
cout << &numbers[0]; // Prints the address of the first element

When an array is passed to a function, it is passed as a pointer to the first element. Therefore, the size of the array cannot be determined inside the function. This is why the size of the array is often passed to the function as a separate parameter. While the array name often 'decays' into a pointer to its first element when used in expressions (e.g. numbers becomes &numbers[0]), the array name itself is not a pointer variable you can reassign. It’s more accurate to say the array name is an identifier that can be converted to a pointer to its first element in most expressions. Unlike a pointer variable, you cannot do operations like numbers++. Therefore, operations like numbers++ are not allowed, whereas a regular pointer can be incremented and decremented. The pointer can be used to access each of the elements of the array, and the pointer can also be used to access an element of the array by using the pointer arithmetic.

3.2 Pointer Arithmetic with Arrays

Pointer arithmetic can be used to access elements of an array. Incrementing a pointer by 1 moves it to the next element in the array, and decrementing it moves it to the previous element. The size of the data type that the pointer is pointing to determines how many bytes the pointer is moved in memory. For instance, if ptr points to an array of integers, ptr + 1 moves to the next integer in the array. This is because the pointer is incremented by the size of an integer. Similarly, if ptr points to an array of characters, ptr + 1 moves to the next character, which corresponds to a move of one byte in memory. Pointer arithmetic with arrays can reduce the complexity of array access, eliminating the need to access elements via their index. The following examples show how to access array elements by using pointer arithmetic:

int numbers[] = {11, 22, 33, 44};
int *ptr = numbers;
cout << *ptr; // Prints 11
cout << *(ptr + 2); // Prints 33

Pointer arithmetic is an efficient way to access array elements, but it does require careful attention to array bounds. Accessing an array element outside of the allocated array may result in a segmentation fault or other undefined behavior. In the example above, ptr points to the first element of the numbers array, and ptr + 2 points to the third element of the array. The value at the memory address of the third element is accessed by dereferencing the pointer using the asterisk. Also, ptr + 2 is equivalent to &numbers[2]. Pointer arithmetic can be used as an alternative to array indexing, but in some cases, array indexing is more readable and less error prone.

4. Pointers and Functions

4.1 Passing Pointers to Functions

Pointers can be passed as arguments to functions, allowing the function to modify the original variables in the caller. This is known as pass-by-reference. When a variable is passed by value, a copy of the variable is made, and any modifications to the copy do not affect the original variable. However, when a pointer is passed to a function, the function receives the memory address of the variable, and it can modify the variable directly. This is how a function can modify the original variable in the caller. Pass-by-reference is very useful when passing large objects or arrays, as it avoids the overhead of cloning the object or array. The following example demonstrates the pass-by-reference using pointers:

void square(int *pNumber) {
  *pNumber *= *pNumber;
}
int number = 8;
square(&number); // number becomes 64

Passing pointers to functions allows for more efficient memory usage and reduces memory allocation overhead. However, it also introduces the risk of modifying the original variable inadvertently. This can be avoided by using a const pointer, which prevents the function from modifying the original variable. The const keyword is used to declare that a variable cannot be modified.

Be careful with the terminology:

  • const int *p (pointer to const int): You can’t use *p to modify the integer, but you can change what p points to.
  • int * const p (const pointer to int): You can change the integer value via *p but you cannot change the pointer p itself to point elsewhere.
    Ensure you use the correct form of const as per your needs.

Passing a const pointer to a function is a good programming practice that should be used whenever possible. Pass by reference is an important concept in C++, as it allows functions to modify variables in the caller and to avoid the overhead of cloning when passing large objects or arrays.

4.2 Returning Pointers from Functions

Functions can also return pointers, which is useful for returning dynamically allocated memory or for providing access to data that is not local to the function. However, it is critical to manage the lifetime of the data pointed to by the returned pointer. If a function returns a pointer to a local variable of the function, it is a serious error, because the local variable is destroyed when the function exits. Such a pointer will point to memory that has been deallocated, which can cause undefined behavior. Therefore, a function should not return a pointer to a local variable. Instead, a function should return a pointer to dynamically allocated memory or to a variable that has been passed to the function as a parameter, as illustrated in the following example:

int *squarePtr(int *pNumber) {
  *pNumber *= *pNumber;
  return pNumber;
}
int number = 8;
int *result = squarePtr(&number); // number becomes 64

When returning dynamically allocated memory from a function, the caller becomes responsible for deallocating the memory using the delete operator when it is no longer needed. Failure to do so results in a memory leak. When returning a pointer that is passed to a function as an argument, the caller is responsible for ensuring the lifetime of the data is long enough. It is important to understand that the function does not own the memory when the pointer is passed to the function as a parameter. Therefore, the caller is responsible for ensuring the memory is valid for the life time of the program. Returning pointers from functions is a powerful feature of C and C++, but it requires careful management of memory and lifetime of the data.

5. Dynamic Memory Allocation

5.1 The new and delete Operators

Dynamic memory allocation is done using the new operator in C++, which allocates memory at runtime from the heap. The new operator returns a pointer to the allocated memory. When the allocated memory is no longer needed, it must be deallocated by using the delete operator. Failure to deallocate memory that is allocated using new results in a memory leak. The delete operator frees the dynamically allocated memory and makes it available for future allocations. When using dynamic memory allocation, it is important to ensure that the memory is properly deallocated to avoid memory leaks. The following example shows the use of the new and delete operators:

int *ptr = new int; // Allocate memory for an integer
*ptr = 10;
cout << *ptr; // Prints 10
delete ptr; // Deallocate the memory

The new operator can also be used to allocate memory for arrays, which is done by using the new[] operator. When using new[] to allocate memory for an array, the delete[] operator must be used to deallocate the memory. The new[] operator returns a pointer to the first element of the array. The delete[] operator deallocates the memory allocated for the array, and it must be used when the memory is no longer needed. Failure to deallocate the memory allocated using new[] results in a memory leak, and it is more difficult to identify and debug memory leaks when using dynamic memory allocation. The following shows dynamic array allocation:

int *arr = new int[5]; // Allocate memory for an array of 5 integers
arr[0] = 1;
delete[] arr; // Deallocate the memory

5.2 Memory Management Best Practices

Proper memory management is crucial when working with pointers and dynamic memory allocation. Memory leaks occur when memory is allocated using new but never deallocated using delete. This can cause programs to consume more and more memory over time, resulting in performance degradation and eventually program crashes. To avoid memory leaks, always deallocate memory allocated with new using delete when it is no longer needed. Use delete[] to deallocate memory allocated for an array using new[]. It is also important to avoid double frees, which occur when the same memory is deallocated more than once. This can cause memory corruption and lead to unpredictable program behavior. Double frees can be avoided by setting a pointer to null after deleting the memory it points to.

It is also important to avoid dangling pointers, which are pointers that point to memory that has been deallocated. Dereferencing a dangling pointer results in undefined behavior. Dangling pointers can be avoided by setting pointers to null after deallocating the memory they point to. This is especially important when returning pointers from functions or when passing pointers between functions. Also, avoid doing pointer arithmetic that goes out of the bounds of arrays. Such errors can result in unpredictable behavior and can cause memory corruption. The best way to avoid these problems is to use smart pointers whenever possible, which are classes that automatically manage the lifetime of dynamically allocated memory.

6. Key Takeaways of Pointer variables

Pointer variables are a powerful tool in C and C++, allowing for direct memory manipulation and efficient dynamic memory allocation. They enable the creation of complex data structures and provide a way for functions to modify variables in the caller. However, pointers also introduce complexity and the risk of memory-related errors, such as memory leaks, dangling pointers, and segmentation faults. Proper management of pointers is essential for writing robust and reliable code. Understanding the concepts of pointer arithmetic, dereferencing, and dynamic memory allocation is crucial for mastering C and C++. To mitigate these risks, best practices such as initializing pointers, avoiding dangling pointers, and using smart pointers, should be diligently followed. The correct usage of the new and delete operators is essential to prevent memory leaks, and careful pointer arithmetic is essential to prevent out-of-bounds access. With proper understanding and care, pointer variables can be a fundamental tool in a developer's arsenal.

Note: The concept of pointers is common to both C and C++, but the use of new and delete for dynamic memory allocation is specific to C++. In C, malloc and free are used. When using C++, prefer modern memory management techniques such as smart pointers (std::unique_ptr, std::shared_ptr) to reduce the risk of memory leaks and dangling pointers.

Learning Resource: This content is for educational purposes. For the latest information and best practices, please refer to official documentation.

Text byTakafumi Endo

Takafumi Endo, CEO of ROUTE06. After earning his MSc from Tohoku University, he founded and led an e-commerce startup acquired by a major retail company. He also served as an EIR at Delight Ventures.

Last edited on