Instagram
youtube
Facebook
Twitter

Pointers and References

Pointers and References:

Pointers and references are two important generalities in C that allow for circular access and manipulation of variables. While they serve analogous purposes, they have some differences in their syntax and behaviour.

  • Pointers: It is a variable that holds the memory address of another variable. Pointers are declared using the asterisk(*) symbol before the variable name. To assign the address of the variable to a pointer, you use the address-of operator(&). Dereferencing a pointer means accessing the value stored at the memory address it points to using the dereference operator(*). Pointers can be reassigned to point to different memory locations. Pointers can be used for dynamic memory allocation and deallocation using the new and delete operators.

Illustration:

#include <iostream>

using namespace std;

int main() {

    int num = 10;

    int* ptr;  

    ptr = &num; 

    cout << "Value of num: " << num << endl;

    cout << "Address of num: " << &num << endl;

    cout << "Value of ptr: " << ptr << endl; 

    cout << "Value pointed to by ptr: " << *ptr << endl; 

    int arr[5] = {10, 20, 30, 40, 50};

    int* arrPtr = arr;  

    cout << "First element of arr: " << *arrPtr << endl;

    cout << "Second element of arr: " << *(arrPtr + 1) << endl;

    return 0;

}
  • References: A reference is an alias or indispensable name for an existing variable. References are declared using the ampersand(&) symbol after the variable name. A reference must be initialised when declared and cannot be changed to refer to another variable. Modifying a reference will modify the original variable it references. References provide an accessible way to work with existing variables without the need for pointer syntax.

Illustration:

#include <iostream>

using namespace std;

int main() {

    int num = 10;

    int &refNum = num;  

    cout << "num: " << num << endl;

    cout << "refNum: " << refNum << endl;

    refNum = 20;  

    cout << "num after modification: " << num << endl;

    cout << "refNum after modification: " << refNum << endl;

    return 0;

}

Pointers and references are both used in colourful scenarios, such as passing variables to functions by reference, creating linked data structures, and dynamic memory management. Understanding their differences and applicable use cases is important for writing effective and correct C++ code.

  • Smart Pointers:Smart pointers are a feature in C++ that provides automatic memory operation and helps prevent memory leaks. They’re defined in the <memory> header and implemented as classes. Smart pointers are designed to wrap raw pointers and manage the lifetime of dynamically allocated objects.

There are three types of smart pointers available in C++:

  • std::unique_ptr: std::unique_ptr is used to manage a single object allocated on the heap. It ensures exclusive ownership of the pointed object, meaning only one std::unique_ptr can own it at a time. When the std::unique_ptr goes out of scope or is explicitly reset, it deletes the associated object automatically.

Illustration:

#include <iostream>

#include <memory>

using namespace std;

int main() {

    unique_ptr<int> uniquePtr = make_unique<int>(5);

    cout << "Value: " << *uniquePtr << endl;

    return 0;

}
  • std::shared_ptr: std::shared_ptr allows multiple smart pointers to share ownership of the same object. It keeps track of the number of references to the object and automatically deallocates it when the laststdshared_ptr pointing to it goes out of scope or is reset. std::shared_ptr uses a reference count mechanism to manage ownership.

Illustration:

#include <iostream>

#include <memory>

using namespace std;

class MyClass {

public:

    MyClass() { cout << "MyClass constructed." << endl; }

    ~MyClass() { cout << "MyClass destructed." << endl; }

};

int main() {

    shared_ptr<MyClass> sharedPtr1 = make_shared<MyClass>();

    shared_ptr<MyClass> sharedPtr2 = sharedPtr1; 

    return 0;

}  
  • std::weak_ptr: std::weak_ptr is used in conjunction with std::weak_ptr to break cyclic dependencies and avoid memory leaks. Unlike std::shared_ptr, std::weak_ptr doesn’t increment the reference count of the object. It provides an anonymous, weak reference to the object held by a std::shared_ptr.

Illustration:

#include <iostream>

#include <memory>

using namespace std;

int main() {

    shared_ptr<int> sharedPtr = make_shared<int>(10);

    weak_ptr<int> weakPtr = sharedPtr; 

    if (auto locked = weakPtr.lock()) {  

        cout << "Weak pointer value: " << *locked << endl;

    } else {

        cout << "Weak pointer is expired." << endl;

    }

    return 0;

}

Smart pointers automatically handle deallocation, reducing the risk of memory leaks and making memory management more accessible. Still, it’s important to note that intelligent pointers have overhead compared to raw pointers due to their redundant tasks. Using them appropriately is also pivotal to avoiding indirect dependence and implicit hanging references.

Using intelligent pointers can significantly improve code safety and readability, as they provide an advanced level of abstraction and help manage dynamic memory efficiently in C++. 

  • Raw Pointers:Raw pointers are the most introductory form of pointers in C++. They’re simple variables that store the memory addresses of other variables or objects. Unlike smart pointers, raw pointers don’t provide any automatic memory operations, and it’s the programmer’s responsibility to handle memory allocation and deallocation.

Then how do raw pointers work?

  • Declaration and Initialization:Raw pointers are declared by specifying the data type followed by an asterisk(*) before the variable name. They can be initiated by assigning the memory address of another variable or the result of a memory allocation operation (e.g., with new).

Illustration:

#include <iostream>

using namespace std;

int main() {

    int* myPointer;

    myPointer = new int;

    *myPointer = 42;

    cout << "Value stored in the dynamic memory: " << *myPointer << endl;

    delete myPointer;

    myPointer = nullptr;

    return 0;

}
  • Dereferencing:To access the value stored at the memory address pointed to by a raw pointer, you use the dereference operator(*).

Illustration:

#include <iostream>

using namespace std;

int main() {

    int* myPointer;

    myPointer = new int;

    *myPointer = 42;

    cout << "Value stored in the dynamic memory: " << *myPointer << endl;

    delete myPointer;

    myPointer = nullptr;

    return 0;

}

Dynamic Memory Allocation:Raw pointers are generally used to manage dynamically allocated memory using new. Dynamic memory allocation allows you to allocate memory on the heap, which persists until explicitly deallocated.

Illustration:

#include <iostream>

using namespace std;

int main() {

    int* ptr;  

    ptr = new int;

    if (ptr != nullptr) {

        *ptr = 42; 

        cout << "Dynamically allocated value: " << *ptr << endl;

        delete ptr;

    } else {

        cout << "Memory allocation failed!" << endl;

    }

    return 0;

}
  • Manual Deallocation:Unlike smart pointers, raw pointers don’t automatically release the memory they point to. It’s essential to explicitly deallocate memory using the delete operator to avoid memory leaks.

illustration: 

#include <iostream>

using namespace std;

int main() {

    int* ptr = new int; 

    if (ptr != nullptr) {

        *ptr = 42;  

        cout << "Dynamically allocated value: " << *ptr << endl;

        delete ptr;

    } else {

        cout << "Memory allocation failed!" << endl;

 }

      return 0;

}

Since raw pointers lack automatic memory operation, they’re prone to causing memory leaks and hanging pointer issues if not used precisely. Thus, using smart pointers or other RAII (resource acquisition and initialization) techniques whenever possible is recommended to ensure safer and more dependable memory operation in C++.