C++ Variables and Fundamental Types
Understand C++ fundamental types, fixed-width integers, floating point, char, bool, const, and how initialization actually works in modern C++.
What you'll learn
- ✓Declare and initialize variables the modern way
- ✓Pick the right integer and floating-point type
- ✓Use const, constexpr, and auto effectively
- ✓Avoid the classic narrowing and overflow traps
- ✓Read type names with cv-qualifiers correctly
Prerequisites
- •A working compiler — see /blog/cpp-install-and-first-program
C++ is statically typed: every variable has a type the compiler knows at compile time. Picking the right type avoids whole categories of bugs and is the first habit of a competent C++ programmer.
Fundamental types
C++ inherits a small set of fundamental types from C and layers a few extras on top. The ones you will use daily:
int— a signed integer, at least 16 bits, usually 32 on modern systems.long,long long— wider signed integers;long longis at least 64 bits.unsigned int,unsigned long long— non-negative integers; wrap on overflow.float,double,long double— IEEE 754 floating point.char,signed char,unsigned char— 1 byte, used for bytes or ASCII.bool—trueorfalse.
The widths of int and long are platform-dependent. When you need a precise size, use the fixed-width aliases from <cstdint>.
#include <cstdint>
std::int32_t pixel_count = 1920 * 1080;
std::uint64_t file_size = 4'500'000'000ULL;
std::int8_t temperature = -12;
Note the digit separators (') and the ULL suffix forcing the literal type. Both are modern conveniences.
Initialization styles
C++ has several initialization syntaxes. Modern code should prefer brace initialization because it disallows narrowing conversions.
int a = 5; // copy initialization
int b(5); // direct initialization
int c{5}; // brace initialization — preferred
int d{}; // value initialization to 0
// int e{3.14}; // error: narrowing conversion rejected
int e = 3.14; // compiles, silently truncates to 3
Brace init catches the bug at compile time. Keep this habit and you will lose hours fewer to silent truncation.
const and constexpr
const marks a variable as immutable after initialization. constexpr is stronger: the value must be computable at compile time.
const int hours_per_day = 24;
constexpr int seconds_per_day = 24 * 60 * 60;
constexpr int square(int x) { return x * x; }
constexpr int s = square(9); // computed at compile time
Default to const for any local you do not intend to mutate. The compiler optimizes around it and reviewers immediately know your intent.
auto and type deduction
auto tells the compiler to deduce the type from the initializer. It is not dynamic typing; the type is still fixed at compile time.
auto count = 42; // int
auto ratio = 3.14; // double
auto name = std::string{"ada"};
Use auto when the type is obvious from the right-hand side or when spelling it out adds noise (think iterator types). Avoid auto when the explicit type makes the code easier to read.
Floating point reality
float is 32-bit and gives ~7 decimal digits of precision. double is 64-bit and gives ~15. Default to double unless you have a measured reason to use float. Never compare floats with ==:
#include <cmath>
bool nearly_equal(double a, double b, double eps = 1e-9) {
return std::abs(a - b) < eps;
}
Signed vs unsigned
Use signed integers by default. Unsigned types wrap silently on underflow, which produces astronomically large values that crash loops and indexing logic. Reserve unsigned for bit manipulation, sizes from the standard library (std::size_t), and protocols that demand it.
for (int i = 0; i < n; ++i) { /* fine */ }
// Subtle bug: if v.size() is 0, the expression below is huge.
for (std::size_t i = 0; i < v.size() - 1; ++i) { /* dangerous */ }
Type aliases
Long type names hurt readability. Use using to alias them. typedef works too but the syntax is awkward.
using Id = std::uint64_t;
using Callback = void(*)(int);
Id user_id = 42;
A worked example
Combining the ideas — read a temperature, convert, print:
#include <iostream>
int main() {
constexpr double freezing_f = 32.0;
constexpr double scale = 5.0 / 9.0;
std::cout << "Enter temperature in Fahrenheit: ";
double f{};
std::cin >> f;
const double c = (f - freezing_f) * scale;
std::cout << f << "F = " << c << "C\n";
}
Notice the use of constexpr for compile-time constants, brace init for f, and const for c. Every choice is deliberate.
Common pitfalls
- Integer division:
1 / 2is0. Cast one operand todoubleto force floating math. - Implicit narrowing in
=initialization. Brace init catches it. - Mixing signed and unsigned in comparisons triggers warnings and surprising results.
- Uninitialized locals contain garbage; always initialize.
Where to go next
You can now hold data. Time to do something with it. Read Control Flow to branch and loop, then Functions and References to factor logic into reusable units.
Wrap up
Pick the narrowest type that fits your data, default to brace initialization, mark immutables const or constexpr, and reach for auto only when it adds clarity. These habits prevent most of the silent bugs that plague C++ codebases.