Skip to content
C++
Language
since C++11
Beginner

Declare and Use Variables in C++

Learn to declare, initialize, and use typed variables in C++ — the building block behind every program you will write.

By the end of this page, you will be able to declare variables of the most common C++ types, initialize them safely using modern syntax, mark values that should never change with const, and recognize the mistakes that trip up almost every beginner.

What and Why

Every program needs to remember things — a player's score, a temperature reading, a username. A variable is a named slot in memory where your program stores one piece of data. Before you can use that slot, you have to tell C++ two things: what the slot is called and what kind of data it will hold.

That second requirement is what makes C++ a statically typed language. The type is not decoration — it determines how many bytes the variable occupies, what operations are legal on it, and what the bit pattern means. Storing a fractional number in a slot meant for a whole number is a mistake C++ catches before your program even runs.

This compile-time checking feels strict at first, but it is one of the things that makes C++ programs reliable. The compiler is your first line of defense against bugs.

Step by Step

1 — Declaring a variable

A declaration names the variable and picks its type. Nothing more.

cpp
#include <iostream>

int main() {
    int score;        // declares an integer variable called score
    score = 42;       // assigns the value 42 to it
    std::cout << score << "\n";
}

int is short for integer — a whole number (no decimal point). The line int score; reserves memory; the line score = 42; puts a value there.

Warning: reading an uninitialized variable like score before assigning to it is undefined behavior — your program may print garbage, crash, or do something worse. Always initialize.

2 — Initializing at the point of declaration

C++ lets you declare and initialize in one step. Prefer this — it eliminates the uninitialized-variable trap entirely.

cpp
#include <iostream>

int main() {
    int score = 42;
    std::cout << score << "\n";
}

C++11 introduced brace initialization (also called uniform initialization), which works for every type and is harder to misuse:

cpp
#include <iostream>

int main() {
    int    score   {42};
    double ratio   {3.14};
    bool   active  {true};
    char   grade   {'A'};

    std::cout << score << " " << ratio << " " << active << " " << grade << "\n";
}

Braces refuse to silently truncate values (e.g., stuffing a double into an int), so the compiler will error rather than lose precision. This is the initialization style you should reach for by default throughout this site.

3 — The most common built-in types

C++ has several categories of fundamental types. Here are the ones you will use most often:

TypeCategoryMeaningTypical size
intIntegerwhole number (signed)32 bits
long longIntegerlarger whole numberat least 64 bits
unsigned intIntegernon-negative whole number32 bits
floatFloating-pointfractional number (less precise)32 bits
doubleFloating-pointfractional number (more precise)64 bits
boolBooleantrue or false8 bits
charCharactera single character8 bits

For most everyday uses int and double cover what you need. Use long long when values may exceed ~2 billion. Use float only when you deliberately need less precision (e.g., GPU/graphics work).

cpp
#include <iostream>

int main() {
    int          apples       {5};
    long long    population   {8'100'000'000LL};   // digit separator for readability
    unsigned int flags        {255u};
    float        temperature  {98.6f};             // f suffix marks a float literal
    double       price        {1.99};
    bool         inStock      {true};
    char         rating       {'B'};

    std::cout << apples << " apples at $" << price << "\n";
    std::cout << "World population: " << population << "\n";
    std::cout << "In stock: " << inStock << "\n";
}

float vs double: Floating-point literals without a suffix (e.g., 3.14) are double. Add an f suffix (3.14f) to make them float. Avoid mixing the two in arithmetic unless you have a specific reason — double is almost always the right default.

4 — The sizeof operator

sizeof returns the size of a type or variable in bytes. This is useful when you need to know exactly how much memory something occupies.

cpp
#include <iostream>

int main() {
    std::cout << "int:       " << sizeof(int)       << " bytes\n";
    std::cout << "long long: " << sizeof(long long) << " bytes\n";
    std::cout << "double:    " << sizeof(double)    << " bytes\n";
    std::cout << "float:     " << sizeof(float)     << " bytes\n";
    std::cout << "bool:      " << sizeof(bool)      << " bytes\n";
    std::cout << "char:      " << sizeof(char)      << " bytes\n";
}

On a typical 64-bit system this prints 4 8 8 4 1 1. Sizes can vary between platforms, which is why the fixed-width types in the next section exist.

5 — Let the compiler deduce the type with auto

When the type is obvious from the initializer, you can write auto instead and let the compiler figure it out. This reduces repetition without hiding what is happening.

cpp
#include <iostream>

int main() {
    auto score   {42};     // deduced as int
    auto ratio   {3.14};   // deduced as double
    auto flag    {true};   // deduced as bool
    auto letter  {'A'};    // deduced as char

    // auto forces an initializer — you cannot write:
    // auto x;   // compile error: nothing to deduce from

    std::cout << score << " " << ratio << "\n";
}

auto is especially useful in longer declarations (iterators, range loops, complex return types) where spelling out the type would be noisy. For simple variables at the beginner level, explicit types are often clearer — use auto when the type is genuinely obvious or verbose.

Watch out: auto s {"hello"}; deduces const char*, not std::string. Write std::string s {"hello"}; when you need a proper string object.

6 — Text with std::string

For anything longer than a single character you need std::string from the <string> header.

cpp
#include <iostream>
#include <string>

int main() {
    std::string name {"Alice"};
    std::cout << "Hello, " << name << "!\n";

    name = "Bob";        // reassignment works just like int
    std::cout << "Hello, " << name << "!\n";
}

7 — Constants with const

When a value should never change after initialization, say so with const. The compiler will reject any code that tries to modify it.

cpp
#include <iostream>

int main() {
    const double kPi {3.14159265358979};
    // kPi = 3.0;  // error: cannot assign to a const variable

    double radius {5.0};
    double area   {kPi * radius * radius};
    std::cout << "Area: " << area << "\n";
}

Making values const is not just a safety measure — it documents intent. A future reader (including yourself in six months) immediately knows this value is fixed.

Common Patterns

Pattern 0 — Fixed-width integers when exact size matters

The size of int and long long can vary between platforms. When you need a guaranteed size — for file formats, network protocols, or embedded targets — use the fixed-width types from <cstdint>:

cpp
#include <cstdint>
#include <iostream>

int main() {
    std::int32_t   counter  {0};              // always exactly 32-bit signed
    std::uint32_t  flags    {0xFF'FF'00'00u}; // always exactly 32-bit unsigned
    std::int64_t   fileSize {1'073'741'824LL};// 1 GiB, always 64-bit signed

    std::cout << sizeof(std::int32_t) << " bytes\n";  // always 4
}
Fixed typeSizeSigned range
std::int8_t1 byte−128 to 127
std::int16_t2 bytes−32 768 to 32 767
std::int32_t4 bytes−2 147 483 648 to 2 147 483 647
std::int64_t8 bytes±9.2 × 10¹⁸

uint variants (std::uint32_t, etc.) are unsigned (non-negative) and hold twice the positive range.

Note: std::int8_t often behaves like char on many compilers. Prefer std::int16_t or wider for numeric values.

Pattern 1 — Accumulating a running total

cpp
#include <iostream>

int main() {
    int total {0};

    total = total + 10;
    total += 25;    // shorthand for total = total + 25
    total += 5;

    std::cout << "Total: " << total << "\n";   // 40
}

The += operator is idiomatic C++ for accumulation loops.

Pattern 2 — Capturing user input

cpp
#include <iostream>
#include <string>

int main() {
    std::string username;
    int age {0};

    std::cout << "Enter your name: ";
    std::cin >> username;

    std::cout << "Enter your age: ";
    std::cin >> age;

    std::cout << "Hello, " << username << ". You are " << age << " years old.\n";
}

std::cin >> reads from the keyboard and stores the result in the variable you give it. The type of the variable tells cin how to parse the input.

Pattern 3 — Computed constants

Group all magic numbers at the top of a function as named constants. This makes the intent clear and the code easy to update.

cpp
#include <iostream>

int main() {
    const int    kSecondsPerMinute {60};
    const int    kMinutesPerHour   {60};
    const double kTaxRate          {0.08};

    int    durationMinutes {90};
    double itemPrice       {49.99};

    int    durationSeconds {durationMinutes * kSecondsPerMinute};
    double totalWithTax    {itemPrice * (1.0 + kTaxRate)};

    std::cout << durationMinutes << " min = " << durationSeconds << " sec\n";
    std::cout << "Price with tax: $" << totalWithTax << "\n";
}

What Can Go Wrong

Mistake 1 — Reading before writing

cpp
int score;
std::cout << score;  // undefined behavior — score has no value yet

Fix: always initialize at the point of declaration.

cpp
int score {0};
std::cout << score;  // safe: prints 0

Mistake 2 — Integer division surprises

cpp
#include <iostream>

int main() {
    int a {7};
    int b {2};
    std::cout << a / b << "\n";   // prints 3, not 3.5
}

When both operands are int, / performs integer division and discards the remainder. If you need the fractional part, at least one operand must be a double.

cpp
#include <iostream>

int main() {
    double a {7.0};
    int    b {2};
    std::cout << a / b << "\n";   // prints 3.5
}

Mistake 3 — Narrowing inside braces catches you (intentionally)

cpp
double pi {3.14};
int    truncated {pi};   // compile error with brace-init — value would be narrowed

This is the brace-initialization safety net working correctly. Either use a double variable or perform an explicit cast to make your intent visible:

cpp
int truncated {static_cast<int>(pi)};   // explicit: you mean to drop the decimal

Mistake 4 — Forgetting the std:: prefix

string alone does not work — you either need std::string or a using namespace std; declaration (which is fine in small examples but avoided in larger programs for clarity).

Quick Reference

ConceptSyntaxNotes
Declare + initializeint x {42};Prefer braces in C++11 and later
Reassignx = 99;Only for non-const variables
Constantconst int x {42};Must initialize; cannot reassign
Type deductionauto x {42};Compiler picks the type from the value
Integer typesint, long long, unsigned intlong long for very large numbers
Floating-pointfloat (less precise), double (default)Float literals need f suffix: 3.14f
Other typesbool, char, std::stringNeed <string> for the last one
Fixed-widthstd::int32_t, std::uint64_tFrom <cstdint>; guaranteed size
Inspect sizesizeof(int)Returns bytes as std::size_t
Accumulatex += value;Equivalent to x = x + value;
Read inputstd::cin >> x;Parses according to type of x

All examples on this page require C++11 or later. Compile with -std=c++11 (or any newer flag).

auto Type Deduction Rules

auto asks the compiler to deduce the variable's type from its initializer, but it silently strips top-level const qualifiers and references from the deduced type. This stripping behavior surprises beginners who expect auto to copy a type exactly as-is. The fix is to spell out what you want: use auto& to keep a reference, and const auto& to keep a const reference.

cpp
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::string name {"Alice"};

    auto        copy  = name;    // std::string — a new copy
    auto&       ref   = name;    // std::string& — a reference to name
    const auto& cref  = name;    // const std::string& — read-only reference

    ref = "Bob";                 // modifies name through the reference
    std::cout << name << "\n";   // "Bob"
    // cref = "Carol";           // error: cref is const

    // In range-for loops the same rules apply:
    std::vector<int> v {1, 2, 3};

    for (auto x : v)         { /* x is a copy — modifying x does not affect v */ }
    for (auto& x : v)        { x *= 2; }   // modifies elements in place
    for (const auto& x : v)  { std::cout << x << " "; }   // 2 4 6
    std::cout << "\n";
}

As a rule of thumb: use const auto& when you are only reading a value (avoids an unnecessary copy), auto& when you need to modify through the reference, and plain auto when you genuinely want an independent copy.


Structured Bindings (C++17)

Structured bindings let you unpack a compound value — a pair, tuple, array, or aggregate struct — into named variables in a single declaration. Before C++17 you had to access pair.first and pair.second directly, or call std::get<0>(tuple) and std::get<1>(tuple). Structured bindings make the same code far more readable.

cpp
#include <iostream>
#include <map>
#include <string>
#include <tuple>
#include <utility>

// Works with std::pair
void demo_pair() {
    std::pair<std::string, int> entry {"Alice", 42};
    auto [name, age] = entry;   // 'name' is std::string, 'age' is int
    std::cout << name << " is " << age << "\n";
}

// Works with std::tuple
void demo_tuple() {
    auto data = std::make_tuple("Bob", 3.14, true);
    auto [label, value, active] = data;
    std::cout << label << " " << value << " " << active << "\n";
}

// Works with map iteration — the classic use case
void demo_map() {
    std::map<std::string, int> scores {{"Alice", 95}, {"Bob", 82}};

    for (const auto& [name, score] : scores) {
        std::cout << name << ": " << score << "\n";
    }
}

// Works with plain arrays
void demo_array() {
    int rgb[3] {255, 128, 0};
    auto [r, g, b] = rgb;
    std::cout << "R=" << r << " G=" << g << " B=" << b << "\n";
}

int main() {
    demo_pair();
    demo_tuple();
    demo_map();
    demo_array();
}

Structured bindings bind to the members in declaration order. They are syntactic sugar — under the hood the compiler creates a single anonymous variable and makes each name an alias for one element. Requires -std=c++17.


decltype — Inspecting the Type of an Expression

decltype(expr) produces the type of expr without evaluating it — the expression is never executed, only its type is examined. This is useful in template and generic code where you need to declare a variable or return type that matches some other expression's type, without knowing what that type is ahead of time.

cpp
#include <iostream>
#include <type_traits>

int main() {
    int   a {5};
    float b {2.5f};

    // Declare 'result' to have exactly the type of (a * b)
    // — which is float because of implicit conversion rules
    decltype(a * b) result = a * b;
    std::cout << result << "\n";   // 12.5

    // decltype preserves references and const, unlike auto
    int& ref = a;
    decltype(ref) ref2 = a;    // ref2 is int& (reference preserved)
    // decltype(ref) alone;    // would be error: reference must be initialized

    // decltype(auto) combines deduction with full preservation:
    // useful in return types where you want to forward the exact type
    auto     copy  = ref;          // int (auto strips the reference)
    decltype(auto) preserved = ref; // int& (decltype(auto) keeps it)

    std::cout << std::is_same_v<decltype(copy),      int>  << "\n";  // 1
    std::cout << std::is_same_v<decltype(preserved), int&> << "\n";  // 1
}

For everyday beginner code decltype rarely appears directly — but you will encounter it in library headers and template signatures. Its key property is that it is purely a compile-time introspection tool: the operand expression is never run.


Value Categories: lvalue, rvalue, and xvalue

Every expression in C++ belongs to a value category that determines whether you can take its address and whether it can be moved from — understanding the basics protects you from confusing compiler errors. The full taxonomy has five categories, but at the beginner level three matter: an lvalue is an expression that refers to a named, persistent object; an rvalue is a temporary with no name that will be destroyed at the end of the statement; an xvalue is an object that is being explicitly moved.

cpp
#include <iostream>
#include <string>

void takes_lvalue_ref (std::string&       s) { std::cout << "lvalue ref\n"; }
void takes_rvalue_ref (std::string&&      s) { std::cout << "rvalue ref\n"; }

int main() {
    std::string name {"Alice"};

    takes_lvalue_ref(name);          // name is an lvalue — has a name, lives on
    takes_rvalue_ref(std::string{"Bob"});  // temporary is an rvalue — no name

    // You cannot bind an rvalue to a plain lvalue reference:
    // takes_lvalue_ref(std::string{"Charlie"});  // error

    // You cannot take the address of an rvalue:
    // &(name + "!");   // error — result of + is a temporary

    // An lvalue can be explicitly cast to an rvalue with std::move:
    // (This signals "I no longer need name's contents — move them")
    takes_rvalue_ref(std::move(name));   // xvalue: named object treated as movable
    // name is now in a valid but unspecified state — don't use it after moving
}

Why does this matter? It controls which constructor or assignment operator overload is called, and whether the compiler copies data or moves it (cheap). For now, the practical rule is: prefer const& parameters for things you only read, and && parameters for things you are taking ownership of. The deeper move-semantics story is in Move Semantics.


Implicit Conversions and Narrowing

C++ silently converts between numeric types in many situations, but these conversions can lose information — brace initialization exists specifically to catch such losses at compile time. When you use = or () to initialize a variable, the compiler will quietly truncate a double to an int, or wrap an int around if it overflows an unsigned char. With {} initialization, the same conversions become compile errors.

cpp
#include <iostream>

int main() {
    double pi = 3.14159;

    int a = pi;        // OK syntactically — silently drops the decimal: a == 3
    // int b {pi};     // error: narrowing conversion from double to int

    int big = 300;
    unsigned char c = big;  // OK syntactically — wraps to 44 (300 % 256)
    // unsigned char d {big};  // error: narrowing

    // Explicit cast: you say "I know what I'm doing"
    int truncated {static_cast<int>(pi)};   // fine: explicit, readable
    std::cout << truncated << "\n";         // 3

    // Integer promotions: small types promote to int in arithmetic
    char x = 10;
    char y = 20;
    // decltype(x + y) is int, not char — the sum is computed as int
    auto sum = x + y;   // sum is int
    std::cout << sizeof(sum) << "\n";  // 4 (int), not 1 (char)
}

The practical advice: always initialize with {} so narrowing is a compile error rather than a silent surprise. When you need to convert between types on purpose, use static_cast<TargetType>(value) to make the conversion explicit and searchable in code review.


constexpr Variables

A constexpr variable is evaluated entirely at compile time and baked into the program image — it is a true compile-time constant, not merely a read-only variable. const means "do not change this after initialization", but the initializer can still be a runtime value. constexpr is stricter: the initializer must be a constant expression that the compiler can compute before the program starts, which allows it to appear in array sizes, template arguments, and other places that require compile-time values.

cpp
#include <iostream>
#include <array>

int main() {
    // const — read-only, but the value could come from runtime (e.g., user input)
    const int runtimeConst = 42;

    // constexpr — guaranteed compile-time; can be used as array size
    constexpr int kMaxItems = 64;
    constexpr double kPi    = 3.14159265358979;
    constexpr int kSquare   = kMaxItems * kMaxItems;   // computed at compile time

    std::array<int, kMaxItems> buffer {};  // fine: kMaxItems is compile-time

    // A plain const variable whose value is known at compile time is also
    // usable in this context on most compilers — but constexpr makes it explicit:
    // std::array<int, runtimeConst> buf2 {};  // may or may not work — avoid

    std::cout << "Pi: "           << kPi      << "\n";
    std::cout << "Max items: "    << kMaxItems << "\n";
    std::cout << "Max squared: "  << kSquare   << "\n";
    std::cout << "Buffer size: "  << buffer.size() << "\n";
}

Prefer constexpr over plain const for numeric constants that never depend on runtime input. It communicates intent precisely, enables compiler optimizations, and guarantees that the value is available wherever a constant expression is required. Functions can also be constexpr — a function marked constexpr can be called at compile time when all its arguments are constants, and at runtime when they are not.


What's Next

With variables and types in hand, the next step is controlling the flow of your program:

When you are ready to go deeper on the type system itself, the fundamental types reference covers every built-in type, its size guarantees, and the fixed-width alternatives like int32_t.