Coming Soon

This lesson is currently being developed

Unsigned integers and why to avoid them

Understand the void type and its uses in C++.

Fundamental Data Types
Chapter
Beginner
Difficulty
40min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

4.5 — Unsigned integers and why to avoid them

In this lesson, you'll learn about unsigned integer types, understand their characteristics, and discover why they can be problematic for beginners and even experienced programmers.

What are unsigned integers?

Unsigned integers are data types that can only store non-negative whole numbers (zero and positive integers). They cannot store negative values. The word "unsigned" means there's no sign bit - all bits are used to represent magnitude.

Think of unsigned integers like a car's odometer - it can only count up from zero and never shows negative values.

C++ unsigned integer types

C++ provides unsigned versions of all integer types:

#include <iostream>
#include <climits>

int main()
{
    std::cout << "Unsigned Integer Types and Ranges:\n\n";
    
    // unsigned char (usually 8 bits)
    std::cout << "unsigned char:\n";
    std::cout << "  Size: " << sizeof(unsigned char) << " byte(s)\n";
    std::cout << "  Range: 0 to " << static_cast<unsigned int>(UCHAR_MAX) << "\n\n";
    
    // unsigned short (usually 16 bits)
    std::cout << "unsigned short:\n";
    std::cout << "  Size: " << sizeof(unsigned short) << " byte(s)\n";
    std::cout << "  Range: 0 to " << USHRT_MAX << "\n\n";
    
    // unsigned int (usually 32 bits)
    std::cout << "unsigned int:\n";
    std::cout << "  Size: " << sizeof(unsigned int) << " byte(s)\n";
    std::cout << "  Range: 0 to " << UINT_MAX << "\n\n";
    
    // unsigned long (at least 32 bits)
    std::cout << "unsigned long:\n";
    std::cout << "  Size: " << sizeof(unsigned long) << " byte(s)\n";
    std::cout << "  Range: 0 to " << ULONG_MAX << "\n\n";
    
    // unsigned long long (at least 64 bits)
    std::cout << "unsigned long long:\n";
    std::cout << "  Size: " << sizeof(unsigned long long) << " byte(s)\n";
    std::cout << "  Range: 0 to " << ULLONG_MAX << "\n";
    
    return 0;
}

Typical Output:

Unsigned Integer Types and Ranges:

unsigned char:
  Size: 1 byte(s)
  Range: 0 to 255

unsigned short:
  Size: 2 byte(s)
  Range: 0 to 65535

unsigned int:
  Size: 4 byte(s)
  Range: 0 to 4294967295

unsigned long:
  Size: 8 byte(s)
  Range: 0 to 18446744073709551615

unsigned long long:
  Size: 8 byte(s)
  Range: 0 to 18446744073709551615

The advantage: larger positive range

Unsigned integers can store larger positive values than their signed counterparts:

#include <iostream>
#include <climits>

int main()
{
    std::cout << "Comparing signed vs unsigned ranges:\n\n";
    
    std::cout << "8-bit comparison:\n";
    std::cout << "signed char: " << static_cast<int>(SCHAR_MIN) 
              << " to " << static_cast<int>(SCHAR_MAX) << " (range: " 
              << (static_cast<int>(SCHAR_MAX) - static_cast<int>(SCHAR_MIN) + 1) << ")\n";
    std::cout << "unsigned char: 0 to " << static_cast<int>(UCHAR_MAX) 
              << " (range: " << (static_cast<int>(UCHAR_MAX) + 1) << ")\n\n";
    
    std::cout << "32-bit comparison:\n";
    std::cout << "int: " << INT_MIN << " to " << INT_MAX << "\n";
    std::cout << "unsigned int: 0 to " << UINT_MAX << "\n";
    std::cout << "Unsigned can store values up to " << UINT_MAX 
              << " vs signed max of " << INT_MAX << "\n\n";
    
    // Demonstrate the advantage
    unsigned int largeValue = 3000000000U;  // This fits in unsigned int
    std::cout << "Large unsigned value: " << largeValue << std::endl;
    
    // This would overflow in signed int on most systems
    // int signedValue = 3000000000;  // Warning/Error: too large for signed int
    
    return 0;
}

Output:

Comparing signed vs unsigned ranges:

8-bit comparison:
signed char: -128 to 127 (range: 256)
unsigned char: 0 to 255 (range: 256)

32-bit comparison:
int: -2147483648 to 2147483647
unsigned int: 0 to 4294967295
Unsigned can store values up to 4294967295 vs signed max of 2147483647

Large unsigned value: 3000000000

Problem 1: Underflow behavior

The biggest issue with unsigned integers is what happens when they go below zero:

#include <iostream>

int main()
{
    std::cout << "Demonstrating unsigned underflow:\n\n";
    
    unsigned int count = 3;
    std::cout << "Starting count: " << count << std::endl;
    
    // Subtract normally
    for (int i = 0; i < 5; ++i)
    {
        std::cout << "count = " << count;
        count = count - 1;  // Subtract 1
        std::cout << " → after -1: " << count;
        
        if (count > 1000000)  // Detect wrap-around
            std::cout << " (WRAPPED AROUND!)";
        
        std::cout << std::endl;
    }
    
    std::cout << "\nThis is dangerous because unsigned integers never become negative!\n";
    
    return 0;
}

Output:

Demonstrating unsigned underflow:

Starting count: 3
count = 3 → after -1: 2
count = 2 → after -1: 1
count = 1 → after -1: 0
count = 0 → after -1: 4294967295 (WRAPPED AROUND!)
count = 4294967295 → after -1: 4294967294 (WRAPPED AROUND!)

This is dangerous because unsigned integers never become negative!

Problem 2: Subtraction surprises

Subtracting larger values from smaller unsigned values can cause unexpected behavior:

#include <iostream>

int main()
{
    std::cout << "Unsigned subtraction problems:\n\n";
    
    unsigned int a = 10;
    unsigned int b = 20;
    
    std::cout << "a = " << a << ", b = " << b << std::endl;
    
    // This looks innocent but is dangerous!
    unsigned int result = a - b;
    
    std::cout << "a - b = " << result << std::endl;
    std::cout << "Expected: -10, but got: " << result << " (wrapped around!)\n\n";
    
    // This creates problems in conditions
    std::cout << "Testing if (a - b) > 5:\n";
    if (result > 5)
    {
        std::cout << "Condition is true! (But shouldn't be for negative result)\n";
    }
    else
    {
        std::cout << "Condition is false\n";
    }
    
    // Compare with signed integers
    std::cout << "\nWith signed integers:\n";
    int signedA = 10;
    int signedB = 20;
    int signedResult = signedA - signedB;
    std::cout << signedA << " - " << signedB << " = " << signedResult << " (correct!)\n";
    
    return 0;
}

Output:

Unsigned subtraction problems:

a = 10, b = 20
a - b = 4294967286
Expected: -10, but got: 4294967286 (wrapped around!)

Testing if (a - b) > 5:
Condition is true! (But shouldn't be for negative result)

With signed integers:
10 - 20 = -10 (correct!)

Problem 3: Loop problems

Unsigned integers can cause infinite loops in seemingly simple code:

#include <iostream>

void demonstrateDangerousLoop()
{
    std::cout << "Dangerous loop with unsigned integers:\n";
    
    unsigned int i = 5;
    int iterations = 0;
    
    // This loop looks like it should run 6 times (5, 4, 3, 2, 1, 0)
    // But it becomes infinite!
    while (i >= 0 && iterations < 10)  // Added safety limit
    {
        std::cout << "i = " << i << std::endl;
        i = i - 1;
        iterations++;
        
        // When i becomes 0 and we subtract 1, it wraps to a huge number
        // The condition (i >= 0) is always true for unsigned!
    }
    
    std::cout << "Loop stopped at iteration " << iterations 
              << " (would be infinite without safety limit!)\n";
    std::cout << "Final value of i: " << i << "\n\n";
}

void demonstrateSafeLoop()
{
    std::cout << "Safe loop with signed integers:\n";
    
    int i = 5;
    
    // This works correctly
    while (i >= 0)
    {
        std::cout << "i = " << i << std::endl;
        i = i - 1;
    }
    
    std::cout << "Loop ended normally\n";
    std::cout << "Final value of i: " << i << "\n";
}

int main()
{
    demonstrateDangerousLoop();
    demonstrateSafeLoop();
    
    return 0;
}

Output:

Dangerous loop with unsigned integers:
i = 5
i = 4
i = 3
i = 2
i = 1
i = 0
i = 4294967295
i = 4294967294
i = 4294967293
i = 4294967292
Loop stopped at iteration 10 (would be infinite without safety limit!)
Final value of i: 4294967291

Safe loop with signed integers:
i = 5
i = 4
i = 3
i = 2
i = 1
i = 0
Loop ended normally
Final value of i: -1

Problem 4: Mixing signed and unsigned

Mixing signed and unsigned types in expressions can lead to unexpected conversions:

#include <iostream>

int main()
{
    std::cout << "Mixing signed and unsigned integers:\n\n";
    
    int signedValue = -1;
    unsigned int unsignedValue = 1;
    
    std::cout << "signedValue = " << signedValue << std::endl;
    std::cout << "unsignedValue = " << unsignedValue << std::endl;
    
    // This comparison doesn't work as expected!
    std::cout << "\nTesting (signedValue < unsignedValue):\n";
    if (signedValue < unsignedValue)
    {
        std::cout << "signedValue < unsignedValue: true\n";
    }
    else
    {
        std::cout << "signedValue < unsignedValue: false (WRONG!)\n";
        std::cout << "signedValue was converted to unsigned: " 
                  << static_cast<unsigned int>(signedValue) << std::endl;
    }
    
    // Demonstrate the conversion
    std::cout << "\nWhat happens during comparison:\n";
    std::cout << "signedValue (-1) converts to unsigned: " 
              << static_cast<unsigned int>(signedValue) << std::endl;
    std::cout << "So we're comparing " << static_cast<unsigned int>(signedValue) 
              << " < " << unsignedValue << std::endl;
    
    return 0;
}

Output:

Mixing signed and unsigned integers:

signedValue = -1
unsignedValue = 1

Testing (signedValue < unsignedValue):
signedValue < unsignedValue: false (WRONG!)
signedValue was converted to unsigned: 4294967295

What happens during comparison:
signedValue (-1) converts to unsigned: 4294967295
So we're comparing 4294967295 < 1

When unsigned integers might be appropriate

Despite the problems, there are legitimate uses for unsigned integers:

Bit manipulation and flags

#include <iostream>
#include <bitset>

int main()
{
    std::cout << "Appropriate use: Bit flags\n";
    
    // Using unsigned for bit manipulation
    unsigned int permissions = 0;
    
    const unsigned int READ_PERMISSION = 1;    // Binary: 001
    const unsigned int WRITE_PERMISSION = 2;   // Binary: 010
    const unsigned int EXECUTE_PERMISSION = 4; // Binary: 100
    
    // Set permissions
    permissions |= READ_PERMISSION;
    permissions |= WRITE_PERMISSION;
    
    std::cout << "Permissions: " << std::bitset<8>(permissions) << std::endl;
    
    // Check permissions
    if (permissions & READ_PERMISSION)
        std::cout << "Has read permission\n";
    
    if (permissions & EXECUTE_PERMISSION)
        std::cout << "Has execute permission\n";
    else
        std::cout << "No execute permission\n";
    
    return 0;
}

System interfaces that require unsigned

#include <iostream>
#include <vector>

int main()
{
    std::cout << "System interface example:\n";
    
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // std::vector::size() returns size_t (unsigned type)
    // But we should be careful when using it
    
    std::cout << "Vector size: " << numbers.size() << std::endl;
    
    // Safe way to iterate
    for (size_t i = 0; i < numbers.size(); ++i)
    {
        std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl;
    }
    
    // Or better yet, use range-based for loop
    std::cout << "\nUsing range-based for loop (preferred):\n";
    for (int number : numbers)
    {
        std::cout << "Number: " << number << std::endl;
    }
    
    return 0;
}

Best practices: Avoiding unsigned integer problems

✅ Prefer signed integers

// Good: Use signed integers as default choice
int count = 0;
int size = 100;
int index = 0;

// Avoid unless specifically needed
// unsigned int count = 0;

✅ Use explicit checks instead of relying on wrap-around

#include <iostream>

int main()
{
    int count = 5;
    
    // Safe countdown loop
    while (count > 0)  // Explicit check
    {
        std::cout << "Countdown: " << count << std::endl;
        count--;
    }
    
    std::cout << "Finished countdown\n";
    return 0;
}

✅ Be careful with subtraction

#include <iostream>

int safeUnsignedSubtract(unsigned int a, unsigned int b)
{
    // Return signed result to handle negative values
    return static_cast<int>(a) - static_cast<int>(b);
}

int main()
{
    unsigned int a = 10;
    unsigned int b = 20;
    
    int result = safeUnsignedSubtract(a, b);
    std::cout << a << " - " << b << " = " << result << std::endl;
    
    return 0;
}

✅ Convert unsigned to signed when needed

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // Convert size to signed for safer arithmetic
    int size = static_cast<int>(numbers.size());
    
    // Now safe to use in calculations that might go negative
    for (int i = size - 1; i >= 0; --i)
    {
        std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl;
    }
    
    return 0;
}

Summary of why to avoid unsigned integers

  1. Unexpected wrap-around: Subtracting below zero wraps to huge positive values
  2. Infinite loops: Loop conditions like i >= 0 are always true for unsigned
  3. Subtle bugs: Mixing signed and unsigned causes unexpected conversions
  4. Counter-intuitive behavior: Results don't match mathematical expectations

The golden rule: Use signed integers (int) as your default choice. Only use unsigned integers when:

  • Working with bit manipulation or flags
  • Interfacing with APIs that specifically require unsigned types
  • You genuinely need the extended positive range AND you're very careful about wrap-around

Quiz

  1. What happens when you subtract 1 from an unsigned integer that equals 0?
  2. Why can mixing signed and unsigned integers cause problems?
  3. What's wrong with this loop? for (unsigned int i = 10; i >= 0; --i)
  4. When might unsigned integers be appropriate to use?
  5. What's the maximum value that can be stored in a 32-bit unsigned integer?

Practice exercises

Try these exercises to understand unsigned integer pitfalls:

  1. Write a program that demonstrates unsigned integer wrap-around with different operations (addition, subtraction, multiplication)
  2. Create a safe countdown function that works correctly with both signed and unsigned inputs
  3. Implement a function that safely compares values that might be negative, avoiding signed/unsigned mixing issues
  4. Write a bit manipulation program that legitimately uses unsigned integers for flag operations

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion