Coming Soon
This lesson is currently being developed
Unsigned integers and why to avoid them
Understand the void type and its uses in C++.
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
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
- Unexpected wrap-around: Subtracting below zero wraps to huge positive values
- Infinite loops: Loop conditions like
i >= 0
are always true for unsigned - Subtle bugs: Mixing signed and unsigned causes unexpected conversions
- 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
- What happens when you subtract 1 from an unsigned integer that equals 0?
- Why can mixing signed and unsigned integers cause problems?
- What's wrong with this loop?
for (unsigned int i = 10; i >= 0; --i)
- When might unsigned integers be appropriate to use?
- 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:
- Write a program that demonstrates unsigned integer wrap-around with different operations (addition, subtraction, multiplication)
- Create a safe countdown function that works correctly with both signed and unsigned inputs
- Implement a function that safely compares values that might be negative, avoiding signed/unsigned mixing issues
- Write a bit manipulation program that legitimately uses unsigned integers for flag operations
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions