Types and how data lives in memory · about 40 min

Characters are small integers

Why this matters

Text is everywhere: file names, configuration files, user input, network protocols, log lines. In C there is no special "letter" kind of value. A character is a small integer, and text is a sequence of those integers in memory. Once you see that, a lot of C idioms make sense at a glance: turning '7' into the number 7 with a subtraction, converting case by adding 32, or checking whether a character is a digit with two comparisons. It also explains several bugs, such as characters that print as negative numbers.

Character encodings and ASCII

To store text, everyone has to agree which number stands for which character. That agreement is a character encoding. The foundation of almost every encoding in use is ASCII (1963), which assigns the numbers 0 to 127:

  code  char      code  char      code  char      code  char
   10   \n         48   0          65   A          97   a
   32   (space)    49   1          66   B          98   b
   33   !          ...             ...             ...
   ...             57   9          90   Z         122   z

Some facts about this table are used constantly:

The full table is one command away in the sandbox terminal: man 7 ascii.

The char type and character literals

C's char type is one byte, the smallest integer type. A character literal is a character in single quotes, 'A', and it is simply another way to write the integer 65. These two lines store the same byte:

char letter = 'A';
char same = 65;

printf decides how to show it: %c prints the character with that code, %d prints the number.

#include <stdio.h>

int main(void)
{
    char letter = 'A';
    printf("%c is stored as %d (0x%x)\n", letter, letter, letter);
    printf("'a' = %d, 'z' = %d, '0' = %d, ' ' = %d, '\\n' = %d\n", 'a', 'z', '0', ' ', '\n');
    printf("%c%c%c\n", 72, 105, 33);
    return 0;
}
A is stored as 65 (0x41)
'a' = 97, 'z' = 122, '0' = 48, ' ' = 32, '\n' = 10
Hi!

Single and double quotes mean different things: 'A' is one character (an integer), "A" is a string (an array of characters ending with a zero byte; strings get their own lessons). Escape sequences work in character literals too: '\n', '\t', '\'', '\\', and '\0' for the character with code 0.

Note: a curious detail: in C, the type of the literal 'A' is actually int, not char, so sizeof('A') is 4. It makes no practical difference when you store it in a char.

Arithmetic on characters

Because characters are integers, you can compute with them. The three idioms you will see in real C code over and over:

char next = 'A' + 1;          /* 'B': the next letter */
int value = '7' - '0';        /* 7: digit character to its numeric value */
char upper = 'g' - 'a' + 'A'; /* 'G': lowercase to uppercase */
after A comes B
'7' - '0' = 7
g -> G

The second one deserves a picture, because converting text to numbers is how every program reads numeric input:

  character:  '0'  '1'  '2'  '3'  '4'  '5'  '6'  '7'  '8'  '9'
  code:        48   49   50   51   52   53   54   55   56   57
  code - '0':   0    1    2    3    4    5    6    7    8    9

Writing '0' instead of 48 makes the intent obvious and does not depend on the encoding.

The third idiom works by computing the letter's position in the alphabet ('g' - 'a' is 6) and adding it to the start of the other range. Positions are a powerful trick: to wrap around the alphabet, compute the position, do arithmetic modulo 26, and convert back.

ctype.h: classification without the arithmetic

For everyday checks, the standard library has <ctype.h>:

functiontrue for
isdigit(c)'0' to '9'
isalpha(c)letters
isalnum(c)letters and digits
isspace(c)space, tab, newline, carriage return, vertical tab, form feed
isupper(c), islower(c)uppercase / lowercase letters
toupper(c), tolower(c)return the converted character (others unchanged)

The is functions return an int that is nonzero for true and 0 for false (not necessarily 1). They also handle locales and non-ASCII systems, so prefer them in real code. You will use them a lot once you can make decisions with if.

Is char signed?

char is a byte, but is it -128 to 127 or 0 to 255? The C standard leaves it to the implementation. On x86-64 Linux, char is signed; on ARM Linux (Raspberry Pi, phones) it is unsigned. C therefore has three distinct types: char, signed char and unsigned char.

char c = 200;
printf("char 200 -> %d\n", c);
char 200 -> -56

on x86-64, because 200 does not fit in a signed char and is reduced to 200 - 256. For ASCII text this never matters (all codes are below 128). For raw bytes it matters a lot: when processing binary data or the individual bytes of UTF-8 text, use unsigned char or uint8_t.

Beyond ASCII: UTF-8 in one paragraph

ASCII has no é, no €, no Chinese or emoji. Today's standard is Unicode, which assigns a number (a code point) to over a hundred thousand characters, and UTF-8, which encodes each code point as 1 to 4 bytes. ASCII characters stay single bytes with the same values, which is why C code that only cares about ASCII keeps working on UTF-8 text. But one character may now be several chars: "héllo" takes 6 bytes plus the terminating zero, not 5. Proper Unicode text processing in C needs extra libraries; this track sticks to ASCII.

Common mistakes

Recap

Going further: run printf 'h\xc3\xa9llo\n' | od -c in the sandbox terminal to see the two bytes that make up é in UTF-8.

Exercises

Exercise 1 · quiz

Predict the output

What does this print?

printf("%d %c\n", 'A' + 2, 'A' + 2);
Answer
  • 67 C

'A' is just the int 65, so 'A' + 2 is 67. %d prints that number in decimal; %c prints the character whose code is 67, which is C. The value is the same; only the formatting differs.

Exercise 2 · write code

From digit character to number

Write a function

int digit_value(char c)

that takes a digit character '0' to '9' and returns its numeric value 0 to 9. For example digit_value('7') returns 7. You may assume c is always a digit. Do it with arithmetic on the character codes (no table, no list of cases).

The check calls your function directly; main can contain whatever you like.

File: main.c

Starting point

#include <stdio.h>

int digit_value(char c)
{
    return c;   /* this returns the character code, e.g. 55 for '7' */
}

int main(void)
{
    printf("%d\n", digit_value('7'));
    return 0;
}
Hint 1

The digit characters have consecutive codes: '0' is 48, '1' is 49, ... '9' is 57.

Hint 2

Subtract the code of '0'. Write it as '0', not 48, so the intent is clear.

Reference solution
#include <stdio.h>

int digit_value(char c)
{
    return c - '0';
}

int main(void)
{
    printf("%d\n", digit_value('7'));
    return 0;
}

Exercise 3 · quiz

Is char signed?

Which statements about plain char (without signed or unsigned) are true? Select all that apply.

Answer
  • It is always exactly one byte

  • Whether it is signed is up to the implementation: signed on x86-64 Linux, unsigned on ARM Linux

  • char, signed char and unsigned char are three distinct types

sizeof(char) is 1 by definition. Its signedness is implementation-defined, which is why code that stores bytes above 127 (binary data, UTF-8) should use unsigned char (or uint8_t). The compiler treats char as a separate type from both explicit variants even though it behaves like one of them, which matters for warnings and later for pointers.

Exercise 4 · write code

Shift a letter

The Caesar cipher replaces each letter with the letter a fixed number of places later in the alphabet, wrapping around from z back to a. Write a function

char shift3(char c)

that shifts a lowercase letter by 3: 'a' becomes 'd', 'w' becomes 'z', 'x' becomes 'a', 'z' becomes 'c'. You may assume c is always a lowercase letter 'a' to 'z'.

Use arithmetic only, no if and no list of cases. You will need the remainder operator %: a % b is what is left over after dividing a by b, so 26 % 26 is 0, 27 % 26 is 1 and 5 % 26 is 5.

The check calls your function for every lowercase letter.

File: main.c

Starting point

#include <stdio.h>

char shift3(char c)
{
    return c + 3;   /* works for a..w, breaks for x, y, z */
}

int main(void)
{
    printf("%c%c%c\n", shift3('a'), shift3('b'), shift3('z'));
    return 0;
}
Hint 1

Turn the letter into a position 0..25 first: c - 'a'.

Hint 2

Add 3 to the position and take % 26 so 26, 27, 28 wrap to 0, 1, 2.

Hint 3

Turn the position back into a letter by adding 'a': (c - 'a' + 3) % 26 + 'a'.

Reference solution
#include <stdio.h>

char shift3(char c)
{
    return (c - 'a' + 3) % 26 + 'a';
}

int main(void)
{
    printf("%c%c%c\n", shift3('a'), shift3('b'), shift3('z'));
    return 0;
}