Project 1: Circular Buffer of Circular Buffers
Due: Tuesday, September 25, 8:59:59pm
Links: [Project Submission] [Late Submissions] [Project Grading] [Grading Guidelines] [Academic Conduct]Objectives
The objective of this programming assignment is to have you review C++ programming using following the features: object-oriented design, dynamic memory allocation (of arrays) and pointer manipulation.Introduction
Note: Circular Buffers are described in Section 5.2.4 of our textbook (p. 211). Please read that section before proceeding.
Two common data structures for implementing First-In-First-Out (FIFO) queues are singly-linked lists and arrays used as circular buffers. The advantage of using an array is that we can add and remove items from the FIFO queue quickly simply by updating the indices of the start and the end of the queue. As items are added and removed from the array, the indices would "wrap around" and space in the array is reused. In the example below, 14 is at the start of the queue and 21 is at the end. If we added another item to the FIFO queue, it would go in index #1. If we remove an item from the FIFO queue, 14 would be removed.
When using an array for a circular buffer, we must make sure that the array is big enough to hold the largest number of items that can be in the FIFO queue simultaneously. Suppose we added two more values–22 and 23–to the circular buffer above. Then, the buffer would be full:
We cannot add another item to the circular buffer. What do we do? (There is a version of circular buffers that allow the data structure to drop the oldest item in the queue if the buffer is full. This is useful in some applications. For example, if we use the circular buffer to store the last 5 seconds of an audio recording, we can just "erase" the oldest part of the recording and reuse the space for the newest. For this project we are not allowed to drop any data.)
The standard trick when we run out of space in an array is to make a new array that holds twice as much data and copy the data over, freeing the old array afterwards. Copying is slow, though. So, instead of copying, we will just add another array for new values, and keep both arrays. (Note that we are still using the strategy of allocating a new array that is twice as big.) The following diagram shows what happens if we add 24, 25, 26, 27, 28 and 29 to the example above.
The resulting situation is slightly more complex than a simple expanded single array. We now have to remember that when we remove something from the FIFO queue, we remove it from the start of the old array. but when we add something to the FIFO queue, we add it to the end of the new array. For example, if we added 30, 31 and 32 and removed two items from the queue above, then we would have:
In a simple world, eventually, all of the items in the old array will be removed and we would be left with just the new array. We then fall back to the "normal" circular buffer implemented as a single array. For example, if we removed 11 items from the figure above, we would get:
But, what if we add lots of items to the FIFO queue and even the second array fills up? Then, we just create a third array. What if that one becomes full? Then we create another one. And if that one is full? In the general case, we have a bunch of arrays. We keep track of which array is the oldest and which one is the newest. We always add to the newest array and remove from the oldest array:
Once the oldest array is emptied, we can deallocate that array, and we remove items from the next oldest array. If the newest array is full, then we create another with twice the capacity. We use an array of pointers to keep track of these circular buffers. In the general case, the array of pointers might also wrap around, because (yes, you guessed it) the array of pointers (in blue below) is itself a circular buffer:
Interesting Observation
The limited version of the data structure we are implementing here, where the outer buffer is fixed at some size (in our case, 7), displays an interesting behavior: the ultimate capacity before it throws an exception depends on the exact usage pattern. Imagine a toy case where we limit our outer circular buffer to size 3, and start our first inner buffer at size 1. If the user starts right off doing nothing but enqueues, the whole data structure will top out at 7 items before throwing an exception on the 8th addition. However, if the user enqueues 7 items, but then dequeues them all, it will cause all but the newest–and largest, at size 4–inner buffer to be deallocated. Then, if the user starts enqueuing again, after 4 items are added, another inner buffer will be allocated with size 8, and a third with size 16 after that if the user keeps enqueuing. At that point, our total capacity would be 28. You can see that the implementation we are doing for this project is therefore overly sensitive to the exact growth pattern of the user's storage needs, which is not a good characteristic of a data structure. Luckily, you are not planning to try to sell this system commercially...Assignment
Your assignment is to implement the "circular buffer of circular buffers" data structure described above. For this project, you will have very limited design choices. You are required to use the class definitions given in InnerCB.h and CBofCB.h. (Specifications are given below.) The InnerCB class is an implementation of a simple circular buffer of int values. (Like the green arrays above.) The CBofCB class is the circular buffer of circular buffers.
Specifications
Here are the specifics of the assignment, including a description for what each member function must accomplish.
Requirement: Place the implementation of the InnerCB member functions in a file called InnerCB.cpp and the implementation of the CBofCB member functions in CBofCB.cpp.
Requirement: All member functions must be implemented from scratch. In particular, you are not allowed to use any classes from the Standard Template Library (STL), not even vector.
Requirement: You are not allowed to add anything or remove anything from these header files. Your submission must compile with the original .h files without modification.
Requirement: Do not use global variables to circumvent the prohibition from altering InnerCB.h and CBofCB.h. Actually, just don't use global variables period.
Requirement: Your code must compile on GL with the exact Unix commands given below. (See "How to Submit".)
Requirement: Your code must not have any memory leaks. When you run your code under valgrind on GL, it must report:
Requirement: Your implementation must be efficient. Tests p1test11.cpp, p1test12.cpp and p1test13.cpp should take approximately 16, 32 and 64 seconds of user time to run on GL. The actual number of seconds do not matter as much as the fact that each program takes about twice as long as the previous one. This shows that your implementation is taking linear time.
These are the member functions of the InnerCB class:
-
InnerCB(int n=10) ; This is the constructor for the InnerCB class. It should initialize the data members and allocate memory to hold n int values. The member m_buffer should point to this memory. The default value of n is 10. (Yes, that's a magic number.)
-
InnerCB(const InnerCB& other) ; This is the copy constructor for the InnerCB class. The copy constructor must create a complete copy of the InnerCB object other. The new copy must have separately allocated memory for the circular buffer of int. Do not use the assignment operator to implement the copy constructor. Just don't. No really, it's a bad idea.
-
~InnerCB() ; This is the destructor for the InnerCB class. You must deallocate memory. Remember that you should never call a destructor explicitly. Well, almost never. If you have never heard of "placement new" in C++, then don't ever call the destructor explicitly.
-
void enqueue(int data) ; This function should add the value in the data parameter to the circular buffer. Remember to wrap around. If the buffer is full, then throw an overflow_error exception. This exception is defined in stdexcept so
#include at the top.
-
int dequeue() ; This function should remove the oldest item in the circular buffer and return that value. If the buffer is empty, then throw an underflow_error exception. This is also defined in stdexcept.
-
bool isFull() ; This function should return true if the circular buffer is full.
-
bool isEmpty() ; This function should return true if the circular buffer is empty.
-
int capacity() ; This function should return the number of int values that can be stored in the amount of space allocated in the array of int that m_buffer points to. I.e., it's the length of the array of the circular buffer.
-
int size() ; Returns the number of items stored in the circular buffer.
-
const InnerCB& operator=(const InnerCB& rhs) ; This is the overloaded assignment operator. If you forgot what that means, crack open your C++ textbook again. Remember to check for self-assignment. Remember to deallocate space of the host object. Remember to allocate new space. The InnerCB objects on the left hand side (LHS) and right hand side of the assignment (RHS) are not required to have the same capacity before the assignment. After the assignment the LHS should be an exact duplicate of the RHS with the same capacity, and have the items stored in the exact same locations of m_buffer. You can't use the copy constructor to implement the assignment operator. That would create a third object, which isn't what you want. Really, that doesn't work. If you don't "remember" why you have to check for self-assignment, then go read up on assignment operators. It will actually save you time.
-
void dump() ; This is a debugging function that prints out the contents of the InnerCB object. See sample outputs below for suggested format. (You do not have to follow the format exactly, so don't worry about counting the number of dashes or spaces.)
-
bool inspect (int* &buf, int &cap, int &size, int &start, int &end) ; This function is used for grading. You do not need to implement anything for this function. In fact, it is important that you don't provide any code for this function, since the grading programs have their own implementations and your code won't compile if the compiler sees two implementations.
These are the member functions of the CBofCB class:
-
CBofCB() ; This is the default constructor for the CBofCB class. It should initialize all of the data members and set up a single inner circular buffer that holds 10 int values. Note that m_buffers is an array of pointers to InnerCB. It is NOT a dynamically allocated array. You do need to initialize the array of pointers to NULL.
-
CBofCB(const CBofCB& other) ; This is the copy constructor for the CBofCB class. You have to make a complete copy of the other object. Make good use of the InnerCB copy constructor. The admonition to not use the CBofCB assignment operator to implement this copy constructor also holds here.
-
~CBofCB() ; This is the destructor. You must deallocate space used by the InnerCB objects that the array of pointers in m_buffers points to. Remember that you don't do that by calling the InnerCB destructor explicitly, because you never call destructors explicitly.
-
void enqueue(int data) ; This member function adds the value in the data parameter to the data structure. Recall from the discussion above that you always add to the newest InnerCB. You may have to create a new InnerCB if the current newest InnerCB is full. This new newest array should have twice the capacity of the previous newest array. If this cannot be done because the outer circular buffer is also full, then just throw an overflow_error exception.
-
int dequeue() ; This function removes and returns the oldest value stored in the data structure. If the entire data structure is empty, then just throw an underflow_error exception. Recall from above that the oldest value is stored in the oldest InnerCB. If by removing this oldest item causes the oldest InnerCB to become empty, then you must deallocate this InnerCB and remove it from the outer circular buffer. However, if the empty buffer is the only InnerCB in the whole data structure, then do not deallocate it. We want to always have at least one InnerCB in place.
-
bool isFull() ; Returns true if it is not possible to add any more items to the data structure. This would occur if every entry of m_buffers is already pointing to an InnerCB and the newest InnerCB is full. (This also implies that all except the oldest InnerCB are full, too.)
-
bool isEmpty() ; Returns true if there are no items stored in the data structure anywhere.
-
int size() ; Returns the number of items stored in the data structure. Note that this is not the number of InnerCB objects that you have pointed to by pointers in the m_buffers array. It is the total number of int values you have stored in all of the InnerCB objects.
-
const CBofCB& operator=(const CBofCB& rhs) ; This is the overloaded assignment operator for the CBofCB class. Read the reminders about assignment operators written in the specifications of the assignment operator for the InnerCB class. They also apply here.
Before the assignment, the left hand side (LHS) and the right hand side (RHS) might not have pointers to InnerCBs of the same capacity. However, after the assignment, the LHS should be an exact duplicate of the RHS. They should have the same value for m_obSize, each allocated InnerCB should have the same capacity and be referenced by the same location in m_buffers, and each InnerCB on the RHS should have an exact duplicate on the LHS.
-
void dump() ; As before, this is a debugging function that prints out the contents of the entire data structure. Make good use of InnerCB::dump(). See sample output below for suggested format.
-
bool inspect (InnerCB** &buf, int &cap, int &size, int &start, int &end) ; As with InnerCB::inspect(), this function is used for grading. Just don't do anything with this function, you will be fine.
Test Programs
The following test programs should be used to check the compatibility of your implementation. These programs do not check the complete correctness of your implementation. Even if your implementation compiles and runs correctly with these programs, it does not mean your implementation is error-free. Grading will be done using different programs.
In general, you must do the testing yourself --- testing is part of programming. This is a more comprehensive set of test programs than we will provide in subsequent projects. For the first project, we are trying to show you the minimum amount of testing you should be doing.
Conversely, if your implementation does not compile or does not run correctly with these test programs, then it is unlikely that it will compile or run correctly with the grading programs.
- p1test01.cpp and p1test01.txt (sample output).
- p1test02.cpp and p1test02.txt (sample output).
- p1test03.cpp and p1test03.txt (sample output).
- p1test04.cpp and p1test04.txt (sample output).
- p1test05.cpp and p1test05.txt (sample output).
- p1test06.cpp and p1test06.txt (sample output).
- p1test07.cpp and p1test07.txt (sample output).
- p1test08.cpp and p1test08.txt (sample output).
- p1test09.cpp and p1test09.txt (sample output).
- p1test10.cpp and p1test10.txt (sample output).
- p1test11.cpp and p1test11.txt (sample output).
- p1test12.cpp and p1test12.txt (sample output).
- p1test13.cpp and p1test13.txt (sample output).
These files are also available on GL in the directory:
Implementation Notes
- Remember to wrap around when indices go past the end of the array.
- Don't confuse size and capacity. The size is the number of items stored in the circular buffer. The capacity is how much space was allocated.
How to Submit
You must submit the following files to the proj1 directory.
- InnerCB.cpp
- CBofCB.cpp
You do not need to submit InnerCB.h or CBofCB.h because it should not have changed.
If you followed the instructions in the Project Submission page to set up your directories, you can submit your code using this Unix command:
Do remember to exit from the script command. This creates a file called typescript that will record any compilation errors. Yes, we know you can edit this file, but the compilation errors will just show up when we compile the programs again and you will still get lots of points deducted. This step is to compel you to fix any changes needed to get your program to compile on GL without any errors.
Note: cd to the appropriate alternate directory if you are submitting late.
Run t01.out thru t10.out under valgrind:
If you do not see
after each run, then you have a memory leak. You are not ready to submit. Go back and fix your program. If valgrind reports other memory errors (e.g., reading from invalid memory or writing to invalid memory) then you don't have a memory leak, but you have some other sort of bug. You should also go back and fix your program.
(Note: do not run t11.cpp, t12.cpp or t13.cpp under valgrind: they take too long.)
Finally, use the script command to record your timing runs for p1test11.cpp, p1test12.cpp and p1test13.cpp. We want to use a separate file to record this, so use the -a option for script:
Output from the programs are omitted above. The 17.722u, 34.421u and 68.571u are the user times from running the three programs.
Now you can delete the executable files with
Then you should just have 4 files in your submission directory. Check using the ls command. You can also double check that you are in the correct directory using the pwd command. (You should see your username instead of xxxxx.)