10 Building C++ Programs

A practical progression through C++ program structure, functions, arrays, pointers, classes, debugging, testing, and project organization.

From language features to a complete program

A complete C++ program is easier to understand when each feature has a clear responsibility. A practical beginner structure is:

  1. Headers and declarations provide required library facilities.

  2. Data types and classes describe the data being manipulated.

  3. Functions divide work into named operations.

  4. main coordinates input, processing, and output.

A single program may combine input streams, arrays, loops, conditions, functions, pointers, classes, and assertions. Integration means choosing how these features cooperate, not merely placing them in the same file.

For example, a score-processing program can store scores in a class, read them with a loop, validate an index with an if statement, and calculate a total in a member function. C++ statements include declarations and expressions, selection statements such as if and switch, iteration statements such as for and while, and jump statements such as return, break, and continue.

The central design question is: what responsibility belongs in each part? Input and output should handle interaction, processing should perform calculations and decisions, data types should represent information, and tests should check expected behavior.

Takeaway: Build programs by assigning one focused responsibility to each class, function, and control structure.

Functions, parameters, and responsibilities

A function should normally perform one clear operation. A declaration specifies a function's return type, name, and parameters; a definition supplies its body. Focused functions are easier to understand, reuse, and test.

For example, isValidScore can decide whether one score is acceptable, while countPassing can iterate through scores and return the number meeting a condition. The latter combines an parameter, a loop, an if statement, an accumulator variable, and a returned result.

When a built-in is passed in the form int scores[], the function does not automatically know how many elements exist. Pass the size separately, or use a size-aware type such as std:: or std::vector. A read-only parameter should usually be marked so that the function's promise is visible to both readers and the compiler.

A function that finds the largest element illustrates an important precondition: the caller must provide a valid, nonempty . The function can initialize its result from the first element and then compare each remaining element. If an empty is possible, the interface must define how that case is handled instead of assuming it cannot occur.

Takeaway: Give each function one job, make its inputs and preconditions explicit, and communicate non-modification with .

Arrays, indexing, and iteration

A built-in stores a fixed number of elements of one type. If int values[5] is declared, its valid indices are 00 through 44. A loop must stop before reaching index 55. For example, an indexed loop can print every element by starting at i = 0, continuing while i < 5, and incrementing i after each iteration.

Reading or writing outside an 's bounds is . The result may look correct, be incorrect, or fail unpredictably, so the loop condition is part of the program's correctness rather than a minor detail.

Use an indexed loop when the position matters, such as when comparing an element with a neighboring element or assigning by index. Use a range-based for loop when the program only needs each value; an accumulator can add each value to a running total without referring to an index.

When a function receives an in a -like form, it cannot infer the element count from that parameter alone. Keep the size associated with the data, or choose a container that represents its size more directly.

Takeaway: Match the loop form to the task and make every access stay within the valid index range.

Pointers and object lifetime

A stores the address of an object. The address-of operator & obtains an address, while the dereference operator * accesses the object located there.

For example, a function with an int* number parameter can first check number != nullptr and then use *number *= 2. Calling that function with &value allows it to change the original value, because the function receives the object's address rather than a separate copy.

The null check matters: dereferencing a null is invalid. Pointers and arrays are related in many expressions, but they are not identical. An represents a fixed collection, whereas a is an object capable of holding an address. arithmetic should be used only when the is known to refer to an element of the same or to the position immediately past that .

Object lifetime is equally important. In a best-student search, a function may return a to an element in an existing . That is valid only while the still exists; using it after the 's lifetime ends is invalid. For dynamically allocated memory, modern C++ generally favors containers and smart pointers over manually pairing new and delete, because resource-managing types reduce leaks and lifetime errors.

Takeaway: Check validity, respect the lifetime of the pointed-to object, and prefer abstractions that manage resources automatically.

Classes and controlled interfaces

A class is a user-defined type that can contain data members, member functions, constructors, nested types, and other members. A small class can protect its internal representation and expose operations that preserve valid states.

For example, a Counter can keep its value private and provide increment, reset, and getValue operations. Unrelated code cannot change the private value directly; it must use the public interface. The after getValue() indicates that the member function does not modify the object.

This design demonstrates core object-oriented ideas:

  • groups data and operations while controlling access.

  • Abstraction exposes useful behavior without requiring callers to know implementation details.

  • Composition builds a larger object from smaller objects.

  • Inheritance can express an “is-a” relationship, but it should be used only when that relationship is genuine.

  • Polymorphism lets a common interface work with different object types and is usually introduced after classes and member functions are understood.

For introductory programs, a small class with private data and a few focused public operations is often more useful than a complex inheritance hierarchy.

Takeaway: Use a class to represent a coherent concept, protect its state, and expose meaningful operations rather than raw implementation details.

An integrated search example

The best-student example shows how the pieces cooperate. A Student class encapsulates a name and score. An stores several Student objects. The bestStudent function checks for a null or a nonpositive size, then scans the remaining elements with a loop and condition.

The function returns a to the best element instead of copying the object. The expression best->getScore() accesses a member through a . Returning Student* prevents the result from being used to modify the selected student through that .

A typical flow is:

  1. Construct the objects in an .

  2. Determine the size while the is still available.

  3. Call the search function.

  4. that a result exists when the design requires a nonempty input.

  5. Use the returned object to display its name and score.

The algorithm must also define how ties are handled. In the shown comparison, a later student replaces the current best only when its score is strictly greater, so an equal score does not replace the earlier best. That behavior should be recognized and tested rather than assumed.

The returned is valid only while the remains alive. This lifetime rule is part of the function's contract, just as the handling of null or empty input is part of that contract.

Takeaway: Integration works when data representation, traversal, validation, use, and output each have a deliberate role.

Diagnosing and debugging failures

Good debugging begins by identifying which category of problem occurred:

  1. Compile-time errors include invalid syntax, undeclared names, and incompatible types.

  2. Linker errors occur when a declared or called function has no matching definition in the linked program.

  3. Runtime errors happen while the program executes, such as a crash.

  4. Logic errors occur when the program runs but computes the wrong result.

  5. results from operations for which C++ imposes no requirements, such as out-of-bounds access or invalid dereferencing.

Compile with useful warnings. A typical command for a compiler such as g++ is g++ -std=c++20 -Wall -Wextra -Wpedantic -g main.cpp -o program. The exact warning options vary by compiler, and -g commonly includes debugging information.

Read the first diagnostic carefully because later messages may be consequences of the first error. A productive debugging cycle is:

  1. Reproduce the failure with the smallest input possible.

  2. State what should happen.

  3. Inspect values where the result first becomes incorrect.

  4. Change one thing at a time.

  5. Recompile and rerun relevant tests.

  6. Keep the test that exposed the bug.

A debugger can pause execution, inspect variables, step through functions, and show the call stack. Assertions and temporary output can also help, but deliberate tests are preferable for preserving a permanent check.

Takeaway: Classify the failure first, find where the observed state diverges from the expected state, and change only one factor at a time.

Testing contracts and boundary cases

Tests should state an expected result and cover both ordinary and unusual inputs. For a function that finds the best student, useful cases include:

  • one student;

  • several students with different scores;

  • tied scores;

  • the highest score in the first position;

  • the highest score in the last position;

  • an empty input case, if the design allows it; and

  • invalid scores, if validation is required.

A focused test can call bestStudent with students whose scores are 60, 99, and 72, then use to check that the returned is not null, that the selected name is "B", and that the selected score is 99.

Testing small functions independently makes failures easier to locate. Boundary tests are especially valuable because errors often occur at the first or last valid element, with an empty collection, or at the transition between valid and invalid input.

Assertions are appropriate for assumptions that should hold during development. Expected user mistakes should instead be handled through the program's normal validation and error-reporting paths. Ordinary assertions can be disabled by defining NDEBUG before including <cassert>, so they should not be the only mechanism for handling conditions that must be managed in a finished program.

Takeaway: Test representative, boundary, tied, empty, and invalid cases according to the contract of each function.

Organizing a growing C++ project

As a program grows, separate its interface from its implementation. A project might contain main.cpp, Student.h, Student.cpp, and tests.cpp in a project directory.

A header commonly contains a class declaration, including its data members, constructors, and member-function declarations. The source file contains the corresponding definitions. Include guards, or the modern equivalent #pragma once, prevent a header from being processed repeatedly in one translation unit.

A useful separation of responsibilities is:

  • input and output for interaction with users or files;

  • processing for calculations and decisions;

  • data representation for arrays, classes, and other objects; and

  • testing for checking expected results.

This organization allows processing functions to be tested without repeatedly entering keyboard input. Keep functions short, choose descriptive variable names, initialize objects, avoid unnecessary global variables, and give each class or function a focused task.

As a final design check, ask whether each operation belongs where it is located. Input code should not need to know the internal representation of a class, and a calculation should be testable without depending on interactive output.

Takeaway: Separate interface, implementation, interaction, processing, and tests so that each part can change and be checked independently.