Skip to content

C Base Conversion: Binary, Decimal, and Hexadecimal

Overview

This lecture connects two threads of CS 315: writing real command-line tools in C and understanding how numbers are represented and converted inside a computer. We first review how to parse optional command-line arguments (the options consumed by tools like ls -a -l and gcc -o) using a simple loop, then build the conceptual machinery behind Project 1's numconv tool: the difference between a numeric quantity, its string representation, and its machine-integer form; positional notation in base 10, base 2, and base 16; bit width and value ranges; and the two core conversion algorithms (string-to-integer using ASCII arithmetic, and integer-to-string using division and modulo).

Learning Objectives

  • Parse optional command-line arguments in C using a loop that detects and consumes options beginning with -
  • Distinguish a numeric quantity from its string representation and from its machine-integer (binary) representation
  • Apply positional notation to evaluate numbers in decimal (base 10), binary (base 2), and hexadecimal (base 16)
  • Read C integer literals written in decimal, binary (0b), and hex (0x) and explain that they all denote the same machine value
  • Identify the most-significant and least-significant bit, and compute the value range of an n-bit unsigned number
  • Convert a decimal string to a machine integer using ASCII codes and accumulation
  • Convert a machine integer to a digit string in any base using repeated division and modulo
  • Detect the base of an input string from its prefix (0b, 0x, or none) for Project 1's numconv

Prerequisites

  • Basic C: main(int argc, char *argv[]), arrays, while/for loops, if/else
  • C strings as char * / char[] terminated by '\0'
  • Comfort with printf for output and reading program output
  • Powers of 2 and elementary arithmetic
  • Familiarity with the shell and how command-line arguments are passed to a program

1. Optional Command-Line Arguments

Every Unix tool you use is a C program that inspects its command-line arguments. Many of those arguments are options (also called flags or switches) that begin with a -:

ls -a              # one option: -a (show hidden files)
ls -a -l           # two options: -a and -l
ls -l -a           # same two options, different order
gcc -o <filename>  # an option that TAKES an argument: -o names the output file

Two important observations from these examples:

  • Order should not matter. ls -a -l and ls -l -a produce the same result. A well-written tool accepts its options in any order.
  • Some options take an argument. -a and -l are standalone toggles, but gcc -o foo consumes the next argument (foo) as the name of the output file. The option and its value travel together.

The Loop-Based Parsing Strategy

In this course we parse arguments ourselves, without third-party libraries such as getopt. The strategy is a single loop that walks across the arguments and, for each one, checks whether it is a known option and consumes it (and its argument, if any):

loop:
    * check for each option
      and consume

Visually, the parser scans argv from left to right. When it sees -a it records "show hidden" and advances; when it sees -l it records "long format" and advances:

ls   -a   -l
     └┬┘  └┬┘
      │    └── recognized, consume
      └─────── recognized, consume
flowchart LR
    A[Start at argv index 1] --> B{Argument<br/>starts with '-'?}
    B -- yes --> C[Identify which option]
    C --> D{Option takes<br/>an argument?}
    D -- yes --> E[Consume next argv<br/>as its value]
    D -- no --> F[Set a flag]
    E --> G[Advance index]
    F --> G
    B -- no --> H[Treat as positional<br/>argument]
    H --> G
    G --> I{More arguments?}
    I -- yes --> B
    I -- no --> J[Done parsing]

The key idea is that the loop body looks at the current argument once, classifies it, and then advances past everything it just handled. Options that take a value advance by two positions; standalone flags advance by one.


2. A Worked Example: echo-repeat

To make option parsing concrete, consider a tool echorepeat that prints a string, optionally with a header and footer, and optionally repeated -n times. Its full command line looks like this:

echorepeat -h -f -n <count> <string>
Token Meaning
-h print a header line
-f print a footer line
-n <count> repeat the main string count times
<string> the string to print (the positional argument)

A run with a header and footer but no repeat count:

echorepeat -h -f foo

might produce:

- - - - -
foo
- - - - -

Counting Arguments with argc

C delivers the command line as argc (the count) and argv (an array of strings). The program name itself is always argv[0]. For the call echorepeat -h foo:

echorepeat   -h   foo
    0         1     2      <- argv index

argc == 3

So argc counts the program name plus every argument. With three tokens on the line, argc == 3 and the valid indices are argv[0], argv[1], and argv[2].

Walking the Indices

When the parser loops over the real arguments it starts at index 1 (skipping the program name) and walks forward. For echorepeat -h -f foo:

echorepeat   -h    -f    foo
            i=1   i=2    i=3
             │     │      └── i has now reached argc; loop stops
             │     └───────── recognized -f, advance
             └─────────────── recognized -h, advance

The loop continues while i < argc. Each recognized option advances i; when an option needs a value (like -n), the parser reads argv[i+1] and advances i by two so it does not re-examine the value as if it were another option.

Conditional Output

Once parsing is finished, output is driven by the flags that were set. In pseudocode:

if (header_flag) {
    print_header();
}

for (int r = 0; r < count; r++) {
    printf("%s\n", string_arg);
}

if (footer_flag) {
    print_footer();
}

The instructor noted two design points worth carrying into every assignment:

  • Keep main small. Split the work into a parser that reads the options into a small struct, and an output generator that consumes that struct. Do not pile all logic into main.
  • Robust error handling is out of scope for this class. Do basic checks (for example, that a required value follows -n), but you do not need to handle every malformed input gracefully.
flowchart LR
    A[argc, argv] --> B[parse_args]
    B --> C["struct options
(header, footer, count, string)"]
    C --> D[generate_output]
    D --> E[stdout]

3. Three Faces of a Number

Before converting between bases, we must separate three distinct things that beginners tend to conflate. Consider the number two hundred forty-five written three ways:

   245            "245"              [ 245 ]
 quantity         string         machine integer
                                    (binary)
Form What it is How it lives in memory
245 (quantity) The abstract mathematical value A concept; not stored directly
"245" (string) Three printable characters '2', '4', '5' Three (plus one) bytes of ASCII codes
machine integer The CPU's binary encoding of the value A fixed-width pattern of bits

The String "245" Is Bytes of ASCII

A C string is an array of char, each holding one byte. The string "245" is not the number 245 — it is the characters '2', '4', '5' followed by a terminating '\0':

"245"
 byte  byte  byte
 '2'   '4'   '5'
  50    52    53      <- ASCII codes

Each character has an ASCII code. The character '2' is the byte value 50, '4' is 52, '5' is 53. These codes have nothing to do with the digit values 2, 4, 5 — they are just how the characters are encoded. Converting the string to a real number means turning those byte codes into the quantity 245.

The Machine Integer Is Binary

The CPU stores 245 as a fixed-width binary pattern:

binary
[ 245 ]   =   1 1 1 1 0 1 0 1
machine
integer        ↑
            address r1, r2 (lives in memory / a register)

So 1111 0101 is the binary representation of 245. This binary pattern is the form the machine actually computes with. The summary slide phrased the central insight as: numeric quantities and their computer representations are different things; computation happens in binary.

The Conversion Round-Trip

Project 1's job is exactly the round-trip between these forms:

            parse                       format
"245"  ───────────────►  machine int  ───────────────►  "0b...", "0xF5", "245"
 string                   (uint32_t)                       output strings

We read an input string, convert it to a uint32_t machine integer, then convert that integer back out to one or more strings in the requested bases.


4. Bases: Decimal, Binary, Hexadecimal

A base (or radix) is the number of distinct digits a positional system uses. We work with three:

Base Name Digits C prefix
10 decimal 0 1 2 3 4 5 6 7 8 9 none
2 binary 0 1 0b
16 hexadecimal 0-9 A B C D E F 0x

The number of distinct digits is always equal to the base. Decimal has 10 digits, binary has 2, hex has 16. Because we run out of the usual digit characters at 9, hexadecimal borrows the letters A through F to stand for the values 10 through 15.

The same underlying quantity can be written in any base. The next sections show how positional notation lets us evaluate a number in each base, and these all describe the same machine integer.


5. Decimal (Base 10) and Positional Notation

We already know base 10 intuitively, but writing out the mechanics reveals the pattern that generalizes to every base. Take 245:

 2 1 0          <- position (exponent), counted from the right starting at 0
 2 4 5

Each digit is multiplied by the base raised to its position, and the results are summed:

245 = (2 × 10²) + (4 × 10¹) + (5 × 10⁰)
    =  2 × 100  +  4 × 10   +  5 × 1
    =   200     +    40      +    5
    = 245
Position Digit Place value Contribution
2 2 \(10^2 = 100\) 200
1 4 \(10^1 = 10\) 40
0 5 \(10^0 = 1\) 5

The general rule, for a number with digits \(d_{n-1} \dots d_1 d_0\) in base \(b\):

\[\text{value} = \sum_{i=0}^{n-1} d_i \times b^{\,i}\]

This single formula is all of base conversion. Change the base \(b\) and the place values change, but the procedure is identical. (Note that anything raised to the power 0 is 1, which is why the rightmost place value is always 1.)


6. Binary (Base 2)

Binary uses only the digits 0 and 1, and the place values are powers of 2. Evaluate 0b1101:

 3 2 1 0          <- positions
0b1 1 0 1
   8 4 2 1        <- place values (2³ 2² 2¹ 2⁰)
0b1101 = (1 × 2³) + (1 × 2²) + (0 × 2¹) + (1 × 2⁰)
       =  1 × 8   +  1 × 4   +  0 × 2   +  1 × 1
       =    8     +    4      +    0      +    1
       =  13
Position Bit Place value Contribution
3 1 \(2^3 = 8\) 8
2 1 \(2^2 = 4\) 4
1 0 \(2^1 = 2\) 0
0 1 \(2^0 = 1\) 1

So 0b1101 is the binary representation of the quantity 13.

The Same Value, Three Literals

This is a good moment to see that the value is independent of how you write it. All three of these C declarations store the exact same machine integer in x:

int x = 13;        // decimal literal
int x = 0b1101;    // binary literal   (GCC extension)
int x = 0xD;       // hexadecimal literal

Decimal 13, binary 0b1101, and hex 0xD all denote the value thirteen. The prefix tells the compiler how to read the digits; once read, they become identical bits in memory. (The 0b binary literal prefix is a GCC/Clang extension; 0x is standard C.)


7. Bits, Width, and Value Range

A single binary digit is a bit. In a multi-bit number, the two ends have names:

        0b 1 1 0 1
           ↑       ↑
        most      least
     significant  significant
        bit          bit
        (MSB)       (LSB)
  • The most significant bit (MSB) is the leftmost bit. It carries the largest place value.
  • The least significant bit (LSB) is the rightmost bit. It carries the place value \(2^0 = 1\).

How Many Values Fit in n Bits?

With n bits there are \(2^n\) distinct bit patterns, so an n-bit unsigned number can represent:

n-bit binary numbers
    2ⁿ  possible values
range   0  to  (2ⁿ) − 1

The range starts at 0 (all bits zero) and ends at \(2^n - 1\) (all bits one). It is \(2^n - 1\), not \(2^n\), because zero consumes one of the \(2^n\) patterns.

Worked Example: 4 Bits

2⁴ = 16
0 to 15
0000  ...  1111
Bits Distinct patterns (\(2^n\)) Min Max (\(2^n - 1\))
4 16 0 15
8 256 0 255
16 65,536 0 65,535
32 4,294,967,296 0 4,294,967,295

A 4-bit number runs from 0000 (0) to 1111 (15), which is exactly 16 values. This is why Project 1 uses uint32_t: 32 bits give a range of 0 to 4,294,967,295, matching the largest test input 0b11111111111111111111111111111111 = 4294967295.


8. Hexadecimal (Base 16)

Binary is unwieldy to read and write — 32 bits is a long string of 0s and 1s. Hexadecimal compresses it: because \(2^4 = 16\), each hex digit corresponds to exactly 4 bits. This makes hex the natural shorthand for binary.

The Digit Correspondence Table

Dec (10) Bin (2) Hex (16) (lowercase)
0 0000 0 0
1 0001 1 1
2 0010 2 2
3 0011 3 3
4 0100 4 4
5 0101 5 5
6 0110 6 6
7 0111 7 7
8 1000 8 8
9 1001 9 9
10 1010 A a
11 1011 B b
12 1100 C c
13 1101 D d
14 1110 E e
15 1111 F f

Note that hex digits 10 through 15 are written A-F (or a-f). Project 1 must accept both upper and lower case on input. Notice also that the 0b1101 example from earlier is row 13: D in hex, which matches int x = 0xD from Section 6.

Hex-to-Decimal: A Worked Example

Evaluate 0x2AF with positional notation, base 16:

   2 1 0          <- positions
0x 2 A F
0x2AF = (2 × 16²) + (A × 16¹) + (F × 16⁰)
      = (2 × 256) + (10 × 16) + (15 × 1)
      =    512    +    160     +    15
      =  687
Position Hex digit Value Place value Contribution
2 2 2 \(16^2 = 256\) 512
1 A 10 \(16^1 = 16\) 160
0 F 15 \(16^0 = 1\) 15

So 0x2AF = 687. The only twist versus decimal is remembering that A means 10 and F means 15 when you multiply.

Why 4 Bits Per Hex Digit Matters

Because each hex digit is exactly one 4-bit group (a nibble), you can convert between hex and binary by simple substitution, no arithmetic required:

Hex:     0x  2     A     F
Binary:    0010  1010  1111
           └──┘  └──┘  └──┘
            2     A     F

So 0x2AF = 0010 1010 1111 in binary. Going the other way, you group the bits into fours from the right and replace each group with its hex digit.


9. Project 1: numconv and Base Detection

Project 1 has two parts. The warm-up numinfo classifies a string as a valid decimal, binary, and/or hex value. The main tool numconv converts a number from one base into the others.

What numconv Does

$ numconv 687 -o 16
0x2AF

$ numconv 0x2AF -o 10
687

The -o <base> option selects the output base, and may be repeated. When multiple output bases are requested, the order is fixed: base 2, then base 10, then base 16.

$ numconv 213 -o 2 -o 16 -o 10
0b11010101
213
0xd5

The Normalization Strategy

Rather than write a converter for every (input base, output base) pair, we funnel everything through a single machine integer:

numstr            machine          numstr
"245"   ────────►   int    ────────►  (base)
                  uint32_t           "0x..."

Every input string is parsed into a uint32_t. Then that one integer is formatted out into whichever base(s) the user asked for. This collapses what could be nine conversions into two reusable directions:

dec str  ┐                      ┌─►  dec str
bin str  ┼──►   uint32_t   ──────┼─►  bin str
hex str  ┘                      └─►  hex str
flowchart LR
    A["dec str"] --> N["uint32_t"]
    B["bin str"] --> N
    C["hex str"] --> N
    N --> D["dec str"]
    N --> E["bin str"]
    N --> F["hex str"]
    style N fill:#f9f,stroke:#333,stroke-width:2px

Detecting the Input Base from the Prefix

To parse the input we first decide what base the input string is in by looking at its first two characters (its prefix):

input base
    if (prefix(str) == "0b")  ==  binary
    if (prefix(str) == "0x")  ==  hex
    else                          decimal
Prefix Base Example
0b 2 (binary) 0b1010
0x 16 (hex) 0xFF
(none) 10 (decimal) 1234

After detecting the base, we strip the prefix and convert the remaining digits using the appropriate base. The next two sections show those conversions concretely.


10. String to Integer: ASCII Arithmetic

Now we implement the parse direction by hand. Project 1 forbids atoi, scanf, and printf("%d"), so we must do the digit math ourselves. Start with the decimal case using "245".

Indexing the String

char *s = "245";

s[0] = '2'
s[1] = '4'
s[2] = '5'
s[3] = '\0'    // terminating null byte

The string has a hidden fourth byte: the null terminator '\0', which marks the end. Looping until we hit '\0' is how we know when to stop.

From Character to Digit Value

The trick is the relationship between a digit character and its numeric value in the ASCII table:

ASCII
'0' = 48
'1' = 49
'2' = 50
'3' = 51
'4' = 52
'5' = 53

The digit characters are consecutive in ASCII. The character '2' is code 50, and '0' is code 48, so '2' - '0' = 50 - 48 = 2. Subtracting '0' from any digit character yields its numeric value:

int v0 = s[0] - '0';   // '2': 50 - 48 = 2
int v1 = s[1] - '0';   // '4': 52 - 48 = 4
int v2 = s[2] - '0';   // '5': 53 - 48 = 5

Reassembling the Quantity

With each digit's value extracted, positional notation rebuilds the number:

int v = (v0 * 100) + (v1 * 10) + (v2 * 1);
//    = (  2 * 100) + (  4 * 10) + (  5 * 1)
//    = 245

This is exactly the base-10 expansion from Section 5, but now driven by characters from a string.

A General Loop

Hard-coding the place values (100, 10, 1) only works for a 3-character string. The general approach processes the string left to right and multiplies the running total by the base before adding each new digit. Here is the decimal version the instructor wrote:

uint32_t decstr_to_uint32(char *s) {
    int v = 0;
    int d;
    int i = 0;

    while (s[i] != '\0') {
        v = v * 10;        // shift existing digits up one place
        d = s[i] - '0';    // value of the current digit
        v = v + d;         // add it in
        i = i + 1;
    }
    return v;
}

Tracing "245":

Step s[i] d v = v*10 + d
start 0
i=0 '2' 2 0*10 + 2 = 2
i=1 '4' 4 2*10 + 4 = 24
i=2 '5' 5 24*10 + 5 = 245
i=3 '\0' loop ends → 245

To generalize to any base, replace the literal 10 with the detected base, and replace s[i] - '0' with a char_to_digit helper that also handles the hex letters:

int char_to_digit(char c) {
    if (c >= '0' && c <= '9') {
        return c - '0';            // 0..9
    }
    if (c >= 'a' && c <= 'f') {
        return c - 'a' + 10;       // 10..15 (lowercase)
    }
    if (c >= 'A' && c <= 'F') {
        return c - 'A' + 10;       // 10..15 (uppercase)
    }
    return -1;                     // not a valid digit
}

11. Integer to String: Division and Modulo

The reverse direction takes a machine integer and produces a string of digit characters. The tool here is repeated division by the base, collecting remainders as digits. Use 245 to decimal again so we can check the answer.

The Two Operators

For a value v and base 10:

245 / 10  = 24     <- quotient (division strips the last digit)
245 % 10  = 5      <- remainder (modulo extracts the last digit)

Integer division by the base drops the lowest digit; modulo by the base returns that lowest digit. Repeating the process peels digits off the right end one at a time.

Extracting Each Digit

Starting with v = 245 and base 10:

d0 = v % 10  = 245 % 10 = 5     →  ascii: c0 = '0' + d0 = '5'
v  = v / 10  = 245 / 10 = 24

d1 = v % 10  =  24 % 10 = 4     →  ascii: c1 = '0' + d1 = '4'
v  = v / 10  =  24 / 10 = 2

d2 = v % 10  =   2 % 10 = 2     →  ascii: c2 = '0' + d2 = '2'
v  = v / 10  =   2 / 10 = 0     →  loop stops when v == 0
Step v d = v % 10 char '0' + d v = v / 10
1 245 5 '5' 24
2 24 4 '4' 2
3 2 2 '2' 0

Digit to Character

Just as we subtracted '0' to go from character to digit, we add '0' to go from digit back to character. For digits above 9 (hex), add an offset from 'a' (or 'A'):

char digit_to_char(int d) {
    if (d < 10) {
        return '0' + d;          // 0..9  ->  '0'..'9'
    }
    return 'a' + (d - 10);       // 10..15 -> 'a'..'f'
}

The Digits Come Out Backwards

Notice the digits were produced in the order 5, 4, 2 — the reverse of "245". The least significant digit comes out first. So the algorithm builds the string into a buffer and then reverses it (or fills the buffer from the high end toward the low end). Here is the general routine:

void int_to_string(uint32_t value, char *str, int base) {
    char buf[33];          // enough for 32 binary digits + '\0'
    int i = 0;

    if (value == 0) {
        buf[i] = '0';
        i = i + 1;
    }
    while (value != 0) {
        int rem = value % base;
        buf[i] = digit_to_char(rem);
        value = value / base;
        i = i + 1;
    }

    // buf now holds the digits least-significant first; reverse into str
    int j = 0;
    while (i > 0) {
        i = i - 1;
        str[j] = buf[i];
        j = j + 1;
    }
    str[j] = '\0';
}
flowchart TD
    A["value, base"] --> B{"value == 0?"}
    B -- yes --> C["emit one '0'"]
    B -- no --> D["rem = value % base"]
    D --> E["push digit_to_char(rem)"]
    E --> F["value = value / base"]
    F --> G{"value != 0?"}
    G -- yes --> D
    G -- no --> H["reverse collected digits"]
    C --> H
    H --> I["null-terminate string"]

After producing the bare digits, numconv prepends the right prefix for the output base: 0b for binary, nothing for decimal, 0x for hex.


12. Putting It Together

The complete numconv data flow chains everything from this lecture:

flowchart TD
    A["input string
e.g. 0x2AF"] --> B["detect base from prefix
(Section 9)"]
    B --> C["strip prefix"]
    C --> D["string_to_int
ASCII arithmetic (Section 10)"]
    D --> E["uint32_t value
e.g. 687"]
    E --> F["for each requested -o base
(order: 2, 10, 16)"]
    F --> G["int_to_string
div / mod (Section 11)"]
    G --> H["prepend prefix
0b / (none) / 0x"]
    H --> I["print line"]

For numconv 0x2AF -o 10:

  1. Prefix 0x → base 16.
  2. Strip prefix → "2AF".
  3. string_to_int("2AF", 16) → 687 (the machine integer).
  4. Requested output base 10.
  5. int_to_string(687, str, 10)"687".
  6. No prefix for decimal → print 687.

The same uint32_t 687 could just as easily be formatted as 0b1010101111 or 0x2af, which is the whole point of normalizing through a single integer.


Key Concepts

Concept Definition Example
Option (flag) A command-line argument beginning with - that selects behavior -a, -l, -o 10
Option with argument An option that consumes the next token as its value gcc -o foo, -n 5
argc / argv Argument count and the array of argument strings; argv[0] is the program name echorepeat -h fooargc == 3
Quantity vs string vs machine int The abstract value, its character spelling, and its binary encoding 245, "245", 1111 0101
Base (radix) The count of distinct digits in a positional system 10, 2, 16
Positional notation Value = sum of each digit times the base raised to its position \(245 = 2{\cdot}10^2 + 4{\cdot}10^1 + 5{\cdot}10^0\)
Bit A single binary digit, 0 or 1 0b1101 has 4 bits
MSB / LSB Most / least significant bit (leftmost / rightmost) In 1101, MSB=1, LSB=1
n-bit range An n-bit unsigned number holds 0 to \(2^n - 1\) 4 bits: 0..15
Hex digit = 4 bits Each hex digit maps to exactly one 4-bit group F = 1111
Base prefix First chars indicating input base 0b=binary, 0x=hex, none=decimal
ASCII digit trick c - '0' converts a digit char to its value '5' - '0' = 53 − 48 = 5
String→int v = v*base + digit, left to right "245" → 245
Int→string Repeated % base (digit) and / base (rest), then reverse 245 → "245"

Practice Problems

Problem 1: Decimal Expansion

Using positional notation, show that 703 in base 10 evaluates to 703, listing each digit's place value and contribution.

Click to reveal solution
 2 1 0          <- positions
 7 0 3

703 = (7 × 10²) + (0 × 10¹) + (3 × 10⁰)
    = (7 × 100) + (0 × 10)  + (3 × 1)
    =   700     +    0       +    3
    = 703
| Position | Digit | Place value | Contribution | |----------|-------|-------------|--------------| | 2 | 7 | 100 | 700 | | 1 | 0 | 10 | 0 | | 0 | 3 | 1 | 3 |

Problem 2: Binary to Decimal

Convert 0b10110 to decimal.

Click to reveal solution
 4 3 2 1 0       <- positions
 1 0 1 1 0
16 8 4 2 1       <- place values

0b10110 = (1 × 16) + (0 × 8) + (1 × 4) + (1 × 2) + (0 × 1)
        =    16     +    0     +    4    +    2    +    0
        = 22
So `0b10110` = 22.

Problem 3: Hex to Decimal

Convert 0x1F4 to decimal, then write the equivalent binary by substituting 4 bits per hex digit.

Click to reveal solution
   2 1 0         <- positions
0x 1 F 4

0x1F4 = (1 × 16²) + (F × 16¹) + (4 × 16⁰)
      = (1 × 256) + (15 × 16) + (4 × 1)
      =    256    +    240    +    4
      = 500

Binary by 4-bit substitution:
  1     F     4
0001  1111  0100   ->  0b000111110100
So `0x1F4` = 500 = `0b000111110100`.

Problem 4: Value Range

How many distinct values can a 12-bit unsigned number hold, and what is its maximum value?

Click to reveal solution
distinct values = 2¹² = 4096
range           = 0 to (2¹²) − 1 = 0 to 4095
maximum         = 4095
There are 4096 patterns (from `0000 0000 0000` to `1111 1111 1111`), so the range is 0 through 4095. It is 4095, not 4096, because the value 0 uses one of the patterns.

Problem 5: Trace string_to_int

Trace decstr_to_uint32("306") using the loop v = v*10 + (s[i]-'0'). Show v after each iteration. ('3'=51, '0'=48, '6'=54.)

Click to reveal solution
start:           v = 0
i=0 s[0]='3':    d = 51 - 48 = 3;   v = 0*10 + 3   = 3
i=1 s[1]='0':    d = 48 - 48 = 0;   v = 3*10 + 0   = 30
i=2 s[2]='6':    d = 54 - 48 = 6;   v = 30*10 + 6  = 306
i=3 s[3]='\0':   loop ends

Result: 306

Problem 6: Integer to Hex String

Convert the machine integer 687 to a hex string using repeated % 16 and / 16. Show the digits produced and the final (reversed) string.

Click to reveal solution
v = 687, base 16:

d0 = 687 % 16 = 15  -> 'f'      v = 687 / 16 = 42
d1 =  42 % 16 = 10  -> 'a'      v =  42 / 16 = 2
d2 =   2 % 16 = 2   -> '2'      v =   2 / 16 = 0   (stop)

Digits produced (LSB first): f, a, 2
Reversed:                     2, a, f

With the 0x prefix:  0x2af
This matches Section 8: 687 = `0x2AF`. (The autograder accepts lowercase hex output.)

Further Reading


Summary

  1. Optional arguments are parsed with a loop that scans argv, recognizes options starting with -, and consumes each one (and its value, if it takes one), allowing options in any order.

  2. argc counts the program name plus every argument, and the parser walks argv from index 1 forward, stopping when the index reaches argc. Keep main small by splitting parsing from output generation.

  3. A number has three faces: the abstract quantity, the string of ASCII characters that spells it, and the binary machine integer the CPU computes with. Project 1 converts between the string and the machine integer.

  4. Positional notation is the one formula behind all bases: value = sum of each digit times the base raised to its position. Decimal, binary, and hex differ only in their base and digit set.

  5. Binary uses powers of 2; hexadecimal uses powers of 16, and because \(2^4 = 16\), each hex digit equals exactly 4 bits, making hex a compact stand-in for binary.

  6. An n-bit unsigned number holds \(2^n\) patterns, giving a range of 0 to \(2^n - 1\); this is why uint32_t (0 to 4,294,967,295) is used for Project 1.

  7. String-to-integer uses ASCII arithmetic: subtract '0' (or an A–F offset) to get each digit's value, then accumulate left to right with v = v * base + digit.

  8. Integer-to-string uses division and modulo: % base peels off the lowest digit and / base removes it; the digits emerge least-significant first and must be reversed, with the right base prefix (0b, none, 0x) prepended.