Unit 5: Dynamic Memory Management and Polymorphism - Subjective Questions
CAP455 — Object Oriented Programming Using C++ • Practice Questions with Detailed Answers
20 questions
Define dynamic memory allocation. Why is it important in C++ programming?
Dynamic memory allocation is the process of allocating memory during program execution rather than at compile time. Dynamically allocated memory is obtained from the free store (heap) using the new operator and released using the delete operator.
Importance:
- The required memory size may not be known at compile time.
- It allows programs to create variable-sized arrays and objects at runtime.
- It supports dynamic data structures such as linked lists, trees, graphs, stacks, and queues.
- It enables objects to exist beyond the scope in which their pointers were declared.
- It helps use memory efficiently by allocating it only when required.
- It is essential for implementing runtime polymorphism when derived objects are accessed through base-class pointers.
However, dynamically allocated memory must be managed carefully to avoid memory leaks, dangling pointers, and undefined behavior.
Explain how the new and delete operators are used for dynamically allocating and deallocating a single object in C++.
The new operator allocates memory from the free store and returns the address of the allocated memory. For an object, it also invokes the object's constructor.
Example:
int* p = new int(25);This statement:
- Allocates memory for one integer.
- Initializes the integer with
25. - Stores its address in
p.
The value can be accessed using dereferencing:
cout << *p;The allocated memory is released using delete:
delete p;
p = nullptr;For a class object:
Student* s = new Student();
delete s;Here, new Student() invokes the constructor, while delete s invokes the destructor and then releases the memory. Assigning nullptr after deletion helps prevent accidental use of a dangling pointer.
Describe dynamic allocation and deallocation of arrays using new[] and delete[]. What happens if the wrong form of delete is used?
A dynamic array is allocated when its size is determined at runtime.
Allocation example:
int n;
cin >> n;
int* values = new int[n];The elements can be accessed using normal array notation:
for (int i = 0; i < n; ++i) {
cin >> values[i];
}The array must be released using delete[]:
delete[] values;
values = nullptr;For an array of objects, constructors are called for every element during allocation, and destructors are called for every element during delete[].
Student* group = new Student[n];
delete[] group;Using delete instead of delete[] for an array, or using delete[] for a single object, results in undefined behavior. It may cause incomplete destruction, heap corruption, or program failure.
What is a memory leak? Explain its causes, consequences, and methods of prevention in C++.
A memory leak occurs when dynamically allocated memory is no longer needed but is not released, and the program loses the ability to access that memory.
Example:
int* p = new int(10);
p = new int(20);The address of the first allocated integer is overwritten, so that memory cannot be deleted.
Common causes:
- Forgetting to call
deleteordelete[]. - Returning from a function before deallocation.
- Overwriting the only pointer to allocated memory.
- Failing to release memory when an exception occurs.
- Incorrect ownership rules between objects.
Consequences:
- Increasing memory consumption.
- Reduced system performance.
- Allocation failures in long-running programs.
- Program or system instability.
Prevention:
- Match every
newwithdeleteand everynew[]withdelete[]. - Set pointers to
nullptrafter deletion. - Follow RAII so resources are owned by objects.
- Prefer standard containers such as
std::vector. - Prefer smart pointers such as
std::unique_ptrandstd::shared_ptr. - Use memory-analysis tools to detect leaks.
Explain dynamic memory allocation failure. Compare the exception-based and std::nothrow methods of handling allocation failure.
Dynamic allocation can fail when the requested block is too large or when sufficient free memory is unavailable.
By default, a failed new expression throws an exception of type std::bad_alloc.
#include <new>
try {
int* data = new int[1000000];
delete[] data;
} catch (const std::bad_alloc& e) {
cout << "Allocation failed: " << e.what();
}Alternatively, std::nothrow can be used. In this case, failed allocation returns nullptr instead of throwing an exception.
#include <new>
int* data = new (std::nothrow) int[1000000];
if (data == nullptr) {
cout << "Allocation failed";
} else {
delete[] data;
}Comparison:
- Ordinary
newreports failure throughstd::bad_alloc. new (std::nothrow)reports failure throughnullptr.- Exception handling is usually suitable for normal C++ application design.
std::nothrowis useful where exception handling is disabled or explicit pointer checking is required.
In both approaches, the program should respond safely rather than dereferencing an invalid pointer.
Why should a base class destructor be declared virtual? Explain with an appropriate example.
A base-class destructor should be virtual when objects of derived classes may be deleted through base-class pointers. A virtual destructor ensures that the derived-class destructor executes before the base-class destructor.
class Base {
public:
virtual ~Base() {
cout << "Base destroyed\n";
}
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() override {
delete[] data;
cout << "Derived destroyed\n";
}
};
Base* p = new Derived();
delete p;Because Base::~Base() is virtual, delete p calls:
Derived::~Derived()Base::~Base()
If the base destructor were non-virtual, deleting a derived object through Base* would produce undefined behavior. The derived destructor might not execute, causing resources owned by the derived object to leak.
A class intended for polymorphic use should therefore normally have a virtual destructor, even if its destructor body is empty.
Distinguish between compile-time polymorphism and runtime polymorphism in C++.
Polymorphism means providing one interface with multiple forms of behavior.
| Basis | Compile-time polymorphism | Runtime polymorphism |
|---|---|---|
| Binding | Early or static binding | Late or dynamic binding |
| Decision time | During compilation | During program execution |
| Main mechanisms | Function overloading, operator overloading, templates | Function overriding using virtual functions |
| Inheritance | Not always required | Usually requires inheritance |
| Execution overhead | Generally lower | Usually has a small dispatch overhead |
| Flexibility | Behavior depends on static types and signatures | Behavior depends on the actual object type |
Compile-time example:
void display(int);
void display(double);The compiler selects the appropriate overloaded function.
Runtime example:
Base* p = new Derived();
p->show();If show() is virtual, the version belonging to Derived is called based on the actual object type.
Thus, compile-time polymorphism emphasizes efficiency, while runtime polymorphism provides greater flexibility and extensibility.
Explain function overloading and operator overloading as forms of compile-time polymorphism.
In compile-time polymorphism, the compiler determines which implementation to use before program execution.
Function overloading
Multiple functions may have the same name if their parameter lists differ in number, type, or order.
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}The return type alone cannot distinguish overloaded functions.
Operator overloading
An existing C++ operator can be given a class-specific meaning.
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
};For c1 + c2, the compiler selects Complex::operator+ based on operand types.
Key points:
- Both mechanisms use early binding.
- They improve readability when used meaningfully.
- Operator precedence, associativity, and number of operands cannot be changed.
- New operator symbols cannot be invented.
What is a virtual function? Describe how virtual functions implement runtime polymorphism.
A virtual function is a member function declared with the keyword virtual in a base class and designed to be overridden by derived classes.
class Shape {
public:
virtual void draw() const {
cout << "Drawing a shape\n";
}
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing a circle\n";
}
};
Shape* p = new Circle();
p->draw();
delete p;Although the static type of p is Shape*, its dynamic object type is Circle. Therefore, Circle::draw() is selected at runtime.
Implementation concept:
- A polymorphic class is commonly represented internally using a virtual-function table or vtable.
- Each polymorphic object commonly stores a hidden pointer called a vptr.
- A virtual call uses this information to locate the function associated with the actual object type.
The exact vtable mechanism is implementation-dependent, but the runtime behavior is guaranteed by the C++ language.
Define a pure virtual function. How does it differ from an ordinary virtual function?
A pure virtual function is a virtual function declared by placing = 0 at the end of its declaration.
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};It specifies an interface that derived classes are expected to implement.
Difference from an ordinary virtual function:
- An ordinary virtual function normally provides a base-class implementation.
- A pure virtual function expresses that the base class does not provide a complete usable implementation for that operation.
- A class containing at least one pure virtual function is abstract.
- An abstract class cannot be instantiated directly.
- A derived class remains abstract unless it overrides all inherited pure virtual functions.
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};Pure virtual functions are especially useful for defining common interfaces for unrelated concrete implementations.
Compare abstract classes and concrete classes in C++. Give suitable examples.
An abstract class is a class that cannot be instantiated directly. It contains or inherits at least one pure virtual function for which no final override has been provided.
A concrete class provides implementations for all inherited pure virtual functions and can be instantiated.
class Employee {
public:
virtual double calculatePay() const = 0;
virtual ~Employee() = default;
};
class SalariedEmployee : public Employee {
double monthlySalary;
public:
SalariedEmployee(double salary) : monthlySalary(salary) {}
double calculatePay() const override {
return monthlySalary;
}
};Here, Employee is abstract and SalariedEmployee is concrete.
Comparison:
- Abstract classes define general interfaces and may also contain data and implemented functions.
- Concrete classes represent complete objects.
Employee e;is invalid becauseEmployeeis abstract.SalariedEmployee e(50000);is valid.- Abstract classes allow different concrete classes to be processed uniformly through base references or pointers.
An abstract class should generally have a virtual destructor when it is used polymorphically.
Explain early binding and late binding. Under what conditions does each occur in C++?
Binding is the process of associating a function call with the function implementation that will execute.
Early binding
Early binding, also called static binding, occurs at compile time. It is used for:
- Non-virtual member functions.
- Function overloading.
- Operator overloading.
- Calls where the compiler can determine the target statically.
class Base {
public:
void show() { cout << "Base"; }
};A call through Base* selects Base::show() based on the pointer's static type.
Late binding
Late binding, also called dynamic binding, occurs at runtime. It requires:
- A virtual function in the base class.
- An override in a derived class.
- A call normally made through a base-class pointer or reference.
Base& ref = derivedObject;
ref.show();If show() is virtual, the implementation is selected according to the actual object referred to by ref.
Early binding is generally faster, whereas late binding supports runtime extensibility and subtype polymorphism.
What is a dynamic constructor? Explain how a constructor can allocate memory dynamically and how the corresponding class should release it.
A dynamic constructor is a constructor that allocates memory dynamically while initializing an object. The term does not describe a special C++ constructor category; it describes a constructor that uses dynamic allocation.
#include <cstring>
class String {
char* text;
public:
String(const char* source) {
text = new char[std::strlen(source) + 1];
std::strcpy(text, source);
}
~String() {
delete[] text;
}
};The constructor calculates the required memory at runtime and allocates only that amount. The destructor releases the array using delete[].
When a class directly owns dynamic memory, it must also handle copying and assignment correctly. Otherwise, default memberwise copying can make two objects point to the same memory, causing double deletion or unintended sharing.
Such a class should follow the Rule of Three by defining a destructor, copy constructor, and copy-assignment operator. In modern C++, it may follow the Rule of Five or use std::string, std::vector, or smart pointers to simplify safe resource management.
Design a class that uses a dynamic constructor and follows the Rule of Three. Explain why each special member function is required.
A class owning dynamic memory must ensure that every object has independent ownership of its resource.
#include <algorithm>
#include <cstddef>
class Buffer {
std::size_t size;
int* data;
public:
explicit Buffer(std::size_t n)
: size(n), data(new int[n]{}) {}
~Buffer() {
delete[] data;
}
Buffer(const Buffer& other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
int* newData = new int[other.size];
std::copy(other.data, other.data + other.size, newData);
delete[] data;
data = newData;
size = other.size;
}
return *this;
}
};Purpose of each function:
- The constructor dynamically allocates the array.
- The destructor prevents a memory leak.
- The copy constructor performs a deep copy when creating one object from another.
- The copy-assignment operator releases the old resource and creates a deep copy of the source.
- The self-assignment check prevents unnecessary or unsafe work.
- Allocating the replacement before deleting the old array provides better exception safety.
In production code, std::vector<int> or std::unique_ptr<int[]> would usually be preferred.
What is a self-referential class? Explain its role in implementing dynamic data structures.
A self-referential class contains at least one pointer or reference capable of referring to another object of the same class.
class Node {
public:
int data;
Node* next;
Node(int value) : data(value), next(nullptr) {}
};A class cannot directly contain an object of its own type because that would require an infinitely large object. However, it can contain a pointer to its own type because a pointer has a fixed size.
Applications:
- Singly linked lists use a
nextpointer. - Doubly linked lists use
nextandpreviouspointers. - Trees use child pointers.
- Graphs use collections of pointers to neighboring nodes.
Nodes are commonly created dynamically:
Node* first = new Node(10);
first->next = new Node(20);Each dynamically allocated node must eventually be deleted. Careful ownership design is necessary to prevent memory leaks, dangling links, and double deletion.
Using a self-referential class, describe how nodes are dynamically inserted into and deleted from the beginning of a singly linked list.
A singly linked list can be represented using self-referential nodes.
class Node {
public:
int data;
Node* next;
Node(int value, Node* link = nullptr)
: data(value), next(link) {}
};Insertion at the beginning
void insertFront(Node*& head, int value) {
head = new Node(value, head);
}The new node points to the old first node, and head is updated to point to the new node. This operation has time complexity .
Deletion from the beginning
void deleteFront(Node*& head) {
if (head != nullptr) {
Node* oldHead = head;
head = head->next;
delete oldHead;
}
}The next node becomes the new head, and the old first node is released. This operation also has time complexity .
Releasing the entire list
while (head != nullptr) {
deleteFront(head);
}Before deleting a node, any links needed to access remaining nodes must be saved. Otherwise, the remaining nodes may become unreachable and leak.
Develop a C++ example that demonstrates runtime polymorphism using an abstract base class, derived concrete classes, dynamic allocation, and a virtual destructor.
The following hierarchy calculates the areas of different shapes:
#include <iostream>
#include <vector>
using namespace std;
class Shape {
public:
virtual double area() const = 0;
virtual void display() const {
cout << area() << '\n';
}
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
double length, width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double area() const override {
return length * width;
}
};
int main() {
vector<Shape*> shapes;
shapes.push_back(new Circle(2.0));
shapes.push_back(new Rectangle(3.0, 4.0));
for (const Shape* shape : shapes) {
shape->display();
}
for (Shape* shape : shapes) {
delete shape;
}
}Explanation:
Shapeis abstract becausearea()is pure virtual.CircleandRectangleare concrete classes.- Objects are allocated dynamically and stored through
Shape*pointers. - Calls to
area()are resolved at runtime. - The virtual destructor makes deletion through
Shape*safe. - In modern C++,
std::vector<std::unique_ptr<Shape>>would provide automatic ownership and stronger exception safety.
What is object slicing? How is it related to polymorphism, and how can it be avoided?
Object slicing occurs when a derived-class object is copied into a base-class object by value. The derived-specific portion is discarded, leaving only the base-class subobject.
class Base {
public:
virtual void show() const { cout << "Base"; }
};
class Derived : public Base {
public:
void show() const override { cout << "Derived"; }
};
Derived d;
Base b = d;
b.show();Although show() is virtual, b is an independent Base object after slicing. Therefore, it prints Base.
Slicing can also occur when objects are passed or returned by value:
void process(Base object);Avoidance methods:
- Pass polymorphic objects by reference:
void process(const Base& object). - Use base-class pointers where nullability or dynamic ownership is required.
- Store smart pointers such as
std::unique_ptr<Base>in containers. - Use a virtual
clone()function when polymorphic copying is required.
Runtime polymorphism preserves derived behavior only when the object is accessed through an appropriate base reference or pointer without being sliced.
Compare C++ dynamic allocation using new and delete with C-style allocation using malloc() and free().
| Feature | new and delete |
malloc() and free() |
|---|---|---|
| Language | C++ operators | C library functions |
| Returned type | Correctly typed pointer | void* |
| Object construction | new invokes constructors |
malloc() does not invoke constructors |
| Object destruction | delete invokes destructors |
free() does not invoke destructors |
| Failure behavior | Usually throws std::bad_alloc |
Returns nullptr |
| Size expression | Uses a type | Requires an explicit byte count |
| Customization | Can be overloaded | Cannot be overloaded as C++ operators |
Examples:
Student* s = new Student();
delete s;Student* s = static_cast<Student*>(malloc(sizeof(Student)));
free(s);The second example only reserves raw storage; it does not properly construct or destroy a Student object. Therefore, new and delete are more appropriate than malloc() and free() for class objects.
Allocation and deallocation families must never be mixed. Memory allocated with new must use delete, memory allocated with new[] must use delete[], and memory allocated with malloc() must use free().
Explain how RAII and smart pointers improve dynamic memory management in polymorphic C++ programs.
RAII, or Resource Acquisition Is Initialization, associates a resource with the lifetime of an object. The object's constructor acquires the resource, and its destructor releases it. This makes cleanup automatic when control leaves a scope, including during exception handling.
Smart pointers implement RAII for dynamic objects.
std::unique_ptr<T>represents exclusive ownership.std::shared_ptr<T>represents shared ownership through reference counting.std::weak_ptr<T>observes an object managed byshared_ptrwithout increasing its reference count.
Polymorphic example:
#include <memory>
#include <vector>
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
for (const auto& shape : shapes) {
shape->display();
}No explicit delete is needed. When the vector is destroyed, its smart pointers delete their objects automatically.
The base class must still have a virtual destructor when a derived object is deleted through a base-class smart pointer. Smart pointers reduce leaks and clarify ownership, but shared_ptr cycles must be broken using weak_ptr.
Define dynamic memory allocation. Why is it important in C++ programming?
Dynamic memory allocation is the process of allocating memory during program execution rather than at compile time. Dynamically allocated memory is obtained from the free store (heap) using the new operator and released using the delete operator.
Importance:
- The required memory size may not be known at compile time.
- It allows programs to create variable-sized arrays and objects at runtime.
- It supports dynamic data structures such as linked lists, trees, graphs, stacks, and queues.
- It enables objects to exist beyond the scope in which their pointers were declared.
- It helps use memory efficiently by allocating it only when required.
- It is essential for implementing runtime polymorphism when derived objects are accessed through base-class pointers.
However, dynamically allocated memory must be managed carefully to avoid memory leaks, dangling pointers, and undefined behavior.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →