Data Types

Data Types

The C++ type system consists of the following parts:

  1. Basic types (representative keywords/types are given in parentheses)
    1. No type/void type (void)
    2. (Since C++11) Null pointer type (std::nullptr_t)
    3. Arithmetic types
      1. Integer types (int)
      2. Boolean/bool type (bool)
      3. Character types (char)
      4. Floating-point types (float,double)
  2. Compound types1

Boolean Type

A variable of type bool can have only two possible values: true and false.

In general, a variable of type bool occupies 11 byte of space (in general, 11 byte =88 bits).

Tip

The number of bits in a byte can be obtained through the macro constant CHAR_BIT in the <climits> (C++) / <limits.h> (C) header files.

Boolean Type in C

See also Differences between C++ and other common languages - bool.

C originally had no Boolean type. It was not until C99 that the _Bool keyword was introduced as a Boolean type, and it is treated as an unsigned integer type.

Note

Since C23, the bool type in C is no longer defined using the integer zero and nonzero values; instead, it is defined as a type sufficient to store the two constants true and false.

For convenience, stdbool.h provides three macros, bool,true,false, defined as follows:

#define bool _Bool
#define true 1
#define false 0

These macros were removed in C23. Since C23, true,false, and bool have been introduced as keywords, while _Bool has been retained as an alternative spelling2.

In addition, since C23, the bit width of the Boolean type can be obtained through the macro constant BOOL_WIDTH in <limits.h>.

Integer Types

Used to store integers. The most basic integer type is int.

Note

For historical reasons, Boolean types and character types are treated as special integer types in C++.

In almost all cases, character types other than signed char and unsigned char should not be used as integer types.

Integer types generally have five width levels: char,short,int,long,long long.

The C++ standard guarantees 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long).

For historical reasons, integer types have multiple popular width models. To solve this problem, C99/C++11 introduced fixed-width integer types.

Size of the int Type

The C++ standard specifies that int has at least 1616 bits.

In fact, on the vast majority of current platforms, int has 3232 bits.

The int keyword can be modified with the following modifiers:

Signedness:

  • signed: denotes a signed integer (the default);
  • unsigned: denotes an unsigned integer.

Size:

  • short: denotes an integer of at least 1616 bits;
  • long: denotes an integer of at least 3232 bits;
  • (Since C++11) long long: denotes an integer of at least 6464 bits.

The following table gives the widths and representation ranges of the integer types in general cases (on a small number of platforms, the representation ranges of some types may differ from this table):

Type nameEquivalent typeWidth (C++ standard)Common widthLess common width
signed charsigned char88--
unsigned charunsigned char88--
short,short int,signed short,signed short intshort int≥16\geq 161616-
unsigned short,unsigned short intunsigned short int≥16\geq 161616-
int,signed,signed intint≥16\geq 1632321616 (common in Win16 APIs)
unsigned,unsigned intunsigned int≥16\geq 1632321616 (common in Win16 APIs)
long,long int,signed long,signed long intlong int≥32\geq 3232326464 (common on 64-bit Linux and macOS)
unsigned long,unsigned long intunsigned long int≥32\geq 3232326464 (common on 64-bit Linux and macOS)
long long,long long int,signed long long,signed long long intlong long int≥64\geq 646464-
unsigned long long,unsigned long long intunsigned long long int≥64\geq 646464-

When the width is xx, the representation range of a signed type is −2x−1∼2x−1−1-2^{x-1}\sim 2^{x-1}-13, and that of an unsigned type is 0∼2x−10 \sim 2^x-1. Specifically:

WidthRepresentation range
88Signed: −27∼27−1-2^{7}\sim 2^{7}-1, unsigned: 0∼28−10 \sim 2^{8}-1
1616Signed: −215∼215−1-2^{15}\sim 2^{15}-1, unsigned: 0∼216−10 \sim 2^{16}-1
3232Signed: −231∼231−1-2^{31}\sim 2^{31}-1, unsigned: 0∼232−10 \sim 2^{32}-1
6464Signed: −263∼263−1-2^{63}\sim 2^{63}-1, unsigned: 0∼264−10 \sim 2^{64}-1
Equivalent Type Notations

When no ambiguity is caused, some modifiers may be omitted, or the order of modifiers may be adjusted. This means that the same type can have multiple equivalent notations.

For example, int, signed, int signed, and signed int denote the same type, while unsigned long and unsigned long int denote the same type.

In addition, some compilers implement extended integer types. For example, GCC implements 128-bit integers: the signed version __int128_t and the unsigned version __uint128_t. If you want to use these types in a contest, read the contest rules carefully to determine whether using extended integer types is permitted or supported.

Note

The STL may not provide sufficient support for extended integer types, so be particularly careful when using them.

Example Code
#include <cmath>
#include <iostream>

int f1(int n) {
  return abs(n);  // Good
}

int f2(int n) {
  return std::abs(n);  // Good
}

__int128_t f3(__int128_t n) {
  return abs(n);  // Bad
}

// Wrong
// __int128_t f4(__int128_t n) {
//   return std::abs(n);
// }

int main() {
  std::cout << "f1: " << f1(-42) << std::endl;
  std::cout << "f2: " << f2(-42) << std::endl;
  // std::cout << "f3: " << f3(-42) << std::endl; // Wrong
  // std::cout << "f4: " << f4(-42) << std::endl; // Wrong
  return 0;
}

The example code above has the following problems:

  1. __int128_t f3(__int128_t) uses the C-style absolute value function, whose signature is int abs(int), so n is first forcibly converted to int, and only then is the abs function called.
  2. __int128_t f4(__int128_t) uses the C++-style absolute value function, but it has no overload with the signature __int128_t std::abs(__int128_t), so it cannot compile.
  3. C++ stream output does not support __int128_t and __uint128_t.

One solution is as follows:

Corrected Code
#include <cmath>
#include <iostream>

__int128_t abs(__int128_t n) { return n < 0 ? -n : n; }

std::ostream &operator<<(std::ostream &os, __uint128_t n) {
  if (n > 9) os << n / 10;
  os << (int)(n % 10);
  return os;
}

std::ostream &operator<<(std::ostream &os, __int128_t n) {
  if (n < 0) {
    os << '-';
    n = -n;
  }
  return os << (__uint128_t)n;
}

int f1(int n) { return abs(n); }

int f2(int n) { return std::abs(n); }

__int128_t f3(__int128_t n) { return abs(n); }

int main() {
  std::cout << "f1: " << f1(-42) << std::endl;
  std::cout << "f2: " << f2(-42) << std::endl;
  std::cout << "f3: " << f3(-42) << std::endl;
}

Character Types

They are divided into “narrow character types” and “wide character types”. Since wide character types are almost never used in algorithm contests, only narrow character types are introduced here.

A narrow character type generally has 88 bits. Its underlying storage is still an integer, and the one-to-one correspondence between characters and integers is generally implemented through ASCII encoding. There are three such types:

  • signed char: the type represented by signed characters, with a representation range of −128∼127-128 \sim 127.

  • unsigned char: the type represented by unsigned characters, with a representation range of 0∼2550 \sim 255.

  • char has the same representation and alignment as either signed char or unsigned char, but is always a distinct type.

    The signedness of char depends on the compiler and target platform: the default setting usually has no signedness on ARM and PowerPC, while it usually has signedness on x86 and x64.

    GCC can be given -fsigned-char or -funsigned-char as a compiler argument to specify that char is treated as signed char or unsigned char; for other compilers, consult the documentation. Note that specifying signedness different from the architecture default may break the ABI and cause the program to malfunction.

Note

Unlike other integer types, char,signed char,unsigned char are three different types.

Generally speaking, signed char,unsigned char should not be used to store characters. In the vast majority of cases, these two types are treated as integer types.

Floating-Point Types

Used to store “real numbers” (note that these are not real numbers in the strict mathematical sense, but approximations of real numbers under certain rules), including the following three types:

  • float: single-precision floating-point type. If supported, it corresponds to the IEEE-754 binary32 format.
  • double: double-precision floating-point type. If supported, it corresponds to the IEEE-754 binary64 format.
  • long double: extended-precision floating-point type. If supported, it corresponds to IEEE-754 binary128; otherwise, if supported, it corresponds to the IEEE-754 binary64 extended format; otherwise, it corresponds to a non-IEEE-754 extended floating-point format whose precision is better than binary64 and whose value range is at least as good as binary64; otherwise, it corresponds to IEEE-754 binary64.
Floating-point formatWidthMaximum positive numberNumber of precision digits
IEEE-754 binary32 format32323.4×10383.4\times 10^{38}6∼96\sim 9
IEEE-754 binary64 format64641.8×103081.8\times 10^{308}15∼1715\sim 17
IEEE-754 binary64 extended format≥80\geq 80≥1.2×104932\geq 1.2\times 10^{4932}≥18∼21\geq 18\sim 21
IEEE-754 binary128 format1281281.2×1049321.2\times 10^{4932}33∼3633\sim 36

The smallest negative number in an IEEE-754 floating-point format is the opposite of the largest positive number.

Because the representation range of float is small and its precision is low, double is commonly used to represent floating-point numbers in practice.

In addition, floating-point types can support some special values:

  • Infinity (positive or negative): INFINITY.
  • Negative zero: -0.0, for example 1.0 / 0.0 == INFINITY,1.0 / -0.0 == -INFINITY.
  • Not a number (NaN): std::nan,NAN, which can generally be produced by operations such as 0.0 / 0.0. It is unequal to any value (including itself); since C++11, std::isnan can be used to determine whether a floating-point number is NaN.

No Type

The void type is the no-type type. Unlike the types above, a variable cannot be declared as type void. However, a function may have return type void, indicating that the function has no return value.

Null Pointer Type

See the corresponding section on pointers.

Fixed-Width Integer Types

Support for fixed-width integers has been provided since C++11, specifically:

  • <cstdint>: provides several fixed-width integer types, as well as macro constants for the maximum and minimum values of each fixed-width integer type.
  • <cinttypes>: provides format macro constants for fixed-width integer types for the std::fprintf family and the std::fscanf family of functions.

There are the following kinds of fixed-width integers:

  • intN_t: a signed integer type whose width is exactly NN bits, such as int32_t.
  • int_fastN_t: the fastest signed integer type whose width is at least NN bits, such as int_fast32_t.
  • int_leastN_t: the smallest signed integer type whose width is at least NN bits, such as int_least32_t.

For the unsigned versions, simply add the letter u before the signed version, such as uint32_t,uint_least8_t.

The standard requires the following 16 types to be implemented:

int_fast8_t,int_fast16_t,int_fast32_t,int_fast64_t,

int_least8_t,int_least16_t,int_least32_t,int_least64_t,

uint_fast8_t,uint_fast16_t,uint_fast32_t,uint_fast64_t,

uint_least8_t,uint_least16_t,uint_least32_t,uint_least64_t.

On this basis, the vast majority of compilers also implement the following 8 types:

int8_t,int16_t,int32_t,int64_t,

uint8_t,uint16_t,uint32_t,uint64_t.

When the corresponding type is implemented, the C++ standard requires the macro constants for its maximum value, minimum value, and width. Their names are formed by removing the final _t from the type name, converting the remainder to uppercase, and adding a suffix:

  • _MAX denotes the maximum value, for example, INT32_MAX is the maximum value of int32_t.
  • _MIN denotes the minimum value, for example, INT32_MIN is the minimum value of int32_t.
Note

Fixed-width integer types are essentially aliases for ordinary integer types, so mixing fixed-width integer types and ordinary integer types may affect cross-platform compilation. For example:

Example Code
#include <algorithm>
#include <cstdint>
#include <iostream>

int main() {
  long long a;
  int64_t b;
  std::cin >> a >> b;
  std::cout << std::max(a, b) << std::endl;
  return 0;
}

On 64-bit Windows, int64_t is generally long long int, while on 64-bit Linux it is generally long int. Therefore, this code cannot compile when using GCC on 64-bit Linux, but can compile when using MSVC on 64-bit Windows, because std::max requires its two input parameter types to be the same.

In addition, since C++17, the class template std::numeric_limits has been provided in <limits> for querying properties of various arithmetic types, such as their maximum value, minimum value, whether they are integer types, and whether they are signed.

#include <cstdint>
#include <limits>

std::numeric_limits<int32_t>::max();  // Maximum value of int32_t, 2'147'483'647
std::numeric_limits<int32_t>::min();  // Minimum value of int32_t, -2'147'483'648

std::numeric_limits<double>::min();  // Minimum value of double, approximately 2.22507e-308
std::numeric_limits<double>::epsilon();  // Difference between 1.0 and the next representable value of double,
                                         // approximately 2.22045e-16

Type Conversion

Sometimes (for example, when a function accepts an int parameter but is passed a variable of type double), we need to convert one type to another.

C++'s type conversion mechanism is relatively complex. Here we mainly introduce two conversions for basic data types: numeric promotion and numeric conversion.

Numeric Promotion

During numeric promotion, the value itself remains unchanged.

Note

C-style variadic arguments undergo default argument promotion during argument passing. For example:

Example Code
#include <stdarg.h>
#include <stdio.h>

void test(int tot, ...) {
  va_list valist;
  int i;

  // Initialize the variadic argument list
  va_start(valist, tot);

  for (i = 0; i < tot; ++i) {
    // Get the value of the i-th variable
    double xx = va_arg(valist, double);  // Correct
    // float xx = va_arg(valist, float); // Wrong

    // Output the underlying storage contents of the i-th variable
    printf("i = %d, value = 0x%016llx\n", i, *(long long *)(&xx));
  }

  // Clean up the memory of the variadic argument list
  va_end(valist);
}

int main() {
  float f;
  double fd, d;
  f = 123.;   // 0x42f60000
  fd = 123.;  // 0x405ec00000000000
  d = 456.;   // 0x407c800000000000
  test(3, f, fd, d);
}

When test is called, f is promoted to double, so its underlying storage contents are the same as those of fd, and the output is

i = 0, value = 0x405ec00000000000
i = 1, value = 0x405ec00000000000
i = 2, value = 0x407c800000000000

If double xx = va_arg(valist, double); is changed to float xx = va_arg(valist, float);, GCC should issue a warning similar to the following:

In file included from test.c:2:
test.c: In function 'test':
test.c:14:35: warning: 'float' is promoted to 'double' when passed through '...'
  14 |         float xx = va_arg(valist, float);
     |                                   ^
test.c:14:35: note: (so you should pass 'double' not 'float' to 'va_arg')
test.c:14:35: note: if this code is reached, the program will abort

The program will terminate before output at this point.

This also explains why printf's %f can match both float and double.

Integer Promotion

A prvalue of a small integer type (such as char) can be converted to a prvalue of a larger integer type (such as int).

More specifically, arithmetic operators do not accept types smaller than int as their arguments; after lvalue-to-rvalue conversion, integer promotion is performed automatically when applicable.

More specifically, the following rules apply:

  • When the source type is signed char, signed short / short, it can be promoted to int.
  • When the source type is unsigned char, unsigned short, if int can hold the value range of the source type, it can be promoted to int; otherwise, it can be promoted to unsigned int. (Since C++20, char8_t also follows this rule.)
  • The promotion rule for char depends on whether its underlying type is signed char or unsigned char.
  • A bool type can be converted to int: false becomes 0, and true becomes 1.
  • If the value range of the destination type contains that of the source type, and the value range of the source type cannot be contained by int or unsigned int, the source type can be promoted to the destination type.4
Note

char -> short is not numeric promotion, because char must first be promoted to int / unsigned int, followed by int / unsigned int -> short, which does not satisfy the conditions for numeric promotion.

For example (assume below that int is 32 bits, unsigned short is 16 bits, signed char and unsigned char are 8 bits, and bool is 1 bit):

  • (signed char)'\0' - (signed char)'\xff' first promotes (signed char)'\0' to (int)0 and (signed char)'\xff' to (int)-1, then performs the operation between int values, with the final result (int)1.
  • (unsigned char)'\0' - (unsigned char)'\xff' first promotes (unsigned char)'\0' to (int)0 and (unsigned char)'\xff' to (int)255, then performs the operation between int values, with the final result (int)-255.
  • false - (unsigned short)12 first promotes false to (int)0 and (unsigned short)12 to (int)12, then performs the operation between int values, with the final result (int)-12.

Floating-Point Promotion

A floating-point number with a smaller width can be promoted to one with a larger width (for example, when variables of type float and double undergo arithmetic, the float variable is promoted to double), without changing its value.

Numeric Conversion

During numeric conversion, the value may change.

Note

Numeric promotion takes precedence over numeric conversion. For example, bool -> int is numeric promotion rather than numeric conversion.

Integer Conversion

<!-- scripts.linter.preprocess.fix_details off -->
  • If the destination type is an unsigned integer type of width xx, the result is the original value  mod 2x\bmod 2^x.

    • If the destination type is wider than the source type:

      • If the source type is signed, sign extension is generally performed first, followed by the conversion.

        For example:

        • When (short)-1 ((short)0b1111'1111'1111'1111) is converted to unsigned int, sign extension is first performed, yielding 0b1111'1111'1111'1111'1111'1111'1111'1111, followed by integer conversion, yielding (unsigned int)4'294'967'295 ((unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111).
        • When (short)32'767 ((short)0b0111'1111'1111'1111) is converted to unsigned int, sign extension is first performed, yielding 0b0000'0000'0000'0000'0111'1111'1111'1111, followed by integer conversion, yielding (unsigned int)32'767 ((unsigned int)0b0000'0000'0000'0000'0111'1111'1111'1111).
      • If the source type is unsigned, zero extension is first performed, followed by the conversion.

        For example, when (unsigned short)65'535 ((unsigned short)0b1111'1111'1111'1111) is converted to unsigned int, zero extension is first performed, yielding 0b0000'0000'0000'0000'1111'1111'1111'1111, followed by integer conversion, yielding (unsigned int)65'535 ((unsigned int)0b0000'0000'0000'0000'1111'1111'1111'1111).

    • If the destination type is no wider than the source type, truncation is first performed, followed by the conversion.

      For example, when (unsigned int)4'294'967'295 ((unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111) is converted to unsigned short, truncation is first performed, yielding 0b1111'1111'1111'1111, followed by integer conversion, yielding (unsigned short)65'535 ((unsigned short)0b1111'1111'1111'1111).

  • If the destination type is a signed integer type of width xx, in general, the result can be considered the original value  mod 2x\bmod 2^x.5

    For example, when (unsigned int)4'294'967'295 ((unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111) is converted to short, the result is (short)-1 ((short)0b1111'1111'1111'1111).

  • If the destination type is bool, this is Boolean conversion.

  • If the source type is bool, false is converted to the corresponding type's 0, and true to the corresponding type's 1.

<!-- scripts.linter.preprocess.fix_details on -->

Floating-Point Conversion

Converting a floating-point number with a larger width to one with a smaller width rounds it to the closest value representable by the destination type.

Floating-Point-Integer Conversion

  • When a floating-point number is converted to an integer, all fractional digits of the floating-point number are discarded.

    If the destination type is bool, this is Boolean conversion.

  • When an integer is converted to a floating-point number, it is rounded to the closest value representable by the destination type.

    If the value cannot fit in the destination type, the behavior is undefined.

    If the source type is bool, false is converted to zero, while true is converted to one.

Boolean Conversion

When another type is converted to type bool, a zero value is converted to false, and a nonzero value is converted to true.

Defining Variables

Simply speaking6, defining a variable requires a type specifier (indicating the variable's type) and the name of the variable to be defined.

For example, each of the following statements is a variable definition statement.

int oi;
double wiki;
char org = 'c';

In the program fragments we have encountered so far, variables defined inside braces are local variables, while variables defined outside braces are global variables. There are actual exceptions, but they need not be understood yet.

Global variables without an initialization value at definition are initialized to 00. Local variables do not have this property and need to be assigned an initial value manually; otherwise, they may cause bugs that are difficult to detect.

Variable Scope

The scope is the block of code in which a variable can take effect.

The scope of a global variable begins at its definition7 and continues to the end of the file.

The scope of a local variable begins at its definition and continues to the end of the code block.

A code block consists of several statements enclosed by a pair of braces.

int g = 20;  // Define the global variable

int main() {
  int g = 10;         // Define the local variable
  printf("%d\n", g);  // Output g
  return 0;
}

If a variable with the same name is defined in a nested block of a code block, the inner block cannot access the variable with the same name in the outer block.

For example, in the code above, the value of gg output is 1010. Therefore, to prevent unexpected errors, try to avoid giving local and global variables the same name.

Constants

A constant is a fixed value that does not change during program execution.

The value of a constant cannot be modified after its definition. Add the const keyword when defining it.

const int a = 2;
a = 3;

If the value of a constant is modified, a compilation error is reported: error: assignment of read-only variable 'a'.

References and notes

  1. Working Draft, Standard for Programming Language C++
  2. Types - cppreference.com
  3. Arithmetic types - cppreference.com in C
  4. Fundamental types - cppreference.com
  5. Fixed-width integer types (since C++11) - cppreference.com
  6. William Kahan (1 October 1997). "Lecture Notes on the Status of IEEE Standard 754 for Binary Floating-Point Arithmetic".
  7. Implicit conversions - cppreference.com
  8. Declarations - cppreference
  9. Scope - cppreference.com

Footnotes

  1. Includes array types, reference types, pointer types, class types, function types, and so on. Since this article is intended for beginners, they are not introduced specifically here. See Types - cppreference.com for details. ↩

  2. See https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3054.pdf ↩

  3. Before C++20, it was specified that signed integers must at least cover the representation range of ones' complement (that is, −2x−1+1∼2x−1−1-2^{x-1}+1\sim 2^{x-1}-1), but in fact the vast majority of implementations use two's complement; since C++20, it has further been specified that signed integers must use two's complement. See Range of values - cppreference for details. ↩

  4. This excludes wide character types, bit-fields, and enumeration types; see Integral conversions - cppreference for details. ↩

  5. Effective since C++20. Before C++20, the result was implementation-defined. See Integral conversions - cppreference for details. ↩

  6. When defining a variable, specifiers other than the type specifier may also be included. See Declarations - cppreference for details. ↩

  7. More precisely, the point of declaration. ↩

This content comes from OI Wiki: Data Types. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.