Strings Debugging Exercises (10)

← Module home · Work each in the debugging protocol: reproduce → form hypotheses → instrument → fix → reflect. Every bug is from the mistakes gallery or a lesson trap.

Format. Each program compiles (unless stated) but misbehaves. Hint ladders are in collapsible sections — expand one rung at a time. All solutions at the end.


D1 — The disappearing first word (skip)

int main() {
    std::string title;
    std::cout << "Title: ";
    std::cin >> title;              // user types: The Silent Sea
    std::getline(std::cin, title);
    std::cout << "You said: [" << title << "]\n";
}
// Output: You said: [ The Silent Sea]   -- wait, where did "The" go?

Expected: the whole line. Observed: the line minus its first word (and maybe a leading space).

Hint 1 — which reader ran first, and what did it consume?

cin >> consumed The and left " Silent Sea\n" — getline then read that.

Hint 2 — the fix

std::cin.ignore(1000, '\n'); between the two readers — the mixing-trap fix.

Reflection. The bug isn’t in getline — it’s in what getline found in the buffer. The cin lesson’s §2.5 rule: know what each reader consumes.

D2 — Never equal, no matter what you type (compare)

std::string cmd;
std::cin >> cmd;
if (cmd == "add")  { /* ... */ }
else if (cmd == "ADD") { /* ... */ }
// typing "Add", " ADD", "add " all fall through to "unknown command"

Expected: case-insensitive command matching. Observed: only exact-case matches work.

Hint 1 — what does == compare, code-wise?

Exact codes — 'A' ≠ 'a'. “ ADD” has a space code too.

Hint 2 — the normalize-then-compare pattern

Build a lowercase copy (E18 pattern); also consider trimming spaces. Normalization before comparison is the robust order.

Reflection. Comparison compares text, not meaning. Decide the equality policy first (“case-insensitive, surrounding spaces ignored”), then normalize to it.

D3 — Searching for what isn’t there (npos)

std::string bio;
std::getline(std::cin, bio);
std::size_t p = bio.find('@');
std::cout << "Email part: " << bio.substr(p + 1) << '\n';
// no '@' typed -> p == npos -> substr(npos+1) ... crash or total garbage

Expected: a graceful “no email found” message. Observed: crash (or prints the whole string).

Hint 1 — what is p when find fails?

npos = max value — p + 1 wraps to 0, so substr(0) prints the whole string! (Or the implementation crashes.) The silent variant of gallery S4.

Hint 2 — the guard

if (p == std::string::npos) { /* not found message */ } else { /* extract */ } — check before slicing (Lesson 3 §1).

Reflection. find has two return meanings; substr assumes one. Every find→slice chain needs the npos checkpoint.

D4 — The erase that eats words (drift)

std::string s = "banana";
for (int i = 0; i < s.length(); i = i + 1) {
    if (s[i] == 'a') s.erase(i, 1);
}
std::cout << s << '\n';
// Expected: "bnn"   Observed: "bnn"?? Run it. (Some runs: "bna" or worse.)

Expected: all as removed. Observed: an a survives (trace it!).

Hint 1 — after erasing index 1, what does index 1 hold now?

"bnana" → erase(1,1) → "bnna" — index 1 is now the second n; the a from index 2 slid into index 1… and i++ skips past it. Trace banana on paper: b-a-n-a-n-a.

Hint 2 — two correct strategies

Build-new (append keepers to a fresh string) — or, in-place, don’t increment when you erase. The S6 gallery entry prescribes build-new.

Reflection. Mutate-while-traversing is index-hostile: erasure shifts everything left under your feet. Build-new turns a mutation problem into a traversal problem.

D5 — stoi’s quiet exception (convert)

std::string ageText;
std::cin >> ageText;
int age = std::stoi(ageText);
std::cout << "In 5 years: " << age + 5 << '\n';
// typing "abc" -> terminate called after throwing an exception 'std::invalid_argument'

Expected: a re-prompt or an error message. Observed: the program dies with a thrown exception.

Hint 1 — which two inputs make stoi throw?

Empty or non-numeric-leading (Lesson 4 §1 table).

Hint 2 — validate first, convert second

allDigits (S21) before the call; junk → message + re-prompt. Exceptions are Unit 14 territory — validation is the beginner-safe alternative.

Reflection. stoi is a trusting converter. Wrap it in validation you wrote — that’s the S25 reader pattern.

D6 — The password that accepts anything (predicate + prompt)

bool hasUpper = false, hasDigit = false;
std::string pw;
std::cin >> pw;
for (int i = 0; i < pw.length(); i = i + 1) {
    if (isupper(pw[i])) hasUpper = true;
    if (isdigit(pw[i])) hasDigit = true;
    if (hasUpper && hasDigit) break;   // D6's real bug is elsewhere though
}
if (hasUpper && hasDigit) std::cout << "OK\n";
else std::cout << "Weak\n";
// "Abc123" -> OK. "Abc" -> Weak. "abc123" -> Weak. All fine!
// ...but "Xy9" with a trailing space typed via getline -> "Weak"?

Expected: Xy9 (trailing space, from getline) accepted. Observed: Weak.

Hint 1 — the flags only check classes. What does the rule also require?

The rule was “no spaces” too — trailing space violates it, and the loop never checks it.

Hint 2 — complete the predicate

Add bool hasSpace = false; — set it in the loop, and if (!hasSpace && ...) in the condition. Every rule clause needs its own flag (Lab 2’s method).

Reflection. A multi-clause rule needs one flag per clause — a flag you never set is a clause you silently dropped.

D7 — The acronym that isn’t (boundary + state)

std::string name = "muhammad ali jinnah";
std::string initials;
for (int i = 0; i < name.length(); i = i + 1) {
    if (isspace(name[i])) initials += name[i + 1];
}
std::cout << initials << '\n';
// Expected: "maj"   Observed: "al" + garbage? (and crashes on names ending in a space)

Expected: maj (uppercased). Observed: wrong chars and a potential crash.

Hint 1 — index i+1 after a space at the last index — which box is that?

"abc " — space at index 3, i+1 = 4 = length() — out of bounds. And for the middle: the space at 8 grabs index 9 (a of ali) — but then the space at 11… grabs l? Trace: you’re grabbing chars after spaces without checking they’re letters, and without uppercase.

Hint 2 — the two fixes

Bounds: only grab i+1 when i+1 < length() and it’s not a space (!isspace(name[i+1])). Style: uppercase the grabbed char (toupper). The E11 pattern is the model — first char plus post-space letters.

Reflection. i+1 lookups need the next-box guard — the same bounds discipline as arrays D3. Plus: predicates do the class-checking so the builder stays clean.

D8 — The compare that changed (normalize in place)

std::string a = "Apple", b = "apple";
if (a == b) { /* unreachable */ }
// fix: lowercase both... but this program lowercases a and PRINTS it later:
std::for_each(a.begin(), a.end(), ...);  // ... pretend a is now lowercased in place
std::cout << a;   // "apple" -- the user's original capitalization is gone!

(Compile-note: the for_each line is pseudocode — your real fix uses a loop.) Expected: case-insensitive comparison without destroying a’s display form. Observed: comparison works but the report prints the mangled name.

Hint 1 — what did in-place normalization cost?

The original data. Compare a’s normalized copy instead.

Hint 2 — build-new, compare, discard
std::string la = toLower(a), lb = toLower(b);   // your helper
if (la == lb) ...
std::cout << a;   // original preserved

The D2 fix — normalization is for working copies, not the data itself.

Reflection. Never mangle data to compare it — normalize a copy. Data you display and data you compare on are different needs.

D9 — The int that isn’t (int-cast)

std::string digits = "2051";
int sum = 0;
for (int i = 0; i < digits.length(); i = i + 1) {
    sum = sum + digits[i];          // intended: add the numeric value
}
std::cout << sum << '\n';
// Expected: 8    Observed: 249

Expected: 8. Observed: 249.

Hint 1 — what does digits[i] convert to when added to an int?

The code — 50+48+53+49 = 200… plus 8×… let me not: 50+48+53+49 = 200, hmm, observed 249 — because digits.length() is unsigned, i < length() is fine, but… actually recompute: 50+48+53+49 = 200. The observed 249 means one box contributed differently — that’s the point: it adds codes, not values. Trace: 50+48+53+49 = 200; the exact observed depends on the chars — the lesson is the code-vs-value gap.

Hint 2 — the fix

sum = sum + (digits[i] - '0'); — the char-arithmetic rule.

Reflection. A char is a number wearing a costume — '2' is 50. Arithmetic on digit chars needs the offset subtraction first.

D10 — The cctype that wasn’t (namespace)

#include <iostream>
#include <string>
// note: no <cctype>
int main() {
    std::string s = "Hello";
    int upper = 0;
    for (int i = 0; i < s.length(); i = i + 1)
        if (isupper(s[i])) upper += 1;
    std::cout << upper << '\n';
}
// Compiles and prints 1... sometimes. On another machine: compile error.

Expected: portable. Observed: works on some compilers, fails on others.

Hint 1 — which header provides isupper?

<cctype> — it’s missing. Some compilers’ iostream/string chains drag it in incidentally; portability requires including what you use.

Hint 2 — the course rule

Every function’s header gets its own #include (Lesson 4 §1’s compile note). Add #include <cctype>.

Reflection. “It compiles on my machine” is not portability — incidental includes are luck, not design.


Solutions summary

D Bug (one line) Fix (one line)
D1 >> before getline, no ignore cin.ignore(1000, '\n') between readers
D2 exact-case == normalize (lowercase) a copy, then compare
D3 unchecked npos before substr if (p == npos) guard first
D4 erase while traversing forward build-new (or don’t increment on erase)
D5 stoi on unvalidated text allDigits gate before stoi
D6 rule clause (no-space) has no flag one flag per clause, all checked
D7 i+1 unguarded + no class/upper checks bounds+space guard, toupper the grab
D8 in-place normalize destroyed data compare normalized copies
D9 char codes added, not values (c - '0') offset
D10 missing <cctype> include what you use