You are implementing a sensor packet class that owns dynamically allocated memory.
The class manages a fixed-size buffer of exactly 8 bytes stored on the heap.
Your goal is to correctly implement resource ownership using:
This problem is designed to verify correct handling of dynamic memory in Embedded C++, where improper copy behavior can cause double-free, memory corruption, or silent data errors.
Class Design:
Private:
uint8_t* data;Public operations:
uint8_t arr[8]SensorData(const SensorData& other)~SensorData()void setIndex3()data[3] = 99void print() constProgram Flow:
a using those valuesa into object b using copy initializationb by calling setIndex3()a, then print bCorrect Behavior:
a must retain the original datab must show the modified value at index 3
Example Input:
1 2 3 4 5 6 7 8 Example Output:
1 2 3 4 5 6 7 8
1 2 3 99 5 6 7 8
Constraints:
new uint8_t[8]delete[]data pointer
In C++, when you assign one object to another (a = b) or pass by value, the compiler performs a Shallow Copy by default. It copies the bits of the object exactly.
int, float, GPIO_Config).b holds a pointer to a buffer, a gets a copy of the pointer, not the buffer. Both objects now point to the same memory. When one dies, it frees the memory, leaving the other with a Dangling Pointer.To fix this, we implement Deep Copy logic using the Copy Constructor and Copy Assignment Operator.
1. Copy Constructor
Used when creating a new object from an existing one (Buffer b2 = b1;).
class Buffer {
int* ptr;
int size;
public:
// 1. Normal Constructor
Buffer(int s) : size(s) { ptr = new int[size]; }
// 2. Copy Constructor (Deep Copy)
Buffer(const Buffer& other) : size(other.size) {
ptr = new int[size]; // Allocate NEW memory
memcpy(ptr, other.ptr, size * sizeof(int)); // Copy data
}
~Buffer() { delete[] ptr; }
};2. Copy Assignment Operator
Used when updating an already existing object (b2 = b1;). This is more complex because b2 already has memory that must be cleaned up first.
// 3. Copy Assignment Operator
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this; // Handle self-assignment (b1 = b1)
delete[] ptr; // Free old memory
size = other.size; // Copy size
ptr = new int[size]; // Allocate NEW memory
memcpy(ptr, other.ptr, size * sizeof(int)); // Copy data
return *this; // Return reference for chaining (a = b = c)
}| Feature | Shallow Copy (Default) | Deep Copy (Custom) |
|---|---|---|
| Pointers | Copies the address (pointer value). | Copies the data at the address. |
| Memory | Shared between objects (Risky). | Independent memory per object. |
| Destruction | Double-Free crash (Both free same ptr). | Safe (Each frees its own). |
| Speed | Fast (copying 4 bytes). | Slow (copying N bytes). |
1. The "Rule of Three"
If your class needs a custom Destructor (to free memory or close a handle), you must also define a Copy Constructor and Copy Assignment Operator.
In firmware, this often applies to circular buffers, packet managers, or flash file system wrappers.
2. Disabling Copy (Hardware Ownership)
For hardware drivers (e.g., UART, SPI), copying makes no sense. You cannot "clone" a physical peripheral.
The standard practice is to delete copy semantics to enforce Unique Ownership.
class UART {
public:
UART(const UART&) = delete; // ❌ No Copying
UART& operator=(const UART&) = delete; // ❌ No Assignment
};| Pitfall | Details |
|---|---|
| ❌ Double Free | The #1 bug with default copy. Object A is destroyed (frees ptr). Object B is destroyed (frees same ptr) → System Hard Fault. |
| ❌ Self-Assignment | If you write obj = obj;, a naive assignment operator might delete ptr before copying from it, corrupting data. Always check if (this == &other). |
| ❌ Object Slicing | If you assign a Derived object to a Base object variable (Base b = Derived d;), the "Derived" parts are sliced off (lost). Always pass polymorphic objects by pointer or reference. |
| ✅ Pass by Reference | To avoid the performance cost of Deep Copies, always pass objects to functions using const Reference (void func(const Buffer& b)), not by value. |