Projeto Analisador de Texto

Quantas palavras tem esse texto? Qual palavra eu mais repito? Neste projeto você vai escrever um programa que lê um arquivo de texto e responde a essas perguntas, em sete etapas. No caminho aparece uma surpresa que todo programador de língua portuguesa encontra um dia: para o computador, café tem 5 "letras". Você vai entender por quê, contar do jeito certo e conferir cada número com o wc, o contador de palavras do Linux.

Você vai praticar
  • Ler um arquivo linha por linha (arquivos e getline) e tratar o arquivo que não existe.
  • A diferença entre bytes e caracteres no UTF-8 (Acentos e UTF-8).
  • Separar palavras com std::istringstream e limpar a pontuação com <cctype>.
  • Contar cada palavra com std::map e ordenar pela contagem com std::sort e uma lambda.
  • Um relatório com std::format e um programa que recebe o nome do arquivo na linha de comando.

Todos os programas foram compilados e executados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp/texto, com o g++ 15.2 e o comando padrão do guia. O programa fica num arquivo só, analisar.cpp. Nas caixas Experimente, o texto está numa aba ao lado do código: você pode trocá-lo pelo seu.


O que vamos construir

$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
Análise de texto.txt

Linhas                   7
Palavras                65
Palavras diferentes     45
Caracteres             332
Bytes                  342

Palavra mais longa: paciência (9 letras)

Mais frequentes:
 1. a           6 ######
 2. um          4 ####
 3. ana         3 ###
 4. caderno     3 ###
 5. café        3 ###
  1. Abrir o arquivo e contar as linhas.
  2. Bytes × caracteres: o UTF-8.
  3. Contar as palavras.
  4. Contar cada palavra, sem pontuação e sem maiúsculas.
  5. As mais frequentes: ordenar pela contagem.
  6. A palavra mais longa.
  7. O relatório e o nome do arquivo na linha de comando.

Etapa 1 · Abrir o arquivo e contar as linhas

Primeiro, o texto. A Ana escreveu sobre um dia na lojinha, com acentos, vírgulas, pontos e até uma exclamação, tudo o que vai dar trabalho. Ela criou o arquivo com um here-document do Bash (no seu computador, use qualquer editor de texto e salve em UTF-8):

$ mkdir -p cpp/texto
$ cd cpp/texto
$ cat > texto.txt <<'FIM'
> A lojinha da Ana abre cedo, antes do sol.
> O café está quente, e a Ana conta os cadernos.
> Um caderno azul, um caderno verde, um caderno sem cor.
> Às dez horas chega a primeira cliente: a avó do Bruno.
> Ela quer lápis, borracha e paciência.
> A Ana não vende paciência, mas oferece um café.
> No fim do dia, a lojinha fecha. O café acabou!
> FIM

O programa abre o arquivo, lê uma linha por vez e numera:

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        std::cout << linhas << ": " << linha << '\n';
    }
    std::cout << "Linhas: " << linhas << '\n';
}
TrechoO que faz
std::ifstream arq{"texto.txt"};Abre o arquivo para leitura (input file stream, "fluxo de entrada de arquivo"). O nome é procurado na pasta em que o programa roda.
if (!arq)"Se o arquivo não abriu." Um ifstream vale false quando algo deu errado (arquivo que não existe, sem permissão…).
std::cerrA saída de erros (character error). Aparece no terminal como o cout, mas pode ser separada dele (por exemplo, 2> erros.txt).
return 1;Termina o main com código de saída 1: "deu errado". Scripts e o Bash olham esse número.
while (std::getline(arq, linha))Lê a próxima linha (sem o \n do fim) para linha. Quando o arquivo acaba, o getline "falha" e o laço termina.
$ cat texto.txt
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
1: A lojinha da Ana abre cedo, antes do sol.
2: O café está quente, e a Ana conta os cadernos.
3: Um caderno azul, um caderno verde, um caderno sem cor.
4: Às dez horas chega a primeira cliente: a avó do Bruno.
5: Ela quer lápis, borracha e paciência.
6: A Ana não vende paciência, mas oferece um café.
7: No fim do dia, a lojinha fecha. O café acabou!
Linhas: 7
Quando dá errado: o arquivo que não existe

Sem o if (!arq) e com o nome digitado errado ("testo.txt"), o programa não reclama de nada:

$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
Linhas: 0
$ echo $?
0

"0 linhas" e código de saída 0, "sucesso". Um ifstream que não abriu não lança exceção: ele só fica em estado de falha, e o primeiro getline já devolve "acabou". O programa mente sem saber. Com a verificação (e o mesmo nome errado):

$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
Não consegui abrir testo.txt
$ echo $?
1

Regra: depois de abrir um arquivo, confira sempre se abriu.

Missão (etapa 1): na aba texto.txt desta caixa há uma linha em branco entre a 3 e a 4. Conte também as linhas em branco (linha.empty() é true para uma linha vazia) e mostre no fim:

Linhas: 8
Linhas em branco: 1
#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        std::cout << linhas << ": " << linha << '\n';
    }
    std::cout << "Linhas: " << linhas << '\n';
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.

Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    int emBranco = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        if (linha.empty()) {
            ++emBranco;
        }
        std::cout << linhas << ": " << linha << '\n';
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Linhas em branco: " << emBranco << '\n';
}

Etapa 2 · Bytes × caracteres

Quantas letras tem a palavra café? Quatro. Pergunte ao C++ com std::string("café").size() e ele responde 5. Veja os bytes do arquivo com o od (octal dump, aqui em hexadecimal) e conte com o wc, que tem -c para bytes e -m para caracteres:

$ printf 'café' | od -An -tx1
 63 61 66 c3 a9
$ printf 'café' | wc -c
5
$ printf 'café' | wc -m
4

c, a e f ocupam um byte cada (63 61 66). O é ocupa dois: c3 a9. É a codificação UTF-8, a do Linux, da web e deste guia. Ela guarda cada caractere em 1 a 4 bytes, e o primeiro byte diz quantos vêm:

Bits do byteTeste em C++SignificaExemplo
0xxxxxxxc < 0x80Caractere de 1 byte (os do inglês, números, pontuação).a = 61
110xxxxx(c & 0xE0) == 0xC0Começo de um caractere de 2 bytes.é = c3 a9
1110xxxx(c & 0xF0) == 0xE0Começo de um caractere de 3 bytes.€ = e2 82 ac
11110xxx(c & 0xF8) == 0xF0Começo de um caractere de 4 bytes (emojis).😀 = f0 9f 98 80
10xxxxxx(c & 0xC0) == 0x80Continuação: o 2º, 3º ou 4º byte de um caractere.o a9 do é

Daí a receita: para contar caracteres, conte os bytes que não são de continuação. O & (E bit a bit) com 0xC0 (11000000) guarda só os dois primeiros bits do byte; se eles forem 10 (0x80), é continuação.

#include <fstream>
#include <iostream>
#include <string>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        std::cout << linhas << ": " << linha.size() << " bytes, "
                  << contarCaracteres(linha) << " caracteres\n";
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Caracteres: " << caracteres << '\n';
    std::cout << "Bytes: " << bytes << '\n';
}
TrechoO que faz
for (unsigned char c : texto)Percorre o texto byte a byte. unsigned char vê o byte como um número de 0 a 255; com char (que no Linux x86-64 tem sinal), o c3 viraria −61. Veja char.
(c & 0xC0) != 0x80"Não é byte de continuação": começa um caractere novo.
std::size_tO tipo sem sinal que o C++ usa para tamanhos (o mesmo de size()).
linha.size() + 1O getline tira o \n do fim de cada linha, mas ele ocupa um byte (e é um caractere) no arquivo. Somamos de volta.
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
1: 41 bytes, 41 caracteres
2: 48 bytes, 46 caracteres
3: 54 bytes, 54 caracteres
4: 56 bytes, 54 caracteres
5: 39 bytes, 37 caracteres
6: 50 bytes, 47 caracteres
7: 47 bytes, 46 caracteres
Linhas: 7
Caracteres: 332
Bytes: 342
$ wc -c texto.txt
342 texto.txt
$ wc -m texto.txt
332 texto.txt

Os números batem com o wc: 342 bytes e 332 caracteres. A linha 1 não tem acento (41 = 41); a linha 2 tem dois, café e está (48 bytes, 46 caracteres). A diferença total, 10, é o número de letras acentuadas do texto.

Caractere não é o mesmo que "letra que você vê"

Esta contagem é a de pontos de código, a mesma do wc -m. Na maioria dos textos em português ela coincide com as letras que você vê. Mas há casos em que não: um é pode ser gravado como e + um acento "solto" (dois pontos de código) e alguns emojis são vários pontos de código juntos. Para esses casos existem bibliotecas especializadas, como a ICU. Mais em Acentos e UTF-8.

Missão (etapa 2): a função contarCaracteres está "trapaceando": devolve o número de bytes, e o total de caracteres sai 342. Escreva-a de verdade, contando só os bytes que não são de continuação. A saída deve terminar com:

Caracteres: 332
Bytes: 342
#include <fstream>
#include <iostream>
#include <string>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    // Missão: conte só os bytes que NÃO são de continuação.
    return texto.size();
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        std::cout << linhas << ": " << linha.size() << " bytes, "
                  << contarCaracteres(linha) << " caracteres\n";
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Caracteres: " << caracteres << '\n';
    std::cout << "Bytes: " << bytes << '\n';
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <fstream>
#include <iostream>
#include <string>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        std::cout << linhas << ": " << linha.size() << " bytes, "
                  << contarCaracteres(linha) << " caracteres\n";
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Caracteres: " << caracteres << '\n';
    std::cout << "Bytes: " << bytes << '\n';
}

Etapa 3 · Contar as palavras

O que é uma palavra? Para começar, a mesma definição do wc: um pedaço de texto entre espaços. O C++ já sabe separar assim: o >> de um fluxo pula os espaços e lê até o próximo espaço. Para usar o >> numa std::string, transformamos a linha num fluxo com std::istringstream (input string stream, de <sstream>):

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        bytes += linha.size() + 1;
        caracteres += contarCaracteres(linha) + 1;

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
        }
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Palavras: " << palavras << '\n';
    std::cout << "Caracteres: " << caracteres << '\n';
    std::cout << "Bytes: " << bytes << '\n';
}
TrechoO que faz
std::istringstream fluxo{linha};Um "cin de mentira" que lê de dentro da linha.
while (fluxo >> palavra)Lê a próxima palavra; quando a linha acaba, o laço termina. Espaços repetidos e tabulações são pulados.
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
Linhas: 7
Palavras: 65
Caracteres: 332
Bytes: 342
$ wc texto.txt
  7  65 342 texto.txt

As colunas do wc são linhas, palavras e bytes: 7, 65, 342, iguais às nossas. Mas repare que, para o >>, cedo, (com a vírgula) é uma palavra. Para contar palavras, tanto faz. Para saber quais palavras se repetem, vai fazer diferença.

Missão (etapa 3): conte também as frases, contando os caracteres ., ! e ? de cada linha (um for (char c : linha)). Mostre Frases: 8 logo depois das palavras.

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        bytes += linha.size() + 1;
        caracteres += contarCaracteres(linha) + 1;

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
        }
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Palavras: " << palavras << '\n';
    std::cout << "Caracteres: " << caracteres << '\n';
    std::cout << "Bytes: " << bytes << '\n';
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0, frases = 0;
    std::string linha;
    while (std::getline(arq, linha)) {
        ++linhas;
        bytes += linha.size() + 1;
        caracteres += contarCaracteres(linha) + 1;

        for (char c : linha) {
            if (c == '.' || c == '!' || c == '?') {
                ++frases;
            }
        }

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
        }
    }
    std::cout << "Linhas: " << linhas << '\n';
    std::cout << "Palavras: " << palavras << '\n';
    std::cout << "Frases: " << frases << '\n';
    std::cout << "Caracteres: " << caracteres << '\n';
    std::cout << "Bytes: " << bytes << '\n';
}

Etapa 4 · Contar cada palavra

Para saber quantas vezes cada palavra aparece, um std::map da palavra para a contagem: std::map<std::string, int>. O truque é ++contagem[palavra]: se a palavra é nova, o [] a cria com 0 e o ++ faz 1; se já existe, soma 1. (Aqui o [] criar a chave é exatamente o que queremos, ao contrário do estoque.) Primeira tentativa, sem limpar nada:

#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++contagem[palavra];
        }
    }

    std::cout << contagem.size() << " palavras diferentes:\n";
    int i = 0;
    for (const auto& [palavra, vezes] : contagem) {
        std::cout << palavra << '=' << vezes << ' ';
        if (++i % 8 == 0) {
            std::cout << '\n';
        }
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
49 palavras diferentes:
A=2 Ana=3 Bruno.=1 Ela=1 No=1 O=2 Um=1 a=4 
abre=1 acabou!=1 antes=1 avó=1 azul,=1 borracha=1 caderno=3 cadernos.=1 
café=2 café.=1 cedo,=1 chega=1 cliente:=1 conta=1 cor.=1 da=1 
dez=1 dia,=1 do=3 e=2 está=1 fecha.=1 fim=1 horas=1 
lojinha=2 lápis,=1 mas=1 não=1 oferece=1 os=1 paciência,=1 paciência.=1 
primeira=1 quente,=1 quer=1 sem=1 sol.=1 um=3 vende=1 verde,=1 
Às=1

O map mostra tudo em ordem, e os problemas aparecem: A e a contados separados, café e café. também, assim como paciência, e paciência.. Precisamos limpar cada palavra antes de contar: tirar a pontuação e passar para minúsculas. As funções de <cctype> ajudam:

FunçãoPergunta ou fazExemplo
std::isalnum(c)É letra ou número? (alphanumeric)'a' sim, ',' não
std::isalpha(c)É letra?'7' não
std::ispunct(c)É pontuação?'!' sim
std::tolower(c)Devolve a minúscula.'A' → 'a'

Essas funções só entendem um byte por vez, e só os caracteres do inglês. Por isso a nossa limpar guarda as letras e números e todo byte de 0x80 para cima (os pedaços das letras acentuadas), sem tentar mexer neles:

#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
            }
        }
    }

    std::cout << contagem.size() << " palavras diferentes:\n";
    int i = 0;
    for (const auto& [palavra, vezes] : contagem) {
        std::cout << palavra << '=' << vezes << ' ';
        if (++i % 8 == 0) {
            std::cout << '\n';
        }
    }
    std::cout << '\n';
}
TrechoO que faz
std::isalnum(c) || c >= 0x80Fica: letra ou número do inglês, ou byte de caractere acentuado. Sai: vírgula, ponto, dois-pontos, exclamação.
static_cast<char>(std::tolower(c))tolower recebe e devolve int; convertemos de volta para char. Com bytes ≥ 0x80 ele não muda nada.
if (!limpa.empty())Um "!" ou um "-" sozinho vira texto vazio: não conta. (Um travessão "—" não: ele tem 3 bytes ≥ 0x80 e passaria. Fica para os desafios.)
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
45 palavras diferentes:
a=6 abre=1 acabou=1 ana=3 antes=1 avó=1 azul=1 borracha=1 
bruno=1 caderno=3 cadernos=1 café=3 cedo=1 chega=1 cliente=1 conta=1 
cor=1 da=1 dez=1 dia=1 do=3 e=2 ela=1 está=1 
fecha=1 fim=1 horas=1 lojinha=2 lápis=1 mas=1 no=1 não=1 
o=2 oferece=1 os=1 paciência=2 primeira=1 quente=1 quer=1 sem=1 
sol=1 um=4 vende=1 verde=1 Às=1

De 49 para 45 palavras diferentes: a=6, café=3, paciência=2. Duas coisas ficaram estranhas, e as duas são o UTF-8 de novo:

  • Às continua com maiúscula: o tolower não sabe que c3 80 é um "À". Se o texto tivesse Às e às, seriam contadas separadas (desafio no fim).
  • lápis veio depois de lojinha, e Às no fim de tudo. O map ordena comparando bytes: o á começa com c3, que é maior que qualquer letra sem acento.

Missão (etapa 4): a limpar está devolvendo a palavra sem mudar nada, e o programa mostra 49 palavras diferentes. Escreva-a: monte um texto novo só com os bytes que são std::isalnum ou >= 0x80, passando cada um por std::tolower. Devem aparecer 45 palavras diferentes e café=3.

#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    // Missão: monte um texto novo só com letras, números e bytes >= 0x80,
    // passando cada um por std::tolower.
    return palavra;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
            }
        }
    }

    std::cout << contagem.size() << " palavras diferentes:\n";
    int i = 0;
    for (const auto& [palavra, vezes] : contagem) {
        std::cout << palavra << '=' << vezes << ' ';
        if (++i % 8 == 0) {
            std::cout << '\n';
        }
    }
    std::cout << '\n';
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
            }
        }
    }

    std::cout << contagem.size() << " palavras diferentes:\n";
    int i = 0;
    for (const auto& [palavra, vezes] : contagem) {
        std::cout << palavra << '=' << vezes << ' ';
        if (++i % 8 == 0) {
            std::cout << '\n';
        }
    }
    std::cout << '\n';
}

Etapa 5 · As mais frequentes

O map está em ordem de palavra; queremos em ordem de contagem. Como no estoque, copiamos para um vetor e ordenamos. Cada item do map é um par (palavra, contagem), então o vetor é de std::pair<std::string, int> (pair). No lugar da listagem da etapa 4, no fim do main (com #include <algorithm>, <utility> e <vector>):

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        std::cout << i + 1 << ". " << ranking[i].first << ": " << ranking[i].second << '\n';
    }
}
TrechoO que faz
ranking(contagem.begin(), contagem.end())Cria o vetor já com uma cópia de todos os pares do map, de uma vez.
[](const auto& a, const auto& b)Uma lambda com auto nos parâmetros: o compilador descobre que são pares. Evita escrever std::pair<std::string, int> duas vezes.
a.second, a.firstA contagem e a palavra do par.
if (a.second != b.second) return a.second > b.second;Contagens diferentes: a maior vem antes.
return a.first < b.first;Empate: ordem alfabética. Sem esse desempate, palavras com a mesma contagem poderiam sair em qualquer ordem.
i < ranking.size() && i < 5Até 5 palavras, sem passar do fim num texto com menos de 5.
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
1. a: 6
2. um: 4
3. ana: 3
4. caderno: 3
5. café: 3

Num texto de verdade, o topo sempre é ocupado por palavras como "a", "o", "de", "que" (as stopwords, "palavras vazias"). Ignorá-las é um dos desafios.

Missão (etapa 5): em vez das 5 primeiras, mostre todas as palavras que aparecem 2 vezes ou mais. Só a condição do for muda (dica: ranking[i].second >= 2). A última linha deve ser:

10. paciência: 2
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
            }
        }
    }

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        std::cout << i + 1 << ". " << ranking[i].first << ": " << ranking[i].second << '\n';
    }
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
            }
        }
    }

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    for (std::size_t i = 0; i < ranking.size() && ranking[i].second >= 2; ++i) {
        std::cout << i + 1 << ". " << ranking[i].first << ": " << ranking[i].second << '\n';
    }
}

Etapa 6 · A palavra mais longa

Para achar a mais longa, guardamos a "campeã" até agora e comparamos cada palavra limpa com ela. A pergunta é: comparar o quê? size() conta bytes. Já temos a função certa da etapa 2, contarCaracteres (copie-a para este arquivo). As linhas novas:

    std::string maisLonga;              // começa vazia: 0 letras
    …
            if (!limpa.empty()) {
                ++contagem[limpa];
                if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                    maisLonga = limpa;
                }
            }
    …
    std::cout << "Palavra mais longa: " << maisLonga << " ("
              << contarCaracteres(maisLonga) << " letras)\n";
$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
Palavra mais longa: paciência (9 letras)
1. a: 6
2. um: 4
3. ana: 3
4. caderno: 3
5. café: 3
Quando dá errado: medir com size()

Troque as duas contarCaracteres(…) do teste e da frase por .size() (limpa.size() > maisLonga.size()). Compila sem aviso e mostra:

$ g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar
$ ./analisar
Palavra mais longa: paciência (10 letras)
1. a: 6
2. um: 4
3. ana: 3
4. caderno: 3
5. café: 3

"paciência" tem 9 letras, não 10: o ê ocupa 2 bytes. Aqui a palavra escolhida ainda foi a certa, por sorte. Num texto com "atenção" (7 letras, 9 bytes) e "primeiro" (8 letras, 8 bytes), o size() escolheria "atenção". Regra: em texto com acentos, size() responde "quantos bytes"; para "quantas letras", conte os caracteres.

Missão (etapa 6): ache também a palavra mais curta e mostre Palavra mais curta: a. Cuidado: a maisLonga pode começar vazia (0 letras) porque qualquer palavra é maior; a maisCurta não. Dica: troque quando maisCurta.empty() ou quando a palavra for menor.

#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string maisLonga;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
                if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                    maisLonga = limpa;
                }
            }
        }
    }

    std::cout << "Palavra mais longa: " << maisLonga << " ("
              << contarCaracteres(maisLonga) << " letras)\n";

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        std::cout << i + 1 << ". " << ranking[i].first << ": " << ranking[i].second << '\n';
    }
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main() {
    std::ifstream arq{"texto.txt"};
    if (!arq) {
        std::cerr << "Não consegui abrir texto.txt\n";
        return 1;
    }

    std::map<std::string, int> contagem;
    std::string maisLonga;
    std::string maisCurta;
    std::string linha;
    while (std::getline(arq, linha)) {
        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            std::string limpa = limpar(palavra);
            if (!limpa.empty()) {
                ++contagem[limpa];
                if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                    maisLonga = limpa;
                }
                if (maisCurta.empty() || contarCaracteres(limpa) < contarCaracteres(maisCurta)) {
                    maisCurta = limpa;
                }
            }
        }
    }

    std::cout << "Palavra mais longa: " << maisLonga << " ("
              << contarCaracteres(maisLonga) << " letras)\n";
    std::cout << "Palavra mais curta: " << maisCurta << '\n';

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        std::cout << i + 1 << ". " << ranking[i].first << ": " << ranking[i].second << '\n';
    }
}

Etapa 7 · O relatório

Falta juntar tudo num relatório bonito e deixar o programa analisar qualquer arquivo, não só o texto.txt. Quem diz qual é o usuário, na linha de comando: ./analisar outro.txt. O main passa a receber dois parâmetros:

int main(int argc, char* argv[]) {
    std::string nome = argc > 1 ? argv[1] : "texto.txt";
    std::ifstream arq;
    if (nome != "-") {                     // "-" quer dizer: ler do teclado
        arq.open(nome);
        if (!arq) {
            std::cerr << "Não consegui abrir " << nome << '\n';
            return 1;
        }
    }
    std::istream& entrada = (nome == "-") ? std::cin : arq;

    …
TrechoO que faz
int main(int argc, char* argv[])A forma do main que recebe a linha de comando. argc (argument count) é quantos pedaços ela tem; argv (argument vector) são os pedaços. Em ./analisar outro.txt: argc = 2, argv[0] = "./analisar", argv[1] = "outro.txt".
argc > 1 ? argv[1] : "texto.txt"Com argumento, usa ele; sem, o padrão (operador ternário).
arq.open(nome);Abre depois de criado. Só abrimos se o nome não for "-".
std::istream& entrada = … ? std::cin : arq;Uma referência para "de onde ler": o teclado (std::cin) ou o arquivo. Os dois são fluxos de entrada (std::istream), e o resto do programa usa entrada sem saber qual é. O - para "ler da entrada padrão" é um costume dos programas do Linux (o cat - faz o mesmo).

E o fim do main, com o relatório em colunas e uma barrinha para cada palavra frequente:

    …
    std::cout << std::format("Análise de {}\n\n", nome);
    std::cout << std::format("{:<20}{:>6}\n", "Linhas", linhas);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras", palavras);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras diferentes", contagem.size());
    std::cout << std::format("{:<20}{:>6}\n", "Caracteres", caracteres);
    std::cout << std::format("{:<20}{:>6}\n", "Bytes", bytes);
    std::cout << std::format("\nPalavra mais longa: {} ({} letras)\n", maisLonga,
                             contarCaracteres(maisLonga));

    std::cout << "\nMais frequentes:\n";
    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        const auto& [palavra, vezes] = ranking[i];
        std::cout << std::format("{:>2}. {:<10}{:>3} {}\n", i + 1, palavra, vezes,
                                 std::string(vezes, '#'));
    }
}
TrechoO que faz
"{:<20}{:>6}\n"Rótulo em 20 colunas à esquerda, número em 6 à direita: os números se alinham.
const auto& [palavra, vezes] = ranking[i];Desestrutura o par em dois nomes que dizem o que são (melhor que .first e .second).
std::string(vezes, '#')Um texto com vezes repetições de #: a barra do gráfico.

O resultado com o texto.txt está no começo da página. Com outro nome de arquivo, com um que não existe e lendo do teclado:

$ ./analisar texto.txt | head -3
Análise de texto.txt

Linhas                   7
$ ./analisar nada.txt
Não consegui abrir nada.txt
$ echo $?
1
$ echo "Oi, oi, OI! Tudo bem? Tudo." | ./analisar -
Análise de -

Linhas                   1
Palavras                 6
Palavras diferentes      3
Caracteres              28
Bytes                   28

Palavra mais longa: tudo (4 letras)

Mais frequentes:
 1. oi          3 ###
 2. tudo        2 ##
 3. bem         1 #

Com ./analisar - sozinho, o programa espera você digitar; termine com Ctrl+D numa linha vazia (o "fim da entrada") para ver o relatório.

Missão (etapa 7): a caixa roda ./analisar texto.txt. Acrescente ao relatório a média de letras por palavra: some contarCaracteres(limpa) de cada palavra numa variável letras e mostre letras / palavras com uma casa decimal ({:>6.1f}; converta para double antes de dividir, senão a divisão é inteira). A linha nova:

Letras por palavra     3.9
#include <algorithm>
#include <cctype>
#include <format>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main(int argc, char* argv[]) {
    std::string nome = argc > 1 ? argv[1] : "texto.txt";
    std::ifstream arq;
    if (nome != "-") {                     // "-" quer dizer: ler do teclado
        arq.open(nome);
        if (!arq) {
            std::cerr << "Não consegui abrir " << nome << '\n';
            return 1;
        }
    }
    std::istream& entrada = (nome == "-") ? std::cin : arq;

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0;
    std::map<std::string, int> contagem;
    std::string maisLonga;

    std::string linha;
    while (std::getline(entrada, linha)) {
        ++linhas;
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
            std::string limpa = limpar(palavra);
            if (limpa.empty()) {
                continue;
            }
            ++contagem[limpa];
            if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                maisLonga = limpa;
            }
        }
    }

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    std::cout << std::format("Análise de {}\n\n", nome);
    std::cout << std::format("{:<20}{:>6}\n", "Linhas", linhas);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras", palavras);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras diferentes", contagem.size());
    std::cout << std::format("{:<20}{:>6}\n", "Caracteres", caracteres);
    std::cout << std::format("{:<20}{:>6}\n", "Bytes", bytes);
    std::cout << std::format("\nPalavra mais longa: {} ({} letras)\n", maisLonga,
                             contarCaracteres(maisLonga));

    std::cout << "\nMais frequentes:\n";
    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        const auto& [palavra, vezes] = ranking[i];
        std::cout << std::format("{:>2}. {:<10}{:>3} {}\n", i + 1, palavra, vezes,
                                 std::string(vezes, '#'));
    }
}
A lojinha da Ana abre cedo, antes do sol.
O café está quente, e a Ana conta os cadernos.
Um caderno azul, um caderno verde, um caderno sem cor.
Às dez horas chega a primeira cliente: a avó do Bruno.
Ela quer lápis, borracha e paciência.
A Ana não vende paciência, mas oferece um café.
No fim do dia, a lojinha fecha. O café acabou!
#include <algorithm>
#include <cctype>
#include <format>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main(int argc, char* argv[]) {
    std::string nome = argc > 1 ? argv[1] : "texto.txt";
    std::ifstream arq;
    if (nome != "-") {                     // "-" quer dizer: ler do teclado
        arq.open(nome);
        if (!arq) {
            std::cerr << "Não consegui abrir " << nome << '\n';
            return 1;
        }
    }
    std::istream& entrada = (nome == "-") ? std::cin : arq;

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0;
    std::map<std::string, int> contagem;
    std::string maisLonga;
    std::size_t letras = 0;

    std::string linha;
    while (std::getline(entrada, linha)) {
        ++linhas;
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
            std::string limpa = limpar(palavra);
            if (limpa.empty()) {
                continue;
            }
            ++contagem[limpa];
            letras += contarCaracteres(limpa);
            if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                maisLonga = limpa;
            }
        }
    }

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    std::cout << std::format("Análise de {}\n\n", nome);
    std::cout << std::format("{:<20}{:>6}\n", "Linhas", linhas);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras", palavras);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras diferentes", contagem.size());
    std::cout << std::format("{:<20}{:>6}\n", "Caracteres", caracteres);
    std::cout << std::format("{:<20}{:>6}\n", "Bytes", bytes);
    std::cout << std::format("{:<20}{:>6.1f}\n", "Letras por palavra",
                             static_cast<double>(letras) / palavras);
    std::cout << std::format("\nPalavra mais longa: {} ({} letras)\n", maisLonga,
                             contarCaracteres(maisLonga));

    std::cout << "\nMais frequentes:\n";
    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        const auto& [palavra, vezes] = ranking[i];
        std::cout << std::format("{:>2}. {:<10}{:>3} {}\n", i + 1, palavra, vezes,
                                 std::string(vezes, '#'));
    }
}

Experimente: esta caixa roda ./analisar - e digita duas linhas para você. Troque o texto da entrada pelo seu (um parágrafo de um livro, uma letra de música) e veja o relatório.

#include <algorithm>
#include <cctype>
#include <format>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main(int argc, char* argv[]) {
    std::string nome = argc > 1 ? argv[1] : "texto.txt";
    std::ifstream arq;
    if (nome != "-") {                     // "-" quer dizer: ler do teclado
        arq.open(nome);
        if (!arq) {
            std::cerr << "Não consegui abrir " << nome << '\n';
            return 1;
        }
    }
    std::istream& entrada = (nome == "-") ? std::cin : arq;

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0;
    std::map<std::string, int> contagem;
    std::string maisLonga;

    std::string linha;
    while (std::getline(entrada, linha)) {
        ++linhas;
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
            std::string limpa = limpar(palavra);
            if (limpa.empty()) {
                continue;
            }
            ++contagem[limpa];
            if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                maisLonga = limpa;
            }
        }
    }

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    std::cout << std::format("Análise de {}\n\n", nome);
    std::cout << std::format("{:<20}{:>6}\n", "Linhas", linhas);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras", palavras);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras diferentes", contagem.size());
    std::cout << std::format("{:<20}{:>6}\n", "Caracteres", caracteres);
    std::cout << std::format("{:<20}{:>6}\n", "Bytes", bytes);
    std::cout << std::format("\nPalavra mais longa: {} ({} letras)\n", maisLonga,
                             contarCaracteres(maisLonga));

    std::cout << "\nMais frequentes:\n";
    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        const auto& [palavra, vezes] = ranking[i];
        std::cout << std::format("{:>2}. {:<10}{:>3} {}\n", i + 1, palavra, vezes,
                                 std::string(vezes, '#'));
    }
}

O código completo

Compile com g++ -std=c++23 -Wall -Wextra analisar.cpp -o analisar e rode com ./analisar, ./analisar arquivo.txt ou ./analisar -.

#include <algorithm>
#include <cctype>
#include <format>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

// Quantos caracteres (não bytes) um texto em UTF-8 tem.
std::size_t contarCaracteres(const std::string& texto) {
    std::size_t n = 0;
    for (unsigned char c : texto) {
        if ((c & 0xC0) != 0x80) {     // não é byte de continuação
            ++n;
        }
    }
    return n;
}

// Tira a pontuação e passa para minúsculas (só as letras sem acento).
std::string limpar(const std::string& palavra) {
    std::string limpa;
    for (unsigned char c : palavra) {
        if (std::isalnum(c) || c >= 0x80) {
            limpa += static_cast<char>(std::tolower(c));
        }
    }
    return limpa;
}

int main(int argc, char* argv[]) {
    std::string nome = argc > 1 ? argv[1] : "texto.txt";
    std::ifstream arq;
    if (nome != "-") {                     // "-" quer dizer: ler do teclado
        arq.open(nome);
        if (!arq) {
            std::cerr << "Não consegui abrir " << nome << '\n';
            return 1;
        }
    }
    std::istream& entrada = (nome == "-") ? std::cin : arq;

    int linhas = 0;
    std::size_t bytes = 0, caracteres = 0, palavras = 0;
    std::map<std::string, int> contagem;
    std::string maisLonga;

    std::string linha;
    while (std::getline(entrada, linha)) {
        ++linhas;
        bytes += linha.size() + 1;                    // +1: o '\n' que o getline tirou
        caracteres += contarCaracteres(linha) + 1;

        std::istringstream fluxo{linha};
        std::string palavra;
        while (fluxo >> palavra) {
            ++palavras;
            std::string limpa = limpar(palavra);
            if (limpa.empty()) {
                continue;
            }
            ++contagem[limpa];
            if (contarCaracteres(limpa) > contarCaracteres(maisLonga)) {
                maisLonga = limpa;
            }
        }
    }

    std::vector<std::pair<std::string, int>> ranking(contagem.begin(), contagem.end());
    std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) {
            return a.second > b.second;               // mais vezes primeiro
        }
        return a.first < b.first;                     // empate: ordem alfabética
    });

    std::cout << std::format("Análise de {}\n\n", nome);
    std::cout << std::format("{:<20}{:>6}\n", "Linhas", linhas);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras", palavras);
    std::cout << std::format("{:<20}{:>6}\n", "Palavras diferentes", contagem.size());
    std::cout << std::format("{:<20}{:>6}\n", "Caracteres", caracteres);
    std::cout << std::format("{:<20}{:>6}\n", "Bytes", bytes);
    std::cout << std::format("\nPalavra mais longa: {} ({} letras)\n", maisLonga,
                             contarCaracteres(maisLonga));

    std::cout << "\nMais frequentes:\n";
    for (std::size_t i = 0; i < ranking.size() && i < 5; ++i) {
        const auto& [palavra, vezes] = ranking[i];
        std::cout << std::format("{:>2}. {:<10}{:>3} {}\n", i + 1, palavra, vezes,
                                 std::string(vezes, '#'));
    }
}

Desafios extras

  1. Maiúsculas acentuadas. Faça Às virar às. Dica: em UTF-8, as maiúsculas acentuadas do português são c3 80 a c3 9d, e a minúscula é o segundo byte + 0x20 (À = c3 80, à = c3 a0).
  2. Palavras vazias. Ignore "a", "o", "e", "de", "do", "da", "um" no ranking, guardando-as num std::set.
  3. Palavras com hífen. "guarda-chuva" vira "guardachuva". Mantenha o hífen quando ele estiver entre letras.
  4. Arquivos do Windows. Linhas terminadas em \r\n deixam um \r no fim de cada linha lida pelo getline, e os bytes contados mudam. Detecte e remova.
  5. Vários arquivos. ./analisar a.txt b.txt c.txt: um relatório para cada um (um laço em argv) e um total no fim, como o wc faz.
  6. Ordem alfabética de gente. Faça "lápis" vir antes de "lojinha" comparando as palavras sem acento (troque á por a só para comparar).

Resumo

  • std::ifstream arq{nome}; + if (!arq): um arquivo que não abriu não lança exceção, só fica em falha. Confira sempre; avise em std::cerr e saia com return 1.
  • while (std::getline(arq, linha)) lê linha por linha, sem o \n.
  • Em UTF-8, um caractere tem de 1 a 4 bytes. size() conta bytes; para contar caracteres, conte os bytes com (c & 0xC0) != 0x80, usando unsigned char. Confira com wc -c e wc -m.
  • std::istringstream + >> separa palavras por espaços, como o wc -w; a pontuação vem grudada.
  • <cctype> (isalnum, tolower) só entende os caracteres do inglês: preserve os bytes ≥ 0x80.
  • ++contagem[palavra] num std::map<std::string, int> conta ocorrências; para ordenar por contagem, copie para um vector de pair e use std::sort com uma lambda que desempata.
  • int main(int argc, char* argv[]) recebe a linha de comando; std::istream& deixa o mesmo código ler de arquivo ou do teclado.