Skip to content

Variables

Data Types

C++'s type system consists of the following components:

  1. Fundamental types (keywords/representative types in parentheses)
    1. Void type (void)
    2. (Since C++11) Null pointer type (std::nullptr_t)
    3. Arithmetic types
      1. Integral types (int)
      2. Boolean type (bool)
      3. Character types (char)
      4. Floating-point types (float, double)
  2. Compound types3

Boolean Type

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

Generally, a bool variable occupies \(1\) byte (typically, \(1\) byte = \(8\) bits) of space.

Tip

The macro constant CHAR_BIT from the header <climits> (C++) / <limits.h> (C) can be used to get the number of bits in a byte.

Boolean Type in C

Also refer to Differences between C++ and Other Common Languages - bool.

C originally had no boolean type until C99 introduced the _Bool keyword as a boolean type, which is treated as an unsigned integer type.

Note

Starting from C23, C's bool type is no longer defined using zero and non-zero integer values but as a type large enough to store the two constants true and false.

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

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

These macros were removed in C23, and starting from C23, true, false, and bool are introduced as keywords, while _Bool is retained as an alternative spelling4.

Additionally, starting from C23, the macro constant BOOL_WIDTH from <limits.h> can be used to get the bit width of the boolean type.

Integral Types

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

Note

Due to historical reasons, boolean and character types are considered special integral types in C++.

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

Integral types generally have 5 gradations in bit width: char, short, int, long, long long.

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

Due to historical reasons, there are multiple popular models for integer bit widths. To address this, C99/C++11 introduced Fixed-width integer types.

Size of int

The C++ standard specifies that int must be at least \(16\) bits wide.

In fact, on most modern platforms, int is \(32\) bits wide.

For the int keyword, the following modifier keywords can be used:

Sign:

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

Size:

  • short: indicates an integer of at least \(16\) bits;
  • long: indicates an integer of at least \(32\) bits;
  • (Since C++11) long long: indicates an integer of at least \(64\) bits.

The table below shows the bit width and range of each integer type in general (on some platforms, the range of some types may differ from the table):

Type Name Equivalent Type Bit Width (C++ Standard) Bit Width (Common) Bit Width (Rare)
signed char signed char \(8\) - -
unsigned char unsigned char \(8\) - -
short, short int, signed short, signed short int short int \(\geq 16\) \(16\) -
unsigned short, unsigned short int unsigned short int \(\geq 16\) \(16\) -
int, signed, signed int int \(\geq 16\) \(32\) \(16\) (common in Win16 API)
unsigned, unsigned int unsigned int \(\geq 16\) \(32\) \(16\) (common in Win16 API)
long, long int, signed long, signed long int long int \(\geq 32\) \(32\) \(64\) (common in 64-bit Linux, macOS)
unsigned long, unsigned long int unsigned long int \(\geq 32\) \(32\) \(64\) (common in 64-bit Linux, macOS)
long long, long long int, signed long long, signed long long int long long int \(\geq 64\) \(64\) -
unsigned long long, unsigned long long int unsigned long long int \(\geq 64\) \(64\) -

When the bit width is \(x\), the range of a signed type is \(-2^{x-1}\sim 2^{x-1}-1\), and the range of an unsigned type is \(0 \sim 2^x-1\). Specifically:

Bit Width Range
\(8\) Signed: \(-2^{7}\sim 2^{7}-1\), Unsigned: \(0 \sim 2^{8}-1\)
\(16\) Signed: \(-2^{15}\sim 2^{15}-1\), Unsigned: \(0 \sim 2^{16}-1\)
\(32\) Signed: \(-2^{31}\sim 2^{31}-1\), Unsigned: \(0 \sim 2^{32}-1\)
\(64\) Signed: \(-2^{63}\sim 2^{63}-1\), Unsigned: \(0 \sim 2^{64}-1\)
Equivalent Type Representations

When no ambiguity arises, some modifier keywords can be omitted, or the order of modifier keywords can be adjusted. This means the same type can have multiple equivalent representations.

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

Additionally, some compilers implement extended integer types, such as GCC's 128-bit integers: signed __int128_t and unsigned __uint128_t. If you want to use these types in a contest, read the contest rules carefully to determine if extended integer types are allowed or supported.

Note

The STL may not have sufficient support for extended integer types, so extra caution is needed when using them.

Example Code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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 issues:

  1. In __int128_t f3(__int128_t), the C-style absolute value function is used, whose signature is int abs(int), so n is first cast to int before calling abs.
  2. In __int128_t f4(__int128_t), the C++-style absolute value function is used, which does not have an overload with signature __int128_t std::abs(__int128_t), so compilation fails.
  3. C++ stream output does not support __int128_t and __uint128_t.

Here is one possible solution:

Corrected Code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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

Divided into "narrow character types" and "wide character types". Since wide character types are rarely used in competitive programming, only narrow character types are introduced here.

Narrow character types are typically \(8\) bits wide. They are essentially stored as integers, usually via ASCII encoding to map characters to integers. There are three types:

  • signed char: A signed character type, with a range of \(-128 \sim 127\).
  • unsigned char: An unsigned character type, with a range of \(0 \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: ARM and PowerPC defaults are usually unsigned, while x86 and x64 defaults are usually signed.

    GCC can specify whether char is treated as signed char or unsigned char via the compiler flags -fsigned-char or -funsigned-char. Refer to documentation for other compilers. Note that specifying a signedness different from the architecture default may break the ABI and cause programs to malfunction.

Note

Unlike other integral types, char, signed char, and unsigned char are three distinct types.

Generally, signed char and unsigned char should not be used to store characters; in most cases, these two types are treated as integral types.

Floating-Point Types

Used to store "real numbers" (note: not strictly real numbers but approximations under certain rules), including the following three:

  • float: Single-precision floating-point type. If supported, matches the IEEE-754 binary32 format.
  • double: Double-precision floating-point type. If supported, matches the IEEE-754 binary64 format.
  • long double: Extended-precision floating-point type. If supported, matches the IEEE-754 binary128 format; otherwise, if supported, matches the IEEE-754 binary64 extended format; otherwise, matches some non-IEEE-754 extended floating-point format with precision better than binary64 and value range at least as good as binary64; otherwise, matches the IEEE-754 binary64 format.
Floating-Point Format Bit Width Maximum Positive Value Precision Bits
IEEE-754 binary32 format \(32\) \(3.4\times 10^{38}\) \(6\sim 9\)
IEEE-754 binary64 format \(64\) \(1.8\times 10^{308}\) \(15\sim 17\)
IEEE-754 binary64 extended format \(\geq 80\) \(\geq 1.2\times 10^{4932}\) \(\geq 18\sim 21\)
IEEE-754 binary128 format \(128\) \(1.2\times 10^{4932}\) \(33\sim 36\)

The minimum negative number of an IEEE-754 floating-point format is the opposite of its maximum positive number.

Because float has a small range and low precision, double is commonly used to represent floating-point numbers in practice.

Additionally, floating-point types can support special values:

  • Infinity (positive or negative): INFINITY.
  • Negative zero: -0.0, e.g., 1.0 / 0.0 == INFINITY, 1.0 / -0.0 == -INFINITY.
  • Not-a-Number (NaN): std::nan, NAN, typically generated by operations like 0.0 / 0.0. It compares unequal to any value (including itself). Since C++11, std::isnan can be used to check if a floating-point number is NaN.

Void Type

The void type is a typeless type. Unlike the types above, a variable cannot be declared as void. However, a function's return value can be void, indicating the function returns no value.

Null Pointer Type

Refer to the corresponding section on pointers.

Fixed-width Integer Types

C++11 provides support for fixed-width integers, as follows:

  • <cstdint>: Provides several fixed-width integer types and macro constants for their maximum, minimum values, etc.
  • <cinttypes>: Provides format macro constants for fixed-width integer types for std::fprintf family and std::fscanf family functions.

Fixed-width integers include the following:

  • intN_t: Signed integer type with width exactly \(N\) bits, e.g., int32_t.
  • int_fastN_t: The fastest signed integer type with width at least \(N\) bits, e.g., int_fast32_t.
  • int_leastN_t: The smallest signed integer type with width at least \(N\) bits, e.g., int_least32_t.

The unsigned versions are prefixed with a 'u', e.g., uint32_t, uint_least8_t.

The standard mandates the implementation of the following 16 types:

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.

Most compilers additionally 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 mandates the implementation of macro constants representing the maximum, minimum, and bit width of the type, formatted by removing the _t suffix from the type name, converting to uppercase, and adding a suffix:

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

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

Example Code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#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;
}

int64_t is usually long long int on 64-bit Windows, but long int on 64-bit Linux. Therefore, this code fails to compile with GCC on 64-bit Linux but compiles with MSVC on 64-bit Windows because std::max requires both arguments to have the same type.

Additionally, since C++17, <limits> provides the std::numeric_limits class template to query properties of arithmetic types, such as maximum, minimum, whether it's integral, signed, etc.

1
2
3
4
5
6
7
8
9
#include <cstdint>
#include <limits>

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

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

Type Conversions

Sometimes (e.g., when a function expects an int argument but a double variable is passed), we need to convert one type to another.

C++'s type conversion mechanisms are complex. Here we mainly introduce two conversions for fundamental data types: numeric promotions and numeric conversions.

Numeric Promotions

During numeric promotion, the value itself remains unchanged.

Note

C-style variadic arguments undergo default argument promotions when passed by value. For example:

Example Code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdarg.h>
#include <stdio.h>

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

  // Initialize 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 content of the i-th variable
    printf("i = %d, value = 0x%016llx\n", i, *(long long *)(&xx));
  }

  // Clean up the variadic argument list memory
  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 calling test, f is promoted to double, so its underlying storage content becomes the same as fd. The output is:

1
2
3
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 give a warning similar to:

1
2
3
4
5
6
7
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 then terminate before output.

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

Integral Promotion

Prvalues of small integer types (e.g., char) can be converted to prvalues of larger integer types (e.g., int).

Specifically, arithmetic operators do not accept types smaller than int as arguments, and after lvalue-to-rvalue conversion, integral promotion is automatically applied if applicable.

Specifically, the rules are:

  • 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 represent all values 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.
  • bool can be converted to int: false becomes 0, true becomes 1.
  • If the target type's value range includes the source type's, and the source type's value range cannot be contained by int and unsigned int, then the source type can be promoted to the target type.5
Note

char->short is not a numeric promotion because char is first promoted to int / unsigned int, then int / unsigned int->short, which does not satisfy the conditions for numeric promotion.

For example (assuming int is 32-bit, unsigned short is 16-bit, signed char and unsigned char are 8-bit, 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 int arithmetic, resulting in (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 int arithmetic, resulting in (int)-255.
  • false - (unsigned short)12 first promotes false to (int)0 and (unsigned short)12 to (int)12, then performs int arithmetic, resulting in (int)-12.

Floating-point Promotion

Floating-point numbers with smaller bit width can be promoted to floating-point numbers with larger bit width (e.g., when a float variable and a double variable are used in arithmetic, the float variable is promoted to double), with the value unchanged.

Numeric Conversions

During numeric conversion, the value may change.

Note

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

Integral Conversions

  • If the target type is an unsigned integer type with bit width \(x\), the conversion result is the original value modulo \(2^x\).

    • If the target type's bit width is greater than the source type's bit width:

      • If the source type is signed, sign extension is generally performed before conversion.

        For example:

        • Converting (short)-1 ((short)0b1111'1111'1111'1111) to unsigned int first performs sign extension to get 0b1111'1111'1111'1111'1111'1111'1111'1111, then performs integer conversion, resulting in (unsigned int)4'294'967'295 ((unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111).
        • Converting (short)32'767 ((short)0b0111'1111'1111'1111) to unsigned int first performs sign extension to get 0b0000'0000'0000'0000'0111'1111'1111'1111, then performs integer conversion, resulting in (unsigned int)32'767 ((unsigned int)0b0000'0000'0000'0000'0111'1111'1111'1111).
      • If the source type is unsigned, zero extension is performed before conversion.

        For example, converting (unsigned short)65'535 ((unsigned short)0b1111'1111'1111'1111) to unsigned int first performs zero extension to get 0b0000'0000'0000'0000'1111'1111'1111'1111, then performs integer conversion, resulting in (unsigned int)65'535 ((unsigned int)0b0000'0000'0000'0000'1111'1111'1111'1111).

    • If the target type's bit width is not greater than the source type's bit width, truncation is performed before conversion.

      For example, converting (unsigned int)4'294'967'295 ((unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111) to unsigned short first truncates to 0b1111'1111'1111'1111, then performs integer conversion, resulting in (unsigned short)65'535 ((unsigned short)0b1111'1111'1111'1111).

  • If the target type is a signed integer type with bit width \(x\), then generally, the conversion result can be considered as the original value modulo \(2^x\).6

    For example, converting (unsigned int)4'294'967'295 ((unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111) to short results in (short)-1 ((short)0b1111'1111'1111'1111).

  • If the target type is bool, it is a Boolean conversion.

  • If the source type is bool, false is converted to 0 of the target type, and true is converted to 1 of the target type.

Floating-point Conversions

Converting a floating-point number with larger bit width to one with smaller bit width rounds the number to the nearest value representable in the target type.

Floating-Integral Conversions

  • When converting a floating-point number to an integer, the fractional part is discarded.

    If the target type is bool, it is a Boolean conversion.

  • When converting an integer to a floating-point number, it is rounded to the nearest value representable in the target type.

    If the value cannot fit into the target type, the behavior is undefined.

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

Boolean Conversion

When converting other types to bool, zero values become false, and non-zero values become true.

Defining Variables

Simply put1, defining a variable requires a type specifier (indicating the variable's type) and the variable name.

For example, the following statements are variable definitions.

1
2
3
int oi;
double wiki;
char org = 'c';

In the program segments we have encountered so far, variables defined inside braces are local variables, while variables defined outside braces are global variables. There are exceptions, but they are not necessary to understand now.

Global variables defined without an initializer are initialized to \(0\). Local variables do not have this property; they must be manually initialized, otherwise they may cause hard-to-find bugs.

Variable Scope

Scope is the code block where a variable can be accessed.

The scope of a global variable starts from its point of definition2 to the end of the file.

The scope of a local variable starts from its point of definition to the end of the code block.

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

1
2
3
4
5
6
7
int g = 20;  // Define global variable

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

If a variable with the same name is defined in an inner block of a code block, the outer block's variable with the same name cannot be accessed in the inner block.

For example, in the code above, the output value of \(g\) will be \(10\). Therefore, to avoid unexpected errors, try to avoid naming local variables the same as global variables.

Constants

Constants are fixed values that do not change during program execution.

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

1
2
const int a = 2;
a = 3;

If you modify a constant's value, an error will occur during compilation: error: assignment of read-only variable‘a’.

References and Notes

  1. Working Draft, Standard for Programming Language C++
  2. Types - cppreference.com
  3. C language's Arithmetic types - cppreference.com
  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

  1. When defining a variable, besides the type specifier, other specifiers can be included. See Declarations - cppreference for details. ↩

  2. More precisely, the point of declaration. ↩

  3. Includes array types, reference types, pointer types, class types, function types, etc. Since this article is for beginners, they are not introduced here. Refer to Types - cppreference.com for details. ↩

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

  5. Does not include wide character types, bit-fields, and enumeration types. See Integral conversions - cppreference. ↩

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