Characters and ctype.h
A char is a small integer that the program usually interprets as a character code. On this system the code is ASCII, so letters are contiguous and the difference between lower and upper case is a fixed 32. The functions in ctype.h test and convert those codes without the program naming a number.
Characters are codes
A character constant such as 'A' has type int in C, as lesson 9 showed. Printing it with %c writes the character, %d the code, and %X the code in hexadecimal. The last line uses the ASCII gap directly.
#include <stdio.h>
int main(void)
{
char letters[] = {'A', 'a', '0', ' '};
for (int i = 0; i < 4; i++) {
printf("char [%c] code %3d hex %02X\n",
letters[i], letters[i], letters[i]);
}
printf("'A' to 'a' is %d\n", 'a' - 'A');
return 0;
}
gcc -Wall -Wextra codes.c -o codes
./codes
char [A] code 65 hex 41
char [a] code 97 hex 61
char [0] code 48 hex 30
char [ ] code 32 hex 20
'A' to 'a' is 32
The codes are not guaranteed to be ASCII by the C standard, but every system this course supports uses it. Testing a letter against the range 65 to 90 works only under that assumption, which is why ctype.h exists.
Classifying characters
isalpha, isdigit, isspace and ispunct take a character code and return non-zero for a match. They are the readable replacement for range comparisons, and they follow the current locale. The example counts each class of characters in one sentence.
#include <ctype.h>
#include <stdio.h>
int main(void)
{
char text[] = "Hello, 42 world!";
int letters = 0;
int digits = 0;
int spaces = 0;
int punctuation = 0;
for (int i = 0; text[i] != '\0'; i++) {
unsigned char c = (unsigned char) text[i];
if (isalpha(c)) {
letters++;
} else if (isdigit(c)) {
digits++;
} else if (isspace(c)) {
spaces++;
} else if (ispunct(c)) {
punctuation++;
}
}
printf("letters: %d\n", letters);
printf("digits: %d\n", digits);
printf("spaces: %d\n", spaces);
printf("punctuation: %d\n", punctuation);
return 0;
}
gcc -Wall -Wextra classify.c -o classify
./classify
letters: 10
digits: 2
spaces: 2
punctuation: 2
The chain of tests is a switch on classes rather than on values, so the order only matters for the last catch-all. The explicit unsigned char is not decoration; the next two sections explain why.
Changing case
toupper and tolower return the converted code, or the input unchanged when it is not a letter of the right case. They return int, so the result is cast back before it is stored in a char. The program shows a full conversion and counts the bytes it changed.
#include <ctype.h>
#include <stdio.h>
int main(void)
{
char text[] = "Mixed Case 42";
int changed = 0;
for (int i = 0; text[i] != '\0'; i++) {
char upper = (char) toupper((unsigned char) text[i]);
if (upper != text[i]) {
changed++;
text[i] = upper;
}
}
printf("%s\n", text);
printf("changed: %d\n", changed);
return 0;
}
gcc -Wall -Wextra case.c -o case
./case
MIXED CASE 42
changed: 7
Seven letters changed: M, C and the five lower-case ones. The digits and the space were left alone by toupper itself, so no explicit test was needed before the assignment.
Pass unsigned char
The ctype functions accept an unsigned char value or EOF (-1) and nothing else. A plain char is signed on x86_64, so a byte above 127 arrives as a negative number, which is neither a character nor EOF. The cast makes the value 200 positive again, and the functions answer for that byte.
#include <ctype.h>
#include <stdio.h>
int main(void)
{
char raw = (char) 200;
printf("as int: %d\n", raw);
printf("alpha: %d\n", isalpha((unsigned char) raw) != 0);
printf("printable: %d\n", isprint((unsigned char) raw) != 0);
return 0;
}
gcc -Wall -Wextra highbyte.c -o highbyte
./highbyte
as int: -56
alpha: 0
printable: 0
Byte 200 is not a letter in the C locale and not printable, which is a correct answer. Without the cast the argument would be -56, and the library is allowed to do anything with a value outside its domain; in practice it reads a table before its start.
Narrowing and subscripts
Storing the result of toupper in a char without a cast is a narrowing conversion, and -Wconversion reports it. The value fits for every character this program handles, but the compiler cannot see that.
#include <ctype.h>
#include <stdio.h>
int main(void)
{
char upper = toupper('a');
printf("%c\n", upper);
return 0;
}
gcc -Wall -Wextra -Wconversion conv.c -o conv
conv.c: In function 'main':
conv.c:6:18: warning: conversion from 'int' to 'char' may change value [-Wconversion]
6 | char upper = toupper('a');
| ^~~~~~~
The second warning is about indexing with a plain char. A table of 256 counters is indexed by a byte, and a negative char would index before the array. GCC reports the signed type even when the current value is positive:
#include <stdio.h>
int main(void)
{
int counts[256] = {0};
char c = 'a';
counts[c]++;
printf("%d\n", counts['a']);
return 0;
}
gcc -Wall -Wextra subscript.c -o subscript
./subscript
subscript.c: In function 'main':
subscript.c:8:11: warning: array subscript has type 'char' [-Wchar-subscripts]
8 | counts[c]++;
| ^
1
The fix is to declare the index as unsigned char, which limits it to 0 to 255 and matches the table size. The same idea applies to every table indexed by a byte, including the classification tables behind ctype.h.
Exercises
- Print the codes of the digits from '0' to '9' and check that they are contiguous.
- Count the vowels of a string with tolower and a switch, then compare with the manual version from lesson 23.
- Convert every character of a string to upper case with toupper, then again with the ASCII arithmetic and the unsigned char cast, and compare the two results.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What type does the character constant
'A'have in C?Answer
intA character constant has type
int, which is whyprintfcan print it with%c,%dor%X. -
What do the functions in
ctype.htest?Answer
A character code and its class
isalpha,isdigit,isspaceand the rest classify a character code, and they follow the current locale. -
Why are
ctypearguments cast tounsigned char?Answer
To avoid a negative value outside the function domain
A plain
charis signed on x86_64, and a byte above 127 becomes negative; the ctype functions accept only unsigned-char values or EOF. -
What does
toupperreturn, and what must the caller do with it?Answer
An
intthat is cast back tochartoupperreturns anint, so the result is cast before it is stored in achar, or-Wconversionwarns. -
What is the numeric difference between
'a'and'A'on this system?Answer
32
The ASCII codes are 97 and 65, and their difference is 32.
-
What does this program print?
#include <ctype.h> #include <stdio.h> int main(void) { char text[] = "Hello, 42 world!"; int letters = 0, digits = 0, spaces = 0, punctuation = 0; for (int i = 0; text[i] != '\0'; i++) { unsigned char c = (unsigned char) text[i]; if (isalpha(c)) letters++; else if (isdigit(c)) digits++; else if (isspace(c)) spaces++; else if (ispunct(c)) punctuation++; } printf("letters: %d\ndigits: %d\nspaces: %d\npunctuation: %d\n", letters, digits, spaces, punctuation); return 0; }Answer
letters: 10 / digits: 2 / spaces: 2 / punctuation: 2
The sentence has ten letters, two digits, two spaces and two punctuation marks.
-
What does this program print?
#include <ctype.h> #include <stdio.h> int main(void) { char text[] = "Mixed Case 42"; int changed = 0; for (int i = 0; text[i] != '\0'; i++) { char upper = (char) toupper((unsigned char) text[i]); if (upper != text[i]) { changed++; text[i] = upper; } } printf("%s\nchanged: %d\n", text, changed); return 0; }Answer
MIXED CASE 42 / changed: 7
Seven letters change: the two already upper-case letters stay the same and the five lower-case ones become upper case.
-
What does this program print?
#include <stdio.h> int main(void) { char raw = (char) 200; printf("%d\n", raw); return 0; }Answer
-56
A plain
charis signed on this platform, so the byte 200 is interpreted as -56. -
This program is built with
gcc -std=c17 -Wall -Wextra -Wconversion conv.c -o conv. Which warning flag does GCC print?#include <ctype.h> #include <stdio.h> int main(void) { char upper = toupper('a'); printf("%c\n", upper); return 0; }Answer
-Wconversion
Storing the
intresult oftoupperin acharis a narrowing conversion, reported by-Wconversion. -
This program is built with
gcc -std=c17 -Wall -Wextra subscript.c -o subscript. What does GCC warn about?#include <stdio.h> int main(void) { int counts[256] = {0}; char c = 'a'; counts[c]++; printf("%d\n", counts['a']); return 0; }Answer
array subscript has type 'char' [-Wchar-subscripts]
A signed
charcan hold a negative value, and an index before the table is out of bounds.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev