C++ Programming
Pointers, Dynamic Memory and Ownership
PGCP-AC
A pointer stores an address through which a program can locate an object or function. Pointers support arrays, dynamic structures, polymorphism, callbacks and low-level interfaces. Their main difficulty is lifetime: an address is useful only while it designates a live object and the permitted access remains valid.
1. Addresses and dereferencing
int value = 42;
int* pointer = &value;
*pointer = 50;
The address-of operator obtains the address of value. The pointer type states that the address is interpreted as an int object. Dereferencing designates that object, so assignment through the pointer changes value.
Dereferencing null, an invalid address or a pointer to an object whose lifetime ended is undefined behaviour. Pointer type and object lifetime must both be correct.
For class objects, arrow combines dereference and member access:
Student* student = ¤t;
student->print();
2. Null pointers
nullptr is the modern null-pointer literal:
int* selected = nullptr;
It converts to pointer types without behaving like an ordinary integer, which makes overload resolution safer than legacy uses of zero or NULL. Null represents absence. Use a reference when a valid object is required and a pointer when absence has a defined meaning.
3. Pointer constness
const int* read_only = &value;
int* const fixed_pointer = &value;
const int* const fixed_read_only = &value;
The first form prevents modification of the int through the pointer but permits reseating the pointer. The second fixes the pointer after initialization but permits modification of the int. The third applies both restrictions.
4. Arrays and pointer arithmetic
In most expressions, a built-in array converts to a pointer to its first element:
int values[]{10, 20, 30};
int* first = values;
std::cout << *(first + 1);
Adding one advances by one element. Ordinary pointer arithmetic is defined only within the same array and its one-past position. The one-past address is a valid endpoint for comparison but cannot be dereferenced.
Subtracting two pointers into the same array yields their element distance. Operations involving unrelated array pointers do not gain meaning merely because their numeric addresses can be printed.
5. Pointer parameters
void double_values(int* values, std::size_t count) {
for (std::size_t i = 0; i < count; ++i) {
values[i] *= 2;
}
}
The pointer supplies element access, while count supplies the missing extent. A pointer parameter is itself passed by value; reseating it does not reseat the caller's pointer. Modern interfaces can use span for non-owning contiguous ranges and vector for owning dynamic sequences.
6. Dynamic objects
int* value = new int{42};
delete value;
The new expression obtains suitable storage and initializes or constructs an object. Delete destroys that object and releases the matching allocation. After deletion, the old address no longer denotes a live object even if the pointer is non-null.
Ordinary allocation failure throws std::bad_alloc. If construction throws, a new expression releases the storage it acquired before propagating the exception.
7. Dynamic arrays
int* values = new int[count]{};
delete[] values;
Array new must be paired with delete[]. Scalar new must be paired with scalar delete. Mismatching them is undefined behaviour. Manual arrays also require separately tracking size, cleanup, exceptions and ownership, so vector should normally be preferred.
8. new and malloc are different
Malloc obtains raw bytes and returns void pointer access. It does not construct C++ objects. Free releases a malloc-family allocation and does not run destructors.
New is type-aware and establishes object lifetime through initialization or construction. Delete invokes destruction and releases matching storage. Never mix these families. Realloc moves raw byte regions and is generally unsuitable for nontrivial C++ objects with constructors and invariants.
9. Lifetime errors
A dangling pointer refers to an object whose lifetime has ended. A memory leak leaves an owned resource unreleased. Double deletion releases one allocation twice. Use-after-free accesses storage after release.
int* first = new int{7};
int* alias = first;
delete first;
Both first and alias now dangle. Setting first to null would not repair alias. The underlying problem is unclear ownership rather than the stored pointer bit pattern.
10. RAII
Resource Acquisition Is Initialization binds a resource to an object's lifetime. A constructor establishes ownership and a destructor releases it. Automatic destruction then handles normal returns and exceptions.
Strings, vectors, file streams, locks and smart pointers use this model. Each resource should have an identifiable owner and transfer of ownership should be explicit.
11. Exclusive ownership
unique_ptr represents one owner:
auto widget = std::make_unique<Widget>(argument);
widget->run();
It releases the object automatically. It cannot be copied, because copying would create two exclusive owners, but it can be moved:
auto destination = std::move(widget);
After the move, the source is empty. make_unique combines allocation and construction in a concise exception-safe expression.
12. Shared and weak ownership
shared_ptr represents shared strong ownership. The object is destroyed after the final strong owner disappears. Reference counting and a control block add cost and make lifetime less local, so shared ownership should be used only when the design truly requires it.
Strong owners can form a cycle and keep one another alive. weak_ptr observes a shared object without extending its lifetime. Its lock operation returns a temporary shared owner when the object still exists.
13. Non-owning pointers
A raw pointer does not inherently state ownership. Modern code commonly uses raw pointers for nullable observation and smart pointers or containers for ownership. The owner must outlive all observers and operations such as vector reallocation can invalidate pointers to elements.
Clear interfaces distinguish borrowing from transfer. Passing a raw pointer does not by itself authorize deletion.
14. The this pointer
Inside a non-static member function, this identifies the current object:
class Counter {
int value{};
public:
Counter& add(int amount) {
this->value += amount;
return *this;
}
};
It can disambiguate a member from a parameter and supports returning the current object for chaining. Static member functions have no current object and no this pointer.
15. Polymorphic ownership
A base pointer can refer to a derived object. Virtual functions dispatch according to the dynamic type. If an object can be deleted through a base pointer, the base needs a virtual destructor:
std::unique_ptr<Base> object =
std::make_unique<Derived>();
Without a suitable virtual destructor, deletion through the base is undefined behaviour.
16. Arrays of pointers
An array may store pointer values rather than the pointed objects:
const char* names[]{"Asha", "Neel", "Ravi"};
The array contains three addresses. The string literals have static storage duration, so these observations remain valid for the program. An array of dynamically allocated pointers is more difficult: the programmer must know whether each pointer owns a separate allocation and must release each owner correctly before releasing the pointer array.
A vector of values is simpler when objects should be stored together. A vector of unique pointers is suitable when elements require stable addresses, polymorphism or independent dynamic lifetime:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
The vector owns the smart pointers and each smart pointer owns one Shape object.
17. Pointer to pointer
A pointer can point to another pointer:
int value = 10;
int* pointer = &value;
int** outer = &pointer;
One dereference produces pointer; two dereferences reach value. Pointer-to-pointer parameters appear in legacy interfaces that change a caller's pointer and in C-style tables of strings. In ordinary C++, returning a value, using a reference to pointer or using an ownership type usually makes the contract clearer.
The command-line argv parameter is commonly written as char** because it designates a sequence of pointers to character arrays. This does not mean the application owns those argument strings.
18. Void pointers and byte access
A void pointer can hold the address of an object without retaining its specific object type. It cannot be dereferenced directly because the compiler does not know the pointed representation or element size. A suitable conversion is required before typed access.
Low-level C APIs use void pointers for generic storage, callbacks and allocation. C++ templates, variants, base-class interfaces and type-erased library facilities normally preserve more type information and should be preferred.
Object representation may be inspected through character or byte types under the language rules, but arbitrary reinterpretation of bytes as an unrelated object can violate alignment, lifetime and aliasing requirements. An address conversion alone does not create a valid object.
19. Custom deleters and non-memory resources
Smart pointers can own resources that require a cleanup function other than delete. A unique pointer with a custom deleter can manage a C library handle:
using FileHandle = std::unique_ptr<FILE, decltype(&std::fclose)>;
FileHandle file(std::fopen("data.txt", "r"), &std::fclose);
When file leaves scope, the deleter calls fclose. This demonstrates that ownership is broader than heap memory. Files, sockets, operating-system handles and library contexts all need explicit lifetime policies.
The deleter becomes part of a unique pointer's type and can affect its size. A shared pointer stores its deleter in the control block. Factory functions can hide these details behind a clear resource-owning alias or wrapper class.
20. Manual C-style string operations
Pointer traversal explains traditional string functions. A length operation advances until the null terminator:
std::size_t text_length(const char* text) {
const char* current = text;
while (*current != '\0') {
++current;
}
return static_cast<std::size_t>(current - text);
}
This function requires a non-null pointer to a valid null-terminated array. Without those preconditions it can read outside valid storage. A copy or concatenation function additionally needs destination capacity; the source terminator alone provides no information about free destination space.
These exercises teach pointer movement and terminators, but application code should use string and bounded views. They track length, manage storage and integrate with standard algorithms.
21. Ownership in function interfaces
Parameter and return types should reveal lifetime intent:
- T by value transfers or copies a value;
- T reference borrows a required object;
- const T reference borrows for reading;
- T pointer commonly borrows optional access;
- unique pointer transfers exclusive ownership;
- shared pointer shares ownership;
- weak pointer observes shared state without owning it.
A function accepting unique ownership normally takes a unique pointer by value. The caller must move its owner, making transfer visible. A function that merely uses the object should accept a reference or raw observer rather than shared pointer, so it does not claim an ownership need it does not have.
Returning a smart pointer is efficient and communicates ownership. Returning a raw pointer into an owned object is acceptable only when the owner's lifetime and invalidation rules are clear.
22. Exception safety
Manual code can leak when later work throws:
Widget* widget = new Widget;
perform_operation();
delete widget;
If perform_operation throws, delete is skipped. A local unique pointer destroys its object during stack unwinding. The same principle applies to a vector, string, file stream and lock guard.
Acquire each resource directly into an RAII owner. Avoid performing several independent raw allocations in one expression or between acquisition and ownership establishment. When every resource is already owned, exceptions become ordinary control transfer rather than cleanup hazards.
23. Practical considerations
- Address-of obtains an address; dereference accesses its object.
- nullptr is the type-safe modern null literal.
- A one-past pointer is an endpoint and cannot be dereferenced.
- Pointer arithmetic is limited to one array and its endpoint.
- New constructs objects, whereas malloc only obtains raw bytes.
- Match new with delete and array new with array delete.
- A dangling pointer can remain non-null.
- Nulling one alias does not repair another dangling alias.
- unique_ptr expresses exclusive ownership.
- weak_ptr breaks shared ownership cycles.
- A raw pointer does not state ownership.
- this identifies the current object in a non-static member function.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.