📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What are bits and bytes, how big are the .NET numeric types, and what happens when an int overflows?

Bits, bytes and two's complement; the sizes and ranges of C# numeric types; unchecked versus checked overflow; and bitwise operations and flags enums.

Junior Coding Intermediate bits-and-bytes bitwise computer-basics flags-enum overflow two-complement

🎯 Key points

  • A bit is 0 or 1; a byte is 8 bits and the smallest addressable unit; C# type sizes are fixed by the spec on every platform (only nint/nuint and pointers vary)
  • Know the sizes: byte 1, short 2, int 4, long 8, Int128 16, Half 2, float 4, double 8, decimal 16 bytes; BigInteger is arbitrary precision (heap-allocated, slower)
  • Signed integers use two's complement, so the range is asymmetric (int.MinValue has no positive counterpart), and casting between signed and unsigned reinterprets the same bits ((uint)-1 is 4,294,967,295)
  • C# integer arithmetic is unchecked by default and silently wraps; floats go to Infinity/NaN; decimal always throws
  • Make overflow explicit: checked blocks or the CheckForOverflowUnderflow project property, wider types, or INumber<T>.CreateChecked / CreateSaturating / CreateTruncating
  • Validate sizes and counts from untrusted input before arithmetic; a wrapped count * size under-allocates buffers
  • Bitwise operators (&, |, ^, ~, <<, >>, >>>), BitOperations and [Flags] enums work directly on bits; smaller types and bitmaps save memory and cache

⏱ 30-second answer

A bit is a single 0 or 1; a byte is 8 bits and is the smallest unit memory addresses. C# numeric types have fixed sizes on every platform: byte 1 byte, short 2, int 4, long 8, Int128 16, Half 2, float 4, double 8, decimal 16. Signed integers use two's complement, so int runs from βˆ’2,147,483,648 to 2,147,483,647. Integer arithmetic is unchecked by default: int.MaxValue + 1 silently wraps to int.MinValue, while float/double never throw and produce Infinity or NaN, and decimal always throws. Make overflow explicit where a wrap would be a bug (money, counters, sizes from external input): use checked, a wider type, or generic-math conversions such as int.CreateChecked(x) and int.CreateSaturating(x).

πŸ—£ Full answer (senior)

Bits and bytes

  • A bit holds 0 or 1. A byte is 8 bits, so 256 possible values (0–255).
  • Memory is addressed in bytes. Larger types are several consecutive bytes.
  • sizeof(int) is 4, and Unsafe.SizeOf<T>() gives the size of any unmanaged type.

Sizes and ranges in C# (.NET 10)

Type Bytes Range / precision
bool 1 true/false (one byte, not one bit)
byte / sbyte 1 0–255 / βˆ’128–127
short / ushort / char 2 βˆ’32,768–32,767 / 0–65,535 / one UTF-16 code unit
int / uint 4 Β±2.1 billion / 0–4.29 billion
long / ulong 8 Β±9.2 Γ— 10¹⁸
Int128 / UInt128 16 Β±1.7 Γ— 10³⁸
nint / nuint 4 or 8 pointer-sized (8 in a 64-bit process)
Half / float / double 2 / 4 / 8 binary floating point, about 3 / 7 / 15–17 significant digits
decimal 16 base-10 floating point, 28–29 significant digits
BigInteger variable arbitrary size, stored on the heap, slower

Two's complement

Signed integers store negative numbers by inverting the bits and adding one. Addition then uses the same circuit for signed and unsigned values, and:

  • The range is asymmetric: int.MinValue is βˆ’2Β³ΒΉ but int.MaxValue is 2Β³ΒΉ βˆ’ 1.
  • -int.MinValue cannot be represented: unchecked negation gives back int.MinValue, and Math.Abs(int.MinValue) throws.
  • Casting between signed and unsigned keeps the bits: (uint)(-1) is 4,294,967,295.

Overflow behaviour by type

Type family On overflow Why
Integers (default) silently wrap no check instruction, fastest
Integers in checked throw OverflowException explicit opt-in
float / double / Half become Β±Infinity; invalid operations (0/0, βˆžβˆ’βˆž) give NaN; no exception IEEE 754 rules, which the CPU implements in hardware
decimal always throws implemented in software, designed for money
Casts ((byte)300) truncate to the low bits (44) unless checked

Constant expressions that overflow are compile errors. The project property <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> makes all integer arithmetic checked unless marked unchecked. In generic code, INumber<T> offers T.CreateChecked, T.CreateSaturating and T.CreateTruncating.

Bitwise operations

&, |, ^, ~, <<, >> and >>> (unsigned right shift). Common uses: flags enums, masks, hashing, binary protocols. System.Numerics.BitOperations provides PopCount, LeadingZeroCount, RotateLeft and IsPow2, which map to single CPU instructions where available.

Production example

A reporting job summed transaction amounts in cents into an int. Once a large merchant's monthly total passed about $21.4 million (2Β³ΒΉ cents), the total silently became negative and a payout report showed a loss. The fix was to use long (or decimal for money in dollars), add checked arithmetic in the aggregation, and add a test with values near the limit.

πŸ” What the interviewer is checking

Checks basic data representation and whether the candidate knows a classic silent-failure mode in C#.

Green flags: knows type sizes, explains two's complement and the asymmetric range, knows C# is unchecked by default and how to opt in, gives a realistic overflow example, comfortable with bitwise operators and flags enums.

Red flags: thinks overflow throws by default, uses double for money, can't explain why int.MinValue is special.

πŸ“š Learn in depth

1. How two's complement works (8-bit example)

  +5  = 0000 0101
  invert -> 1111 1010
  add 1  -> 1111 1011  = -5

  0111 1111 = +127  (sbyte.MaxValue)
  1000 0000 = -128  (sbyte.MinValue)
  127 + 1 -> 1000 0000 -> wraps to -128

The top bit is the sign bit. Adding 5 + (βˆ’5) in binary gives 1 0000 0000; the carry falls off the top, leaving 0. That's why hardware needs only one adder.

2. Where overflow bites

Situation What goes wrong Safer choice
Summing money in cents as int Wraps negative above ~$21.4m long or decimal, checked
(a + b) / 2 midpoint in binary search a + b overflows for large indexes a + (b - a) / 2
Hash code combination Overflow is fine and intended HashCode.Combine in unchecked code
Converting long file sizes to int Truncates for files > 2 GiB Keep long; use checked((int)x) if you must narrow
Math.Abs(int.MinValue) Throws Handle the edge case or use long
Time maths in milliseconds int ms overflows after ~24.8 days long or TimeSpan

3. Checked versus unchecked in practice

  • Unchecked is the default because overflow checks add a compare and branch to arithmetic in hot loops.
  • Turning on CheckForOverflowUnderflow for Debug builds only is a common way to catch bugs in tests without paying the cost in Release. Decide deliberately: behaviour then differs between configurations.
  • Generic maths (INumber<T>) has T.CreateChecked, CreateSaturating and CreateTruncating for explicit conversion behaviour.

4. Bit manipulation patterns

set bit n:      x |=  (1 << n)
clear bit n:    x &= ~(1 << n)
toggle bit n:   x ^=  (1 << n)
test bit n:     (x & (1 << n)) != 0
lowest set bit: x & -x
power of two:   x > 0 && (x & (x - 1)) == 0   (or BitOperations.IsPow2)

Shifts in C# mask the count: for int, x << 33 is the same as x << 1, because only the low five bits of the count are used.

5. Flags enums

A [Flags] enum gives each value its own bit so several can be combined in one integer. Use powers of two (or 1 << n), include None = 0, and test with HasFlag or (value & Flag) == Flag. ToString() prints combinations such as Read, Write.

6. bool is a byte, and why that matters

A bool uses a whole byte (and four bytes in some interop contexts). Storing a million flags as bool[] uses about 1 MB; BitArray or a ulong[] bitmap uses about 125 KB. For hot data, smaller means more fits in CPU cache.

7. Try it yourself

  1. Print int.MaxValue + 1 in an unchecked block and in a checked block, and explain both results.
  2. Write a method that returns the number of set bits in a uint with a loop, then compare it with BitOperations.PopCount in BenchmarkDotNet.
  3. Create a [Flags] enum Permissions { None = 0, Read = 1, Write = 2, Delete = 4 }, combine values, print them, and remove one with &= ~.
  4. Turn on <CheckForOverflowUnderflow> for Debug in a test project and write a test that proves your summing code throws instead of wrapping.

πŸ’» Code

namespace BitsAndBytes;

using System.Numerics;

[Flags]
public enum Permissions
{
    None = 0,
    Read = 1 << 0,
    Write = 1 << 1,
    Delete = 1 << 2,
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine($"int is {sizeof(int)} bytes, range {int.MinValue}..{int.MaxValue}");

        int max = int.MaxValue;
        int wrapped = unchecked(max + 1);
        Console.WriteLine(wrapped);                      // -2147483648

        try
        {
            int boom = checked(max + 1);
        }
        catch (OverflowException ex)
        {
            Console.WriteLine($"checked: {ex.Message}");
        }

        // Safe midpoint for large indexes
        int lo = 2_000_000_000, hi = 2_100_000_000;
        int mid = lo + (hi - lo) / 2;
        Console.WriteLine(mid);

        // Money: never sum cents in an int
        long totalCents = 0;
        foreach (var cents in new long[] { 1_500_000_000, 1_500_000_000 })
        {
            totalCents = checked(totalCents + cents);
        }
        Console.WriteLine(totalCents);

        var perms = Permissions.Read | Permissions.Write;
        perms &= ~Permissions.Write;
        Console.WriteLine(perms);                        // Read

        Console.WriteLine(BitOperations.PopCount(0b1011_0001u)); // 4
        Console.WriteLine(BitOperations.IsPow2(64));            // True
    }
}

⚠ Common mistakes

  • Mistake: Assuming C# throws on integer overflow. Why it is wrong: By default it silently wraps, so a total can turn negative and nothing in the logs says why. Instead: Use checked where wrap-around would be a bug, and pick types big enough for realistic maximums.
  • Mistake: Trusting sizes and counts from external input. Why it is wrong: A file header or request that says count = 1_000_000_000 and recordSize = 8 makes count * recordSize wrap in int, so you allocate a tiny buffer and later read past it, or bypass a length check. Instead: Validate against sensible maximums first, compute sizes in long or checked context, and reject values that do not fit (int.CreateChecked(longValue) throws).
  • Mistake: Converting between signed and unsigned without thinking. Why it is wrong: The cast keeps the bits and changes their meaning: (uint)(-1) is 4,294,967,295 and (int)uint.MaxValue is -1, so a negative length passed to an API taking uint becomes a huge size. Mixing int and uint in an expression also promotes to long, which hides the problem until the value is narrowed again. Instead: Validate the sign before converting, use checked casts, and keep public APIs on signed types (as .NET does with int lengths).
  • Mistake: Narrowing with a plain cast. Why it is wrong: (int)fileLength discards high bits for files over 2 GiB, giving a wrong or negative size. Instead: Keep long, or use checked((int)value); in generic code choose CreateChecked (throw), CreateSaturating (clamp) or CreateTruncating (wrap) deliberately.
  • Mistake: Storing money in float or double. Why it is wrong: Binary floating point cannot represent 0.1 exactly, so sums drift by fractions of a cent. Instead: Use decimal, or integer cents in a long.
  • Mistake: Comparing floating-point results with ==, or with double.Epsilon as the tolerance. Why it is wrong: 0.1 + 0.2 == 0.3 is false, and double.Epsilon is the smallest positive double (about 4.9e-324), far too small to be a tolerance. Half has only about 3 significant digits. Instead: Compare with a tolerance suited to the magnitude, check double.IsNaN explicitly, and use decimal for exact decimal values.
  • Mistake: Computing a midpoint as (lo + hi) / 2. Why it is wrong: For large indexes the sum overflows and produces a negative index. Instead: Use lo + (hi - lo) / 2.
  • Mistake: Flags enum values that are not powers of two. Why it is wrong: Values like 1, 2, 3 overlap bits, so Read | Write equals the third value and HasFlag gives wrong answers. Instead: Use 1 << n values and include None = 0.
  • Mistake: Thinking bool uses one bit, or using long everywhere "to be safe". Why it is wrong: bool uses a byte, and oversized types double memory and cache footprint for large arrays. Instead: Pick the narrowest type that fits the domain maximum; use BitArray or a ulong bitmap for millions of flags.

πŸ› Senior insight: trade-offs

Overflow bugs are rare but expensive, because they appear only at scale, long after the code shipped. Seniors choose types from the domain's realistic maximum and make overflow behaviour explicit:

Need Tool
Wrap is a bug checked, CheckForOverflowUnderflow in Debug and tests
Clamp to the range (sensor values, pixel maths) int.CreateSaturating(x), Math.Clamp
Wrap is intended (hashing) unchecked, HashCode.Combine
Exact decimal maths (money) decimal or integer cents in long

Performance versus safety: a checked operation adds a compare-and-branch. In business code (orders, invoices, file sizes) that cost is invisible, so checked arithmetic is the right default there. In tight numeric loops (image processing, hashing, SIMD code) seniors keep unchecked arithmetic, prove the ranges with tests, and validate inputs once at the boundary instead of on every operation. decimal is roughly 10–20Γ— slower than double because it is done in software, which is fine for money and wrong for physics or ML.

They treat overflow as a security issue too: attacker-controlled lengths that wrap can under-allocate buffers or bypass range checks. And they weigh memory: 10 million long values are 80 MB while int is 40 MB and a bitmap of flags is about 1.2 MB, which changes how much fits in CPU cache.

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate says an int is four bytes. Why: Checks what happens when multi-byte integers are written to files or the network.
    How to answer

    Endianness is the order in which the bytes of a multi-byte value are stored. Little-endian puts the least significant byte first, so 0x12345678 is stored as 78 56 34 12. Big-endian puts the most significant byte first: 12 34 56 78. x64 and Arm64 machines running .NET are little-endian, but network protocols ("network byte order") and many file formats are big-endian. In .NET I use BinaryPrimitives.ReadInt32BigEndian and friends so the byte order is explicit and portable. BitConverter follows the machine, and BinaryReader/BinaryWriter are always little-endian.

  2. When: Candidate uses binary or hex literals in a bitwise example. Why: Checks fluency with the notations used to inspect bits and bytes.
    How to answer

    They're all positional number systems; only the base changes. Decimal is base 10 (digits 0–9), binary is base 2 (0 and 1) and hexadecimal is base 16 (0–9 then A–F). Computers store everything in binary because transistors have two states. Developers use hex because one hex digit is exactly four bits, so a byte is always two hex digits: 0xFF is 1111 1111 is 255. That makes memory addresses, hashes, GUIDs and byte dumps short and easy to read. In C# you write 0b1010 or 0xFF and format with ToString("X2") or Convert.ToHexString.

↩ Usually asked after

More in Computer Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me