Coming Soon
This lesson is currently being developed
Using an integrated debugger: Watching variables
Master using IDE debugging tools effectively.
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.8 — Using an integrated debugger: Watching variables
In this lesson, you'll learn how to monitor variable values during program execution using the integrated debugger's variable watching capabilities. This is crucial for understanding how your program's data changes over time and identifying where bugs occur.
What is variable watching?
Variable watching allows you to monitor the values of specific variables as your program executes. When execution pauses (at breakpoints or while stepping), you can:
- See current values of variables in scope
- Monitor how values change over time
- Examine complex data structures
- Add custom expressions to watch
- Compare expected vs. actual values
Types of variable viewing
Automatic variables (Locals)
Variables automatically shown in the current scope.
#include <iostream>
#include <vector>
void demonstrateLocalVariables()
{
int localInt = 42; // Will appear in Locals window
double localDouble = 3.14159; // Will appear in Locals window
bool localBool = true; // Will appear in Locals window
std::vector<int> localVector = {1, 2, 3, 4, 5}; // Will show size and contents
// Set breakpoint here to examine locals
std::cout << "Local variables initialized" << std::endl;
// Modify some values
localInt *= 2; // Watch this change in Locals
localDouble += 1.0; // Watch this change in Locals
localVector.push_back(6); // Watch vector size change
// Set another breakpoint here to see changes
std::cout << "Local variables modified" << std::endl;
}
/*
In the Locals/Variables window, you'll see:
Before modifications:
┌─────────────────────────────────────────┐
│ Name │ Value │ Type │
├─────────────────────────────────────────┤
│ localInt │ 42 │ int │
│ localDouble │ 3.14159 │ double │
│ localBool │ true │ bool │
│ localVector │ {size=5} │ vector<int> │
│ [0] │ 1 │ int │
│ [1] │ 2 │ int │
│ [2] │ 3 │ int │
│ [3] │ 4 │ int │
│ [4] │ 5 │ int │
└─────────────────────────────────────────┘
After modifications:
┌─────────────────────────────────────────┐
│ Name │ Value │ Type │
├─────────────────────────────────────────┤
│ localInt │ 84 │ int │
│ localDouble │ 4.14159 │ double │
│ localBool │ true │ bool │
│ localVector │ {size=6} │ vector<int> │
│ [0] │ 1 │ int │
│ [1] │ 2 │ int │
│ [2] │ 3 │ int │
│ [3] │ 4 │ int │
│ [4] │ 5 │ int │
│ [5] │ 6 │ int │
└─────────────────────────────────────────┘
*/
int main()
{
demonstrateLocalVariables();
return 0;
}
Watch expressions
Custom expressions you add to monitor specific values or calculations.
#include <iostream>
#include <string>
class Student
{
public:
std::string name;
int age;
double gpa;
Student(const std::string& n, int a, double g) : name(n), age(a), gpa(g) {}
bool isHonorRoll() const
{
return gpa >= 3.5; // Add watch expression: student.isHonorRoll()
}
int yearsUntilGraduation() const
{
return 22 - age; // Add watch expression: student.yearsUntilGraduation()
}
};
void demonstrateWatchExpressions()
{
Student student("Alice", 19, 3.7);
// Set breakpoint here and add these watch expressions:
// 1. student.name (shows "Alice")
// 2. student.gpa (shows 3.7)
// 3. student.isHonorRoll() (shows true)
// 4. student.gpa * 4.0 (shows 14.8 - semester hours for GPA)
// 5. student.age > 18 (shows true)
std::cout << "Student created: " << student.name << std::endl;
// Modify GPA and watch expressions update
student.gpa = 3.2;
// Set breakpoint here to see watch expressions change:
// - student.isHonorRoll() now shows false
// - student.gpa * 4.0 now shows 12.8
std::cout << "GPA updated" << std::endl;
}
/*
Watch expressions you can add:
┌──────────────────────────────────────────┐
│ Expression │ Value │ Type │
├──────────────────────────────────────────┤
│ student.name │ "Alice"│ string│
│ student.gpa │ 3.7 │ double│
│ student.isHonorRoll() │ true │ bool │
│ student.gpa * 4.0 │ 14.8 │ double│
│ student.age > 18 │ true │ bool │
│ student.yearsUntilGraduation() │ 3 │ int │
└──────────────────────────────────────────┘
After GPA change:
┌──────────────────────────────────────────┐
│ Expression │ Value │ Type │
├──────────────────────────────────────────┤
│ student.name │ "Alice"│ string│
│ student.gpa │ 3.2 │ double│
│ student.isHonorRoll() │ false │ bool │
│ student.gpa * 4.0 │ 12.8 │ double│
│ student.age > 18 │ true │ bool │
│ student.yearsUntilGraduation() │ 3 │ int │
└──────────────────────────────────────────┘
*/
int main()
{
demonstrateWatchExpressions();
return 0;
}
Memory view
Examine raw memory contents (advanced feature).
#include <iostream>
void demonstrateMemoryViewing()
{
int numbers[] = {10, 20, 30, 40, 50};
int* ptr = numbers;
// Set breakpoint here
// In memory view, examine address of 'numbers' or 'ptr'
// You'll see the actual bytes: 0A 00 00 00 14 00 00 00 1E 00 00 00...
std::cout << "Array address: " << ptr << std::endl;
for (int i = 0; i < 5; ++i)
{
std::cout << "numbers[" << i << "] = " << numbers[i]
<< " (address: " << &numbers[i] << ")" << std::endl;
}
}
/*
Memory view shows raw bytes:
Address | Bytes (hex) | Interpretation
-----------|--------------------|---------------
0x001FF5A0 | 0A 00 00 00 | 10 (int)
0x001FF5A4 | 14 00 00 00 | 20 (int)
0x001FF5A8 | 1E 00 00 00 | 30 (int)
0x001FF5AC | 28 00 00 00 | 40 (int)
0x001FF5B0 | 32 00 00 00 | 50 (int)
This is useful for understanding:
- Memory layout
- Pointer arithmetic
- Data type sizes
- Endianness
- Buffer overflows
*/
Watching variables during loops
#include <iostream>
#include <vector>
void demonstrateLoopWatching()
{
std::vector<int> numbers = {1, 3, 5, 7, 9, 2, 4, 6, 8};
int sum = 0;
int evenCount = 0;
int oddCount = 0;
double average = 0.0;
// Set breakpoint in loop and watch these variables change
for (size_t i = 0; i < numbers.size(); ++i)
{
int current = numbers[i];
sum += current;
if (current % 2 == 0)
evenCount++;
else
oddCount++;
average = static_cast<double>(sum) / (i + 1);
// Breakpoint here - watch variables update each iteration
std::cout << "Processing " << current << ", sum=" << sum
<< ", avg=" << average << std::endl;
}
}
/*
Watch these expressions during loop execution:
┌──────────────────────────────────────────────────────────────┐
│ Variable/Expression │ Iteration 0 │ Iteration 4 │ Iteration 8 │
├──────────────────────────────────────────────────────────────┤
│ i │ 0 │ 4 │ 8 │
│ current │ 1 │ 9 │ 8 │
│ sum │ 1 │ 25 │ 45 │
│ evenCount │ 0 │ 1 │ 4 │
│ oddCount │ 1 │ 4 │ 5 │
│ average │ 1.0 │ 5.0 │ 5.0 │
│ numbers[i] │ 1 │ 9 │ 8 │
│ i < numbers.size() │ true │ true │ true │
│ current % 2 == 0 │ false │ false │ true │
└──────────────────────────────────────────────────────────────┘
This helps you see:
- How variables change over time
- When conditions become true/false
- Whether calculations are correct
- Loop boundary conditions
*/
Watching complex data structures
#include <iostream>
#include <vector>
#include <map>
#include <string>
struct Person
{
std::string name;
int age;
std::vector<std::string> hobbies;
};
void demonstrateComplexWatching()
{
// Complex nested data structure
std::map<std::string, Person> people;
people["alice"] = {"Alice Smith", 25, {"reading", "swimming", "coding"}};
people["bob"] = {"Bob Jones", 30, {"gaming", "cooking"}};
people["charlie"] = {"Charlie Brown", 22, {"music", "art", "running", "hiking"}};
// Set breakpoint here and explore the data structure
std::cout << "People database created" << std::endl;
// Add a new person
people["diana"] = {"Diana Wilson", 28, {"dancing", "photography"}};
// Modify existing person
people["alice"].hobbies.push_back("traveling");
people["alice"].age = 26;
// Set breakpoint here to see changes
std::cout << "People database updated" << std::endl;
}
/*
In the debugger, expand the 'people' variable to see:
people (map<string, Person>, size=3 initially, then 4)
├── ["alice"] (Person)
│ ├── name: "Alice Smith"
│ ├── age: 25 (becomes 26 after update)
│ └── hobbies (vector<string>, size=3, then 4)
│ ├── [0]: "reading"
│ ├── [1]: "swimming"
│ ├── [2]: "coding"
│ └── [3]: "traveling" (added later)
├── ["bob"] (Person)
│ ├── name: "Bob Jones"
│ ├── age: 30
│ └── hobbies (vector<string>, size=2)
│ ├── [0]: "gaming"
│ └── [1]: "cooking"
└── ["charlie"] (Person)
├── name: "Charlie Brown"
├── age: 22
└── hobbies (vector<string>, size=4)
├── [0]: "music"
├── [1]: "art"
├── [2]: "running"
└── [3]: "hiking"
Watch expressions you can add:
- people.size() (shows 3, then 4)
- people["alice"].hobbies.size() (shows 3, then 4)
- people.count("diana") (shows 0, then 1)
- people["bob"].age > 25 (shows true)
*/
Using watches to find bugs
#include <iostream>
#include <vector>
// Buggy function - let's use watches to find the problem
double calculateAverage(const std::vector<int>& numbers)
{
if (numbers.empty())
return 0.0;
int sum = 0; // Watch: sum
int count = 0; // Watch: count
// Bug is in this loop - watch the variables
for (size_t i = 0; i <= numbers.size(); ++i) // BUG: should be <, not <=
{
// Add watches for:
// - i
// - numbers.size()
// - i <= numbers.size()
// - numbers[i] (will be invalid on last iteration)
sum += numbers[i]; // This will crash when i == numbers.size()
count++;
}
double average = static_cast<double>(sum) / count; // Watch: average
return average;
}
void debugWithWatches()
{
std::vector<int> testData = {10, 20, 30, 40, 50};
// Set breakpoint before function call
std::cout << "Calculating average of " << testData.size() << " numbers" << std::endl;
try
{
double avg = calculateAverage(testData);
std::cout << "Average: " << avg << std::endl;
}
catch (const std::exception& e)
{
std::cout << "Exception: " << e.what() << std::endl;
}
}
/*
Watch expressions to add for debugging:
Inside calculateAverage():
┌──────────────────────────────────────────────────────────┐
│ Expression │ Iteration 0 │ Iteration 4 │ Iteration 5 │
├──────────────────────────────────────────────────────────┤
│ i │ 0 │ 4 │ 5 │
│ numbers.size() │ 5 │ 5 │ 5 │
│ i <= numbers.size() │ true │ true │ true (BUG!) │
│ i < numbers.size() │ true │ true │ false │
│ numbers[i] │ 10 │ 50 │ ERROR! │
│ sum │ 10 │ 150 │ CRASH │
│ count │ 1 │ 5 │ 6 │
└──────────────────────────────────────────────────────────┘
The watches clearly show:
1. Loop condition i <= numbers.size() allows i to equal 5
2. But valid indices are 0-4 for a vector of size 5
3. Accessing numbers[5] is out of bounds
4. Fix: Change <= to < in the loop condition
*/
Advanced watching techniques
Conditional watches
Some debuggers allow conditional evaluation of watch expressions.
#include <iostream>
#include <vector>
void demonstrateConditionalWatches()
{
std::vector<int> data = {1, -2, 3, -4, 5, -6, 7, -8, 9};
for (size_t i = 0; i < data.size(); ++i)
{
int current = data[i];
// Set breakpoint here
// Add conditional watch expressions:
// - current > 0 ? current : 0 (positive values only)
// - current < 0 ? abs(current) : current (absolute values)
// - i % 2 == 0 ? "even index" : "odd index"
std::cout << "data[" << i << "] = " << current << std::endl;
}
}
/*
Conditional watch expressions:
┌─────────────────────────────────────────────────────────────┐
│ Expression │ i=0 │ i=1 │ i=2 │ i=3 │
├─────────────────────────────────────────────────────────────┤
│ current │ 1 │ -2 │ 3 │ -4 │
│ current > 0 ? current : 0 │ 1 │ 0 │ 3 │ 0 │
│ current < 0 ? abs(current) : current │ 1 │ 2 │ 3 │ 4 │
│ i % 2 == 0 ? "even" : "odd" │even │ odd │even │ odd │
└─────────────────────────────────────────────────────────────┘
This is useful for:
- Filtering data while debugging
- Calculating derived values
- Testing conditions inline
*/
Watch expressions with function calls
You can often call functions in watch expressions.
#include <iostream>
#include <string>
#include <algorithm>
void demonstrateFunctionCallWatches()
{
std::string text = "Hello World";
// Set breakpoint here and add watch expressions that call functions:
// - text.length() (shows 11)
// - text.substr(0, 5) (shows "Hello")
// - std::count(text.begin(), text.end(), 'l') (shows 3)
// - text.find("World") (shows 6)
// - text.empty() (shows false)
std::cout << "Text: " << text << std::endl;
// Modify text
std::transform(text.begin(), text.end(), text.begin(), ::tolower);
// Set breakpoint here - same watch expressions show updated values:
// - text.length() (still 11)
// - text.substr(0, 5) (now shows "hello")
// - std::count(text.begin(), text.end(), 'l') (still 3)
// - text.find("world") (shows 6)
std::cout << "Modified text: " << text << std::endl;
}
IDE-specific watch features
Visual Studio Code
Accessing watch features:
- Variables panel: Shows local variables automatically
- Watch panel: Add custom watch expressions
- Call Stack panel: Shows function call hierarchy
Adding watches:
- Type expression in Watch panel
- Right-click variable → "Add to Watch"
- Hover over variables while debugging
Visual Studio
Watch windows:
- Locals window: Automatic local variables
- Autos window: Recently used variables
- Watch windows: Custom expressions (Watch 1, Watch 2, etc.)
- Quick Watch: Ctrl+Alt+Q for temporary watches
Features:
- Drag variables to watch windows
- Edit values directly in watch windows
- Format specifiers (e.g., variable,x for hex display)
Code::Blocks
Debug windows:
- Watches: Add custom expressions
- Local variables: Automatic scope variables
- Memory dump: Raw memory viewing
Usage:
- Right-click variable → "Watch variable"
- Debug → Debugging windows → Watches
- Double-click values to edit them
Best practices for watching variables
- Start with locals: Examine automatic variables first, then add custom watches
- Watch key calculations: Monitor intermediate results of complex calculations
- Use meaningful expressions: Watch
student.isEligible()
rather than complex conditions - Remove unused watches: Clean up watch list to avoid clutter
- Watch boundary conditions: Monitor loop indices and array bounds
- Compare expected vs. actual: Add watches for both expected and calculated values
Common watch expression patterns
#include <iostream>
#include <vector>
#include <algorithm>
void commonWatchPatterns()
{
std::vector<int> scores = {85, 92, 78, 96, 88, 74, 91};
// Set breakpoint here and try these common watch patterns:
// Collection properties:
// - scores.size()
// - scores.empty()
// - scores.capacity()
// Statistical calculations:
// - *std::max_element(scores.begin(), scores.end())
// - *std::min_element(scores.begin(), scores.end())
// - std::count_if(scores.begin(), scores.end(), [](int x){ return x >= 90; })
// Conditional expressions:
// - scores.size() > 5 ? "many scores" : "few scores"
// - std::all_of(scores.begin(), scores.end(), [](int x){ return x > 0; })
// Memory and pointers:
// - &scores[0] (address of first element)
// - scores.data() (pointer to data)
// - sizeof(scores) (size of vector object, not elements)
std::cout << "Analyzing " << scores.size() << " scores" << std::endl;
}
Summary
Variable watching is a powerful debugging technique that lets you monitor data changes in real-time:
Types of variable viewing:
- Locals/Autos: Automatically shown variables in current scope
- Watch expressions: Custom expressions you add to monitor
- Memory view: Raw memory contents (advanced)
What you can watch:
- Simple variables (int, double, bool, etc.)
- Complex objects (classes, structs)
- Containers (vector, map, array, etc.)
- Expressions and calculations
- Function call results
- Conditional expressions
Best practices:
- Start with automatic variables, add custom watches as needed
- Monitor key calculations and boundary conditions
- Use meaningful expressions that reflect your logic
- Clean up unused watches regularly
- Compare expected vs. actual values
Effective variable watching helps you understand exactly how your program's data changes during execution, making it much easier to identify where bugs occur and why.
In the next lesson, you'll learn about using the call stack to understand how your program got to its current state.
Quiz
- What's the difference between the Locals window and Watch expressions?
- How can conditional watch expressions help during debugging?
- What information can you get from watching complex data structures like vectors and maps?
- When would you use function call expressions in watches?
- How do watch expressions help identify off-by-one errors in loops?
Practice exercises
-
Debug using watches: Use variable watching to find the bug in this function:
int findMaxElement(const std::vector<int>& arr) { int maxValue = 0; // Potential bug here for (size_t i = 0; i < arr.size(); ++i) { if (arr[i] > maxValue) maxValue = arr[i]; } return maxValue; }
-
Watch complex calculations: Set up watches for this function to verify the math:
double compound_interest(double principal, double rate, int years) { double amount = principal; for (int i = 0; i < years; ++i) { amount = amount * (1 + rate); } return amount - principal; }
-
Monitor data structure changes: Watch how this data structure evolves:
void processGrades() { std::map<std::string, std::vector<int>> studentGrades; studentGrades["Alice"] = {85, 90, 92}; studentGrades["Bob"] = {78, 85, 88}; // Add watches to monitor changes as grades are added/modified studentGrades["Alice"].push_back(95); studentGrades["Charlie"] = {90, 92, 96, 88}; }
-
Practice watch expressions: Create a program with loops, conditions, and calculations, then practice adding different types of watch expressions to monitor the program's behavior.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions