Author: Blau Araujo
Date: 0101-01-01 00:00:00+00:00

Curso Básico da Linguagem C

Anotações das aulas ao vivo

Table of Contents

1. Sobre o curso

- 1.1. Do que vamos precisar…

- 1.2. Requisitos…

- 1.3. O que vamos aprender…

- 1.4. Como vamos aprender…

- 1.4.1. Aulas divididas em 3 estágios:

- 1.4.2. Repositório do curso:

- 1.5. Por que aprender…

- 1.6. Como clonar o repositório do curso…

- 1.6.1. Importante!

- 1.7. Como atualizar o repositório do curso…

2. História

- 2.1. Resumo do vídeo

- 2.2. Conceitos abordados na aula

- 2.2.1. O que é um programa de computador?

- 2.2.2. O que é linguagem de máquina?

- 2.2.3. O que é uma linguagem de programação?

- 2.2.4. O que são linguagens de baixo e alto nível?

- 2.2.5. O que é compilação?

- 2.2.6. O que são implementações?

- 2.2.7. O que são arquiteturas orientadas a palavras?

- 2.2.8. O que são arquiteturas orientadas a bytes?

- 2.3. Características da linguagem C

- 2.4. Implementação GNU/Linux

- 2.4.1. Compilador:

- 2.4.2. Biblioteca C Padrão:

- 2.4.3. Obter a especificação padrão do gcc…

- 2.5. Compilação

- 2.5.1. Etapas:

- 2.6. Exemplo de um programa em C

- 2.6.1. Compilação:

- 2.6.2. Execução:

- 2.7. Exercícios

- 2.7.1. Pesquise e explique

- 2.7.2. Analise o programa

- 2.7.3. Compile, encontre, explique e corrija os bugs

- 2.7.4. Compile, pesquise e responda

- 2.7.5. Pesquise e responda

3. Dados e instruções

- 3.1. O que são programas

- 3.1.1. Instruções
- 3.1.2. Dados
- 3.1.3. Exemplo (x10.c)
- 3.1.4. Outro exemplo (x10const.c)
- 3.1.5. Mais um exemplo (quadrado.c)

- 3.2. Dúvidas da aula ao vivo

- 3.2.1. Para que serve a função 'main'?

- 3.2.2. Qual é o significado de 'void'?

- 3.2.3. Por que o tipo do retorno de 'main' é 'int'?

- 3.2.4. A função 'main' poderia retornar outros tipos de dados?

- 3.2.5. A função 'main' poderia retornar uma string?

- 3.2.6. Por que o cabeçalho 'stdio.h' foi incluído no programa?

- 3.3. Exercícios

- 3.3.1. Pesquise e responda
- 3.3.2. Operações aritméticas
- 3.3.3. Números mágicos
- 3.3.4. Compile, corrija e responda
- 3.3.5. Pesquise e responda

4. Tipos de dados

- 4.1. Exemplo da última aula

- 4.2. Tipos são tamanhos de dados na memória

- 4.2.1. Maior número que se pode escrever com 4 bytes (32bits):

- 4.2.2. O que aconteceu?

- 4.3. Exemplo corrigido

- 4.3.1. Tipos primitivos
- 4.3.2. Qualificador 'unsigned' (sem sinal)
- 4.3.3. Qualificador 'long'
- 4.3.4. Tipos customizados

- 4.3.5. Expressões constantes (valores literais)

- 4.3.6. O tipo 'void'

- 4.3.7. Modelagem de tipos (type casting)

- 4.4. Exercícios

- 4.4.1. Pesquise e demonstre

- 4.4.2. Desafio: conversão de Fahrenheit para Celsius

- 4.4.3. Pesquise e responda
- 4.4.4. Compile, corrija e responda
- 4.4.5. Analise, pesquise e comente o código

5. Variáveis e ponteiros

- 5.1. Qual é a diferença?

- 5.2. Variáveis

- 5.2.1. Declaração e definição
- 5.2.2. Atributos das variáveis
- 5.2.3. Escopo de variáveis

- 5.3. Ponteiros

- 5.3.1. Declaração do ponteiro

- 5.4. Perguntas da aula ao vivo

- 5.4.1. A cada execução do programa, os endereços das variáveis mudam?

- 5.4.2. O ponto da declaração de uma variável global faz diferença?

- 5.5. Exercícios

- 5.5.1. Pesquise e responda

- 5.5.2. Analise o código, pesquise e responda

- 5.5.3. Compile, analise e demonstre
- 5.5.4. Pesquise e responda
- 5.5.5. Desafio: quadrado de um número

6. Estruturas de controle de fluxo

- 6.1. O que são estruturas de controle?

- 6.1.1. Controle de fluxo
- 6.1.2. O loop 'for'
- 6.1.3. Loop 'while'
- 6.1.4. Loop 'do..while'

- 6.1.5. Estrutura de decisão 'if..else if..else'

- 6.1.6. Estrutura 'switch..case'

- 6.2. Exercícios

- 6.2.1. Desafio: caracteres alfanuméricos ASCII

- 6.2.2. Desafio: dias da semana

- 6.2.3. Desafio: conversão de base 10 para 16 e vice-versa

7. Vetores

- 7.1. O que são vetores

- 7.1.1. Utilidade dos vetores

- 7.1.2. Declaração e inicialização de vetores

- 7.1.3. Tamanho de um vetor em tempo de compilação

- 7.1.4. Inicialização parcial
- 7.1.5. Acesso aos elementos de um vetor
- 7.1.6. Iteração pelos elementos
- 7.1.7. Vetores e funções
- 7.1.8. Relação entre vetores e ponteiros
- 7.1.9. Vetores de caracteres (strings)

- 7.1.10. Mais diferenças entre vetores e ponteiros

- 7.2. Exercícios

- 7.2.1. Desafio: Função 'printarrayshift'
- 7.2.2. Desafio: Função 'arrayshift'
- 7.2.3. Desafio: Função 'arrayreverse'

8. Vetores, ponteiros e strings

- 8.1. Relações entre vetores e ponteiros

- 8.1.1. Notações de acesso
- 8.1.2. Vetores e funções
- 8.1.3. Strings são vetores de caracteres

- 8.1.4. Inicializadores escalares e agregados

- 8.1.5. Vetores de comprimentos variáveis (VLA)

- 8.2. Exercícios

- 8.2.1. Desafio: Função 'strblen'
- 8.2.2. Pesquise e analise o código
- 8.2.3. Desafio: Palavras invertidas
- 8.2.4. Desafio: Função 'arrayisort'

- 8.2.5. Desafio: codificador e decodificador ROT13

9. Processos e layout de memória

- 9.1. Filosofia UNIX

- 9.1.1. Primeiro princípio: especialização
- 9.1.2. Segundo princípio: modularidade
- 9.1.3. Terceiro princípio: fluxos de texto

- 9.1.4. A interface de linha de comando (CLI)

- 9.2. O que são processos

- 9.2.1. Como programas são executados

- 9.3. Layout de memória

10. O vetor de argumentos

- 10.1. Vetores e strings

- 10.1.1. Vetores de "ponteiros"
- 10.1.2. Vetores de strings
- 10.1.3. Vetor de strings terminado com NULL
- 10.1.4. Parâmetros da função 'main'
- 10.1.5. Conversão de strings para números

- 10.2. Exercícios

- 10.2.1. Desafio: Programa 'listenv'
- 10.2.2. Desafio: Programa 'tconv'
- 10.2.3. Desafio: Programa '2fer'
- 10.2.4. Desafio: Programa 'fatp'
- 10.2.5. Desafio: Programa 'round'

11. Entrada e saída de dados

- 11.1. Tabela de descritores de arquivos

- 11.1.1. Fluxos de dados padrão
- 11.1.2. Uma nota sobre dispositivos
- 11.1.3. Representação em '/proc'
- 11.1.4. Redirecionamentos e pipes
- 11.1.5. Armazenamento temporário (buffers)

- 11.1.6. Buffers de entrada e saída do programa

- 11.1.7. Buffer de saída do programa
- 11.1.8. Buffer de entrada do programa
- 11.1.9. Entrada padrão não bufferizada

12. Leitura da entrada padrão com 'scanf'

- 12.1. Verificando a ligação com a entrada padrão

- 12.2. Leitura interativa da entrada padrão

- 12.2.1. Leitura formatada de linhas com 'scanf'

- 12.2.2. Para reduzir as chances de erro
- 12.2.3. Descarregando o buffer de entrada
- 12.2.4. Inicialização de valores numéricos
- 12.2.5. Buffer overflow

- 12.3. Exercícios

- 12.3.1. Desafio: Conversão Milhas->Km e Km->Milhas

- 12.3.2. Desafio: Separação de linhas em campos

- 12.3.3. Desafio: Implementando um 'cat' com 'scanf'

13. Leitura da entrada padrão com 'fgets'

- 13.1. Ainda sobre a função 'scanf'

- 13.2. A função 'fgets'

- 13.2.1. Uma nota sobre o tipo ponteiro para FILE

- 13.2.2. O problema da quebra de linha
- 13.2.3. Retorno nulo ambíguo
- 13.2.4. Conversão para números

- 13.3. Exercícios

- 13.3.1. Desafio: Fatorial de 'n'

- 13.3.2. Desafio: Qual é o tipo?

14. Leitura da entrada padrão com 'read'

- 14.1. Chamadas de sistema

- 14.2. A função 'read'

- 14.2.1. Conversão para string incluindo o terminador nulo

- 14.2.2. Quando não converter os bytes lidos em string

- 14.2.3. O buffer do terminal

- 14.2.4. Descarga do buffer do terminal

- 14.2.5. Implementação de 'flushstdin' com a chamada 'read'

- 14.2.6. Descarga condicional do buffer do terminal

- 14.2.7. Definindo um prompt

- 14.3. Exercícios

- 14.3.1. Desafio: Função 'readline'

- 14.3.2. Desafio: Funções auxiliares

15. Abertura de arquivos para leitura

- 15.1. Como os arquivos são acessados

- 15.1.1. Diferença entre streams e descritores de arquivos

- 15.1.2. Procedimento geral de acesso a arquivos para leitura

- 15.2. Exercícios

- 15.2.1. O arquivo 'passwd'

16. Abertura de arquivos para escrita

- 16.1. Modos de abertura de arquivos

- 16.1.1. Modos complementares

- 16.1.2. Modos texto e binário

- 16.2. Modos de escrita

- 16.2.1. Abertura de um arquivo para escrita ("w")

- 16.2.2. Abertura de um arquivo para append ("a")

- 16.2.3. Notas gerais

- 16.3. Exercícios

- 16.3.1. Programa 'nf' (new file)

- 16.3.2. Programa 'hd' (here doc)

17. Abertura de arquivos para leitura e escrita

- 17.1. Modos de abertura para leitura e escrita

- 17.1.1. Manipulação do ponteiro interno
- 17.1.2. Abertura de arquivos com "r+"
- 17.1.3. Abertura de arquivos com "a+"
- 17.1.4. Procedimentos úteis

- 17.2. Exercícios

- 17.2.1. Contando e inserindo linhas
- 17.2.2. Desafio: Trocando palavras
- 17.2.3. Desafio: O código Da Vinci

1. Sobre o curso

Vídeo desta aula

1.1. Do que vamos precisar…

- Editores: Geany, GNU Emacs, Vim, GNU Nano, etc…

- Shell: GNU Bash

- Repositório: https://gitlab.com/blau_araujo/cblc

- Compilador: GNU Compiler Collection (gcc)
- Depuração: GNU Debugger (gdb)
- Documentação: Páginas de manual

1.2. Requisitos…

- Uma instalação do GNU/Linux
- Noções de operação do sistema pelo terminal
- Atenção e cuidado com os detalhes
- Uma boa dose de curiosidade
- Vontade de aprender

1.3. O que vamos aprender…

- Como computadores executam programas

- Como o sistema possibilita a execução de programas

- Como códigos em texto viram código de máquina

- Princípios de programação imperativa e estruturada

- Elementos de um código em C
- Variáveis, funções e tipos de dados
- Memória, endereços e estruturas de dados
- Estruturas de decisão e repetição
- Expressões lógicas e aritméticas
- Programas "recicláveis" (bibliotecas)

1.4. Como vamos aprender…

1.4.1. Aulas divididas em 3 estágios:

- Apresentação do conteúdo gravado
- Demosntrações ao vivo
- Exercícios e desafios

1.4.2. Repositório do curso:

- Anotações das aulas
- Exemplos e exercícios
- Tira-dúvidas (aba Issues)

1.5. Por que aprender…

- Linguagem muito simples

- Perfeita para aprender os princípios da programação

- Tem implementação nativa do GNU/Linux

- Enfatiza o papel de quem programa na solução de problemas

- Intimamente ligada ao gerenciamento de memória

- Linguagem viva, relevante e amplamente utilizada

1.6. Como clonar o repositório do curso…

Com o git instalado, executar:

git clone https://bolha.dev/blau_araujo/cblc

Isso criará o diretório cblc no diretório corrente, por exemplo:

:~/cursos$ git clone https://bolha.dev/blau_araujo/cblc
:~/cursos$ ls
cblc
:~/cursos$ cd cblc
:~/cursos/cblc$

1.6.1. Importante!

Não faça alterações no diretório do curso, ou você terá dificuldades para atualizá-lo!

1.7. Como atualizar o repositório do curso…

Entre no diretório clonado e execute:

git pull origin main

2. História

Vídeo desta aula

2.1. Resumo do vídeo

- Ken Thompson cria a linguagem B (1969) para ter uma linguagem de alto nível para reescrever o UNIX.

- Dennis Ritchie continua aprimorando a linguagem B.

- Em 1972, a linguagem B está tão diferente que recebe o nome de linguagem C.

- Em 1978, Brian Kernighan e Dennis Ritchie lançam o livro: The C Programming Language, que acaba se tornando a primeira especificação da linguagem.

- Entre 1983 e 1989, são escritas as primeiras especificações ANSI (ISO/IEC) para a linguagem.

- Os racunhos de 1987 (C87) são utilizados para escrever a segunda edição do livro, com o nome de The ANSI C Programming Language.

2.2. Conceitos abordados na aula

2.2.1. O que é um programa de computador?

Um programa pode ser entendido como uma forma de manipular dados através de instruções para a realização de alguma tarefa.

2.2.2. O que é linguagem de máquina?

A linguagem de máquina (ou código de máquina) é o conjunto de códigos numéricos binários (numeração de base 2) que, transformados em sinais elétricos, irão acionar as operações implementadas na CPU do computador (OpCodes).

2.2.3. O que é uma linguagem de programação?

Uma linguagem de programação é um conjunto de especificações e regras que definem uma forma de escrever programas sem recorrer à escrita direta em código de máquina.

2.2.4. O que são linguagens de baixo e alto nível?

Toda linguagem de programação oferece algum grau de abstração (redução de complexidades) em relação à escrita do código de máquina (que é a única linguagem que a CPU do computador é capaz de interpretar).

A linguagem Assembly, uma linguagem de baixo nível, oferece basicamente um conjunto de mnemônicos para facilitar a escrita de instruções da CPU (OpCodes) e algumas macros e pseudo-instruções para orientar a montagem do arquivo binário do programa.

Exemplo de programa em Assembly (NASM x86_64) que não faz nada e sai com estado de término 42.

section .text
global _start ; Ponto de entrada do programa

_start:
; Convenção da chamada de sistema 'exit'
mov rax, 60 ; Passar o número da chamada de sistema (60 = exit) para RAX.
mov rdi, 42 ; Passar o valor do estado de término (42) para RDI
syscall ; Chamar o kernel

A linguagem C, por sua vez, abstrai procedimentos completos (rotinas) na forma de palavras-chave, funções da biblioteca padrão, estruturas e construções de linguagem, o que faz dela uma linguagem de alto nível.

Exemplo de programa em C que não faz nada e sai com estado de término 42.

// Ponto de entrada do programa (função 'main')...
int main(void) {
return 42; // Terminar a função retornando o valor 42...
}

2.2.5. O que é compilação?

Compilação é o processo de traduzir um código escrito em uma linguagem de alto nível (por exemplo, C) para uma linguagem de montagem (por exemplo, Assembly).

2.2.6. O que são implementações?

Toda linguagem de programação precisa ser implementada conforme suas especificações.

Linguagens compiladas, por exemplo, são implementadas através dos seus compiladores, enquanto as linguagens interpretadas são implementadas nos seus interpretadores.

No caso da linguagem C, as especificações são implementadas na forma de um compilador (cc, nos Unix, e gcc, no Projeto GNU) e de uma biblioteca padrão (libc, nos Unix, e glibc, no Projeto GNU).

2.2.7. O que são arquiteturas orientadas a palavras?

São sistemas computacionais que utilizam quantidades arbitrárias de bits como unidade de tamanho de dados. Exemplo: o PDP-7 utilizava palavras de 18 bits.

2.2.8. O que são arquiteturas orientadas a bytes?

São sistemas computacionais que utilizam múltiplos de bytes (8 bits) para definir tamanhos de dados. Exemplo: a arquitetura x86_64 utiliza palavras de 2 bytes, o que corresponde ao tipo short int na linguagem C.

2.3. Características da linguagem C

- Linguagem compilada
- De alto nível
- De uso geral
- Imperativa
- Procedural
- Estruturada

2.4. Implementação GNU/Linux

2.4.1. Compilador:

- GNU Compiler Collection (gcc)

2.4.2. Biblioteca C Padrão:

- GNU Libc (glibc)

2.4.3. Obter a especificação padrão do gcc…

:~$ gcc -E -dM -x c /dev/null | grep '__STDC_VERSION__'
#define __STDC_VERSION__ 201710L

- -E: Para a compilação após gerar o código pré-processado.

- -dM: Filtra todas as diretivas que definem macros.

- -x c: Especifica a linguagem (no caso, c).

- /dev/null: Compilar o arquivo vazio /dev/null.

2.5. Compilação

Transformação do código-fonte em texto do programa para um código binário em linguagem de máquina.

2.5.1. Etapas:

1 - Pré-processamento:

gcc -E arquivo.c → Saída padrão

2 - Compilação:

gcc -S arquivo.c → arquivo.s (fonte em assembly)

3 - Montagem:

gcc -c arquivo.c → arquivo.o (objeto binário)

4 - Link-edição:

gcc arquivo.c → a.out (arquivo binário executável)

2.6. Exemplo de um programa em C

Programa salve.c:

#include <stdio.h>

int main(void) {

puts("Salve, simpatia!");

return 0;
}

2.6.1. Compilação:

:~/git/cblc/aulas/01-historia$ gcc -o salve salve.c

2.6.2. Execução:

:~/git/cblc/aulas/01-historia$ ./salve
Salve, simpatia!

2.7. Exercícios

2.7.1. Pesquise e explique

As classificações abaixo são utilizadas para descrever as características da linguagem C: pesquise e explique cada uma delas.

- Linguagem de alto nível;
- Linguagem compilada;
- Linguagem procedural;
- Linguagem imperativa;
- Linguagem estruturada.

2.7.2. Analise o programa

Dado o programa abaixo (salve.c), responda cada uma das perguntas nos comentários.

#include <stdio.h> // O que é isso e para que serve?

// Por que todo o programma é escrito na função 'main()'?
int main(void) {

puts("Salve, simpatia!"); // O que faz e como se usa a função 'puts()'?

return 0; // Para que serve esta instrução?
}

2.7.3. Compile, encontre, explique e corrija os bugs

O uso de números mágicos (valores literais que não dão pistas de seus significados) é desaconselhado porque dificultam a leitura semântica do código. Sendo assim, nós utilizaremos a constante simbólica EXIT_SUCCESS, definida na biblioteca padrão com valor 0, em vez do inteiro literal 0 como argumento da instrução return:

#include <stdio.h>

int main(void) {
puts("Salve, simpatia!");
return EXIT_SUCCESS
}

Observe o resultado da compilação:

:~$ gcc salve.c -o salve

- Quantos erros foram encontrados e quais são eles?

- Quais são as correções sugeridas pelo gcc?

- Como ficou o código corrigido?

2.7.4. Compile, pesquise e responda

Dado o código abaixo (por exemplo, main.c):

#define EXIT_SUCCESS 0

int puts(const char *string);

int main(void) {
puts("Salve, simpatia!");
return EXIT_SUCCESS;
}

Compilação:

:~$ gcc main.c
:~$

- Nenhum erro foi reportado, mas por quê?

- Como você executaria o binário resultante?

2.7.5. Pesquise e responda

Quando não explicitamos a especificação utilizada para compilar o nosso programa (opção -std=PADRÃO do gcc), qual será o padrão utilizado e como podemos confirmar isso pela linha de comandos?

3. Dados e instruções

Vídeo desta aula

3.1. O que são programas

Programas podem ser entendidos como formas de manipular dados através de instruções para realizar tarefas.

3.1.1. Instruções

- Declarações de variáveis e funções
- Atribuições e alterações de variáveis
- Chamadas de funções
- Retorno de funções, etc…

Toda instrução simples é terminada com ;.

3.1.2. Dados

- Constantes simbólicas
- Expressões constantes
- Operações lógicas e aritméticas
- Valores retornados por funções
- Endereços de memória, etc…

Tudo que expressa um valor é chamado de expressão.

3.1.3. Exemplo (x10.c)

#include <stdio.h>

int main(void) {
int num;

num = 5;

printf("%d x 10 = %d\n", num, num * 10);
return 0;
}

O que temos aqui:

- int num; → Declaração da variável num.

- num = 5; → Atribuição de um valor à variável num.

- printf(...); → Chamada da função printf.

- "%d x 10 = %d\n" → Primeiro argumento da função printf.

- num → Acesso ao dado na variável num.

- num * 10 → Dado na variável num vezes 10.

3.1.4. Outro exemplo (x10const.c)

#include <stdio.h>

#define MULT 10 // Multiplicador.

int main(void) {
int num = 5;

printf("%d x 10 = %d\n", num, num * MULT);
return 0;
}

O que há de novo:

- #define MULT 10 → Definição da constante simbólica MULT.

- int num = 5; → Inicialização da variável num na própria declaração.

- num * MULT → Dado em num vezes a expansão de MULT.

O valor de MULT será expandido no pré-processamento!

3.1.5. Mais um exemplo (quadrado.c)

#include <stdio.h>

int quadrado(int base) {
return base * base;
}

int main(void) {
int num = 5;

printf("%d^2 = %d\n", num, quadrado(num));
return 0;
}

O que há de novo desta vez:

- int quadrado(int base)... → Definição da função quadrado.

- return base * base → Retorno da função quadrado.

- quadrado(num); → Chamada da função quadrado.

3.2. Dúvidas da aula ao vivo

Anotações feitas pelos próprios alunos.

3.2.1. Para que serve a função 'main'?

A função main é o ponto de entrada do programa, ou seja, é o ponto a partir do qual o programa começará a ser executado. Quando a função main termina, o programa é terminado retornando um valor inteiro (int) para o sistema, que utilizará este valor como estado de término do programa.

3.2.2. Qual é o significado de 'void'?

No exemplo da aula, void (que significa vazio) é utilizado para explicitar que a função main não receberá argumentos:

int main(void) { /* ... */ }

Isso não é obrigatório e existem situações em que a função main receberá dados do ambiente de execução (sistema operacional), como argumentos de linha de comando e variáveis exportadas para o programa, por exemplo:

int main(int arg, char **argv) { /* ... */ }

A palavra-chave void tem outros usos não relacionados com o caso do exemplo da aula.

3.2.3. Por que o tipo do retorno de 'main' é 'int'?

Para atender às especificações da linguagem (5.1.2.2.1 Program startup).

3.2.4. A função 'main' poderia retornar outros tipos de dados?

Em tese, sim, mas a função main tem um papel diferenciado e deve seguir as especificações da linguagem. De qualquer forma, o retorno da função main significa o término do programa e o valor retornado sempre será um valor numérico inteiro utilizado como estado de término pelo sistema.

No sistema, o estado de término de um programa é registrado como um inteiro sem sinal de 8 bits (valores de 0 a 255), onde 0 representa o término com sucesso.

Video sobre o retorno da função main.

3.2.5. A função 'main' poderia retornar uma string?

Além do que foi respondido antes, nenhuma função pode retornar os dados associados a vetores. Vetores são endereços de memória (inteiros longos, na arquitetura x86-64) e strings são vetores de caracteres. Mais uma vez, o valor retornado seria um número (o endereço da cadeia de caracteres).

3.2.6. Por que o cabeçalho 'stdio.h' foi incluído no programa?

Para que pudéssemos utilizar a função printf.

A linguagem C implementa muito poucas instruções nativas, deixando a implementação de outras funcionalidades e abstrações para as funções da biblioteca C padrão. Por sua vez, a biblioteca C padrão é muito grande e possui centenas de funções implementadas. Então, para otimizar o acesso a elas, nós só precisamos das declarações das funções que serão efetivamente utilizadas nos nossos programas.

Nós poderíamos fazer essas declarações nos códigos onde elas fossem utilizadas, mas a própria biblioteca padrão oferece vários arquivos de cabeçalhos: arquivos contendo declarações de funções e outras definições, que podem ser inseridos nos nossos códigos apenas quando necessário.

- Vídeo sobre cabeçalhos e bibliotecas (parte 1)

- Vídeo sobre cabeçalhos e bibliotecas (parte 2)

3.3. Exercícios

3.3.1. Pesquise e responda

- Quais são as finalidades da função printf?

- Em que cabeçalho da glibc ela é declarada?

- Quantos argumentos ela recebe?

- Como fazer para que a saída imprima quebras de linha?

- Para que servem os especificadores de formato %d, %s, %c e %f?

3.3.2. Operações aritméticas

Escreva um programa que, dado um valor inteiro associado à variável num, calcule e imprima os resultados das seguintes operações:

- num multiplicado por 15;
- num somado a 42;
- 123 menos num;
- Divisão de num por 5;
- Resto da divisão de num por 5.

Esquema geral da saída do programa:

NUM x 15 = VALOR
NUM + 42 = VALOR
123 - NUM = VALOR
NUM / 5 = VALOR
RESTO DE NUM / 5 = VALOR

3.3.3. Números mágicos

O uso de valores literais sem um significado óbvio (números mágicos) é uma prática indesejável, mas pode ser evitada de várias formas. Sendo assim, analise as situações abaixo e proponha uma solução.

Caso 1: Propriedades de círculos

Meu programa tem funções para calcular a área e o perímetro de um círculo cujo raio (um inteiro) é recebido como argumento:

#include ...

float circ_perim(int raio) {
/* Circunferência = 2πr */
return 2 * 3.14 * raio;
}

double circ_area(int raio) {
/* Área = πr² */
return 3.14 * raio * raio;
}

int main(void) { ... }

Caso 2: Multiplicador constante

No meu programa, todas as operações aritméticas envolvem a multiplicação de um valor por 10.

#include <stdio.h>

int main(void) {
int num1 = 13;
int num2 = 29;

printf("%d x %d = %d\n", num1, 10, num1 * 10);
printf("%d x %d = %d\n", num1 + num2, 10, (num1 + num2) * 10);
printf("%d x %d = %d\n", num2, 10, num2 * 10);
}

3.3.4. Compile, corrija e responda

Dado o programa abaixo (por exemplo, main.c)…

#include <stdio.h>

int main(void) {
char *fmt = "Circunferência de um círculo de raio %d: %.2f\n";
printf(fmt, r, circ_perim(r));
return 0;
}

float circ_perim(int raio) {
/* Circunferência = 2πr */
return 2 * 3.14 * raio;
}

- Qual foi o erro encontrado na compilação?

- Por que esse erro aconteceu?

- Qual foi a solução adotada?

- A sua solução é a única possível?

- Se não for, cite outras possibilidades.

3.3.5. Pesquise e responda

Pesquise, entre as funções da glibc, aquelas que podem solucionar os problemas abaixo e demonstre como elas podem ser utilizadas.

- Pausar a execução do programa por 10 segundos.

- Apenas imprimir uma quebra de linha.

- Ler um número inteiro interativamente e atribuí-lo a uma variável.

- Imprimir um número inteiro decimal em base 8 e base 16.

- Terminar a execução do programa com um valor de estado de término.

4. Tipos de dados

- Vídeo desta aula

- Livro recomendado: O Fantástico Mundo da Linguagem C - Alexandre Firori

4.1. Exemplo da última aula

Arquivo: quadrado.c

#include <stdio.h>

int quadrado(int base) {
return base * base;
}

int main(void) {
int num = 100000;

printf("%d^2 = %d\n", num, quadrado(num));
return 0;
}

O que há de novo:

int num = 100000; → A base será 100.000

Compilando e executando…

:~$ gcc quadrado.c
:~$ ./a.out
100000^2 = 1410065408

O resultado está obviamente errado!

4.2. Tipos são tamanhos de dados na memória

- Bit: algarismos 0 ou 1;

- Byte: 8 bits;

- Tipo int: 32 bits == 4 bytes (arquitetura x86_64).

4.2.1. Maior número que se pode escrever com 4 bytes (32bits):

(2^32) - 1 = 4.294.967.295

Quadrado de 100.000:

100.000^2 = 10.000.000.000

4.2.2. O que aconteceu?

Bytes necessários para escrever 10.000.000.000:

:~$ printf '0x%016x\n' 10000000000
0x00000002540be400
↑
5 bytes

Bytes truncados pelo limite do tipo int (4 bytes):

:~$ printf '0x%016x\n' 1410065408
0x00000000540be400
↑
4 bytes

Precisamos de mais espaço para escrever o resultado:

Tipo | Tamanho | Faixa de valores
char | 1 byte | -128 a 127
short | 2 bytes | -32.768 a 32.767
int | 4 bytes | -2.147.483.648 a 2.147.483.647
long | 8 bytes | -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807

Todos esses tipos consideram o uso de valores com sinal!

4.3. Exemplo corrigido

Arquivo: quadrado-long.c

#include <stdio.h>

unsigned long quadrado(long base) {
return base * base; /* Qual é o tipo dessa expressão? */
}

int main(void) {
long num = 100000;

printf("%ld^2 = %lu\n", num, quadrado(num));
return 0;
}

Compilando e executando:

:~$ gcc quadrado-long.c
:~$ ./a.out
100000^2 = 10000000000

4.3.1. Tipos primitivos

Tipo | Tamanho | Faixa de valores (x86_64)
char | 1 byte | -128 a 127
short | 2 bytes | -32.768 a 32.767
int | 4 bytes | -2.147.483.648 a 2.147.483.647
float | 4 bytes | 1,18E-38 a 3,40E+38
double | 8 bytes | 2,23E-308 a 1,80E+308

Os tipos float e double são representações inteiras que codificam números de ponto flutuante em 32 e 64 bits, respectivamente.

4.3.2. Qualificador 'unsigned' (sem sinal)

Tipo | Tamanho | Faixa de valores (x86_64)
unsigned char | 1 byte | 0 a 255
unsigned short | 2 bytes | 0 a 65.535
unsigned int | 4 bytes | 0 a 4.294.967.295

4.3.3. Qualificador 'long'

Tipo | Tamanho | Faixa de valores (x86_64)
long | 8 bytes | -9223372036854775808 a 9223372036854775807
unsigned long | 8 bytes | 0 a 18446744073709551615
long double | 16 bytes | 3,36E-4932 a 1,19E+4932

4.3.4. Tipos customizados

Nós podemos criar apelidos para tipos:

typedef [QUALIFICADORES] TIPO NOME;

Exemplos:

typedef int i32;
typedef unsigned int u32;
typedef long int i64;
typedef unsigned long int u64;

Para diferenciar de nomes de variáveis, é comum utilizar o sufixo _t (de type):

typedef int i32_t;
typedef unsigned int u32_t;
typedef long int i64_t;
typedef unsigned long int u64_t;

4.3.5. Expressões constantes (valores literais)

EXPRESSÃO | TIPO
42 | int
42U | unsigned int
42L | long int
42UL | unsigned long int
42LL | long long int
42ULL | unsigned long long int
'a' | int
12.3 | double
1e16 | double
12.3F | float
1e16F | float
12.3L | long double
1e16L | long double
"salve" | unsigned long (endereço)

4.3.6. O tipo 'void'

No contexto de declarações de funções, void representa a ausência de tipo, mas pode ter outros significados.

Funções sem valor de retorno

void func(int num) {
printf("num x 2 = %d\n", num * 2);
}

Retorno não é o que a função imprime, mas o valor que ela expressa!

Funções que não recebem argumentos

int func(void) {
puts("Nada a fazer...");
return 42;
}

Ponteiros de dados de tipo indeterminado

void *ptr;

Ponteiros são variáveis que recebem endereços da memória (aula 5).

4.3.7. Modelagem de tipos (type casting)

Type casting é a conversão explícita de um tipo de dado para outro, utilizando a sintaxe (TIPO) antes de um valor ou de uma variável:

Exemplo:

(double)5 /* O inteiro '5' será compilado como 'double'. */

4.4. Exercícios

4.4.1. Pesquise e demonstre

No programa quadrado-long.c, da aula 2, encontre uma forma de demonstrar o tipo do valor avaliado pela expressão base * base nesta função:

unsigned long quadrado(long base) {
return base * base; /* Qual é o tipo desse expressão? */
}

4.4.2. Desafio: conversão de Fahrenheit para Celsius

Criar um programa que converta valores inteiros de -100ºF a 100ºF para Celsius e apresente os resultados no terminal.

Fórmula da conversão: C = 5 × (F - 32) / 9

Os valores em Fahrenheit serão expressos pela variável i, declarada no loop for.

Por exemplo:

for (int i = -100; i <= 100; i++) {
/* O código que você achar que deve ser executado... */
}

4.4.3. Pesquise e responda

O operador sizeof(TIPO) expressa o tamanho em bytes de TIPO com o tipo size_t.

- O que é o tipo size_t?

- Como utilizá-lo no seu programa sem incluir cabeçalhos?

- Em quais cabeçalhos ele é definido?

- Qual é o especificador de formatos para ele no printf?

4.4.4. Compile, corrija e responda

O programa abaixo deveria imprimir 0.25 no terminal…

#include <stdio.h>

int main(void) {
int valor = 1 / 4;

printf("%d\n", valor);

return 0;
}

Responda:

- Depois de compilado e executado, qual foi o valor impresso?

- Por que isso aconteceu?

- Como solucionar?

4.4.5. Analise, pesquise e comente o código

Programa dump.c:

#include <stdio.h>

void print_bytes(char *label, void *addr, size_t size);

int main(void) {

int num = 100000;

int sqr_int = num * num;
unsigned long sqr_long = (unsigned long)num * num;

print_bytes("num ", &num, sizeof(num));
printf("-> %d\n", num);

print_bytes("sqr_int ", &sqr_int, sizeof(sqr_int));
printf("-> %d\n", sqr_int);

print_bytes("sqr_long", &sqr_long, sizeof(sqr_long));
printf("-> %lu\n", sqr_long);

return 0;
}

void print_bytes(char *label, void *addr, size_t size) {

unsigned char *bytes = (unsigned char *)addr;

printf("%p - %s - ", bytes, label);

for (size_t i = 0; i < 8; i++)
printf("%02x ", i > (size - 1) ? 0 : bytes[i]);
}

5. Variáveis e ponteiros

vídeo desta aula

5.1. Qual é a diferença?

- Variáveis são elementos da linguagem que representam dados na memória.

- Ponteiros são variáveis que representam endereços de dados na memória.

5.2. Variáveis

5.2.1. Declaração e definição

Variáveis precisam ser declaradas e definidas antes de serem utilizadas.

Exemplo (var1.c):

#include <stdio.h>

int main(void) {

int a = 10; /* Variável declarada e inicializada (definida). */
int b; /* Variável declarada, mas não inicializada! */

printf("a = %d\n", a);
printf("b = %d\n", b);
printf("a + b = %d\n", a + b);

return 0;
}

Compilando e executando:

:~$ gcc -o var1 var1.c
:~$ ./var1
a = 10
b = 361680176
a + b = 361680186

Observe:

- A compilação não emitiu avisos e nem terminou com erro.

- A variável b (não inicializada) avaliou um valor qualquer.

Por que isso aconteceu?

Ao ser declarada, a variável recebeu um endereço e um tipo (int). Como não foi inicializada, seu valor será o que quer que esteja nos 4 bytes da memória a partir do endereço designado para b.

Por exemplo:

a -> 0x7ffd45c33fdc: 0a 00 00 00 (10)
b -> 0x7ffd45c33fd8: 3a cd 8e 15 (361680176) <-- LIXO!

Como evitar esse erro?

A primeira (e mais óbvia) opção é não se esquecer de atribuir um valor à variável antes de usá-la, mas a compilação poderia ter sido feita com as opções de avisos:

- -Wall: Habilita todos os avisos sobre problemas evitáveis no código.

- -Wextra: Habilita avisos adicionais.

- -Werror: Trata todos os avisos como erros fatais.

Exemplo:

:~$ gcc -o var1 var1.c -Wall -Wextra -Werror
var1.c: In function ‘main’:
var1.c:10:5: error: ‘b’ is used uninitialized [-Werror=uninitialized]
10 | printf("b = %d\n", b);
| ^~~~~~~~~~~~~~~~~~~~~
var1.c:7:9: note: ‘b’ was declared here
7 | int b; /* Variável declarada, mas não inicializada! */
| ^
cc1: all warnings being treated as errors

5.2.2. Atributos das variáveis

Toda variável tem como atributos…

- Um nome (identificador);
- Um valor associado;
- O endereço do valor associado.

O nome da variável expressa o seu valor:

int a = 21;
int b = a; // O valor de 'b' é o valor de 'a' (21).
int c = a + b; // O valor de 'c' é 21+21 (42).

Para que uma variável expresse o endereço do valor associado, nós utilizamos o operador unário & antes de seu nome:

int a = 123;

printf("Valor de a : %d", a);
printf("Endereço de a: %p", &a); // Imprime o endereço do dado em 'a'.

O tamanho do tipo da variável é expresso com o operador sizeof(VAR):

int a = 10;
int b = sizeof(a); // O valor de 'b' será 4 (bytes).

O tipo do valor expresso por sizeof é size_t, um apelido para unsigned long.

5.2.3. Escopo de variáveis

- Toda variável declarada numa função só é visível na própria função (escopo local).

- Toda variável declarada fora de funções é visível em todo o programa (escopo global).

Exemplo (var3.c):

#include <stdio.h>

int c = 17; // Variável global.

// As variáveis 'num' e 'b' são locais à função 'soma10'.
int soma10(int num) {
int b = 10;
return b + num;
}

int main(void) {

int a = 25; // A variável 'a' é local à função 'main'.

// Isso causará um erro na compilação!
printf("a = %d\nb = %d\nc = %d\nnum = %d\n", a, b, c, num);

return 0;
}

5.3. Ponteiros

Antes de qualquer coisa, os ponteiros são simplesmente variáveis, embora possuam algumas particularidades:

- São declarados com o operador de derreferência (ou indireção) *NOME.

- Recebem endereços como valores.

- Seus tipos são suas unidades aritméticas.

- Seus nomes expressam os endereços associados.

- Quando derreferenciados (*NOME), expressam os valores nos endereços.

Como ponteiros são associados a endereços, isso lhes dá algumas propriedades especiais quando participam de operações aritméticas, mas isso será visto nas próximas aulas.

5.3.1. Declaração do ponteiro

int a = 25;
int *pa = &a; // O valor de 'pa' será o endereço de 'a'.
int b = *pa; // O valor de 'b' será o valor no endereço em 'pa'.

5.4. Perguntas da aula ao vivo

As anotações foram feitas pelos alunos.

5.4.1. A cada execução do programa, os endereços das variáveis mudam?

Sim, porque o espaço de endereços é designado pelo sistema operacional assim que o processo para executar o programa é criado. Além disso, variáveis locais são armazenadas na pilha do espaço de endereços à medida em que as funções de que elas fazem parte são chamadas.

5.4.2. O ponto da declaração de uma variável global faz diferença?

Sim, toda variável tem que ser declarada antes do ponto no código em que é utilizada.

5.5. Exercícios

5.5.1. Pesquise e responda

- Existe uma forma para alterar uma variável em uma função a partir de outra função: como fazer isso?

- Por que as variáveis de uma função, em princípio, são locais à própria função?

- Se o valor associado a um ponteiro é um endereço, o que termos com a avaliação de &NOME_DO_PONTEIRO?

5.5.2. Analise o código, pesquise e responda

Este é mais um "Olá, mundo":

#include <stdio.h>

char *msg = "Salve, simpatia!";

int main(void) {
puts(msg);
return 0;
}

Se ponteiros recebem endereços como valores, por que eu fiz a atribuição de uma string e o meu programa funcionou?

5.5.3. Compile, analise e demonstre

#include <stdio.h>

int main(void) {

int a = 0;
int b = 875569217;
int c = 1280655661;
int d = 1129071171;

char *p = (char *)&d;

int i = 0;
while (*(p + i) != '\0') {
putchar(*(p + i));
i++;
}
putchar('\n');

// printf("%p\n%p\n%p\n%p\n", &d, &c, &b, &a);

return 0;
}

- Como o código funciona?

- O que estou tentando imprimir?

- Com o printf comentado, eu consigo imprimir o que eu quero?

- Se eu tirar o comentário, eu tenho a impressão que eu quero?

- Por que acontece essa diferença de comportamento?

- Como imprimir corretamente apenas o que eu quero?

5.5.4. Pesquise e responda

- Para que serve e como usar a função putchar?

- Quando e por que utilizar putchar('\n') em vez de puts("")?

- Como funciona a estrutura de repetição while?

- Para que servem os especificadores de formato %zu e %p?

5.5.5. Desafio: quadrado de um número

Crie um programa que peça a digitação de um número inteiro ao usuário e imprima o quadrado desse número.

Requisitos:

- O programa deve testar se o número digitado é um inteiro válido.

- Se não for, o programa deve terminar com uma mensagem enviada para a saída padrão de erros (stderr) e estado de término 1.

Dicas:

- Utilize a função scanf para ler o número digitado.

- Existe uma variante do printf que possibilita imprimir na saída de erros em vez da saída padrão.

- Existe um cabeçalho que define constante com os limites dos tipos inteiros.

6. Estruturas de controle de fluxo

Vídeo desta aula

6.1. O que são estruturas de controle?

Construtos da linguagem que alteram a ordem normal da execução do programa que, em princípio, seria linha a linha de cima para baixo.

Construtos: são os elementos fundamentais da sintaxe e da semântica de uma linguagem.

6.1.1. Controle de fluxo

A alteração do fluxo normal de execução pode ser produzida por estruturas e alguns construtos da linguagem C.

Estruturas de repetição

- Loop for
- Loop while
- Loop do..while

Estruturas de decisão

- Estrutura if..else if..else

- Estrutura switch..case

Construtos de desvio incondicional

- return: Finaliza a execução de uma função e retorna um valor.

- break: Interrompe a execução de um switch ou de um loop.

- continue: Salta para a próxima iteração de um loop.

- goto <rótulo>: Salta para um rótulo definido como rótulo:.

O goto torna o código confuso e difícil de entender e manter, levando a um estilo conhecido como "código espaguete", o que deve ser evitado a todo custo, pois isso vai contra os conceitos que definem um código como estruturado!

6.1.2. O loop 'for'

O loop for uma estrutura de repetição com sintaxe compacta que combina a inicialização de uma variável, a condição de continuidade das repetições e a atualização do valor associado à variável em uma expressão tripla, possibilitando a repetição controlada de um bloco de código um número definido de vezes.

Com o loop for, as repetições são controladas pela iterações com o valor associado a uma variável.

Iterar: da álgebra, o termo significa valer-se do resultado de uma equação, obtido através de cálculos sucessivos.

Sintaxe

for (INICIALIZAÇÃO; CONDIÇÂO; ALTERAÇÃO) {
BLOCO DE INSTRUÇÕES...
}

Quando houver apenas uma instrução…

for (INICIALIZAÇÂO; CONDIÇÃO; ALTERAÇÃO) INSTRUÇÃO;

for (INICIALIZAÇÂO; CONDIÇÃO; ALTERAÇÃO)
INSTRUÇÃO;

Loop for infinito…

for (;;) {
BLOCO DE INSTRUÇÕES...
}

Um loop infinito pressupõe a definição de uma condição de parada no BLOCO DE INSTRUÇÕES e o uso de um construto de desvio incondicional, geralmente, o break.

Exemplo do vídeo

#include <stdio.h>

int main(void) {

for (int i = 0; i < 10; i++) {
printf("%d - %d\n", i, i * 10);
}

return 0;
}

Notas

- A declaração de uma variável i, na primeira expressão, faz com que ela seja visível apenas no bloco de instruções.

- Ao término das repetições, a variável deixa de existir.

- A variável i também poderia ser declarada fora da estrutura for.

- O operação de pós incremento <var>++, na terceira expressão, avalia var e soma 1 ao seu valor.

- A variável i poderia ser manipulada de qualquer outra forma na terceira expressão.

Compilando e executando…

:~$ gcc exemplo.c
:~$ ./a.out
0 - 0
1 - 10
2 - 20
3 - 30
4 - 40
5 - 50
6 - 60
7 - 70
8 - 80
9 - 90

6.1.3. Loop 'while'

O loop while repete a execução de um bloco de instruções enquanto a sua expressão de controle continuar avaliando como verdadeira ou qualquer valor diferente de 0. Como a continuidade dos ciclos é condicionada à verdade da expressão de controle, o loop while pode ser classificado como um loop condicional.

Sintaxe

while (CONDIÇÃO) {
BLOCO DE INSTRUÇÕES...
}

Quando houver apenas uma instrução…

while (CONDIÇÃO) INSTRUÇÃO;

while (CONDIÇÃO)
INSTRUÇÃO;

Loop while infinito…

while (1) {
BLOCO DE INSTRUÇÕES...
}

IMPORTANTE!

Todas as estruturas de repetição podem ser usadas sem a definição de um bloco de instruções ou apenas uma instrução, mas isso só faz sentido em loops onde é possível avaliar condições através de uma expressão, como é o caso dos loops while e do..while.

Por exemplo, este loop é muito utilizado para limpar o conteúdo do buffer de entrada após a digitação de uma entrada do usuário:

int c;
while ((c = getchar()) != '\n' && c != EOF);

Aqui, o objetivo é apenas ler os bytes restante no buffer de entrada, o que pode ser feito na própria expressão de controle do while.

Exemplo do vídeo

#include <stdio.h>

int main(void) {

int i = 0;
while (i < 10) {
printf("%d", i);
i += 2;
}

return 0;
}

Nota

Este exemplo reproduz com o loop while o mesmo comportamento de um loop for, mas sem tornar a variável i local ao bloco de instruções:

- int i = 0; - Inicialização da variável que será iterada.

- i < 10 - Condição de continuidade.

- i += 2; - Alteração da variável i.

Compilando e executando…

:~$ gcc exemplo.c
:~$ ./a.out
0
2
4
6
8

6.1.4. Loop 'do..while'

Enquanto o loop while avalia sua expressão de controle antes de executar o bloco de instruções, o loop do..while tem que executar o bloco de instruções pelo menos uma vez antes de avaliar a expressão de controle: fora isso, os funcionamentos são idênticos.

Sintaxe

do {
BLOCO DE INSTRUÇÕES...
} while (CONDIÇÃO);

Quando houver apenas uma instrução…

do INSTRUÇÃO;
while (CONDIÇÃO);

do
INSTRUÇÃO;
while (CONDIÇÃO);

Loop do..while infinito…

do {
BLOCO DE INSTRUÇÕES...
} while (1);

Exemplo do vídeo

#include <stdio.h>

int main(void) {

int i = 10;
do
printf("%d", i);
i--;
while (i > 0);

return 0;
}

Notas

- Ainda estamos reproduzindo o comportamento do for.

- A operação de pós decremento <var>-- avalia var antes de subtrair 1 de seu valor.

Compilando e executando…

:~$ gcc exemplo.c
:~$ ./a.out
10
9
8
7
6
5
4
3
2
1

6.1.5. Estrutura de decisão 'if..else if..else'

A estrutura if..else if..else (ou simplesmente if) possibilita a execução de um bloco de instruções caso a avaliação de um expressão resulte em verdadeira ou em um valor diferente de 0.

Sintaxe

if (CONDIÇÃO_1) {
BLOCO EXECUTADO SE CONDIÇÃO_1 FOR VERDADEIRA...
} else if (CONDIÇÃO_2) {
BLOCO EXECUTADO SE CONDIÇÃO_2 FOR VERDADEIRA...
} else if (CONDIÇÃO_N) {
BLOCO EXECUTADO SE CONDIÇÃO_N FOR VERDADEIRA...
} else {
BLOCO EXECUTADO SE NENHUMA DAS CONDIÇÔES FOR VERDADEIRA...
}

Quando houver apenas uma instrução nos blocos de instruções…

if (CONDIÇÃO_1)
INSTRUÇÃO SE CONDIÇÃO_1 FOR VERDADEIRA...
else if (CONDIÇÃO_2)
INSTRUÇÃO SE CONDIÇÃO_2 FOR VERDADEIRA...
else if (CONDIÇÃO_N)
INSTRUÇÃO SE CONDIÇÃO_N FOR VERDADEIRA...
else
INSTRUÇÃO SE NENHUMA DAS CONDIÇÔES FOR VERDADEIRA...

Notas

- Apenas o bloco if é obrigatório.

- É possível escrever quantos blocos else if forem necessários.

- Só podemos escrever um bloco else, se necessário.

- A estrutura if termina assim que um dos seus blocos é executado.

- Eu prefiro me referir aos blocos de instruções como consequências.

Exemplo do vídeo

#include <stdio.h>

int main(void) {

for (int i = 0; i <= 20; i++) {
if (i % 2 == 0) {
printf("%d é par\n", i);
} else if (i % 15 == 0) {
printf("%d é ímpar e divisível por 3 e 5\n", i);
} else {
printf("%d é ímpar\n", i);
}
}
return 0;
}

Compilando e executando…

:~$ gcc exemplo.c
:~$ ./a.out
0 é par
1 é ímpar
2 é par
3 é ímpar
4 é par
5 é ímpar
6 é par
7 é ímpar
8 é par
9 é ímpar
10 é par
11 é ímpar
12 é par
13 é ímpar
14 é par
15 é ímpar e divisível por 3 e 5
16 é par
17 é ímpar
18 é par
19 é ímpar
20 é par

Dica: 'else if' abreviado

A escrita do bloco else if pode ser um pouco verbosa demais para quem escreve muitos scripts em Bash, que usa elif. Não existe uma abreviação nativa em C, mas nós podemos implementá-la com uma macro:

#define elif else if

Por exemplo:

#include <stdio.h>

#define elif else if

int main(void) {

for (int i = 1; i <= 15; i++) {
if (!(i % 15)) {
puts("FizzBuzz");
} elif (!(i % 3)) {
puts("Fizz");
} elif (!(i % 5)) {
puts("Buzz");
} else {
printf("%d\n", i);
}
}
}

Compilando e executando…

:~$ gcc -Wall fizzbuzz.c
:~$ ./a.out
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

6.1.6. Estrutura 'switch..case'

A estrutura switch..case possibilita selecionar a execução de um ou mais blocos de instruções diferentes com base na correspondência entre o valor de uma expressão e os valores especificados nas cláusulas case.

Cláusulas: seções de uma estrutura sintática que definem condições ou comportamentos específicos dentro da totalidade do comando de que fazem parte.

Sintaxe

switch (CONTROLE) {
case CONST_1:
BLOCO DE INSTRUÇÕES...
[break;]
case CONST_2:
BLOCO DE INSTRUÇÕES...
[break;]
case CONST_N:
BLOCO DE INSTRUÇÕES...
[break;]
default:
BLOCO DE INSTRUÇÕES...
}

Notas

- A expressão CONTROLE deve avaliar um tipo compatível com int.

- Os valores nas cláusulas case devem ser expressões constantes ou expressões ou valores que possam ser resolvidos em tempo de compilação.

- Sejam quais forem os valores nas cláusulas case, eles devem ser do mesmo tipo da expressão CONTROLE.

Exemplo do vídeo

#include <stdio.h>

int main(void) {

int flag;

for (int i = 1; i <= 20; i++) {
flag = 0;
flag += (i % 2) ? 0 : 1;
flag += (i % 3) ? 0 : 2;

switch (flag) {
case 1:
printf("%d é divisível por 2\n", i);
break;
case 2:
printf("%d é divisível por 3\n", i);
break;
case 3:
printf("%d é divisível por 2 e 3\n", i);
break;
default:
printf("%d\n", i);
}
}

return 0;
}

Compilando e executando…

:~$ gcc exemplo.c
:~$ ./a.out
1
2 é divisível por 2
3 é divisível por 3
4 é divisível por 2
5
6 é divisível por 2 e 3
7
8 é divisível por 2
9 é divisível por 3
10 é divisível por 2
11
12 é divisível por 2 e 3
13
14 é divisível por 2
15 é divisível por 3
16 é divisível por 2
17
18 é divisível por 2 e 3
19
20 é divisível por 2

Bônus: expressão condicional (ternária)

A linguagem C implementa a avaliação condicional de expressões na forma de uma expressão ternária (uma expressão com 3 termos):

CONDIÇÃO ? EXPRESSÃO_SE_VERDADEIRO : EXPRESSÃO_SE_FALSO

Aqui, se a expressão CONDIÇÃO avaliar verdadeiro ou um valor diferente de 0, o valor de toda a expressão ternária será o valor avaliado no seu segundo termo (depois de ?); se avaliar falso ou igual a 0, a expressão ternária terá o valor avaliado no seu terceiro termo (depois de :).

6.2. Exercícios

6.2.1. Desafio: caracteres alfanuméricos ASCII

Crie um programa que produza a seguinte saída:

======================================================
DEC OCT HEXA CHR | DEC OCT HEXA CHR | DEC OCT HEXA CHR
-----------------+------------------+-----------------
48 60 0x30 0 | 65 101 0x41 A | 97 141 0x61 a
49 61 0x31 1 | 66 102 0x42 B | 98 142 0x62 b
50 62 0x32 2 | 67 103 0x43 C | 99 143 0x63 c
51 63 0x33 3 | 68 104 0x44 D | 100 144 0x64 d
52 64 0x34 4 | 69 105 0x45 E | 101 145 0x65 e
53 65 0x35 5 | 70 106 0x46 F | 102 146 0x66 f
54 66 0x36 6 | 71 107 0x47 G | 103 147 0x67 g
55 67 0x37 7 | 72 110 0x48 H | 104 150 0x68 h
56 70 0x38 8 | 73 111 0x49 I | 105 151 0x69 i
57 71 0x39 9 | 74 112 0x4a J | 106 152 0x6a j
| 75 113 0x4b K | 107 153 0x6b k
| 76 114 0x4c L | 108 154 0x6c l
| 77 115 0x4d M | 109 155 0x6d m
| 78 116 0x4e N | 110 156 0x6e n
| 79 117 0x4f O | 111 157 0x6f o
| 80 120 0x50 P | 112 160 0x70 p
| 81 121 0x51 Q | 113 161 0x71 q
| 82 122 0x52 R | 114 162 0x72 r
| 83 123 0x53 S | 115 163 0x73 s
| 84 124 0x54 T | 116 164 0x74 t
| 85 125 0x55 U | 117 165 0x75 u
| 86 126 0x56 V | 118 166 0x76 v
| 87 127 0x57 W | 119 167 0x77 w
| 88 130 0x58 X | 120 170 0x78 x
| 89 131 0x59 Y | 121 171 0x79 y
| 90 132 0x5a Z | 122 172 0x7a z
======================================================

6.2.2. Desafio: dias da semana

Pesquise e crie um programa que receba um número inteiro entre 1 e 7 como argumento e imprima o nome do dia correspondente da semana.

Requisitos:

- Utilize uma estrutura switch para resolver o problema.

- Se o número não estiver no intervalo de 1 a 7, o programa deve imprimir Número inválido.

- Se não houver argumentos, a mensagem deve ser Número incorreto de argumentos.

- Em ambos os casos anteriores, o programa deve terminar com erro.

- Se o argumento recebido for -h, o programa deve exibir uma mensagem de uso e terminar com sucesso.

Dicas:

Para receber argumentos, os parâmetros da função main devem ser (nesta ordem):

- int argc - Número de argumentos recebidos.

- char **argv - Vetor de palavras (strings) recebidas como argumentos.

Cada palavra no vetor argv pode ser acessada com argv[N], onde N é o índice do argumento acessado.

O primeiro argumento (argv[0]) sempre será a invocação do programa.

Todo argumento é recebido como uma string, inclusive os numéricos.

6.2.3. Desafio: conversão de base 10 para 16 e vice-versa

Crie um programa que receba um inteiro como argumento e imprima…

- Se o número começar com 0x, o número convertido da base 16 para a base 10.

- Se começar com um inteiro diferente de 0, o número convertido da base 10 para 16.

- Se começar com 0 ou tiver letras inválidas para um número em hexa, uma mensagem de erro.

7. Vetores

Vídeo desta aula

7.1. O que são vetores

Vetores são estruturas de dados que agrupam valores do mesmo tipo na forma de listas.

- Cada valor em um vetor é um elemento.

- O nome do vetor é o endereço do seu primeiro elemento.

- Cada elemento é identificado por um índice escrito entra colchetes após o nome.

- A representação do índice, incluindo os colchetes, é chamada de subscrito.

Embora os termos sejam intercambiáveis, um vetor é, mais precisamente, uma matriz unidimensional, ou seja, uma lista de valores. Já o termo matriz e mais utilizado quando nos referimos a um vetor cujos elementos são outros vetores, o que forma uma matriz multidimensional. Finalmente, array é só o termo em inglês para matriz.

7.1.1. Utilidade dos vetores

Quando estamos lidando com conjuntos de dados relacionados, é preferível trabalhar com vetores em vez de variáveis, porque isso possibilita uma melhor organização e estruturação dos dados, além de facilitar a sua manipulação.

Exemplo:

float nota1 = 7.5;
float nota2 = 9.0;
float nota3 = 8.3;
float nota4 = 8.6;

float soma = nota1 + nota2 + nota3 + nota4;
float media = soma / 4;

Com vetores…

float notas[4];

notas[0] = 7.5;
notas[1] = 9.0;
notas[2] = 8.3;
notas[3] = 8.6;

float soma = 0;
for (int i = 0; i < 4; i++) soma += notas[i];

float media = soma / 4;

Nós também poderíamos inicializar o vetor na declaração:

float notas[4] = {7.5, 9.0, 8.3, 8.6};

float soma = 0;
for (int i = 0; i < 4; i++) soma += notas[i];

float media = soma / 4;

As diferenças entre os exemplos com vetores e com variáveis separadas são…

Semanticamente:

- Uma variável expressa e dá significado a um valor.

- Um vetor expressa e dá significado a um conjunto relacionado de valores.

Sintaticamente:

- Para obter o valor associado à variável, nós avaliamos o nome da variável.

- Para obter o valor do primeiro elemento de um vetor, nós derreferenciamos o nome do vetor (*<vetor>).

- Para obter o endereço do valor associado a uma variável, nós usamos &<var>.

- Para obter o endereço do primeiro elemento do vetor, nós avaliamos o nome do vetor.

- A operação &<vetor> avalia o endereço de todo o conjunto de elementos do vetor.

- Para obter os valores dos demais elementos, nós somamos *(<vetor> + índice).

- A linguagem C implementa uma notação mais semântica para acesso aos valores dos elementos na forma de um subscrito para a escrita de um índice após o nome do vetor (<vetor>[índice]).

Pragmaticamente:

Como os índices são numéricos, nós podemos iterar os elementos através de operações com expressões que avaliem valores compatíveis com o tipo int. Isso facilita muito o acesso e a manipulação de valores, especialmente em situações onde temos que lidar com grandes quantidades de dados.

Quantidade de dados não é, necessariamente, a mesma coisa que volume de dados!

7.1.2. Declaração e inicialização de vetores

Para declarar um vetor, nós definimos os tipos dos seus elementos, seu nome e um subscrito (um par de colchetes) contendo a quantidade de elementos que o vetor receberá:

float notas[4];

Enquanto não forem definidos, os valores nos elementos será o que houver na quantidade de bytes reservada a partir do endereço do vetor (lixo de memória).

Nós também podemos inicializar um vetor na sua declaração:

float notas[4] = {7.5, 9.0, 8.3, 8.6};

7.1.3. Tamanho de um vetor em tempo de compilação

Quando um vetor é inicializado sem a definição da quantidade de elementos em seu subscrito, o compilador obtém essa quantidade da lista de valores:

float notas[] = {7.5, 9.0, 8.3, 8.6}; // 4 elementos...

Importante!

Isso só vale para a inicialização de vetores nas suas declarações:

float notas[]; // Errado!

7.1.4. Inicialização parcial

Quando a quantidade de elementos é definida na inicialização do vetor, a ausência de valores correspondentes na lista de elementos é preenchida com zeros:

int num[5] = {1, 2}; // => {1, 2, 0, 0, 0}

O que possibilita, inclusive, inicializar um vetor zerado:

int num[5] = {0}; // => {0, 0, 0, 0, 0}

7.1.5. Acesso aos elementos de um vetor

O acesso para manipulação dos elementos de um vetor é feita através de seus índices, levando em conta que o primeiro elemento sempre terá índice 0:

int num_list[5];
/* ... */
num_list[0] = 23; // Definição do valor do elemento 0 (primeiro elemento).
/* ... */
num_list[4] = 19; // Alteração do elemento 4 (último elemento).

7.1.6. Iteração pelos elementos

Nós podemos iterar os elementos de um vetor em loops:

#include <stdio.h>

int main() {

int v[5] = {0};

v[0] = 23;
v[4] = 19;

for (int i = 0; i < 5; i ++)
printf("v[%d]: %d\n", i, v[i]);

putchar('\n');

for (int i = 0; i < 5; i ++) {
v[i] = i + 1;
printf("v[%d]: %d\n", i, v[i]);
}

return 0;
}

Compilando e executando…

:~$ testes.c
:~$ a.out
v[0]: 23
v[1]: 0
v[2]: 0
v[3]: 0
v[4]: 19

v[0]: 1
v[1]: 2
v[2]: 3
v[3]: 4
v[4]: 5

7.1.7. Vetores e funções

Vetores não podem ser passados para funções, mas os seus endereços podem:

#include <stdio.h>

void print_array(int v[], int size) {
for (int i = 0; i < size; i++)
printf("v[%d] => %d\n", i, v[i]);
}

int main() {

int vetor[5];

for (int i = 0; i < 5; i ++) vetor[i] = i + 1;

print_array(vetor, 5);
}

Compilando e executando…

:~$ testes.c
:~$ a.out
v[0] => 1
v[1] => 2
v[2] => 3
v[3] => 4
v[4] => 5

Importante! É preciso ter cuidado com a quantidade de elementos de um vetor, pois o gcc não emite avisos nem erros quando tentamos ler ou escrever dados fora dos limites do vetor, o que pode causar muitos problemas.

Ainda no exemplo, observe que a função print_array tem o parâmetro int v[], que se parece com a notação de um vetor, mas esta é só uma sintaxe utilizada na linguagem C para representar que o argumento esperado é o endereço de um valor de determinado tipo (no caso, int) e que esse endereço, semanticamente, será o de um vetor.

Essa sintaxe tem exatamente o mesmo efeito de um ponteiro como parâmetro:

void print_array(int *v, int size) {
for (int i = 0; i < size; i++)
printf("v[%d] => %d\n", i, v[i]); // Notação de vetor...
}

Isso também vale para todas as expressões que utilizem a notação de vetores, porque nome[índice] é uma representação semântica que será interpretada como *(nome + índice) pelo compilador.

Logo, a função também poderia ser escrita assim:

void print_array(int *v, int size) {
for (int i = 0; i < size; i++)
printf("v[%d] => %d\n", i, *(v + i)); // Notação de ponteiro...
}

7.1.8. Relação entre vetores e ponteiros

Um equívoco muito comum, é a ideia de que vetores "decairiam" para ponteiros. Isso simplesmente não é verdade: ponteiros são variáveis que recebem endereços como valores, enquanto vetores são, e sempre serão, endereços, onde quer que seus nomes sejam utilizados.

#include <stdio.h>

int main() {

int v[4] = {0};
int *p = v;

printf("vetor : %p\n", v);
printf("&vetor : %p\n", &v);
printf("ponteiro : %p\n", p);
printf("&ponteiro: %p\n", &p);

return 0;
}

Compilando e executando…

:~$ gcc testes.c
:~$ ./a.out
vetor : 0x7fff16832290 <-- Endereço de v[0]
&vetor : 0x7fff16832290 <-- Endereço expresso por v
ponteiro : 0x7fff16832290 <-- Valor no ponteiro (endereço expresso por v)
&ponteiro: 0x7fff16832288 <-- Endereço do valor no ponteiro

Nos parâmetros de funções, TIPO nome[] é uma sintaxe alternativa para TIPO *nome e ambas representam a declaração de um ponteiro que receberá, como sempre, um endereço, não uma cópia de um vetor.

Do mesmo modo, o uso da notação de vetores em expressões é apenas uma sintaxe alternativa para operações realizadas com endereços: é por isso que vetor[i] é o mesmo que *(vetor + i).

7.1.9. Vetores de caracteres (strings)

Strings são vetores de caracteres (cadeias de bytes) terminados com o caractere nulo (\0).

Portanto…

char string[] = "banana";

É o mesmo que…

char string[] = {'b', 'a', 'n', 'a', 'n', 'a', '\0};

7.1.10. Mais diferenças entre vetores e ponteiros

Observe o exemplo…

#include <stdio.h>

int main() {

char vstr[] = "bacana";
char *pstr = "calado";

printf("%p --> %s\n", vstr, vstr);
printf("%p --> %s\n", pstr, pstr);

return 0;
}

Essas strings são diferentes:

char vstr[] = "bacana";
char *pstr = "calado";

Enquanto o vetor vstr recebeu uma lista de caracteres, o ponteiro pstr recebeu o endereço para um dado numa região do espaço de memória, atribuído pelo sistema operacional ao programa, chamada de .rodata, onde são armazenados os dados que não podem ser alterados (read only).

Isso pode ser verificado despejando o conteúdo do binário do programa com o utilitário readelf:

:~$ readelf -x .rodata a.out

Despejo máximo da secção ".rodata":
0x00002000 01000200 63616c61 646f0025 70202d2d ....calado.%p --
0x00002010 3e202573 0a00 > %s..

Veja que apenas a string calado aparece nesta seção do binário.

A string bacana, por sua vez poderá ser encontrada na seção .text do binário, que é onde nós encontramos o código da função main:

:~$ readelf -x .text a.out

Despejo máximo da secção ".text":
0x00001050 31ed4989 d15e4889 e24883e4 f0505445 1.I..^H..H...PTE
0x00001060 31c031c9 488d3dce 000000ff 154f2f00 1.1.H.=......O/.
0x00001070 00f4662e 0f1f8400 00000000 0f1f4000 ..f...........@.
0x00001080 488d3d91 2f000048 8d058a2f 00004839 H.=./..H.../..H9
0x00001090 f8741548 8b052e2f 00004885 c07409ff .t.H.../..H..t..
0x000010a0 e00f1f80 00000000 c30f1f80 00000000 ................
0x000010b0 488d3d61 2f000048 8d355a2f 00004829 H.=a/..H.5Z/..H)
0x000010c0 fe4889f0 48c1ee3f 48c1f803 4801c648 .H..H..?H...H..H
0x000010d0 d1fe7414 488b05fd 2e000048 85c07408 ..t.H......H..t.
0x000010e0 ffe0660f 1f440000 c30f1f80 00000000 ..f..D..........
0x000010f0 f30f1efa 803d1d2f 00000075 2b554883 .....=./...u+UH.
0x00001100 3dda2e00 00004889 e5740c48 8b3dfe2e =.....H..t.H.=..
0x00001110 0000e829 ffffffe8 64ffffff c605f52e ...)....d.......
0x00001120 0000015d c30f1f00 c30f1f80 00000000 ...]............
0x00001130 f30f1efa e977ffff ff554889 e54883ec .....w...UH..H..
0x00001140 10c745f1 62616361 c745f461 6e610048 ..E.baca.E.ana.H <-- Aqui!
0x00001150 8d05ae0e 00004889 45f8488d 55f1488d ......H.E.H.U.H.
0x00001160 45f14889 c6488d05 9f0e0000 4889c7b8 E.H..H......H...
0x00001170 00000000 e8b7feff ff488b55 f8488b45 .........H.U.H.E
0x00001180 f84889c6 488d0580 0e000048 89c7b800 .H..H......H....
0x00001190 000000e8 98feffff b8000000 00c9c3 ...............

As seções do binário e os segmentos do layout de memória serão assunto de uma aula futura.

As consequências disso, são:

- Os caracteres de calado não poderão ser alterados por aritmética de ponteiros.

- A tentativa de alterar dados em .rodata não resultará em erros de compilação, mas causará falha de segmentação (escrita em segmentos não autorizados da memória).

- Enquanto o endereço em pstr será de um dado em .rodata, o endereço de vstr[0] estará na pilha do espaço de memória, onde alterações são permitidas.

A compilação e execução do exemplo deixa isso bem claro:

:~$ gcc testes.c
:~$ a.out
0x7ffc25b8c0b1 --> bacana <-- endereço na pilha
0x558add3cc004 --> calado <-- endereço em .rodata

7.2. Exercícios

7.2.1. Desafio: Função 'printarrayshift'

Dado o programa abaixo:

#include <stdio.h>

int main(void) {
int vetor[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int desloca = ???; // <--- inicializar com o valor do deslocamento!

print_array_shift(/* ??? */);

return 0;
}

Escreva a função print_array_shift de acordo com essa descrição:

PROTÓTIPO:
void print_array_shift(int *arr, int size, int shift);

DESCRIÇÃO:
Desloca para a esquerda os elementos do vetor 'arr', de tamanho 'size',
em 'shift' posições e imprime os valores na sequência resultante.

No teste, programa deverá imprimir no terminal:

:~$ ./a.out
6 7 8 9 10 1 2 3 4 5

7.2.2. Desafio: Função 'arrayshift'

Dado o programa abaixo:

#include <stdio.h>

int main(void) {
int vetor[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int desloca = ???; // <--- inicializar com o valor do deslocamento!

array_shift(/* ??? */);

/* Rotina de impressão do resultado do deslocamento */

return 0;
}

Crie a função array_shift de acordo com a descrição abaixo:

PROTÓTIPO:
void array_shift(int *arr, int size, int shift);

DESCRIÇÃO:
Desloca para a esquerda os elementos do vetor 'arr', de tamanho 'size',
em 'shift' posições.

A saída do teste deve ser a mesma do desafio anterior.

7.2.3. Desafio: Função 'arrayreverse'

Dado o programa abaixo:

#include <stdio.h>

int main(void) {
int vetor[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

/* Rotina da impressão do vetor original */

array_reverse(/* ??? */);

/* Rotina de impressão do vetor invertido */

return 0;
}

Escreva a função array_reverse de acordo com a descrição abaixo:

PROTÓTIPO:
void array_reverse(int *arr, int size);

DESCRIÇÃO:
Inverte a ordem dos elementos do vetor 'arr'.

O teste deverá imprimir…

:~$ ./a.out
1 2 3 4 5 6 7 8 9 10
10 9 8 7 6 5 4 3 2 1

8. Vetores, ponteiros e strings

Vídeo desta aula

Parte do conteúdo desta aula foi antecipado nas anotações da aula 6!

8.1. Relações entre vetores e ponteiros

Embora sejam conceitos totalmente distintos, existem relações importantes entre vetores e ponteiros que nós precisamos conhecer.

8.1.1. Notações de acesso

Tanto ponteiros quanto vetores (e seus elementos) podem ser acessados de duas formas…

Acesso | Aritmética | Subscrito
Valor/Elemento | *(nome + índice) | nome[índice]
Endereço | nome + índice | &nome[índice]

Exemplo:

char v[] = {65, 66, 67, 68, 69, 0}; // Vetor de caracteres (bytes)
char *ptr = "FGHIJ"; // Ponteiro para uma string em .rodata (read only)

Impressão do endereço de um caractere:

// Impressões do vetor...
printf("v[2] : %c (%d) --> &v[2]: %p\n", v[2], v[2], &v[2]);
printf("*(v + 2): %c (%d) --> v + 2: %p\n", *(v + 2), *(v + 2), v + 2);

// Impressões do ponteiro...
printf("p[2] : %c (%d) --> &p[2]: %p\n", p[2], p[2], &p[2]);
printf("*(p + 2): %c (%d) --> p + 2: %p\n", *(p + 2), *(p + 2), p + 2);

Saída do programa:

v[2] : C (67) --> &v[2]: 0x7ffda53a16c4
*(v + 2): C (67) --> v + 2: 0x7ffda53a16c4
p[2] : H (72) --> &p[2]: 0x562d0c93100a
*(p + 2): H (72) --> p + 2: 0x562d0c93100a

8.1.2. Vetores e funções

Observe esta função:

int array_sum(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++)
sum += arr[i]; // arr é um ponteiro, não um vetor!
return sum;
}

- Funções não podem receber vetores como argumentos, apenas seus endereços.

- Escreve-se int arr[] apenas para indicar que o argumento esperado é o endereço de um vetor, já que arr será compilado como um ponteiro.

- Do mesmo modo, utiliza-se arr[i], na iteração, para manter a coerência semântica, já que a notação nome[índice] também pode ser utilizada com ponteiros.

Este mesmo exemplo poderia ser escrito apenas com notações de ponteiros e aritmética de ponteiros:

int array_sum(int *arr, int size) {
int sum = 0;
for (int i = 0; i < size; i++)
sum += *(arr + i); // Notação aritmética!
return sum;
}

Em ambos os casos, a função lidará com o ponteiro (arr) que recebeu como argumento o endereço de um vetor. O vetor em si, jamais será convertido em ponteiro e nem tampouco "decairá para um ponteiro"!

8.1.3. Strings são vetores de caracteres

Strings são vetores de caracteres terminados com o caractere nulo (\0).

Declaração de um vetor para receber uma string…

char str[TAMANHO];

Quando o vetor é declarado sem ser inicializado, é obrigatório definir a quantidade de elementos que o vetor receberá.

Declaração e inicialização de um vetor com uma string…

char str[] = {'b', 'a', 'c', 'a', 'n', 'a', '\0'};

Quando o vetor é inicializado, a quantidade de elementos pode ser omitida para ser definida na compilação.

A presença da terminação do caractere terminador '\0' é que define uma cadeia de caracteres como uma string, ou seja: uma cadeia de bytes que pode ser utilizada com segurança em funções que procuram o terminador para determinar o fim do processamento da cadeia de caracteres.

Declaração e inicialização de um vetor com uma string literal…

char str[] = "bacana";

Quando a string literal "bacana" é utilizada na inicialização do vetor, o compilador copia cada um de seus bytes para o vetor e inclui o terminador nulo ('\0') como último elemento.

Inicialização de um ponteiro com uma string literal…

char *ptr = "cabana";

Ponteiros são variáveis que recebem endereços como argumentos. Sendo assim, o compilador copia os bytes da string literal "cabana" e inclui o terminador nulo na seção .rodata (read only data) como um vetor de caracteres de 7 elementos e passa o endereço do primeiro byte para o ponteiro.

O endereço de uma string, que é um vetor, é o endereço de seu primeiro byte.

8.1.4. Inicializadores escalares e agregados

Ponteiros são variáveis e, portanto, requerem inicializadores escalares: no caso, especificamente endereços.

Todos esses dados são escalares e podem inicializar variáveis:

// Variável inicializada com o valor '10' (expressão constante)...
int a = 10;

// Ponteiro inicializado com valor '&a' (endereço de 'a')...
int *pa = &a;

// Ponteiro inicializado com o endereço do primeiro byte de uma string...
char *ps = "uma string literal";

Vetores, por sua vez, são estruturas que agrupam vários dados de mesmo tipo e, por isso, requerem inicializadores agregados.

Todos esses dados são agregados de valores e podem inicializar vetores…

// Vetor inicializado com uma lista de 5 valores...
int n[5] = {1, 2, 3, 4, 5};

// Vetor inicializado com 5 valores '0'...
int z[5] = {0};

// Vetor inicializado com 5 caracteres (bytes)...
char c[] = {'a', 'b', 'c', 'd', '\0'}; // Quantidade de elementos definida na compilação...

// Vetor inicializado com 7 caracteres (bytes)...
char s[] = "qwerty"; // O sétimo byte é o caractere nulo (\0 == 0x00)...

Mas, repare nessas linhas…

char *ps = "uma string literal";
char s[] = "qwerty";

As strings literais são dados agregados do tipo char *, mas podem ser utilizadas para inicializar tanto variáveis quanto vetores: tudo depende do contexto…

long a = (long)"uma string"; // Endereço da string modelado para long int.

char *p = "outra string"; // O endereço da string é do tipo 'char[N bytes]'.

char s[] = "mais uma"; // O endereço da string será o endereço do vetor,
// que só recebe os caracteres como elementos.

char f[] = {'o', 'i', '\0'}; // Equivale e inicializar com "oi".

Notas:

- Por ser uma estrutura de dados, o "tipo" de um vetor é frequentemente descrito como TIPO[TAMANHO], mas isso não é um tipo da linguagem C – é apenas uma forma utilizada para expressar a ideia de um dado que é composto por uma certa quantidade de elementos de um determinado tipo. Sendo um vetor de caracteres, uma string literal na memória, por exemplo, seria descrita como char[TAMANHO].

- Quando atribuída a um elemento escalar, como uma variável, um ponteiro ou o argumento de uma função, a string literal expressa o endereço de seu primeiro byte, ou seja, o endereço do primeiro elemento de um vetor do tipo char.

8.1.5. Vetores de comprimentos variáveis (VLA)

A alocação do espaço ocupado por um vetor é feita estaticamente em tempo de compilação, o que nos impediria de declarar e inicializar vetores com uma quantidade de elementos obtida pela avaliação de uma variável ou pelo retorno de uma função, por exemplo: isso é chamado de VLA (variable-length array).

O GCC, porém, implementa o suporte a VLA na forma de uma extensão habilitada por padrão, possibilitando a declaração de vetores com número de elementos obtidos dinamicamente, mas não a sua inicialização:

int size = 10;

int a[size]; // Permitido no GCC...
int v[size] = {0}; // Proibido!

Outros compiladores podem não suportar VLAs:

Compilador | Suporte a VLAs | Observações
MSVC (cl.exe) | Não suporta | Gera erro de compilação
GCC (Linux/Mingw-w64) | Suporta | VLAs alocados na pilha
Clang (MinGW/WSL) | Suporta | Mesmo comportamento do Linux
Clang-cl (modo MSVC) | Não suporta | Igual ao MSVC

8.2. Exercícios

8.2.1. Desafio: Função 'strblen'

Crie uma função que retorne um inteiro correspondente ao comprimento em bytes de uma string sem incluir o terminador nulo.

Exemplo de teste…

#include <stdio.h>

int main(void) {

char *str = "Olá, mundo!";

/* \042 é o caractere " em octal na tabela ASCII. */
printf("A string \042%s\042 tem %d bytes.\n", str, str_blen(????????));

return 0;
}

8.2.2. Pesquise e analise o código

No último exercício, você obteve um número de bytes maior do que a quantidade de caracteres do texto em *str. Isso acontece porque caracteres acentuados são compostos por mais de um byte (caracteres multibyte).

O código abaixo implementa a função str_mblen, para retornar o número de caracteres contando caracteres multibyte como um único caractere:

#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
#include <locale.h>

int str_mblen(const char *str) {
setlocale(LC_ALL, "");
int count = 0;
int len;

while (*str) {
len = mblen(str, MB_CUR_MAX);
if (len < 1) break;
str += len;
count++;
}

return count;
}

int main() {
const char *txt = "Olá, mundo!";

printf("Número de caracteres: %d\n", str_mblen(txt));

return 0;
}

Seu objetivo é explicar:

- Como a função str_mblen funciona?

- Como a função mblen é utilizada?

- O que é a macro MB_CUR_MAX?

- Por que foi necessário definir setlocale?

- O que está sendo testado no loop while (*str)?

8.2.3. Desafio: Palavras invertidas

Escreva uma função que inverta a ordem dos caracteres de cada palavra em uma string, mas preservando a ordem das palavras.

Exemplo:

Original --> Maria das Couves
Inversão --> airaM sad sevuoC

8.2.4. Desafio: Função 'arrayisort'

Escreva a função array_sort para organizar, em ordem crescente, os valores em um dado vetor de inteiros.

Vetor para testes…

int num[] = {3, 7, 5, 9, 3, 2, 0, 4, 1, 6, 8};

8.2.5. Desafio: codificador e decodificador ROT13

O método de codificação ROT13 desloca em 13 caracteres à frente no alfabeto cada um dos caracteres de um texto.

Exemplos…

a => n
b => o
c => p
e => r
t => g (depois de 'z', continua em 'a')

Seu objetivo é criar as funções rot13_encode e rot13_decode para, respectivamente, codificar e decodificar uma string no método ROT13.

Requisitos…

- Não alterar caracteres multibyte.

- As funções devem apenas imprimir os resultados.

Referência…

As suas funções devem se comportar como os comandos abaixo:

:~$ echo abacate | tr 'A-Za-z' 'N-ZA-Mn-za-m'
nonpngr
:~$ echo avião | tr 'A-Za-z' 'N-ZA-Mn-za-m'
nivãb
:~$ echo cvgnatn | tr 'A-Za-z' 'N-ZA-Mn-za-m'
pitanga

9. Processos e layout de memória

Vídeo desta aula

9.1. Filosofia UNIX

Doug McIlroy, que implementou o conceito de pipes no Unix, resumiu a filosofia UNIX em três princípios:

- Escreva programas que façam apenas uma coisa, mas que a façam bem feita.

- Escreva programas que trabalhem juntos.

- Escreva programas que manipulem fluxos de texto, pois esta é uma interface universal.

Se repararmos bem, esses princípios de caracterizam perfeitamente o desenvolvimento do sistema operacional Unix e a forma como ele foi projetado para ser operado.

Lembre-se…

Os sistemas operacionais parecidos com o Unix (Unix-like) podem ser descritos através de seus quatro conjuntos de softwares mais essenciais, o que também se aplica ao nosso GNU/Linux:

Componente | Unix/Unix-like | GNU/Linux
Kernel | Unix Kernel, BSD Kernel, etc… | Linux
Biblioteca C padrão | libc | glibc
Shell | sh | bash
Utilitários da base do sistema | Programas como: cat, grep, sed, etc… | GNU Coreutils

9.1.1. Primeiro princípio: especialização

Escreva programas que façam apenas uma coisa, mas que a façam bem feita.

Separação de atribuições…

No escopo do sistema como um todo, nós podemos relacionar esse princípio com a separação entre o espaço do kernel e o espaço de usuário:

- Espaço do kernel: região de memória onde o kernel do sistema operacional executa e tem acesso privilegiado a todos os recursos de hardware e software.

- Espaço do usuário: região de memória onde os processos dos programas são executados com permissões restritas e têm o acesso ao hardware intermediado pelo kernel.

Criação de programas…

Para a escrita de programas, nós temos a libc, que abstrai as chamadas às funções internas do kernel (chamadas de sistema, ou syscalls) e centenas de outras funcionalidades.

Interface padrão de operação…

Mesmo no espaço de usuário, o shell se diferencia dos demais programas da base do sistema porque, em princípio, é somente através dele que os outros programas são executados pelo usuário.

O shell é a interface padrão entre o sistema e o usuário que, através de linhas de comandos em texto, declara o que quer que seja executado.

Programas para diversas tarefas úteis…

Finalmente, o usuário tem acesso a dezenas de utilitários da base do sistema, cada um com a sua especialidade, para a realização de inúmeros tipos de tarefas.

9.1.2. Segundo princípio: modularidade

Escreva programas que trabalhem juntos.

Com a especialização, vem a necessidade de fazer com que cada componente do sistema, de acordo com suas atribuições, seja projetado para trabalhar com outros programas.

Linha de comandos…

Para operar o sistema, o usuário tem acesso a um terminal onde seus comandos poderão ser digitados e enviados para o shell:

+---------+ +----------+ +---------+ +--------+
| TECLADO | ---→ | | ---→ | SHELL | ---→ | KERNEL |
+---------+ | | +---------+ +--------+
| TERMINAL | ↑
+---------+ | | +---------------+ |
| MONITOR | ←--- | | ←--→ | NOVO PROCESSO | ←--+
+---------+ +----------+ +---------------+

Quando o shell recebe a linha de um comando, ele interpreta o que foi digitado e, se for o caso, faz uma chamada de sistema fork para que o kernel crie uma cópia (clone) de seu processo. Em seguida, no processo clonado, o shell faz uma chamada de sistema exec, para que o kernel substitua parte dos dados copiados do processo do shell pelos dados do programa que será executado.

Tarefas complexas…

Cada utilitário da base do sistema é construído segundo o conceito de interface de linha de comando, ou CLI. Isso quer dizer que, além das suas especialidades, cada um deles é capaz de trocar dados com outros programas para que o usuário, a partir de programas simples, seja capaz de realizar tarefas complexas.

Por exemplo, digamos que você tenha um arquivo com uma lista de pedidos e queira filtrar as compras realizadas em um dado mês. Você poderia utilizar o programa grep, que é especializado em localizar e imprimir linhas de texto a partir de padrões descritos por expressões regulares:

:~$ grep '03/2025' pedidos.data
"Maria das Couves", "02/03/2025", "2564", "Porta copos"
"Antônio dos Santos", "09/03/2025", "7544", "Toalha de mesa"
"João da Silva", "01/03/2025", "3762", "Jogo de 12 talheres"

Se, além das linhas impressas, você precisar ordenar o resultado pelos nomes dos clientes, o grep será insuficiente sozinho, mas você pode recorrer ao utilitário sort para processar a saída produzida pelo grep escrevendo apenas um comando:

:~$ grep '03/2025' pedidos.data | sort
"Antônio dos Santos", "09/03/2025", "7544", "Toalha de mesa"
"João da Silva", "01/03/2025", "3762", "Jogo de 12 talheres"
"Maria das Couves", "02/03/2025", "2564", "Porta copos"

9.1.3. Terceiro princípio: fluxos de texto

Escreva programas que manipulem fluxos de texto, pois esta é uma interface universal.

Como vimos, até aqui…

- 0 terminal recebe um fluxo de caracteres digitado em um teclado e envia para o shell;

- O shell interpreta o texto recebido e monta textos correspondendo aos argumentos que serão passados para as chamadas de sistema;

- O kernel executa a chamada de sistema e cria um novo processo para executar o programa que foi invocado… Mas não é só isso!

Além dos dados do programa (basicamente, o conteúdo de seu binário), o novo processo incluirá outros dados que já estavam registrados no processo do shell:

- A quantidade de palavras utilizadas para invocar o programa;

- A lista das palavras utilizadas para invocar o programa;

- Uma lista das variáveis que serão herdadas pelo novo processo;

- Uma lista de arquivos padrão que poderão ser utilizados para enviar e receber fluxos de texto.

Sendo assim…

- As palavras utilizadas para invocar o programa são chamadas de argumentos de linha de comando.

- A lista de variáveis herdadas pelo programa irão compor um ambiente de dados que poderão ser utilizados pelo programa.

- A lista de arquivos padrão para receber e enviar fluxos de caracteres para o terminal são os descritores de arquivos padrão: stdin (entrada padrão), stdout (saída padrão) e stderr (saída padrão de erros).

Mas existem mecanismos no kernel que possibilitam o desvio dos fluxos de dados padrão para outros arquivos. Assim, se eu quiser enviar a saída do meu programa para um arquivo, em vez de para o terminal, eu posso redirecionar a saída padrão (stdout, descritor de arquivos 1) para esse arquivo:

ls -l > arquivos.txt

Do mesmo modo, eu poderia desviar a saída padrão de um programa para a entrada padrão de outro programa. Para isso, o kernel precisaria criar um outro tipo de arquivo, chamado de pipe, para canalizar o fluxo de texto entre os dois programas. Na linha de comandos…

:~$ grep '03/2025' pedidos.data | sort

Nesse caso, os dois programas são executados ao mesmo tempo e, enquanto o primeiro envia dados para um pipe, o segundo lê este mesmo arquivo:

+-----------------------------+ +------+ +------+
| grep '03/2025' pedidos.data | ---> | PIPE | ---> | sort |
+-----------------------------+ +------+ +------+

Assim que o grep terminar o envio de linhas de texto, o sort será terminado.

9.1.4. A interface de linha de comando (CLI)

A filosofia Unix nos ajuda a compreender, de um modo mais amplo, como os sistemas Unix-like foram pensados para lidar com o hardware, possibilitarem a criação e execução de programas e serem operados. Especialmente quanto à operação do sistema pela linha de comandos, são as convenções da interface de linha de comandos (CLI), implementada no shell, que determinam como os programas e o sistema deverão lidar com os nossos comandos.

Um comando simples, seque este esquema geral:

[EXPORTAÇÕES] [INVOCAÇÃO] [ARGS...] [REDIRECIONAMENTO ARQUIVO]

- Exportações: uma ou mais variáveis que serão exportadas para o ambiente do processo do programa executado.

- Invocação: o caminho e o nome do programa que será executado.

- Argumentos: lista de palavras que serão passadas para o processo do programa como opções ou informações adicionais.

- Redirecionamento: através de operadores do shell, a saída do programa pode ser desviada para um arquivo ou um arquivo poderá ser informado para ser lido e processado pelo programa.

A invocação (o caminho e o nome do programa) sempre será o primeiro argumento da lista de argumentos.

Operadores de controle do shell

Quando a linha de um comando contém a invocação de mais de um programa, é necessário estabelecer como essas invocações se relacionam, o que é feito com os operadores de controle do shell:

Encadeamento incondicional (; ou \n)

O comando seguinte será executado após o término do anterior.

Encadeamento assíncrono (&)

O comando seguinte será executado em paralelo com o anterior e em segundo plano.

Encadeamento condicional "se sucesso" (&&)

O comando seguinte só será executado se, e quando, o último comando executado terminar com sucesso.

Encadeamento condicional "se erro" (||)

O comando seguinte só será executado se, e quando, o último comando executado terminar com erro.

Encadeamento por pipe (|)

Os comandos serão executados em paralelo e a saída do primeiro será canalizada para a entrada do segundo.

9.2. O que são processos

Um processo é um conjunto de estruturas de dados que o kernel utiliza para gerenciar a execução de programas. No centro dessas estruturas de dados está uma faixa virtual contínua de endereços de memória que o kernel designará para cada programa em execução. Essa faixa virtual de endereços de memória, também chamada de memória virtual ou espaço de endereços, representa toda a memória disponível a que o processo do programa terá acesso.

Além disso, o kernel disponibiliza todas as informações sobre todos os processos em execução na forma de um sistema de arquivos virtual (procfs) montado no diretório /proc. Nele, cada processo terá um subdiretório nomeado segundo seu número de identificação (PID).

9.2.1. Como programas são executados

Para o sistema operacional, executar um programa significa criar um novo processo e designar um espaço de memória para ele. Para chegar a este ponto, o programa terá que ser invocado por outro programa já em execução (processo pai, geralmente o shell) que, através de duas chamadas de sistema responsáveis por (chamada fork) criar uma duplicata do processo pai (um clone) como um novo processo e depois substituir parte dos dados no espaço de memória dessa duplicata pelos dados encontrados no binário do programa que será executado.

Aqui está um exemplo em C que mostra. simplificadamente, a execução de outro programa:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void) {
// Chamada fork...
pid_t pid = fork();

// Se o fork falhar...
if (pid < 0) {
perror("fork falhou");
exit(EXIT_FAILURE);
}

/*
Um segundo processo é criado e o código seguinte será executado
por ambos os processos (eles são idênticos!), mas com valores
de pid diferentes:

- Para o processo filho, pid == 0
- Para o processo pai, pid == PID do processo filho

Por isso temos essa estrutura if...
*/
if (pid == 0) {
// Processo filho...
printf("[Filho] PID: %d, iniciado.\n", getpid());
char *args[] = {"ls", "-l", NULL};
// Chamada exec (execvp)...
execvp(args[0], args);
// Se execvp falhar...
perror("execvp falhou");
exit(EXIT_FAILURE);
} else {
// Processo pai...
printf("[Pai] PID: %d, criou filho PID: %d\n", getpid(), pid);
printf("[Pai] Aguardando término do filho...\n");
int status;
waitpid(pid, &status, 0);
printf("[Pai] Processo filho %d terminou com status %d\n", pid, WEXITSTATUS(status));
printf("[Pai] Voltando ao controle.\n");
}

return 0;
}

Compilando e executando:

:~$ gcc -Wall exemplo.c
:~$ ./a.out
[Pai] PID: 1810185, criou filho PID: 1810186
[Pai] Aguardando término do filho...
[Filho] PID: 1810186, iniciado.
total 56
-rwxrwxr-x 1 blau blau 16280 mar 22 08:00 analise
-rw-rw-r-- 1 blau blau 1549 mar 22 08:05 analise.c
-rwxrwxr-x 1 blau blau 16304 mar 30 11:37 a.out
-rw-rw-r-- 1 blau blau 594 mar 22 11:22 exemplo.c
-rw-rw-r-- 1 blau blau 325 mar 22 10:45 fizzbuzz.c
-rw-rw-r-- 1 blau blau 1743 mar 17 21:32 limites.c
-rw-rw-r-- 1 blau blau 340 mar 20 14:33 str.c
-rw-rw-r-- 1 blau blau 882 mar 30 11:37 teste.c
[Pai] Processo filho 1810186 terminou com status 0
[Pai] Voltando ao controle.

No GNU/Linux, o shell chama a syscall clone, em vez de fork, e execve, em vez de execvp.

9.3. Layout de memória

Quando o processo é iniciado, ele recebe uma faixa contínua de endereços virtuais de memória, segundo o layout abaixo:

ENDEREÇOS MAIS ALTOS
+--------------------------+ ---+
| Vetor Ambiente | |
+--------------------------+ |
| Vetor Argumentos | |
+--------------------------+ PILHA (STACK)
| Quantidade de argumentos | |
+--------------------------+ |
| Dados das funções | |
+------------+-------------+ ---+
| ↓ |
| |
| ↑ |
+------------+-------------+
| | <- Mapeamento de arquivos.
| HEAP | <- Bibliotecas dinâmicas.
| | <- Alocação dinâmica.
+--------------------------+
| .bss | Dados globais e estáticos não inicializados.
+--------------------------+
| .data | Dados globais e estáticos inicializados.
+--------------------------+
| .rodata | Dados constantes (read only).
+--------------------------+
| .text | Código do programa.
+--------------------------+
ENDEREÇOS MAIS BAIXOS

Conteúdo do binário executável

As seções do binário do executável serão copiadas para os segmentos mais baixos desse espaço de endereços.

Região do HEAP

Acima dos dados do binário, uma grande região é designada para a alocação dinâmica de espaços em memória para receber dados processados durante a execução do programa: o heap. Nesta mesma região, também são carregados os conteúdos binários das bibliotecas carregadas dinamicamente, como a glibc, o ld-linux e a biblioteca vdso, do kernel.

Região da pilha (stack)

Nos endereços mais altos, é configurada uma estrutura de dados chamada pilha. Como o nome sugere, é uma estrutura onde os dados são "empilhados" uns sobre os outros, como numa pilha de pratos.

Vetores de argumentos e ambiente

Na base da pilha, nós encontramos um vetor de strings contendo as variáveis exportadas para o processo (vetor ambiente). Imediatamente acima, nós temos outro vetor de strings com as palavras utilizadas na linha do comando para invocar a execução do programa e, eventualmente, seus argumentos (vetor de argumentos ou parâmetros). Por último, no topo da pilha, nós encontraremos a quantidade de palavras no vetor de argumentos.

Chamadas de funções

Ao longo da execução do programa, os dados das funções que forem chamadas serão incluídos no topo da pilha e serão removidos quando elas terminarem.

Esta aula não tem exercícios.

10. O vetor de argumentos

Vídeo desta aula

10.1. Vetores e strings

Observe o exemplo:

char str[] = "banana";
printf("Tamanho de str : %zu\n", sizeof(str)); // Imprime 7.

O vetor str é do tipo "array of char", tem 7 elementos (char[7]) e seu nome é o endereço do primeiro caractere.

char *pstr = "cabana";
printf("Tamanho de pstr: %zu\n", sizeof(pstr)); // Imprime 8.

O operador sizeof avaliou 8 bytes porque é o tamanho de um endereço.

O ponteiro pstr é do tipo "pointer to char", não tem referência sobre o tamanho da string e seu nome avalia o endereço do primeiro caractere.

10.1.1. Vetores de "ponteiros"

Um vetor de ponteiros é um ponteiro para um vetor de endereços:

int d1[] = {1,2,3};
int d2[] = {4,5,6};
int d3[] = {7,8,9};

int *v[] = {d1, d2, d3};

printf("Tamanho de v : %zu\n", sizeof(v)); // Imprime 24 (3*8).
printf("Tamanho de v[0]: %zu\n", sizeof(v[0])); // Imprime 8 (Endereço tem 8 bytes).
printf("Tamanho de d1 : %zu\n", sizeof(d1)); // Imprime 12 (3*4).

O operador sizeof avaliou 8 bytes porque é o tamanho de um endereço.

O tipo de v é int * porque os tipos de seus elementos serão interpretados na compilação como "pointer to int", em vez de "array of int".

Para receber esse tipo de vetor, o parâmetro dessa função teria que ser decladado como…

void print_arr(int *arr[], int size);

Ou…

void print_arr(int **arr, int size);

10.1.2. Vetores de strings

Vetores de strings são vetores de ponteiros:

char *frutas[] = {"banana", "laranja", "abacate"};

printf("Tamanho de frutas : %zu\n", sizeof(frutas)); // Imprime 24 (3*8).
printf("Tamanho de frutas[0]: %zu\n", sizeof(frutas[0])); // Imprime 8.

O operador sizeof avaliou 8 bytes porque é o tamanho de um endereço.

As strings literais são interpretadas como "pointer to char" (char *).

Numa função…

void print_list(char **list);

10.1.3. Vetor de strings terminado com NULL

Os vetores de argumentos e de ambiente são vetores de strings terminados com uma string nula (contendo apenas '\0'), o que pode ser representado por (void *)0 (ponteiro nulo) ou simplesmente NULL.

Esta seria uma lista de strings terminada com NULL.

char *frutas[] = {"banana", "laranja", "abacate", NULL};

int i = 0;
while (frutas[i]) {
printf("[%d] => %s\n", i, frutas[i]);
i++;
}
/* Imprime:
[0] => banana
[1] => laranja
[2] => abacate
*/

10.1.4. Parâmetros da função 'main'

As especificação dizem que a função main pode ser definida sem receber nenhum parâmetro…

int main(void) { /* ... */ }

Ou com dois parâmetros:

Primeiro parâmetro

Variável inteira para a quantidade de argumentos (tipicamente argc).

Segundo parâmetro

Ponteiro para o endereço da lista de argumentos da linha do comando que iniciou o programa (tipicamente argv).

int main(int argc, char **argv) { /* ... */ }

Em algumas implementações, a função main pode declarar um terceiro parâmetro para receber a lista das variáveis exportadas para o ambiente do processo:

int main(int argc, char **argv, char **envp) { /* ... */ }

Mesmo que o terceiro parâmetro não seja declarado, o ambiente pode ser acessado com a função getenv (stdlib.h) ou com o ponteiro global environ.

#include <stdio.h>
#include <stdlib.h>

// Para informar que 'environ' existe...
extern char **environ;

int main(int argc, char **argv) {

printf("environ[0] => %s\n", environ[0]); // Imprime: SHELL=/bin/bash

char *env = getenv("SHELL");
printf("getenv(\042SHELL\042) => %s\n", env); // Imprime: /bin/bash

return 0;
}

10.1.5. Conversão de strings para números

Como todos os argumentos são recebidos como strings, haverá situações em que eles deverão ser convertidos para os valores numéricos correspondentes.

Exemplo: programa 'soma.c'

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

long str2int(char *str) {

int num; // Recebe o inteiro convertido.
char *end; // Recebe o endereço onde termina o inteiro na string.

errno = 0; // Zerar a variável global 'errno'.

// Converte a parte numérica inicial de 'str' pata long int.
num = strtol(str, &end, 10);

// Checagem de erros...
if (str == end) {
// A parte inicial da string não era numérica...
fprintf(stderr, "Nenhum dígito encontrado!\n");
exit(EXIT_FAILURE);
} else if (errno == ERANGE) {
// O número está fora dos limites do tipo 'long int'...
fprintf(stderr, "Número fora dos limites!\n");
exit(EXIT_FAILURE);
} else if (*end != '\0') {
// Caracteres não convertidos restantes...
fprintf(stderr, "Caracteres inválidos: %s\n", end);
exit(EXIT_FAILURE);
}

return num;
}

int main(int argc, char **argv) {
int soma = 0;
int num;

for (int i = 1; i < argc; i++) {
num = str2int(argv[i]);
if (i == 1) {
printf("%d ", num);
} else {
printf("+ %d ", num);
}
soma += num;
}
printf("= %d\n", soma);

return 0;
}

Aqui, nós usamos a função strtol, que converte os dígitos encontrado na parte inicial de uma string em seu correspondente numérico com o tipo long int na base numérica especificada no terceiro argumento.

long strtol(const char *restrict nptr, char **_Nullable restrict endptr, int base);

Seu retorno é o valor numérico convertido, a menos que o valor exceda os limites do tipo long int, o que fará com que o retorno seja LONG_MIN ou LONG_MAX. Se esta for a situação, a global errno receberá o valor da constante simbólica ERANGE, o que nos levou a utilizar o cabeçalho errno.h no exemplo.

Como a conversão pode retornar o valor 0, é preciso inicializar errno com 0 antes de chamar strtol.

Ao fazer a conversão, o endereço do término da parte numérica é registrado no parâmetro endptr, o que possibilita fazer diversas verificações, como:

- Se o endereço em endptr for igual ao endereço do início da string, a parte inicial contém dígitos;

- Se o valor em endptr for igual a \0, a string só contém dígitos.

Além disso, se houver uma parte final não numérica, ela pode ser recuperada a partir do endereço em endptr.

Compilando e executando…

:~$ gcc -Wall -o soma soma.c
:~$ ./soma 1 2 3 4
1 + 2 + 3 + 4 = 10

10.2. Exercícios

10.2.1. Desafio: Programa 'listenv'

Crie um programa que liste as variáveis exportadas para seu ambiente de duas formas:

Sem argumentos: lista todas as variáveis no ambiente…

:~$ listenv

Com nomes de variáveis: lista apenas os valores das variáveis informadas…

:~$ listenv VAR1 VAR2 ...

Para cada variável inexistente passada como argumento, imprimir na saída padrão de erros (stderr)…

:~$ listenv SHELL VAR2 VAR3
/bin/bash
VAR2 não está definida no ambiente
VAR3 não está definida no ambiente

10.2.2. Desafio: Programa 'tconv'

Crie um programa que converta temperaturas segundo a sintaxe abaixo:

tconv 23.4c Converte 23.4 graus Celsius para Fahrenheit
tconv 100f Converte 100 graus Fahrenheit para Celsius

A unidade deve ser indiferente à caixa de texto (f = F, c = C).

10.2.3. Desafio: Programa '2fer'

Crie um programa que, para cada NOME recebido como argumento, imprima:

Um para NOME, um para mim.

Se nenhum nome for passado, imprimir:

Um para você, um para mim.

10.2.4. Desafio: Programa 'fatp'

Escreva um programa que imprima os fatores primos de um número inteiro positivo maior do que 1 recebido como argumento.

:~$ fatp 90
2 3 3 5
:~$ fatp 36
2 2 3 3
:~$ fatp 43
43 é primo!

10.2.5. Desafio: Programa 'round'

Escreva um programa que, recebendo um número com ponto flutuante como argumento, faça o seu arredondamento das seguintes formas:

:~$ round 2.5
3
:~$ round 2.4
2
:~$ round -c 2.3
3
:~$ round -f 2.9
2
:~$ round -t 2.6
2

As opções -t (truncate), -f (floor) e -c (ceil) podem ser escritos antes ou depois do número. As opções -t e -f são sinônimas.

11. Entrada e saída de dados

Vídeo desta aula

11.1. Tabela de descritores de arquivos

Um fluxo de dados (ou stream) é a abstração, em software, do transito de dados entre dispositivos de hardware e processos no espaço de usuário.

Por exemplo:

- A leitura dos dados digitados no teclado do terminal;

- A impressão (ou exibição) de mensagens no display do terminal;

- O acesso a um arquivo para escrita e/ou leitura de dados.

No sistema operacional, esses fluxos são estabelecidos e controlados por diversas estruturas (structs), associadas individualmente a cada processo, chamadas de tabelas de descritores de arquivos.

Ao serem iniciados, os processos herdam vários dados de seus respectivos processos pais, entre eles, uma cópia da tabela de descritores de arquivos representando, no mínimo, três fluxos de dados padrão.

11.1.1. Fluxos de dados padrão

Por padrão, todo processo inicia com acesso a, pelo meno, três dispositivos ligados a um terminal:

- Entrada padrão (/dev/stdin): leitura da digitação no terminal;

- Saída padrão (/dev/stdout): exibição de mensagens no terminal;

- Saída padrão de erros (/dev/stderr): exibição de mensagens de erros no terminal.

Cada um desses dispositivos padrão é disponibilizado para cada processo, individualmente, através de uma entrada na sua tabela de descritores de arquivos:

- stdin: descritor de arquivos 0;
- stdout: descritor de arquivos 1;
- stderr: descritor de arquivos 2.

11.1.2. Uma nota sobre dispositivos

Sistemas parecidos com o UNIX geralmente trabalham com 7 tipos de arquivos:

Diretórios

Arquivos que indexam a localização de outros arquivos.

Arquivos comuns

Como os arquivos dos nossos textos, programas e imagens.

Ligações simbólicas
Arquivos que representam outros arquivos.
Dispositivos caractere

Abstração da comunicação com dispositivos de hardware sem o acúmulo de dados.

Dispositivos bloco

Abstração da comunicação com dispositivos de hardware com acúmulo de dados.

Pipes/FIFOs

Arquivos que interfaceiam a troca de dados entre processos.

Sockets

Arquivos de interfaceiam o acesso a serviços.

Então, quando dizemos algo como: "dispositivo de terminal", ou "dispositivo padrão de entrada" (ou de saída), nós estamos falando de um desses tipos de arquivos.

11.1.3. Representação em '/proc'

Como vimos na aula 8, o kernel disponibiliza todas as informações sobre todos os processos em execução na forma de um sistema de arquivos virtual (procfs) montado no diretório /proc. Nele, cada processo terá um subdiretório nomeado segundo seu número de identificação (PID) e, neste subdiretório, existe um outro subdiretório com as ligações simbólicas entre os descritores de arquivos do processo e os arquivos e dispositivos a que ele tem acesso: é o diretório /proc/PID/fd.

Se listarmos, por exemplo, o diretório fd do processo do shell, nós veremos algo assim:

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 blau blau 64 abr 23 08:09 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:09 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:09 2 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:09 255 -> /dev/pts/1

Aqui, $$ é a expansão do parâmetro especial do shell que contém o número do seu processo.

Observe que os três descritores de arquivos padrão (0, 1 e 2) estão presentes como ligações simbólicas (outro tipo de arquivo) para o mesmo dispositivo de terminal (/dev/pts/1).

Uma pequena demonstração

Com a função system, declarada no cabeçalho stdlib.h, nós podemos executar o utilitário ls para listar os descritores de arquivos atribuídos ao processo iniciado para a execução do nosso programa (fdlist.c):

#include <stdlib.h>

int main(void) {
system("ls -l /proc/$(pidof a.out)/fd");
return 0;
}

O utilitário pidof exibe o número do PID de um processo a partir do nome do programa que o iniciou passado como argumento: no caso, o nosso a.out.

Compilando e executando:

:~$ gcc -Wall fdlist.c
:~$ ./a.out
total 0
lrwx------ 1 blau blau 64 abr 23 08:16 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:16 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:16 2 -> /dev/pts/1

Repare que, como o processo foi iniciado pela mesma sessão do shell do exemplo anterior, nós temos os três descritores de arquivos padrão ligados ao mesmo terminal de antes (/dev/pts/1). Isso aconteceu porque esses fluxos de dados foram herdados do processo pai (o processo do shell).

11.1.4. Redirecionamentos e pipes

Também na aula 8, nós vimos que as linhas de comandos simples podem conter operadores de redirecionamento de fluxos de dados de/para arquivos, ou ainda, poderiam ser encadeados com outros comandos simples através do operador de pipe.

Redirecionamentos

Quando um fluxo de dados é redirecionado, por exemplo, para ler um arquivo, o que é feito com o operador de redirecionamento de leitura (<), a ligação do descritor de arquivos 0 (stdin) passa a ser feita com o arquivo.

Por exemplo, se eu tiver um arquivo chamado lista.txt e redirecionar seu conteúdo para leitura pelo programa do exemplo anterior, o resultado seria esse:

:~$ ./a.out < lista.txt
total 0
lr-x------ 1 blau blau 64 abr 23 08:47 0 -> /home/blau/lista.txt
lrwx------ 1 blau blau 64 abr 23 08:47 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:47 2 -> /dev/pts/1

Já se eu quisesse escrever a saída do programa em um arquivo, bastaria fazer um redirecionamento de escrita (operadores > ou >>):

:~$ ./a.out > lista.txt
:~$ cat lista.txt
total 0
lrwx------ 1 blau blau 64 abr 23 08:49 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 08:49 1 -> /home/blau/lista.txt
lrwx------ 1 blau blau 64 abr 23 08:49 2 -> /dev/pts/1

Como eu redirecionei a saída padrão do programa para um arquivo, nada foi mostrado no terminal e, por isso, eu exibi o conteúdo do arquivo com o utilitário cat.

Pipes

Com os pipes é semelhante, mas nós teríamos uma ligação com outro processo interfaceada por um arquivo do tipo pipe criado pelo sistema.

Por exemplo, eu posso encadear o nosso programa de exemplo em um pipe com o utilitário cat:

:~$ ./a.out | cat
total 0
lrwx------ 1 blau blau 64 abr 23 08:56 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 08:56 1 -> pipe:[2551457]
lrwx------ 1 blau blau 64 abr 23 08:56 2 -> /dev/pts/1

Aqui, a saída do nosso programa (descritor de arquivos 1) está ligada ao arquivo pipe:[2551457] para escrita. Ao mesmo tempo, o processo do cat teria a sua entrada padrão (descritor de arquivos 0) ligada ao mesmo arquivo para leitura.

Eu posso até modificar meu programa para exibir os diretórios fd dos dois processos:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
puts("Processo do programa:");
fflush(stdout);
system("ls -l /proc/$(pidof a.out)/fd");

puts("\nProcesso do cat:");
fflush(stdout);
system("ls -l /proc/$(pidof cat)/fd");
return 0;
}

Compilando e executando com o pipe novamente:

:~$ gcc -Wall fdlist.c
:~$ ./a.out | cat
Processo do programa:
total 0
lrwx------ 1 blau blau 64 abr 23 09:06 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 09:06 1 -> pipe:[2550676]
lrwx------ 1 blau blau 64 abr 23 09:06 2 -> /dev/pts/1

Processo do cat:
total 0
lr-x------ 1 blau blau 64 abr 23 09:06 0 -> pipe:[2550676]
lrwx------ 1 blau blau 64 abr 23 09:06 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:06 2 -> /dev/pts/1

11.1.5. Armazenamento temporário (buffers)

Na última versão do exemplo, eu precisei chamar a função fflush(stdout), e veja o que aconteceria sem ela:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
puts("Processo do programa:");
system("ls -l /proc/$(pidof a.out)/fd");

puts("\nProcesso do cat:");
system("ls -l /proc/$(pidof cat)/fd");
return 0;
}

Compilando e executando:

:~$ gcc -Wall fdlist.c
:~$ ./a.out | cat
total 0
lrwx------ 1 blau blau 64 abr 23 09:12 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 09:12 1 -> pipe:[2569607]
lrwx------ 1 blau blau 64 abr 23 09:12 2 -> /dev/pts/1
total 0
lr-x------ 1 blau blau 64 abr 23 09:12 0 -> pipe:[2569607]
lrwx------ 1 blau blau 64 abr 23 09:12 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:12 2 -> /dev/pts/1
Processo do programa:

Processo do cat:

Notou que as linhas impressas pela função puts só foram exibidas depois das listagens feitas pela função system?

Isso aconteceu porque, em um pipe, a saída do programa é totalmente bufferizada, ou seja, os dados impressos por funções como puts e printf são acumuladas no buffer de saída do processo, mas não podem ser despejadas imediatamente na saída padrão até que outros subprocessos o façam. Como nós temos as chamadas da função system, que iniciam subprocessos para executar comandos, as listagens dos diretórios são despejadas e só depois as linhas das chamadas de puts são despejadas.

Isso não aconteceria se não houvesse um pipe:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
puts("Processo do programa:");
system("ls -l /proc/$(pidof a.out)/fd");

puts("\nProcesso do cat:");
system("ls -l /proc/$(pidof cat)/fd");
return 0;
}

Compilando e executando:

:~$ gcc -Wall fdlist.c
:~$ ./a.out
Processo do programa:
total 0
lrwx------ 1 blau blau 64 abr 23 09:21 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:21 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:21 2 -> /dev/pts/1

Processo do cat:
ls: não foi possível acessar '/proc//fd': Arquivo ou diretório inexistente

Ignorando o erro (o cat não estava em execução), veja que as linhas foram impressas na ordem em que apareciam no programa, sem a função fflush. Isso porque, sem pipes ou redirecionamentos ocupando a saída padrão, o buffer de saída do programa é liberado linha a linha (na ocorrência do caractere de quebra de linha \n). Mas, com um redirecionamento, por exemplo…

:~$ ./a.out > lista.txt
ls: não foi possível acessar '/proc//fd': Arquivo ou diretório inexistente
:~$ cat lista.txt
total 0
lrwx------ 1 blau blau 64 abr 23 09:38 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 09:38 1 -> /home/blau/lista.txt
lrwx------ 1 blau blau 64 abr 23 09:38 2 -> /dev/pts/1
Processo do programa:

Processo do cat:

Nota: veja que a mensagem de erro não foi escrita no arquivo, mas foi exibida no terminal, porque ela foi enviada através da saída padrão de erros (stderr).

O que a função fflush faz, com o fluxo de dados stdout como argumento, é forçar a descarga do que houver no buffer de saída do programa, independente de haver pipes ou redirecionamentos.

11.1.6. Buffers de entrada e saída do programa

Voltando um pouco o assunto, algumas funções da libc/glibc alocarão algum espaço no heap do processo do programa para acumular dados recebidos pela entrada padrão ou destinados à saída padrão.

Por exemplo:

- Produzem um buffer de saída: funções puts, printf, etc;

- Produzem um buffer de entrada: funções scanf, fgets, etc.

11.1.7. Buffer de saída do programa

No caso do buffer de saída, como vimos, o tipo de acumulação é determinado, entre outras coisas, pela eventualidade de algum pipe ou redirecionamento:

Bufferização linha a linha

Programa executado sem pipes ou redirecionamentos.

Bufferização total

Programa executado em um pipe ou com redirecionamentos.

Sem bufferização

Com chamadas de sistema (ex: write) ou quando configurado explicitamente.

11.1.8. Buffer de entrada do programa

De certo modo, a acumulação de dados lidos de arquivos ou do terminal são mais simples, ou melhor, não estão sujeitos a tantas condições como o buffer de saída, mas também têm as suas particularidades.

Com a funções como scanf e fgets, a entrada é bufferizada e todos os bytes recebidos são acumulados. No entanto, caberá às características de cada função determinar como esses dados serão consumidos no programa.

Por exemplo, se utilizarmos as funções scanf ou fgets para ler algo digitado no terminal (entrada interativa), pode ser que, de tudo que for digitado, apenas uma parte seja consumida, fazendo com que reste alguma coisa no buffer de entrada. Os dados restantes poderão vir a ser consumidos nas chamadas subsequentes dessas funções, com resultados bastante inconvenientes!

Como a glibc não possui uma função para consumir os dados restantes no buffer de entrada após uma leitura, é comum utilizarmos algo assim depois da chamada de uma função com entrada bufferizada (se a leitura for da entrada padrão):

int ch;
while ((ch = getchar()) != '\n' && ch != EOF);

Nas próximas aulas, nós falaremos das particularidades da leitura da entrada padrão com as funções scanf e fgets.

11.1.9. Entrada padrão não bufferizada

Quando a entrada padrão não é bufferizada, nós ficamos sujeitos às regras de bufferização do lado do sistema: em especial, o buffer do dispositivo de terminal.

Por exemplo, numa leitura interativa com a chamada de sistema read, onde nós limitamos a quantidade de bytes que serão consumidos, pode restar algo do que foi digitado no buffer do terminal e, se o nosso programa não consumir esses bytes excedentes, eles serão descarregados na saída padrão pelo próprio terminal, quando o processo do nosso programa terminar, e serão lidos pelo shell como se fosse um comando.

Nas próximas três aulas, nós vamos explorar a leitura interativa de dados digitados no terminal e todas essas particularidades serão demonstradas.

Esta aula não tem exercícios.

12. Leitura da entrada padrão com 'scanf'

Vídeo desta aula

12.1. Verificando a ligação com a entrada padrão

A entrada padrão (stream stdin ou descritor de arquivos 0) pode estar ligada…

- A um terminal para leitura interativa (digitação);

- A um arquivo via redirecionamento de leitura;

- À saída de um processo via pipe ou arquivo FIFO;

- A um arquivo de forma programática (ex: funções freopen, dup2).

Para verificar se o fluxo stdin está ligado a um terminal, nós podemos utilizar a função isatty (unistd.h):

int isatty(int fd);

Onde int fd é o descritor de arquivos testado. O retorno será o inteiro 1, se fd estiver ligado a um terminal, ou 0, se estiver ligado a um pipe ou redirecionado para um arquivo.

Exemplo (test-tty.c):

#include <stdio.h>
#include <unistd.h>

int main(void) {
if (isatty(0)) {
puts("STDIN ligada ao terminal.");
} else {
puts("STDIN ligada a um arquivo ou pipe.");
}

return 0;
}

Compilação e testes:

:~$ gcc -Wall test-tty.c
:~$ ./a.out
STDIN ligada ao terminal.
:~$ ./a.out < /dev/null
STDIN ligada a um arquivo ou pipe.
:~$ : | ./a.out
STDIN ligada a um arquivo ou pipe.

O comando interno do shell : é o comando nulo, que não faz nada e sempre termina com sucesso.

Para associar programaticamente a entrada padrão a um arquivo (de nome recebido como argumento, por exemplo), nós podemos utilizar a função freopen para abrir um arquivo para leitura através de um fluxo (stream) previamente aberto: no caso o stream stdin…

FILE *freopen(const char *caminho, const char *modo, FILE *stream);

Onde:

- caminho: string do nome do arquivo;

- modo: string de modo de abertura ("r", "w", "rw");

- stream: um fluxo de dados previamente aberto.

Retorna um ponteiro para o tipo FILE ou NULL, em caso de erro.

O propósito original desta função é alterar o arquivo associado a um dos fluxos padrão (stdin, stdout ou stderr).

Exemplo (redir-stdin.c):

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {

// Diretório 'fd' do processo de 'a.out'...
puts("Antes do redirecionamento...");
system("ls -l /proc/$(pidof a.out)/fd");

// Nome do arquivo...
char *filename;
if (argc > 1) {
// Primeiro argumento...
filename = argv[1];
} else {
// Dispositivo nulo...
filename = "/dev/null";
}

// Redirecionamento...
FILE* stream = freopen(filename, "r", stdin);
if (!stream) {
perror("freopen");
return EXIT_FAILURE;
}

// Diretório 'fd' do processo de 'a.out'...
puts("Depois do redirecionamento...");
system("ls -l /proc/$(pidof a.out)/fd");

return EXIT_SUCCESS;
}

Compilação…

:~$ gcc -Wall redir-stdin.c

Executando sem argumentos (redireciona para /dev/null):

:~$ ./a.out
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /dev/null
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0

Executando com argumentos (nome do arquivo no primeiro argumento):

:~$ ./a.out /etc/shells
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /etc/shells
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0

No caso de um erro de abertura (ex: arquivo "banana" não existe):

:~$ ./a.out banana
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:21 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 2 -> /dev/pts/0
freopen: No such file or directory

12.2. Leitura interativa da entrada padrão

Seja o conteúdo de um arquivo ou uma entrada digitada no terminal, a leitura da entrada padrão pode ser feita com várias formas e com diversas funções da biblioteca padrão:

- Leitura de caracteres: getchar, getc(stdin) e fgetc(stdin);

- Leitura de linhas (até \n): fgets(buf, size, stdin), getline(&buf, &size, stdin);

- Leitura formatada de linhas: scanf, fscanf(stdin, ...);

Mas, como a leitura das linhas de um arquivo é mais controlada (o conteúdo é previamente conhecido), nós vamos concentrar nossa atenção na leitura de linhas digitadas no terminal por um usuário (leitura interativa). Para isso, nós estudaremos três funções (nesta e nas próximas aulas):

- Leitura formatada de linhas: scanf;

- Leitura de linhas: fgets;

- Leitura de caracteres: chamada de sistema read.

12.2.1. Leitura formatada de linhas com 'scanf'

A função scanf lê os bytes de uma linha enquanto encontra casamentos com os especificadores definidos em uma string de formato (como no printf). A parte da linha que encontrar casamento é passada para um ou mais endereços de variáveis (ou endereços em ponteiros) e tudo que vier depois permanece no buffer de entrada do processo.

Outros pontos importantes:

- Cada especificador de formato é casado com apenas uma palavra da linha lida (palavras são cadeias de caracteres delimitadas por espaços, tabulações e quebras de linha).

- Retorna o número de casamentos com as especificações de formatos que foram encontrados e atribuídos (o que pode ser menor do que a quantidade de especificadores fornecidos).

- Em caso de erro, retorna zero ou EOF (com vários significados possíveis).

- Altera a variável errno (errno.h), o que possibilita obter descrições de erros, por exemplo, com as funções perror (stdio.h) ou strerror (string.h).

- No caso da formatação como string, o caractere nulo ('\0') é inserido após o último byte do casamento encontrado.

- Tudo que delimita a quantidade de bytes lidos é a forma como a string de formato é construída.

- Uma string de formato mal construída pode levar a vulnerabilidades de memória.

A função scanf é considerada insegura por muitos programadores, mas a verdade é que ela é reconhecidamente difícil de ser utilizada corretamente.

De man 3 scanf:

It is very difficult to use these functions correctly, and it is preferable to read entire lines with fgets(3) or getline(3) and parse them later with sscanf(3) or more specialized functions such as strtol(3).

Ou seja, o próprio manual diz que é muito difícil utilizar corretamente as funções da família do scanf e pode ser mais fácil ler linhas inteiras com funções como fgets e getline e fazer os processamentos necessários com a função sscanf ou outras funções mais especializadas em conversões de cadeias de caracteres em valores numéricos.

12.2.2. Para reduzir as chances de erro

A maior parte dos problemas com scanf decorre da conversão de strings. Por isso, é sempre bom atentar para alguns detalhes, como:

- Sempre validar o retorno de scanf e fazer o tratamento dos erros.

- Na leitura de uma linha inteira como string (por exemplo, uma linha que contenha apenas uma palavra), sempre limitar a quantidade de caracteres lidos no especificador %s (ex: %9s para ler até 9 bytes).

- Na leitura de caracteres únicos, o especificador %c deve ser precedido de um espaço.

Exemplo 1: Leitura de duas strings

char str1[10];
char str2[10];

// "%Ns" não deve incluir '\0'!
scanf("%9s", str1);
scanf("%9s", str2);

Exemplo 2: Separando a entrada digitada em campos

char str1[10];
char str2[10];

scanf("%9s %9s", str1, str2);

printf("str1: %s\n", str1);
printf("str2: %s\n", str2);

Exemplo 3: Lendo caracteres

Neste caso, apenas o primeiro caractere será casado, seja ele qual for.

printf("Confirma (s/n)? ");
char reply;
scanf("%c", &reply);

Mas, se o usuário digitar espaços antes dos caracteres esperados, o primeiro espaço é que será consumido. Para evitar isso…

scanf(" %c", &reply);

Exemplo 4: Lendo vários caracteres

char a, b, c;
scanf(" %c %c %c", &a, &b, &c);
printf("A:%c B:%c C:%c\n", a, b, c);

Exemplo 5: Lendo e formatando números

Inteiros:

printf("Digite um inteiro: ");
int num;
scanf("%d", &num);

Ponto flutuante:

printf("Digite um float: ");
float f;
int c = scanf("%f", &f);

12.2.3. Descarregando o buffer de entrada

A função scanf não consome nada que não case com o formato especificado, o que inclui a quebra de linha. Todo o restante da linha permanece no buffer de entrada e, na ausência de uma função da biblioteca padrão para descarregá-lo, nós podemos criar uma função como esta:

void stdin_flush(void) {
char ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}

Ela consumiria todos os caracteres restantes no buffer de entrada enquanto o caractere atribuído a ch fosse diferente da quebra de linha (\n) ou de -1, que é o valor retornado por getchar quando tenta ler caracteres após o fim de um arquivo (constante simbólica EOF).

Isso é particularmente necessário quando houver leituras consecutivas da entrada padrão com scanf…

int a;
scanf("%d", &b);
stdin_flush();

int b;
scanf("%d", &b);
stdin_flush();

/* ... */

12.2.4. Inicialização de valores numéricos

Nos exemplos anteriores, nós temos um problema que precisa ser considerado: a leitura pode falhar e, nesse caso, nada será escrito nos endereços das variáveis passados para scanf. Como as variáveis não foram inicializadas, seu conteúdo será lixo de memória. Portanto, é importante que essas variáveis sejam devidamente inicializadas com algum valor, por exemplo:

int a = 0;
scanf("%d", &b);
stdin_flush();

int b = 0;
scanf("%d", &b);
stdin_flush();

/* ... */

Este problema é relevante com variáveis que recebem valores numéricos (char, int, float, double, etc), porque, em geral, elas são utilizadas em cálculos e até o lixo de memória pode ser tratado como número válido, levando a resultados enganosos.

12.2.5. Buffer overflow

A leitura de strings com scanf é bastante suscetível a causar vulnerabilidades graves de memória, como o buffer overflow, que é quando a quantidade de dados escritos a partir do endereço de um buffer excedem seu limite máximo e continuam sendo escritos em endereços válidos subsequentes.

Quando o programa tentar escrever os bytes excedentes em uma região inválida da memória, nós temos uma falha de segmentação.

Observe este exemplo:

char ref[10] = "123456789"; // Uma string qualquer.
char str[10]; // Buffer para a stringa digitada no terminal.

scanf("%s", str); // Especificador %s sem limite.

printf("str: %s\n", str);
printf("ref: %s\n", ref);

Ao ser compilado e executado, os 10 bytes de ref serão escritos na pilha e, acima deles (em um endereço 10 bytes mais baixo), serão reservados os 10 bytes para o buffer str.

Se o usuário digitar abc, esta parte da pilha ficará assim:

+------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13

+------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x00 | lixo | lixo | lixo | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09

A impressão no terminal será:

str: abc
ref: 123456789

Mas, se digitar abcdefghjij, este será o resultado:

buffer overflow
↓
+------+------+------+------+------+------+------+------+------+------+
ref | 0x00 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13

+------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x6A |
+------+------+------+------+------+------+------+------+------+------+
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09

E isso vai imprimir:

str: abcdefghij
ref:

Porque o caractere nulo (0x00) foi escrito no décimo primeiro endereço após o endereço de str (endereço 0x0A), que é o primeiro byte de ref.

Esse problema poderia ser evitado especificando o limite do tamanho da string:

char ref[10] = "123456789"; // Uma string qualquer.
char str[10]; // Buffer para a stringa digitada no terminal.

scanf("%9s", str); // Limite de 9 bytes para a string.

printf("str: %s\n", str);
printf("ref: %s\n", ref);

Executando e digitando abcdefghij, os dados na pilha seriam…

+------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13

+------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09

E nós veríamos isso no terminal:

str: abcdefghi <- O 'j' não foi consumido...
ref: 123456789

12.3. Exercícios

12.3.1. Desafio: Conversão Milhas->Km e Km->Milhas

Utilizando a função scanf para receber os valores digitados no formato:

<valor>[m, k ou km]

Crie um programa que converta o valor de uma unidade para outra:

- Se a unidade for k ou km, converter para milhas;

- Se a unidade for m, converter para quilômetros;

- Se não houver unidade, presume-se o valor em quilômetros;

- Espaços entre o valor e a unidade devem ser ignorados;

- As unidades podem ser escritas em maiúsculas ou minúsculas;

- Tudo após a unidade deve ser ignorado;

- Valores devem poder ser passados com representações de ponto flutuante;

- Resultados sempre serão impressos em ponto flutuante e duas casas de precisão.

12.3.2. Desafio: Separação de linhas em campos

Considerando o arquivo ender.csv, abaixo:

Maria das Couves, Rua da Horta, 23, Hortolândia, PR
João da Silva, Av. da Prata, 42, Silverado, RS
Antônio Pereira, Av. dos Pomares, 65, Pomorânia, SP

Crie um programa que, usando scanf, liste os dados em cada linha desta forma:

Nome : ...
Endereço: ...
Cidade : ...
Estado : ...

O programa deve ser executado com:

./a.out < ender.csv

12.3.3. Desafio: Implementando um 'cat' com 'scanf'

Crie um programa que, ao ser executado em um pipe ou com um redirecionamento de leitura, imprima todas as linhas de um arquivo. Se executado sem operadores, o programa deve imprimir todas as linhas digitadas no terminal antes do atalho Ctrl+D.

Exemplos:

./a.out < /etc/shells # Imprime todas as linhas de /etc/shells
cat /etc/shells | ./a.out # Imprime todas as linhas enviadas pelo cat
./a.out # Espera Ctrl+D para imprimir todas as linhas digitadas

13. Leitura da entrada padrão com 'fgets'

Vídeo desta aula

13.1. Ainda sobre a função 'scanf'

Se, por um lado, a função scanf é conveniente para ler e converter dados na entrada padrão, também é preciso considerar os cuidados que ela exige, especialmente quando lidamos com a passagem dos dados lidos para um buffer de caracteres (geralmente, criado para receber uma string). A sua principal causa de problemas é a falta de um controle simples e explícito da quantidade de bytes que poderão ser escritos na memória. Na função scanf, todo controle possível é aquele que escrevemos em sua string de formato, por exemplo:

char buf[10];
scanf("%9s", buf);

Aqui, os 9 primeiros bytes lidos serão consumidos, acrescidos do terminador nulo (totalizando 10 bytes) e copiados para o endereço expresso pelo vetor buf, que foi declarado para receber até 10 bytes (incluindo o terminador nulo). Mas, pode acontecer de, um dia, nós mudarmos de ideia e reduzirmos o limite de buf, digamos, para 8 bytes. Isso exigiria uma alteração na chamada de scanf – o que pode ser facilmente esquecido!

Aliás, essa é uma das justificativas para o uso de constantes simbólicas em vez dos chamados números mágicos. Então, é comum que vetores, como o nosso vetor buf, sejam declarado assim:

#include <stdio.h>
...
#define BUFMAX 10
...
int main(...) {
char buf[BUFMAX];
...
scanf("%9s", buf);
...
}

E isso tornaria ainda mais arriscada qualquer modificação no tamanho definido por BUFMAX.

13.2. A função 'fgets'

Segundo sua man page, a função fgets recebe três argumentos:

char *fgets(char s[restrict .size], int size, FILE *restrict stream);

- char s[restrict .size] - Um buffer para receber os dados lidos;

- int size - A quantidade máxima de bytes a serem escritos no buffer;

- FILE *restrict stream - Uma estrutura do tipo FILE representando o fluxo de dados que será lido.

Quando chamada, a função fgets lê, no máximo, a quantidade de bytes expressa por size - 1, inclui o terminador nulo ('\0') e escreve esses bytes no buffer.

Por exemplo:

#include <stdio.h>

#define BUFMAX 10

int main(void) {
char buf[BUFMAX];
fgets(buf, BUFMAX, stdin);

...
}

Aqui, stdin é uma macro que expande uma estrutura FILE predefinida com o descritor de arquivos 0 (entrada padrão).

De pronto, fica evidente a facilidade que nós termos para alterar o tamanho do vetor buf quando necessário: basta alterar a constante simbólica BUFMAX.

13.2.1. Uma nota sobre o tipo ponteiro para FILE

No nível do sistema, o acesso a arquivos é sempre estabelecido através de uma estrutura complexa identificada por um número inteiro: o descritor de arquivos, assunto da aula 10. Então, quando utilizamos chamadas de sistema, como read e write, o fluxo de dados (stream) é passado como o número de um descritor de arquivos. Por isso, nós utilizamos as macros (unistd.h):

Macro | Significado
STDIN_FILENO | Valor inteiro relativo ao descritor de arquivos 0.
STDOUT_FILENO | Valor inteiro relativo ao descritor de arquivos 1.
STDERR_FILENO | Valor inteiro relativo ao descritor de arquivos 2.

Nas funções da biblioteca padrão, que abstrai outras estruturas de controle associadas aos descritores de arquivos, os fluxos de dados são passados como ponteiros para essas estruturas através do tipo FILE * (ponteiro para FILE). Os fluxos de dados padrão também são abstraídos como macros definidas na biblioteca de funções:

Macro | Significado
stdin | Ponteiro para FILE associado ao descritor de arquivos 0.
stdout | Ponteiro para FILE associado ao descritor de arquivos 1.
stderr | Ponteiro para FILE associado ao descritor de arquivos 2.

13.2.2. O problema da quebra de linha

A função fgets lê, no máximo size - 1 bytes, o que é ótimo, mas nem sempre a linha lida terá tantos bytes. Nesses casos, a leitura será interrompida depois de uma quebra de linha (caractere '\n') ou depois de alcançado o fim do arquivo (EOF). Ou seja, se a leitura terminar com uma quebra de linha, o caractere '\n' continuará entre os bytes que serão escritos no buffer.

Observe:

#include <stdio.h>

#define BUFMAX 10

int main(void) {
char buf[BUFMAX];

printf("Digite até %d caracteres: ", BUFMAX - 1);
fgets(buf, BUFMAX, stdin);
printf("%s\n", buf);

return 0;
}

Executando o programa:

:~$ ./a.out
Digite até 9 caracteres: 123456789
123456789
:~$ ./a.out
Digite até 9 caracteres: 12345
12345

:~$

Quando eu executei o programa pela primeira vez, eu digitei exatamente 9 caracteres e a quebra de linha (inserida com o Enter) não chegou a ser lida por fgets.

+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+

Mas, na segunda vez, eu digitei apenas 5 caracteres e teclei Enter, totalizando os 6 caracteres lidos por fgets. Sendo assim, a quebra de linha final também foi escrita no buffer e impressa.

+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+

Pode parecer um inconveniente se estivermos lidando com a digitação de linhas no terminal, mas não remover o caractere '\n' faz todo sentido quando estamos lendo linhas de arquivos!

De todo modo, nós podemos criar uma função para remover a quebra de linha, se for o caso:

void trim_string(char *str, int size) {
int i = 0;
while (i < size && str[i] != '\0') {
if (str[i] == '\n') {
str[i] = '\0';
break;
}
}
i++;
}

Se estiver entrando com um buffer muito grande, o tipo de size pode ser alterado para size_t (unsigned long int).

No nosso exemplo, a função trim_string poderia ser utilizada assim:

# include <stdio.h>

#define BUFMAX 10;

// Remove a quebra de linha final do buffer...
void trim_string(char *str, int size);

int main(void) {
char buf[BUFMAX];

printf("Digite até %d caracteres: ", BUFMAX - 1);
fgets(buf, BUFMAX, stdin);
trim_string(buf, BUFMAX);

printf("%s\n", buf);

return 0;
}

// Remove a quebra de linha final do buffer...
void trim_string(char *str, int size) {
int i = 0;
while (i < size && str[i] != '\0') {
if (str[i] == '\n') {
str[i] = '\0';
break;
}
}
i++;
}

Compilando e executando novamente:

:~$ ./a.out
Digite até 9 caracteres: 12345
12345
:~$

E o conteúdo do buffer seria:

+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x00 | 0x00 | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+

13.2.3. Retorno nulo ambíguo

No caso de sucesso, a função fgets retorna o endereço do buffer ou NULL, no caso de um erro, ou se o fim do arquivo for alcançado sem que algo seja lido (por exemplo, ao ler um arquivo vazio ou numa leitura interativa cancelada com a combinação de teclas Ctrl+D).

Como você pode ver, o significado do retorno NULL é ambíguo, mas isso só será uma preocupação se estivermos lendo arquivos. De todo modo, nós podemos testar o que aconteceu com as funções feof e/ou ferror.

- feof(FILE *stream) - Testa o estado do indicador de fim de arquivo (0 = não difinido).

- ferror(FILE *stream) - Testa o estado do indicador de erros (0 = não definido).

Se isso for relevante, nós podemos chamar a função fgets desta forma:

#include <stdio.h>

#define BUFMAX 10

int main(void) {
char buf[BUFMAX];

if (fgets(buf, BUFMAX, stdin) == NULL) {
// Só preciamos testar se o retorno for NULL...
if (feof(stdin)) {
// Se feof() retornar algo diferente de zero...
fprintf(stderr, "Fim de arquivo alcançado.\n");
} else if (ferror(stdin)) {
// Se ferror() retornar algo diferente de zero...
fprintf(stderr, "Erro de leitura.\n");
}
return 1;
}

...

return 0;
}

13.2.4. Conversão para números

Diferente da função scanf, fgets apenas lê caracteres e escreve num buffer como uma string (caracteres terminados com '\0'). Isso significa que, se estivermos interessados em valores numéricos, os bytes no buffer terão que ser tratado, geralmente em três etapas:

- Remoção da quebra de linha, se houver uma;
- Conversão para o tipo desejado;
- Validação do resultado segundo o tipo;

Nós já falamos sobre a remoção da quebra de linha, então podemos nos concentrar nas duas etapas seguintes. Porém, os métodos de conversão mais comuns já integram alguma forma de validação (geralmente, como retornos). Sendo assim, vamos nos concentrar no que pode ser feito para obter os tipos esperados.

Conversão para inteiros com 'sscanf'

Provavelmente, a forma mais simples e direta para converter o conteúdo de um buffer para tipos numéricos é com a função sscanf:

char buf[BUFMAX];
int i;

fgets(buf, BUFMAX, stdin);

sscanf(buf, "%d", &i);

Aqui, se houver dígitos válidos no começo de buf, eles serão convertidos em seu correspondente inteiro na base 10, exatamente como seria feito lendo uma entrada digitada no terminal com a função scanf – e com suas mesmas limitações:

- A dificuldade de detectar entradas inválidas (por exemplo, 42abc seria casado e convertido para o inteiro 42);

- O retorno será apenas a quantidade de casamentos com a string de formato que forem convertidos e escritos nas variáveis, ou EOF, no caso do fim do buffer ser alcançado antes de um primeiro casamento ou no caso de um erro;

- Não oferece uma detecção de erros detalhada, como com as funções que alteram a variável errno.

Além disso, sscanf não lida bem com valores fora dos limites do tipo de destino, por exemplo:

#include <stdio.h>
#include <limits.h>

int main(void) {
char str[] = "9999999999";
int i = 0;

sscanf(str, "%d", &i);

printf("Valor de INT_MAX : %d\n", INT_MAX);
printf("Dígitos na string: %s\n", str);
printf("Valor convertido : %d\n", i);

return 0;
}

Neste exemplo, o comportamento de sscanf é indeterminado, mas compilaria sem erros:

:~$ gcc -Wall teste.c
:~$ ./a.out
Valor de INT_MAX : 2147483647
Dígitos na string: 9999999999
Valor convertido : 1410065407

Aqui, 9999999999 é convertido para um valor com 5 bytes (0x02540be3ff), dos quais, apenas os 4 últimos cabem no espaço do tipo int (0x540be3ff), resultando no valor 1410065407.

Conversão para inteiros longos com 'strtol'

A função strtol converte a parte inicial de uma string para long de acordo com a base de numeração indicada. O endereço do primeiro caractere inválido é atribuído a um ponteiro – e isso nos dá uma excelente forma de controle da conversão.

Simplificadamente, este seria o protótipo da função:

long strtol(char *str, char **endptr, int base);

Exemplo:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
char str[] = "1234567abc";
char *end;

long i = strtol(str, &end, 10);

printf("Valor convertido : %ld\n", i);
printf("Primeiro caractere inválido: 0x%02x\n", *end);

return 0;
}

Compilando e executando:

:~$ gcc -Wall teste.c
:~$ ./a.out
Valor convertido: 1234567
Primeiro caractere inválido: 0x61

O caractere 0x61 é a, na tabela ASCII.

Outros detalhes:

- No caso de valores maiores que o limite máximo de um inteiro longo, o retorno será LONG_MAX.

- No caso de valores menores que o limite mínimo de um inteiro longo, o retorno será LONG_MIN.

- Em ambos os casos, a variável errno recebe o valor de ERANGE (valor fora da faixa do tipo).

- Se o valor for inválido para a base de numeração, errno recebe o valor de EINVAL (valor inválido para a base).

- A descrição do erro em errno pode ser impressa com a função perror, chamada logo após strtol.

- Se não houver dígitos e caracteres válidos no começo da string, o endereço do primeiro caractere inválido será o mesmo da string e a função retornará 0.

- O ponto negativo de strtol é que o retorno é long e, se precisarmos de um valor int, teremos que verificar os limites máximo e mínimo do valor convertido.

Outras funções de conversão

- strtoll - Idêntica a strtol, mas converte para long long int.

- strtod, strtof e strtold - Funcionam como strtol, mas convertem para double, float e long double, respectivamente, sem a passagem de uma base de numeração.

- atoi, atof e atol - São muito simples, mas devem ser evitadas pela falta de validação e de meios para tratamento de erros.

13.3. Exercícios

13.3.1. Desafio: Fatorial de 'n'

Escreva um programa para calcular o fatorial de um número inteiro positivo entre 1 e 20 recebido por digitação no terminal. Além da impressão do resultado, o programa deve validar:

- Se a entrada digitada corresponde a um inteiro positivo;

- Se o número digitado está na faixa de valores esperado;

- Se existem outros caracteres além dos dígitos válidos.

O fatorial de um número é o produto de todos os números naturais menores ou iguais a ele mesmo. Ex.: 5! = 5 × 4 × 3 × 2 × 1 = 120

13.3.2. Desafio: Qual é o tipo?

Escreva um programa que peça um número qualquer ao usuário e imprima o tipo adequando para o número digitado (se for um número válido). O programa deve terminar com erro (e mensagem em stderr) se a string digitada não corresponder a um valor numérico válido.

São representações numéricas válidas:

- Inteiros positivos e negativos em base 2, 8, 10 e 16;

- Números de ponto flutuante com ou sem notação científica;

- Caracteres alfanuméricos e de pontuação da tabela ASCII.

14. Leitura da entrada padrão com 'read'

Vídeo desta aula

14.1. Chamadas de sistema

Chamadas de sistema são funções internas que o kernel implementa para que os programas (executados no espaço de usuário) tenham acesso a serviços e funcionalidades do sistema disponíveis no espaço do kernel. Entre essas funcionalidades, estão o acesso a arquivos para leitura e escrita, a alocação de espaço em memória, a criação de processos, o término de programas e muitas outras.

Portanto, nossos programas em C não interagem diretamente com o hardware ou com recursos protegidos do sistema. Em vez disso, eles fazem chamadas de sistema para "pedir" que o kernel realize essas operações por eles.

Isso também significa que todas as funções da biblioteca C padrão que nós utilizamos até agora, em algum momento, também fazem chamadas de sistema. Por exemplo, a função fgets utiliza a chamada de sistema read para preencher o buffer de entrada do programa e, em seguida, copiar parte desse conteúdo para um determinado endereço na memória – o que também envolve algumas outras chamadas de sistema.

Se quisermos utilizar a chamada de sistema read para escrever bytes diretamente no endereço de destino, evitando o buffer de entrada do processo, nós podemos utilizar a função read da glibc, que é uma interface para a chamada de sistema read propriamente dita.

As man pages das interfaces para as chamadas de sistema implementadas na glibc podem ser encontradas na seção 2 (ex.: man 2 read).

14.2. A função 'read'

A função read é um wrapper (embalagem) da chamada de sistema read e é declarada no cabeçalho unistd.h da seguinte forma:

ssize_t read(int fd, void buf[.count], size_t count);

Onde:

- int fd - O número de um descritor de arquivos aberto para leitura;

- void buf[.count] - O endereço do buffer de destino dos bytes lidos;

- size_t count - A quantidade máxima de bytes a serem lidos.

O tipo ssize_t é um apelido para long e size_t para unsigned long.

Por exemplo:

#include <unistd.h>

#define BUFMAX 10

int main(void) {

char buf[BUFMAX];
read(STDIN_FILENO, buf, BUFMAX);

...

return 0;
}

Isso fará com que até 10 bytes (BUFMAX) recebidos pela entrada padrão (descritor de arquivos 0, valor expandido pela macro STDIN_FILENO) sejam escritos na memória a partir do endereço de buf. Mas, diferente de fgets e scanf, não haverá a inclusão do terminador nulo ('\0'). Se quisermos garantir um byte para receber o terminador, nós precisamos fazer como na função fgets: ler count - 1 bytes…

#include <unistd.h>

#define BUFMAX 10

int main(void) {

char buf[BUFMAX];
read(STDIN_FILENO, buf, BUFMAX - 1);

...

return 0;
}

14.2.1. Conversão para string incluindo o terminador nulo

Para converter o conteúdo do buffer em uma string, nós temos que implementar uma forma de acrescentar o terminador nulo – por exemplo, utilizando a quantidade de bytes efetivamente lidos, que é retornada pela função read em caso de sucesso:

#include <unistd.h>

#define BUFMAX 10

int main(void) {

char buf[BUFMAX];
ssize_t bytes = 0;

if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
// Tratamento do terminador nulo...
buf[bytes] = '\0';

/* ... */

return 0;
}

O retorno 0 indica o fim do arquivo e -1 indica um erro, cujo identificador é atribuído à variável errno e pode ser exibido com a função perror.

A solução do exemplo faz com que read tenha um comportamento semelhante ao de fgets:

- A leitura é feita até BUFMAX - 1;

- O caractere seguinte no buffer (buf[bytes]) recebe '\0';

- O caractere '\n' será preservado se estiver no limite de leitura.

Para demonstrar, digamos que o usuário digitou 12345 e teclou Enter. O valor de bytes será 6 e os bytes em buf serão:

buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | lixo | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+

Depois de alterado o byte em buf[bytes] (buf[6]), nós teremos:

buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+

Se for digitado 1234567890, nós teremos inicialmente:

buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | lixo |
+------+------+------+------+------+------+------+------+------+------+

Os caracteres 0x30 (0) e 0x0a ('\n') não serão lidos, o valor de bytes será 9 e buf[9] receberá o terminador nulo:

buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+

14.2.2. Quando não converter os bytes lidos em string

O tratamento anterior só faz sentido quando queremos utilizar a entrada como uma string. Mas nada disso será necessário, nem será adequado, se estivermos lendo…

- Dados binários (arquivos binários em geral);

- Dados em blocos fixos (setores de disco, pacotes de rede, etc);

- Transferências de dados entre processos;
- Dados para cálculos de checksum e hash;
- Dados para compressão ou criptografia…

Entre outras tantas situações.

14.2.3. O buffer do terminal

Quando um programa com a função read é executado para receber dados digitados em um terminal, a digitação é acumulada em um buffer gerenciado pelo terminal e só será enviada para o processo do programa quando o usuário teclar Enter. No programa, a função read consome parte desses dados e os bytes que não forem consumidos permanecerão no buffer do terminal.

Os dados residuais não são descartados e permanecem disponíveis para futuras chamadas de leitura (inclusive por processos subsequentes, caso o processo do programa termine) ou até o terminal ser fechado. Isso pode causar efeitos inesperados em programas interativos executados na sequência (como o próprio shell, por exemplo), pois o conteúdo remanescente no buffer do terminal será lido automaticamente.

Veja este exemplo:

#include <stdio.h>
#include <unistd.h>

#define BUFMAX 10

int main(void) {

char buf[BUFMAX];
ssize_t bytes = 0;

if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
// Tratamento do terminador nulo...
buf[bytes] = '\0';

printf("%s\n", buf);

return 0;
}

Compilando e executando:

:~$ gcc -Wall teste.c
:~$ ./a.out
123456789012345
123456789
:~$ 012345 <-- O Bash tentou interpretar e executar o conteúdo do buffer!
bash: 012345: comando não encontrado

A função read consumiu apenas os 9 primeiros caracteres digitados, mas os 6 caracteres restantes continuaram no buffer do terminal e foram lidos e interpretados pelo processo do shell (interativo) como a linha de um comando.

14.2.4. Descarga do buffer do terminal

A solução para isso é simples e já é velha conhecida: consumir todos os bytes restantes na entrada padrão, por exemplo, com a função que nós criamos em outras aulas…

void flush_stdin(void) {
char c;
while((c = getchar()) != '\n' && c != EOF);
}

Mas esta função utiliza getchar que, assim como fgets e scanf, fará a leitura do buffer de entrada criado no processo pela glibc. Certamente, o conteúdo do buffer do terminal será copiado para o buffer de entrada do programa. Porém, sem garantias de que haverá algo no buffer do terminal, pode acontecer que a nossa função fique presa em getchar até que o usuário tecle Enter novamente, enviando a quebra de linha que o loop while espera para terminar.

Por exemplo, utilizando a função flush_stdin, o nosso exemplo funcionaria corretamente quando houvesse mais caracteres digitados do que o read pode consumir:

:~$ gcc -Wall teste.c
:~$ ./a.out
123456789012345
123456789
:~$

Contudo, digitando menos caracteres do que o limite, nós teríamos que teclar Enter duas vezes: a primeira para a leitura de read e a segunda para getchar…

:~$ gcc -Wall teste.c
:~$ ./a.out
12345
<--- Aguardando um segundo ENTER
12345
<--- Quebra de linha na string impressa
:~$

A segunda linha em branco era esperada porque nós não fizemos nada para remover a quebra de linha lida por read, mas a primeira aconteceu porque eu tive que teclar Enter mais uma vez para que getchar tivesse algo para ler.

14.2.5. Implementação de 'flushstdin' com a chamada 'read'

Antes de passarmos à solução da descarga do buffer do terminal, vamos aproveitar que estamos lidando com chamadas de sistema para implementar uma versão da função flush_stdin com read, em vez de getchar:

void flush_tty_buffer(void) {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n');
}

Importante! Tanto read quanto STDIN_FILENO dependem da inclusão de unistd.h.

O princípio é o mesmo, só que, desta vez, o loop vai continuar enquanto read retornar que leu 1 byte e este byte for diferente de '\n'. A outra diferença é que, em vez de ler o buffer do programa, a nossa nova função lerá diretamente o buffer do terminal.

Esta função não poderia ser utilizada se os bytes remanescentes estivessem no buffer de entrada do programa!

Mesmo assim, o problema continua: se o buffer do terminal estiver vazio, read ficará esperando o recebimento de uma linha, o que só acontecerá quando nós digitarmos novamente um Enter.

14.2.6. Descarga condicional do buffer do terminal

Se a causa do problema é a possibilidade do buffer do terminal estar vazio, basta condicionar a chamada de flush_stdin, ou de flush_tty_buffer, à verificação desta condição, o que pode ser avaliado de, pelo menos, duas formas: utilizando os bytes lidos e a sua quantidade ou verificando o estado do buffer do terminal.

Solução 1: Teste da presença de '\n'

buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x0A | 0X00 |
+------+------+------+------+------+------+------+------+------+------+

Aqui, a digitação foi 12345678\n (exatamente BUFMAX - 1 bytes) e, mesmo que fosse digitada qualquer quantidade de bytes menor que essa, o buffer do terminal estaria vazio e o caractere '\n' estaria presente na string. Logo, nós poderíamos utilizar a seguinte condição:

if (buf[bytes - 1] != '\n') {
flush_stdout(); // Ou flush_tty_buffer
}

É uma solução interessante porque, além de simples, ela já verifica a existência da quebra de linha na string e nós poderíamos aproveitar, se fosse o caso, para removê-la:

if (buf[bytes - 1] == '\n') {
buf[bytes - 1] = '\0'; // Remoção da quabra de linha
} else {
flush_stdout(); // Ou flush_tty_buffer
}

Solução 2: verificação do estado do buffer do terminal

A chamada de sistema ioctl, no cabeçalho sys/ioctl.h, manipula parâmetros de dispositivos especiais (como terminais, por exemplo). Seus argumentos são:

- O descritor de arquivos do dispositivo (ex.: 0 ou STDIN_FILENO);

- O número da operação com o dispositivo (ex.: FIONREAD, quantidade de bytes disponíveis para leitura imediata);

- Um ponteiro para o endereço que vai receber a informação obtida.

Sendo assim, nós poderíamos criar uma função para retornar 0, no caso de buffer vazio, ou a quantidade de bytes remanescentes no buffer do terminal:

#include <sys/ioctl.h>

...

int is_tty_empty(void) {
int r = 0;
return (ioctl(STDIN_FILENO, FIONREAD, &r) == 0);
}

Portanto, nossa função retornaria 1 (verdadeiro), se o buffer do terminal estivesse vazio, ou 0 (falso), se não estivesse.

A ideia de criar uma função, em vez de utilizar ioctl diretamente, tem a ver com a atribuição de um valor semântico ao código.

No nosso exemplo, ela poderia ser utilizada desta maneira:

if (!is_tty_empty()) {
flush_stdout(); // Ou flush_tty_buffer
}

Qual solução utilizar?

Não existe uma resposta certa neste caso: tudo depende do contexto do programa. Se eu estivesse buscando uma solução mais robusta e aplicável em diversas situações, eu escolheria a segunda (ioctl), mas escolheria a primeira em contextos menos rigorosos, onde a quebra de linha tivesse que ser detectada e removida da string de qualquer forma.

Eu vou optar pela primeira solução desta vez, então o nosso código ficará assim:

#include <stdio.h>
#include <unistd.h>

#define BUFMAX 10

void flush_tty_buffer(void) {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n');
}

int main(void) {

char buf[BUFMAX];
ssize_t bytes = 0;

if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}

// Tratamento do terminador nulo...
buf[bytes] = '\0';

if (buf[bytes - 1] == '\n') {
// Remoção condicional da quebra de linha...
buf[bytes - 1] = '\0';
} else {
// Esvaziamento condicional do buffer do terminal...
flush_tty_buffer(); // Ou flush_stdin
}

printf("%s\n", buf);

return 0;
}

Compilando e testando…

:~$ gcc -Wall teste.c
:~$ ./a.out
12345 <-- 6 bytes digitados (buffer tty vazio)
12345 <-- Quebra de linha removida
:~$ ./a.out
12345678 <-- 9 bytes digitados (buffer vazio)
12345678 <-- Quebra de linha removida
:~$ ./a.out
123456789012345 <-- 16 bytes digitados (7 bytes no buffer tty)
123456789 <-- Não havia quebra de linha para remover
:~$

14.2.7. Definindo um prompt

O nosso programa precisa de um prompt, então vamos implementá-lo:

...

char buf[BUFMAX];
ssize_t bytes = 0;

printf("Digite algo: "); // Nosso prompt!

if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}

...

Agora, nós temos a impressão da string "Digite algo: " antes da leitura do terminal. Entretanto, ao compilar e executar o programa, nada é exibido até nós teclarmos Enter:

:~$ gcc -Wall teste.c
:~$ ./a.out
1234567890
Digite algo: 123456789 <-- O prompt só foi impresso aqui!
:~$

Isso acontece porque printf utiliza o buffer de saída do programa (criado pela biblioteca padrão) e, por padrão, os dados na saída podem ser:

- Totalmente bufferizados: quando o programa escreve em arquivos ou pipes;

- Bufferizados por linha: quando a escrita é em um terminal.

Como estamos escrevendo no terminal, a saída de printf é bufferizada por linha e só é liberada quando o buffer de saída fica cheio (geralmente, 8192 bytes), quando recebe o caractere de quebra de linha ('\n') ou quando é esvaziada explicitamente.

No caso do exemplo, a saída não foi descarregada porque a string na chamada de printf não tem uma quebra de linha (exatamente como nós queremos). Uma solução simples, porém, é descarregar o buffer explicitamente com a função fflush:

...

char buf[BUFMAX];
ssize_t count = 0;

// Prompt...
printf("Digite algo: ");
// Descarga do buffer de saída...
fflush(stdout);

if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1;
}

...

Compilando e testando:

:~$ gcc -Wall teste.c
:~$ ./a.out
Digite algo: 1234567890
123456789
:~$

O problema da bufferização por linha não é exatamente causado pelo uso da chamada de sistema read. Na verdade, as funções de mais alto nível (como fgets e scanf) é que são implementadas com mecanismos para descargar o buffer de saída automaticamente.

14.3. Exercícios

14.3.1. Desafio: Função 'readline'

Crie uma função que leia uma única linha digitada no terminal e receba como único argumento uma string que servirá como prompt.

Características da função:

- Leitura deve ser implementada com a chamada de sistema read.

- A função implementa um buffer estático de tamanho BUFSIZ (stdio.h) para receber a digitação.

- A leitura continua até uma quebra de linha ou até BUFSIZ.

- Consome todos os caracteres digitados, inclusive a quebra de linha.

- Em caso de sucesso, retornará o endereço do buffer estático.

- Testa se está sendo utilizada com pipes e redirecionamentos e termina com erro, se este for o caso.

- Toda e qualquer situação de erro deve causar retorno NULL.

- O prompt é impresso sem quebra de linha.

- Os bytes lidos devem ser convertidos em uma string (terminadas com '\0').

- A quebra de linha deve ser removida da string.

Protótipo da função:

char *readline(const char *prompt);

14.3.2. Desafio: Funções auxiliares

Utilizando a chamada de sistema read, implemente as funções abaixo:

- int str_size(const char *str): Retorna a quantidade de bytes da string str.

- void trim_str(const char *str): Remove espaços em branco e quebras de linha nas extremidades da string str.

- char **str_split(const char *str, cont char *delim): Transforma uma string em um vetor de strings separadas na ocorrência do delimitador delim.

- char *str_join(const char *str, const char *glue): Transforma um vetor de strings em uma string única com os elementos do vetor unidos por glue.

15. Abertura de arquivos para leitura

Vídeo desta aula

15.1. Como os arquivos são acessados

Em sistemas Unix-like, um arquivo é qualquer abstração de recursos de entrada e saída representada como fluxos de dados (streams) acessados por meio de operações padronizadas, independentemente de seu conteúdo ou finalidade.

Neste sentido, existem sete tipos de arquivos:

- Diretórios
- Arquivos regulares (ou comuns)
- Ligações simbólicas
- Dispositivos especiais caractere
- Dispositivos especiais de bloco
- Arquivos de pipe (ou FIFO)
- Sockets

Quando queremos acessar um arquivo, seja de que tipo for, nós precisamos que o sistema disponibilize vários recursos, como:

- Um descritor de arquivo (file descriptor): um número inteiro que identifica de forma única o arquivo aberto no contexto do processo.

- Uma tabela de descritores do processo: uma estrutura que associa os descritores de arquivos a entradas na tabela global de arquivos abertos.

- A tabela global de arquivos abertos (file table): que armazena informações como a posição atual de leitura/gravação e as flags de acesso.

- A tabela de inodes: que contém os metadados do arquivo, como permissões, o "dono" do arquivo, o tipo do arquivo e o ponteiro para os dados no disco.

- Os buffers do sistema: usados para otimizar acesso ao conteúdo do arquivo acumulando dados temporariamente na memória.

Na linguagem C, tudo isso é abstraído como o tipo FILE *, que é um ponteiro para a estrutura de dados (uma struct) disponibilizada para cada operação de acesso ao fluxo de dados (stream) que irá representar o arquivo.

Resumindo:

- No sistema, arquivos são abstrações de recursos de entrada e saída representados como fluxos de dados;

- Na linguagem C, os fluxos de dados, bem como os recursos que eles representam, são abstraídos com o tipo FILE * (ponteiro para FILE).

O tipo FILE * é definido na biblioteca padrão (cabeçalho stdio.h).

15.1.1. Diferença entre streams e descritores de arquivos

Descritores de arquivos (tipo int)

- São expressos como inteiros (tipo int).

- Visíveis em /proc/PID/fd.

- Identificam arquivos em chamadas de sistema (open, close, read, write, etc).

- Representam diretamente uma entrada na tabela de arquivos abertos do processo.

Stream (tipo ponteiro para FILE)

- São abstrações de todos os recursos abertos para operações com arquivos.

- Utilizados em funções da biblioteca padrão (fopen, fclose, fgets, fprintf, etc).

- Utilizam descritores de arquivos para interagir com o sistema.

- Envolvem acumulação (buffering) na memória do processo.

15.1.2. Procedimento geral de acesso a arquivos para leitura

Geralmente, as operações com arquivos envolvem três etapas: abertura, processamento e fechamento.

Abertura

- Disponibilização dos recursos para acesso ao arquivo.

- Definição do tipo de operação (escrita, append e/ou leitura).

- Com a chamada de sistema open, recebemos um descritor de arquivos.

- Com a função fopen, recebemos um ponteiro para FILE (stream).

- Com fopen, um ponteiro nulo (NULL) será retornado no caso de erros.

- Com open, o retorno será -1 no caso de erros.

- Com ambas as funções, a variável errno receberá a identificação do erro.

Neste curso, nós utilizaremos apenas a função fopen.

Exemplo:

char *file = "arquivo.txt";
FILE *stream = fopen(file, "r");

Aqui, o arquivo arquivo.txt, se existir no diretório corrente, será aberto para leitura (modo de abertura "r"). Se tudo correr bem, nós teremos o fluxo de dados stream para manipular o arquivo.

Para tratar eventuais erros, nós podemos fazer algo assim:

char *file = "arquivo.txt";
FILE *stream = fopen(file, "r");
if (!stream) {
perror("Erro ao abrir o arquivo");
return EXIT_FAILURE;
}

A função perror imprime, na saída padrão de erros (stderr), a descrição do último erro atribuído à variável errno. A string, passada como argumento, será utilizada como prefixo da mensagem impressa.

Processamento

É quando nós realizamos alguma operação com o arquivo, como ler seu conteúdo, escrever algo nele, obter seu tamanho em bytes, reposicionar seu ponteiro interno, etc. Por exemplo, nós podemos abrir o arquivo /etc/shells para ler e imprimir todo seu conteúdo:

char *file = "/etc/shells";

// Abertura do arquivo...
FILE *stream = fopen(file, "r");
if (!stream) {
perror("Erro ao abrir o arquivo");
return EXIT_FAILURE;
}

// Processamento...
char line[BUFSIZ]; // Buffer para acumular cada linha lida
while (fgets(line, BUFSIZ, stream)) {
printf("%s", line);
}

Aqui, fgets lerá o conteúdo do arquivo associado a stream (/etc/shells) até encontrar uma quebra de linha ou chagar a BUFSIZ-1 bytes.

A macro BUFSIZ é definida em stdio.h e expande o valor 8192 (8kB).

De um modo ou de outro, a estrutura de dados associada a stream registrará a quantidade de bytes que já foram lidos para que, no caso de um acesso subsequente, a leitura continue a partir do byte seguinte – isso é o que chamamos de ponteiro interno do arquivo.

O ponteiro interno do arquivo pode ser obtido e manipulado com as funções fseek, ftell e rewind.

Consequentemente, enquanto fgets não retornar NULL (no caso de um erro ou do fim do arquivo), o conteúdo do buffer line será impresso e o arquivo será lido novamente a partir da nova posição de seu ponteiro interno.

Fechamento

- Liberação dos recursos disponibilizados na abertura.

- Tratamento adequado para os dados pendentes.

- Possibilita a verificação de erros de fechamento.

- Com close ou fclose, o retorno será 0 em caso de sucesso.

- Com close, o retorno será -1 em caso de erro.

- Com fclose, o retorno será EOF em caso de erro (que também tem valor -1).

- Com ambas as funções, a variável errno receberá a identificação do erro.

Os arquivos abertos são fechados com o fim do processo, mas é sempre preferível fechá-los explicitamente para garantir a integridade dos dados e evitar vulnerabilidades (como o vazamento de recursos).

Para fechar o arquivo do nosso exemplo:

char *file = "/etc/shells";

// Abertura do arquivo...
FILE *stream = fopen(file, "r");
if (!stream) {
perror("Erro ao abrir o arquivo");
return EXIT_FAILURE;
}

// Processamento...
char line[BUFSIZ]; // Buffer para acumular cada linha lida
while (fgets(line, BUFSIZ, stream)) {
printf("%s", line);
}

// Fechamento do arquivo...
fclose(stream);

15.2. Exercícios

15.2.1. O arquivo 'passwd'

Cada linha do arquivo /etc/passwd representa um usuário do sistema e tem o seguinte formato:

nome:senha:uid:gid:gecos:home:shell

O termo GECOS vem de General Electric Comprehensive Operating Supervisor, e era o nome de um antigo sistema operacional do início dos anos 1960. Como as primeiras versões do Unix precisavam interagir com o ambiente GECOS, o campo foi introduzido em /etc/passwd para receber informações de contato do usuário em um formato compatível (nome completo, sala, ramal, etc).

Exemplo:

blau:x:1000:1000:Blau Araujo:/home/blau:/bin/bash

Com base nisso…

Impressão do conteúdo do arquivo

Abra /etc/passwd para leitura e imprima todo seu conteúdo, linha por linha.

Número de usuários

Leia o arquivo e imprima a quantidade total de usuários no seu sistema.

Busca de usuários

Tendo o nome de um usuário como argumento (por exemplo, root), imprima apenas a linha de /etc/passwd correspondente.

Dica: utilize funções como strstr ou strncmp para encontrar strings nas linhas, mas cuidado com os falsos positivos!

Informações de usuários

Tendo um nome de usuário como argumento, imprima seus dados no formato:

Login : ...
GECOS : ...
Diretório home: ...
Shell padrão : ...

Dica: a função strtok pode ser uma ótima opção para separar a linha em campos, mas não é a única opção.

16. Abertura de arquivos para escrita

Vídeo desta aula

16.1. Modos de abertura de arquivos

Em relação ao sentido do fluxo de dados, nós podemos realizar duas operações com arquivos abertos: leitura e escrita. Com a função fopen, isso é definido em seu segundo argumento através de uma string de modo:

String | Modo | Descrição
"r" | Leitura | Abre o arquivo para leitura a partir de seu início. Se o arquivo não existir, causa um erro.
"w" | Escrita | Apaga os dados originais do arquivo para escrever novos. Se o arquivo não existir, ele é criado.
"a" | Append | Abre o arquivo para acrescentar novos dados ao seu final. Se o arquivo não existir, ele é criado.

Na chamada de sistema open, as strings de modo seriam equivalentes às flags:

- "r" → O_RDONLY
- "w" → O_WRONLY | O_CREAT | O_TRUNC
- "a" → O_WRONLY | O_CREAT | O_APPEND

As flags múltiplas são passadas como operações OR bit-a-bit.

16.1.1. Modos complementares

Incluindo o caractere + ao final da string de modo, também será possível realizar a operação oposta:

String | Modo | Descrição
"r+" | Leitura e escrita | Posiciona o ponteiro interno no início do arquivo para leitura e escrita. Se o arquivo não existir, causa um erro.
"w+" | Escrita e leitura | Apaga os dados originais do arquivo para escrever novos e ler o que for escrito. Se o arquivo não existir, ele é criado.

"a+"

Append e leitura

Posiciona o ponteiro interno no fim do arquivo para leitura e para acrescentar novos dados. Se o arquivo não existir, ele é criado.

Nós voltaremos a este assunto na próxima aula, mas deve ser evidente que precisaremos controlar o ponteiro interno do arquivo para realizar operações de leitura e escrita em um mesmo stream.

16.1.2. Modos texto e binário

Sistemas Unix-like não fazem distinção entre arquivos texto e binários em operações de entrada e saída (I/O). Portanto, o caractere b, que pode ser incluído ao final ou entre os caracteres da string de modo, não terá efeito e só precisaria ser utilizado no caso da possibilidade do programa ser portado para outros sistemas.

16.2. Modos de escrita

Em termos gerais, as etapas de trabalho com arquivos abertos para escrita ou append são os mesmos de quando falamos da leitura:

- Abertura do arquivo para obter um stream;
- Processamento dos dados;
- Fechamento do stream.

16.2.1. Abertura de um arquivo para escrita ("w")

Neste exemplo, o arquivo linha.txt será criado (ou truncado, se já existir) para receber uma linha digitada pelo usuário no terminal:

// Abertura do arquivo para escrita...
char *file = "linha.txt";
FILE *stream = fopen(file, "w");
if (!stream) {
perror("Erro na abertura do arquivo");
return EXIT_FAILURE;
}

// Processamento...
char line[BUFSIZ]; // Buffer para receber a linha digitada.
fgets(line, BUFSIZ, stdin); // Lê a digitação no terminal.
fprintf(stream, "%s", line) // Copia a string no buffer para o arquivo.

// Fechamento do arquivo...
fclose(stream);

Assim, toda vez que o programa for executado, seu conteúdo será substituído pelo que nós digitarmos (exceto na primeira vez, quando o arquivo for criado).

16.2.2. Abertura de um arquivo para append ("a")

Para que o arquivo linha.txt receba novas linhas a cada execução do exemplo anterior, basta trocar a string de modo:

// Abertura do arquivo para append...
char *file = "linha.txt";
FILE *stream = fopen(file, "a");
if (!stream) {
perror("Erro na abertura do arquivo");
return EXIT_FAILURE;
}

// Processamento...
char line[BUFSIZ]; // Buffer para receber a linha digitada.
fgets(line, BUFSIZ, stdin); // Lê a digitação no terminal.
fprintf(stream, "%s", line) // Copia a string no buffer para o arquivo.

// Fechamento do arquivo...
fclose(stream);

16.2.3. Notas gerais

- Enquanto o arquivo estiver aberto, nós podemos realizar diversas operações de escrita.

- No caso do modo "w", o conteúdo anterior do arquivo só será truncado na sua abertura, não a cada escrita com o arquivo já aberto.

- Toda vez que algo é escrito no arquivo, seu ponteiro interno é posicionado no byte seguinte ao último byte escrito.

16.3. Exercícios

16.3.1. Programa 'nf' (new file)

No Unix e no GNU, é comum as pessoas associarem a criação de novos arquivos vazios ao utilitário touch, mas isso é só um efeito colateral de seu verdadeiro propósito, que é alterar a data/hora de último acesso ao arquivo. Sendo assim, crie um programa realmente dedicado a criar novos arquivos vazios.

Uso proposto:

nf ARQUIVOS...

Notas:

- Os caminhos e nomes dos novos arquivos devem ser passados como argumentos.

- Se nenhum argumento for passado, o programa terminará com erro, imprimindo suas informações de uso.

- Se algum dos arquivos já existir, o programa não fará nada com ele e apenas exibirá uma mensagem em stderr com o seguinte formato:

Arquivo NOME já existe: nada será feito!

- Para cada caminho inexistente, o programa deve exibir em stderr:

O diretório DIR não existe: impossível criar ARQUIVO!

- Se algum arquivo não puder ser criado por falta de privilégios do usuário, a mensagem de erro deve ser:

Impossível criar ARQUIVO: acesso negado!

- Mesmo que algum arquivo já exista, os demais devem ser criados normalmente.

- Ao terminar, o programa deve exibir em stdout a quantidade de arquivos criados no seguinte formato:

Arquivos criados: NUM_ARQUIVOS_CRIADOS de NUM_DE_ARQUIVOS_SOLICITADOS

16.3.2. Programa 'hd' (here doc)

No shell do Unix e do GNU, é possível criar novos arquivos digitando seus conteúdos com o mecanismo do here doc:

:~$ cat << FIM >> ARQUIVO

Assim, o utilitário cat lê as linhas digitadas, imprime essas linhas e o shell as redireciona para append em ARQUIVO. Portanto, crie um programa que reproduza este comportamento de forma autônoma, ou seja, sem operadores do shell.

Uso proposto:

hd ARQUIVO

Requisitos:

- Apenas o primeiro argumento será aceito como o caminho e o nome do arquivo que receberá as linhas digitadas no terminal.

- O programa deve avisar ao usuário se o arquivo já existir, dando a opção de continuar com o seguinte prompt:

O arquivo ARQUIVO já existe, deseja modifícá-lo (s/N)?

- A opção padrão, aceita com Enter, é N (não).

- Se o usuário aceitar continuar, as novas linhas devem ser escritas ao final do arquivo.

- O programa vai parar de receber novas linhas quando o usuário teclar Ctrl+D.

- Antes de terminar, o programa deve exibir o conteúdo atual do arquivo.

17. Abertura de arquivos para leitura e escrita

Vídeo desta aula

17.1. Modos de abertura para leitura e escrita

As strings de modos "r", "w" e "a", da função fopen, quando sucedidas do caractere +, indicam a abertura do arquivo também para suas respectivas operações contrarias:

Modo | Leitura | Escrita | Truncar | Offset | Criar
"r" | Sim | Não | Não | Início | Não
"r+" | Sim | Sim | Não | Início | Não
"w" | Não | Sim | Sim | Início | Sim
"w+" | Sim | Sim | Sim | Início | Sim
"a" | Não | Sim | Não | Fim | Sim
"a+" | Sim | Sim | Não | Fim | Sim

De acordo com a tabela acima, nós precisamos observar alguns pontos importantes:

- Se a string de modo iniciar com w, o arquivo sempre será truncado na sua abertura.

- Se a string de modo iniciar com a, o ponteiro interno sempre será posicionado no fim do arquivo na sua abertura.

- Se a string de modo iniciar com r, o ponteiro interno sempre será posicionado no início do arquivo na sua abertura.

- Se não existir, o arquivo só será criado se a string de modo iniciar com w ou a.

17.1.1. Manipulação do ponteiro interno

O ponteiro interno do arquivo representa o número do byte que será lido ou escrito no próximo acesso: ou seja, seu offset (ou ponto de partida). Isso é particularmente importante quando abrimos arquivos para ambas as operações com as strings de modo "r+" ou "a+", visto que o modo "w+" truncará o conteúdo do arquivo.

A função 'fseek'

As funções da família fseek (stdio.h) são utilizadas para posicionar ou obter informações sobre o ponteiro interno do arquivo:

Função | Descrição | Retorno
fseek | Define a posição do ponteiro interno. | Sucesso: 0 (int)
ftell | Obtém a posição corrente do ponteiro interno. | Sucesso: número do byte (long)
rewind | Posiciona o ponteiro interno no byte 0. | void
fsetpos | Interface para fseek referenciando a definição do offset a partir do início do arquivo. | Sucesso: 0 (int)
fgetpos | Interface para ftell escrevendo o offset obtido em um dado endereço. | Sucesso: 0 (int)

Notas:

- Com a função fseek, o novo offset é definido com uma expressão do tipo long.

- Com a função fsetpos, o novo offset é definido em um ponteiro do tipo fpos_t.

- Com a função fgetpos, o offset corrente é escrito em um ponteiro do tipo fpos_t.

A função fseek trabalha com streams, mas é possível manipular o ponteiro interno a partir de descritores de arquivos com a chamada de sistema lseek.

Pontos de origem de 'fseek'

A função fseek recebe 3 argumentos:

- O stream associado ao arquivo;

- A expressão do valor do offset (tipo long);

- Um inteiro determinando o ponto de origem para a definição do novo offset.

Este é o seu protótipo na glibc (stdio.h):

int fseek(FILE *stream, long offset, int whence);

O termo whence significa "a partir de onde" e representa o ponto de origem do deslocamento do ponteiro interno do arquivo.

O terceiro argumento (whence) pode receber qualquer valor inteiro, mas a glibc oferece três macros para os pontos de origem mais comuns:

- SEEK_SET: o início do arquivo;
- SEEK_END: o fim do arquivo;
- SEEK_CUR: o offset corrente.

17.1.2. Abertura de arquivos com "r+"

Como o offset inicial será o byte 0, se a primeira operação for uma escrita, os primeiros bytes do arquivo serão sobrescritos. Caso esta não seja a intenção, será preciso deslocar o ponteiro interno para o fim do arquivo:

// Abertura do arquivo para leitura e escrita...
char *file = "arquivo.txt";
FILE *stream = fopen(file, "r+");

// Posicionar offset no fim do arquivo...
fseek(stream, 0, SEEK_END);

// Rotinas de escrita...

Aqui, nós deslocamos o ponteiro interno em zero bytes a partir do fim do arquivo (origem em SEEK_END).

17.1.3. Abertura de arquivos com "a+"

O offset inicial será o fim do arquivo e, se quisermos começar a leitura a partir de seu início, será preciso deslocar o ponteiro interno para lá:

// Abertura do arquivo para leitura e escrita...
char *file = "arquivo.txt";
FILE *stream = fopen(file, "r+");

// Posicionar offset no início do arquivo...
fseek(stream, 0, SEEK_SET);

// Rotinas de leitura...

Desta vez, nós deslocamos o ponteiro interno em zero bytes a partir do início do arquivo (origem em SEEK_SET).

17.1.4. Procedimentos úteis

Para encerrar esta aula (e o nosso curso), aqui estão alguns exemplos de procedimentos gerais úteis envolvendo a manipulação de arquivos.

Quantidade de linhas

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
// Abertura do arquivo para leitura...
char *file = "meuarquivo.txt";
FILE *stream = fopen(file, "r");
if (!stream) {
fprintf(stderr, "Erro ao abrir o arquivo!\n");
return EXIT_FAILURE;
}

// Contagem de linhas...
char buf[BUFSIZ]; // Buffer para ler blocos do arquivo (8192 bytes).
int lines = 0; // Total de linhas encontradas.

// Lê uma linha até encontrar \n, até BUFSIZ-1 ou até EOF...
while (fgets(buf, BUFSIZ, stream) != NULL) {
// Só conta uma linha se houver \n no buffer...
if (buf[strlen(buf) - 1] == '\n' ) lines++;
}
// Se a última linha não contiver \n, ela também será contada...
if (lines > 0 && buf[strlen(buf) - 1] != '\n') lines++;

// Fechamento do arquivo...
fclose(stream);

// Impressão do resultado...
printf("%d linhas", lines);

return EXIT_SUCCESS;
}

Obter o tamanho do arquivo

#include <stdio.h>
#include <stdlib.h>

int main(void) {
// Abertura do arquivo...
char *file = "meuarquivo.txt";
FILE *stream = fopen(file, "rb");
if (!stream) {
perror("Erro ao abrir o arquivo");
return EXIT_FAILURE;
}

// Posiciona o ponteiro de leitura no final do arquivo...
if (fseek(stream, 0, SEEK_END) != 0) {
perror("Erro ao deslocar o offset para o final");
fclose(stream);
return EXIT_FAILURE;
}

// Obtém o offset corrente (tamanho do arquivo em bytes)...
long fsize = ftell(stream);
if (fsize == -1L) {
perror("Erro ao obter o offset corrente");
fclose(stream);
return EXIT_FAILURE;
}

// Exibe o tamanho do arquivo...
printf("Tamanho do arquivo: %ld bytes\n", fsize);

// Fechamento do arquivo...
fclose(stream);
return EXIT_SUCCESS;
}

Testar se um arquivo está vazio (0 bytes)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
// Abertura do arquivo...
char *file = "meuarquivo.txt";
FILE *stream = fopen(file, "r");
if (!stream) {
perror("Erro na abertura do arquivo");
return EXIT_FAILURE;
}

// is_empty recebe 1 se o arquivo estiver vazio...
int is_empty = (fseek(stream, 0, SEEK_END) == 0 && ftell(stream) == 0);

// Fechamento do arquivo...
fclose(stream);

// O programa terminará com sucesso (0) se o arquivo estiver vazio...
return !is_empty;
}

17.2. Exercícios

17.2.1. Contando e inserindo linhas

Dado o arquivo lorem.txt, abaixo:

Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio.
Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.
Integer in mauris eu nibh euismod gravida. Duis ac turpis.
Integer vitae libero ac risus egestas placerat. Praesent eu lacus quam.
Morbi ac felis. Suspendisse ac metus gravida, cursus nunc eu, dictum risus.

Escreva um programa que determine quantas linhas ele tem e escreva esse resultado no início deste mesmo arquivo segundo o formato abaixo:

Este arquivo tinha X linhas, agora tem X+2 linhas...

Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio.
Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.
Integer in mauris eu nibh euismod gravida. Duis ac turpis.
Integer vitae libero ac risus egestas placerat. Praesent eu lacus quam.
Morbi ac felis. Suspendisse ac metus gravida, cursus nunc eu, dictum risus.

17.2.2. Desafio: Trocando palavras

Utilizando o mesmo arquivo anterior (na sua forma original), crie um programa que substitua todas as ocorrências da palavra gravida por >ACHEI<, imprimindo no terminal a quantidade de ocorrências e em que linhas e colunas do texto elas foram encontradas.

17.2.3. Desafio: O código Da Vinci

Utilizando, mais uma vez, o arquivo de exemplo na sua forma original, crie um programa que reescreva seu conteúdo invertendo a ordem dos caracteres de cada linha, como se estivéssemos vendo o texto em um espelho.

Date: 9 de maio de 2025

Author: Blau Araujo

Created: 2025-09-12 sex 14:03

Validate

Table of Contents

- Sobre o curso

- Do que vamos precisar...

- Requisitos...

- O que vamos aprender...

- Como vamos aprender...

- Por que aprender...

- Como clonar o repositório do curso...

- Como atualizar o repositório do curso...

- História

- Resumo do vídeo
- Conceitos abordados na aula
- Características da linguagem C
- Implementação GNU/Linux
- Compilação
- Exemplo de um programa em C
- Exercícios

- Dados e instruções

- O que são programas
- Dúvidas da aula ao vivo
- Exercícios

- Tipos de dados

- Exemplo da última aula
- Tipos são tamanhos de dados na memória
- Exemplo corrigido
- Exercícios

- Variáveis e ponteiros

- Qual é a diferença?
- Variáveis
- Ponteiros
- Perguntas da aula ao vivo
- Exercícios

- Estruturas de controle de fluxo

- O que são estruturas de controle?

- Exercícios

- Vetores

- O que são vetores

- Exercícios

- Vetores, ponteiros e strings

- Relações entre vetores e ponteiros

- Exercícios

- Processos e layout de memória

- Filosofia UNIX
- O que são processos
- Layout de memória

- O vetor de argumentos

- Vetores e strings

- Exercícios

- Entrada e saída de dados

- Tabela de descritores de arquivos

- Leitura da entrada padrão com 'scanf'

- Verificando a ligação com a entrada padrão
- Leitura interativa da entrada padrão
- Exercícios

- Leitura da entrada padrão com 'fgets'

- Ainda sobre a função 'scanf'
- A função 'fgets'
- Exercícios

- Leitura da entrada padrão com 'read'

- Chamadas de sistema
- A função 'read'
- Exercícios

- Abertura de arquivos para leitura

- Como os arquivos são acessados

- Exercícios

- Abertura de arquivos para escrita

- Modos de abertura de arquivos
- Modos de escrita
- Exercícios

- Abertura de arquivos para leitura e escrita

- Modos de abertura para leitura e escrita

- Exercícios