Projeto Contador de Palavras

A Ana escreve os textos da vitrine da lojinha e das postagens nas redes sociais, e às vezes precisa saber quantas palavras ou letras eles têm. O Linux já tem um programa para isso, o wc (word count, contagem de palavras). Neste projeto você vai escrever o seu próprio wc em C, do zero, e conferir cada versão contra o wc de verdade. No caminho, vai entender como um programa lê um texto letra por letra e por que "café" tem 4 letras e 5 bytes.

Você vai praticar
  • Ler a entrada um caractere por vez com getchar até o EOF, e por que a variável tem de ser int.
  • Mandar um arquivo para a entrada do programa com < e digitar até o Ctrl+D.
  • Contar palavras com uma variável de estado ("estou dentro de uma palavra?") e isspace (ctype.h).
  • Abrir os arquivos que vêm na linha de comando (argc e argv), juntar as contagens numa struct e mostrar o total.
  • Separar bytes, caracteres e letras no UTF-8 (Acentos e UTF-8) com uma máscara de bits.

O que vamos construir

O contador pronto, na máquina da Ana, com dois textos dela. Logo abaixo, o wc do Linux com as mesmas colunas: os números batem.

$ ./contar poema.txt lista.txt
 Linhas Palavras  Letras  Caract.   Bytes  Arquivo
      5       24      93      123     126  poema.txt
      3        5      23       30      33  lista.txt
      8       29     116      153     159  total
$ wc -lwmc poema.txt lista.txt
  5  24 123 126 poema.txt
  3   5  30  33 lista.txt
  8  29 153 159 total

O wc -lwmc mostra linhas (-l), palavras (-w), caracteres (-m) e bytes (-c). O wc não conta letras; o nosso conta.

  1. Linhas e caracteres com getchar.
  2. Palavras: dentro ou fora.
  3. Arquivos pelo nome e o total.
  4. Letras, acentos e bytes.

Antes de começar

Você precisa ter visto while, Caracteres (char), if e else e, para a versão 3, Argumentos da Linha de Comando e Arquivos (fopen e fclose). A Ana criou a pasta e dois textos de teste com o editor de textos dela. O poema.txt tem uma linha em branco e acentos; a lista.txt tem espaços duplos de propósito:

$ mkdir -p ~/c/texto
$ cd ~/c/texto
$ cat poema.txt
A lojinha da Ana abre às oito.
Tem caderno, caneta e borracha,
e um gato que dorme no balcão.

Quem chega cedo ganha café.
$ cat lista.txt
arroz
feijão  e  macarrão
pão

Versão 1 · Linhas e caracteres com getchar

Pense numa caixa de supermercado que conta moedas: ela pega uma por uma, até o pote ficar vazio. O programa faz igual com o texto: pega um caractere com getchar ("get character", pegar um caractere), soma 1 no contador e repete, até a entrada acabar. Cada vez que o caractere é o Enter ('\n'), uma linha terminou.

#include <stdio.h>

int main(void) {
    long linhas = 0;
    long caracteres = 0;
    int c;

    while ((c = getchar()) != EOF) {
        caracteres++;
        if (c == '\n') {
            linhas++;
        }
    }

    printf("Linhas:     %ld\n", linhas);
    printf("Caracteres: %ld\n", caracteres);
    return 0;
}
LinhaO que faz
long linhas = 0;Os contadores começam em zero. São long porque um texto pode ser grande (no gcc do laboratório, um long vai até 9 quintilhões; no editor do guia, até 2 bilhões, o que também sobra).
int c;Guarda o caractere lido. É int, e não char, de propósito: o getchar devolve um byte (de 0 a 255) ou o valor especial EOF, e precisa de um tipo que caiba os 257 valores (veja o cenário abaixo).
while ((c = getchar()) != EOF)Três coisas numa linha: lê um caractere, guarda em c e compara com EOF ("end of file", fim do arquivo). Os parênteses em volta de c = getchar() são obrigatórios: sem eles, o != seria feito primeiro (aula Precedência).
caracteres++;Todo caractere conta, inclusive espaços e Enters.
if (c == '\n') linhas++;Uma linha termina no Enter. Contar linhas é contar '\n'.

Há dois jeitos de dar um texto para o programa. O primeiro é mandar um arquivo para a entrada com <: o terminal abre o arquivo e o getchar lê dele, como se alguém digitasse muito rápido (aula stdin, stdout e stderr). O wc confirma as contas:

$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar < poema.txt
Linhas:     5
Caracteres: 126
$ wc -l -c poema.txt
  5 126 poema.txt

O segundo é digitar. O programa espera; você escreve as linhas e, no fim, aperta Ctrl+D numa linha vazia, que no terminal do Linux quer dizer "a entrada acabou". Aí o getchar devolve EOF e o laço termina:

$ ./contar
Olá, mundo!
Tchau.                          « depois do Enter, a Ana apertou Ctrl+D
Linhas:     2
Caracteres: 20

20 caracteres? Olá, mundo! tem 11 e Tchau. tem 6, mais os dois Enters: 19. O que sobrou é o á, que ocupa 2 bytes no UTF-8, e o getchar lê bytes. Por enquanto, o que chamamos de "caracteres" é na verdade "bytes", como o wc -c. A versão 4 conserta o nome e a conta.

A última linha sem Enter não conta

Se o arquivo não termina com Enter, a última linha não tem '\n', e nem o nosso programa nem o wc a contam:

$ printf 'um\ndois\ntrês' > sem-enter.txt
$ cat sem-enter.txt
um
dois
três$ ./contar < sem-enter.txt                « o $ grudou: não há Enter depois de "três"
Linhas:     2
Caracteres: 13
$ wc -l sem-enter.txt
2 sem-enter.txt

Não é defeito: no Linux, uma "linha" é por definição um texto que termina em '\n', e o wc -l conta os '\n'. Por isso os editores de código põem um Enter no fim do arquivo. Contar também a última linha incompleta é um dos desafios.

char c: o programa para no meio do arquivo

A Ana escreveu char c; em vez de int c;. Compilou sem nenhum aviso, e com o poema funcionou. Depois ela testou com um arquivo que não é texto, o próprio programa ls do Linux:

$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar < poema.txt
Linhas:     5
Caracteres: 126
$ ./contar < /usr/bin/ls
Linhas:     3
Caracteres: 8288
$ wc -c /usr/bin/ls
150568 /usr/bin/ls
$ xxd -s 8284 -l 8 /usr/bin/ls
0000205c: 0000 0000 ffd2 0100                      ........

O que aconteceu: o programa parou depois de 8288 bytes, de um arquivo de 150568. O xxd mostra o byte seguinte, o de posição 8288 (0x2060): é ff, o 255. O EOF vale -1, e um char com sinal (o padrão no gcc e no editor) só guarda de -128 a 127: o byte 255 vira -1 ao entrar em c, e o programa acha que o arquivo acabou. Num texto em português o byte 255 não aparece (no UTF-8 ele nunca é usado), por isso o erro passou despercebido com o poema.

Como consertar: int c;. Com int, o byte 255 é 255 e o EOF é -1: dois valores diferentes. Vale para getchar, getc e fgetc, que devolvem int pelo mesmo motivo (aula Caracteres).

Missão (versão 1): o contador conta os caracteres, mas as linhas saem 0. No lugar do comentário, conte uma linha cada vez que c for o Enter ('\n'). A entrada da missão é o poema da Ana: devem sair Linhas: 5 e Caracteres: 126.

#include <stdio.h>

int main(void) {
    long linhas = 0;
    long caracteres = 0;
    int c;

    while ((c = getchar()) != EOF) {
        caracteres++;
        // se c for o Enter ('\n'), conte mais uma linha
    }

    printf("Linhas:     %ld\n", linhas);
    printf("Caracteres: %ld\n", caracteres);
    return 0;
}
#include <stdio.h>

int main(void) {
    long linhas = 0;
    long caracteres = 0;
    int c;

    while ((c = getchar()) != EOF) {
        caracteres++;
        if (c == '\n') {
            linhas++;
        }
    }

    printf("Linhas:     %ld\n", linhas);
    printf("Caracteres: %ld\n", caracteres);
    return 0;
}

Versão 2 · Palavras: dentro ou fora

Contar palavras parece fácil: "conte os espaços". Mas entre duas palavras pode haver dois espaços, um Tab ou uma linha em branco. Um jeito que nunca erra: imagine o porteiro de uma loja, que conta quem entra. Ele não conta os passos de cada cliente, só o momento em que alguém passa da rua para dentro. O programa guarda numa variável se está dentro de uma palavra ou fora (num espaço); cada vez que passa de fora para dentro, começou uma palavra nova.

texto:    feijão  e  macarrão
estado:   DDDDDDFFDFFDDDDDDDD          « D = dentro, F = fora
          ↑       ↑  ↑
          1       2  3                 « conta só quando passa de F para D
#include <stdio.h>
#include <ctype.h>

int main(void) {
    long linhas = 0;
    long palavras = 0;
    long caracteres = 0;
    bool dentro = false;    // estou no meio de uma palavra?
    int c;

    while ((c = getchar()) != EOF) {
        caracteres++;
        if (c == '\n') {
            linhas++;
        }
        if (isspace(c)) {
            dentro = false;         // espaço, Tab ou Enter: a palavra acabou
        } else if (!dentro) {
            dentro = true;          // primeira letra de uma palavra nova
            palavras++;
        }
    }

    printf("Linhas:     %ld\n", linhas);
    printf("Palavras:   %ld\n", palavras);
    printf("Caracteres: %ld\n", caracteres);
    return 0;
}
#include <ctype.h>
A biblioteca que classifica caracteres (aula ctype.h).
bool dentro = false;
O estado: começa fora de palavra. No C23, bool, true e false já vêm na linguagem C23; antes era preciso #include <stdbool.h> (aula Booleanos).
isspace(c)
Verdadeiro para os "espaços em branco": o espaço, o Tab ('\t'), o Enter ('\n') e alguns outros ('\r', '\v', '\f'). O wc separa as palavras do mesmo jeito.
dentro = false;
Achou um espaço: a palavra acabou (ou já estava fora, e continua fora).
else if (!dentro)
Não é espaço e estava fora: é a primeira letra de uma palavra. Marca dentro = true e conta. As letras seguintes da mesma palavra caem no else if com dentro já verdadeiro, e não contam de novo.
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar < poema.txt
Linhas:     5
Palavras:   24
Caracteres: 126
$ wc poema.txt
  5  24 126 poema.txt
$ ./contar < lista.txt
Linhas:     3
Palavras:   5
Caracteres: 33
$ wc lista.txt
 3  5 33 lista.txt
$ printf 'um\tdois   três\n\n\n  quatro\n' | ./contar
Linhas:     4
Palavras:   4
Caracteres: 27

Sem argumentos, o wc mostra as três contagens nesta ordem: linhas, palavras e bytes. Ele também ajusta a largura das colunas ao tamanho dos arquivos; o que importa são os números. O último teste, com o printf … | (o | manda a saída de um comando para a entrada do outro), tem um Tab, três espaços seguidos, duas linhas em branco e espaços no começo da linha: são 4 palavras, e o programa não se confunde. Repare também que "caderno," é uma palavra: para o wc e para nós, palavra é tudo o que fica entre espaços, pontuação inclusa.

Uma palavra a mais no poema e duas a mais na lista

A primeira ideia da Ana foi "toda palavra termina num espaço ou num Enter; basta contar os dois":

        if (c == ' ' || c == '\n') {
            palavras++;     // toda palavra termina num espaço ou num Enter
        }
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar < poema.txt
Linhas:     5
Palavras:   25                « o wc diz 24
Caracteres: 126
$ ./contar < lista.txt
Linhas:     3
Palavras:   7                 « o wc diz 5
Caracteres: 33

O que aconteceu: a linha em branco do poema é um Enter sem palavra antes, e contou como palavra. Na lista, cada espaço duplo de feijão e macarrão conta duas vezes. O programa contou separadores, e o número de separadores só é igual ao de palavras quando o texto é "bem comportado". Um Tab no lugar do espaço ainda grudaria duas palavras numa.

Como consertar: conte as passagens de fora para dentro de uma palavra, com a variável dentro, e use isspace para reconhecer todos os tipos de espaço.

Missão (versão 2): as palavras saem 0. No lugar dos dois comentários, escreva o if com isspace(c) e o else if (!dentro) que conta uma palavra nova. A entrada da missão é a lista da Ana, com os espaços duplos: devem sair Palavras: 5 e Linhas: 3.

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

int main(void) {
    long linhas = 0;
    long palavras = 0;
    long caracteres = 0;
    bool dentro = false;    // estou no meio de uma palavra?
    int c;

    while ((c = getchar()) != EOF) {
        caracteres++;
        if (c == '\n') {
            linhas++;
        }
        // se c for espaço (isspace), a palavra acabou: dentro = false
        // senão, se ainda não estava dentro, começou uma palavra nova
    }

    printf("Linhas:     %ld\n", linhas);
    printf("Palavras:   %ld\n", palavras);
    printf("Caracteres: %ld\n", caracteres);
    return 0;
}
#include <stdio.h>
#include <ctype.h>

int main(void) {
    long linhas = 0;
    long palavras = 0;
    long caracteres = 0;
    bool dentro = false;    // estou no meio de uma palavra?
    int c;

    while ((c = getchar()) != EOF) {
        caracteres++;
        if (c == '\n') {
            linhas++;
        }
        if (isspace(c)) {
            dentro = false;         // espaço, Tab ou Enter: a palavra acabou
        } else if (!dentro) {
            dentro = true;          // primeira letra de uma palavra nova
            palavras++;
        }
    }

    printf("Linhas:     %ld\n", linhas);
    printf("Palavras:   %ld\n", palavras);
    printf("Caracteres: %ld\n", caracteres);
    return 0;
}

Versão 3 · Arquivos pelo nome e o total

O wc de verdade recebe os nomes dos arquivos na linha de comando (wc poema.txt lista.txt), mostra uma linha para cada um e, se forem vários, o total. Para isso, a versão 3 tem três novidades:

  • a contagem vira uma função, contar(FILE *f), que lê de qualquer arquivo aberto, inclusive do teclado;
  • as três contagens viajam juntas numa struct, Contagem;
  • o main recebe os nomes em argv e abre um arquivo por vez.
#include <stdio.h>
#include <ctype.h>

typedef struct {
    long linhas;
    long palavras;
    long caracteres;
} Contagem;

// Conta tudo o que der para ler de f, até o fim do arquivo.
Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.caracteres++;
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld  %s\n", t.linhas, t.palavras, t.caracteres, nome);
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Caract.  Arquivo\n");

    if (argc == 1) {                    // sem nomes: lê o teclado (stdin)
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;                   // pula para o próximo arquivo
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        total.linhas += t.linhas;
        total.palavras += t.palavras;
        total.caracteres += t.caracteres;
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}
LinhaO que faz
typedef struct { … } Contagem;As três contagens de um texto numa ficha só (aula Structs). A função devolve a ficha inteira de uma vez.
Contagem t = {0};Todos os campos começam em zero.
getc(f)O mesmo que o getchar, mas do arquivo f. Na verdade, getchar() é o mesmo que getc(stdin).
printf("%7ld %8ld %7ld %s\n", …)Cada número numa coluna de largura fixa, alinhado à direita, embaixo do título (aula Formatos do printf).
int main(int argc, char *argv[])argc é quantas palavras há na linha de comando, contando o próprio programa; argv[1], argv[2]… são os nomes dos arquivos (aula Argumentos da Linha de Comando).
if (argc == 1) mostrar(contar(stdin), "-");Sem nomes, lê a entrada padrão (stdin), que também é um FILE *, como o wc faz. O "-" no lugar do nome é a convenção do Linux para "entrada padrão".
perror(argv[i]); erros++; continue;O arquivo não abriu: mostra o motivo (aula errno e perror), anota o erro e pula para o próximo nome. Um arquivo faltando não impede de contar os outros.
fclose(f);Fecha cada arquivo assim que termina de contar. Um programa que conta mil arquivos sem fechar nenhum esgota o limite de arquivos abertos do sistema.
total.linhas += t.linhas;Acumula a contagem de cada arquivo no total.
if (argc > 2)Só mostra o total quando há mais de um arquivo, como o wc.
return erros > 0 ? 1 : 0;Código de saída 1 se algum arquivo falhou, como o wc (aula Operador Ternário). Um script que use o contador fica sabendo que algo deu errado.
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt
 Linhas Palavras  Caract.  Arquivo
      5       24     126  poema.txt
$ ./contar poema.txt lista.txt
 Linhas Palavras  Caract.  Arquivo
      5       24     126  poema.txt
      3        5      33  lista.txt
      8       29     159  total
$ wc poema.txt lista.txt
  5  24 126 poema.txt
  3   5  33 lista.txt
  8  29 159 total
$ ./contar < lista.txt
 Linhas Palavras  Caract.  Arquivo
      3        5      33  -

E com um arquivo que não existe no meio? Os dois avisam, contam os outros e saem com o código 1:

$ ./contar poema.txt nao-existe.txt lista.txt; echo $?
 Linhas Palavras  Caract.  Arquivo
      5       24     126  poema.txt
nao-existe.txt: No such file or directory
      3        5      33  lista.txt
      8       29     159  total
1
$ wc poema.txt nao-existe.txt lista.txt; echo $?
  5  24 126 poema.txt
wc: nao-existe.txt: Arquivo ou diretório inexistente
  3   5  33 lista.txt
  8  29 159 total
1

A única diferença é a língua do erro. O wc chama setlocale ao começar e usa o idioma do sistema da Ana, o português; o nosso programa não chama, e o perror fala inglês, o padrão do C. O editor do guia também mostra No such file or directory.

Falha de segmentação quando falta um arquivo

Numa primeira versão, a Ana esqueceu o continue depois do perror. O gcc não avisou nada:

$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt nao-existe.txt lista.txt
 Linhas Palavras  Caract.  Arquivo
      5       24     126  poema.txt
nao-existe.txt: No such file or directory
Falha de segmentação     (imagem do núcleo gravada) ./contar poema.txt nao-existe.txt lista.txt
$ echo $?
139

O que aconteceu: o perror mostrou o erro, mas o programa seguiu em frente e chamou contar(f) com f valendo NULL. O getc(NULL) tentou ler da memória do endereço zero, e o sistema matou o programa (código 139; veja Ponteiro Nulo). A lista.txt nem chegou a ser contada.

Como consertar: depois de tratar o erro, saia do caminho normal: continue (pula para o próximo arquivo, dentro de um laço) ou return. Conferir o NULL só adianta se o programa fizer alguma coisa diferente quando ele aparece.

Missão (versão 3): o programa conta os dois arquivos das abas, mas a linha total sai zerada. No lugar do comentário, some t em total, campo por campo (total.linhas += t.linhas; e os outros dois). O total deve ser 8 29 159.

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

typedef struct {
    long linhas;
    long palavras;
    long caracteres;
} Contagem;

// Conta tudo o que der para ler de f, até o fim do arquivo.
Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.caracteres++;
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld  %s\n", t.linhas, t.palavras, t.caracteres, nome);
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Caract.  Arquivo\n");

    if (argc == 1) {                    // sem nomes: lê o teclado (stdin)
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;                   // pula para o próximo arquivo
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        // some t em total, campo por campo
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}
A lojinha da Ana abre às oito.
Tem caderno, caneta e borracha,
e um gato que dorme no balcão.

Quem chega cedo ganha café.

arroz
feijão  e  macarrão
pão

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

typedef struct {
    long linhas;
    long palavras;
    long caracteres;
} Contagem;

// Conta tudo o que der para ler de f, até o fim do arquivo.
Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.caracteres++;
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld  %s\n", t.linhas, t.palavras, t.caracteres, nome);
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Caract.  Arquivo\n");

    if (argc == 1) {                    // sem nomes: lê o teclado (stdin)
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;                   // pula para o próximo arquivo
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        total.linhas += t.linhas;
        total.palavras += t.palavras;
        total.caracteres += t.caracteres;
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}

Versão 4 · Letras, acentos e bytes

Na versão 1 ficou uma pendência: Olá, mundo! deu um "caractere" a mais. É que o getc lê bytes, e no UTF-8, a codificação de texto do Linux e da internet, as letras acentuadas ocupam 2 bytes (aula Acentos e UTF-8). Veja os bytes de "café":

$ echo -n 'café' | xxd
00000000: 6361 66c3 a9                             caf..
$ echo -n 'café' | wc -m
4
$ echo -n 'café' | LANG=C wc -m
5

São 5 bytes: 63 (c), 61 (a), 66 (f) e o par c3 a9, que juntos formam o é. O wc -m conta 4 caracteres, porque sabe que o sistema da Ana está em UTF-8 (pt_BR.UTF-8); com LANG=C, ele passa a contar bytes e diz 5. A versão 4 separa as três ideias:

Contagem"café"O que contaIgual a
bytes5cada byte do arquivowc -c
caracteres4cada símbolo que aparece na tela, inclusive espaços e pontuaçãowc -m
letras4só as letras, com ou sem acento(o wc não tem)

Como achar o começo de cada caractere

O UTF-8 foi desenhado para isso ser fácil. Os bytes do meio de um caractere (os que continuam um caractere já começado, como o a9 do é) sempre começam com os bits 10. Nenhum outro byte começa assim:

ByteEm binárioO que é
63 (c)0110 0011começa com 0: um caractere inteiro de 1 byte (ASCII)
c31100 0011começa com 11: o primeiro byte de um caractere de 2 bytes ou mais
a91010 1001começa com 10: continuação; não é um caractere novo

Para olhar só os dois primeiros bits, a máscara 0xC0 (1100 0000) apaga os outros seis (aula Máscaras e Flags): c & 0xC0 dá 0x80 (1000 0000) exatamente para os bytes de continuação. Então: caracteres = bytes que não são continuação.

E as letras? As do inglês, o isalpha reconhece. As acentuadas do português (á à â ã é ê í ó ô õ ú ç e as maiúsculas) começam todas pelo byte 0xC3 no UTF-8. Contamos uma letra para cada 0xC3, e a continuação dele não conta de novo.

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

typedef struct {
    long linhas;
    long palavras;
    long letras;
    long caracteres;    // como o wc -m: "ã" conta uma vez
    long bytes;         // como o wc -c: "ã" conta duas
} Contagem;

// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
    return (c & 0xC0) == 0x80;
}

// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
    return isalpha(c) || c == 0xC3;
}

Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.bytes++;
        if (!continua_caractere(c)) {
            t.caracteres++;
        }
        if (comeca_letra(c)) {
            t.letras++;
        }
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld %8ld %7ld  %s\n", t.linhas, t.palavras,
           t.letras, t.caracteres, t.bytes, nome);
}

void somar(Contagem *total, Contagem t) {
    total->linhas += t.linhas;
    total->palavras += t.palavras;
    total->letras += t.letras;
    total->caracteres += t.caracteres;
    total->bytes += t.bytes;
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Letras  Caract.   Bytes  Arquivo\n");

    if (argc == 1) {
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        somar(&total, t);
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}
long letras; long caracteres; long bytes;
A struct ganhou dois campos; o antigo caracteres, que contava bytes, agora se chama bytes.
bool continua_caractere(int c)
(c & 0xC0) == 0x80: verdadeiro só para os bytes 10xxxxxx. Com uma função de nome claro, o if lá embaixo se lê como uma frase: "se não continua um caractere, conte um caractere".
bool comeca_letra(int c)
Uma letra sem acento (isalpha, que no padrão do C só reconhece A a Z e a a z) ou o primeiro byte de uma letra acentuada (0xC3).
void somar(Contagem *total, Contagem t)
Com cinco campos, as somas do main viraram uma função. Ela recebe o endereço do total para poder mudá-lo, e usa -> para chegar nos campos (aula Structs e Ponteiros).
$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt lista.txt
 Linhas Palavras  Letras  Caract.   Bytes  Arquivo
      5       24      93      123     126  poema.txt
      3        5      23       30      33  lista.txt
      8       29     116      153     159  total
$ wc -lwmc poema.txt lista.txt
  5  24 123 126 poema.txt
  3   5  30  33 lista.txt
  8  29 153 159 total

O poema tem 3 letras acentuadas (às, balcão, café): 126 bytes − 3 = 123 caracteres, como o wc -m. A lista tem 3 também (feijão, macarrão, pão). E as letras? O wc não conta, mas o grep sabe separar letras: grep -o '[[:alpha:]]' mostra cada letra numa linha, e o wc -l conta as linhas.

$ grep -o '[[:alpha:]]' poema.txt | wc -l
93
O limite do 0xC3

O teste c == 0xC3 cobre as letras acentuadas do português, do espanhol e do francês. Letras de outras línguas (o ł do polonês, o grego, o russo) começam por outros bytes e não seriam contadas; e o 0xC3 também começa dois símbolos que não são letras, o × e o ÷. Para um contador que entenda qualquer língua, a saída é a biblioteca de caracteres "largos" do C (<wchar.h> e <wctype.h>, com setlocale), que fica para depois.

As letras acentuadas contam dobrado

A primeira ideia da Ana para as letras acentuadas foi "todo byte acima de 127 é parte de uma letra": return isalpha(c) || c >= 0x80;.

$ gcc -std=c23 -Wall -Wextra contar.c -o contar
$ ./contar poema.txt lista.txt
 Linhas Palavras  Letras  Caract.   Bytes  Arquivo
      5       24      96      123     126  poema.txt          « eram 93
      3        5      26       30      33  lista.txt          « eram 23
      8       29     122      153     159  total

O que aconteceu: cada letra acentuada tem dois bytes acima de 127 (c3 e a continuação), e as duas contaram. O poema e a lista têm 3 acentos cada um: 3 a mais em cada.

Como consertar: conte só o primeiro byte da letra (0xC3), nunca a continuação. A mesma ideia vale para os caracteres: o que conta é o começo, e não cada pedaço.

Missão (versão 4): a coluna Caract. está igual à coluna Bytes, porque o programa conta todo byte como caractere. Use a função continua_caractere para contar só os bytes que começam um caractere. O poema deve dar 123 caracteres e a lista 30, como o wc -m.

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

typedef struct {
    long linhas;
    long palavras;
    long letras;
    long caracteres;    // como o wc -m: "ã" conta uma vez
    long bytes;         // como o wc -c: "ã" conta duas
} Contagem;

// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
    return (c & 0xC0) == 0x80;
}

// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
    return isalpha(c) || c == 0xC3;
}

Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.bytes++;
        t.caracteres++;     // conta todo byte: está igual a t.bytes
        if (comeca_letra(c)) {
            t.letras++;
        }
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld %8ld %7ld  %s\n", t.linhas, t.palavras,
           t.letras, t.caracteres, t.bytes, nome);
}

void somar(Contagem *total, Contagem t) {
    total->linhas += t.linhas;
    total->palavras += t.palavras;
    total->letras += t.letras;
    total->caracteres += t.caracteres;
    total->bytes += t.bytes;
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Letras  Caract.   Bytes  Arquivo\n");

    if (argc == 1) {
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        somar(&total, t);
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}
A lojinha da Ana abre às oito.
Tem caderno, caneta e borracha,
e um gato que dorme no balcão.

Quem chega cedo ganha café.

arroz
feijão  e  macarrão
pão

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

typedef struct {
    long linhas;
    long palavras;
    long letras;
    long caracteres;    // como o wc -m: "ã" conta uma vez
    long bytes;         // como o wc -c: "ã" conta duas
} Contagem;

// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
    return (c & 0xC0) == 0x80;
}

// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
    return isalpha(c) || c == 0xC3;
}

Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.bytes++;
        if (!continua_caractere(c)) {
            t.caracteres++;
        }
        if (comeca_letra(c)) {
            t.letras++;
        }
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld %8ld %7ld  %s\n", t.linhas, t.palavras,
           t.letras, t.caracteres, t.bytes, nome);
}

void somar(Contagem *total, Contagem t) {
    total->linhas += t.linhas;
    total->palavras += t.palavras;
    total->letras += t.letras;
    total->caracteres += t.caracteres;
    total->bytes += t.bytes;
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Letras  Caract.   Bytes  Arquivo\n");

    if (argc == 1) {
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        somar(&total, t);
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}

Agora é a sua vez: a caixa abaixo é o contador pronto, sem arquivo. Clique em Rodar, digite um texto seu (com acentos), aperte Enter no fim de cada linha e, para terminar, Ctrl+D. Confira as contas à mão com uma frase curta.

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

typedef struct {
    long linhas;
    long palavras;
    long letras;
    long caracteres;    // como o wc -m: "ã" conta uma vez
    long bytes;         // como o wc -c: "ã" conta duas
} Contagem;

// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
    return (c & 0xC0) == 0x80;
}

// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
    return isalpha(c) || c == 0xC3;
}

Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.bytes++;
        if (!continua_caractere(c)) {
            t.caracteres++;
        }
        if (comeca_letra(c)) {
            t.letras++;
        }
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld %8ld %7ld  %s\n", t.linhas, t.palavras,
           t.letras, t.caracteres, t.bytes, nome);
}

void somar(Contagem *total, Contagem t) {
    total->linhas += t.linhas;
    total->palavras += t.palavras;
    total->letras += t.letras;
    total->caracteres += t.caracteres;
    total->bytes += t.bytes;
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Letras  Caract.   Bytes  Arquivo\n");

    if (argc == 1) {
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        somar(&total, t);
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}

O programa completo

A versão final, igual à versão 4. Compile com gcc -std=c23 -Wall -Wextra contar.c -o contar:

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

typedef struct {
    long linhas;
    long palavras;
    long letras;
    long caracteres;    // como o wc -m: "ã" conta uma vez
    long bytes;         // como o wc -c: "ã" conta duas
} Contagem;

// No UTF-8, os bytes de 0x80 a 0xBF (10xxxxxx) continuam uma letra
// que começou antes. Todos os outros começam um caractere novo.
bool continua_caractere(int c) {
    return (c & 0xC0) == 0x80;
}

// As letras acentuadas do português (á, ã, ç, é, ô, ú...) começam
// todas pelo byte 0xC3 no UTF-8.
bool comeca_letra(int c) {
    return isalpha(c) || c == 0xC3;
}

Contagem contar(FILE *f) {
    Contagem t = {0};
    bool dentro = false;
    int c;
    while ((c = getc(f)) != EOF) {
        t.bytes++;
        if (!continua_caractere(c)) {
            t.caracteres++;
        }
        if (comeca_letra(c)) {
            t.letras++;
        }
        if (c == '\n') {
            t.linhas++;
        }
        if (isspace(c)) {
            dentro = false;
        } else if (!dentro) {
            dentro = true;
            t.palavras++;
        }
    }
    return t;
}

void mostrar(Contagem t, const char *nome) {
    printf("%7ld %8ld %7ld %8ld %7ld  %s\n", t.linhas, t.palavras,
           t.letras, t.caracteres, t.bytes, nome);
}

void somar(Contagem *total, Contagem t) {
    total->linhas += t.linhas;
    total->palavras += t.palavras;
    total->letras += t.letras;
    total->caracteres += t.caracteres;
    total->bytes += t.bytes;
}

int main(int argc, char *argv[]) {
    printf(" Linhas Palavras  Letras  Caract.   Bytes  Arquivo\n");

    if (argc == 1) {
        mostrar(contar(stdin), "-");
        return 0;
    }

    Contagem total = {0};
    int erros = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            perror(argv[i]);
            erros++;
            continue;
        }
        Contagem t = contar(f);
        fclose(f);
        mostrar(t, argv[i]);
        somar(&total, t);
    }
    if (argc > 2) {
        mostrar(total, "total");
    }
    return erros > 0 ? 1 : 0;
}

O que o projeto juntou: while com getchar e EOF, isspace e isalpha, uma variável de estado bool, structs, argc e argv, fopen e fclose, perror, máscaras de bits e o UTF-8.

Rodando no laboratório

A Ana conferiu uma frase digitada, com quatro letras acentuadas, e olhou os bytes com o xxd:

$ ./contar
 Linhas Palavras  Letras  Caract.   Bytes  Arquivo
Ação e reação.                  « Enter e depois Ctrl+D
      1        3      11       15      19  -
$ echo 'Ação e reação.' | xxd
00000000: 41c3 a7c3 a36f 2065 2072 6561 c3a7 c3a3  A....o e rea....
00000010: 6f2e 0a                                  o..

11 letras (Ação 4, e 1, reação 6), 15 caracteres (as letras, 2 espaços, o ponto e o Enter) e 19 bytes, porque ç (c3 a7) e ã (c3 a3) aparecem duas vezes cada um, com 2 bytes.


Desafios extras

  1. Escolher as colunas. Como o wc, aceite as opções -l, -w e -c antes dos nomes dos arquivos e mostre só as colunas pedidas (um argv[i][0] == '-' indica uma opção).
  2. A última linha sem Enter. Conte também a última linha quando o arquivo não termina em '\n'. Dica: guarde o último caractere lido; no fim, se ele não for '\n' e o arquivo não estiver vazio, some 1.
  3. A linha mais comprida. Mostre o tamanho da maior linha, em caracteres, como o wc -L.
  4. Palavras sem pontuação. Faça "caderno," e "caderno" serem iguais: trate também ispunct(c) como separador. Quantas palavras dá o poema agora? E "guarda-chuva"?
  5. Frequência das letras. Com um array long freq[26], conte quantas vezes aparece cada letra de A a Z (sem diferenciar maiúscula de minúscula, com tolower) e mostre a mais comum.

Exercícios

1. Por que a variável que recebe o getchar() deve ser int, e não char?

2. Quantas palavras o contador da versão 2 acha em " bom dia " (com os espaços)?

3. O arquivo oi.txt tem só o texto pão e um Enter. Quantos bytes e quantos caracteres ele tem?

4. No UTF-8, quais bytes continuam um caractere começado antes?

5. Complete o laço que lê a entrada até o fim:

while ((c = getchar()) != )

6. Complete o comando que manda o arquivo poema.txt para a entrada do contador:

./contar poema.txt

7. Complete a chamada que lê um caractere do arquivo f, e não do teclado:

c = (f);

Resumo

  • getchar e getc leem um byte por vez e devolvem int: um byte de 0 a 255 ou EOF. Com char c, o byte 255 vira o fim do arquivo.
  • No terminal, a entrada acaba com Ctrl+D; com < arquivo, o programa lê o arquivo como se fosse o teclado.
  • Linhas = número de '\n'; uma última linha sem Enter não conta, nem no wc.
  • Palavras: conte as passagens de fora para dentro de uma palavra, com uma variável de estado e isspace. Contar espaços erra com espaços duplos e linhas em branco.
  • Um FILE * serve para arquivos e para o stdin; depois de um perror, saia do caminho normal (continue ou return).
  • No UTF-8, bytes ≠ caracteres: os bytes 10xxxxxx continuam um caractere, e (c & 0xC0) != 0x80 acha os começos.
  • Compare sempre com uma ferramenta de confiança: o wc mostrou cada erro na hora.

Próximo projeto: Lista de Tarefas.