Fundamentals of Programming (C/C++)

Chapter 13
Program Correctness, Testing, Security, and Performance

Bachelors Program in Computer Applications
Fundamentals of Programming (C/C++)   •   Bachelor

Program Correctness, Testing, Security, and Performance

Introduction

A program is not successful merely because it compiles or produces the expected result for one input. A dependable program must satisfy its specification, behave safely for valid and invalid inputs, remain understandable to other developers, and use computing resources appropriately. These concerns are connected: unclear requirements make testing incomplete; unsafe input handling can invalidate correctness; and premature optimization can introduce defects.

In C and C++, the need for disciplined practice is particularly strong. Manual memory management, pointers, arrays, integer conversions, undefined behavior, and concurrency can create failures that are difficult to observe. This chapter presents a practical quality framework based on defensive programming, testing, debugging, static and dynamic analysis, secure coding, standards, and measured optimization.

Learning Objectives

After studying this chapter, you should be able to:

  • distinguish correctness, robustness, reliability, security, and performance;
  • apply defensive programming and design-by-contract ideas;
  • design unit, integration, system, and regression tests;
  • debug systematically rather than by guesswork;
  • use static analysis, assertions, sanitizers, and profilers appropriately;
  • validate input and avoid common C/C++ security vulnerabilities;
  • apply coding standards and review practices;
  • optimize performance using measurements without sacrificing correctness.

1. Correctness as a Property of Behavior

Program correctness means that an implementation conforms to its specified behavior. A specification may describe normal results, permitted inputs, error handling, resource limits, and externally observable effects. Correctness therefore includes more than a successful output.

A useful distinction is:

  • Functional correctness: the program computes the required results.
  • Robustness: it handles abnormal, unexpected, or boundary conditions safely.
  • Reliability: it continues to provide correct service over time and repeated use.
  • Security: it preserves confidentiality, integrity, and availability despite misuse or attack.
  • Maintainability: its structure permits safe modification.
  • Performance: it meets time, memory, throughput, or latency requirements.

These properties can conflict. For example, a faster algorithm that reads beyond an array boundary is not an improvement. Performance is valuable only within the permitted correctness and security conditions.

Preconditions, postconditions, and invariants

A precondition states what must be true before an operation begins. A postcondition states what must be true after successful completion. An invariant is a property that remains true at a particular stage of execution or throughout a data structure's valid lifetime.

For a function that computes an average, a precondition may be that the count is positive. A postcondition may be that the result equals the sum divided by the count. An array-processing loop may maintain the invariant that every element before the current index has already been processed.

Assertions are executable checks of assumptions:

#include <cassert>

int average(const int values[], int count) {
    assert(values != nullptr);
    assert(count > 0);
    long long total = 0;
    for (int i = 0; i < count; ++i) total += values[i];
    return static_cast<int>(total / count);
}

Assertions are primarily for programmer assumptions and development diagnostics. They should not replace validation of untrusted input, because assertions may be disabled in release builds. An invalid user-supplied count should receive controlled error handling, not merely an assertion.

Defensive programming

Defensive programming assumes that inputs, external systems, and even internal assumptions may fail. Its practices include:

  1. validate values, ranges, formats, and relationships;
  2. initialize variables and choose safe defaults;
  3. check return values and allocation results;
  4. keep ownership and lifetime explicit;
  5. avoid dangerous implicit conversions;
  6. fail safely and provide useful diagnostics;
  7. make invalid states difficult to represent.

For example, a file-reading function should distinguish end-of-file, malformed data, and an operating-system error. Returning a plausible default for all three cases conceals failures and can corrupt later computations.

2. Testing as Evidence of Quality

Testing executes a program under selected conditions and compares observed behavior with expected behavior. Testing cannot prove that an unrestricted program has no defects, but well-designed tests provide strong evidence and expose failures early.

The testing levels

LevelMain subjectTypical question
UnitFunction, class, or small moduleDoes this component satisfy its contract?
IntegrationInteracting componentsDo interfaces and data formats agree?
SystemComplete applicationDoes the system meet requirements?
AcceptanceUser or stakeholder scenarioIs the delivered product fit for use?
RegressionPreviously fixed or affected behaviorDid a change break existing behavior?

A test case consists of setup, input, execution, expected result, and cleanup. Tests should be repeatable, independent where possible, and capable of identifying the cause of failure.

Designing effective tests

Testing every possible input is usually infeasible. Instead, select representative and high-risk classes:

  • normal values;
  • minimum and maximum permitted values;
  • values just outside the permitted range;
  • empty collections and single-element collections;
  • duplicate, sorted, and reverse-sorted data;
  • malformed text, unexpected whitespace, and very long input;
  • resource failures and unavailable files;
  • combinations of conditions that interact.

Equivalence partitioning divides inputs into classes expected to behave similarly. Boundary-value analysis emphasizes limits because off-by-one errors commonly occur there. Property-based testing checks general properties, such as “sorting preserves the multiset of elements and produces nondecreasing order,” rather than checking only selected examples.

A test oracle determines what the correct result is. It may be a mathematical calculation, a trusted reference implementation, an invariant, or a business rule. When no simple oracle exists, metamorphic testing checks relationships: if all input values are increased by a constant, for example, a correctly implemented sum should increase by that constant multiplied by the number of values.

Unit-test example

Suppose a function returns the position of a target in an array or -1 if absent:

int findIndex(const int a[], int n, int target);

A useful test set includes n == 0, a one-element array, a target at the first and last positions, repeated targets, and a missing target. The contract must specify whether the first or any matching position is returned. Without that decision, a failing test may reveal ambiguity in the specification rather than an implementation defect.

Tests should be automated where practical. A simple assertion-based test might be:

#include <cassert>

void testFindIndex() {
    int a[] = {4, 7, 9};
    assert(findIndex(a, 3, 4) == 0);
    assert(findIndex(a, 3, 9) == 2);
    assert(findIndex(a, 3, 5) == -1);
}

Production testing frameworks add fixtures, setup and teardown, expressive failure reports, test discovery, mocking, and coverage measurement. Coverage is useful for finding untested code, but high line coverage does not guarantee meaningful assertions or correct requirements.

Integration and regression testing

Integration defects often arise from incompatible assumptions: one function counts bytes while another expects characters; one module uses local time while another uses UTC; or one component transfers ownership while another deletes the same object. Integration tests should exercise real interfaces and representative data formats.

When a defect is fixed, first create a test that reproduces it, then correct the implementation, and finally retain the test as a regression test. This prevents the same failure from returning unnoticed.

3. Debugging and Root-Cause Analysis

Debugging is the controlled process of locating, explaining, and correcting a defect. It is not equivalent to adding random print statements. A disciplined cycle is:

  1. reproduce the failure reliably;
  2. reduce it to the smallest useful input;
  3. observe state at meaningful boundaries;
  4. form a specific hypothesis;
  5. test the hypothesis with a debugger or instrumented build;
  6. identify the root cause, not only the visible symptom;
  7. apply a minimal correction;
  8. run focused and complete regression tests.

A debugger permits breakpoints, step execution, call-stack inspection, variable watches, and conditional stops. Core dumps and stack traces are valuable for crashes. Logging should include event context, relevant identifiers, severity, and timestamps, while avoiding passwords, secret keys, and unnecessary personal data.

A failure may be delayed from its cause. A buffer overflow can corrupt memory and crash later in an unrelated function; an uninitialized value can produce a different result on different builds. Therefore, the crash location is evidence, not necessarily the root cause.

4. Static and Dynamic Analysis

Static analysis examines source code, intermediate representation, or binaries without executing the program. Compilers warn about unreachable code, suspicious conversions, shadowed variables, and some uninitialized uses. Linters and specialized analyzers detect style violations, resource leaks, null dereferences, data races, and security patterns.

Warnings should be treated as quality signals. A project should use a documented warning policy, commonly enabling high warning levels and preventing newly introduced warnings. Suppressions should be narrow and justified rather than globally hiding diagnostics.

Dynamic analysis observes execution. It includes tests, debuggers, memory checkers, sanitizers, race detectors, and profilers. Dynamic tools reveal behavior that depends on actual paths and inputs, but they cannot inspect code paths that tests never execute.

Important C/C++ sanitizers include:

  • AddressSanitizer (ASan): detects many out-of-bounds accesses, use-after-free, and double-free errors;
  • UndefinedBehaviorSanitizer (UBSan): detects selected forms of undefined behavior, such as invalid shifts and signed overflow checks;
  • ThreadSanitizer (TSan): detects many data races in supported environments;
  • MemorySanitizer (MSan): detects uses of uninitialized memory in supported toolchains.

Sanitizers add overhead and are normally used in test or diagnostic builds. They complement, rather than replace, code review and testing. A clean sanitizer run means that tested executions did not trigger the instrumented categories; it is not a universal proof of safety.

5. Secure Input Handling

Input is untrusted until validated. Sources include users, files, environment variables, network messages, command-line arguments, and inter-process communication. Secure input handling has four stages:

  1. Identify the expected type and limits. For example, a quantity may be a nonnegative integer no greater than 10,000.
  2. Parse explicitly. Detect malformed text, overflow, underflow, and trailing unexpected characters.
  3. Validate semantic rules. A syntactically valid date may still be impossible; a valid identifier may not belong to the current user.
  4. Use safely. Preserve length boundaries, encode output for its destination, and avoid constructing commands from raw input.

In C, functions such as gets are unsafe and must never be used. Prefer bounded operations such as fgets, then validate the resulting string. In C++, std::string, std::getline, and standard containers reduce some memory hazards, but they do not automatically validate meaning or prevent denial-of-service through enormous input.

Integer conversion deserves special attention. Signed overflow is undefined behavior in C++, while unsigned arithmetic wraps modulo a power of two. Before computing a size such as count * sizeof(Item), ensure that the multiplication cannot overflow and that the result fits the allocation interface. Prefer containers such as std::vector when appropriate, and use checked conversion between signed and unsigned types.

Common vulnerabilities include:

  • buffer overflow and out-of-bounds access;
  • use-after-free and double-free;
  • format-string vulnerabilities;
  • command or SQL injection;
  • path traversal;
  • accidental exposure of secrets;
  • race conditions and time-of-check/time-of-use errors;
  • denial of service through unbounded work or memory consumption.

Input validation is not a substitute for output encoding. Data safe for plain text may be unsafe when inserted into HTML, a shell command, or a SQL statement. Use parameterized database interfaces and safe library abstractions rather than concatenating commands.

6. Coding Standards, Review, and Maintainability

Coding standards reduce variation and make defects easier to see. A useful standard addresses naming, formatting, initialization, error handling, ownership, comments, exception policy, and forbidden constructs. It should be automated where possible through formatters, linters, pre-commit checks, and continuous integration.

Good comments explain intent, constraints, or non-obvious decisions; they should not merely restate syntax. Functions should have focused responsibilities, limited side effects, and clear interfaces. Resource ownership should be explicit. In modern C++, RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime, reducing leaks and cleanup omissions.

Code review is most effective when reviewers examine requirements, failure paths, boundary conditions, security assumptions, and tests—not only formatting. Review should be respectful and evidence-based. A review checklist can ask:

  • Are all external inputs validated?
  • Are errors propagated or handled deliberately?
  • Can any pointer be null, dangling, or out of bounds?
  • Are ownership and thread-safety assumptions clear?
  • Are tests representative and independent?
  • Does the change affect performance or privacy?

7. Performance Without Sacrificing Correctness

Performance concerns the resources and time required to perform useful work. Relevant measures include elapsed time, CPU time, memory consumption, throughput, latency, and energy. A program may have different performance requirements for average and worst-case behavior.

Algorithmic complexity gives a high-level prediction. For example, scanning a collection of n elements is generally O(n), while comparing every pair is generally O(n²). Complexity describes growth as input size increases; it does not measure actual time on a particular machine and should not replace measurement.

A simple throughput measure is:

throughput = completed operations ÷ elapsed time

If 12,000 operations complete in 3 seconds, throughput is 4,000 operations per second. The relationship assumes the operations and time interval are measured consistently. Latency, the time for one operation, may be more important than average throughput in interactive systems.

Profiling and optimization

Profiling identifies where execution time or memory is actually spent. Sampling profilers periodically record the active call stack; instrumentation records function events but can add greater overhead. Heap profilers investigate allocation behavior. Measure a representative workload, establish a baseline, change one significant factor, and compare using repeated runs.

A sound optimization process is:

  1. define the performance requirement;
  2. create correctness tests and a representative benchmark;
  3. profile to locate a bottleneck;
  4. select an optimization with a reasoned hypothesis;
  5. implement the smallest useful change;
  6. rerun correctness, security, and performance tests;
  7. document the trade-off.

Common improvements include choosing a better algorithm, reducing unnecessary copying, reserving container capacity, improving data locality, batching I/O, and avoiding repeated expensive parsing. Compiler optimization should be enabled in performance builds, but optimization can expose undefined behavior that happened to remain hidden in an unoptimized build. Correct code must remain correct under all permitted compiler settings.

Optimization has limits. If a fraction p of execution time is improved by a factor s, the ideal overall speedup is:

S = 1 ÷ ((1 − p) + p ÷ s)

Here, 0 ≤ p ≤ 1, s ≥ 1, and the formula assumes the improved and unimproved portions are measured consistently with negligible added overhead. If 80% of execution time is improved by a factor of 4, then S = 1 ÷ (0.2 + 0.8 ÷ 4) = 1 ÷ 0.4 = 2.5. Thus the complete program is ideally 2.5 times faster, not four times faster. This illustrates why profiling and attention to the dominant cost matter.

8. An Integrated Quality Workflow

Quality activities are strongest when integrated into the development lifecycle:

Integrated program quality workflow Specifycontracts, risks Implementsafe interfaces Analyzereview, static tools Test and measuretests, sanitizers, profile feedback, correction, regression testing, monitored release
Integrated workflow from specification to monitored release
Figure: Integrated workflow from specification to monitored release

Figure: Quality activities form a feedback loop rather than a one-time final inspection.

Continuous integration can compile multiple configurations, run unit and integration tests, apply static analysis, execute sanitizer builds, and preserve artifacts. Release gates should be based on risk: a safety-critical component requires stronger evidence than a disposable prototype. After deployment, monitoring should detect failures, unusual resource use, and security events without collecting unnecessary personal information.

9. Illustrative Case: A Student Enrollment Utility

Background. A university develops a C++ utility that imports enrollment records from a comma-separated file and calculates the number of students in each course.

Problem. The first version assumes every line has three fields, stores names in fixed-size character arrays, converts course capacity with unchecked atoi, and reports only the final total. Long names corrupt memory; malformed capacities become zero; and a missing file is indistinguishable from an empty file.

Analysis. The team writes a specification: each record must contain an identifier, a course code, and a nonnegative integer capacity within a defined limit. Blank lines may be ignored, but malformed records must be reported with line numbers. The import operation must not partially publish results as if the file were valid.

Approach. The team uses std::string and bounded line input, parses fields explicitly, checks conversion errors and range limits, and stores records in a vector. A temporary result is built and committed only after the complete file passes validation. Unit tests cover empty files, valid one-record files, duplicate course codes, malformed separators, negative and oversized capacities, maximum line length, and missing files. Integration tests use real file streams. Static analysis and ASan are run in continuous integration; profiling is performed only after correctness is established.

Outcome. The revised utility rejects malformed input with actionable diagnostics, avoids the memory corruption, and makes file errors visible. Profiling reveals that repeated linear searches dominate processing for large files, so a hash map is introduced and validated against the original implementation on test data. The change improves performance while retaining a reference-based correctness check.

Learning points. Clear contracts expose ambiguous behavior; validation must address both syntax and meaning; tools complement rather than replace tests; and optimization should follow measurement and preserve a trusted correctness oracle.

10. Challenges, Ethics, and Contemporary Practice

No tool detects every defect. Static analysis may produce false positives; dynamic analysis depends on exercised paths; tests may encode an incorrect specification; profilers can perturb timing; and security controls may reduce convenience or performance. Teams must interpret evidence rather than treat any single metric as proof.

Organizations should protect personal data in logs and test datasets, restrict access to diagnostic artifacts, and disclose vulnerabilities responsibly. Supply-chain security is increasingly important: dependencies should be inventoried, updated, verified where feasible, and evaluated for licensing and known vulnerabilities. Reproducible builds, dependency scanning, fuzz testing, and automated mutation testing are contemporary extensions of traditional quality practice.

AI-assisted coding can generate tests, explain diagnostics, or suggest optimizations, but generated code must undergo human review, compilation, testing, security analysis, and license and privacy checks. An AI suggestion is a hypothesis, not evidence of correctness.

Best-Practice Checklist

  • Write a contract before implementing significant behavior.
  • Validate all untrusted input, including size and semantic constraints.
  • Enable strong compiler warnings and review every new warning.
  • Use unit, integration, regression, and risk-based system tests.
  • Run sanitizers and static analysis in automated builds.
  • Keep ownership, lifetime, and error handling explicit.
  • Reproduce and minimize failures before changing code.
  • Profile representative workloads before optimizing.
  • Re-run correctness and security tests after performance changes.
  • Record assumptions, limitations, and unresolved risks.

Chapter Summary

Program quality is multidimensional: correctness, robustness, reliability, security, maintainability, and performance must be considered together. Preconditions, postconditions, invariants, assertions, and defensive programming make assumptions visible. Unit, integration, system, acceptance, and regression tests provide progressively broader evidence, while boundary analysis, equivalence classes, property-based tests, and meaningful oracles improve test value.

Debugging should proceed from reproducible failure to root cause. Static analysis, dynamic analysis, sanitizers, code review, and continuous integration detect different classes of defects and are most effective in combination. Secure programs parse and validate untrusted input, control resource use, preserve memory safety, and use context-appropriate safe interfaces. Coding standards improve consistency and maintainability.

Performance work must be measurement-driven. Complexity analysis predicts scalability, profiling identifies actual bottlenecks, and speedup is limited by the portion of work that remains unchanged. The central professional principle is simple: optimize only after establishing correctness, and verify correctness again after optimization.

Key Terms

Correctness: Conformance of program behavior to its specification.

Precondition: Requirement that must hold before an operation begins.

Postcondition: Property expected after successful completion.

Invariant: Property maintained during a computation or data structure lifetime.

Defensive programming: Designing software to detect and safely handle invalid conditions.

Unit test: Test of a small isolated component.

Integration test: Test of interactions among components.

Regression test: Test retained to prevent a previously fixed defect from returning.

Test oracle: Method of determining the expected result.

Static analysis: Analysis performed without executing the program.

Dynamic analysis: Analysis based on observing program execution.

Sanitizer: Instrumentation tool that detects selected runtime errors.

Undefined behavior: Execution for which the language standard imposes no requirements.

Input validation: Checking input format, range, and meaning before use.

Fuzz testing: Automated testing with many generated or mutated inputs.

Profiling: Measurement of where time, memory, or other resources are consumed.

Throughput: Completed work per unit of elapsed time.

Latency: Time required to complete an operation.

RAII: C++ technique that binds resource management to object lifetime.

Technical debt: Future cost created by expedient but inadequate design or implementation decisions.

Review Questions

Short Answer Questions

  1. Distinguish functional correctness from robustness.
  2. What are preconditions, postconditions, and invariants?
  3. Why does high code coverage not guarantee high software quality?
  4. State two differences between static and dynamic analysis.
  5. Why should profiling precede most performance optimizations?

Long Answer Questions

  1. Explain how boundary-value analysis and equivalence partitioning can be applied to a function that accepts an integer quantity from 1 to 500.
  2. Describe a disciplined debugging workflow for a program that crashes only on large inputs.
  3. Compare unit, integration, system, acceptance, and regression testing, giving an example of each.
  4. Discuss the role and limitations of ASan, UBSan, TSan, and static analysis in C++ development.
  5. Explain how secure input handling, ownership management, and coding standards reduce vulnerabilities in C/C++ programs.

Analytical / Application Questions

  1. A program spends 70% of its execution time in a parser. An optimization makes the parser five times faster. Using the speedup equation, calculate the ideal overall speedup and explain why it is less than five.
  2. Design a test strategy for a file-import function that accepts records with a name, course code, and nonnegative capacity. Include normal, boundary, malformed, security, and integration tests.
  3. A benchmark shows that a new data structure is faster, but two regression tests fail. Decide whether the change should be accepted and propose a process for investigating the failures.

Multiple Choice Questions with Answers

Question 1

Which statement best describes an invariant?

A. A value supplied by an external user

B. A property that remains true at a defined stage of computation

C. A compiler optimization setting

D. A test that runs only after deployment

Correct Answer: B. A property that remains true at a defined stage of computation

Question 2

Which testing level primarily examines interactions between modules?

A. Unit testing

B. Integration testing

C. Acceptance testing

D. Mutation testing

Correct Answer: B. Integration testing

Question 3

Why should assertions not be the sole method for validating user input?

A. Assertions always slow programs permanently

B. Assertions cannot check Boolean expressions

C. Assertions may be disabled in release builds

D. Assertions are only available in C, not C++

Correct Answer: C. Assertions may be disabled in release builds

Question 4

Which tool is primarily intended to detect many out-of-bounds and use-after-free errors?

A. AddressSanitizer

B. A formatter

C. A linker

D. A code generator

Correct Answer: A. AddressSanitizer

Question 5

What is the safest general approach for database input?

A. Concatenate raw input into SQL text

B. Ignore all punctuation

C. Use parameterized database interfaces

D. Convert all input to uppercase

Correct Answer: C. Use parameterized database interfaces

Question 6

A program completes 12,000 operations in 3 seconds. What is its throughput?

A. 400 operations per second

B. 4,000 operations per second

C. 9,000 operations per second

D. 36,000 operations per second

Correct Answer: B. 4,000 operations per second

Question 7

In the speedup equation, what does p represent?

A. The fraction of execution time improved

B. The number of processors

C. The program's memory usage

D. The probability of a test failure

Correct Answer: A. The fraction of execution time improved

Question 8

Which activity should generally occur before optimization?

A. Deleting regression tests

B. Disabling compiler warnings

C. Profiling a representative workload

D. Replacing all data structures

Correct Answer: C. Profiling a representative workload

Question 9

Which practice most directly supports regression prevention?

A. Removing a test after it passes

B. Keeping a test that reproduces a fixed defect

C. Testing only the user interface

D. Ignoring failures in diagnostic builds

Correct Answer: B. Keeping a test that reproduces a fixed defect

Question 10

What is a central purpose of defensive programming?

A. To make every program use the maximum memory available

B. To conceal errors from users and developers

C. To detect invalid conditions and fail safely

D. To eliminate the need for specifications

Correct Answer: C. To detect invalid conditions and fail safely

Fundamentals of Programming (C/C++)
Chapter 13 of 15