Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 2 – Types, values and expressions

C++ fundamental types: integers, floats, char, bool

What each C++ fundamental type holds, why long is 8 bytes on macOS and Linux but 4 on Windows, and how to pick exact widths with <cstdint>.

  • Beginner
  • 18 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 18 minutes

What you will learn

  • Choose an integer type by the range of values it must hold
  • Use the fixed-width types from <cstdint> when sizes matter
  • Query sizes and limits with sizeof and std::numeric_limits

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose an integer type by the range of values it must hold
  • Use the fixed-width types from <cstdint> when sizes matter
  • Query sizes and limits with sizeof and std::numeric_limits

Before you start

On this page

The fundamental types are the ones built into the C++ language: the integers (short, int, long and long long, each signed or unsigned), the floating-point types (float, double and long double), the character types (char and its relatives), bool, and void. The standard does not fix their sizes. It fixes a minimum width for each integer type and the order between them, and the rest comes from the platform. On a 64-bit Mac or Linux machine an int is 4 bytes and a long is 8, while on 64-bit Windows a long is only 4. Even the sign of plain char differs between platforms. So choose a type by the range of values it must hold, ask the compiler with sizeof and std::numeric_limits when you need the facts, and use the exact-width types of <cstdint>, such as std::int64_t, wherever a size is part of the contract.

The types and their sizes

sizeof(T) is the size of a type in bytes, where a byte is a char, 8 bits on every platform you are likely to use. This program prints it for every fundamental type:

sizes.cpp C++ · sizes.cpp
#include <cstddef>
#include <print>

template <class T>
void show(const char* name) {
    std::println("{:<12} {:>2} bytes  {:>3} bits", name, sizeof(T), 8 * sizeof(T));
}

int main() {
    show<bool>("bool");
    show<char>("char");
    show<short>("short");
    show<int>("int");
    show<long>("long");
    show<long long>("long long");
    show<float>("float");
    show<double>("double");
    show<long double>("long double");
    show<wchar_t>("wchar_t");
    show<char8_t>("char8_t");
    show<char16_t>("char16_t");
    show<char32_t>("char32_t");
    show<void*>("void*");
    show<std::size_t>("std::size_t");
}
Sizes on a 64-bit Mac, with both compilers Python · sizes_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra sizes.cpp -o sizes
./sizes
g++-16 -std=c++23 -Wall -Wextra sizes.cpp -o sizes
./sizes
""")

Output

$ clang++ -std=c++23 -Wall -Wextra sizes.cpp -o sizes
$ ./sizes
bool          1 bytes    8 bits
char          1 bytes    8 bits
short         2 bytes   16 bits
int           4 bytes   32 bits
long          8 bytes   64 bits
long long     8 bytes   64 bits
float         4 bytes   32 bits
double        8 bytes   64 bits
long double   8 bytes   64 bits
wchar_t       4 bytes   32 bits
char8_t       1 bytes    8 bits
char16_t      2 bytes   16 bits
char32_t      4 bytes   32 bits
void*         8 bytes   64 bits
std::size_t   8 bytes   64 bits
$ g++-16 -std=c++23 -Wall -Wextra sizes.cpp -o sizes
$ ./sizes
bool          1 bytes    8 bits
char          1 bytes    8 bits
short         2 bytes   16 bits
int           4 bytes   32 bits
long          8 bytes   64 bits
long long     8 bytes   64 bits
float         4 bytes   32 bits
double        8 bytes   64 bits
long double   8 bytes   64 bits
wchar_t       4 bytes   32 bits
char8_t       1 bytes    8 bits
char16_t      2 bytes   16 bits
char32_t      4 bytes   32 bits
void*         8 bytes   64 bits
std::size_t   8 bytes   64 bits

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sizes_session.py

As in every session of this track, the lines that start with $ are commands, and the rest is what they printed. Both compilers agree, because the sizes belong to the platform rather than to the compiler: the operating system’s data model and the processor’s calling convention decide them, and every compiler for that platform follows. 64-bit macOS and Linux use a data model called LP64, in which long and pointers are 64 bits. 64-bit Windows uses LLP64, in which only long long and pointers are, and Microsoft’s documentation lists long as 4 bytes there.

Bytes per type on 64-bit macOS and Linux against 64-bit Windows: long and wchar_t differ, short, int, long long and pointers do not.Bytes per type on 64-bit systemsmacOS and Linux (LP64)Windows (LLP64)short22int44long84long long88void*88wchar_t42

Sizes of the integer types on 64-bit systems

Text description of the diagram

A bar chart with two bars per type, one for 64-bit macOS and Linux (the LP64 data model) and one for 64-bit Windows (the LLP64 data model). Each bar is labelled with its size in bytes.

  • short: 2 bytes on both.
  • int: 4 bytes on both.
  • long: 8 bytes on macOS and Linux, 4 bytes on Windows.
  • long long: 8 bytes on both.
  • void*, a pointer: 8 bytes on both.
  • wchar_t: 4 bytes on macOS and Linux, 2 bytes on Windows.

The standard itself promises much less. Since C++20 it sets these minimum widths, and requires each type to be at least as wide as the one before it. The Mac column comes from the run above, the Windows column from Microsoft’s documentation:

Type Standard minimum 64-bit Mac 64-bit Windows
signed char 8 bits 1 byte 1 byte
short 16 bits 2 bytes 2 bytes
int 16 bits 4 bytes 4 bytes
long 32 bits 8 bytes 4 bytes
long long 64 bits 8 bytes 8 bytes

An int of 16 bits is allowed, and some small embedded processors have one. The safe reading of the table is that int holds at least -32,768 to 32,767, long at least about plus or minus 2.1 billion, and long long at least about plus or minus 9.2 × 10¹⁸. Anything more is a property of your platform.

Ranges with std::numeric_limits

<limits> describes each arithmetic type: its lowest and largest values, how many bits or decimal digits it keeps, whether it is signed. It works at compile time, so it costs nothing at run time:

limits.cpp C++ · limits.cpp
#include <limits>
#include <print>

template <class T>
void row(const char* name) {
    using limits = std::numeric_limits<T>;
    std::println("{:<18} {:>26} {:>26}", name, limits::lowest(), limits::max());
}

int main() {
    std::println("{:<18} {:>26} {:>26}", "type", "lowest", "max");
    row<signed char>("signed char");
    row<unsigned char>("unsigned char");
    row<short>("short");
    row<int>("int");
    row<unsigned>("unsigned");
    row<long long>("long long");
    row<unsigned long long>("unsigned long long");
    row<float>("float");
    row<double>("double");
    std::println("");
    std::println("decimal digits a float keeps: {}, a double: {}", std::numeric_limits<float>::digits10,
                 std::numeric_limits<double>::digits10);
}
Lowest and largest values Python · limits_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra limits.cpp -o limits
./limits
g++-16 -std=c++23 -Wall -Wextra limits.cpp -o limits
./limits
""")

Output

$ clang++ -std=c++23 -Wall -Wextra limits.cpp -o limits
$ ./limits
type                                   lowest                        max
signed char                              -128                        127
unsigned char                               0                        255
short                                  -32768                      32767
int                               -2147483648                 2147483647
unsigned                                    0                 4294967295
long long                -9223372036854775808        9223372036854775807
unsigned long long                          0       18446744073709551615
float                          -3.4028235e+38              3.4028235e+38
double               -1.7976931348623157e+308    1.7976931348623157e+308

decimal digits a float keeps: 6, a double: 15
$ g++-16 -std=c++23 -Wall -Wextra limits.cpp -o limits
$ ./limits
type                                   lowest                        max
signed char                              -128                        127
unsigned char                               0                        255
short                                  -32768                      32767
int                               -2147483648                 2147483647
unsigned                                    0                 4294967295
long long                -9223372036854775808        9223372036854775807
unsigned long long                          0       18446744073709551615
float                          -3.4028235e+38              3.4028235e+38
double               -1.7976931348623157e+308    1.7976931348623157e+308

decimal digits a float keeps: 6, a double: 15

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 limits_session.py

Every signed integer runs from -2ᴺ⁻¹ to 2ᴺ⁻¹ - 1 for its N bits, and every unsigned one from 0 to 2ᴺ - 1. That is guaranteed since C++20, which made two’s complement the only allowed representation of signed integers; earlier standards also allowed two others that mainstream processors no longer use. What two’s complement did not change is overflow: going past max() with a signed type is still undefined behaviour, the subject of the next lesson.

Two details of the output are worth a second look. For floating-point types, lowest() is the most negative value, while min() means something else: the smallest positive normal number, about 1.2 × 10⁻³⁸ for a float. And digits10 says how many decimal digits a type keeps without fail: 6 for float and 15 for double. The floating-point lesson takes these apart.

Exact widths with <cstdint>

When a size is part of a contract, such as a file format, a network protocol or an interface to hardware, use the fixed-width types of <cstdint>: std::int8_t, std::int16_t, std::int32_t and std::int64_t, and the unsigned std::uint8_t to std::uint64_t. Each has exactly the width in its name, with no padding bits. std::size_t, the type sizeof returns and the standard containers use for sizes, is unsigned, and on mainstream platforms as wide as a pointer: 8 bytes here.

Fixed-width types and the smallest type for a value

fixed_width.cpp

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <print>
#include <type_traits>

int main() {
    std::int32_t ms_per_day{86'400'000};          // fits: at most 2,147,483,647
    std::int64_t ms_per_year{31'536'000'000};     // needs more than 32 bits
    std::uint8_t grade{66};                       // one byte, 0 to 255
    std::println("{} ms a day, {} ms a year", ms_per_day, ms_per_year);

    // std::uint8_t is a character type underneath, and streams print characters:
    std::cout << "cout prints grade as " << grade << '\n';
    std::println("println prints grade as {}", grade);

    static_assert(sizeof(std::int64_t) == 8);
    static_assert(std::is_unsigned_v<std::size_t>);
    static_assert(std::is_same_v<std::uint8_t, unsigned char>);
}

choose_types.cpp

#include <cstdint>
#include <print>
#include <utility>

// The smallest fixed-width signed type that can hold value.
constexpr const char* smallest_signed(long long value) {
    if (std::in_range<std::int8_t>(value)) return "std::int8_t";
    if (std::in_range<std::int16_t>(value)) return "std::int16_t";
    if (std::in_range<std::int32_t>(value)) return "std::int32_t";
    return "std::int64_t";
}

int main() {
    for (long long value : {100LL, -200LL, 40'000LL, 86'400'000LL, 3'000'000'000LL, 31'536'000'000LL}) {
        std::println("{:>14} needs {}", value, smallest_signed(value));
    }
    // The same checks while the program compiles: a wrong choice stops the build.
    static_assert(std::in_range<std::int16_t>(-200));
    static_assert(!std::in_range<std::int16_t>(40'000));
    static_assert(std::in_range<std::uint16_t>(40'000));
    static_assert(!std::in_range<std::int32_t>(3'000'000'000LL));
    static_assert(std::in_range<std::uint32_t>(3'000'000'000LL));
}
Both files on both compilers Python · fixed_width_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra fixed_width.cpp -o fixed_width
./fixed_width
clang++ -std=c++23 -Wall -Wextra choose_types.cpp -o choose_types
./choose_types
g++-16 -std=c++23 -Wall -Wextra fixed_width.cpp -o fixed_width
./fixed_width
g++-16 -std=c++23 -Wall -Wextra choose_types.cpp -o choose_types
./choose_types
""")

Output

$ clang++ -std=c++23 -Wall -Wextra fixed_width.cpp -o fixed_width
$ ./fixed_width
86400000 ms a day, 31536000000 ms a year
cout prints grade as B
println prints grade as 66
$ clang++ -std=c++23 -Wall -Wextra choose_types.cpp -o choose_types
$ ./choose_types
           100 needs std::int8_t
          -200 needs std::int16_t
         40000 needs std::int32_t
      86400000 needs std::int32_t
    3000000000 needs std::int64_t
   31536000000 needs std::int64_t
$ g++-16 -std=c++23 -Wall -Wextra fixed_width.cpp -o fixed_width
$ ./fixed_width
86400000 ms a day, 31536000000 ms a year
cout prints grade as B
println prints grade as 66
$ g++-16 -std=c++23 -Wall -Wextra choose_types.cpp -o choose_types
$ ./choose_types
           100 needs std::int8_t
          -200 needs std::int16_t
         40000 needs std::int32_t
      86400000 needs std::int32_t
    3000000000 needs std::int64_t
   31536000000 needs std::int64_t

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fixed_width_session.py

The first file shows the classic surprise of the 8-bit types. On these platforms std::uint8_t is another name for unsigned char, and std::cout prints a char type as a character, so the grade 66 comes out as the letter B. std::println formats signed char and unsigned char as numbers, which is one more reason to prefer it. With std::cout, write +grade: the unary plus promotes the value to int.

The second file answers the question behind every choice of type, “which type holds this value?”, with std::in_range from <utility> (C++20). It is true when a value fits the type given in angle brackets, whatever the signs of the two types, and it works at compile time, so the static_assert lines at the end turn a wrong choice into a build error. 86,400,000 milliseconds, one day, needs 32 bits; 31,536,000,000, one year of 365 days, needs 64. The numbers are ordinary: a counter of milliseconds or of bytes outgrows std::int32_t sooner than people expect.

For everyday work these rules are enough:

  • int for counts and indexes that stay well below 2 billion, which is what most code does.
  • long long or std::int64_t for anything that can reach 2 billion: file sizes, money in the smallest unit, timestamps in milliseconds. Avoid plain long in portable code, because its size depends on the platform.
  • The fixed-width types for data that leaves the program, so that every machine reads the same bytes.
  • Signed types for arithmetic, as rule ES.102 of the C++ Core Guidelines advises. std::size_t appears whenever you ask a container for its size, and mixing it with signed values has traps of its own, covered in the next lesson.

Three character types, not two

char, signed char and unsigned char are three distinct types, even though plain char behaves exactly like one of the other two. Which one is up to the platform:

char_sign.cpp C++ · char_sign.cpp
#include <climits>
#include <print>
#include <type_traits>

int main() {
    char byte = static_cast<char>(0xC8);   // the bit pattern 11001000
    std::println("plain char is signed: {}", std::is_signed_v<char>);
    std::println("range of char: {} to {}", CHAR_MIN, CHAR_MAX);
    std::println("the byte 0xC8 as an int: {}", static_cast<int>(byte));
    std::println("char is the same type as signed char: {}", std::is_same_v<char, signed char>);
}
Plain char on macOS, and as Linux on arm64 sees it Python · char_sign_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra char_sign.cpp -o char_sign
./char_sign
g++-16 -std=c++23 -Wall -Wextra char_sign.cpp -o char_sign
./char_sign
# The same file as compilers build it for Linux on arm64, where plain char is unsigned:
g++-16 -std=c++23 -Wall -Wextra -funsigned-char char_sign.cpp -o char_sign
./char_sign
""")

Output

$ clang++ -std=c++23 -Wall -Wextra char_sign.cpp -o char_sign
$ ./char_sign
plain char is signed: true
range of char: -128 to 127
the byte 0xC8 as an int: -56
char is the same type as signed char: false
$ g++-16 -std=c++23 -Wall -Wextra char_sign.cpp -o char_sign
$ ./char_sign
plain char is signed: true
range of char: -128 to 127
the byte 0xC8 as an int: -56
char is the same type as signed char: false
# The same file as compilers build it for Linux on arm64, where plain char is unsigned:
$ g++-16 -std=c++23 -Wall -Wextra -funsigned-char char_sign.cpp -o char_sign
$ ./char_sign
plain char is signed: false
range of char: 0 to 255
the byte 0xC8 as an int: 200
char is the same type as signed char: false

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 char_sign_session.py

On Apple silicon, plain char is signed: Apple’s documentation lists it as one of the places where Apple’s 64-bit ABI differs from Arm’s standard one. The byte 0xC8 therefore reads as -56. Arm’s own procedure call standard, AAPCS64, which Linux on arm64 follows, makes plain char unsigned. GCC’s -funsigned-char builds the same file that way, and the byte reads as 200. On Windows, Microsoft’s compiler makes char signed unless you pass /J. Code that works on raw bytes, such as a checksum or a file parser, should therefore say what it means: unsigned char, std::uint8_t or std::byte, never plain char. A whole program, its libraries included, must be built with one setting, so treat -funsigned-char as a way to test, not as a fix.

The other character types hold units of text encodings. char8_t, char16_t and char32_t hold UTF-8, UTF-16 and UTF-32 code units, at 1, 2 and 4 bytes everywhere, as the sizes session showed. wchar_t is the odd one: 4 bytes on macOS and Linux, 2 bytes on Windows, so it holds different encodings on different systems. A later lesson of this module is about text encodings.

bool

bool holds true or false and takes 1 byte on these platforms. It converts both ways with numbers:

bools.cpp C++ · bools.cpp
#include <iostream>
#include <print>

int main() {
    bool from_negative = -3;      // any value other than zero converts to true
    bool from_zero = 0.0;         // zero converts to false
    int from_true = true;         // true converts to 1, false to 0
    std::println("{} {} {}", from_negative, from_zero, from_true);
    std::println("true + true = {}", true + true);
    std::cout << "cout prints true as " << true << ", with std::boolalpha as " << std::boolalpha << true << '\n';
}
Conversions to and from bool Python · bools_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra bools.cpp -o bools
./bools
g++-16 -std=c++23 -Wall -Wextra bools.cpp -o bools
./bools
""")

Output

$ clang++ -std=c++23 -Wall -Wextra bools.cpp -o bools
$ ./bools
true false 1
true + true = 2
cout prints true as 1, with std::boolalpha as true
$ g++-16 -std=c++23 -Wall -Wextra bools.cpp -o bools
$ ./bools
true false 1
true + true = 2
cout prints true as 1, with std::boolalpha as true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bools_session.py

Any number other than zero converts to true, and zero to false; true converts to 1 and false to 0. In arithmetic a bool is promoted to int first, so true + true is 2. std::cout prints a bool as 1 or 0 unless you set std::boolalpha, while std::println prints true and false.

Floating-point types in brief

float and double are the IEEE 754 binary formats of 32 and 64 bits on every mainstream platform: a float keeps 6 decimal digits without fail and a double 15, as the limits session printed. Prefer double unless memory is tight, as in large arrays or on graphics hardware. long double is the least portable fundamental type. On Apple silicon it is identical to double, 8 bytes, which Apple documents as another difference from the standard Arm ABI, where it is a 16-byte quad-precision type. The next lessons cover what floating-point numbers can and cannot store exactly.

Interview questions

Fresher · Is an int always 32 bits in C++?

No. The standard only requires an int to be at least 16 bits wide, and no narrower than short. It is 32 bits on all mainstream 32-bit and 64-bit desktop, server and phone platforms, which is why people assume it, but small embedded processors may use 16. The type that really differs between today’s platforms is long: 8 bytes on 64-bit Linux and macOS, 4 bytes on 64-bit Windows. When a program needs an exact width, it should say so with std::int32_t or std::int64_t from <cstdint>, and it can check an assumption at compile time with static_assert(sizeof(int) == 4);.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 How many bytes is a long on 64-bit Windows?

    Choose one answer.

    Show the answer to question 1

    Answer: 4

    64-bit Windows uses the LLP64 data model, in which int and long are 32 bits and only long long and pointers are 64. Every compiler for the platform follows it. On 64-bit macOS and Linux (LP64) a long is 8 bytes.

  2. Question 2 of 6 What does this print?

    Read the code, then choose one answer.

    std::uint8_t grade{66};
    std::cout << grade << '\n';
    Show the answer to question 2

    Answer: B

    std::uint8_t is another name for unsigned char on these platforms, and streams print character types as characters: 66 is the code of B. std::cout << +grade or std::println("{}", grade) prints 66.

  3. Question 3 of 6 Which of these does the C++ standard guarantee?

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • A long long is at least 64 bits wide
    • sizeof(char) is 1
    • An int is at least 16 bits wide

    The standard fixes minimum widths and defines sizeof in units of char, so sizeof(char) is 1 by definition. Exactly 32 bits for int and 8 bytes for long are properties of particular platforms; 64-bit Windows has a 4-byte long.

  4. Question 4 of 6 A function adds up the bytes of a file. Which parameter type makes it give the same result on every platform?

    Choose one answer.

    Show the answer to question 4

    Answer: const unsigned char*

    Plain char is signed on some platforms and unsigned on others, so a byte such as 0xC8 adds -56 on one and 200 on another. unsigned char (or std::uint8_t, or std::byte) always reads a byte as 0 to 255. signed char is portable too, but it reads bytes above 127 as negative numbers, which a byte sum does not want.

  5. Question 5 of 6 What is the value of total?

    Read the code, then choose one answer.

    bool flag = -3;
    int total = flag + true;
    Show the answer to question 5

    Answer: 2

    -3 is not zero, so flag is true. In arithmetic both bools are promoted to int, so the sum is 1 + 1.

  6. Question 6 of 6 What does std::numeric_limits<double>::min() return?

    Choose one answer.

    Show the answer to question 6

    Answer: The smallest positive normal double, about 2.2 × 10⁻³⁰⁸

    For floating-point types, min() is the smallest positive normal value; the most negative one is lowest(). For integer types the two are the same. The gap between 1.0 and the next double is epsilon().

Key takeaways

  • The standard fixes minimum widths (short and int 16 bits, long 32, long long 64), not sizes; the platform decides the rest.
  • 64-bit macOS and Linux are LP64 (long is 8 bytes); 64-bit Windows is LLP64 (long is 4 bytes). Avoid plain long in portable code.
  • sizeof gives sizes in bytes, and std::numeric_limits<T> gives lowest(), max(), digits10 and more, at compile time.
  • Use std::int32_t, std::int64_t and the other <cstdint> types where the width matters, and std::in_range to check that a value fits.
  • std::uint8_t is a character type underneath: std::cout prints it as a character, std::println as a number.
  • Plain char is signed on Apple silicon and Windows and unsigned on Linux for arm64. Use unsigned char or std::byte for bytes.
  • Non-zero converts to true, true to 1; long double is just a double on Apple silicon.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.