A program trace is a record of what exists and what changes after each statement. This review brings pointers, dynamic arrays, strings, and vectors together around that habit. Before calculating an answer, identify the object, its valid positions, and its lifetime.
The examples below explain the review topics through short, independent program traces. For foundations, revisit pointers, C strings, C++ strings, and vectors.
Three questions for every statement #
Ask what the statement changes: a variable's value, the object reached through a pointer, or whether that object still exists. These are different events.
In int* p, the asterisk is part of a pointer declaration. In *p = 9, it dereferences an existing pointer, giving access to the target. In p = &value, the address-of operator supplies value's address. Replacing p's stored address does not copy or automatically destroy its previous target.
The stream operators have another role: std::cin >> value extracts input, while std::cout << value inserts output. Context matters because a symbol's meaning depends on the expression around it.
Trace two pointers and one target #
An alias is another way to reach the same object. Draw a box for each pointer and a separate box for the allocated integer. An arrow represents the address stored in a pointer.
int* first = new int(7);
int* second = first;
*second += 2;
delete first;
first = nullptr;
second = nullptr;The first statement creates one allocated integer containing seven and stores its address in first. The second statement copies that address into second. It creates another pointer variable, but no second integer. The third statement follows second's arrow and changes the shared integer to nine. Before deletion, *first and *second would both read nine.
delete first ends the allocated integer's lifetime and releases its storage. It does not erase the two pointer variables. Both now hold stale addresses, and neither may be used to access the former target. The final assignments explicitly make each pointer represent no target. Delete the allocation once; deleting it again through second would be invalid.[1]
For a reverse-diagram problem, read the arrows first. Two arrows to one live box suggest copying an address. A changed target value suggests dereferencing and assigning. A new separate box requires creation of another object. A removed allocated box suggests the appropriate deletion operation.
Separate the pointer's lifetime from its target's #
The usual stack diagram models automatic local variables and active calls. Dynamic storage, commonly called the heap or free store, has a separate lifetime. These diagrams teach ownership and lifetime; they do not require a particular physical memory layout.
An ordinary local pointer disappears when its scope ends, but a raw allocation is not automatically released just because that pointer disappears. Losing the last usable address before releasing the allocation produces a memory leak. Repeated leaks can exhaust resources. A leak and a stack overflow are different failures; one does not simply imply the other.
Match cleanup to allocation: new int pairs with scalar delete, while new int[count] pairs with delete[]. A pointer aimed at an ordinary local integer obtained through &value must not be used to delete that integer.[1]
Array indexing can follow a shared address #
Consider a built-in array and a pointer to its first element:
int readings[3] {2, 5, 8};
int* view = readings;
for (int i = 0; i < 3; ++i) {
view[i] += 1;
}At index zero, view reaches readings' first integer and changes two to three. At index one, five becomes six. At index two, eight becomes nine. The array is now {3, 6, 9}. The pointer did not create a copy. Its indexed accesses reached those same three elements.
The loop stops when i becomes three, which is the element count and the first invalid index. A pointer does not record the array's length. A function receiving such a pointer also needs an accurate count to traverse it safely.
Allocate nested structures one layer at a time #
int** rows = new int*[2]; creates an array containing two pointer elements. It does not also create two rows of integers. Each inner pointer must receive a valid row allocation before rows[r][c] can access a value.
Suppose the two inner rows have lengths three and five. There are three allocations altogether: one outer pointer array and two inner integer arrays. Cleanup releases the two inner arrays first, while their addresses are still available, and then releases the outer array. All three use delete[] because all three were allocated as arrays.
A built-in rectangular array such as int grid[2][3] has a different type and representation from int**. In an ordinary function parameter, the later extent helps describe the row type, as in void inspect(int grid[][3], int rowCount);. A pointer-based structure receives dimensions separately and may have unequal row lengths. Do not treat these forms as interchangeable just because both use two indexes.
C strings need room for their terminator #
A C string is text in a character array ending at \0, the null character. It is different from the digit character '0'. A five-position buffer can contain four visible characters plus the terminator.
Copying and appending functions need sufficient destination storage. A count parameter in a related strn function limits a particular operation; it is not a universal guarantee that the destination is large enough. In particular, strncpy can copy a prefix without adding a terminator when the source reaches the specified limit. The programmer must ensure both capacity and termination before treating the result as a C string.[2]
When tracing a character array, mark the terminator explicitly. Whole-string output stops there even if later array positions contain other characters. Those positions still belong to the array, but they are outside that C string's text.
Read C++ string operations by result type #
For a C++ string, write indexes above the characters before tracing. Use the supplementary example std::string word = "planet";:
word.length()is six: a character count.word.at(2)is'a': one character.word.substr(2, 3)is"ane": three characters beginning at index two.
Both bracket indexing and at access characters at valid positions. at throws an out-of-range exception when its index is at least the string's size; ordinary bracket indexing does not provide that checked interface. Keep ordinary character accesses strictly below the size.[3]
Searching asks a different question. word.find("an") returns two because the complete substring begins there. word.find_first_of("tn") returns three because the earliest character belonging to the set is n at index three. word.find_first_not_of("pla") also returns three because n is the first character absent from that set. A failed search such as word.find("zz") returns std::string::npos, not a usable character index.[4]
The calling object is the string being searched. The argument describes the substring or character set being sought. Unless the example explicitly changes word, treat each query as starting from the same original text.
Compare at the first differing character #
Lexicographic order compares strings left to right. Matching characters delay the decision. The first differing pair determines it; if one string is an exact prefix, the shorter string comes first.
For strcmp("map", "mat"), m and a match. The final comparison is p versus t, so the result is negative. Equal strings produce zero, and a first string that comes later produces a positive result. Use the sign of strcmp's answer, not a guessed exact magnitude. C++ string comparison operators express the ordering test directly.[5]
Comparison is case-sensitive. The review's ASCII examples place digits before uppercase letters and uppercase letters before lowercase letters. That explains why "map" comes after "Map" there. It is an encoding-based comparison, not a rule for natural-language dictionary sorting.
Vector size describes the elements that exist #
Size counts existing elements. Capacity describes storage available before another allocation is required. Reserved capacity does not create additional elements that can be indexed.
std::vector<int> scores {4, 8};
scores.reserve(6);
scores.push_back(12);
scores.resize(5);Initially size is two. After reserve, size remains two and capacity is at least six, not necessarily exactly six. After push_back, size is three. After resize, size is five; the two added integers are zero-initialized. The existing values remain {4, 8, 12} followed by those zeros. Use size as the traversal bound.[6]
A readable printing helper combines the container rules with ordinary syntax:
void printValues(const std::vector<int>& values) {
for (std::size_t i = 0; i < values.size(); ++i) {
std::cout << "Value " << i + 1 << ": "
<< values[i] << '\n';
}
}This snippet uses <vector>, <iostream>, and <cstddef>. The return type is void because the helper prints rather than returning a value. The const reference permits reading the caller's vector without copying it. The initializer uses =, the member call is values.size(), indexing uses brackets, and the complete output statement ends with a semicolon.
Practice with explained answers #
After int value = 6; int* p = &value; *p = 11;, what does value contain? Eleven. Dereferencing p reaches value, so the assignment changes that same integer. This is automatic storage, so there is no matching delete.
A vector has size three and capacity eight. Is index five available for an ordinary element access? No. Only indexes zero through two describe existing elements. Reserve creates spare storage; resize or insertion creates additional elements.
A diagram shows two pointers aimed at one allocated integer containing four. Which operation could change that box to ten without creating another box? An assignment such as *p = 10;, assuming p points to that live target. Assigning a new address to p would move an arrow instead.
Finally, distinguish the foundations during function tracing. A value parameter is a local copy; a reference parameter gives access to the caller's object. A return supplies a result to the caller, while printed output is a separate effect. Track each independently and resume the caller exactly where the call occurred.