Week 02 / 14 Object-Oriented Programming
Lab 2 - Pointers, dynamic memory and ownership
We hunt for leaks in a program that allocates and never frees, then build a small grades application on the heap and decide, for every block, who releases it.
- Use the address-of and dereference operators and nullptr
- Pass pointers and references to functions and say what each can change
- Allocate dynamic arrays with new[] and release them with delete[]
- Find memory leaks and dangling pointers in existing code
- Say, for every heap block, who owns it and who deletes it
- Draw the stack and the heap of a small program
Plan (90 min)
| Time | Activity |
|---|---|
| 0-5 | Recap through questions: what is &a, what is *p? |
| 5-15 | Predict the output, find the leaks in the starting program |
| 15-30 | Demo: new[] / delete[], stack and heap in the debugger |
| 30-60 | Individual work: the grades application |
| 60-75 | Requirement change: an independent copy |
| 75-85 | Individual checks: draw the memory, answer the questions |
| 85-90 | Three things you know now |
Starting program
The code is in the repository, in the L02 folder. Read it before you run it. Count the new[] and the delete[] lines.
#include<iostream>
using namespace std;
int sumArray(int arr[], int nrElem)
{
int sum = 0;
for (int i = 0; i < nrElem; i++)
{
sum = sum + arr[i];
}
return sum;
}
int main()
{
int staticArray[10], sumMain = 0; // const int* pointer
//staticArray = new int[10]; NONO because it's a ct pointer
const int* dynamicArray2 = new int[10]; // pointer to constant integer, elements cannot be changed, but you can assign a new address to the pointer
int* const dynamicArray = new int[10]; // constant pointer, can't point to a new address (value of the pointer is constant), but the elements can be changed
const int* const dynamicArray3 = new int[10]; // least modifiable
//dynamicArray2[0]++;
dynamicArray2 = new int[10];
for (int i = 0; i < 10; i++)
{
staticArray[i] = i;
}
for (int i = 0; i < 10; i++)
{
dynamicArray[i] = i * 2;
}
sumMain = sumArray(staticArray, 10);
cout << "The sum of numbers from 1 to 10 is: " << sumMain << endl;
int sum2 = 0;
sum2 = sumArray(dynamicArray, 10);
cout << sum2 << endl;
cout << "dynamicArray: " << dynamicArray << endl; // the address of the 1st elem
cout << "dynamicArray: " << *dynamicArray << endl; // the *s value of the 1st elem
cout << "dynamicArray: " << *(dynamicArray + 1) << endl; // the *s value of the 2nd elem
cout << "dynamicArray: " << 1[dynamicArray] << endl; // commutativity of the sum -> same as dynamicArray[1]
return 0;
} Find the defects
Where did the memory go?
The program runs and prints sensible numbers, yet it is wrong. Find every heap block that is never released. Then read the first cout message again and compare it with what staticArray really holds.
Hint
There are fournew int[10] expressions and zero delete[].Hint
dynamicArray2 = new int[10]; overwrites the only copy of the first block’s address. After that line, nobody can release the first block.Solution
delete[] dynamicArray2; // release the first block before losing it
dynamicArray2 = new int[10];
// ... the rest of main ...
delete[] dynamicArray;
delete[] dynamicArray2;
delete[] dynamicArray3;Each new[] gets exactly one delete[]. The message says “from 1 to 10”, but the array holds 0 to 9: the sum 45 is right, the text is wrong. That is a logical error, not a memory one.
The const lines
Two lines in main are commented out: staticArray = new int[10]; and dynamicArray2[0]++;. Uncomment them one at a time (only the code, the explanation after it must stay a comment). Read the compiler message and explain it using the three kinds of const pointer.
Hint
A static array name can’t be made to point elsewhere. What kind of pointer isdynamicArray2?Solution
staticArray is an array, so it can’t be assigned. dynamicArray2 is a const int*: you can move it, but you can’t change the ints through it, so dynamicArray2[0]++ is an error. dynamicArray is the opposite (int* const), and dynamicArray3 allows neither.
A deliberately broken function
This function compiles (g++ only warns), but it is wrong.
int* createValue() {
int value = 10;
return &value;
}It returns an address. Why is it unsafe? When does value stop existing? Rewrite it so the caller gets a valid result, and say who has to release the memory.
Hint
value is a local variable. Where do local variables live, and what happens to them at return?Solution
value lives on the stack of createValue and is destroyed when the function returns, so the caller gets a dangling pointer. Two correct versions:
int createValue() { return 10; } // return by value, no pointer at all
int* createValue() { return new int(10); } // the caller owns it and must delete itMandatory questions
- It is an address. Why is it unsafe?
- When does
valuestop existing? - What is a dangling pointer?
- Who is responsible for deleting dynamically allocated memory?
- Draw it: which variables are on the stack, which blocks are on the heap, and which arrow goes where?
Predict, then check:
#include <iostream>
using namespace std;
void setTo(int* p, int& r) { *p = 1; r = 2; p = nullptr; }
int main() {
int a = 0, b = 0;
int* q = &a;
setTo(q, b);
cout << a << " " << b << " " << (q == &a);
} #include <iostream>
using namespace std;
int main() {
int* a = new int[3]{1, 2, 3};
int* b = a;
b[1] = 20;
cout << a[1] << " " << (a == b) << endl;
delete[] a;
a = nullptr;
} After the last line, a is safe but b still holds the old address. Who should have released the block, a or b? Is it ok to call delete[] b; as well?
Minimum task
Grades on the heap
Write the four functions below. addGrade must grow the array by one: allocate a bigger block, copy, release the old one, update the pointer and the count. Test it, then draw the stack and the heap right after addGrade returns.
STACK HEAP
grades -----------> [7][9][10][6]
n = 4Hint
addGrade changes both the pointer and the count of the caller, so both are taken by reference: int*& grades, int& n.Hint
Release the old block only after you have copied it, and before you overwritegrades.Hint
IndeleteGrades, set the pointer to nullptr and the count to 0, so the caller can’t use the old block by mistake.#include <iostream>
using namespace std;
// The caller owns the returned block.
int* createGrades(int n) {
// TODO: allocate n ints, all 0 (until you do, main stops at grades[0])
return nullptr;
}
void addGrade(int*& grades, int& n, int grade) {
// TODO: grow the array by one and put grade at the end
}
double average(const int* grades, int n) {
// TODO
return 0;
}
void deleteGrades(int*& grades, int& n) {
// TODO: release, then reset grades and n
}
int main() {
int n = 3;
int* grades = createGrades(n);
grades[0] = 7; grades[1] = 9; grades[2] = 10;
addGrade(grades, n, 6);
cout << n << " " << average(grades, n) << endl; // expected: 4 8
deleteGrades(grades, n);
}Input (stdin)
Solution
int* createGrades(int n) { return new int[n](); }
void addGrade(int*& grades, int& n, int grade) {
int* bigger = new int[n + 1];
for (int i = 0; i < n; i++) bigger[i] = grades[i];
bigger[n] = grade;
delete[] grades;
grades = bigger;
n++;
}
double average(const int* grades, int n) {
if (n == 0) return 0;
double s = 0;
for (int i = 0; i < n; i++) s += grades[i];
return s / n;
}
void deleteGrades(int*& grades, int& n) {
delete[] grades;
grades = nullptr;
n = 0;
}Requirement change
New requirement: write int* copyGrades(const int* grades, int n), which returns an independent copy. Change copy[0] and show that grades[0] did not change. How many blocks are on the heap now, and who releases each one? What would go wrong if copyGrades just returned grades?