Coming Soon

This lesson is currently being developed

Finding issues before they become problems

Learn preventive programming techniques.

Debugging C++ Programs
Chapter
Beginner
Difficulty
30min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

3.10 — Finding issues before they become problems

In this lesson, you'll learn preventive debugging techniques that help you catch issues before they become bugs. This proactive approach saves significant time and effort compared to reactive debugging after problems occur.

The philosophy of defensive programming

Defensive programming is the practice of writing code that anticipates and handles potential problems before they cause failures. The core principles are:

  1. Assume inputs will be invalid
  2. Expect the unexpected
  3. Make failures obvious and early
  4. Document assumptions and invariants
  5. Use tools to catch problems automatically

Think of it as wearing a seatbelt - you hope you'll never need it, but you're prepared just in case.

Input validation and bounds checking

Always validate inputs to prevent problems downstream.

#include <iostream>
#include <vector>
#include <stdexcept>

class SafeArrayOperations
{
public:
    // Unsafe version - problems waiting to happen
    static int unsafeGetElement(const std::vector<int>& arr, size_t index)
    {
        return arr[index];  // No bounds checking - will crash on invalid index
    }
    
    // Safe version - validates inputs
    static int safeGetElement(const std::vector<int>& arr, size_t index)
    {
        // Input validation prevents problems
        if (arr.empty())
        {
            throw std::invalid_argument("Cannot access element of empty array");
        }
        
        if (index >= arr.size())
        {
            throw std::out_of_range("Index " + std::to_string(index) + 
                                  " is out of bounds for array of size " + 
                                  std::to_string(arr.size()));
        }
        
        return arr[index];
    }
    
    // Even safer - returns optional result instead of throwing
    static std::pair<bool, int> tryGetElement(const std::vector<int>& arr, size_t index)
    {
        if (arr.empty() || index >= arr.size())
        {
            return {false, 0};  // Indicates failure
        }
        
        return {true, arr[index]};  // Indicates success with value
    }
};

void demonstrateInputValidation()
{
    std::cout << "=== Input Validation Demo ===" << std::endl;
    
    std::vector<int> numbers = {10, 20, 30, 40, 50};
    std::vector<int> empty;
    
    // Test with valid input
    try
    {
        int value = SafeArrayOperations::safeGetElement(numbers, 2);
        std::cout << "Valid access: numbers[2] = " << value << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "Error: " << e.what() << std::endl;
    }
    
    // Test with invalid index
    try
    {
        int value = SafeArrayOperations::safeGetElement(numbers, 10);
        std::cout << "This shouldn't print" << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "Caught expected error: " << e.what() << std::endl;
    }
    
    // Test with empty array
    try
    {
        int value = SafeArrayOperations::safeGetElement(empty, 0);
        std::cout << "This shouldn't print either" << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "Caught expected error: " << e.what() << std::endl;
    }
    
    // Test safer approach
    auto [success, value] = SafeArrayOperations::tryGetElement(numbers, 2);
    if (success)
    {
        std::cout << "Safe access succeeded: " << value << std::endl;
    }
    else
    {
        std::cout << "Safe access failed" << std::endl;
    }
}

int main()
{
    demonstrateInputValidation();
    return 0;
}

Output:

=== Input Validation Demo ===
Valid access: numbers[2] = 30
Caught expected error: Index 10 is out of bounds for array of size 5
Caught expected error: Cannot access element of empty array
Safe access succeeded: 30

Assertions for catching logic errors

Use assertions to document and verify your assumptions.

#include <iostream>
#include <cassert>
#include <cmath>

class GeometryCalculator
{
public:
    // Calculate area of triangle using Heron's formula
    static double triangleArea(double a, double b, double c)
    {
        // Document and verify preconditions
        assert(a > 0 && "Side 'a' must be positive");
        assert(b > 0 && "Side 'b' must be positive");
        assert(c > 0 && "Side 'c' must be positive");
        
        // Triangle inequality - sides must be able to form a triangle
        assert(a + b > c && "Triangle inequality violated: a + b > c");
        assert(a + c > b && "Triangle inequality violated: a + c > b");
        assert(b + c > a && "Triangle inequality violated: b + c > a");
        
        double s = (a + b + c) / 2.0;  // Semi-perimeter
        double area = std::sqrt(s * (s - a) * (s - b) * (s - c));
        
        // Post-condition - area should be positive
        assert(area > 0 && "Calculated area must be positive");
        
        return area;
    }
    
    // Calculate circle area with invariant checking
    static double circleArea(double radius)
    {
        // Precondition
        assert(radius >= 0 && "Radius cannot be negative");
        
        const double PI = 3.14159265359;
        double area = PI * radius * radius;
        
        // Post-condition
        assert(area >= 0 && "Area cannot be negative");
        
        // Invariant - area should increase with radius
        if (radius > 0)
        {
            assert(area > 0 && "Area should be positive for positive radius");
        }
        
        return area;
    }
};

void demonstrateAssertions()
{
    std::cout << "=== Assertions Demo ===" << std::endl;
    
    // Valid triangle
    double area1 = GeometryCalculator::triangleArea(3.0, 4.0, 5.0);
    std::cout << "Triangle (3,4,5) area: " << area1 << std::endl;
    
    // Valid circle
    double area2 = GeometryCalculator::circleArea(5.0);
    std::cout << "Circle (r=5) area: " << area2 << std::endl;
    
    // Uncomment these to see assertions fail:
    // GeometryCalculator::triangleArea(-1.0, 2.0, 3.0);  // Negative side
    // GeometryCalculator::triangleArea(1.0, 2.0, 10.0);  // Triangle inequality violated
    // GeometryCalculator::circleArea(-5.0);              // Negative radius
}

// Custom assertion macro with more information
#ifdef DEBUG
    #define REQUIRE(condition, message) \
        if (!(condition)) { \
            std::cout << "ASSERTION FAILED: " << message << std::endl; \
            std::cout << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
            std::cout << "Expression: " << #condition << std::endl; \
            abort(); \
        }
#else
    #define REQUIRE(condition, message) // Nothing in release builds
#endif

// Enable debug mode for this example
#define DEBUG

void demonstrateCustomAssertions()
{
    std::cout << "\n=== Custom Assertions Demo ===" << std::endl;
    
    int score = 85;
    
    REQUIRE(score >= 0, "Score cannot be negative");
    REQUIRE(score <= 100, "Score cannot exceed 100");
    
    std::string grade = (score >= 90) ? "A" : 
                       (score >= 80) ? "B" : 
                       (score >= 70) ? "C" : 
                       (score >= 60) ? "D" : "F";
    
    REQUIRE(!grade.empty(), "Grade must be assigned");
    
    std::cout << "Score " << score << " receives grade: " << grade << std::endl;
    
    // Uncomment to see custom assertion fail:
    // REQUIRE(score > 100, "This will fail and show detailed information");
}

int main()
{
    demonstrateAssertions();
    demonstrateCustomAssertions();
    return 0;
}

Output:

=== Assertions Demo ===
Triangle (3,4,5) area: 6
Circle (r=5) area: 78.5398

=== Custom Assertions Demo ===
Score 85 receives grade: B

Error handling strategies

Implement robust error handling to gracefully manage problems.

#include <iostream>
#include <fstream>
#include <string>
#include <optional>
#include <system_error>

class FileProcessor
{
public:
    // Poor error handling - problems hidden
    static void badFileProcessing(const std::string& filename)
    {
        std::ifstream file(filename);
        
        // No error checking!
        std::string line;
        while (std::getline(file, line))
        {
            processLine(line);  // May fail silently
        }
        
        // File may not have been opened, but we don't know
    }
    
    // Better error handling - explicit error checking
    static bool goodFileProcessing(const std::string& filename, std::string& errorMessage)
    {
        std::ifstream file(filename);
        
        if (!file.is_open())
        {
            errorMessage = "Failed to open file: " + filename;
            return false;
        }
        
        std::string line;
        int lineNumber = 0;
        
        while (std::getline(file, line))
        {
            lineNumber++;
            
            if (!processLine(line))
            {
                errorMessage = "Failed to process line " + std::to_string(lineNumber) + ": " + line;
                return false;
            }
        }
        
        if (file.bad())  // Check for read errors
        {
            errorMessage = "Error reading from file: " + filename;
            return false;
        }
        
        return true;  // Success
    }
    
    // Modern C++ approach using std::optional
    static std::optional<std::vector<std::string>> readLines(const std::string& filename)
    {
        std::ifstream file(filename);
        
        if (!file.is_open())
        {
            std::cout << "Warning: Could not open file " << filename << std::endl;
            return std::nullopt;  // Indicates failure
        }
        
        std::vector<std::string> lines;
        std::string line;
        
        while (std::getline(file, line))
        {
            lines.push_back(line);
        }
        
        if (file.bad())
        {
            std::cout << "Warning: Error reading from file " << filename << std::endl;
            return std::nullopt;
        }
        
        return lines;  // Success - contains the data
    }
    
private:
    static bool processLine(const std::string& line)
    {
        // Simulate line processing that might fail
        if (line.empty())
        {
            return false;  // Empty lines are considered errors for this example
        }
        
        std::cout << "Processed: " << line << std::endl;
        return true;
    }
};

void demonstrateErrorHandling()
{
    std::cout << "=== Error Handling Demo ===" << std::endl;
    
    // Test with non-existent file
    std::string errorMsg;
    bool success = FileProcessor::goodFileProcessing("nonexistent.txt", errorMsg);
    
    if (!success)
    {
        std::cout << "File processing failed: " << errorMsg << std::endl;
    }
    else
    {
        std::cout << "File processing succeeded" << std::endl;
    }
    
    // Test with optional return
    auto maybeLines = FileProcessor::readLines("nonexistent.txt");
    
    if (maybeLines.has_value())
    {
        std::cout << "Read " << maybeLines->size() << " lines" << std::endl;
        for (const auto& line : *maybeLines)
        {
            std::cout << "Line: " << line << std::endl;
        }
    }
    else
    {
        std::cout << "Failed to read file" << std::endl;
    }
}

int main()
{
    demonstrateErrorHandling();
    return 0;
}

Output:

=== Error Handling Demo ===
File processing failed: Failed to open file: nonexistent.txt
Warning: Could not open file nonexistent.txt
Failed to read file

Code review and static analysis

Use systematic code review to catch issues before runtime.

#include <iostream>
#include <vector>
#include <memory>

// Code with various potential issues - practice spotting them!
class PotentialIssuesExample
{
public:
    // Issue 1: Memory management problems
    static int* createArray(size_t size)
    {
        if (size == 0)
            return nullptr;  // Good: handle edge case
        
        int* arr = new int[size];  // Issue: raw pointer, potential memory leak
        
        for (size_t i = 0; i <= size; ++i)  // Issue: off-by-one error (should be i < size)
        {
            arr[i] = static_cast<int>(i);
        }
        
        return arr;  // Issue: caller must remember to delete[]
    }
    
    // Issue 2: Integer overflow
    static int multiply(int a, int b)
    {
        return a * b;  // Issue: no overflow checking
    }
    
    // Issue 3: Unsafe string operations
    static void unsafeCopyString(const char* source, char* dest, size_t destSize)
    {
        // Issue: no null pointer checking
        // Issue: no bounds checking
        strcpy(dest, source);  // Deprecated function - use strcpy_s or string class
    }
    
    // Improved versions addressing the issues:
    
    // Fix 1: Use smart pointers and proper bounds
    static std::unique_ptr<int[]> createArraySafe(size_t size)
    {
        if (size == 0)
            return nullptr;
        
        auto arr = std::make_unique<int[]>(size);
        
        for (size_t i = 0; i < size; ++i)  // Fixed: proper bounds
        {
            arr[i] = static_cast<int>(i);
        }
        
        return arr;  // Automatic cleanup when unique_ptr goes out of scope
    }
    
    // Fix 2: Overflow detection
    static bool multiplySafe(int a, int b, int& result)
    {
        // Check for overflow before performing operation
        if (a != 0 && b != 0)
        {
            if (a > 0 && b > 0 && a > INT_MAX / b)
                return false;  // Positive overflow
            if (a > 0 && b < 0 && b < INT_MIN / a)
                return false;  // Negative overflow
            if (a < 0 && b > 0 && a < INT_MIN / b)
                return false;  // Negative overflow
            if (a < 0 && b < 0 && a < INT_MAX / b)
                return false;  // Positive overflow
        }
        
        result = a * b;
        return true;
    }
    
    // Fix 3: Safe string operations
    static bool copyStringSafe(const std::string& source, std::string& dest)
    {
        try
        {
            dest = source;  // std::string handles memory management
            return true;
        }
        catch (const std::exception& e)
        {
            std::cout << "String copy failed: " << e.what() << std::endl;
            return false;
        }
    }
};

// Code review checklist demonstration
void demonstrateCodeReview()
{
    std::cout << "=== Code Review Checklist Demo ===" << std::endl;
    
    // Test the improved versions
    auto safeArray = PotentialIssuesExample::createArraySafe(5);
    if (safeArray)
    {
        std::cout << "Safe array created. Values: ";
        for (int i = 0; i < 5; ++i)
        {
            std::cout << safeArray[i] << " ";
        }
        std::cout << std::endl;
    }
    
    // Test safe multiplication
    int result;
    bool success = PotentialIssuesExample::multiplySafe(1000000, 1000000, result);
    if (success)
    {
        std::cout << "Multiplication result: " << result << std::endl;
    }
    else
    {
        std::cout << "Multiplication would overflow" << std::endl;
    }
    
    // Test safe string copy
    std::string source = "Hello, World!";
    std::string dest;
    if (PotentialIssuesExample::copyStringSafe(source, dest))
    {
        std::cout << "String copied: " << dest << std::endl;
    }
}

/*
Code Review Checklist:

MEMORY MANAGEMENT:
□ Are all new/malloc matched with delete/free?
□ Are smart pointers used instead of raw pointers?
□ Are there potential memory leaks?
□ Is there proper RAII implementation?

BOUNDS CHECKING:
□ Are array/vector accesses within bounds?
□ Are loop conditions correct (< vs <=)?
□ Are off-by-one errors avoided?
□ Is integer overflow/underflow handled?

ERROR HANDLING:
□ Are return values checked?
□ Are exceptions handled appropriately?
□ Are error conditions documented?
□ Is graceful degradation implemented?

INPUT VALIDATION:
□ Are function parameters validated?
□ Are null pointers checked?
□ Are preconditions documented?
□ Are edge cases handled?

CONST CORRECTNESS:
□ Are parameters that shouldn't change marked const?
□ Are member functions that don't modify marked const?
□ Are return types appropriately const?

RESOURCE MANAGEMENT:
□ Are files properly closed?
□ Are network connections cleaned up?
□ Are temporary resources freed?
□ Is exception safety maintained?
*/

int main()
{
    demonstrateCodeReview();
    return 0;
}

Testing strategies

Implement comprehensive testing to catch issues early.

#include <iostream>
#include <vector>
#include <cassert>

class Calculator
{
public:
    static double divide(double numerator, double denominator)
    {
        if (denominator == 0.0)
            throw std::invalid_argument("Division by zero");
        
        return numerator / denominator;
    }
    
    static int factorial(int n)
    {
        if (n < 0)
            throw std::invalid_argument("Factorial of negative number");
        
        if (n <= 1)
            return 1;
        
        return n * factorial(n - 1);
    }
    
    static double sqrt_approx(double x)
    {
        if (x < 0)
            throw std::invalid_argument("Square root of negative number");
        
        if (x == 0)
            return 0;
        
        // Simple Newton-Raphson approximation
        double guess = x / 2.0;
        for (int i = 0; i < 10; ++i)
        {
            guess = (guess + x / guess) / 2.0;
        }
        
        return guess;
    }
};

class TestFramework
{
public:
    static void assertEqual(double actual, double expected, double tolerance, const std::string& testName)
    {
        if (std::abs(actual - expected) <= tolerance)
        {
            std::cout << "✓ PASS: " << testName << std::endl;
        }
        else
        {
            std::cout << "✗ FAIL: " << testName << " - Expected " << expected 
                      << ", got " << actual << std::endl;
        }
    }
    
    static void assertException(std::function<void()> operation, const std::string& testName)
    {
        try
        {
            operation();
            std::cout << "✗ FAIL: " << testName << " - Expected exception but none was thrown" << std::endl;
        }
        catch (const std::exception& e)
        {
            std::cout << "✓ PASS: " << testName << " - Caught expected exception: " << e.what() << std::endl;
        }
    }
};

void runComprehensiveTests()
{
    std::cout << "=== Comprehensive Testing Demo ===" << std::endl;
    
    // Test normal cases
    TestFramework::assertEqual(Calculator::divide(10.0, 2.0), 5.0, 0.001, "divide(10, 2)");
    TestFramework::assertEqual(Calculator::factorial(5), 120, 0, "factorial(5)");
    TestFramework::assertEqual(Calculator::sqrt_approx(9.0), 3.0, 0.001, "sqrt_approx(9)");
    
    // Test edge cases
    TestFramework::assertEqual(Calculator::divide(0.0, 5.0), 0.0, 0.001, "divide(0, 5)");
    TestFramework::assertEqual(Calculator::factorial(0), 1, 0, "factorial(0)");
    TestFramework::assertEqual(Calculator::sqrt_approx(0.0), 0.0, 0.001, "sqrt_approx(0)");
    
    // Test boundary conditions
    TestFramework::assertEqual(Calculator::factorial(1), 1, 0, "factorial(1)");
    TestFramework::assertEqual(Calculator::sqrt_approx(1.0), 1.0, 0.001, "sqrt_approx(1)");
    
    // Test error conditions
    TestFramework::assertException([]() { Calculator::divide(1.0, 0.0); }, "divide by zero");
    TestFramework::assertException([]() { Calculator::factorial(-1); }, "factorial of negative");
    TestFramework::assertException([]() { Calculator::sqrt_approx(-1.0); }, "sqrt of negative");
    
    // Test large values
    TestFramework::assertEqual(Calculator::sqrt_approx(100.0), 10.0, 0.001, "sqrt_approx(100)");
    
    // Test floating-point precision
    TestFramework::assertEqual(Calculator::sqrt_approx(2.0), 1.414, 0.01, "sqrt_approx(2) approximation");
}

// Property-based testing example
void runPropertyTests()
{
    std::cout << "\n=== Property-Based Testing Demo ===" << std::endl;
    
    // Property: sqrt(x)² should equal x (within tolerance)
    std::vector<double> testValues = {1.0, 4.0, 9.0, 16.0, 25.0, 0.5, 2.5, 10.5};
    
    for (double x : testValues)
    {
        double sqrtX = Calculator::sqrt_approx(x);
        double squared = sqrtX * sqrtX;
        
        TestFramework::assertEqual(squared, x, 0.001, 
            "Property test: sqrt(" + std::to_string(x) + ")² = " + std::to_string(x));
    }
    
    // Property: factorial(n) should be greater than factorial(n-1) for n > 1
    for (int n = 2; n <= 7; ++n)
    {
        int factN = Calculator::factorial(n);
        int factNMinus1 = Calculator::factorial(n - 1);
        
        if (factN > factNMinus1)
        {
            std::cout << "✓ PASS: factorial(" << n << ") > factorial(" << (n-1) << ")" << std::endl;
        }
        else
        {
            std::cout << "✗ FAIL: factorial property violated" << std::endl;
        }
    }
}

int main()
{
    runComprehensiveTests();
    runPropertyTests();
    return 0;
}

Compiler warnings and static analysis

Enable and use compiler warnings to catch potential issues.

// Compile with: g++ -Wall -Wextra -Wpedantic -Werror filename.cpp
#include <iostream>

void demonstrateCompilerWarnings()
{
    std::cout << "=== Compiler Warnings Demo ===" << std::endl;
    
    // These would generate warnings with proper compiler flags:
    
    // Warning: unused variable
    int unusedVariable = 42;
    
    // Warning: comparison between signed and unsigned
    std::vector<int> vec = {1, 2, 3};
    for (int i = 0; i < vec.size(); ++i)  // vec.size() is unsigned
    {
        // Warning: format string doesn't match arguments
        // printf("Value: %d\n", vec[i], "extra argument");
    }
    
    // Warning: missing return statement
    // int badFunction() {
    //     // Missing return statement
    // }
    
    // Warning: potential null pointer dereference
    int* ptr = nullptr;
    // *ptr = 5;  // This would cause undefined behavior
    
    std::cout << "If compiled with warnings enabled, this code would show issues" << std::endl;
}

/*
Useful compiler flags for catching issues:

-Wall              Enable common warnings
-Wextra            Enable extra warnings  
-Wpedantic         ISO C++ compliance warnings
-Werror            Treat warnings as errors
-Wshadow           Warn about variable shadowing
-Wunused           Warn about unused variables
-Wuninitialized    Warn about uninitialized variables
-Wconversion       Warn about type conversions
-Wsign-compare     Warn about signed/unsigned comparisons
-Wformat=2         Extra format string checking

Static analysis tools:
- Clang Static Analyzer
- PVS-Studio  
- PC-lint Plus
- Cppcheck
- SonarQube
*/

int main()
{
    demonstrateCompilerWarnings();
    return 0;
}

Documentation and code contracts

Document your assumptions and contracts clearly.

#include <iostream>
#include <vector>
#include <cassert>

/**
 * @brief Calculates the average of positive numbers in an array
 * @param numbers Input array of integers
 * @pre numbers must not be empty
 * @pre at least one number in the array must be positive
 * @post return value is positive
 * @return Average of all positive numbers in the array
 * @throws std::invalid_argument if preconditions are not met
 */
double averageOfPositives(const std::vector<int>& numbers)
{
    // Precondition checks
    if (numbers.empty())
    {
        throw std::invalid_argument("Array cannot be empty");
    }
    
    int sum = 0;
    int count = 0;
    
    // Find positive numbers
    for (int num : numbers)
    {
        if (num > 0)
        {
            sum += num;
            count++;
        }
    }
    
    if (count == 0)
    {
        throw std::invalid_argument("Array must contain at least one positive number");
    }
    
    double average = static_cast<double>(sum) / count;
    
    // Post-condition check
    assert(average > 0 && "Average of positive numbers must be positive");
    
    return average;
}

/**
 * @brief Binary search implementation
 * @param arr Sorted array to search in
 * @param target Value to find
 * @pre arr must be sorted in ascending order
 * @post if found, returns valid index; if not found, returns -1
 * @return Index of target if found, -1 otherwise
 * @invariant arr remains unchanged
 */
int binarySearch(const std::vector<int>& arr, int target)
{
    // Document the invariant: arr[left..right] may contain target
    int left = 0;
    int right = static_cast<int>(arr.size()) - 1;
    
    while (left <= right)
    {
        // Invariant: if target exists, it's in arr[left..right]
        int mid = left + (right - left) / 2;  // Avoid overflow
        
        if (arr[mid] == target)
        {
            return mid;  // Post-condition: valid index returned
        }
        else if (arr[mid] < target)
        {
            left = mid + 1;  // Maintain invariant: target in arr[mid+1..right]
        }
        else
        {
            right = mid - 1;  // Maintain invariant: target in arr[left..mid-1]
        }
    }
    
    return -1;  // Post-condition: target not found
}

void demonstrateContracts()
{
    std::cout << "=== Code Contracts Demo ===" << std::endl;
    
    // Test averageOfPositives with valid input
    std::vector<int> mixedNumbers = {-2, 3, -1, 4, 0, 5};
    try
    {
        double avg = averageOfPositives(mixedNumbers);
        std::cout << "Average of positives: " << avg << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "Error: " << e.what() << std::endl;
    }
    
    // Test binarySearch with sorted array
    std::vector<int> sortedNumbers = {1, 3, 5, 7, 9, 11, 13};
    int index = binarySearch(sortedNumbers, 7);
    if (index != -1)
    {
        std::cout << "Found 7 at index: " << index << std::endl;
    }
    else
    {
        std::cout << "7 not found" << std::endl;
    }
}

/*
Contract documentation format:

@brief         - Short description of what the function does
@param         - Description of each parameter
@pre           - Preconditions (what must be true when function is called)
@post          - Postconditions (what will be true when function returns)
@return        - Description of return value
@throws        - What exceptions can be thrown
@invariant     - What remains constant during execution
@complexity    - Time/space complexity (for algorithms)

Example contract:
/**
 * @brief Sorts array in ascending order
 * @param arr Array to sort (modified in-place)
 * @pre arr contains comparable elements
 * @post arr is sorted in ascending order
 * @post array size remains unchanged
 * @complexity O(n log n) time, O(log n) space
 * @invariant All original elements remain in array
 */
/*

Benefits of contracts:
1. Clear communication of expectations
2. Easier debugging when contracts fail
3. Better API design
4. Automated testing opportunities
5. Documentation that stays current with code
*/

int main()
{
    demonstrateContracts();
    return 0;
}

Summary

Finding issues before they become problems requires a proactive approach:

Key strategies:

  1. Input validation: Always validate function parameters and user input
  2. Assertions: Document and verify your assumptions with assertions
  3. Error handling: Implement robust error detection and recovery
  4. Code review: Systematically review code for common issues
  5. Testing: Use comprehensive testing including edge cases and error conditions
  6. Compiler warnings: Enable and fix all compiler warnings
  7. Documentation: Document preconditions, postconditions, and invariants

Benefits of preventive debugging:

  • Catches issues early when they're easier to fix
  • Reduces debugging time in the long run
  • Improves code reliability and maintainability
  • Makes assumptions explicit and verifiable
  • Provides better error messages for users

Best practices:

  • Use static analysis tools and compiler warnings
  • Write tests before problems occur
  • Document your assumptions clearly
  • Validate inputs at function boundaries
  • Use modern C++ features for safer code
  • Review code systematically for common pitfalls

By adopting these preventive techniques, you'll spend less time debugging and more time building features, while creating more reliable software.

In the next lesson, you'll review everything you've learned about debugging C++ programs.

Quiz

  1. What is defensive programming and why is it important?
  2. How do assertions help catch problems early?
  3. What are the advantages of validating inputs at function boundaries?
  4. How can compiler warnings help prevent bugs?
  5. What information should be included in function documentation contracts?

Practice exercises

  1. Add defensive programming to this function:

    double calculateGPA(const std::vector<double>& grades)
    {
        double sum = 0;
        for (double grade : grades)
            sum += grade;
        return sum / grades.size();
    }
    
  2. Write comprehensive tests for this function:

    std::string formatPhoneNumber(const std::string& digits)
    {
        if (digits.length() != 10) return "";
        return "(" + digits.substr(0,3) + ") " + 
               digits.substr(3,3) + "-" + digits.substr(6,4);
    }
    
  3. Add error handling to this file reading function:

    std::vector<int> readNumbers(const std::string& filename)
    {
        std::ifstream file(filename);
        std::vector<int> numbers;
        int num;
        while (file >> num)
            numbers.push_back(num);
        return numbers;
    }
    
  4. Create a code review checklist and apply it to a program you've written, identifying potential issues and improvements.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion