C++ Pointers, References, and Dereferencing: Complete Guide and Common Scenarios
C++ Pointers, References, and Dereferencing: Complete Guide and Common Scenarios
Understanding pointers, references, and dereferencing is fundamental to C++ programming. This guide covers all aspects with practical scenarios.
Pointers Basics
What is a Pointer?
A pointer is a variable that stores the memory address of another variable.
int x = 42;
int* ptr = &x; // ptr stores the address of x
std::cout << "Value of x: " << x << std::endl; // 42
std::cout << "Address of x: " << &x << std::endl; // Memory address
std::cout << "Value of ptr: " << ptr << std::endl; // Same address
std::cout << "Address of ptr: " << &ptr << std::endl; // Address of ptr itself
Pointer Declaration and Initialization
// Declaration
int* ptr1; // Pointer to int
int *ptr2; // Same (spacing doesn't matter)
int * ptr3; // Same
// Initialization
int x = 10;
int* ptr = &x; // Points to x
// Null pointer
int* null_ptr = nullptr; // C++11: preferred
int* null_ptr2 = NULL; // C-style (avoid)
int* null_ptr3 = 0; // Also works (avoid)
// Uninitialized pointer (dangerous!)
int* uninit_ptr; // Contains garbage address - undefined behavior if used
Pointer Arithmetic
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr; // Points to first element
std::cout << *ptr << std::endl; // 10
std::cout << *(ptr + 1) << std::endl; // 20
std::cout << *(ptr + 2) << std::endl; // 30
// Increment pointer
ptr++;
std::cout << *ptr << std::endl; // 20
// Array indexing with pointers
std::cout << ptr[0] << std::endl; // 20
std::cout << ptr[1] << std::endl; // 30
// Pointer difference
int* ptr2 = arr + 3;
std::cout << ptr2 - ptr << std::endl; // 3 (number of elements between)
Pointer to Pointer
int x = 42;
int* ptr = &x; // Pointer to int
int** pptr = &ptr; // Pointer to pointer to int
std::cout << x << std::endl; // 42
std::cout << *ptr << std::endl; // 42
std::cout << **pptr << std::endl; // 42
// Modify through double pointer
**pptr = 100;
std::cout << x << std::endl; // 100
References Basics
What is a Reference?
A reference is an alias for an existing variable. It must be initialized and cannot be reassigned.
int x = 42;
int& ref = x; // ref is an alias for x
std::cout << x << std::endl; // 42
std::cout << ref << std::endl; // 42
ref = 100; // Modifies x
std::cout << x << std::endl; // 100
std::cout << ref << std::endl; // 100
Reference Declaration
int x = 10;
int& ref1 = x; // Reference to int
int &ref2 = x; // Same (spacing doesn't matter)
int & ref3 = x; // Same
// Must be initialized
// int& ref; // Error: must be initialized
// Cannot be reassigned
int y = 20;
int& ref = x;
// ref = y; // This assigns value, doesn't rebind reference
Const References
int x = 42;
const int& cref = x; // Const reference - cannot modify through cref
// cref = 100; // Error: cannot modify through const reference
x = 100; // OK: can modify original
// Temporary binding
const int& temp_ref = 42; // OK: binds to temporary
// int& temp_ref2 = 42; // Error: non-const cannot bind to temporary
Reference vs Pointer
int x = 42;
// Pointer
int* ptr = &x;
*ptr = 100; // Modify through pointer
ptr = nullptr; // Can reassign pointer
// Reference
int& ref = x;
ref = 100; // Modify through reference (simpler syntax)
// ref = nullptr; // Error: cannot reassign reference
// Key differences:
// 1. Reference must be initialized, pointer can be null
// 2. Reference cannot be reassigned, pointer can
// 3. Reference syntax is simpler (no * needed)
// 4. Reference cannot be null, pointer can
Dereferencing
What is Dereferencing?
Dereferencing means accessing the value at the address stored in a pointer.
int x = 42;
int* ptr = &x;
// Dereference operator *
int value = *ptr; // Gets value at address stored in ptr
std::cout << value << std::endl; // 42
// Modify through pointer
*ptr = 100;
std::cout << x << std::endl; // 100
Dereferencing Operations
int arr[] = {10, 20, 30};
int* ptr = arr;
// Basic dereference
std::cout << *ptr << std::endl; // 10
// Increment then dereference
std::cout << *++ptr << std::endl; // 20 (pre-increment)
// Dereference then increment
std::cout << *ptr++ << std::endl; // 20, then ptr points to 30
// Array subscript (implicit dereference)
std::cout << ptr[0] << std::endl; // 30
std::cout << *(ptr + 0) << std::endl; // Same as above
// Member access through pointer
struct Point {
int x, y;
};
Point p{10, 20};
Point* pptr = &p;
std::cout << (*pptr).x << std::endl; // 10
std::cout << pptr->x << std::endl; // 10 (arrow operator, preferred)
Arrow Operator (->)
struct Person {
std::string name;
int age;
void print() {
std::cout << name << ", " << age << std::endl;
}
};
Person person{"Alice", 30};
Person* ptr = &person;
// Arrow operator for member access
std::cout << ptr->name << std::endl; // "Alice"
std::cout << ptr->age << std::endl; // 30
ptr->print(); // Calls method
// Equivalent to
std::cout << (*ptr).name << std::endl; // Same, but arrow is preferred
Null Pointer Dereferencing
int* ptr = nullptr;
// Dangerous: dereferencing null pointer
// int value = *ptr; // Undefined behavior - crash or corruption
// Safe: check before dereferencing
if (ptr != nullptr) {
int value = *ptr;
}
// Or use short form
if (ptr) {
int value = *ptr;
}
Common Scenarios
Scenario 1: Function Parameters
Pass by Value
void modify_value(int x) {
x = 100; // Only modifies local copy
}
int main() {
int x = 42;
modify_value(x);
std::cout << x << std::endl; // Still 42
return 0;
}
Pass by Pointer
void modify_pointer(int* ptr) {
if (ptr != nullptr) {
*ptr = 100; // Modifies original
}
}
int main() {
int x = 42;
modify_pointer(&x);
std::cout << x << std::endl; // 100
return 0;
}
Pass by Reference
void modify_reference(int& ref) {
ref = 100; // Modifies original (simpler than pointer)
}
int main() {
int x = 42;
modify_reference(x);
std::cout << x << std::endl; // 100
return 0;
}
Pass by Const Reference (Recommended for Large Objects)
void print_large_object(const std::vector<int>& vec) {
// Can read but not modify
for (const auto& val : vec) {
std::cout << val << " ";
}
}
int main() {
std::vector<int> large_vec(1000000, 42);
print_large_object(large_vec); // Efficient: no copy
return 0;
}
Scenario 2: Dynamic Memory Allocation
// Allocate single object
int* ptr = new int(42);
std::cout << *ptr << std::endl; // 42
delete ptr; // Must delete
ptr = nullptr; // Good practice
// Allocate array
int* arr = new int[10];
for (int i = 0; i < 10; ++i) {
arr[i] = i;
}
delete[] arr; // Use delete[] for arrays
arr = nullptr;
// Common mistake: mismatch
int* ptr2 = new int;
// delete[] ptr2; // Wrong: should be delete
delete ptr2; // Correct
int* arr2 = new int[10];
// delete arr2; // Wrong: should be delete[]
delete[] arr2; // Correct
Scenario 3: Array Manipulation
void process_array(int* arr, size_t size) {
for (size_t i = 0; i < size; ++i) {
arr[i] *= 2; // Modify array elements
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
process_array(arr, 5);
for (int val : arr) {
std::cout << val << " "; // 2 4 6 8 10
}
return 0;
}
// Or using references
void process_array_ref(int (&arr)[5]) { // Size must match
for (int& val : arr) {
val *= 2;
}
}
Scenario 4: Returning Pointers/References
Return Pointer
int* create_int(int value) {
int* ptr = new int(value);
return ptr; // Caller must delete
}
// Better: return by value or use smart pointer
std::unique_ptr<int> create_int_safe(int value) {
return std::make_unique<int>(value);
}
Return Reference
int& get_element(int* arr, size_t index) {
return arr[index]; // Return reference to element
}
int main() {
int arr[] = {10, 20, 30};
int& ref = get_element(arr, 1);
ref = 100; // Modifies arr[1]
std::cout << arr[1] << std::endl; // 100
return 0;
}
// Dangerous: returning reference to local
int& bad_function() {
int x = 42;
return x; // Dangling reference - undefined behavior!
}
Scenario 5: Linked Data Structures
struct Node {
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
class LinkedList {
public:
LinkedList() : head(nullptr) {}
void append(int value) {
Node* new_node = new Node(value);
if (head == nullptr) {
head = new_node;
} else {
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = new_node;
}
}
void print() {
Node* current = head;
while (current != nullptr) {
std::cout << current->data << " ";
current = current->next;
}
std::cout << std::endl;
}
~LinkedList() {
while (head != nullptr) {
Node* temp = head;
head = head->next;
delete temp;
}
}
private:
Node* head;
};
// Usage
LinkedList list;
list.append(10);
list.append(20);
list.append(30);
list.print(); // 10 20 30
Scenario 6: Function Pointers
// Function pointer type
using FuncPtr = int(*)(int, int);
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int main() {
FuncPtr ptr = add;
std::cout << ptr(3, 4) << std::endl; // 7
ptr = multiply;
std::cout << ptr(3, 4) << std::endl; // 12
// Array of function pointers
FuncPtr operations[] = {add, multiply};
std::cout << operations[0](5, 6) << std::endl; // 11
std::cout << operations[1](5, 6) << std::endl; // 30
return 0;
}
Scenario 7: Callback Functions
#include <functional>
class EventHandler {
public:
using Callback = std::function<void(int)>;
void setCallback(Callback cb) {
callback_ = cb;
}
void trigger(int value) {
if (callback_) {
callback_(value);
}
}
private:
Callback callback_;
};
int main() {
EventHandler handler;
// Set callback using lambda
handler.setCallback([](int value) {
std::cout << "Event: " << value << std::endl;
});
handler.trigger(42); // Event: 42
return 0;
}
Scenario 8: Polymorphism
class Base {
public:
virtual void print() {
std::cout << "Base" << std::endl;
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived" << std::endl;
}
};
int main() {
// Pointer to base
Base* ptr = new Derived();
ptr->print(); // Derived (virtual dispatch)
delete ptr;
// Reference to base
Derived d;
Base& ref = d;
ref.print(); // Derived (virtual dispatch)
return 0;
}
Scenario 9: Optional Parameters
void process_data(int* data, size_t size, int* result = nullptr) {
int sum = 0;
for (size_t i = 0; i < size; ++i) {
sum += data[i];
}
if (result != nullptr) {
*result = sum;
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
// Without result
process_data(arr, 5);
// With result
int sum;
process_data(arr, 5, &sum);
std::cout << "Sum: " << sum << std::endl; // 15
return 0;
}
Scenario 10: String Manipulation
#include <cstring>
void reverse_string(char* str) {
if (str == nullptr) return;
size_t len = strlen(str);
char* start = str;
char* end = str + len - 1;
while (start < end) {
char temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
int main() {
char str[] = "Hello";
reverse_string(str);
std::cout << str << std::endl; // "olleH"
return 0;
}
Scenario 11: Two-Dimensional Arrays
// Static 2D array
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// Pointer to first row
int* ptr = matrix[0];
std::cout << ptr[5] << std::endl; // 6 (row 1, col 1)
// Pointer to pointer (dynamic)
int** dynamic_matrix = new int*[3];
for (int i = 0; i < 3; ++i) {
dynamic_matrix[i] = new int[4];
}
// Access
dynamic_matrix[1][2] = 42;
// Cleanup
for (int i = 0; i < 3; ++i) {
delete[] dynamic_matrix[i];
}
delete[] dynamic_matrix;
Scenario 12: Swap Function
// Using pointers
void swap_ptr(int* a, int* b) {
if (a == nullptr || b == nullptr) return;
int temp = *a;
*a = *b;
*b = temp;
}
// Using references (preferred)
void swap_ref(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swap_ptr(&x, &y);
std::cout << x << " " << y << std::endl; // 20 10
swap_ref(x, y);
std::cout << x << " " << y << std::endl; // 10 20
return 0;
}
Smart Pointers
std::unique_ptr
#include <memory>
// Automatic memory management
{
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl; // 42
// Automatically deleted when out of scope
}
// Transfer ownership
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
// std::unique_ptr<int> ptr2 = ptr1; // Error: cannot copy
std::unique_ptr<int> ptr2 = std::move(ptr1); // OK: move ownership
std::shared_ptr
#include <memory>
// Shared ownership
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // OK: shared ownership
std::cout << ptr1.use_count() << std::endl; // 2
std::cout << *ptr1 << std::endl; // 42
std::cout << *ptr2 << std::endl; // 42
// Automatically deleted when last reference is destroyed
std::weak_ptr
#include <memory>
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// Check if object still exists
if (auto locked = weak.lock()) {
std::cout << *locked << std::endl; // 42
} else {
std::cout << "Object destroyed" << std::endl;
}
Smart Pointer vs Raw Pointer
// Raw pointer (manual management)
int* raw_ptr = new int(42);
// ... use raw_ptr ...
delete raw_ptr; // Must remember to delete
// Smart pointer (automatic management)
std::unique_ptr<int> smart_ptr = std::make_unique<int>(42);
// ... use smart_ptr ...
// Automatically deleted - no manual delete needed
// Prefer smart pointers in modern C++
Best Practices
1. Prefer References Over Pointers When Possible
// Good: Use reference
void process(int& value) {
value *= 2;
}
// OK but more verbose: Use pointer
void process_ptr(int* value) {
if (value != nullptr) {
*value *= 2;
}
}
2. Use Smart Pointers Instead of Raw Pointers
// Bad: Raw pointer
int* ptr = new int(42);
delete ptr;
// Good: Smart pointer
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// Automatic cleanup
3. Always Initialize Pointers
// Bad: Uninitialized
int* ptr;
*ptr = 42; // Undefined behavior
// Good: Initialize
int* ptr = nullptr;
if (ptr != nullptr) {
*ptr = 42;
}
4. Check for Null Before Dereferencing
void safe_dereference(int* ptr) {
if (ptr != nullptr) {
*ptr = 42;
}
}
// Or use short form
if (ptr) {
*ptr = 42;
}
5. Use Const Correctness
// Const pointer (pointer itself is const)
int x = 10;
int* const ptr = &x; // Cannot reassign ptr
// ptr = nullptr; // Error
// Pointer to const (value is const)
const int* ptr2 = &x; // Cannot modify through ptr2
// *ptr2 = 20; // Error
// Const pointer to const
const int* const ptr3 = &x; // Both are const
6. Avoid Dangling Pointers/References
// Bad: Dangling pointer
int* get_ptr() {
int x = 42;
return &x; // x is destroyed when function returns
}
// Bad: Dangling reference
int& get_ref() {
int x = 42;
return x; // x is destroyed when function returns
}
// Good: Return by value or use smart pointer
std::unique_ptr<int> get_safe() {
return std::make_unique<int>(42);
}
7. Use nullptr Instead of NULL or 0
// Bad: Old style
int* ptr = NULL;
int* ptr2 = 0;
// Good: Modern C++
int* ptr = nullptr;
Common Pitfalls
1. Memory Leaks
// Bad: Memory leak
void leak() {
int* ptr = new int(42);
// Forgot to delete - memory leak!
}
// Good: Use smart pointer
void no_leak() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// Automatically deleted
}
2. Double Delete
// Bad: Double delete
int* ptr = new int(42);
delete ptr;
delete ptr; // Undefined behavior!
// Good: Set to nullptr after delete
int* ptr = new int(42);
delete ptr;
ptr = nullptr;
delete ptr; // Safe: deleting nullptr is no-op
3. Mismatched new/delete
// Bad: Mismatch
int* ptr = new int;
delete[] ptr; // Wrong!
int* arr = new int[10];
delete arr; // Wrong!
// Good: Match correctly
int* ptr = new int;
delete ptr; // Correct
int* arr = new int[10];
delete[] arr; // Correct
4. Returning Address of Local Variable
// Bad: Dangling pointer
int* bad_function() {
int x = 42;
return &x; // x is destroyed!
}
// Good: Return by value or use dynamic allocation
int good_function() {
int x = 42;
return x; // Returns copy
}
5. Pointer Arithmetic on Non-Array
// Bad: Pointer arithmetic on single object
int x = 42;
int* ptr = &x;
ptr++; // Undefined behavior - points to invalid memory
// Good: Only use pointer arithmetic on arrays
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
ptr++; // OK: points to next element
Summary
Pointers:
- Store memory addresses
- Can be null, reassigned, and used for arithmetic
- Require explicit dereferencing with
* - Need manual memory management (or use smart pointers)
References:
- Aliases for existing variables
- Must be initialized, cannot be reassigned
- Cannot be null
- Simpler syntax (no
*needed) - Automatically dereferenced
Dereferencing:
- Access value at pointer’s address using
* - Use
->for member access through pointers - Always check for null before dereferencing
Best Practices:
- Prefer references for function parameters
- Use smart pointers instead of raw pointers
- Always initialize pointers
- Check for null before dereferencing
- Use
nullptrinstead ofNULL - Avoid dangling pointers/references
Understanding these concepts is crucial for effective C++ programming, especially for memory management, polymorphism, and efficient parameter passing.