Coming Soon
This lesson is currently being developed
Finding issues before they become problems
Learn preventive programming techniques.
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
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:
- Assume inputs will be invalid
- Expect the unexpected
- Make failures obvious and early
- Document assumptions and invariants
- 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:
- Input validation: Always validate function parameters and user input
- Assertions: Document and verify your assumptions with assertions
- Error handling: Implement robust error detection and recovery
- Code review: Systematically review code for common issues
- Testing: Use comprehensive testing including edge cases and error conditions
- Compiler warnings: Enable and fix all compiler warnings
- 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
- What is defensive programming and why is it important?
- How do assertions help catch problems early?
- What are the advantages of validating inputs at function boundaries?
- How can compiler warnings help prevent bugs?
- What information should be included in function documentation contracts?
Practice exercises
-
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(); }
-
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); }
-
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; }
-
Create a code review checklist and apply it to a program you've written, identifying potential issues and improvements.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions