Previous Lecture lect13 Next Lecture

lect13, Thu 05/16

Dynamic Memory; Nodes and Linked Lists

Dynamic memory / the heap / freestore

It’s important to know the difference between the stack and the heap

The stack is where all local variables are stored - variables on the stack are destroyed automatically when they go out of scope

The heap is where all dynamic variables are stored - they are created with new and never go out of scope, but must be destroyed with the delete keyword (i.e., the programmer manages heap memory space manually).

“new” operator: The result of “new” is a pointer to the object that was allocated on the heap.

Example of memory leaks

void g(int x) {
	int* p = new int(x);	// memory leak!
	delete p;		// remove this and see application size grow!
	return;
}

int main() {
for (int i = 0; i < 100000000; i++)
		g(i);
}

Dangling Pointers

Dangling Pointers Example

int g(int x) {
	double* p = new double(x);
	delete p;	// delete value from the heap
	return *p; 	// p is a dangling pointer since heap contents were removed!
			// Results in undefined behavior. Shouldn’t do this!
}

Example code:

int* p = new int;  //creates an integer on the heap and saves its address in p, created on the stack
delete p; //deallocates memory for that integer object. the value inside p is unchanged, but there is no object on the heap at that address

int size = 7; //size is an int created on the stack
int* a = new arr[size]; //creates an array on the heap of size 7, and saves the address of the first element in a.
delete [] a; //deallocates memory for the entire array

Re-writing above to introduce problems: depending on the compiler, the code might or might not produce an error after you try to use a dangling pointer (check what Python Tutor shows you :-)))

#include <iostream>
using namespace std;

int main()
{
    int* p = new int;  //creates an integer on the heap and saves its address in p, created on the stack
    *p = 42;
    cout << &p << endl;
    cout << *p << endl;
    delete p; //deallocates memory for that integer object. the value inside p is unchanged, but there is no object on the heap at that address
    cout << &p << endl; // address of a dangling pointer
    cout << *p << endl;
    *p = 16;
    cout << &p << endl;
    cout << *p << endl;

    int size = 7; //size is an int created on the stack
    int* a = new int[size]; //creates an array on the heap of size 7, and saves the address of the first element in a.
    for (int i = 0; i < size; i++)
    {
        cout << a[0] << endl;

    }
    delete [] a; //deallocates memory for the entire array

    return 0;
}

Lecture material

Intro to Linked lists

What are linked lists?

A linked list is a simple data structure that has nodes on the heap, and each node has a value and a pointer to the next node in the list.

A linked list itself has pointers to the first and last nodes (of the same type).

Why use Linked lists?

Arrays are convenient because all elements are right next to each other in memory and can be quickly accessed.

But you cannot add new elements or quickly delete any - the size of an array is fixed, even of a dynamic one.

A linked list is not a dynamic array! A dynamic array is just an array on the heap - they are different from regular arrays because their size can be defined at runtime, and they differ from linked lists because once the size is defined, the array stays at that size (until you potentially delete and resize it using new []).

Arrays aren’t good for everything!

Structure of linked lists

Every Linked List has a head pointer to the first node of that list and a tail pointer to the last node. In an empty linked list, head and tail pointers are both NULL.

Traversing linked lists

It turns out you can traverse through linked lists pretty easily!

Toy example to illustrate linked lists

#include <iostream>
using namespace std;

struct Node {
    int data;
    Node *next;
};

struct LinkedList {
    Node *head;
    Node *tail;
};

int main()
{
    // let's make a linked list with 1, 2, and 3 in it!
    Node *one = new Node;
    one->data = 1; // equivalent to (*one).data = 1;

    Node *two = new Node;
    two->data = 2; 

    Node *three = new Node;
    three->data = 3;

    one->next = two;
    two->next = three;
    three->next = NULL;

    LinkedList *list = new LinkedList;
    list->head = one;
    list->tail = three;

    return 0;
}

Example of “walking down” a linked list

[10]->[20]->[30]->null
cout << list->head->data << endl; // 10
cout << list->head->next->data << endl; // 20
cout << list->head->next->next->data << endl; // 30
cout << list->head->next->next->next->data << endl; // seg fault!

Linked List Implementation

// LinkedList.h
#ifndef LINKEDLIST_H
#define LINKEDLIST_H

struct Node {
	int data;
	Node* next;
};

struct LinkedList {
	Node* head;
	Node* tail;
};

void printList(LinkedList* list);
void insertToFront(LinkedList* list, int value);
bool exists(LinkedList* list, int value);
int length(LinkedList* list);
void deleteIndex(LinkedList* list, int index);

#endif
--------------
// LinkedList.cpp
#include <iostream>
#include "linkedList.h"
using namespace std;

void printList(LinkedList* list) {
	for (Node* i = list->head; i != NULL; i = i->next) {
		cout << "[" << i->data << "]->";
	}
	cout << "null" << endl;
}

void insertToFront(LinkedList* list, int value) {
	// STUB	
	
	/*
* Order of pointer reassignment matters since we always want to make sure we don't "lose" the address of something we need on the heap.

1. Create new node to insert
2. Assign new node's next pointer to the current list's head.
3. Reassign the list's head to the new node.	
	*/
	return;
}

bool exists(LinkedList* list, int value) {
	for (Node* i = list->head; i != NULL; i = i->next) {
		if (i->data == value)
			return true;
	}
	return false;
}

int length(LinkedList* list) {
	int counter = 0;
	// let's use a while loop instead of a for...
	Node* temp = list->head;
	while (temp != 0) {
		counter++;
		temp = temp->next;
	}
	return counter;
}

void deleteIndex(LinkedList* list, int index) {
	// STUB	
	
	/*
Case 1: Remove first element

1. Reassign list head to list head's next pointer.
2. delete first element.

Case 2: Remove middle element

1. Reassign previous node's next pointer to current node's next pointer.
2. delete current node.

Case 3: Remove last element

1. Set previous node's next pointer to NULL.
2. Reassign list's tail pointer to previous node.
3. delete current node.
	*/
	return;
}
----------
// main.cpp
#include <iostream>
#include <string>
#include <fstream>
#include "linkedList.h"

using namespace std;

int main() {
	LinkedList* list = new LinkedList;
	list->head = 0;
	list->tail = 0;

	cout << length(list) << endl;

	insertToFront(list, 10);
	cout << length(list) << endl;
	insertToFront(list, 20);
	insertToFront(list, 30);
	printList(list);			// 30->20->10

	cout << exists(list, 15) << endl; 	// 0
	cout << exists(list, 30) << endl; 	// 1
	cout << exists(list, -1) << endl; 	// 0

	cout << length(list) << endl;		// 3

	deleteIndex(list, -1); 			// err
	deleteIndex(list, 5);			// err
	deleteIndex(list, 3);			// err
	deleteIndex(list, 2);			// OK!
	printList(list);			// 30->20->null

	return 0;
}
----------
$ g++ -o main main.cpp LinkedList.cpp

We will continue talking about Linked Lists in the next lectures.

Midterm 2 Review

https://ucsb-cs16.github.io/s19-ykk/exam/e02/