C++ Data Types

Every value in a C++ program — a number, a letter, a true/false flag — has to be stored somewhere in memory, and the data type tells the compiler exactly how much memory to reserve and how to interpret the bits stored there. Get the type wrong and you can silently lose data, wrap around to a nonsensical value, or waste memory on values that never needed that much space. Understanding C++’s type system isn’t optional trivia — it is the foundation every variable, function signature, and expression in the language rests on.

Overview: How Data Types Work

C++ is a statically typed language: every variable’s type is fixed at compile time and never changes. When you write int age = 25;, the compiler allocates a fixed-size block of memory (on most modern systems, 4 bytes for int), and treats the bits in that block as a signed binary integer. If you later tried to assign age = "hello";, the compiler would reject the program outright — the type is a promise the compiler enforces at compile time, before the program ever runs.

C++’s built-in types fall into a few families:

  • Integer typesint, short, long, long long, and their unsigned variants — store whole numbers.
  • Floating-point typesfloat, double, long double — store numbers with a fractional part, using an approximate binary representation (IEEE 754 on virtually all modern hardware).
  • Character typeschar, wchar_t, char16_t, char32_t — store a single character, which is really just a small integer interpreted as text via a character encoding.
  • Boolean typebool — stores only true or false.
  • void — represents “no type”, used for functions that return nothing.

Every type has a size (how many bytes it occupies) and a range (the minimum and maximum values it can represent). The C++ standard does not fix exact sizes for most types — it only guarantees minimums and relative orderings: sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). On the overwhelming majority of modern desktop and server systems (including where g++ typically targets x86-64 Linux), the common sizes are: char = 1 byte, short = 2 bytes, int = 4 bytes, long = 8 bytes, long long = 8 bytes, float = 4 bytes, double = 8 bytes. This lesson uses those common sizes, but you should always verify with the sizeof operator rather than assume — especially on embedded systems, 32-bit targets, or when writing portable code.

Integer types come in signed (the default, can hold negative numbers) and unsigned (only non-negative, but doubles the positive range for the same number of bits) forms. Internally, signed integers on essentially all modern hardware use two’s complement representation, where the highest-order bit acts as the sign indicator. Floating-point types trade exactness for range: they store a sign, an exponent, and a fraction (mantissa), which means most decimal values (like 0.1) cannot be represented exactly in binary — only approximated.

Syntax

typeName variableName = initialValue;
typeName variableName{initialValue};   // brace initialization (preferred, catches narrowing errors)
typeName variableName;                  // declared but uninitialized (contains garbage!)
  • typeName — one of the built-in types (int, double, char, bool, etc.) or a modifier combination such as unsigned long.
  • variableName — the identifier used to refer to the stored value.
  • initialValue — the value assigned at creation; its type should match, or safely convert to, typeName.
  • sizeof(type-or-variable) — an operator (not a function call) that returns the number of bytes a type or variable occupies, as an unsigned size_t.

Fundamental Types at a Glance

Type Typical Size Typical Range Use For
bool 1 byte true / false Flags, conditions
char 1 byte -128 to 127 Single characters
short 2 bytes -32,768 to 32,767 Small integers, saving memory
int 4 bytes -2,147,483,648 to 2,147,483,647 General-purpose whole numbers
unsigned int 4 bytes 0 to 4,294,967,295 Counts, sizes, bit flags
long 8 bytes* very large (~±9.2 × 10^18 on 64-bit) Large whole numbers
long long 8 bytes ~±9.2 × 10^18 (guaranteed at least 64 bits) Very large counters, IDs
float 4 bytes ~±3.4 × 10^38 (~7 significant digits) Memory-constrained decimals
double 8 bytes ~±1.8 × 10^308 (~15 significant digits) Default choice for decimals

*long is 8 bytes on Linux and macOS (the LP64 data model) but only 4 bytes on 64-bit Windows — another reason to measure with sizeof rather than assume when writing portable code.

Examples

Example 1: Declaring Types and Inspecting Their Size

#include <iostream>
using namespace std;

int main() {
    int age = 25;
    double price = 19.99;
    char grade = 'A';
    bool passed = true;
    long population = 7800000000;
    unsigned int count = 42;

    cout << "age: " << age << " (size: " << sizeof(age) << " bytes)" << endl;
    cout << "price: " << price << " (size: " << sizeof(price) << " bytes)" << endl;
    cout << "grade: " << grade << " (size: " << sizeof(grade) << " bytes)" << endl;

    cout << boolalpha;
    cout << "passed: " << passed << " (size: " << sizeof(passed) << " bytes)" << endl;
    cout << "population: " << population << " (size: " << sizeof(population) << " bytes)" << endl;
    cout << "count: " << count << " (size: " << sizeof(count) << " bytes)" << endl;

    return 0;
}

Output:

age: 25 (size: 4 bytes)
price: 19.99 (size: 8 bytes)
grade: A (size: 1 bytes)
passed: true (size: 1 bytes)
population: 7800000000 (size: 8 bytes)
count: 42 (size: 4 bytes)

Notice that population is declared long instead of int — 7.8 billion is larger than the maximum int value (about 2.1 billion), so it would overflow if stored in a plain int. sizeof confirms exactly how many bytes the compiler reserved for each variable, which is a great way to check assumptions instead of guessing.

Example 2: Unsigned Wraparound (Integer Overflow)

#include <iostream>
using namespace std;

int main() {
    unsigned short small = 65535;
    cout << "before increment: " << small << endl;

    small = small + 1;
    cout << "after increment: " << small << endl;

    unsigned int big = 0;
    big = big - 1;
    cout << "after decrementing 0: " << big << endl;

    return 0;
}

Output:

before increment: 65535
after increment: 0
after decrementing 0: 4294967295

Unsigned integers don’t have a sign bit, so arithmetic on them is defined to “wrap around” using modular arithmetic instead of overflowing undefined-behavior style. A 16-bit unsigned short can hold 0 to 65,535; adding 1 to the maximum wraps back to 0. Subtracting 1 from an unsigned int holding 0 wraps to its maximum value, 4,294,967,295. This is well-defined behavior for unsigned types (unlike signed overflow, which is undefined behavior in C++), but it is rarely what you actually want — it is a frequent source of subtle bugs.

Example 3: Type Ranges and Conversion

#include <iostream>
#include <limits>
using namespace std;

int main() {
    cout << "int range: " << numeric_limits<int>::min() << " to " << numeric_limits<int>::max() << endl;
    cout << "double range: " << numeric_limits<double>::min() << " to " << numeric_limits<double>::max() << endl;

    double pi = 3.14159;
    int truncated = static_cast<int>(pi);
    cout << "pi truncated to int: " << truncated << endl;

    int a = 7;
    int b = 2;
    double result = static_cast<double>(a) / b;
    cout << "7 / 2 as double: " << result << endl;

    return 0;
}

Output:

int range: -2147483648 to 2147483647
double range: 2.22507e-308 to 1.79769e+308
pi truncated to int: 3
7 / 2 as double: 3.5

The <limits> header’s numeric_limits template gives you the exact minimum and maximum for any type on your specific platform — more reliable than memorizing numbers. Converting double to int truncates the fractional part (it does not round). And dividing two ints always performs integer division, discarding any remainder, unless at least one operand is cast to a floating-point type first — which is why static_cast<double>(a) / b is needed to get 3.5 instead of 3.

Under the Hood: Bits, Bytes, and Binary

When you declare a variable, the compiler reserves a contiguous block of memory sized to the type and remembers, at compile time, how to interpret the bits in that block — the hardware itself just sees raw bytes. For integers, that interpretation is two’s complement: the topmost bit represents a negative weight, so an 8-bit signed value ranges from -128 (10000000) to 127 (01111111), while the same 8 bits interpreted as unsigned range from 0 to 255. This is also why signed and unsigned wraparound differ in defined-ness: unsigned overflow is defined as modular wraparound by the standard, while signed integer overflow is undefined behavior — the compiler is free to assume it never happens, which can produce surprising optimization results.

Floating-point types follow the IEEE 754 standard on nearly all modern CPUs: a double uses 1 sign bit, 11 exponent bits, and 52 fraction (mantissa) bits to approximate a huge range of values, but only about 15–17 significant decimal digits are exact. This is why comparing floating-point numbers with == is unreliable — 0.1 + 0.2 does not exactly equal 0.3 in binary floating point.

char is, under the hood, just a 1-byte integer; when you write char grade = 'A';, the compiler stores the integer value 65 (the ASCII code for ‘A’) and cout knows to print it as a character rather than a number. bool is typically stored as a full byte where 0 means false and any nonzero value means true, even though only one bit of information is logically needed.

Common Mistakes

Mistake 1: Assigning a negative value to an unsigned type.

#include <iostream>
using namespace std;

int main() {
    unsigned int x = -5;
    cout << "x: " << x << endl;
    return 0;
}

Output:

x: 4294967291

This compiles (often with a compiler warning) because -5 is silently converted using modular arithmetic into a huge positive number instead of producing an error. If a value can legitimately be negative, don’t use an unsigned type just because “it’s a count” — use int or a signed type instead:

#include <iostream>
using namespace std;

int main() {
    int x = -5;
    cout << "x: " << x << endl;
    return 0;
}

Output:

x: -5

Mistake 2: Comparing signed and unsigned integers.

#include <iostream>
using namespace std;

int main() {
    int a = -1;
    unsigned int b = 1;
    if (a < b) {
        cout << "a is less than b" << endl;
    } else {
        cout << "a is NOT less than b (surprise!)" << endl;
    }
    return 0;
}

Output:

a is NOT less than b (surprise!)

When you compare a signed and an unsigned integer, C++ implicitly converts the signed value to unsigned before comparing. Here, -1 becomes a huge positive number (4,294,967,295), so a < b is false even though mathematically -1 is clearly less than 1. Avoid mixing signedness in comparisons — keep both operands the same signedness:

#include <iostream>
using namespace std;

int main() {
    int a = -1;
    int b = 1;
    if (a < b) {
        cout << "a is less than b" << endl;
    } else {
        cout << "a is NOT less than b" << endl;
    }
    return 0;
}

Output:

a is less than b

Best Practices

  • Default to int for whole numbers and double for decimals unless you have a specific reason (memory constraints, needing a huge range) to choose otherwise.
  • Only use unsigned types for values that are logically never negative (like array indices or bit masks), and even then, be careful with subtraction and comparisons against signed values.
  • Prefer brace initialization (int x{5};) over = initialization when possible — it causes a compile error on narrowing conversions instead of silently truncating.
  • Never leave a variable uninitialized; an uninitialized fundamental type contains whatever garbage bits were already in memory.
  • Use <cstdint> fixed-width types (int32_t, uint64_t, etc.) when the exact bit width matters, such as in file formats, network protocols, or cross-platform code.
  • Never compare floating-point numbers with ==; instead check whether the difference is within a small tolerance (an epsilon).
  • Use sizeof and numeric_limits to verify assumptions about type sizes and ranges rather than hardcoding numbers from memory.

Practice Exercises

Exercise 1: Declare a variable of each fundamental type covered in this lesson (int, double, char, bool, short, long) with a sensible value, and print each one’s sizeof alongside its value.

Exercise 2: Write a program that stores the value 200 in a variable of type char (assume a signed 8-bit char, range -128 to 127) and prints the result. Explain in a comment why the printed value is not 200.

Exercise 3: Write a program that divides two integer variables holding 9 and 4 and prints the result three ways: as plain integer division, after casting only the numerator to double, and after casting both operands to double. Compare the three outputs and explain why they differ (or don’t).

Summary

  • C++ is statically typed: every variable’s type, and therefore its size and valid range, is fixed at compile time.
  • Integer types (int, short, long, long long) come in signed and unsigned forms; unsigned types wrap around using modular arithmetic instead of going negative.
  • Floating-point types (float, double) use IEEE 754 binary approximation, so they trade exactness for enormous range — never compare them with ==.
  • char is really a 1-byte integer interpreted as text, and bool stores only true/false.
  • Use sizeof and numeric_limits from <limits> to check actual sizes and ranges instead of assuming.
  • Mixing signed and unsigned types in arithmetic or comparisons is a classic source of bugs — keep signedness consistent.