C++ Variables, Data Types, constexpr, auto & Scope Masterclass

⚑ Modern C++ (C++17 / C++20 / C++23) 🟒 Lesson 2 πŸ“‚ Phase 2: Variables & Data Types πŸ“… 2026 Master Edition
πŸ“Œ Covered in this in-depth guide: All Fundamental Types Β· sizeof Β· numeric_limits Β· fixed-width types Β· Initialization Styles Β· Value-Init Β· Scope & Shadowing Β· Static Local Β· const Β· constexpr Β· Pointer Const-ness Β· Type Conversion Β· static_cast Β· auto Β· decltype

Welcome to Phase 2 (Chapter 2): C++ Variables & Data Types Masterclass! Every piece of data in C++ has a type that determines its size in memory, the operations it supports, and the range of values it can hold. C++ is statically typed β€” all variable types are known at compile time, enabling maximum optimization. Modern C++ adds auto for type deduction and constexpr for compile-time constants.

1Fundamental Types β€” Complete Reference
TypeSizeRangeExample Literal
bool1 bytetrue / falsetrue, false
char1 byte-128 to 127 (or 0-255)'A', ' ', ''
signed char1 byte-128 to 127-100
unsigned char1 byte0 to 255255u
short2 bytes-32,768 to 32,76732000
unsigned short2 bytes0 to 65,53565000u
int4 bytes-2,147,483,648 to 2,147,483,64742, -100
unsigned int4 bytes0 to 4,294,967,2954000000000u
long4/8 bytesplatform-dependent100L
long long8 bytes-9.2Γ—10¹⁸ to 9.2Γ—10¹⁸1000000000LL
unsigned long long8 bytes0 to 1.8Γ—10¹⁹18000000000000000000ULL
float4 bytesΒ±3.4Γ—10³⁸ (7 sig digits)3.14f
double8 bytesΒ±1.7Γ—10³⁰⁸ (15 sig digits)3.14, 3.14e2
long double8-16 bytesplatform-dependent (β‰₯double)3.14L
C++ β€” Types, sizeof, numeric_limitsβ–Ά Run in Compiler
#include <iostream>
#include <limits>
#include <cstdint>   // fixed-width types

int main() {
    // Fundamental types
    bool       b   = true;
    char       c   = 'A';
    int        i   = 2'147'483'647;    // digit separator (C++14)
    long long  ll  = 9'223'372'036'854'775'807LL;
    float      f   = 3.14f;
    double     d   = 3.14159265358979;
    long double ld  = 3.14159265358979323846L;

    std::cout << "sizeof bool   = " << sizeof(bool)        << " bytes
";
    std::cout << "sizeof char   = " << sizeof(char)        << " bytes
";
    std::cout << "sizeof int    = " << sizeof(int)         << " bytes
";
    std::cout << "sizeof long   = " << sizeof(long)        << " bytes
";
    std::cout << "sizeof ll     = " << sizeof(long long)   << " bytes
";
    std::cout << "sizeof float  = " << sizeof(float)       << " bytes
";
    std::cout << "sizeof double = " << sizeof(double)      << " bytes
";

    // numeric_limits β€” portable way to get type bounds
    std::cout << "
int min: " << std::numeric_limits<int>::min() << "
";
    std::cout << "int max: " << std::numeric_limits<int>::max() << "
";
    std::cout << "double max: " << std::numeric_limits<double>::max() << "
";
    std::cout << "double eps: " << std::numeric_limits<double>::epsilon() << "
";
    std::cout << "float digits: " << std::numeric_limits<float>::digits10 << "
";

    // Fixed-width types (portable β€” use in embedded/systems code)
    int8_t   s8  = -127;
    uint8_t  u8  = 255;
    int16_t  s16 = -32768;
    int32_t  s32 = 2147483647;
    int64_t  s64 = 9223372036854775807LL;
    uint64_t u64 = 18446744073709551615ULL;
    std::cout << "
Fixed-width: int8=" << (int)s8 << " uint8=" << (int)u8
              << " int64=" << s64 << "
";
    return 0;
}
2Variable Declaration, Initialization & Scope
C++ β€” All initialization styles, scope, shadowingβ–Ά Run in Compiler
#include <iostream>
#include <string>

int globalVar = 100;   // global scope β€” accessible everywhere

int main() {
    // ─── Initialization styles ────────────────────────────────────────────
    int a = 5;              // copy initialization (C-style)
    int b(10);              // direct initialization
    int c{15};              // uniform brace initialization (C++11, prevents narrowing)
    int d = {20};           // copy-list initialization
    auto e = 25;            // type deduction β€” e is int
    auto f = 3.14;          // f is double
    auto g = 'X';           // g is char
    auto h = std::string{"hi"};  // h is std::string

    // int bad{3.14};       // COMPILE ERROR! Narrowing: 3.14 (double) β†’ int
    int notBad = 3.14;      // WARNING at best β€” silently truncates to 3 (use {} instead!)

    // Default initialization
    int uninit;             // UNDEFINED VALUE β€” do NOT read before writing!
    int zero{};             // value-initialized = 0 (guaranteed!)
    int* nullPtr{};         // value-initialized = nullptr
    std::cout << "zero-init int: " << zero << "
";

    // ─── Scope ───────────────────────────────────────────────────────────
    std::cout << "global: " << globalVar << "
";

    int x = 10;             // function scope
    {
        int x = 20;         // block scope β€” SHADOWS outer x
        std::cout << "inner x = " << x << "
";  // 20
    }
    std::cout << "outer x = " << x << "
";      // 10 β€” inner x destroyed

    // Loop variable scope
    for (int i = 0; i < 3; ++i) {
        // i is scoped to the for loop
    }
    // std::cout << i;  // ERROR: i not in scope!

    // Capture global from block
    {
        int globalVar = 999;  // local shadows global
        std::cout << "local shadows global: " << globalVar << "
";
        std::cout << "access global with ::globalVar: " << ::globalVar << "
";
    }
    return 0;
}

void demoStaticLocal() {
    static int callCount = 0;  // static local: initialized once, persists between calls
    ++callCount;
    std::cout << "Called " << callCount << " times
";
}
3const, constexpr & constinit
C++ β€” const, constexpr, constinit, const pointersβ–Ά Run in Compiler
#include <iostream>
#include <cmath>

// const β€” runtime constant (value known at runtime OR compile time)
const double TAX_RATE = 0.18;

// constexpr β€” compile-time constant (MUST be known at compile time)
constexpr double PI     = 3.14159265358979;
constexpr int    MAX_N  = 1000;
constexpr double E      = 2.71828182845904;

// constexpr function β€” computed at compile time if args are constexpr
constexpr int power(int base, int exp) {
    int result = 1;
    for (int i = 0; i < exp; ++i) result *= base;
    return result;
}

constinit double gRate = TAX_RATE;  // guaranteed compile-time init of global

int main() {
    // const β€” cannot be changed after initialization
    const int x = 10;
    // x = 20;  // COMPILE ERROR!

    const int y = [](){ return 42; }();  // can be runtime value
    constexpr int z = power(2, 8);        // MUST be compile time = 256

    std::cout << "PI = " << PI << "
";
    std::cout << "2^8 = " << z << "
";   // 256, computed at compile time

    // Use in array size (constexpr REQUIRED, const not always OK)
    constexpr int SIZE = 10;
    int arr[SIZE]{};                        // OK: constexpr as array size
    // const int n = 10; int arr2[n]{};    // may work but non-standard (VLA)

    // Pointer const-ness β€” four combinations
    int value = 42;
    int other = 99;

    int* p1 = &value;              // pointer to int (both mutable)
    *p1 = 50;  p1 = &other;       // both OK

    const int* p2 = &value;       // pointer to const int (data immutable)
    // *p2 = 50;                  // COMPILE ERROR β€” can't change data
    p2 = &other;                  // OK β€” pointer itself can change

    int* const p3 = &value;       // const pointer to int (pointer immutable)
    *p3 = 50;                     // OK β€” can change data
    // p3 = &other;               // COMPILE ERROR β€” can't rebind pointer

    const int* const p4 = &value; // const pointer to const int (both immutable)
    // *p4 = 50;                  // COMPILE ERROR
    // p4 = &other;               // COMPILE ERROR

    std::cout << "value = " << value << "
";
    return 0;
}
4Type Conversion & Casting
C++ β€” Implicit/explicit conversion, static_castβ–Ά Run in Compiler
#include <iostream>
#include <string>

int main() {
    // Implicit conversion (widening β€” safe)
    int i = 42;
    double d = i;         // int β†’ double (no data loss) βœ…
    long long ll = i;     // int β†’ long long βœ…

    // Implicit conversion (narrowing β€” DANGEROUS)
    double pi = 3.14159;
    int truncated = pi;   // double β†’ int: silently truncates to 3 ⚠️
    std::cout << "truncated: " << truncated << "
";  // 3

    // Explicit cast β€” static_cast (compile-time checked, preferred in C++)
    double result = static_cast<double>(5) / 2;  // 2.5 (not 2!)
    int rounded = static_cast<int>(3.9);          // 3 (truncates)
    char ch = static_cast<char>(65);              // 'A'
    std::cout << "5/2 = " << result << " rounded: " << rounded << " char: " << ch << "
";

    // C-style cast (avoid β€” no compile-time check, no RTTI)
    double x = (double)5 / 2;   // same as static_cast, but unchecked

    // Integer arithmetic gotcha
    int a = 5, b = 2;
    std::cout << "5/2 = " << a/b << "
";                           // 2 (integer division!)
    std::cout << "5.0/2 = " << 5.0/2 << "
";                       // 2.5
    std::cout << "static_cast: " << static_cast<double>(a)/b << "
";// 2.5

    // bool conversions
    std::cout << std::boolalpha;
    bool b1 = 0;       // false
    bool b2 = 1;       // true
    bool b3 = -42;     // true (any non-zero)
    bool b4 = 0.0;     // false
    std::cout << b1 << " " << b2 << " " << b3 << " " << b4 << "
";

    // char and int relationship
    char letter = 'Z';
    int code = letter;   // 'Z' = 90 in ASCII
    std::cout << "Char 'Z' = " << code << " in ASCII
";
    std::cout << "ASCII 65 = '" << static_cast<char>(65) << "'
";
    return 0;
}
5auto, decltype & Type Deduction
C++ β€” auto, decltype, type deduction rulesβ–Ά Run in Compiler
#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <typeinfo>

int main() {
    // auto β€” deduced from initializer
    auto a = 42;            // int
    auto b = 3.14;          // double
    auto c = 'X';           // char
    auto d = true;          // bool
    auto e = 42LL;          // long long
    auto f = 3.14f;         // float

    std::cout << "types: " << typeid(a).name() << " "
              << typeid(b).name() << " " << typeid(c).name() << "
";

    // auto with references β€” important!
    int x = 10;
    auto  copy = x;    // int (copy)
    auto& ref  = x;    // int& (reference)
    auto&& rref = 42;  // int&& (rvalue reference)
    const auto& cref = x;  // const int&

    ref = 99;
    std::cout << "x after ref=99: " << x << "
";   // 99

    // auto in range-based for β€” critical!
    std::vector<int> v{1, 2, 3, 4, 5};

    for (auto val : v) val *= 2;              // val is a COPY β€” v unchanged
    for (auto& val : v) val *= 2;            // val is a REFERENCE β€” v MODIFIED!
    for (const auto& val : v) {}             // read-only reference (efficient)

    std::cout << "v after doubling: ";
    for (auto n : v) std::cout << n << " ";
    std::cout << "
";

    // auto with complex types (where it really shines)
    std::map<std::string, std::vector<int>> bigMap{{"a", {1,2,3}}};
    auto it = bigMap.find("a");   // beats: std::map<std::string, std::vector<int>>::iterator
    if (it != bigMap.end()) std::cout << it->first << "
";

    // decltype β€” type of expression (doesn't evaluate the expression)
    int y = 5;
    double z = 3.14;
    decltype(y + z) result = y + z;  // double (result type of int+double)
    decltype(y)     copy2  = y;      // int (type of y itself)
    std::cout << "decltype(int+double) result: " << result << "
";

    // AAA (Almost Always Auto) style
    auto pi     = 3.14159;
    auto name   = std::string{"C++"};
    auto nums   = std::vector<int>{1,2,3};
    auto lambda = [](int n){ return n*n; };
    std::cout << lambda(7) << "
";
    return 0;
}
6Technical FAQs

Q1: What is the difference between const and constexpr?

const means a variable cannot be modified after initialization; its value may be known at runtime. constexpr guarantees the value is known at compile time and can be used in compile-time contexts like array sizes, template parameters, and switch cases.

Q2: What are fixed-width integer types?

int8_t, int16_t, int32_t, int64_t (from <cstdint>) have guaranteed sizes regardless of platform. Use them for network protocols, file formats, and embedded systems where exact byte sizes matter.

Q3: Why is int not always 4 bytes?

The C++ standard only guarantees int is at least 16 bits. On most 32/64-bit platforms it's 4 bytes, but embedded systems may have 2-byte int. Use sizeof(int) or fixed-width types when you need exact sizes.

Q4: What happens when you overflow an integer?

Signed integer overflow is undefined behaviour in C++ β€” the compiler can assume it never happens. Unsigned integer overflow is well-defined: it wraps modulo 2^n. Always check bounds or use std::numeric_limits before arithmetic that might overflow.

Q5: When should I use auto?

Use auto when the type is obvious from the initializer (auto p = std::make_unique<Foo>()), for complex iterator types, and for lambda captures. Avoid auto when the type is not obvious from context or when you need to document your intent explicitly.