67 Character classification — <ctype.h>
What to know first
charLooking back
Chapter 9 said a character is a number in a code table and an encoding is a way of writing that number in bytes. What, then, is a call like isalpha('가') asking?
A. It is the wrong question. The functions of <ctype.h> judge one byte. In UTF-8 “가” is three bytes, so it cannot even be passed as the argument, and if it were, each byte would be looked at separately. This header is a tool from the ASCII era.
So this chapter is short. The contract of twelve functions, and the rule broken most often in all of C — passing a char straight in is outside the contract — is all of it. The two large subjects waiting behind it, locales and multibyte characters, belong to the next five chapters.
The need for this chapter, and its context
char (chapter 27) there is no explaining why isalpha(c) is dangerous, and the fact that this judgement depends on the locale opens the door to the very next chapter. A small header joins two others.By the end of this chapter
EOF is mixed in among them, and the fact that even this judgement is changed by the locale.The questions this chapter answers
- If a program only ever sees ASCII, may the conversion be skipped?
67.1 The twelve functions at a glance
<ctype.h> fixes eleven predicates and two conversions. The naming rule is simple — is asks a true-or-false question, to changes something.
| Function | True for | In the “C” locale |
|---|---|---|
isalpha | Letters | A~Z, a~z |
isdigit | Digits | 0~9 — these ten, regardless of locale |
isalnum | Letters or digits | The two above together |
isspace | Whitespace | space, \t, \n, \v, \f, \r |
isblank | Whitespace within a line | space, \t |
isupper·islower | Upper- and lower-case | A~Z / a~z |
ispunct | Printing, and neither alphanumeric nor space | !, ,, \#, … |
isprint | Printing characters (space included) | 0x20~0x7E |
isgraph | Printing and not a space | 0x21~0x7E |
iscntrl | Control characters | 0x00~0x1F, 0x7F |
isxdigit | Hexadecimal digits | 0~9, A~F, a~f |
toupper·tolower | (conversion) to upper or lower case | Returned unchanged if it does not apply |
Table 68.1
isdigit is special. The standard nails the set of characters for which it is true to the ten from 0 to 9, regardless of the locale. That contrasts with the other predicates, which may widen. It is the one thing code that parses numbers can lean on.
67.2 The first trap — never pass a char straight in
examples-en/ch67/ctype.c
#include <stdio.h>
#include <ctype.h>
#include <limits.h>
#include <string.h>
/* The ctype functions take an int — but that int is not a char */
int main(void)
{
printf("CHAR_MIN = %d (char on this machine is %s)\n",
CHAR_MIN, CHAR_MIN < 0 ? "signed" : "unsigned");
/* the ASCII range gives no trouble */
printf("isalpha('A') = %d, isdigit('7') = %d, isspace(' ') = %d\n",
isalpha('A') != 0, isdigit('7') != 0, isspace(' ') != 0);
/* bytes of 128 and above are the trouble: passed as a char they go negative */
char bytes[] = { (char)0xC7, (char)0x41, 'A', '\0' }; /* the first byte of the Korean syllable 가 in CP949 */
printf("byte 0xC7 held in a char: %d\n", bytes[0]);
/* the right idiom: convert to unsigned char before passing */
printf("the right call: isalpha((unsigned char)b) = %d\n",
isalpha((unsigned char)bytes[0]) != 0);
/* toupper and tolower follow the same rule */
const char *s = "Hello, World!";
char up[32];
size_t i = 0;
for (; s[i] && i + 1 < sizeof up; i++)
up[i] = (char)toupper((unsigned char)s[i]);
up[i] = '\0';
printf("toupper applied: [%s]\n", up);
/* EOF is a valid argument too — which is why the parameter type is int */
printf("isalpha(EOF) = %d (EOF is an allowed argument)\n", isalpha(EOF) != 0);
return 0;
}
Output
CHAR_MIN = -128 (char on this machine is signed)
isalpha('A') = 1, isdigit('7') = 1, isspace(' ') = 1
byte 0xC7 held in a char: -57
the right call: isalpha((unsigned char)b) = 0
toupper applied: [HELLO, WORLD!]
isalpha(EOF) = 0 (EOF is an allowed argument)
Every function in <ctype.h> takes an int. And the value the standard requires of that argument is one representable as an unsigned char, or EOF.
The trouble is that char is signed on many implementations (chapter 27). As the example prints, the byte 0xC7 held in a char becomes −57, and passing that straight into isalpha passes a value that is not allowed — outside the contract. Real implementations are usually built as array lookups, so it becomes a read from before the start of an array.
There is one idiom.
isalpha((unsigned char)c)
toupper((unsigned char)c)Counter-example. Passing a char straight in
char *p = line;
while (*p) { if (isspace(*p)) ... ; p++; } /* outside the contract past 0x80 */Unless ASCII is guaranteed, always convert.
while (*p) { if (isspace((unsigned char)*p)) ... ; p++; }In a program that handles Korean, Japanese or European text, this one line is the difference between an accident and none.
Q. If a program only ever sees ASCII, may the conversion be skipped?
A. Programs in which “only ASCII arrives” actually holds are rarer than they look. A program mostly takes someone else’s input — file names, user names, pasted strings, and there is no way to stop Korean or an emoji from being among them. One command-line argument is enough to bring in a byte above 0x80.
And it costs nothing. An (unsigned char) conversion usually generates no instruction at all — one of the few places where staying inside the contract is free.
67.3 The second trap — EOF is mixed in
There is a reason the argument type is int and not char. EOF is a valid argument too. What fgetc returns must be passable straight in (chapter 63), and that value may be a byte or may be EOF.
int c;
while ((c = fgetc(f)) != EOF)
if (isalpha(c)) ... /* what fgetc gave is already unsigned char or EOF */Receive c as a char here and two things break at once — EOF becomes indistinguishable from the byte 0xFF, and the value can go negative and fall into the first trap. Chapter 63′s rule, “always receive what fgetc returns in an int”, comes back here.
67.4 The judgement depends on the locale
The set of bytes for which isalpha is true is not fixed. The current locale (precisely, the LC_CTYPE category) settles it. A program starts in the "C" locale, so at first the basis is ASCII — but it can change the moment setlocale is called.
And it is not only the predicates. toupper('i') becoming the dotted capital İ in a Turkish locale is the famous case, and in a locale whose decimal point is a comma even the output of printf("%f") changes.
The locale is a subject in its own right. By what rule its names are made, which standards fix them, and how much it changes are the next two chapters (chapters 68 and 69).
67.5 It means nothing for multibyte characters
One thing remains. The functions of this header look at one byte. “한” in UTF-8 is three bytes, so no byte of it means anything on its own — passing 0xED to isalpha asks “is this fragment a letter?”, and that question has no answer.
To judge a character made of several bytes, there are two roads. Convert to wide characters and use <wctype.h>‘s iswalpha (chapters 70 and 71 treat it), or work on the byte string as it is and do only the judgements you need yourself (chapter 72). This book recommends the latter, and chapter 72 says why.
Recap
| Situation | Rule | If you get it wrong |
|---|---|---|
Calling <ctype.h> | Convert with (unsigned char) | A negative argument — outside the contract |
Handling fgetc’s result | Receive it in an int | EOF confused with 0xFF |
isdigit | 0~9 regardless of locale | — |
| The other predicates | They depend on LC_CTYPE | Change the locale, change the answer |
| Multibyte characters | Per-byte judgement is meaningless | Judging a fragment of a letter |
Table 68.2
The judgement of a single byte is done. Now for the thing that governed it — the locale. The next chapter starts from what a locale is and by what rule a name like ko_KR.UTF-8 is made.