Module 10 ¡ Strings
Characters, ASCII and the char Type in Practice
In this lesson
- Treat a
charas a small number: print it with%cand%d, and do arithmetic on digits and letters. - Test and change characters with
ctype.hinstead of hand-written ranges, and explain why its functions take anint. - 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, soc - 'A' + 1is 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.
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.
toupperandtolowerreturn the changed character, orcunchanged.cis anint: a value anunsigned charcan hold, orEOF.
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.== 1ends the loop when the input runs out andscanfreturnsEOF.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.
| Pass | c | Its code | isupper(c) | capitals after the pass |
|---|---|---|---|---|
| 1 | L | 76 | yes | 1 |
| 2 | i | 105 | no | 1 |
| 3 | m | 109 | no | 1 |
| 4 | a | 97 | no | 1 |
| 5 | a space | 32 | no | 1 |
| 6 | 4 | 52 | no | 1 |
| 7 | 2 | 50 | no | 1 |
| 8 | X | 88 | yes | 2 |
| 9 | '\n' | 10 | not tested: the loop ends | 2 |
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.
| c | Its code | position | position + SHIFT | moved, after % 26 | Printed |
|---|---|---|---|---|---|
l | 108 | 11 | 14 | 14 | o |
a | 97 | 0 | 3 | 3 | d |
z | 122 | 25 | 28 | 2 | c |
y | 121 | 24 | 27 | 1 | b |
'\n' | 10 | the 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.
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.
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;
}
| c | Its code | c - '0' | value after the pass |
|---|---|---|---|
2 | 50 | 2 | 0 x 10 + 2 = 2 |
0 | 48 | 0 | 2 x 10 + 0 = 20 |
2 | 50 | 2 | 20 x 10 + 2 = 202 |
6 | 54 | 6 | 202 x 10 + 6 = 2026 |
'\n' | 10 | not a digit: the loop ends | 2026 |
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.
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;
}
| c | After tolower | Move | x | y | ignored |
|---|---|---|---|---|---|
W | w | up | 0 | 1 | 0 |
d | d | right | 1 | 1 | 0 |
D | d | right | 2 | 1 | 0 |
s | s | down | 2 | 0 | 0 |
a | a | left | 1 | 0 | 0 |
? | ? | none | 1 | 0 | 1 |
w | w | up | 1 | 1 | 1 |
'\n' | the loop ends | 1 | 1 | 1 |
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.
Where this is used
- Databases. SQLite's SQL reader skips
ctype.hand classifies bytes with its own 256-entry table,sqlite3CtypeMap, casting each byte tounsigned charfirst. Its own table answers the same whatever the machine's language settings. - Password rules. Linux's
pam_pwqualitycounts a new password's digits, capitals, small letters and other characters. Its settingsdcredit,ucredit,lcreditandocreditname 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
scanfreturnsEOF.
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.
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.
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.
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.
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 forisalpha('q')and 8192 forisspace(' '). Soisdigit(c) == 1is never true there.Why do these functions take an
int, when a character is achar?So that they also accept
EOF, which is not a character.getchar(), C's other one-character reader, returns anintthat is anunsigned charvalue orEOF, exactly the rangectype.haccepts. A negativecharfalls outside it.Can
%cprint a Bangla letter?Not in one call:
%cprints 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
charholds a code:%cdraws it,%dprints the number, and arithmetic works on the number. - Digits and letters are unbroken runs in ASCII, so
c - '0',c - 'A' + 1and a shift with% 26are plain arithmetic. - Test characters with
ctype.h, not ranges: it is right for every character, and its yes is "not 0". toupperandtolowerreturn 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 tounsigned charforctype.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