Pointer variable
Published
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.
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:
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:
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:
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:
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:
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:
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:
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 whatp
points to.int * const p
(const pointer to int): You can change the integer value via*p
but you cannot change the pointerp
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:
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:
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:
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