C++ Variables, Data Types, constexpr, auto & Scope Masterclass
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.
| Type | Size | Range | Example Literal |
|---|---|---|---|
bool | 1 byte | true / false | true, false |
char | 1 byte | -128 to 127 (or 0-255) | 'A', '
', ' ' |
signed char | 1 byte | -128 to 127 | -100 |
unsigned char | 1 byte | 0 to 255 | 255u |
short | 2 bytes | -32,768 to 32,767 | 32000 |
unsigned short | 2 bytes | 0 to 65,535 | 65000u |
int | 4 bytes | -2,147,483,648 to 2,147,483,647 | 42, -100 |
unsigned int | 4 bytes | 0 to 4,294,967,295 | 4000000000u |
long | 4/8 bytes | platform-dependent | 100L |
long long | 8 bytes | -9.2Γ10ΒΉβΈ to 9.2Γ10ΒΉβΈ | 1000000000LL |
unsigned long long | 8 bytes | 0 to 1.8Γ10ΒΉβΉ | 18000000000000000000ULL |
float | 4 bytes | Β±3.4Γ10Β³βΈ (7 sig digits) | 3.14f |
double | 8 bytes | Β±1.7Γ10Β³β°βΈ (15 sig digits) | 3.14, 3.14e2 |
long double | 8-16 bytes | platform-dependent (β₯double) | 3.14L |
#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;
}
#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
";
}
#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;
}
#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;
}
#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;
}
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.