Vectors, Streams, Exceptions, and Practice Problems, CSCE 120 Exam 3 – Study Notes
offline

Difficulty: Intermediate | Prerequisites: Parts 1 and 2 (Arrays, Dynamic Memory, Rule of Three)


Big Picture

This section ties together the low-level memory work from the previous two parts. The tiny vector (IntVec) shows how a class can wrap a dynamic array and manage its own memory, giving you push_back, pop_back, reserve, and deep-copy behaviour for free. Streams (ifstream, ofstream) let you read from and write to files using the same >> and << operators you already know from cin and cout. Exception types give you a vocabulary for runtime errors. Finally, the practice problems in this section are representative of the free-response format on the exam.


TL;DR

std::vector (and the manual IntVec implementation) wraps a dynamic array with automatic growth, deep-copy semantics, and RAII cleanup. Streams extend >> and << to files. C++ exceptions are organised into logic_error (programmer bugs) and runtime_error (environment failures). The exam practice problems test your ability to allocate, resize, copy, and free dynamic arrays inside functions.


Key Terms

std::vector

A standard library container that manages a dynamic array internally. It tracks both size (number of elements in use) and capacity (total allocated slots). Think of it as a dynamic array that handles its own new[], delete[], and resizing.

Size vs capacity

Size is how many elements the vector currently holds. Capacity is how many it can hold before needing to reallocate. A vector with size 3 and capacity 8 has five unused slots.

push_back

Appends an element to the end of a vector. If size == capacity, the vector reallocates (typically doubling capacity) before inserting.

pop_back

Removes the last element by decrementing size. Does not reallocate or shrink capacity.

reserve(n)

Ensures the vector's capacity is at least n. If already large enough, does nothing. If not, reallocates to n. Does not change size.

Extraction operator (>>)

Pulls data out of a stream (input). Used with cin and ifstream.

Insertion operator (<<)

Sends data into a stream (output). Used with cout and ofstream.

ifstream

Input file stream. Opens a file for reading. In simple terms, the file-based version of cin.

ofstream

Output file stream. Opens a file for writing. The file-based version of cout.

fstream

A file stream that can do both reading and writing.

logic_error

A category of exceptions for errors caused by programmer mistakes (preconditions not met). Subtypes include invalid_argument, domain_error, out_of_range, and length_error.

runtime_error

A category of exceptions for errors that occur during execution and cannot be predicted at compile time. Subtypes include overflow_error, underflow_error, range_error, and system_error.

bad_alloc

Thrown when new fails to allocate memory (the system is out of memory).


Core Content

IntVec: A Tiny Vector Implementation

The IntVec class demonstrates how std::vector works internally. It has three private members: int* data_, size_t size_, and size_t cap_.

Copy constructor (deep copy):

IntVec(const IntVec& other) : data_(nullptr), size_(other.size_), cap_(other.cap_) {
    if (cap_ > 0) {
        data_ = new int[cap_];
        for (size_t i = 0; i < size_; ++i)
            data_[i] = other.data_[i];
    }
}

push_back with doubling growth:

void push_back(int v) {
    if (size_ == cap_) reallocate(cap_ ? cap_ * 2 : 1);
    data_[size_++] = v;
}

If size equals capacity, the internal array is reallocated to twice its current capacity (or 1 if currently zero). Then the new element is placed at the end and size is incremented.

reserve:

void reserve(size_t n) {
    if (n > cap_) reallocate(n);
}

Only reallocates if n exceeds the current capacity.

clear: sets size to 0 but keeps the allocated memory (capacity unchanged).

pop_back: decrements size. No reallocation.

Element access:

  • operator[] provides unchecked access (fast, no bounds check).

  • at(i) checks bounds and throws std::out_of_range if i >= size_.

Pointer Invalidation With Vectors

This is a subtle and important point. When a vector reallocates (e.g. during push_back that exceeds capacity), all pointers and references to its elements become invalid.

std::vector

After reallocation, p still points to the old (now freed) memory. This is a dangling pointer.

Streams and File I/O

The extraction operator >> reads formatted input. The insertion operator << writes formatted output. These work with file streams the same way they work with cin and cout:

std::ifstream file("input.txt");
int* b = new int[3];
for (int i = 3; i > 0; i--)
    file >> b[i - 1];       // reads into b[2], b[1], b[0]
file.close();

Always close files when finished. Always delete[] any dynamically allocated memory before the programme exits.

Built-In Exception Types

The hierarchy under std::exception:

  • logic_error (programmer's fault):

    • invalid_argument – wrong type of input

    • domain_error – input outside a function's domain (e.g. sqrt(-1))

    • out_of_range – index access out of bounds

    • length_error – container too long

  • runtime_error (environment's fault):

    • overflow_error – arithmetic overflow

    • underflow_error – arithmetic underflow

    • range_error – value out of expected range

    • system_error – OS resource problems

    • ios_base::failure – I/O failure

  • bad_* exceptions (unusual conditions):

    • bad_alloc – memory allocation failed

    • bad_cast – invalid dynamic_cast

    • bad_weak_ptr, bad_optional_access

Passing Arrays to Functions and Pointer Behaviour

When you pass a pointer to a function, the function gets a copy of the pointer (not a reference to it). Changes to the pointer itself inside the function do not affect the caller:

void func(int* x) {
    x[2] = 5;          // modifies the array (shared memory)
    x = new int[7];    // changes only the local copy of the pointer
}

If the function needs to redirect the caller's pointer (e.g. after a resize), pass by reference: int*& x.

When you pass an array (pointer) to a function by value, modifying the elements works (both the caller and the function point to the same heap block), but reassigning the pointer itself has no effect on the caller.

Structs With Dynamic Members

When a struct contains a pointer to heap memory, you face the same shallow/deep copy issues as with classes:

struct FruitBox {
    string* fruits;
};

void fill_box(FruitBox& fb) {
    fb.fruits = new string[2]{"apple", "orange"};
}

Passing the struct by value copies the pointer (shallow). Passing by reference lets you modify the original.


Practice Problems (Exam-Style)

Problem 1: get_even_numbers

Given an array A of n integers, return a new dynamically allocated array containing only the even numbers. Set output parameter m to the count of evens.

Pattern: scan and count, allocate, copy, return.

int* get_even_numbers(int* A, unsigned int n, unsigned int& m) {
    m = 0;
    for (unsigned int i = 0; i < n; i++)
        if (A[i] % 2 == 0) m++;

    int* evens = new int[m];
    unsigned int index = 0;
    for (unsigned int i = 0; i < n; i++) {
        if (A[i] % 2 == 0) {
            evens[index] = A[i];
            index++;
        }
    }
    return evens;
}

Problem 2: separate_even_odd

Return a new array with all even elements first, then all odd elements, preserving relative order within each group.

Pattern: two-pass approach. First pass copies evens; second pass copies odds.

int* separate_even_odd(int* A, unsigned int n) {
    int* result = new int[n];
    unsigned int index = 0;
    for (unsigned int i = 0; i < n; i++)
        if (A[i] % 2 == 0) result[index++] = A[i];
    for (unsigned int i = 0; i < n; i++)
        if (A[i] % 2 != 0) result[index++] = A[i];
    return result;
}

Problem 3: combine_strings

Takes a dynamic array of string fragments and an array of word lengths. Concatenates fragments into complete words based on the lengths. Resizes the original array.

Key observations:

  • arr is passed as string*& because the function replaces the entire array.

  • arr_size is passed as size_t& because the new size differs.

  • Validate that word lengths sum to arr_size before proceeding.

  • Allocate a new array of size word_lens_size, concatenate fragments, delete[] the old array, update pointer and size.

Problem 4: get_array_ordered_by_frequency

Given an array a, return a new dynamically allocated array of unique elements sorted by descending frequency.

Steps: build a unique-elements array, build a parallel frequency array, sort both by frequency (selection sort with swaps in both arrays), delete the frequency array, return the uniques array.

Problem 5: resizeArray and addElements

resizeArray creates a new array of newSize, copies old elements, zero-fills new slots, deletes the old array, and returns the new one.

addElements copies new values into the array starting at startIndex.


Formulas / Diagrams

No mathematical formulas. The key structural concept is the stack/heap diagram for vector: the vector object itself (with data_, size_, cap_) lives on the stack, while the actual element storage lives on the heap. On reallocation, the heap block changes address, invalidating any external pointers to it.


Real-World Applications

std::vector is the most-used container in C++. File streams are how programmes read configuration files, process logs, and handle data pipelines. Exception hierarchies let you write catch blocks that handle broad categories of error (all runtime_error subtypes, for instance) without listing every specific case.


Common Misconceptions

  • "push_back always causes a reallocation." It does not. Reallocation only happens when size == capacity. The doubling strategy means most push_back calls are O(1).

  • "reserve changes the size of the vector." It does not. It only changes capacity. Size remains unchanged.

  • "A pointer to v[0] is safe after push_back." Only if push_back did not trigger a reallocation. If it did, the old memory is freed and the pointer is dangling.

  • "clear() frees the vector's memory." It sets size to 0 but does not release the underlying allocation. Capacity stays the same.


Why It Matters / Exam Flags

  • ⚠️ Free-response questions ask you to implement functions that allocate new dynamic arrays, copy selectively, delete old arrays, and update pointers/sizes. Practice the allocate-copy-delete-redirect pattern until it is automatic.

  • ⚠️ Know the difference between size() and capacity() for vectors.

  • ⚠️ Pointer invalidation after vector reallocation is a common conceptual question.

  • ⚠️ Know how to read from a file using ifstream and >>, including reading into a dynamic array.

  • ⚠️ The exception hierarchy (logic_error vs runtime_error vs bad_alloc) may appear as a matching or multiple-choice question.


Quick Self-Test

  1. True or False: reserve(100) on a vector with capacity 200 causes a reallocation.

  1. Fill in the blank: When push_back is called and size == capacity, the typical strategy is to ________ the capacity.

  1. True or False: ifstream is used for writing to files.

  1. Fill in the blank: out_of_range is a subtype of ________.

  1. True or False: After clear(), a vector's capacity is zero.

Answers: 1. False (current capacity is already sufficient). 2. Double. 3. False (ifstream reads; ofstream writes). 4. logic_error. 5. False (capacity is unchanged).


Practice Q&A

Q: What does the following code print?

std::vector

A: 2 and at least 2 (likely 2, since doubling 1 gives 2). The exact capacity depends on implementation, but in the typical doubling scheme, the capacity after the second push_back is 2.

Q: Write a function that reads n integers from a file called "data.txt" into a dynamically allocated array and returns the array.

A:

int* readFromFile(int n) {
    std::ifstream file("data.txt");
    int* arr = new int[n];
    for (int i = 0; i < n; i++)
        file >> arr[i];
    file.close();
    return arr;
}

The caller is responsible for delete[] arr when done.

Q: In get_even_numbers, why do we scan the array twice (once to count, once to copy)?

A: Because we need to know how many even numbers there are before we can allocate the result array of the correct size. Dynamic arrays require a known size at allocation time.

Q: Why is string*& arr used instead of string* arr in combine_strings?

A: The function replaces the entire array (allocates a new one and deletes the old one). The caller's pointer needs to be updated to point to the new array, which requires a reference to the pointer.

Q: What exception type would v.at(100) throw on a vector of size 5?

A: std::out_of_range, which is a subtype of std::logic_error.


Connections to Other Topics

The IntVec/vector implementation ties together dynamic arrays (Part 1) and the Rule of Three (Part 2) into a single cohesive class. File streams connect to the broader topic of operator overloading (the << and >> operators). Exceptions connect to error handling patterns you will encounter in later courses and in production C++ code.


Related Terms / Search Tags

std::vector, IntVec, push_back, pop_back, reserve, clear, size vs capacity, pointer invalidation, reallocation, doubling strategy, ifstream, ofstream, fstream, extraction operator, insertion operator, file I/O, logic_error, runtime_error, out_of_range, invalid_argument, bad_alloc, get_even_numbers, separate_even_odd, combine_strings, resizeArray, CSCE 120, Texas A&M, Exam 3