Skip to content

02 · Variables, Types & Operators

C++ is statically typed: every variable has a fixed type, decided at compile time, that never changes.

Fundamental types

#include <iostream>

int main() {
    int age = 30;                 // whole numbers, typically 32-bit
    double price = 19.99;         // double-precision floating point
    float ratio = 0.5f;           // single-precision (note the "f" suffix)
    char grade = 'A';             // a single character, in single quotes
    bool isActive = true;         // true / false
    long population = 8'000'000;  // digit separators (') improve readability

    std::cout << age << " " << price << " " << grade << " "
              << isActive << std::endl;
    // 30 19.99 A 1   -- bool prints as 1/0 by default
}
Type Typical size Holds
int 4 bytes Whole numbers
double 8 bytes Decimal numbers (default choice for floating point)
float 4 bytes Decimal numbers, less precision, less memory
char 1 byte A single character
bool 1 byte true or false
long / long long 4 / 8 bytes Larger whole numbers

Sizes are platform/compiler dependent guarantees, not fixed constants — use sizeof(int) if you need the exact size on your system.

const and auto

const double PI = 3.14159;   // cannot be reassigned after initialization
// PI = 3.0;                 // compile error: assignment of read-only variable

auto count = 10;             // compiler infers "int" from the initializer
auto name = std::string("Ada");  // infers "std::string"

Prefer const for values that never change — it documents intent and lets the compiler catch accidental reassignment. auto is useful when the type is obvious from context or verbose to spell out; don't overuse it where an explicit type would be clearer to a reader.

Arithmetic operators

int a = 17, b = 5;

std::cout << a + b << std::endl;   // 22
std::cout << a - b << std::endl;   // 12
std::cout << a * b << std::endl;   // 85
std::cout << a / b << std::endl;   // 3  -- integer division truncates!
std::cout << a % b << std::endl;   // 2  -- remainder ("modulo")

double x = 17.0 / 5.0;
std::cout << x << std::endl;       // 3.4 -- floating-point division

Integer division truncating toward zero is one of the most common early bugs — if you need a fractional result, make sure at least one operand is a floating-point type.

Comparison and logical operators

int a = 5, b = 10;

std::cout << (a == b) << std::endl;  // 0 (false) -- equality
std::cout << (a != b) << std::endl;  // 1 (true)  -- inequality
std::cout << (a < b)  << std::endl;  // 1
std::cout << (a >= b) << std::endl;  // 0

bool loggedIn = true, isAdmin = false;
std::cout << (loggedIn && isAdmin) << std::endl;  // 0 -- AND
std::cout << (loggedIn || isAdmin) << std::endl;  // 1 -- OR
std::cout << (!isAdmin) << std::endl;             // 1 -- NOT

= is assignment; == is comparison. Mixing them up (if (a = b) instead of if (a == b)) compiles but silently does the wrong thing — -Wall will warn you about this, which is one more reason to always enable it.

Compound assignment and increment operators

int score = 10;
score += 5;   // score = score + 5  -> 15
score -= 3;   // -> 12
score *= 2;   // -> 24
score /= 4;   // -> 6

int i = 0;
i++;          // post-increment: i becomes 1
++i;          // pre-increment: i becomes 2

For loop counters and simple cases, i++ and ++i behave the same; the difference (whether the old or new value is used as the expression's result) matters once you use the operator inline, e.g. arr[i++].

Type conversion and casting

int wholeNumber = 7;
double asDouble = wholeNumber;         // implicit widening: 7 -> 7.0

double price = 19.99;
int truncated = static_cast<int>(price);  // explicit narrowing: 19.99 -> 19

std::cout << truncated << std::endl;   // 19

static_cast<T>(value) is the safe, explicit way to convert between related types in C++ — prefer it over the old C-style (int)price cast, which is harder to search for and easier to misuse.

How It Actually Works

Every fundamental type maps to a fixed number of bytes the compiler reserves either in a CPU register or on the stack — there's no hidden object header the way there is for, say, a Python int. On a typical 64-bit platform: bool is 1 byte, int is 4 bytes, double is 8 bytes, char is 1 byte. sizeof(x) asks the compiler for that number directly, computed entirely at compile time — it costs nothing at runtime.

Declaring int x = 5; inside a function doesn't call any allocator: the compiler has already decided, while generating machine code for that function, how many bytes of stack space the function needs in total, and x is just a fixed offset into that reserved block (e.g. "4 bytes starting at rbp - 12" in x86-64 terms). Assigning to x is a single mov instruction.

Type conversions are where the mechanism matters most. int i = 3.9; doesn't round — the compiler emits a truncating float-to-int conversion instruction, so the fractional part is discarded, giving 3. Mixing int and double in an expression triggers implicit promotion: the int is widened to double before the operation, so 7 / 2 is integer division (3, remainder discarded at the machine level) while 7 / 2.0 promotes 7 to 7.0 first and does floating-point division. Integer overflow on signed types is undefined behavior — the bit pattern wraps according to two's complement in practice on virtually every real compiler, but the standard doesn't guarantee it, which is why sanitizers flag it even when the output "looks right."

🔀 See this in another language

Exercise

Write a program that declares a rectangle's width and height as double, computes and prints its area and perimeter, then declares an int number of items and a double pricePerItem, computing the total cost. Use const for any value that shouldn't change, and use static_cast to print the total cost rounded down to a whole number of dollars.