Core Concepts
Memory Addresses and Pointers
Every byte in a computer's memory has a unique numerical label known as a memory address. You can think of memory as a hotel and addresses as room numbers.
A pointer is a variable designed to store these memory addresses. While the address itself is the pointer value, the variable holding its called a pointer variable.
The primary utility of pointers is the ability to access and modify data indirectly without using the variable's original name.
Declaration and Basic Usage
Pointers are typed variables. To define one, you specify the data type it points to, followed by an asterisk (*) and the variable name. The ampersand (&) operator retrieves the address of a variable.
#include <iostream>
using namespace std;
int main() {
int score = 50;
// Print the memory address of 'score'
cout << "Address of score: " << &score << endl;
// Declare a pointer that holds the address of an integer
int* ptr = &score;
// Dereferencing: '*' accesses the value at the stored address
cout << "Pointer Address: " << ptr << endl;
cout << "Value via Pointer: " << *ptr << endl;
return 0;
}
Indirect Modification
Once a pointer holds an address, you can change the original variable's value by dereferencing the pointer on the left side of an assignment.
#include <iostream>
using namespace std;
int main() {
int x = 10;
int* p = &x;
// Modify 'x' indirectly
*p = 999;
cout << "Updated x: " << x << endl; // Output: 999
int y = 20;
p = &y; // Reassign pointer to point to y
*p = 888;
cout << "Updated y: " << y << endl; // Output: 888
return 0;
}
Pointer Constants (Const Correctness)
Qualifiers change how a pointer interacts with memory:
- Pointer to Const: The data pointed to cannot be changed via this pointer.
- Const Pointer: The address stored in the pointer cannot be changed.
- Const Pointer to Const: Neither the address nor the data can be changed.
The rule of thumb: read from right to left. int const * p means "p is a pointer to a constant int". int * const p means "p is a constant pointer to an int".
#include <iostream>
using namespace std;
int main() {
int val1 = 100, val2 = 200;
const int* p1 = &val1; // Pointer to const data
// *p1 = 500; // Error: cannot modify data
p1 = &val2; // OK: pointer can move
int* const p2 = &val1; // Const pointer
*p2 = 500; // OK: data can be modified
// p2 = &val2; // Error: cannot change address
const int* const p3 = &val1; // Const pointer to const data
// *p3 = 500; // Error
// p3 = &val2; // Error
return 0;
}
Pointer Size and Stride
Size
The size of a pointer depends on the CPU architecture, not the data type it points to. On a 64-bit system, pointers are typically 8 bytes; on a 32-bit system, they are 4 bytes.
#include <iostream>
using namespace std;
int main() {
int* intPtr;
double* dblPtr;
char** charPtr;
cout << "Size of int*: " << sizeof(intPtr) << endl;
cout << "Size of double*: " << sizeof(dblPtr) << endl;
cout << "Size of char**: " << sizeof(charPtr) << endl;
return 0;
}
Stride (Pointer Arithmetic)
When you increment a pointer (ptr + 1), it moves forward by the size of the data type it points to.
#include <iostream>
using namespace std;
int main() {
char c;
char* cp = &c;
cout << "Char stride: " << (void*)(cp + 1) << " - " << (void*)cp << " = 1 byte" << endl;
int i;
int* ip = &i;
cout << "Int stride: " << (void*)(ip + 1) << " - " << (void*)ip << " = 4 bytes" << endl;
return 0;
}
Null Pointers and Dangling Pointers
- Wild/Dangling Pointers: Pointers that store arbitrary addresses (e.g.,
int* p = (int*)0x123;) or uninitialized pointers. Accessing them causes undefined behavior. - Null Pointers: Assign
nullptr(orNULL) to a pointer if it isn't pointing to a valid object yet. This signals that the pointer is safe to check against.
#include <iostream>
using namespace std;
int main() {
int* unsafe = (int*)0x12345; // Wild pointer (dangerous)
// *unsafe = 10; // Crash or undefined behavior
int* safe = nullptr; // Null pointer
if (safe != nullptr) {
cout << *safe << endl;
} else {
cout << "Pointer is null, cannot dereference." << endl;
}
return 0;
}
Multi-level Pointers
A pointer can store the address of another pointer. While rare in application logic, they are useful for dynamic memory (2D arrays) or modifying the pointer variable itself in a function.
#include <iostream>
using namespace std;
int main() {
int val = 500;
int* lvl1 = &val; // Level 1: points to int
int** lvl2 = &lvl1; // Level 2: points to int*
int*** lvl3 = &lvl2;// Level 3: points to int**
cout << "Value via lvl1: " << *lvl1 << endl;
cout << "Value via lvl2: " << **lvl2 << endl;
cout << "Value via lvl3: " << ***lvl3 << endl;
return 0;
}
Passing Arguments to Functions
Pass by Value
The function receives a copy of the data. Changes inside the function do not affect the caller.
#include <iostream>
using namespace std;
void swapValue(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 5, y = 10;
swapValue(x, y);
cout << "x: " << x << ", y: " << y << endl; // Still 5, 10
return 0;
}
Pass by Address (Pointers)
Passing the address alllows the function to modify the original variable.
#include <iostream>
using namespace std;
void swapAddr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
swapAddr(&x, &y);
cout << "x: " << x << ", y: " << y << endl; // Now 10, 5
return 0;
}
Function Pointers and Callbacks
Function Pointers
A function name decomposes into a memory address. A function pointer stores this address, allowing functions to be passed as arguments.
#include <iostream>
using namespace std;
void greet() {
cout << "Hello from function pointer!" << endl;
}
int main() {
void (*funcPtr)() = greet; // Define pointer and assign address
funcPtr(); // Call via pointer
return 0;
}
Callbacks
Using function pointers as parameters creates callbacks, enabling generic algorithms to execute specific user-defined logic.
#include <iostream>
using namespace std;
int multiply(int a, int b) { return a * b; }
int add(int a, int b) { return a + b; }
// Calculator accepts a function pointer as a callback
int compute(int x, int y, int (*operation)(int, int)) {
return operation(x, y);
}
int main() {
cout << "Sum: " << compute(5, 3, add) << endl;
cout << "Product: " << compute(5, 3, multiply) << endl;
return 0;
}