C++ Programming
Control Statements, Arrays, Strings and Command-Line Arguments
PGCP-AC
A program becomes useful when it can choose, repeat and process collections of data. C++ control statements determine which operations execute and how often. Arrays store fixed-size contiguous sequences, strings represent text and command-line arguments let the environment supply input when a program starts. These features are simple individually, but their boundary rules, conversions and lifetime effects require careful reasoning.
1. Boolean conditions
An if, while or for condition is contextually converted to bool. Zero numeric values and null pointers become false; nonzero values and non-null pointers become true.
if (temperature < 0) {
std::cout << "Freezing\n";
}
Prefer conditions that state intent. if (count != 0) can be clearer than relying on numeric conversion when the reader needs to understand what zero means.
Assignment is an expression, so if (value = 5) assigns and then tests 5, which is true. Compilers commonly warn because equality == was probably intended. Enable warnings and avoid clever conditions containing unrelated side effects.
2. if, else if and else
if (mark >= 75) {
grade = 'A';
} else if (mark >= 60) {
grade = 'B';
} else if (mark >= 40) {
grade = 'C';
} else {
grade = 'F';
}
Conditions are tested from top to bottom and only the first matching branch executes. Ordering matters: testing mark >= 40 first would capture every passing mark and make higher-grade branches unreachable in practice.
An else binds to the nearest unmatched if. Always using braces avoids misleading indentation and makes later edits safer.
Nested conditions are appropriate when the inner choice matters only after the outer condition succeeds. If nesting becomes deep, early returns or named helper functions often express the logic more clearly.
3. switch statements
switch selects a labelled branch using an integral or enumeration expression:
switch (choice) {
case 1:
open_file();
break;
case 2:
save_file();
break;
case 3:
case 4:
show_help();
break;
default:
report_unknown_choice();
}
Case labels must be converted constant expressions and cannot duplicate one another after conversion. default is optional and handles values not named by a case.
Execution begins at the matching label and continues until a control transfer or the switch ends. Omitting break therefore causes fall-through. Several labels may intentionally share one body. When execution intentionally continues into the next case body, C++17's [[fallthrough]]; documents that decision and suppresses appropriate warnings.
Declarations inside a switch need braces when control could jump past initialization:
case 1: {
std::string message = load_message();
display(message);
break;
}
4. while loops
A while loop tests before each iteration:
int value = 1;
while (value <= 5) {
std::cout << value << '\n';
++value;
}
If the initial condition is false, the body never runs. Every loop needs a progress argument: identify which state changes and why that change eventually makes the condition false. Failure to update the controlling state can create an infinite loop.
Sentinel loops continue until a special value appears. Input loops should test extraction itself:
int number;
while (std::cin >> number) {
process(number);
}
This stops cleanly at end-of-file or invalid input rather than processing an unchanged or invalid value.
5. do-while loops
A do-while tests after its body, so the body executes at least once:
int choice;
do {
show_menu();
std::cin >> choice;
} while (choice != 0);
A semicolon is required after the closing condition. Use this loop when one initial execution is part of the problem, such as showing a menu before checking whether to repeat. Do not select it merely to avoid initializing a condition correctly.
6. traditional for loops
for (int i = 0; i < 10; ++i) {
std::cout << i << '\n';
}
A traditional for statement has initialization, condition and iteration expressions. Initialization runs once. The condition is tested before each body execution. The iteration expression runs after the body, including after continue.
Any component may be omitted:
for (;;) {
// deliberate infinite loop
}
The variable declared in the initializer is scoped to the loop. Use a traditional for when the algorithm genuinely needs an index, several synchronized control variables or nonstandard progression.
7. range-based for loops
A range-for visits every element:
int values[]{4, 8, 12};
for (int value : values) {
std::cout << value << '\n'; // value is a copy
}
Choose the declaration according to ownership:
for (int& value : values) {
value *= 2; // modifies originals
}
for (const auto& item : records) {
display(item); // no copy, no modification
}
auto&& is useful in generic code when the range can yield different reference categories. The range expression is evaluated once and its lifetime can be extended in defined cases by the hidden range binding. Avoid iterating a range while structurally invalidating its iterators.
8. break, continue and return
break exits the nearest enclosing loop or switch. It does not exit every nested loop. continue skips the rest of the current loop iteration and proceeds to the next condition check or, in a traditional for, the iteration expression.
return exits the current function and optionally supplies its result:
int find_first(const int values[], int size, int target) {
for (int i = 0; i < size; ++i) {
if (values[i] == target) {
return i;
}
}
return -1;
}
Early return can simplify validation and error paths. Multiple arbitrary jumps can obscure resource and invariant reasoning, but automatic objects are still destroyed when normal return leaves their scopes.
9. Nested loops
Nested loops model grids, pairs and repeated inner work:
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
process(row, column);
}
}
If the outer loop runs R times and the inner loop performs C iterations for each outer iteration, the body runs R × C times. A triangular inner range may produce approximately n²/2 operations, which is still quadratic growth.
A break inside the inner loop exits only that loop. To stop both, use a flag, return from a helper function or restructure the search using an algorithm.
10. Built-in arrays
A built-in array stores a fixed number of same-type elements contiguously:
int marks[5]{72, 81, 65, 90, 77};
Its size is part of its type: int[5] and int[6] are different types. Indexes run from 0 through size minus one. Built-in subscript does not check bounds. Reading or writing outside the array is undefined behaviour.
Partial initialization value-initializes remaining elements:
int values[5]{1, 2}; // 1, 2, 0, 0, 0
The size can be deduced from the initializer: int values[]{1, 2, 3};.
11. Array size and iteration
Within the same scope as an array object:
int values[]{2, 4, 6, 8};
std::size_t count = std::size(values);
std::size communicates intent better than dividing two sizeof expressions. Range-for also preserves size knowledge.
Prefer std::array<T,N> for fixed-size value semantics:
std::array<int, 4> values{2, 4, 6, 8};
values.at(2); // checked access
std::array supports assignment, iterators, size and standard algorithms while retaining inline fixed storage.
12. Array-to-pointer adjustment
In many expressions, a built-in array converts to a pointer to its first element. This loses size information:
void print(const int values[], int count);
In a parameter declaration, const int values[] adjusts to const int*. Therefore, sizeof(values) inside that function reports pointer size, not original array size.
Pass the size explicitly, use a reference to an array when the extent is part of the contract or use a view:
template<std::size_t N>
void print(const int (&values)[N]);
Modern interfaces can use iterators or std::span where available. Owning dynamic sequences should normally use std::vector.
13. Multidimensional arrays
int matrix[2][3]{
{1, 2, 3},
{4, 5, 6}
};
This is an array of two elements, each of which is an array of three int values. Built-in multidimensional arrays use row-major layout: the elements of the first row are contiguous, followed by the second row.
When passing such an array through an adjusted parameter, later dimensions must be known so pointer arithmetic can locate a row:
void display(const int matrix[][3], int rows);
Nested std::array values preserve dimensions more cleanly. A flat std::vector indexed as row * columns + column can represent runtime dimensions.
14. C-style strings
A C-style string is a character sequence terminated by a null character '\0':
char word[] = "cat";
The array needs four chars: 'c', 'a', 't' and '\0'. Functions expecting a C string scan until the terminator, so a missing terminator can cause out-of-bounds access.
strlen counts characters before the terminator, while sizeof(word) gives total array storage in this scope. C-string copy and concatenation require destination-capacity management and are a common source of buffer errors.
Use C strings for interoperability when required; use std::string for normal owning text.
15. std::string
std::string manages text storage and length:
std::string first = "Ada";
std::string last = "Lovelace";
std::string full = first + " " + last;
It supports size, concatenation, comparison, searching, substrings, insertion, erasure and iteration. operator[] is unchecked; at checks and throws std::out_of_range.
c_str() returns a null-terminated pointer for APIs that read C strings. That pointer can be invalidated by later non-const operations on the string, so do not store it across modification.
std::string stores bytes. Correct processing of human-visible Unicode characters may require an encoding-aware library because one UTF-8 character can occupy several bytes.
16. Reading text input
Formatted extraction stops at whitespace:
std::string first_name;
std::cin >> first_name;
std::getline reads an entire line:
std::string line;
std::getline(std::cin, line);
After formatted numeric input, the newline often remains in the stream. Consume leading whitespace deliberately:
std::getline(std::cin >> std::ws, line);
Always check stream state. On invalid input, clear the failure state and discard unsuitable characters according to a defined recovery policy.
17. Command-line arguments
The conventional parameterized main form is:
int main(int argc, char* argv[]) {
// ...
}
argc is the argument count. argv points to argument strings. When argc is positive, argv[0] conventionally represents the program invocation name. User arguments begin at index 1. argv[argc] is a null pointer.
The shell or launch environment parses the original command line before the program receives it. Quoting rules therefore depend on that environment:
app "two words" 42
This commonly supplies two user arguments, not three.
18. Parsing command-line values
Arguments arrive as text and require validation:
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: app <count>\n";
return 1;
}
try {
std::size_t used = 0;
int count = std::stoi(argv[1], &used);
if (used != std::string(argv[1]).size() || count < 0) {
throw std::invalid_argument("invalid count");
}
run(count);
} catch (const std::exception&) {
std::cerr << "count must be a non-negative integer\n";
return 2;
}
}
std::stoi can accept a numeric prefix, so checking the consumed length rejects trailing characters. It can throw for invalid text or out-of-range values. For high-performance non-allocating parsing, std::from_chars provides error codes and an ending pointer.
19. A complete array transformation
int values[]{2, 4, 6};
for (int& value : values) {
value *= 2;
}
for (const int value : values) {
std::cout << value << ' ';
}
The first loop binds value by reference, so assignments change array elements. Final values are 4, 8 and 12. The second loop copies each int for reading. Using int value in the first loop would modify only the local copy and leave the array unchanged.
20. Practical considerations
- Order an else-if chain from specific or restrictive conditions to broader ones.
- Document intentional switch fall-through.
- A do-while body runs at least once.
- break affects the nearest loop or switch; return exits the function.
- Use a reference in range-for when original elements must change.
- Built-in array indexing is unchecked.
- Array parameters adjust to pointers and lose extent information.
- A C string requires a reachable null terminator.
- Use std::string for ordinary owned text.
- Command-line values are untrusted text and need complete parsing and range checks.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.