Signed Integers

Understanding Signed Integers

In C++, integers can be classified as either signed or unsigned. A signed integer is one that can represent both positive and negative values including 0. (E.g., -5, -4, -3, -2, -1, 0, 1, 2, 3, 4).

C++ has four fundamental integer type available to use:

TypeSize
short int2 bytes (16 bits)
int4 bytes (32 bits)
long int4 bytes (32 bits)
long long int8 bytes (64 bits)

When writing negative numbers in everyday life, we use a negative sign. For example, -3 means “negative 3”. We would also typically recognize +3 as “positive 3”. (though common convention dictates that we typically omit plus prefixes).

The attribute of being positive, negative, or zero is called the number's sign.

By default, integers in C++ are signed, which means the number's sign is stored as part of the number. Therefore, a signed integer can hold both positive and negative numbers (and 0).

In binary representation, a single bit (called the sign bit) the leftmost bit is used to store the sign. The non-sign bits (called the magnitude bits) determine the magnitude of the number.

Defining Signed Integers

Here is the preferred way to define the four types of signed integers:

short s;      // prefer "short" instead of "short int"
int i;
long l;       // prefer "long" instead of "long int"
long long ll; // prefer "long long" instead of "long long int"

Although short int, long int, or long long int will work, we prefer the short names for these types (that do not use the int suffix). In addition to being more typing, adding the int suffix makes the type harder to distinguish from variables of type int. This can lead to mistakes if the short or long modifier is inadvertenly missed.

The integer types can also take an optional signed keyword, which by convention is typically placed before the type name:

signed short ss;
signed int si;
signed long sl;
signed long long sll;

However, this keyword should not be used, as it is redundant, since integers are signed by default.

Prefer the shorthand types that do not use the int suffix or signed prefix.

Signed integer ranges

As we all know, a variable with n bits can hold 2^n possible values. We can call the set of specific values that a data type can hold its range. The range of an integer variable is determined by two factors: its size (in bits), and whether it is signed or not.

By definition, an 8-bit signed integer has a range of -128 to 127. This means a signed integer can store any integer value between -128 to 127 (inclusive) safely.

An 8-bit integer contains 8 bits. Thus 2^8 is 256, so an 8-bit integer can hold possible 256 values. 

7 bits are used to hold the magnitude of the number, and 1 bit is used to hold the sign.

Range: An n-bit signed variable has a range of -(2^(n-1)) to (2^(n-1) - 1)

Overflow

What happens if we try to assign the value which is outside of the range.

The C++20 standard makes this statement “If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.”, this is called overflow.

For instance, if we try to assign the value 140 to the 8-bit signed integer, which is outside of the type range. This will result in undefined behavior.

If an arithmetic operation attempts to create a value outside the range that can be represented, this is called integer overflow (or arithmetic overflow). For signed integers, integer overflow will result in undefined behavior.

#include <iostream>

int main()
{
    // assume 4 byte integers
    int x { 2'147'483'647 }; // the maximum value of a 4-byte signed integer
    std::cout << x << '\n';

    x = x + 1; // integer overflow, undefined behavior
    std::cout << x << '\n';

    return 0;
}

// On the author's machine, the above printed:
2147483647
-2147483648

The second printed value output may vary on your machine.

  • In general, overflow results in information being lost, which is almost never desirable. If there is any suspicion that an object might need to store a value that falls outside its range, use a type with a bigger range.

Integer Division

When dividing two integers, C++ works like you would expect when the quotient is a whole number:

#include <iostream>

int main()
{
    std::cout << 20 / 4 << '\n';
    return 0;
}

// Output
5

But let's look at what happens when integer division causes a fractional result:

#include <iostream>

int main()
{
    std::cout << 8 / 5 << '\n';
    return 0;
}

// Outpu
1

When doing division with two integers (called integer division), C++ always produces an integer result. Since integers can't hold fractional values, any fractional portion is simply dropped (not rounded).

For example, when we divided 8/5, it produces the value 1.6. The fractional part (0.6) is dropped, and the result of 1 remains. Alternatively, we can say 8/5 equals 1 remainder 3. The remainder is dropped, leaving 1.

Similarly, -8/5 results in the value -1.