Learn C Programming

Lesson 1 of 9 ¡ Strings

Module 10 ¡ Strings

Characters, ASCII and the char Type in Practice

FreeReading

In this lesson

  • Treat a char as a small number: print it with %c and %d, and do arithmetic on digits and letters.
  • Test and change characters with ctype.h instead of hand-written ranges, and explain why its functions take an int.
  • Read a line character by character, shift its letters with a wrap, and explain a byte above 127 on the Playground.

David reads a house number one character at a time, so the 7 arrives as a character. He wants the next house, so he prints c + 1 with %d, and the screen says 56. His program is not wrong: it answered a question he did not mean to ask. This lesson shows what a character really is, and the library that makes working with one safe.

A character is a small number

Here is David's program, cut down to the line that surprised him.

#include <stdio.h>

int main(void)
{
    char c = '7';

    printf("%d\n", c + 1);
    printf("%c\n", c + 1);
    return 0;
}
56
8

A character is one letter, digit, space or symbol. C stores it as a number, its character code, from Module 1's ASCII table. '7' is code 55, so '7' + 1 is 56.

Module 4 lesson 6 showed that a char in a sum is promoted to int first. So c + 1 is the int 56. %d writes it as a number, and %c writes the character with code 56, which is 8. David asked for the next character's code, when he wanted the next number's value.

So a char holds a code, and %c or %d only decides how that code is shown.

Digits and letters sit in unbroken runs

The ten digits are codes 48 to 57, in order, with no gaps. The capitals are 65 to 90 and the small letters 97 to 122, each run unbroken. Three moves follow.

  • c - '0' turns a digit character into its value: 55 - 48 is 7. Adding '0' to a value from 0 to 9 turns it back into a character.
  • c - 'A' is a capital's distance from A, so c - 'A' + 1 is its place in the alphabet.
  • 'a' - 'A' is 32, the distance from every capital to its small letter.
#include <stdio.h>

int main(void)
{
    char digit = '7';
    int value = digit - '0';
    char next = value + 1 + '0';
    char letter = 'D';

    printf("%c has code %d and value %d\n", digit, digit, value);
    printf("value + 1 = %d, stored back as the character %c\n", value + 1, next);
    printf("%c is letter %d of the alphabet\n", letter, letter - 'A' + 1);
    printf("two letters on from %c: %c\n", letter, letter + 2);
    printf("'a' - 'A' = %d, so %c + 32 is %c\n", 'a' - 'A', letter, letter + 32);
    return 0;
}
7 has code 55 and value 7
value + 1 = 8, stored back as the character 8
D is letter 4 of the alphabet
two letters on from D: F
'a' - 'A' = 32, so D + 32 is d

C17 (section 5.2.1) promises an unbroken run only for the digits. In EBCDIC, a character set IBM mainframes still use, i is 137 and j is 145, with non-letters between. The Playground uses ASCII, so the letter moves work here.

The ASCII neighbourhood: digits, capitals and small letters Codes 32 to 127, in rows of 32 32 sp ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 64 @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ 96 ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ '0' to '9' are 48 to 57 'A' to 'Z' are 65 to 90 'a' to 'z' are 97 to 122 +32 +32 Dashed: the six symbols between Z and a. c >= 'A' && c <= 'z' lets them in. Every small letter sits one row, exactly 32 codes, below its capital.

In rows of 32, each small letter sits one row, exactly 32 codes, below its capital. Six symbols sit between Z and a.

So a digit's value is c - '0', and the case distance is 'a' - 'A'.

ctype.h: ask the library what a character is

Bob tests for a letter with c >= 'a' && c <= 'z', which forgets the capitals. His second try, c >= 'A' && c <= 'z', lets in the six symbols. Both compile in silence, and both are why ctype.h exists.

A library function comes with C, already written and tested, like printf. The header <ctype.h> declares a family of them for single characters.

The ctype.h functions this module uses

#include <ctype.h>

isalpha(c)   isdigit(c)   isspace(c)   isupper(c)   islower(c)
toupper(c)   tolower(c)
  • isalpha: a letter? isdigit: one of '0' to '9'? isspace: whitespace?
  • isupper: a capital? islower: a small letter?
  • Those five return 0 for no, and a number that is not 0 for yes.
  • toupper and tolower return the changed character, or c unchanged.
  • c is an int: a value an unsigned char can hold, or EOF.

Here the library answers for all 128 codes.

#include <stdio.h>
#include <ctype.h>

#define CODES 128

int main(void)
{
    printf("isspace is true for codes:");
    for (int code = 0; code < CODES; code++) {
        if (isspace(code)) {
            printf(" %d", code);
        }
    }
    printf("\n");

    printf("from 'A' to 'z' but not letters:");
    for (int code = 'A'; code <= 'z'; code++) {
        if (!isalpha(code)) {
            printf(" %c", code);
        }
    }
    printf("\n");
    return 0;
}
isspace is true for codes: 9 10 11 12 13 32
from 'A' to 'z' but not letters: [ \ ] ^ _ `

Whitespace means the characters that print as empty space. isspace knows six: tab, newline, vertical tab, form feed, carriage return and space. A hand-written test usually remembers two. The second line is Bob's second try, caught in the act.

So the library wins three ways. It is right for every character, isdigit(c) reads as what it means, and it stays right where letters are not one run.

Its yes is only "not 0", so write if (isdigit(c)), never if (isdigit(c) == 1).

So a character test is one ctype.h call, and its answer is zero or not zero.

toupper and tolower hand back a new character

Alice likes her answers in a table, so she asks about five characters at once, kept in a char array.

#include <stdio.h>
#include <ctype.h>

#define SAMPLES 5

int main(void)
{
    char samples[SAMPLES] = {'q', 'Q', '7', '?', ' '};

    printf("char  code  isalpha  isdigit  toupper  tolower\n");
    for (int i = 0; i < SAMPLES; i++) {
        char c = samples[i];

        printf(" '%c'  %4d  %7d  %7d      '%c'      '%c'\n",
               c, c, isalpha(c) != 0, isdigit(c) != 0, toupper(c), tolower(c));
    }
    return 0;
}
char  code  isalpha  isdigit  toupper  tolower
 'q'   113        1        0      'Q'      'q'
 'Q'    81        1        0      'Q'      'q'
 '7'    55        0        1      '7'      '7'
 '?'    63        0        0      '?'      '?'
 ' '    32        0        0      ' '      ' '

isalpha(c) != 0 is a comparison, so it prints 1 or 0 (Module 4 lesson 2). toupper returns the capital of a small letter and gives anything else back unchanged; tolower is its mirror. Neither changes c: each returns a new value, like every function in Module 7. So to change a character, write c = toupper(c);.

So toupper and tolower convert letters, leave the rest alone, and hand the result back.

Reading a line one character at a time

Zara's password rule says: at least one capital letter. Her program reads the line one character at a time. Module 6 lesson 3 used the same loop to throw a bad line away. Now every character is kept.

Reading one line, character by character

char c = 0;

while (scanf("%c", &c) == 1 && c != '\n') {
    /* use c */
}
  • "%c" reads one character, a space included, so no space goes before it.
  • == 1 ends the loop when the input runs out and scanf returns EOF.
  • c != '\n' ends it at the newline, the character the Enter key adds.
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c = 0;
    int capitals = 0;

    while (scanf("%c", &c) == 1 && c != '\n') {
        if (isupper(c)) {
            capitals++;
        }
    }
    printf("capitals: %d\n", capitals);
    if (capitals > 0) {
        printf("rule met\n");
    } else {
        printf("add a capital letter\n");
    }
    return 0;
}

Trace it for the input Lima 42X, then Enter, one row per character.

PasscIts codeisupper(c)capitals after the pass
1L76yes1
2i105no1
3m109no1
4a97no1
5a space32no1
6452no1
7250no1
8X88yes2
9'\n'10not tested: the loop ends2
capitals: 2
rule met

That output is for the input Lima 42X, then Enter.

Module 3 lesson 4 put a space before %c to skip a leftover newline. Here the spaces are data and the newline is the stop sign, so no space. The == 1 matters because the Playground passes your input exactly as typed. Without Enter no newline arrives, but scanf returns EOF and the loop still stops.

The last section adds the one step a byte above 127 needs.

So the loop reads one character per pass and stops at the newline or at the end of the input.

A Caesar shift: letter arithmetic with a wrap

A Caesar shift replaces each letter with the one k places later in the alphabet. The Roman writer Suetonius says Julius Caesar used k = 3. The hard part is the wrap: three places after z is c.

Module 4's remainder operator does the wrap. Turn the letter into its distance from 'a', a number from 0 to 25. Add k and take the remainder after dividing by 26, which folds 28 back to 2. Then add 'a' to make it a letter again.

#include <stdio.h>

#define SHIFT 3

int main(void)
{
    char c = 0;

    while (scanf("%c", &c) == 1 && c != '\n') {
        int position = c - 'a';
        int moved = (position + SHIFT) % 26;

        printf("%c", 'a' + moved);
    }
    printf("\n");
    return 0;
}

Trace it for the input lazy, then Enter.

cIts codepositionposition + SHIFTmoved, after % 26Printed
l108111414o
a97033d
z12225282c
y12124271b
'\n'10the loop ends, and the last printf ends the line
odcb

That output is for the input lazy, then Enter. The program trusts its input to be small letters; a capital would come out wrong. Keeping the case is Exercise 3, and decoding, which shifts backwards, has its own trap in the problems lesson.

So a wrap is three steps: to a distance, add and take % 26, back to a letter.

Bytes above 127: a signed char and a Bangla letter

ASCII stops at 127. Other scripts are usually stored as UTF-8, where a character takes one to four bytes. A Bangla letter takes three (Module 1 lesson 4). Here is what the reading loop sees.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c = 0;

    while (scanf("%c", &c) == 1 && c != '\n') {
        unsigned char byte = (unsigned char)c;

        printf("as char %4d   as unsigned char %3d   isalpha %d\n",
               c, byte, isalpha(byte) != 0);
    }
    return 0;
}
as char   65   as unsigned char  65   isalpha 1
as char  -32   as unsigned char 224   isalpha 0
as char  -90   as unsigned char 166   isalpha 0
as char -123   as unsigned char 133   isalpha 0

That output is for the input AāĻ…, then Enter, on GCC 12 for x86-64 Linux at Compiler Explorer. The Playground read the same four codes for the same input: 65, -32, -90 and -123.

āĻ… is the three bytes 224, 166 and 133. A plain char may be signed or unsigned (Module 2 lesson 4), and on the Playground it is signed. So 224 comes out as -32, the road Module 4 lesson 6's teaser took from 200 to -56.

That breaks the ctype.h rule: the argument must be a value an unsigned char can hold, or EOF (C17 section 7.4). So isalpha(c) on -32 is undefined behaviour. The Playground's GNU C Library happens to accept it; another library may crash. The cast (unsigned char)c turns -32 back into 224.

None of the three bytes is a letter, even after the cast: each is a third of one. Real Bangla text needs a library that understands UTF-8, such as ICU, the International Components for Unicode. That keeps Module 1 lesson 4's promise.

From here on, a program reading bytes it did not choose writes the cast. Text known to be ASCII may skip it.

So on the Playground a byte above 127 is a negative char, and (unsigned char) makes it safe for ctype.h.

Example 1: yes or no, in either case

The smallest useful ctype.h program: a game asks whether to save, and a player may type y or Y.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char answer = 0;
    int choice = 0;

    scanf(" %c", &answer);
    choice = tolower((unsigned char)answer);

    if (choice == 'y') {
        printf("game saved\n");
    } else if (choice == 'n') {
        printf("not saved\n");
    } else {
        printf("please type y or n\n");
    }
    return 0;
}
game saved

That output is for the input Y. tolower folds both cases into one, so the ladder tests two letters, not four. Here the space in " %c" is right, because spaces before a one-letter answer are noise.

Run in Compiler
Example 2: David's digits become a number

David's first wish, done properly: the loop builds the number that the digit characters spell.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c = 0;
    int value = 0;

    while (scanf("%c", &c) == 1 && isdigit((unsigned char)c)) {
        value = value * 10 + (c - '0');
    }
    printf("%d + 1 = %d\n", value, value + 1);
    return 0;
}
cIts codec - '0'value after the pass
25020 x 10 + 2 = 2
04802 x 10 + 0 = 20
250220 x 10 + 2 = 202
6546202 x 10 + 6 = 2026
'\n'10not a digit: the loop ends2026
2026 + 1 = 2027

That output is for the input 2026, then Enter. Multiplying by 10 pushes the earlier digits one place to the left. scanf("%d") does this walk inside, with a sign and an overflow check. This version has neither, so keep its input to nine digits.

Run in Compiler
Example 3: Kenji's game controls

Kenji's maze game reads a line of moves: w up, s down, a left, d right, in either case. Any other character is ignored and counted. This is the program a beginner writes, and it is a good one.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c = 0;
    int x = 0;
    int y = 0;
    int ignored = 0;

    while (scanf("%c", &c) == 1 && c != '\n') {
        switch (tolower((unsigned char)c)) {
        case 'w':
            y++;
            break;
        case 's':
            y--;
            break;
        case 'a':
            x--;
            break;
        case 'd':
            x++;
            break;
        default:
            ignored++;
            break;
        }
    }
    printf("position (%d, %d), ignored %d\n", x, y, ignored);
    return 0;
}
cAfter tolowerMovexyignored
Wwup010
ddright110
Ddright210
ssdown200
aaleft100
??none101
wwup111
'\n'the loop ends111
position (1, 1), ignored 1

That output is for the input WdDsa?w, then Enter. Thanks to tolower, the switch needs one case per move, not two. The default catches everything else, so a typo never moves the player.

Run in Compiler

Where this is used

  • Databases. SQLite's SQL reader skips ctype.h and classifies bytes with its own 256-entry table, sqlite3CtypeMap, casting each byte to unsigned char first. Its own table answers the same whatever the machine's language settings.
  • Password rules. Linux's pam_pwquality counts a new password's digits, capitals, small letters and other characters. Its settings dcredit, ucredit, lcredit and ocredit name those four classes.
  • Ciphers. ROT13, a Caesar shift with k = 13, hid spoilers on Usenet. Applied twice it gives the text back, and tr 'A-Za-z' 'N-ZA-Mn-za-m' does it in one Unix command.
  • Terminals. Ctrl with a letter sends its place in the alphabet: Ctrl+C is 3, Ctrl+D is 4. At the start of a line, a Unix terminal reads Ctrl+D as the end of input, and scanf returns EOF.

Common mistakes

1. A hand-written range that misses the capitals.

if (c >= 'a' && c <= 'z') {
    letters++;
}

No message at any command line. Inside the reading loop, the input Dhaka Lima gives letters: 7, not 9, because D and L are never counted. Write isalpha((unsigned char)c). You will write the range because you picture the alphabet in small letters.

2. Double quotes around one character.

char c = "A";

The Playground shows it too, in the Compile output tab: warning: initialization of 'char' from 'char *' makes integer from pointer without a cast [-Wint-conversion]. -Wall and -Wall -Wextra say the same, and GCC 14 makes it an error. "A" is a string, a row of characters that lesson 02 opens; char * is Module 11's notation. One run on Compiler Explorer printed an invisible character, code 4, not A. Write 'A'; you will reach for double quotes because printf put text in them.

3. Calling toupper and dropping the answer.

char c = 'q';

toupper(c);
printf("%c\n", c);

No message at any command line, and it prints q. toupper returns the capital and changes nothing else, so store it: c = toupper((unsigned char)c);. You will expect the call to change c because its name sounds like an order.

4. A space before %c inside the reading loop.

while (scanf(" %c", &c) == 1 && c != '\n') {
    if (c == ' ') {
        spaces++;
    }
    read++;
}

No message at any command line. The space in the format skips every space, and the newline too. For the input Go on, Enter, no, this loop printed spaces: 0, characters read: 6. It never saw the space, and it read on into the second line. Without that space it printed spaces: 1, characters read: 5. You will add it because Module 3 lesson 4 taught it as the fix, and after a number, it is.

Brain teaser

Kenji notices that 32 is a power of two, so he swaps + 32 for Module 4's exclusive or, ^.

#include <stdio.h>

int main(void)
{
    printf("%c %c\n", 'A' ^ 32, 'a' ^ 32);
    printf("%d\n", '5' ^ 32);
    return 0;
}

It compiles with no message at any command line. What does it print? Why does ^ 32 swap the case of a letter, and what does it do to '5'?

Write 65 and 97 in binary, eight digits each, and compare them bit by bit. Then find each character's row in the diagram.

Exercise 1Easy

Zara checks what a password line is made of. Count its letters, digits, spaces and other characters.

Input. One line of printable ASCII characters, spaces allowed.

Output. Four lines: letters N, digits N, spaces N and others N. Letters and digits are what isalpha and isdigit say, spaces are ' ', and the newline is not counted.

Constraints. The line holds 0 to 100 characters, with codes from 32 to 126.

Sample. Input Dhaka 2026, go! gives letters 7, digits 4, spaces 2 and others 2 on four lines.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c = 0;
    int letters = 0;
    int digits = 0;
    int spaces = 0;
    int others = 0;

    while (scanf("%c", &c) == 1 && c != '\n') {
        /* Add 1 to the counter that c belongs to. */
    }
    printf("letters %d\n", letters);
    printf("digits %d\n", digits);
    printf("spaces %d\n", spaces);
    printf("others %d\n", others);
    return 0;
}

Graded as char-classes. The hidden tests include an empty line and a line of 100 characters. They catch a range that misses capitals, and a counted newline.

Run in Compiler
Exercise 2Easy

David checks a line by adding up its digits, each on its own: 12 adds 1, then 2.

Input. One line of printable ASCII characters.

Output. The sum of the values of its digits, or 0 if it has none.

Constraints. The line holds 0 to 100 characters, with codes from 32 to 126.

Sample. Input Room 12, floor 3 gives 6.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    char c = 0;
    int sum = 0;

    while (scanf("%c", &c) == 1 && c != '\n') {
        /* If c is a digit, add its value to sum. */
    }
    printf("%d\n", sum);
    return 0;
}

Not graded on its own. char-classes already grades the same walk over a line.

Run in Compiler
Exercise 3Medium

David's club writes notes in code: every letter moves k places forward, from z round to a. Capitals stay capitals, and every other character stays as it is.

Input. k on the first line, then one line of text.

Output. The text, with every letter shifted and everything else unchanged.

Constraints. 0 <= k <= 25. The text holds 0 to 100 characters, with codes from 32 to 126.

Sample. Input 3, then Zoo 42! on the next line, gives Crr 42!.

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int k = 0;
    char c = 0;

    scanf("%d", &k);
    while (scanf("%c", &c) == 1 && c != '\n') {
        /* the rest of the first line: nothing to do */
    }

    while (scanf("%c", &c) == 1 && c != '\n') {
        /* Shift c forward by k places if it is a letter, keeping
           its case, then print it. Print any other c unchanged. */
    }
    printf("\n");
    return 0;
}

Not graded on its own. caesar-decode in the problems lesson grades the same shift, backwards, over many lines.

Run in Compiler

Common doubts

  • Why does isdigit('7') give 2048 and not 1?

    Because a yes is any number that is not 0, as Module 4 lesson 2 warned. The Playground's GNU C Library returns the bit flag it tested. One run on Compiler Explorer gave 2048 for isdigit('7'), 1024 for isalpha('q') and 8192 for isspace(' '). So isdigit(c) == 1 is never true there.

  • Why do these functions take an int, when a character is a char?

    So that they also accept EOF, which is not a character. getchar(), C's other one-character reader, returns an int that is an unsigned char value or EOF, exactly the range ctype.h accepts. A negative char falls outside it.

  • Can %c print a Bangla letter?

    Not in one call: %c prints one byte. Print the three bytes in a row, though, and a UTF-8 screen draws the letter again. One run on Compiler Explorer read āĻ… with the reading loop, echoed each byte with %c, and showed āĻ… and a count of 3 bytes.

Key takeaways

  • A char holds a code: %c draws it, %d prints the number, and arithmetic works on the number.
  • Digits and letters are unbroken runs in ASCII, so c - '0', c - 'A' + 1 and a shift with % 26 are plain arithmetic.
  • Test characters with ctype.h, not ranges: it is right for every character, and its yes is "not 0".
  • toupper and tolower return the new character; store it, or nothing changes.
  • while (scanf("%c", &c) == 1 && c != '\n') reads a line one character at a time, spaces included.
  • On the Playground a byte above 127 is a negative char: cast it to unsigned char for ctype.h, and leave Bangla text to a library.

Next, Bob stores Maria's name in exactly five boxes, and it prints with Dhaka glued to its end. Lesson 02 finds the zero that went missing.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Strings: an Array That Ends in a Zero