Proven C Book한국어 GitHub

67 Character classification — <ctype.h>

What to know first

chapter 9, Characters and text · letters and encodings
chapter 27, Integers · the signedness of char

Looking 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

A small header for judging one byte, but there is a clear reason for the slot: without the signedness of 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

The functions that judge a single byte. The twelve at a glance, the exact range of values allowed as the argument, why EOF is mixed in among them, and the fact that even this judgement is changed by the locale.

The questions this chapter answers

  1. 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.

FunctionTrue forIn the “C” locale
isalphaLettersA~Z, a~z
isdigitDigits0~9these ten, regardless of locale
isalnumLetters or digitsThe two above together
isspaceWhitespacespace, \t, \n, \v, \f, \r
isblankWhitespace within a linespace, \t
isupper·islowerUpper- and lower-caseA~Z / a~z
ispunctPrinting, and neither alphanumeric nor space!, ,, \#, …
isprintPrinting characters (space included)0x20~0x7E
isgraphPrinting and not a space0x21~0x7E
iscntrlControl characters0x00~0x1F, 0x7F
isxdigitHexadecimal digits0~9, A~F, a~f
toupper·tolower(conversion) to upper or lower caseReturned 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

SituationRuleIf you get it wrong
Calling <ctype.h>Convert with (unsigned char)A negative argument — outside the contract
Handling fgetc’s resultReceive it in an intEOF confused with 0xFF
isdigit0~9 regardless of locale
The other predicatesThey depend on LC_CTYPEChange the locale, change the answer
Multibyte charactersPer-byte judgement is meaninglessJudging 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.