This commit is contained in:
Jidong Xiao
2023-09-21 23:23:36 -04:00
22 changed files with 1024 additions and 18 deletions

View File

@@ -0,0 +1,120 @@
# Lab 4 — Memory Diagrams and Memory Debugging
## Checkpoint 1
*estimate: 30-45 minutes*
- Form a group of 2 members. If the number of students in the room is not divisible by 2, the graduate TA will approve a single team with 3 members.
- Introduce yourself to your teammate. Share to your teammate something about yourself (e.g. hobbies, sports, favorite music, etc). Learn something new about your teammate (even if you already know the teammate).
- Our TA/mentor will pick a code snippet for each student, from the following 5 code snippets. Following the conventions used in Data Structures lecture for memory diagramming, draw a picture of the stack and the heap that result from executing the code snippet. Use a ? to represent uninitialized values.
[Code Snippet 1](fruits.cpp)
[Code Snippet 2](grains.cpp)
[Code Snippet 3](desserts.cpp)
[Code Snippet 4](veggies.cpp)
[Code Snippet 5](protein.cpp)
Show your diagram to your teammate, and ask the teammate to reverse engineer code for your diagram; at the same time, you reverse engineer code for your teammate's diagram.
When everyone has finished writing code for the drawing, discuss your answers as a group. Are differences in the original and reverse engineered code valid alternate answers? Or was something misinterpreted? Critique each others diagrams. What could be improved or drawn more clearly? What statements need to be added to each example to clean up / prevent memory leaks?
**To complete this checkpoint**, present your work to a TA/mentor.
## Checkpoint 2 & 3:
Checkpoints 2 and 3 focus on using a memory debugger. It is highly recommended that you thoroughly read
the instructions for Checkpoint 2 and Checkpoint 3 before starting.
Memory debuggers will be a useful tool in many of the assignments this semester, and in C++ development
if you work with the language outside of this course. While a traditional debugger lets you step through your
code and examine variables, a memory debugger instead reports memory-related errors during execution and
can help find memory leaks after your program has terminated. The next time you see a “segmentation
fault”, or it works on your machine but not on Submitty, try running a memory debugger!
Please download the following 4 files needed for this lab:
[buggy_lab4.cpp](./buggy_lab4.cpp)
[first.txt](./first.txt)
[middle.txt](./middle.txt)
[last.txt](./last.txt)
## Checkpoint 2
*estimate: 20-40 minutes*
For Checkpoint 2 of this lab, we will heavily rely on dynamic memory to find the average and smallest number for a set of data from an input file. You will use a memory debugging tool such as DrMemory or Valgrind to fix memory errors and leaks in buggy_lab4.cpp. Make sure to download the provided .txt files as well.
First take a look at the code inside the identifyMeanAndMin() function. You will notice that the syntax used
throughout the program may be a little different than what you are used to. Try and familiarize yourself with
this syntax before you start working on the assignment. What does this line of code do?
```cpp
*(intArray + *numElements) = readInt;
```
Whats going on inside the *for* loop? If you are stuck on this, ask a mentor or TA for help as
soon as possible and refer back to your lecture notes on pointers and arrays.
Once you have done this, compile the program using:
```console
g++ buggy_lab4.cpp -o buggy_lab4.out -g -Wall
```
Try running the program normally using:
```console
./buggy_lab4.out
```
You will notice that a segmentation fault occurs. Now run this program using either Valgrind or DrMemory. If running using Valgrind, remember to use --leak-check=yes or --leak-check=full.
Your memory debugger should give you more context as to why a segmentation fault is occurring, especially if you have compiled with *-g*. To complete this checkpoint, add or modify code in the areas marked Parts 1, 2, and 3 to resolve all memory errors and leaks. As you are working on this, be sure to also think about the questions asked in Checkpoint 3.
It is highly recommended that you tackle one part at a time. For example, after adding a few lines of code to part 1, you will now receive different memory errors when you recompile and run the program using your memory debugger. Similarly, fixing all memory errors in part 2 will generate different memory errors that should be resolved in part 3.
In part 2 of buggy_lab4.cpp, the goal is to print out the contents of intArray in reverse order, while also calculating the sum of all elements in the array and keeping track of the smallest number encountered. Solutions that attempt to print the contents of the array in a different manner or end up with the wrong
value for the smallest number found or sum won't be accepted.
Note that:
- You are only allowed to modify or add code when asked to. This would be between the comments that
indicate parts 1, 2, and 3 inside buggy_lab4.cpp. Do not modify other parts of the code or create any
helper functions.
- You are not allowed to declare new variables; the ones provided are more than enough. Hint: how do
we create memory on the heap?
- You are not allowed to use additional data structures (like arrays or vectors).
**You will receive no credit if you do not follow the above restrictions.**
**To receive credit for this checkpoint**: Fix buggy_lab4.cpp so that it successfully prints out the average
and smallest number for a given set of data and is free of all memory errors and leaks on your local machine.
Submit buggy_lab4.cpp on Submitty and verify that there are no memory errors there as well and show a
mentor or TA both results. Also explain to a mentor TA what you added or modified in the program to
resolve all memory errors.
## Checkpoint 3
*estimate: 15-25 minutes*
As you work through Checkpoint 2, try and pay close attention the analysis given by DrMemory or Valgrind
and think about the following:
1. How does the output from your memory debugger differ when you compile your program with the -g
flag compared to when you leave it out?
2. How would you rewrite the for loop in part 2 to use the bracket [] operator instead of pointer syntax?
3. For DrMemory users, you wouldve encountered all of these errors in parts 1, 2, or 3 of Checkpoint 2:
```console
UNITIALIZED READ
UNADDRESSABLE ACCESS
INVALID HEAP ARGUMENT
LEAK
```
4. What do you think each of these errors mean?
5. For Valgrind users, the errors you will have seen are:
```console
Use of uninitialised value
Invalid write
Invalid read
Conditional jump or move depends on uninitialised value(s)
Mismatched free() / delete / delete []
? bytes in ? blocks are definitely lost in loss record ? of ?
```
**To receive credit for this checkpoint**, discuss your answers to these questions with a TA or mentor.

View File

@@ -0,0 +1,116 @@
#include <iostream>
#include <fstream>
#define MAX_ARRAY_SIZE 50
/** IMPORTANT: SOLUTIONS THAT DO NOT FOLLOW BELOW INSTRUCTIONS WILL RECEIVE NO CREDIT
* Do not add or modify code in this file UNLESS ASKED TO!
* You are NOT allowed to declare new variables; use the ones already declared.
* You are NOT allowed to create new helper functions or modify checkCorrectSmallest()
**/
/** DO NOT MODIFY THIS FUNCTION! **/
int checkCorrectSmallest(int* smallestNum, const std::string& filename) {
if (filename == "first.txt" || filename == "middle.txt") {
if (*smallestNum != 1) {return -1;}
}
else if (filename == "last.txt") {
if (*smallestNum != 22) {return -1;}
}
return 1;
}
/** A function that will identify the mean and smallest number
* in a set of data provided that there are at most 50 numbers
* in the file.
*
* @param filename: The name of a file that contains a list of numbers.
*
* Task: Add or modify code in the appropriate sections to fix memory and logic errors
* without using data structures (such as an array or vector) and without using any
* additional memory on the stack.
**/
void identifyMeanAndMin(const std::string& filename) {
int* numElements;
int* sum;
int* smallestNum;
float* avg;
/** PART 1: ADD CODE BELOW **/
/** PART 1: ADD CODE ABOVE **/
*numElements = 0;
*sum = 0;
int readInt;
int* intArray = new int[MAX_ARRAY_SIZE];
std::ifstream input(filename);
while (input >> readInt) {
*(intArray + *numElements) = readInt;
*numElements += 1;
}
std::cout << "Printing the contents of the array in reverse order: ";
/** PART 2: MODIFY CODE BELOW **/
for (int i = MAX_ARRAY_SIZE; i >= -1; i--) {
// If we're at the beginning of the for loop, initalize *smallestNum
// Else, compare *smallestNum to current element in the for loop
if (i == MAX_ARRAY_SIZE) {
*smallestNum = *(intArray + i);
}
else {
if (*smallestNum > *(intArray + i)) {
*smallestNum = *(intArray + i);
}
}
/** PART 2: MODIFY CODE ABOVE **/
*sum += *(intArray + i);
std::cout << *(intArray + i) << " ";
}
std::cout << std::endl;
if (checkCorrectSmallest(smallestNum, filename) == -1) {
std::cout << "ERROR: incorrect value for smallest number" << std::endl;
return;
}
*avg = *sum / float(*numElements);
std::cout << "The average for the set of numbers in " << filename << " is "
<< *avg << " and the smallest number is " << *smallestNum
<< std::endl;
/** PART 3: ADD AND/OR MODIFY CODE BELOW **/
delete intArray;
/** PART 3: ADD AND/OR MODIFY CODE ABOVE **/
}
int main() {
identifyMeanAndMin("first.txt");
std::cout << std::endl;
identifyMeanAndMin("middle.txt");
std::cout << std::endl;
identifyMeanAndMin("last.txt");
return 0;
}

View File

@@ -0,0 +1,10 @@
bool** cake;
bool pie;
bool fudge;
pie = true;
cake = new bool*[5];
cake[1] = &pie;
bool* donut = new bool;
*donut = false;
cake[2] = donut;
cake[4] = &fudge;

View File

@@ -0,0 +1,4 @@
1
2
3
4

View File

@@ -0,0 +1,10 @@
int pear = 3;
int* apple;
int banana[pear];
int* orange;
apple = new int[pear];
orange = &banana[1];
apple[0] = 6;
apple[1] = 7;
apple[2] = 8;
*orange = 5;

View File

@@ -0,0 +1,10 @@
float* oat[3];
oat[1] = new float;
*oat[1] = 3.14;
oat[2] = new float;
*oat[2] = 6.02;
float rice;
float* wheat;
wheat = oat[2];
float** barley = new float*;
*barley = oat[1];

View File

@@ -0,0 +1,49 @@
22
247
279
240
253
342
87
181
391
57
23
302
168
367
236
240
187
368
216
185
31
255
122
140
69
46
287
69
268
58
134
330
172
291
175
63
184
329
30
337
229
274
130
95
255
331
24
325
228

View File

@@ -0,0 +1,10 @@
89
34
13
5
2
1
3
8
21
55

View File

@@ -0,0 +1,11 @@
int tofu = 3;
int chicken = 2;
double** fish = new double*[tofu];
for (int beef = 0; beef < tofu; beef++) {
fish[beef] = new double[chicken];
}
fish[0][0] = 1.41421;
fish[0][1] = 1.61803;
fish[1][0] = 2.71828;
fish[1][1] = 3.14159;
fish[2][0] = 6.02214;

View File

@@ -0,0 +1,10 @@
char*** carrot;
char** broccoli;
char* tomato;
char radish = 'q';
tomato = new char;
*tomato = 'z';
broccoli = new char*;
*broccoli = tomato;
carrot = new char**;
*carrot = broccoli;

View File

@@ -21,12 +21,16 @@ static int counter;
## 6.2 Dynamic Memory
Dynamic memory is:
- created using the new operator,
- created using the **new** operator,
- accessed through pointers, and
- removed through the delete operator.
- removed through the **delete** operator.
Heres a simple example involving dynamic allocation of integers:
```cpp
<table>
<tr>
<td>
<pre>
int * p = new int;
*p = 17;
cout << *p << endl;
@@ -41,24 +45,381 @@ p = temp;
cout << *p << " " << *q << endl;
delete p;
delete q;
</pre>
</td>
<td><img src="heap.png" alt="heap"</td>
</tr>
</table>
<!--![alt text](heap.png "heap")-->
- The expression *new int* asks the system for a new chunk of memory that is large enough to hold an integer
and returns the address of that memory. Therefore, the statement
```cpp
int * p = new int;
```
- The expression new int asks the system for a new chunk of memory that is large enough to hold an integer
and returns the address of that memory. Therefore, the statement int * p = new int; allocates memory
from the heap and stores its address in the pointer variable p.
- The statement delete p; takes the integer memory pointed by p and returns it to the system for re-use.
- This memory is allocated from and returned to a special area of memory called the heap. By contrast, local
variables and function parameters are placed on the stack as discussed last lecture.
- In between the new and delete statements, the memory is treated just like memory for an ordinary variable,
allocates memory from the heap and stores its address in the pointer variable *p*.
- The statement
```cpp
delete p;
```
takes the integer memory pointed by *p* and returns it to the system for re-use.
- This memory is allocated from and returned to a special area of memory called the **heap**. By contrast, local
variables and function parameters are placed on the stack.
- In between the *new* and *delete* statements, the memory is treated just like memory for an ordinary variable,
except the only way to access it is through pointers. Hence, the manipulation of pointer variables and values is
similar to the examples covered in Lecture 5 except that there is no explicitly named variable for that memory
similar to the examples covered in the pointers lecture except that there is no explicitly named variable for that memory
other than the pointer variable.
- Dynamic allocation of primitives like ints and doubles is not very interesting or significant. Whats more
important is dynamic allocation of arrays and objects.
- Dynamic allocation of primitives like ints and doubles is not very interesting or significant. Whats more important is dynamic allocation of arrays and class objects.
## 6.3 Exercises
- Play this [animation](https://jidongxiao.github.io/CSCI1200-DataStructures/animations/dynamic_memory/example1/index.html) to see what exactly the above code snippet does.
- [Leetcode problem 56: Merge Intervals](https://leetcode.com/problems/merge-intervals/). Solution: [p56_mergeintervals.cpp](../../leetcode/p56_mergeintervals.cpp)
- [Leetcode problem 905: Sort Array By Parity](https://leetcode.com/problems/sort-array-by-parity/). Solution: [p905_sortarraybyparity.cpp](../../leetcode/p905_sortarraybyparity.cpp)
- [Leetcode problem 1929: Concatenation of Array
](https://leetcode.com/problems/concatenation-of-array/). Solution: [p1929_concatenationofarray.cpp](../../leetcode/p1929_concatenationofarray.cpp)
## 6.3 Dynamic Allocation of Arrays
- How do we allocate an array on the stack? What is the code? What memory diagram is produced by the code?
- Declaring the size of an array at compile time doesnt offer much flexibility. Instead we can dynamically allocate an array based on data. This gets us part-way toward the behavior of the standard library vector class. Heres an example:
<table>
<tr>
<td>
<pre>
int main() {
std::cout << "Enter the size of the array: ";
int n,i;
std::cin >> n;
double *a = new double[n];
for (i=0; i &lt; n; ++i) { a[i] = sqrt(i); }
for (i=0; i &lt; n; ++i) {
if ( double(int(a[i])) == a[i] )
std::cout << i << " is a perfect square " << std::endl;
}
delete [] a;
return 0;
}
</pre>
</td>
<td><img src="array.png" alt="array"</td>
</tr>
</table>
- The expression new double[n] asks the system to dynamically allocate enough consecutive memory to hold n
doubles (usually 8n bytes).
- Whats crucially important is that n is a variable. Therefore, its value and, as a result, the size of the
array are not known until the program is executed and the the memory must be allocated dynamically.
- The address of the start of the allocated memory is assigned to the pointer variable a.
- After this, a is treated as though it is an array. For example: a[i] = sqrt( i );
In fact, the expression a[i] is exactly equivalent to the pointer arithmetic and dereferencing expression *(a+i)
which we have seen several times before.
- After we are done using the array, the line: delete [] a; releases the memory allocated for the entire
array and calls the destructor (well learn about these soon!) for each slot of the array. Deleting a dynamically
allocated array without the [] is an error (but it may not cause a crash or other noticeable problem, depending
on the type stored in the array and the specific compiler implementation).
- Since the program is ending, releasing the memory is not a major concern. However, to demonstrate
that you understand memory allocation & deallocation, you should always delete dynamically allocated
memory in this course, even if the program is terminating.
- In more substantial programs it is ABSOLUTELY CRUCIAL. If we forget to release memory repeatedly
the program can be said to have a memory leak. Long-running programs with memory leaks will eventually
run out of memory and crash.
- Play this [animation](https://jidongxiao.github.io/CSCI1200-DataStructures/animations/dynamic_memory/example2/index.html) to see what exactly the above code snippet does.
## 6.4 Dynamic Allocation of Two-Dimensional Arrays
To store a grid of data, we will need to allocate a top level array of pointers to arrays of the data. For example:
```cpp
double** a = new double*[rows];
for (int i = 0; i < rows; i++) {
a[i] = new double[cols];
for (int j = 0; j < cols; j++) {
a[i][j] = double(i+1) / double (j+1);
}
}
```
- Draw a picture of the resulting data structure.
- Then, write code to correctly delete all of this memory.
- Play this [animation](https://jidongxiao.github.io/CSCI1200-DataStructures/animations/dynamic_memory/two_d_array/index.html) to see what exactly the above code snippet does.
## 6.5 Dynamic Allocation: Arrays of Class Objects
We can dynamically allocate arrays of class objects. The default constructor (the constructor that takes no arguments) must be defined in order to allocate an array of objects.
```cpp
class Foo {
public:
Foo();
double value() const { return a*b; }
private:
int a;
double b;
};
Foo::Foo() {
static int counter = 1;
a = counter;
b = 100.0;
counter++;
}
int main() {
int n;
std::cin >> n;
Foo *things = new Foo[n];
std::cout << "size of int: " << sizeof(int) << std::endl;
std::cout << "size of double: " << sizeof(double) << std::endl;
std::cout << "size of foo object: " << sizeof(Foo) << std::endl;
for (Foo* i = things; i < things+n; i++)
std::cout << "Foo stored at: " << i << " has value " << i->value() << std::endl;
delete [] things;
}
```
The above program will produce the following output:
```console
size of int: 4
size of double: 8
size of foo object: 16
Foo stored at: 0x104800890 has value 100
Foo stored at: 0x1048008a0 has value 200
Foo stored at: 0x1048008b0 has value 300
Foo stored at: 0x1048008c0 has value 400
...
```
## 6.6 Exercises
- [Leetcode problem 1480: Running Sum of 1d Array](https://leetcode.com/problems/running-sum-of-1d-array/). Solution: [p1480_runningsumofarray.cpp](../../leetcode/p1480_runningsumofarray.cpp)
## 6.7 Memory Debugging
In addition to the step-by-step debuggers like gdb, lldb, or the debugger in your IDE, we recommend using a memory
debugger like “Dr. Memory” (Windows, Linux, and MacOSX) or “Valgrind” (Linux and MacOSX). These tools can
detect the following problems:
- Use of uninitialized memory
- Reading/writing memory after it has been freed (NOTE: delete calls free)
- Reading/writing off the end of mallocd blocks (NOTE: new calls malloc)
- Reading/writing inappropriate areas on the stack
- Memory leaks - where pointers to mallocd blocks are lost forever
- Mismatched use of malloc/new/new [] vs free/delete/delete []
- Overlapping src and dst pointers in memcpy() and related functions
## 6.8 Sample Buggy Program
Can you see the errors in this program?
```cpp
1 #include <iostream>
2
3 int main() {
4
5 int *p = new int;
6 if (*p != 10) std::cout << "hi" << std::endl;
7
8 int *a = new int[3];
9 a[3] = 12;
10 delete a;
11
12 }
```
## 6.9 Using Dr. Memory http://www.drmemory.org
Heres how Dr. Memory reports the errors in the above program:
```console
~~Dr.M~~ Dr. Memory version 1.8.0
~~Dr.M~~
~~Dr.M~~ Error #1: UNINITIALIZED READ: reading 4 byte(s)
~~Dr.M~~ # 0 main [memory_debugger_test.cpp:6]
hi
~~Dr.M~~
~~Dr.M~~ Error #2: UNADDRESSABLE ACCESS beyond heap bounds: writing 4 byte(s)
~~Dr.M~~ # 0 main [memory_debugger_test.cpp:9]
~~Dr.M~~ Note: refers to 0 byte(s) beyond last valid byte in prior malloc
~~Dr.M~~
~~Dr.M~~ Error #3: INVALID HEAP ARGUMENT: allocated with operator new[], freed with operator delete
~~Dr.M~~ # 0 replace_operator_delete [/drmemory_package/common/alloc_replace.c:2684]
~~Dr.M~~ # 1 main [memory_debugger_test.cpp:10]
~~Dr.M~~ Note: memory was allocated here:
~~Dr.M~~ Note: # 0 replace_operator_new_array [/drmemory_package/common/alloc_replace.c:2638]
~~Dr.M~~ Note: # 1 main [memory_debugger_test.cpp:8]
~~Dr.M~~
~~Dr.M~~ Error #4: LEAK 4 bytes
~~Dr.M~~ # 0 replace_operator_new [/drmemory_package/common/alloc_replace.c:2609]
~~Dr.M~~ # 1 main [memory_debugger_test.cpp:5]
~~Dr.M~~
~~Dr.M~~ ERRORS FOUND:
~~Dr.M~~ 1 unique, 1 total unaddressable access(es)
~~Dr.M~~ 1 unique, 1 total uninitialized access(es)
~~Dr.M~~ 1 unique, 1 total invalid heap argument(s)
~~Dr.M~~ 0 unique, 0 total warning(s)
~~Dr.M~~ 1 unique, 1 total, 4 byte(s) of leak(s)
~~Dr.M~~ 0 unique, 0 total, 0 byte(s) of possible leak(s)
~~Dr.M~~ Details: /DrMemory-MacOS-1.8.0-8/drmemory/logs/DrMemory-a.out.7726.000/results.txt
```
And the fixed version:
```console
~~Dr.M~~ Dr. Memory version 1.8.0
hi
~~Dr.M~~
~~Dr.M~~ NO ERRORS FOUND:
~~Dr.M~~ 0 unique, 0 total unaddressable access(es)
~~Dr.M~~ 0 unique, 0 total uninitialized access(es)
~~Dr.M~~ 0 unique, 0 total invalid heap argument(s)
~~Dr.M~~ 0 unique, 0 total warning(s)
~~Dr.M~~ 0 unique, 0 total, 0 byte(s) of leak(s)
~~Dr.M~~ 0 unique, 0 total, 0 byte(s) of possible leak(s)
~~Dr.M~~ Details: /DrMemory-MacOS-1.8.0-8/drmemory/logs/DrMemory-a.out.7762.000/results.txt
```
**Note**: Dr. Memory on Windows with the Visual Studio compiler may not report a mismatched free() / delete
/ delete [] error (e.g., line 10 of the sample code above). This may happen if optimizations are enabled and the
objects stored in the array are simple and do not have their own dynamically-allocated memory that lead to their
own indirect memory leaks.
## 6.10 Using Valgrind http://valgrind.org/
And this is how Valgrind reports the same errors:
```console
==31226== Memcheck, a memory error detector
==31226== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==31226== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==31226== Command: ./a.out
==31226==
==31226== Conditional jump or move depends on uninitialised value(s)
==31226== at 0x40096F: main (memory_debugger_test.cpp:6)
==31226==
hi
==31226== Invalid write of size 4
==31226== at 0x4009A3: main (memory_debugger_test.cpp:9)
==31226== Address 0x4c3f09c is 0 bytes after a block of size 12 alloc'd
==31226== at 0x4A0700A: operator new[](unsigned long) (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==31226== by 0x400996: main (memory_debugger_test.cpp:8)
==31226==
==31226== Mismatched free() / delete / delete []
==31226== at 0x4A07991: operator delete(void*) (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==31226== by 0x4009B4: main (memory_debugger_test.cpp:10)
==31226== Address 0x4c3f090 is 0 bytes inside a block of size 12 alloc'd
==31226== at 0x4A0700A: operator new[](unsigned long) (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==31226== by 0x400996: main (memory_debugger_test.cpp:8)
==31226==
==31226==
==31226== HEAP SUMMARY:
==31226== in use at exit: 4 bytes in 1 blocks
==31226== total heap usage: 2 allocs, 1 frees, 16 bytes allocated
==31226==
==31226== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31226== at 0x4A06965: operator new(unsigned long) (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==31226== by 0x400961: main (memory_debugger_test.cpp:5)
==31226==
==31226== LEAK SUMMARY:
==31226== definitely lost: 4 bytes in 1 blocks
==31226== indirectly lost: 0 bytes in 0 blocks
==31226== possibly lost: 0 bytes in 0 blocks
==31226== still reachable: 0 bytes in 0 blocks
==31226== suppressed: 0 bytes in 0 blocks
==31226==
==31226== For counts of detected and suppressed errors, rerun with: -v
==31226== Use --track-origins=yes to see where uninitialised values come from
==31226== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 2 from 2)
```
And heres what it looks like after fixing those bugs:
```console
==31252== Memcheck, a memory error detector
==31252== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==31252== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==31252== Command: ./a.out
==31252==
hi
==31252==
==31252== HEAP SUMMARY:
==31252== in use at exit: 0 bytes in 0 blocks
==31252== total heap usage: 2 allocs, 2 frees, 16 bytes allocated
==31252==
==31252== All heap blocks were freed -- no leaks are possible
==31252==
==31252== For counts of detected and suppressed errors, rerun with: -v
==31252== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)
```
## 6.11 How to use a memory debugger
- Detailed instructions on installation & use of these tools are available here:
http://www.cs.rpi.edu/academics/courses/fall23/csci1200/memory_debugging.php
- Memory errors (uninitialized memory, out-of-bounds read/write, use after free) may cause seg faults, crashes,
or strange output.
- Memory leaks on the other hand will never cause incorrect output, but your program will be inefficient and
hog system resources. A program with a memory leak may waste so much memory it causes all programs on
the system to slow down significantly or it may crash the program or the whole operating system if the system
runs out of memory (this takes a while on modern computers with lots of RAM & harddrive space).
- For many future homeworks, Submitty will be configured to run your code with Dr. Memory to search for
memory problems and present the output with the submission results. For full credit your program must be
memory error and memory leak free!
- A program that seems to run perfectly fine on one computer may still have significant memory errors. Running
a memory debugger will help find issues that might break your homework on another computer or when
submitted to the homework server.
- **Important Note**: When these tools find a memory leak, they point to the line of code where this memory
was allocated. These tools does not understand the program logic and thus obviously cannot tell us where it
should have been deleted.
- A final note: STL and other 3rd party libraries are highly optimized and sometimes do sneaky but correct and
bug-free tricks for efficiency that confuse the memory debugger. For example, because the STL string class
uses its own allocator, there may be a warning about memory that is “still reachable” even though youve
deleted all your dynamically allocated memory. The memory debuggers have automatic suppressions for some
of these known “false positives”, so you will see this listed as a “suppressed leak”. So dont worry if you see
those messages.
## 6.12 Diagramming Memory Exercises
- Draw a diagram of the heap and stack memory for each segment of code below. Use a “?” to indicate that the
value of the memory is uninitialized. Indicate whether there are any errors or memory leaks during execution
of this code.
```cpp
class Foo {
public:
double x;
int* y;
};
Foo a;
a.x = 3.14159;
Foo *b = new Foo;
(*b).y = new int[2];
Foo *c = b;
a.y = b->y;
c->y[1] = 7;
b = NULL;
```
See [solution](memory_exercise1_solution.png).
```cpp
int a[5] = { 10, 11, 12, 13, 14 };
int *b = a + 2;
*b = 7;
int *c = new int[3];
c[0] = b[0];
c[1] = b[1];
c = &(a[3]);
```
See [solution](memory_exercise2_solution.png). Note that there is a memory leak of 3 *int*s in this program. Do you see why?
- Write code to produce this diagram:
![alt text](memory_exercise.png "memory exercise")
See [solution](memory_exercise3_solution.cpp).

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
lectures/06_memory/heap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,8 @@
double a[3];
double *b = new double[3];
a[0] = 4.2;
a[1] = 8.6;
a[2] = 2.9;
b[0] = 6.5;
b[1] = 5.1;
b[2] = 3.4;

View File

@@ -0,0 +1,261 @@
# Lecture 7 --- Order Notation & Basic Recursion
- Algorithm Analysis, Formal Definition of Order Notation
- Simple recursion, Visualization of recursion, Iteration vs. Recursion
- “Rules” for writing recursive functions, Lots of examples
## 7.1 Algorithm Analysis
Why should we bother?
- We want to do better than just implementing and testing every idea we have.
- We want to know why one algorithm is better than another.
- We want to know the best we can do. (This is often quite hard.)
How do we do it? There are several options, including:
- Dont do any analysis; just use the first algorithm you can think of that works.
- Implement and time algorithms to choose the best.
- Analyze algorithms by counting operations while assigning different weights to different types of operations
based on how long each takes.
- Analyze algorithms by assuming each operation requires the same amount of time. Count the total number of
operations, and then multiply this count by the average cost of an operation.
## 7.2 Exercise: Counting Example
- Suppose arr is an array of n doubles. Here is a simple fragment of code to sum of the values in the array:
```cpp
double sum = 0;
for (int i=0; i<n; ++i)
sum += arr[i];
```
- What is the total number of operations performed in executing this fragment? Come up with a function describing the number of operations in terms of n.
## 7.3 Exercise: Which Algorithm is Best?
A venture capitalist is trying to decide which of 3 startup companies to invest in and has asked for your help. Heres
the timing data for their prototype software on some different size test cases:
| n | foo-a | foo-b | foo-c |
|-------|----------|----------|----------|
| 10 | 10 u-sec| 5 u-sec | 1 u-sec |
| 20 | 13 u-sec| 10 u-sec | 8 u-sec |
| 30 | 15 u-sec| 15 u-sec | 27 u-sec |
| 100 | 20 u-sec| 50 u-sec | 1000 u-sec|
| 1000 | ? | ? | ? |
Which company has the “best” algorithm?
## 7.4 Order Notation Definition
In this course we will focus on the intuition of order notation. This topic will be covered again, in more depth, in
later computer science courses.
- Definition: Algorithm A is order f(n) — denoted O(f(n)) — if constants k and n<sub>0</sub> exist such that A requires no more than k * f(n) time units (operations) to solve a problem of size n ≥ n<sub>0</sub>.
- For example, algorithms requiring 3n + 2, 5n 3, and 14 + 17n operations are all O(n).
This is because we can select values for k and n0 such that the definition above holds. (What values?)
Likewise, algorithms requiring n<sup>2</sup>/10 + 15n 3 and 10000 + 35n<sup>2</sup> are all O(n<sup>2</sup>).
- Intuitively, we determine the order by finding the asymptotically dominant term (function of n) and throwing
out the leading constant. This term could involve logarithmic or exponential functions of n. Implications for
analysis:
We dont need to quibble about small differences in the numbers of operations.
We also do not need to worry about the different costs of different types of operations.
We dont produce an actual time. We just obtain a rough count of the number of operations. This count is used for comparison purposes.
- In practice, this makes analysis relatively simple, quick and (sometimes unfortunately) rough.
## 7.5 Common Orders of Magnitude
- O(1), a.k.a. CONSTANT: The number of operations is independent of the size of the problem. e.g., compute
quadratic root.
- O(log n), a.k.a. LOGARITHMIC. e.g., dictionary lookup, binary search.
- O(n), a.k.a. LINEAR. e.g., sum up a list.
- O(n log n), e.g., sorting.
- O(n<sup>2</sup>), O(n<sup>3</sup>), O(n<sup>k</sup>), a.k.a. POLYNOMIAL. e.g., find closest pair of points.
- O(2<sup>n</sup>), O(k<sup>n</sup>), a.k.a. EXPONENTIAL. e.g., Fibonacci, playing chess.
## 7.6 Exercise: A Slightly Harder Example
Heres an algorithm to determine if the value stored in variable x is also in an array called foo. Can you analyze
it? What did you do about the if statement? What did you assume about where the value stored in x occurs
in the array (if at all)?
```cpp
int loc=0;
bool found = false;
while (!found && loc < n) {
if (x == foo[loc]) found = true;
else loc++;
}
if (found) cout << "It is there!\n";
```
## 7.7 Best-Case, Average-Case and Worst-Case Analysis
- For a given fixed size array, we might want to know:
The fewest number of operations (best case) that might occur.
The average number of operations (average case) that will occur.
The maximum number of operations (worst case) that can occur.
- The last is the most common. The first is rarely used.
- On the previous algorithm, the best case is O(1), but the average case and worst case are both O(n).
## 7.8 Approaching An Analysis Problem
- Decide the important variable (or variables) that determine the “size” of the problem. For arrays and other
“container classes” this will generally be the number of values stored.
- Decide what to count. The order notation helps us here.
- If each loop iteration does a fixed (or bounded) amount of work, then we only need to count the number
of loop iterations.
- We might also count specific operations. For example, in the previous exercise, we could count the number
of comparisons.
- Do the count and use order notation to describe the result.
## 7.9 Exercise: Order Notation
For each version below, give an order notation estimate of the number of operations as a function of n:
1
```cpp
int count=0;
for (int i=0; i<n; ++i)
for (int j=0; j<n; ++j)
++count;
```
2.
```cpp
int count=0;
for (int i=0; i<n; ++i)
++count;
for (int j=0; j<n; ++j)
++count;
```
3.
```cpp
int count=0;
for (int i=0; i<n; ++i)
for (int j=i; j<n; ++j)
++count;
```
## 7.10 Recursive Definitions of Factorials and Integer Exponentiation
- Factorial is defined for non-negative integers as:
![alt text](factorial_equation.png "factorial equation")
- Computing integer powers is defined as:
![alt text](power_equation.png "power equation")
These are both examples of recursive definitions.
## 7.11 Recursive C++ Functions
C++, like other modern programming languages, allows functions to call themselves. This gives a direct method of
implementing recursive functions. Here are the recursive implementations of factorial and integer power:
```cpp
int fact(int n) {
if (n == 0) {
return 1;
} else {
int result = fact(n-1);
return n * result;
}
}
```
```cpp
int intpow(int n, int p) {
if (p == 0) {
return 1;
} else {
return n * intpow( n, p-1 );
}
}
```
## 7.12 The Mechanism of Recursive Function Calls
- For each recursive call (or any function call), a program creates an activation record to keep track of:
Completely separate instances of the parameters and local variables for the newly-called function.
The location in the calling function code to return to when the newly-called function is complete. (Who
asked for this function to be called? Who wants the answer?)
Which activation record to return to when the function is done. For recursive functions this can be
confusing since there are multiple activation records waiting for an answer from the same function.
- This is illustrated in the following diagram of the call fact(4). Each box is an activation record, the solid lines
indicate the function calls, and the dashed lines indicate the returns. Inside of each box we list the parameters
and local variables and make notes about the computation.
![alt_text](activation_records.png "chain of activation records")
- This chain of activation records is stored in a special part of program memory called the stack
## 7.13 Iteration vs. Recursion
- Each of the above functions could also have been written using a for or while loop, i.e. iteratively. For example, here is an iterative
version of factorial:
```cpp
int ifact(int n) {
int result = 1;
for (int i=1; i<=n; ++i)
result = result * i;
return result;
}
```
- Often writing recursive functions is more natural than writing iterative functions, especially for a first draft of
a problem implementation.
- You should learn how to recognize whether an implementation is recursive or iterative, and practice rewriting
one version as the other. Note: Well see that not all recursive functions can be easily rewritten in iterative
form!
- Note: The order notation for the number of operations for the recursive and iterative versions of an algorithm
is usually the same. However in C, C++, Java, and some other languages, iterative functions are generally
faster than their corresponding recursive functions. This is due to the overhead of the function call mechanism.
Compiler optimizations will sometimes (but not always!) reduce the performance hit by automatically eliminating
the recursive function calls. This is called tail call optimization.
## 7.14 Exercises
1. Draw a picture to illustrate the activation records for the function call
cout << intpow(4, 4) << endl;
2. Write an iterative version of intpow.
3. What is the order notation for the two versions of intpow?
## 7.15 Rules for Writing Recursive Functions
Here is an outline of five steps that are useful in writing and debugging recursive functions. Note: You dont have
to do them in exactly this order...
1. Handle the base case(s).
2. Define the problem solution in terms of smaller instances of the problem. Use wishful thinking, i.e., if someone
else solves the problem of fact(4) I can extend that solution to solve fact(5). This defines the necessary
recursive calls. It is also the hardest part!
3. Figure out what work needs to be done before making the recursive call(s).
4. Figure out what work needs to be done after the recursive call(s) complete(s) to finish the computation. (What
are you going to do with the result of the recursive call?)
5. Assume the recursive calls work correctly, but make sure they are progressing toward the base case(s)!
## 7.16 Location of the Recursive Call — Example: Printing the Contents of a Vector
Here is a function to print the contents of a vector. Actually, its two functions: a driver function, and a true
recursive function. It is common to have a driver function that just initializes the first recursive function call.
```cpp
void print_vec(std::vector<int>& v, unsigned int i) {
if (i < v.size()) {
cout << i << ": " << v[i] << endl;
print_vec(v, i+1);
}
}
```
```cpp
void print_vec(std::vector<int>& v) {
print_vec(v, 0);
}
```
What will this print when called in the following code?
```cpp
int main() {
std::vector<int> a;
a.push_back(3); a.push_back(5); a.push_back(11); a.push_back(17);
print_vec(a);
}
```
How can you change the second print vec function as little as possible so that this code prints the contents
of the vector in reverse order?

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,26 @@
// this is a very bad solution, in this problem we do not need to use dynamic memory, because std::vector automatically uses dynamic memory internally.
// the solution below therefore is just a demonstration of how to use the new and delete operator.
class Solution {
public:
vector<int> runningSum(vector<int>& nums) {
int sum=0;
vector<int> result;
int size = nums.size();
// because runningSum will have size elements, and runningSum[0] is the sum of one element, i.e., nums[0].
for(int i=1;i<(size+1);i++){
int *a = new int[i];
// sum stores the sum from nums[0] to nums[i]
for(int j=0;j<i;j++){
a[j] = nums[j];
sum = sum + a[j];
}
result.push_back(sum);
// allocated memory for an array, and thus delete memory for an array
delete [] a;
// reset sum so as to reuse it.
sum = 0;
}
return result;
}
};