C++ / Basic / Lesson 2
Values, types, and safe initialization
Choose representations deliberately, initialize every local value, and reason about arithmetic without assuming a particular machine layout.
On this page
Lesson goals
- Distinguish integer, floating-point, Boolean, and character values.
- Apply brace initialization and identify narrowing conversions.
- Explain division, overflow, const, and constexpr in C++20.
A type is a set of rules
A variable names an object with a type, storage, and a lifetime. int represents a bounded range of whole numbers; double approximates real-valued quantities; bool represents true or false. These types do not encode units automatically. Choose descriptive names such as duration_seconds instead of hoping an integer documents itself. C++ does not require int to be exactly thirty-two bits. Use numeric_limits when the permitted range matters, and choose fixed-width integer aliases only when their optional availability and exact width suit an external format.
Establish a value at declaration
Write int count{} for zero or int count{7} for a known value. A local int count; has no usable initialized value merely because storage exists; reading it in ordinary arithmetic has undefined behavior in C++20. Braces reject many narrowing conversions, including int count{2.5}. const prevents later assignment to that object through its ordinary interface. constexpr additionally requires constant initialization for the variable. Neither keyword turns an unchecked external input into a valid value, so representation checks still belong at boundaries.
Division depends on operand types
For integer operands, division truncates toward zero; the remainder completes the identity dividend equals quotient times divisor plus remainder when the operation is representable. Assigning an integer quotient to double cannot restore the discarded fractional part. Convert an operand before division when a fractional result is needed. The first example deliberately prints both interpretations. The values are small and exactly represented here, so the shown output is stable under the default stream formatting. General decimal calculations can need an explicit rounding policy.
#include <iostream>
int main() {
const int pages{7};
const int readers{2};
std::cout << pages / readers << " " << pages % readers << "\n";
std::cout << static_cast<double>(pages) / readers << "\n";
}
3 1
3.5
Integer division gives three with remainder one; converting pages before division produces three and a half.
Make conversion intent visible
A cast states an intention but does not prove that the conversion is safe. Converting a floating value to an integer discards its fractional part, and an out-of-range result is not a valid portable conversion. Check range and domain before casting external values. auto deduces a type from an initializer; it does not mean a dynamically changing type. In the second program, total remains int and average is double because the expression contains a double operand. boolalpha asks the stream to spell Boolean values as words.
#include <iostream>
int main() {
constexpr int total{18};
const auto average = total / 4.0;
const bool enough = average >= 4.0;
std::cout << average << " " << std::boolalpha << enough << "\n";
}
4.5 true
The floating operand makes average 4.5; the comparison produces true without changing the numeric value.
Respect the edge of the domain
Signed integer overflow and integer division by zero are undefined behavior, so neither has a guaranteed printed result. Unsigned arithmetic wraps modulo its representable range, which is defined but often wrong for a bill or a count. Mixing signed and unsigned values can convert a negative operand into a large unsigned value before comparison. Prefer a coherent numeric domain, validate counts before conversion, and rearrange overflow checks to avoid performing the dangerous operation inside the check. Floating-point equality also needs a domain-specific accuracy argument.
Common errors and fixes
An average unexpectedly equals three.
- Why it happens
- Both division operands are integers.
- How to fix it
- Convert one operand before division, after checking the denominator.
- Prevent it next time
- Review operand types before choosing the result variable type.
A counter prints inconsistent values.
- Why it happens
- An uninitialized local scalar is read.
- How to fix it
- Initialize it with braces before any read.
- Prevent it next time
- Enable warnings and initialize at declaration.
A large sum appears to become negative.
- Why it happens
- Signed arithmetic exceeded its representable range.
- How to fix it
- Validate the operands against numeric_limits before adding.
- Prevent it next time
- Specify numeric limits; never rely on an observed overflow result.
Check your understanding
1. For eleven tasks shared among four workers, calculate whole tasks per worker, remaining tasks, and the mathematical average. Explain each operand type.
Show worked solution
With int operands, 11 / 4 is 2 and 11 % 4 is 3. Converting the numerator gives static_cast<double>(11) / 4, which is 2.75. Printing 11 / 4 into a double variable still produces 2 because integer division happened first. Check four is nonzero before generalizing to input.
2. Design a safe increment for a nonnegative int counter that may already equal its maximum. Describe both outcomes without evaluating overflow.
Show worked solution
Compare counter with std::numeric_limits<int>::max() from limits. If it equals the maximum, report a capacity error and leave the value unchanged. Otherwise increment it once. The comparison performs no risky addition; checking counter + 1 afterward would already have evaluated the potentially undefined operation. Test zero and the maximum explicitly.
Key takeaways
- Initialize values and reject narrowing unless conversion is justified.
- Operand types determine arithmetic before assignment.
- Undefined behavior has no reliable output contract.