Function contracts, testing, and array foundations

Computer Science II · Lecture 3 ·

Array values 8, 3, 6, 1, and 5 occupy indices zero through four.
An array stores a fixed number of elements. Valid indices begin at zero and end at size minus one.

A program can contain a correct-looking function and still fail because the caller provides the wrong input or the function visits the wrong positions in an array. A contract says what the inputs must mean. Tests check whether the implementation keeps its promise. Array bounds give those promises a concrete place to start.

Review CS1: arrays and active counts for the basic storage model, and CS1: functions for calls and parameters. Here the goal is to connect those ideas to a repeatable way of checking a small piece of code.

State the requirements before testing #

A precondition is a requirement that must hold before a call. An array-access function might require a valid index. A division helper might require a nonzero divisor. A postcondition says what is true afterward, such as the returned value or the changes permitted to an argument.

These statements help locate a failure. If a function promises the sum of four valid elements but adds only three, its implementation broke the promise. If the caller claims that a two-element array has four readable elements, the input violates the requirement. Writing down the responsibility on each side makes the problem easier to explain and correct.

A driver is a small program that calls a function using chosen inputs. It is useful when you want to check one operation without navigating an entire application. A stub temporarily stands in for an unfinished function with a simple response. It can let you test how other pieces fit together, but a stub's successful response does not prove that the missing calculation works.

Functions can call helper functions. Test a helper's own contract, then test the operation that combines its results. A failure in the combined result should lead you to inspect the values crossing each function boundary, not immediately rewrite every component.

Count elements, then identify valid indexes #

A fixed built-in array contains a selected number of same-type elements. int values[4] {}; creates four integers and initializes them to zero. The four valid indexes are zero, one, two, and three. Four is the element count, not the final index.[1]

An index tells you where an element is stored. Its value tells you what is stored there. Keeping these separate matters when an element itself happens to equal its index or the array size.

int values[4] {2, 4, 6, 8};
int total = 0;
for (int i = 0; i < 4; ++i) {
    total += values[i];
}

The initializer places two at index zero, four at index one, six at index two, and eight at index three. The accumulator total begins at zero. The loop begins with i equal to zero, checks its condition, runs the body, then increments i before checking again.

Trace the traversal without skipping the condition #

At index zero, the body adds two, so total becomes two. At index one, it adds four, making total six. At index two, it adds six, making total twelve. At index three, it adds eight, making total twenty.

The update then makes i equal to four. The condition i < 4 is false, so the body does not run with that index. That last condition check is part of why the loop is safe. If the condition were i <= 4, the body would also attempt to read index four, a fifth element that this array does not contain.

Built-in indexing performs no automatic bounds check. An out-of-bounds access is undefined behavior. A run that prints a plausible number does not make it safe: the language has not promised a useful result from the invalid access.

Initialization and logical size #

Initialization supplies known starting values. Reading a local integer element that was never initialized is not a way to discover whether data has been stored there. Keep an explicit count of meaningful inputs instead.

A partially filled array has both an allocated capacity and an active count. If capacity is eight and only three values have been accepted, a sum should visit those three values. Zero-initializing the remaining slots can make a mistaken sum appear harmless, but a mean divided by eight would still be wrong. The loop must use the correct logical range.

Forward traversal visits indexes in increasing order. Reverse traversal begins at the last valid index and works backward. An empty collection has no last active element. Unsigned counters also need care: subtracting one from zero wraps to a large unsigned value rather than becoming negative. A named size constant helps prevent different loops from accidentally using different bounds.

Build tests around boundaries #

Test a single active element and confirm that its value is returned unchanged by a sum. Test multiple elements with a hand-calculated answer. Test the first and last valid positions. If empty input is permitted, define its outcome explicitly; do not let a later average divide by zero.

A helpful test distinguishes an implementation mistake from a lucky result. For example, using only zeros may fail to reveal a skipped element. Using different nonzero values makes it easier to identify which position was omitted or counted twice.

Practice and explanation #

An array contains four values, but a loop stops while i is less than three. Which element is missed? Index three, the fourth element. The loop visits only zero, one, and two.

Why is changing the condition to less than or equal to four not a fix? It adds an invalid fifth access. The intended full traversal uses four as the exclusive upper bound: all indexes below the count, and none equal to it.

References

  1. ↑ C++ working draft: arrays .