Every variable a program creates has to live somewhere inside the computer's memory, and every single memory location has its own unique address. Most of the time, we do not need to think about these addresses at all, because we simply refer to variables by their names and let the compiler handle the rest behind the scenes. Pointers are the feature that lets a C++ programmer step behind that curtain and work with memory addresses directly.
A pointer in C++ is a special kind of variable that does not store an ordinary value like a number or a character. Instead, it stores the memory address of another variable. This might sound abstract at first, but pointers are one of the most practical tools in the language, used everywhere from passing large data efficiently to functions, to building dynamic data structures, to managing memory that is created while the program is actually running.
In this tutorial, you will learn how to declare and initialize pointers, how to use the address-of and dereference operators, how pointer arithmetic works, how pointers relate to arrays, how dynamic memory allocation works using pointers, and what null and dangling pointers are.
A pointer is a variable whose value is the address of another variable, rather than a direct data value. Just like an ordinary variable must be declared with a data type before use, a pointer must also be declared with a type, which tells the compiler what kind of data the pointer is meant to point to.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int subscribers = 50000;
int* subscriberPtr = &subscribers;
cout << channel << " subscriber count: " << subscribers << endl;
cout << channel << " subscriber address stored in pointer: " << subscriberPtr << endl;
return 0;
}
CS Engineering Gyan subscriber count: 50000 CS Engineering Gyan subscriber address stored in pointer: 0x61ff08
In this example, subscriberPtr is a pointer that stores the memory address of the variable subscribers, rather than storing 50000 directly. The actual address printed will vary each time the program runs, since it depends on where the operating system places the variable in memory.
Declaring a pointer requires specifying the data type it will point to, followed by an asterisk and the pointer's name. A pointer is typically initialized using the address-of operator, written as an ampersand, which retrieves the memory address of a variable.
dataType* pointerName; pointerName = &variableName;
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int totalViews = 125000;
int* viewsPtr;
viewsPtr = &totalViews;
cout << channel << " total views: " << totalViews << endl;
cout << channel << " address of totalViews: " << viewsPtr << endl;
return 0;
}
CS Engineering Gyan total views: 125000 CS Engineering Gyan address of totalViews: 0x61ff0c
A pointer that is declared but not assigned any address contains an unpredictable value, sometimes called a garbage value, so it is good practice to initialize every pointer either with a valid address or with a null value before it is used further.
Once a pointer holds the address of a variable, the asterisk symbol can also be used in a different way, this time to access or modify the value stored at that address. This is known as dereferencing a pointer.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int videoLikes = 800;
int* likesPtr = &videoLikes;
cout << channel << " likes through pointer: " << *likesPtr << endl;
*likesPtr = 950;
cout << channel << " updated likes: " << videoLikes << endl;
return 0;
}
CS Engineering Gyan likes through pointer: 800 CS Engineering Gyan updated likes: 950
Here, *likesPtr refers to the value stored at the address the pointer holds. When that value is changed through the pointer, the original variable videoLikes is changed as well, because both are simply two different ways of referring to the exact same memory location.
Unlike ordinary variables, pointers support a limited set of arithmetic operations, and these operations behave differently than they would on plain numbers. Adding one to a pointer does not increase the stored address by exactly one byte, but rather by the size of the data type the pointer refers to.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int weeklyUploads[4] = {2, 3, 1, 4};
int* uploadPtr = weeklyUploads;
cout << channel << " first week uploads: " << *uploadPtr << endl;
uploadPtr++;
cout << channel << " second week uploads: " << *uploadPtr << endl;
return 0;
}
CS Engineering Gyan first week uploads: 2 CS Engineering Gyan second week uploads: 3
In this example, incrementing uploadPtr moves it forward by the size of one integer, which allows it to point to the very next element of the array. This behavior is what makes pointer arithmetic so closely tied to how arrays work internally.
An array name in C++ behaves very similarly to a pointer to its first element, which is why arrays and pointers are often discussed together. This close relationship allows array elements to be accessed either using regular index notation or through pointer notation.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int dailyViews[5] = {1500, 1800, 2100, 1950, 2200};
int* viewsPtr = dailyViews;
for (int i = 0; i < 5; i++) {
cout << channel << " day " << (i + 1) << " views: " << *(viewsPtr + i) << endl;
}
return 0;
}
CS Engineering Gyan day 1 views: 1500 CS Engineering Gyan day 2 views: 1800 CS Engineering Gyan day 3 views: 2100 CS Engineering Gyan day 4 views: 1950 CS Engineering Gyan day 5 views: 2200
The expression *(viewsPtr + i) is functionally equivalent to dailyViews[i], since array indexing in C++ is internally translated into pointer arithmetic by the compiler. Understanding this relationship makes it much easier to reason about how arrays are handled in memory.
Pointers are frequently used as function parameters, since passing a pointer allows a function to directly access and modify the original variable from the calling code, rather than working with a separate copy of it.
#include <iostream>
using namespace std;
void doubleSubscribers(int* countPtr) {
*countPtr = *countPtr * 2;
}
int main() {
string channel = "CS Engineering Gyan";
int subscribers = 30000;
doubleSubscribers(&subscribers);
cout << channel << " subscribers after campaign: " << subscribers << endl;
return 0;
}
CS Engineering Gyan subscribers after campaign: 60000
Because the function receives the address of subscribers rather than a copy of its value, any change made inside the function through the pointer is reflected in the original variable once the function finishes executing.
A null pointer is a pointer that has been deliberately set to point to nothing at all. This is useful for indicating that a pointer is not currently associated with any valid memory address, and checking for a null pointer before dereferencing it is considered a safe programming habit.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int* commentPtr = nullptr;
if (commentPtr == nullptr) {
cout << channel << " comment pointer is not assigned yet" << endl;
}
int latestComment = 1;
commentPtr = &latestComment;
cout << channel << " comment pointer now points to a value: " << *commentPtr << endl;
return 0;
}
CS Engineering Gyan comment pointer is not assigned yet CS Engineering Gyan comment pointer now points to a value: 1
Attempting to dereference a null pointer, meaning trying to read or modify the value it supposedly points to, leads to undefined behavior and commonly crashes the program, so this check is an important safeguard in real applications.
Arrays created in the usual way have a fixed size that must be known while writing the code. Pointers allow a program to request memory while it is actually running, using the new keyword, which is especially useful when the required amount of storage is not known in advance.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int videoCount;
cout << "Enter number of videos to store: ";
cin >> videoCount;
int* videoViews = new int[videoCount];
for (int i = 0; i < videoCount; i++) {
videoViews[i] = (i + 1) * 500;
}
cout << channel << " views for video 1: " << videoViews[0] << endl;
delete[] videoViews;
return 0;
}
Enter number of videos to store: 4 CS Engineering Gyan views for video 1: 500
Memory that is allocated dynamically using new must be released manually using delete or delete[] for arrays, once it is no longer needed. Failing to release dynamically allocated memory results in what is known as a memory leak, where the memory remains reserved even though the program can no longer access it.
A dangling pointer is a pointer that still holds the address of memory that has already been freed or that no longer belongs to the program. Using a dangling pointer can lead to unpredictable results, since the memory it points to may have already been reused for something else entirely.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int* tempPtr = new int(100);
cout << channel << " value before deletion: " << *tempPtr << endl;
delete tempPtr;
tempPtr = nullptr;
cout << channel << " pointer safely reset after deletion" << endl;
return 0;
}
CS Engineering Gyan value before deletion: 100 CS Engineering Gyan pointer safely reset after deletion
Resetting a pointer to nullptr immediately after freeing its memory is a common and reliable technique for avoiding accidental use of a dangling pointer later in the program.
C++ also allows a pointer to store the address of another pointer, rather than the address of an ordinary variable. This concept, known as a pointer to pointer, adds another level of indirection and is useful in certain advanced data structures and function designs.
#include <iostream>
using namespace std;
int main() {
string channel = "CS Engineering Gyan";
int rank = 1;
int* rankPtr = &rank;
int** rankPtrPtr = &rankPtr;
cout << channel << " rank through double pointer: " << **rankPtrPtr << endl;
return 0;
}
CS Engineering Gyan rank through double pointer: 1
Here, rankPtrPtr stores the address of rankPtr, which in turn stores the address of rank. Dereferencing it twice, using two asterisks, ultimately retrieves the original value stored in rank.
| Advantages | Limitations |
|---|---|
| Allow direct access to and manipulation of memory addresses. | Incorrect use can lead to undefined behavior or program crashes. |
| Enable dynamic memory allocation for flexible, resizable storage. | Manual memory management can result in memory leaks if forgotten. |
| Allow functions to modify the original variables passed to them. | Pointer arithmetic errors can cause access to invalid memory locations. |
| Mistake | Correct Practice |
|---|---|
| Using a pointer without initializing it first. | Always assign a valid address or nullptr before using a pointer. |
| Forgetting to free dynamically allocated memory. | Use delete or delete[] for every memory block created with new. |
| Dereferencing a pointer after its memory has been freed. | Reset the pointer to nullptr right after deleting it. |
| Confusing the address-of operator with the dereference operator. | Remember that & retrieves an address, while * accesses the value at that address. |
Pointers give a C++ programmer direct control over memory, allowing values to be accessed, modified, and shared across different parts of a program without unnecessary copying. Understanding how to declare, initialize, and dereference pointers is an essential step toward writing efficient and flexible C++ code.
The close relationship between pointers and arrays, along with the ability to allocate memory dynamically using new and delete, opens the door to building programs that can adapt their memory usage based on actual requirements at runtime. At the same time, concepts like null pointers and dangling pointers highlight why careful and disciplined memory management is such an important skill in C++.
With a solid understanding of pointers, you are now ready to move deeper into Object-Oriented Programming in C++, where these same ideas of memory and references play an important role in how classes and objects interact with one another.