OOP Search

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.

Objectives
  • 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)

TimeActivity
0-5Recap through questions: what is &a, what is *p?
5-15Predict the output, find the leaks in the starting program
15-30Demo: new[] / delete[], stack and heap in the debugger
30-60Individual work: the grades application
60-75Requirement change: an independent copy
75-85Individual checks: draw the memory, answer the questions
85-90Three 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.

L02/L02.cpp
#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.

HintThere are four new int[10] expressions and zero delete[].
HintdynamicArray2 = 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.

HintA static array name can’t be made to point elsewhere. What kind of pointer is dynamicArray2?
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.

Hintvalue 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 it

Mandatory questions

  1. It is an address. Why is it unsafe?
  2. When does value stop existing?
  3. What is a dangling pointer?
  4. Who is responsible for deleting dynamically allocated memory?
  5. 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 = 4
HintaddGrade changes both the pointer and the count of the caller, so both are taken by reference: int*& grades, int& n.
HintRelease the old block only after you have copied it, and before you overwrite grades.
HintIn deleteGrades, 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)

Open in Godbolt

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?

Code in repository: L02

Romanian version