Continuando o Capítulo 1
Até este ponto, a maioria dos programas apresentados pelo K&R processava a entrada diretamente caractere por caractere. Em muitos casos, bastava analisar o caractere atual e manter pequenas quantidades de contexto, como um contador ou um estado. A partir daqui, os exercícios começam a exigir algo diferente: armazenar dados temporariamente para que possam ser analisados ou transformados posteriormente.
Caso não tenha lido a parte 1 desta série: K&R (C) — Capítulo 1 pt. 1
Quando terminei o capítulo, quis sentar e escrever sobre tudo que tinha aprendido. Acabei descobrindo que meu cérebro tinha outros planos:

Arrays em C
Na sequência, o K&R apresenta o uso da estrutura de dados array (também conhecida como vetor), que consiste em uma sequência de elementos armazenados de forma contínua na memória. Em C, arrays possuem tamanho fixo, portanto a quantidade de elementos que podem armazenar precisa ser definida antecipadamente.
O primeiro exemplo é um programa cujo objetivo é percorrer a entrada caractere por caractere e contar quantas vezes cada dígito (0 a 9) aparece, além da quantidade de espaços em branco e de outros caracteres (i.e., letras e símbolos).
Considere o input abc12345 9021 — o output será:
digits = 1 2 1 1 1 1 0 0 0 1
white space = 1
other = 3
A primeira sequência representa a quantidade de ocorrências de cada dígito. O primeiro valor corresponde ao número de vezes que o caractere 0 apareceu, o segundo ao dígito 1, o terceiro ao dígito 2 e assim sucessivamente até o 9.
O código apresentado pelo K&R para resolver esse problema é:
#include <stdio.h>
/* count digits, white space, others */
main()
{
int c, i, nwhite, nother;
int ndigit[10];
nwhite = nother = 0;
for (i = 0; i < 10; ++i)
ndigit[i] = 0;
while ((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++ndigit[c-'0'];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;
printf("digits =");
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
printf(", white space = %d, other = %d\n",
nwhite, nother);
}
O array é declarado com a quantidade de elementos que irá armazenar: 10 posições (int ndigit[10]), correspondentes aos dígitos de 0 a 9. Em seguida, um loop for zera todos os elementos do array, pois ele será utilizado como um contador da frequência dos dígitos.
Além disso, neste exemplo tem um detalhe sutil muito importante que conecta na prática com as seções anteriores deste post. Me refiro especificamente as linhas:
if (c >= '0' && c <= '9')
++ndigit[c-'0'];
Note que o programa está comparando c com '0' e '9', como se fossem caracteres e não números. Isso pode parecer confuso à primeira vista, mas faz sentido quando lembramos que getchar() lê caracteres da entrada.
Embora getchar() retorne um int, o valor retornado representa o código ASCII do caractere digitado. Portanto, se o usuário digitar um número de 0 a 9, o valor armazenado em c será um número entre 48 e 57:
'0' = 48
'1' = 49
'2' = 50
'3' = 51
'4' = 52
'5' = 53
'6' = 54
'7' = 55
'8' = 56
'9' = 57
Por isso, a condição: c >= '0' && c <= '9' é apenas uma forma mais legível de verificar se o valor retornado por getchar() está entre 48 e 57, ou seja, se o caractere lido representa um dígito.
Mas surge outro problema: o array possui apenas 10 posições (0 a 9). Seria estranho desperdiçar dezenas de posições apenas porque os códigos ASCII dos dígitos começam em 48.
A solução apresentada pelo K&R é extremamente elegante. Como os caracteres '0' até '9' ocupam posições sequenciais na tabela ASCII, basta subtrair '0' do valor recebido: ++ndigit[c - '0'];
Imagine que o usuário digitou '5'. O valor armazenado em c será 53. Então: 53 - 48 = 5 — na prática, o código se torna: ++ndigit[5];
Ou seja, a subtração converte o código ASCII do caractere para o índice correto do array. Dessa forma, cada dígito cai exatamente na posição que representa seu valor numérico.
Para mostrar os elementos do array é necessário percorrer cada posição individualmente. Por isso, no exemplo do livro temos este loop for, que imprime os valores armazenados em cada índice do array:
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
Mas por qual motivo precisamos fazer isso? Não existe uma forma de imprimir o array inteiro como em Python, por exemplo: print(ndigit)
A resposta conecta com a seção anterior: em Python, uma lista é um objeto que carrega informações sobre si mesma durante a execução, como quantidade de elementos e sua estrutura interna. Podemos imaginar algo parecido com:
list object
├── tamanho: 3
├── capacidade: 4
└── elementos
├── 1
├── 2
└── 3
Em C, essa estrutura de informações não existe junto dos dados. Um array é uma sequência contínua de valores na memória, sem metadados indicando automaticamente quantos elementos existem ou como esses dados devem ser interpretados.
Por isso, o programador precisa percorrer cada elemento manualmente e também precisa saber previamente o tamanho do array.
Existe uma exceção: enquanto o compilador ainda conhece o array original, é possível calcular seu tamanho usando sizeof. Porém, quando um array é passado como argumento para uma função, ele é convertido em um ponteiro para o primeiro elemento, e essa informação sobre a quantidade de elementos é perdida.
Exercício 1-13. Write a program to print a histogram of the lengths of words in its input
O enunciado pede que o programa leia uma entrada e crie um histograma mostrando quantas palavras possuem cada tamanho. Por exemplo, se a entrada contém quatro palavras: hi, hello, world e map, o programa deve identificar que existe uma palavra de tamanho 2, duas de tamanho 5 e uma de tamanho 3, armazenando essas frequências para depois exibir no histograma.
O desafio está no fato de que o programa não recebe as palavras prontas; ele recebe apenas uma sequência de caracteres e precisa descobrir onde cada palavra começa, termina e qual é o seu tamanho.
Esse exercício é uma combinação dos conceitos já apresentados — e diferente dos exemplos anteriores, o programa não está apenas analisando o caractere atual; ele precisa construir uma informação que não existe diretamente na entrada: o tamanho de cada palavra. Enquanto recebe caracteres, o programa mantém um estado que representa se ele está dentro de uma palavra e um contador acumulando seu tamanho. Quando encontra um separador, esse contexto permite saber que uma palavra terminou e atualizar o índice correspondente no array.
Solução
/*
Exercise 1-13. Write a program to print a histogram of the lengths of words in its input. It is
easy to draw the histogram with the bars horizontal; a vertical orientation is more challenging.
*/
#include <stdio.h>
#define IN 1 // inside word
#define OUT 0 // outside word
#define MAX_WORD_LEN 10 // max length of the input
int main () {
// c: current character. i and j: loop counters. nc: current word length
int c, i, j, nc, state;
nc = i = j = 0;
int histogram[MAX_WORD_LEN];
// initialize array elements to 0
for (i = 0; i < MAX_WORD_LEN; i++)
histogram[i] = 0;
state = OUT;
while ((c = getchar()) != EOF) {
if (c != '\n' && c != '\t' && c != ' ') {
state = IN;
nc++;
}
if (c == '\n' || c == '\t' || c == ' ') {
if (state == IN) {
if (nc > MAX_WORD_LEN) {
printf("max word length = %d\n", MAX_WORD_LEN);
}
else
histogram[nc - 1]++; // word length 1 maps to index 0
}
state = OUT;
nc = 0; // reset nc when outside word
}
}
// handle a word that ends directly at EOF
if (state == IN) {
if (nc > MAX_WORD_LEN) {
printf("max word length = %d\n", MAX_WORD_LEN);
}
else {
printf("\n");
histogram[nc - 1]++;
}
}
// loops for printing the histogram
for (i = 0; i < MAX_WORD_LEN; i++) {
printf("%3d | ", i + 1);
// print '*' repeatedly because C has no string repetition operator
for (j = 0; j < histogram[i]; j++)
printf("*");
printf("\n");
}
return 0;
}
Conceitos aplicados neste exercício:
- Processamento de streams de caracteres.
- Máquina de estados (IN / OUT).
- Estado como representação do contexto atual.
- Contagem e acumulação de informações.
- Arrays como tabelas de frequência.
- Mapeamento de dados para índices de array.
Strings e o terminador \0
Se você se perguntou por que strings em C estão dentro da seção de arrays, é uma boa pergunta.
O motivo é que, na prática, não existe um tipo especial chamado string em C. Strings são construídas utilizando um array de char.
Se conectarmos isso com o fato de que arrays não carregam automaticamente seu tamanho durante a execução, surge uma pergunta natural: "como C sabe quando uma string termina?"
A resposta é o null terminator, o caractere \0. Diferente do EOF, que é um valor especial retornado pelas funções de entrada para indicar o fim de um fluxo, o \0 é realmente armazenado dentro do array como o marcador de fim da string.
Isso significa que uma string de 5 letras ocupa 6 posições no array, pois o \0 também ocupa um espaço.
char word[] = "hello"; — na memória: h e l l o \0.
Note que o compilador adiciona esse terminador automaticamente.
Internamente, funções da biblioteca padrão de C, como strlen(), fazem algo equivalente a:
while (str[i] != '\0')
i++;
Ou seja, elas percorrem o array até encontrar o marcador que indica o fim da string. Inclusive, alguns exercícios no capítulo 1 pedem exatamente a implementação desse tipo de algoritmo.
Exercício 1-19. Write a function reverse(s) that reverses the character string s. Use it to write a program that reverses its input a line at a time.
O exercício pede para criar uma função que receba uma string e inverta a ordem dos seus caracteres. Depois, essa função deve ser utilizada em um programa que leia a entrada linha por linha e imprima cada linha invertida. A ideia não é apenas percorrer os caracteres, mas entender como manipular uma string em C, lembrando que ela é um array de char terminado pelo caractere \0. O programa precisa identificar o tamanho da string, acessar seus elementos e reorganizá-los na ordem inversa, transformando, por exemplo, uma entrada como hello em olleh.
Solução
/*
Exercise 1-19. Write a function reverse(s) that reverses the character string s. Use it to
write a program that reverses its input a line at a time.
*/
/* Since Chapter 1 does not introduce dynamically allocated buffers,
* input longer than the array size is truncated.
*/
#include <stdio.h>
#define ARRAY_SIZE 1000
int get_line(char s[], int lim);
void reverse(char s[]);
int main() {
char line[ARRAY_SIZE];
int len;
while ((len = get_line(line, ARRAY_SIZE)) > 0) {
reverse(line);
if (line[0] != '\0')
printf("%s\n", line);
}
return 0;
}
int get_line(char s[], int lim) {
int c, i, j;
j = 0;
for (i = 0; (c = getchar()) != EOF && c != '\n'; i++) {
if (j < lim - 1) {
s[j] = c;
j++;
}
}
if (c == '\n') {
if (j < lim - 1) {
s[j] = c;
j++;
}
i++;
}
if (j < lim)
s[j] = '\0';
else
s[lim - 1] = '\0';
return i;
}
void reverse(char s[]) {
int i, j, tmp;
i = j = 0;
while (s[j] != '\0')
j++;
j--;
while (j >= 0 && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n')) {
j--;
}
s[j + 1] = '\0';
for (i = 0; i < j ; i++, j--) {
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}
Conceitos aplicados neste exercício:
- Strings em C como arrays de char.
- Percorrer strings até o terminador \0.
- Manipulação de arrays por índice.
- Funções e separação de responsabilidades.
- Modificação de dados através de parâmetros.
- Algoritmos de troca (swap).
- Processamento de entrada linha por linha.
Alguns pontos deste exercício valem a pena serem analisados.
Resumindo as funções e suas responsabilidades:
mainorquestra;get_linecaptura dados;reversetransforma dados.
main
A função main é o ponto de entrada do programa e seu papel é simples: declarar as variáveis e coordenar a execução das outras funções.
Na condição do while, o valor retornado por get_line é atribuído à variável len: while ((len = get_line(line, ARRAY_SIZE)) > 0).
Apesar de len não ser utilizado dentro do corpo do loop (nem na função reverse, explicado o porque posteriormente), ele é necessário para capturar o retorno da função e permitir que o programa verifique se uma linha foi realmente lida. Enquanto get_line retornar um valor maior que zero, o processamento continua.
No corpo do while, a linha capturada é passada para reverse, que modifica o conteúdo do próprio array. Isso é possível porque, ao passar um array como argumento para uma função, C não cria uma cópia dos seus elementos; o parâmetro é convertido em um ponteiro para o primeiro elemento do array, permitindo que a função altere diretamente os dados armazenados na memória original.
Em seguida, o programa verifica se o primeiro elemento da string é \0. Caso seja, significa que a string está vazia e não existe conteúdo para imprimir. Caso contrário, a linha invertida é exibida.
get_line
A função get_line é responsável por capturar uma linha recebida através do input e armazená-la no array passado como argumento.
A função lê os caracteres individualmente utilizando getchar até encontrar uma quebra de linha (\n) ou o fim da entrada (EOF). Cada caractere recebido é armazenado em uma posição do array e, ao final, o terminador \0 é adicionado para indicar o fim da string.
O valor retornado pela função representa a quantidade de caracteres recebidos durante a leitura, mesmo que parte deles não possa ser armazenada caso o limite do array seja atingido.
Quando get_line encontra \n, a leitura é encerrada, mas esse caractere também é armazenado no final do array antes do terminador \0. Isso acontece porque o Enter enviado pelo terminal faz parte da entrada recebida pelo programa, gerando esse caractere de quebra de linha (exceto quando o limite do array é atingido antes que ele possa ser armazenado).
Exemplo: caso o input seja Hello, world in C! seguido de Enter, o array será preenchido com uma sequência de caracteres contendo a linha e o \n gerado pela quebra de linha: H e l l o , w o r l d i n C ! \n \0. O \0 é adicionado pela própria função para indicar o fim da string em C.
Agora que a função foi explicada, há detalhes importantes em sua implementação.
Parâmetros:
char s[]este primeiro argumento é o array onde os caracteres serão armazenados.int limo segundo argumento informa o tamanho máximo disponível, permitindo que a função controle até onde pode escrever e evite ultrapassar os limites da memória.
Variáveis:
crepresenta o caractere atual recebido pela função através degetchar.irepresenta a posição atual da leitura e também funciona como contador da quantidade de caracteres recebidos do input. Ele acompanha todos os caracteres lidos, mesmo aqueles que não podem ser armazenados caso o limite do array seja atingido.jrepresenta a posição atual de armazenamento no array, sendo utilizado separadamente deipara garantir que apenas a quantidade de caracteres que cabe no array seja armazenada.
A diferença entre i e j é um detalhe interessante do código: i acompanha tudo que foi recebido do input, enquanto j acompanha apenas aquilo que realmente foi salvo no array. Por isso, quando a linha é maior que o tamanho disponível, i continua aumentando enquanto j para de avançar.
A implementação tradicional deste exercício não utiliza uma variável separada como j; ela controla essa situação utilizando apenas i. Entretanto, ao resolver o exercício, separar essas duas responsabilidades pareceu uma solução mais clara: uma variável representa a quantidade de dados recebidos, enquanto a outra representa a quantidade de dados armazenados. A solução utilizando apenas i funciona, mas exige algumas decisões adicionais que tornam a lógica um pouco menos direta de acompanhar.
Olhando com atenção a condição de parada do for: for (i = 0; (c = getchar()) != EOF && c != '\n'; i++), quando o caractere \n é encontrado, o loop termina, mas o valor retornado por getchar já foi atribuído à variável c. Ou seja, a quebra de linha foi lida e consumida pela função, apenas não passou pelo corpo do for. Por isso, depois do loop, ainda é possível verificar: if (c == '\n') e armazenar esse caractere no array antes de adicionar o terminador \0.
A condição if (j < lim - 1) existe para garantir que ainda exista espaço suficiente no array para armazenar o caractere atual sem comprometer o espaço reservado para o terminador de string \0.
Mas por que -1? Porque uma string em C sempre precisa de uma posição extra para armazenar o \0, que indica onde ela termina. Se o array possui tamanho lim, o último espaço disponível deve ser reservado para esse terminador.
Posteriormente, após terminar a leitura, o programa adiciona o \0 no final da string:
if (j < lim)
s[j] = '\0';
Esse caractere não faz parte do texto recebido pelo usuário; ele é adicionado pelo programa para transformar a sequência de caracteres armazenada no array em uma string válida em C.
reverse
A função reverse é o núcleo do exercício proposto. Sua responsabilidade é receber uma string (um array de char) e inverter a ordem dos seus caracteres, fazendo com que o último se torne o primeiro, o penúltimo se torne o segundo e assim por diante.
Um detalhe importante é o terminador de string \0: ele não faz parte do conteúdo da string e, portanto, não deve participar da inversão.
Também vale comentar uma decisão de implementação. Optei por não utilizar len como parâmetro da função para torná-la independente de qualquer informação além da própria string. Dessa forma, a função descobre sozinha onde a string termina, percorrendo o array até encontrar o terminador \0.
Entretanto, seria perfeitamente possível evitar esse percurso. Bastaria fazer get_line retornar j (quantidade de caracteres armazenados) em vez de i (quantidade de caracteres lidos) e passar esse valor para reverse. Nesse cenário, a função já receberia o tamanho da string e não precisaria calculá-lo novamente.
Variáveis:
irepresenta o índice que percorre a string do início para o fim.jrepresenta o índice que percorre a string do fim para o início.tmpé uma variável temporária utilizada durante a troca de posições (swap) entre dois caracteres.
O primeiro while percorre a string até encontrar o terminador \0, incrementando j a cada iteração. Quando o laço termina, j está posicionado sobre o terminador de string. Em seguida, seu valor é decrementado para que passe a apontar para o último caractere válido da string, já que o \0 não deve participar da inversão.
while (s[j] != '\0')
j++;
j--;
O próximo while remove espaços, tabulações (\t) e quebras de linha (\n) que estejam no final da string. O laço percorre o array de trás para frente até encontrar o primeiro caractere que não pertence a esse conjunto. Como consequência, strings compostas apenas por esses caracteres acabam se tornando strings vazias. Quando o laço termina, o terminador \0 é reposicionado logo após o último caractere válido encontrado.
Por fim, o laço for realiza a inversão propriamente dita. Enquanto i avança do início para o fim da string, j avança do fim para o início. Em cada iteração, os caracteres s[i] e s[j] trocam de posição utilizando tmp como armazenamento temporário. O processo continua até que os dois índices se encontrem.
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
Exercício 1-22. Write a program to fold long input lines...
Enunciado completo: Write a program to "fold" long input lines into two or more shorter lines after the last non-blank character that occurs before the n-th column of input. Make sure your program does something intelligent with very long lines, and if there are no blanks or tabs before the specified column.
O exercício pede para criar um programa que quebre linhas longas em linhas menores, mas de uma forma inteligente. Em vez de simplesmente cortar a linha em uma posição fixa, o programa deve procurar o último espaço ou tabulação antes de uma determinada coluna e realizar a quebra nesse ponto, evitando separar palavras no meio. Entretanto, o exercício também exige que o programa saiba lidar com casos em que não existe nenhum espaço ou tabulação antes do limite definido, como uma palavra extremamente longa ou uma sequência contínua de caracteres. Nesse caso, o programa precisa tomar alguma decisão para conseguir dividir a linha mesmo sem um ponto ideal de quebra.
Diferente dos exercícios anteriores, aqui o programa não apenas analisa os dados recebidos; ele precisa reorganizar a própria estrutura armazenada, inserindo novos caracteres dentro do array. Esse foi um dos exercícios em que ficou mais evidente que manipular dados em C exige compreender exatamente como essas informações estão organizadas na memória.
Solução
/*
Exercise 1-22. Write a program to ``fold'' long input lines into two or more shorter lines after
the last non-blank character that occurs before the n-th column of input. Make sure your
program does something intelligent with very long lines, and if there are no blanks or tabs
before the specified column.
*/
#include <stdio.h>
#define MAXLINE 1000 // Array max size
#define FOLDCOL 10 // Fold column (\n)
int get_line(char line[], int maxline);
void fold(char line[]);
void insert_char(char line[], int pos, int len, char c);
int string_length(char s[]);
int main(void) {
char line[MAXLINE];
int i;
int len;
while ((len = get_line(line, MAXLINE)) > 0) {
fold(line);
printf("\n%s\n", line);
}
return 0;
}
int get_line(char line[], int maxline) {
int c;
int i;
for (i = 0; i < maxline - 1
&& (c = getchar()) != EOF
&& c != '\n';
i++) {
line[i] = c;
}
if (c == '\n') {
line[i] = c;
i++;
}
line[i] = '\0';
return i;
}
void fold(char line[]) {
int i;
int col = 0;
int last_blank = -1;
for (i = 0; line[i] != '\0'; i++) {
if (line[i] == '\n') {
col = 0;
last_blank = -1;
continue;
}
if (line[i] == ' ') {
last_blank = i;
}
if (col >= FOLDCOL) {
if (last_blank != -1) {
line[last_blank] = '\n';
col = i - last_blank - 1;
last_blank = -1;
}
else {
insert_char(line, i, string_length(line), '\n');
col = 0;
last_blank = -1;
i--;
}
}
col++;
}
}
void insert_char(char line[], int pos, int len, char c) {
int j;
for (j = len; j >= pos; j--)
line[j + 1] = line[j];
line[pos] = c;
}
int string_length(char s[]) {
int i = 0;
while (s[i] != '\0')
i++;
return i;
}
Conceitos aplicados neste exercício:
- Strings em C como arrays de char.
- Armazenamento temporário de entrada.
- Manipulação manual de strings.
- Controle de posição e colunas.
- Preservação de contexto para tomada de decisão.
- Reorganização de dados em memória.
- Tratamento de limites e casos excepcionais.
Esse exercício foi particularmente brutal.
Aqui o padrão das funções main, get_line e string_length é reaproveitado do exercício anterior. Em resumo, main é responsável por orquestrar o fluxo do programa; get_line transforma o fluxo de entrada em uma linha armazenada em um array de char; e string_length percorre a string até encontrar o terminador \0, retornando sua quantidade de caracteres.
Portanto, as novidades deste exercício estão nas funções fold e insert_char.
insert_chardesloca os elementos do array uma posição para a direita a partir de um determinado índice, abrindo espaço para a inserção de um novo caractere.foldimplementa a lógica principal do exercício, decidindo onde as quebras de linha devem ser inseridas.
insert_char
Primeiro, vamos entender a assinatura da função: void insert_char(char line[], int pos, int len, char c).
O void indica que a função não retorna nenhum valor. Os parâmetros recebidos são a string que será modificada, a posição onde o caractere deve ser inserido, o tamanho atual da string e o caractere a ser inserido.
A função é extremamente pequena e sua lógica é relativamente simples:
for (j = len; j >= pos; j--)
line[j + 1] = line[j];
line[pos] = c;
Aqui, j começa no final da string e caminha em direção à posição de inserção. A cada iteração, o elemento atual é copiado para a posição seguinte: line[j + 1] = line[j];.
Como o deslocamento acontece de trás para frente, nenhum dado é sobrescrito antes de ser copiado. Ao final do loop, todos os elementos a partir de pos foram deslocados uma posição para a direita, abrindo espaço para a inserção do novo caractere: line[pos] = c;.
Na prática, a função reorganiza os elementos do array para criar um espaço livre na posição desejada.
fold
fold foi uma função particularmente difícil de implementar.
Aqui surge uma ideia nova em relação aos exercícios anteriores: acompanhar a posição atual dentro da linha enquanto os caracteres são processados. A variável responsável por isso é col, que funciona como um contador de coluna.
Também existe a constante FOLDCOL, que define a partir de qual coluna o programa deve tentar inserir uma quebra de linha (\n).
Já last_blank funciona como um registro da posição do último espaço encontrado durante a leitura da linha. Sempre que um caractere espaço (' ') é encontrado, essa variável é atualizada com seu índice.
O valor inicial -1 também é importante. Ele indica que nenhum espaço foi encontrado até o momento. Dessa forma, quando a coluna limite é atingida, o programa consegue distinguir duas situações: existe um espaço válido para realizar a quebra ou não existe nenhum espaço disponível. Nesse segundo caso, a linha precisa ser cortada mesmo sem um ponto ideal de separação.
O primeiro laço for percorre a string até encontrar o terminador \0. Durante esse percurso, o programa mantém informações sobre a linha atual: onde está a coluna (col) e a posição do último espaço encontrado (last_blank).
No corpo do loop, primeiro é verificado se o caractere atual é uma quebra de linha (\n). Caso seja, significa que uma nova linha começou, então os valores precisam ser reiniciados. A variável col volta para 0, pois a contagem de colunas deve começar novamente, e last_blank volta para -1, indicando que nenhum espaço foi encontrado nessa nova linha.
O continue encerra a iteração atual, evitando que o restante da lógica seja executada para esse caractere.
for (i = 0; line[i] != '\0'; i++) {
if (line[i] == '\n') {
col = 0;
last_blank = -1;
continue;
}
Em seguida, o programa verifica se o caractere atual é um espaço. Caso seja, a posição desse espaço é armazenada em last_blank.
Isso é necessário porque a quebra não deve acontecer simplesmente na coluna limite. O exercício pede para que a linha seja quebrada no último espaço possível antes dessa coluna, evitando separar palavras no meio.
if (line[i] == ' ') {
last_blank = i;
}
A parte mais complexa acontece quando a coluna atual ultrapassa o limite definido: if (col >= FOLDCOL).
Nesse momento, o programa precisa decidir onde inserir a quebra de linha. Primeiro ele verifica se existe algum espaço registrado: if (last_blank != -1). Se existir, basta substituir esse espaço por uma quebra de linha: line[last_blank] = '\n';. Em seguida, last_blank é redefinido para -1, garantindo que a nova linha comece sem nenhum espaço registrado de linhas anteriores.
A parte mais sutil vem logo depois: col = i - last_blank - 1;. Essa operação recalcula a posição da coluna depois da quebra.
Imagine uma linha como: hello world test
Supondo que o espaço entre hello e world esteja no índice 5:
h e l l o _ w o r l d
0 1 2 3 4 5 6 7 8 9 10
Quando o programa substitui o espaço por \n, a linha passa a ser dividida naquele ponto:
hello
world
A variável col não pode continuar com o valor antigo, porque agora estamos em uma nova linha. Precisamos saber quantos caracteres existem depois do ponto onde a quebra aconteceu. Por isso: i - last_blank - 1.
Representa:
ié a posição atual que o programa alcançou;last_blanké a posição onde a quebra foi inserida;-1remove o espaço que existia entre as duas partes.
Usando o exemplo acima para tornar isso concreto: suponha que FOLDCOL seja 8 e que o loop chegou até o d de world, que está no índice 10. Nesse momento, i = 10 e last_blank = 5 (posição do espaço entre hello e world). A conta fica 10 - 5 - 1 = 4, que é exatamente a quantidade de caracteres de world que já foram percorridos antes de d — ou seja, w, o, r, l. O col++ no final da iteração ainda executa, fazendo col chegar a 5, que representa a posição da coluna já incluindo o d.
Caso nenhum espaço tenha sido encontrado antes da coluna limite:
else {
insert_char(line, i, string_length(line), '\n');
A situação é diferente: não existe um ponto natural para quebrar a linha. Então o programa precisa inserir uma quebra exatamente na posição atual.
A função insert_char desloca todos os caracteres seguintes uma posição para a direita e coloca o \n no índice i.
Para visualizar o que acontece com a string após a inserção, imagine que a string seja abcdefgh e que o limite seja atingido no índice 4, ou seja, no caractere e:
a b c d e f g h \0
0 1 2 3 4 5 6 7 8
Após insert_char inserir \n na posição 4, todos os caracteres a partir dali são deslocados uma posição para a direita:
a b c d \n e f g h \0
0 1 2 3 4 5 6 7 8 9
O \n agora ocupa a posição 4. O caractere e, que antes estava em line[4], agora está em line[5].
Depois disso:
col = 0;
last_blank = -1;
i--;
col = 0acontece porque uma nova linha acabou de começar.last_blank = -1acontece porque, depois da quebra, não existe mais nenhum espaço registrado que pertença à nova linha.i--existe por causa do deslocamento causado porinsert_char. Usando o exemplo acima: no momento da inserção,i = 4. Apósinsert_char, o caractereefoi deslocado paraline[5]. No final da iteração, oforexecutai++automaticamente. Sem oi--,ipassaria de 4 para 5 — pulando o\ninserido e processandoenormalmente, como se nada tivesse acontecido. Com oi--,ivai de 4 para 3, e oi++automático doforo traz de volta para 4. Na próxima iteração, o programa encontra o\nna posição 4, resetacolelast_blankpelo bloco já explicado anteriormente, e segue paraena posição 5.
Vale observar que, mesmo após o bloco else, o col++ no final da iteração ainda executa — fazendo col terminar a iteração como 1, não como 0. Isso é correto: na próxima iteração o programa estará processando o \n inserido, que vai zerar col novamente através do bloco if (line[i] == '\n'). Então o col = 1 é temporário e não representa nenhum problema.
Considerações finais
Resolver os exercícios do K&R tende a ser difícil quando um conceito ainda não está internalizado. Além disso, muitos deles passam a impressão de serem apenas exercícios difíceis sem muito sentido. Conforme avancei pelo capítulo, comecei a perceber que existe algo além disso.
Percebi que muitos desses exercícios não estavam ensinando apenas recursos da linguagem C. Por trás de problemas aparentemente simples, surgiam ideias como processamento de streams, manutenção de contexto, máquinas de estados e organização de informações em arrays.
Ao longo da leitura também ficou mais claro que os exercícios do livro normalmente representam problemas reais. fold, por exemplo, mostra que decisões aparentemente simples, como quebrar uma linha em um terminal, editor de texto ou aplicativo de mensagens, precisam ser implementadas por alguém. Em algum lugar existe um algoritmo responsável por decidir onde essa quebra acontece e como lidar com situações que fogem do caso ideal.
O livro raramente apresenta esses conceitos como explicações isoladas. Em vez disso, eles aparecem como consequência da tentativa de resolver um problema. Talvez seja por isso que alguns exercícios pareçam tão simples à primeira vista, mas revelam muito mais quando analisados com atenção.
Ao terminar o capítulo 1, fiquei surpreso com a densidade conceitual que exemplos tão pequenos podiam carregar. Por trás de programas que cabem em poucas linhas, o K&R introduz formas de pensar sobre dados, memória e processamento que continuam aparecendo em diversos contextos da programação. Esse é um aprendizado que certamente carregarei pelo restante da minha jornada.
Esse primeiro contato com o K&R e a linguagem C foi definitivamente desafiador, mas muito divertido.