Material Extra - Revisão de Linguagem C (Parte 1)
Nesta aula faremos uma revisão dos principais conceitos da linguagem C necessários para o restante da disciplina. Assume-se que todos os alunos já cursaram uma disciplina introdutória de programação e possuem familiaridade com a linguagem C. Assim, o objetivo desta aula não é ensinar programação desde o início, mas revisar rapidamente os conceitos que servirão de base para as próximas aulas.
Nesta primeira parte revisaremos conceitos fundamentais de algoritmos, programas, compilação, variáveis, operadores, estruturas básicas da linguagem, vetores, matrizes e strings. No laboratório de revisão de linguagem C (Material Extra) revisaremos os recursos mais importantes para Estruturas de Dados, como funções, modularização, ponteiros, structs e arquivos.
Algoritmos e programas
Antes de escrever qualquer programa é necessário desenvolver um algoritmo.
Um algoritmo é uma sequência finita e ordenada de instruções capaz de resolver um problema bem definido. Um algoritmo deve sempre produzir um resultado correto após um número finito de passos.
Considere, por exemplo, o problema de calcular o quadrado de um número inteiro. Um algoritmo simples para essa tarefa poderia ser descrito da seguinte forma:
1. Ler um número inteiro N. 2. Calcular N × N. 3. Exibir o resultado.
Observe que esse algoritmo independe da linguagem de programação utilizada. Ele descreve apenas a lógica necessária para resolver o problema.
Um programa, por outro lado, é a implementação de um algoritmo utilizando uma linguagem de programação.
Por exemplo, o algoritmo anterior pode ser implementado em linguagem C da seguinte maneira:
#include <stdio.h> int main() { int n; scanf("%d", &n); printf("%d\n", n * n); return 0; }
Durante toda a disciplina utilizaremos a linguagem C para implementar algoritmos e estruturas de dados.
Pensamento computacional
Resolver problemas utilizando computadores envolve muito mais do que escrever código.
O desenvolvimento de um programa normalmente segue uma sequência semelhante à apresentada abaixo:
Problema ↓ Análise ↓ Algoritmo ↓ Programa em C ↓ Compilação ↓ Execução ↓ Testes ↓ Correções
Nas disciplinas introdutórias o foco costuma estar na aprendizagem da linguagem de programação. Em Estruturas de Dados, entretanto, o foco passa a ser a escolha das representações e algoritmos mais adequados para resolver problemas de maneira eficiente.
A linguagem C
A linguagem C foi desenvolvida no início da década de 1970 por Dennis Ritchie e permanece, até hoje, uma das linguagens mais importantes da Computação.
Entre suas principais características destacam-se:
- linguagem compilada;
- linguagem imperativa;
- alto desempenho;
- controle explícito da memória;
- grande portabilidade entre sistemas operacionais.
Essas características tornam C uma excelente linguagem para compreender como estruturas de dados são realmente representadas e manipuladas na memória do computador.
Estrutura básica de um programa em C
Todo programa em C possui uma estrutura semelhante à apresentada abaixo:
#include <stdio.h> int main() { printf("Olá, mundo!\n"); return 0; }
Nesse exemplo podemos identificar alguns elementos importantes:
- #include: importa bibliotecas que serão utilizadas pelo programa.
- main(): função principal, onde a execução do programa começa.
- printf(): função utilizada para escrever informações na saída padrão.
- return 0: indica que o programa terminou sua execução corretamente.
À medida que os programas crescerem, novas funções serão adicionadas, mas a estrutura geral permanecerá bastante semelhante.
Compilação e execução
Os programas escritos em C precisam ser traduzidos para linguagem de máquina antes de serem executados. Esse processo é chamado de compilação.
Durante a disciplina utilizaremos o compilador gcc.
Supondo que o programa esteja armazenado em um arquivo chamado main.c, sua compilação pode ser realizada através do comando:
gcc main.c
Após a compilação será gerado um programa executável.
Em sistemas Linux sua execução normalmente é realizada através do comando:
./a.out
É recomendável utilizar sempre a opção -Wall, que habilita diversos avisos importantes emitidos pelo compilador:
gcc -Wall main.c -o programa
Os avisos do compilador frequentemente indicam erros que podem produzir comportamentos inesperados durante a execução. Uma boa prática é corrigir todos os avisos antes de executar qualquer programa.
Variáveis
Variáveis representam posições da memória utilizadas para armazenar informações durante a execução de um programa. Sempre que uma variável é declarada, o compilador reserva um espaço na memória suficiente para armazenar um valor daquele tipo.
Toda variável possui quatro características principais:
| Atributo | Descrição |
|---|---|
| Identificador | Nome utilizado para acessar a variável no programa. |
| Tipo | Define quais valores podem ser armazenados e quanto espaço será reservado na memória. |
| Endereço | Posição da memória onde a variável está armazenada. |
| Valor | Conteúdo atualmente armazenado na variável. |
Os principais tipos primitivos utilizados nesta disciplina ocupam aproximadamente o seguinte espaço na memória:
| Tipo | Tamanho | Exemplo |
|---|---|---|
char |
1 byte | char letra = 'A'; |
int |
4 bytes | int idade = 20; |
float |
4 bytes | float nota = 8.5; |
double |
8 bytes | double media = 7.35; |
Considere o seguinte programa:
#include <stdio.h> int main() { int idade = 20; float nota = 8.5; char sexo = 'F'; double media = 7.35; return 0; }
Durante a execução do programa, o compilador reserva espaço para cada variável. A tabela abaixo apresenta uma representação simplificada da memória. Os endereços são fictícios e utilizados apenas para fins didáticos.
| Endereço | Identificador | Tipo | Tamanho | Valor |
|---|---|---|---|---|
E1000 |
idade |
int |
4 bytes | 20 |
E1004 |
nota |
float |
4 bytes | 8.5 |
E1008 |
sexo |
char |
1 byte | 'F' |
E1009 |
media |
double |
8 bytes | 7.35 |
Podemos representar essa mesma situação através de um mapa simplificado da memória:
Endereço Conteúdo E1000 ───► idade = 20 E1004 ───► nota = 8.5 E1008 ───► sexo = 'F' E1009 ───► media = 7.35
Observe que variáveis de tipos diferentes ocupam quantidades diferentes de memória. O endereço da próxima variável depende da quantidade de bytes ocupada pela variável anterior.
Por exemplo, como um int ocupa 4 bytes, a variável nota começa quatro bytes após idade. Como um char ocupa apenas 1 byte, a variável seguinte inicia logo após ele.
Observação sobre a notação de endereços. Os endereços apresentados nesta seção são apenas exemplos. Em um programa real, o sistema operacional e o compilador escolhem automaticamente onde cada variável será armazenada. Na prática, endereços de memória são normalmente representados em hexadecimal (com o prefixo 0x, como 0x1000). Para não misturar essa conversão de base com os conceitos de memória que estamos revisando agora, vamos usar aqui uma notação simplificada, em decimal: a letra E seguida de um número, como E1000 — lendo-se "endereço 1000". Assim, as contas de "quanto cada variável ocupa" podem ser feitas em decimal, sem a etapa extra de converter para hexadecimal.
|
Além do valor armazenado em uma variável, também é possível manipular diretamente seu endereço utilizando ponteiros. Esse conceito será revisado e é fundamental para implementar listas encadeadas, árvores e diversas outras estruturas de dados.
Operadores
Os operadores permitem realizar operações sobre variáveis e constantes. Em linguagem C eles podem ser classificados em três grupos principais: operadores aritméticos, relacionais e lógicos.
| Categoria | Operador | Descrição | Exemplo |
|---|---|---|---|
| Aritméticos | + |
Adição | a + b |
- |
Subtração | a - b |
|
* |
Multiplicação | a * b |
|
/ |
Divisão | a / b |
|
% |
Resto da divisão inteira | a % b |
|
| Relacionais | > |
Maior que | a > b |
< |
Menor que | a < b |
|
>= |
Maior ou igual | a >= b |
|
<= |
Menor ou igual | a <= b |
|
== |
Igual | a == b |
|
!= |
Diferente | a != b |
|
| Lógicos | && |
E lógico (AND) | x > 0 && y > 0 |
|| |
OU lógico (OR) | x == 0 || y == 0 |
|
! |
Negação lógica (NOT) | !encontrou |
Os operadores relacionais produzem um valor lógico (verdadeiro ou falso), sendo amplamente utilizados em estruturas condicionais e de repetição. Os operadores lógicos permitem combinar ou inverter condições.
Por exemplo:
if (idade >= 18 && nota >= 6.0) { printf("Aprovado.\n"); }
Entrada e saída de dados
A maioria dos programas precisa receber informações do usuário, processá-las e apresentar algum resultado. Em linguagem C, essas operações são realizadas principalmente através das funções da biblioteca stdio.h, responsável pela entrada e saída padrão de dados.
As duas funções mais utilizadas são printf() e scanf(), apresentadas na tabela abaixo.
| Função | Sintaxe | Finalidade |
|---|---|---|
printf() |
printf("formato", argumentos); |
Escreve informações na saída padrão (normalmente, o terminal). |
scanf() |
scanf("formato", &variáveis); |
Lê informações da entrada padrão (normalmente, o teclado). |
A função printf() recebe como primeiro parâmetro uma string de formatação, que determina como as informações serão apresentadas ao usuário. Caso seja necessário imprimir valores armazenados em variáveis, esses valores são informados como parâmetros adicionais, respeitando a mesma ordem em que aparecem os especificadores de formato.
A função scanf() também recebe uma string de formatação, mas sua finalidade é indicar quais tipos de dados deverão ser lidos. Os parâmetros seguintes correspondem às variáveis onde os valores digitados pelo usuário serão armazenados.
Considere o exemplo abaixo:
int idade; float nota; printf("Informe a idade e a nota: "); scanf("%d %f", &idade, ¬a); printf("Idade: %d\n", idade); printf("Nota: %.1f\n", nota); printf("Idade: %d Nota: %.1f\n", idade, nota);
Observe que os especificadores presentes na string de formatação (%d e %f) correspondem exatamente aos parâmetros informados após a string. Em printf(), esses parâmetros são os valores que serão exibidos. Em scanf(), são os endereços das variáveis onde os valores lidos serão armazenados.
Especificadores de formato
Os especificadores de formato indicam o tipo de dado que será lido ou escrito. Os principais utilizados nesta disciplina são apresentados na tabela abaixo.
| Especificador | Tipo de dado | Exemplo |
|---|---|---|
%d |
Inteiro (int) |
printf("%d", idade); |
%f |
Real (float) |
printf("%.2f", nota); |
%lf |
Real (double) |
scanf("%lf", &media); |
%c |
Caractere (char) |
printf("%c", letra); |
%s |
String (char[]) |
printf("%s", nome); |
Observação 1. Na função scanf(), normalmente é necessário utilizar o operador &, que fornece o endereço da variável onde o valor lido será armazenado. Esse conceito será estudado em detalhes quando revisarmos ponteiros. A principal exceção ocorre com strings, pois um vetor de caracteres já representa um endereço de memória e, portanto, normalmente não utilizamos o operador &.
Observação 2. Embora seja bastante utilizada em exemplos introdutórios, a função scanf() possui algumas limitações e deve ser utilizada com cuidado. Leituras incorretas, entradas inválidas ou o uso inadequado de especificadores de formato podem causar comportamentos inesperados. Além disso, funções como scanf("%s", ...) podem provocar problemas de segurança caso o tamanho do vetor não seja controlado. Ao longo da disciplina veremos alternativas mais robustas para leitura de dados quando necessário.
Observação 3. A função printf(), por outro lado, é considerada segura para exibição de dados quando utilizada corretamente. Ela continuará sendo empregada durante toda a disciplina para apresentar mensagens, resultados de algoritmos e conteúdo das estruturas de dados implementadas.
|
As funções de entrada e saída serão utilizadas em praticamente todos os programas desenvolvidos ao longo da disciplina, seja para receber dados do usuário, seja para exibir o conteúdo de vetores, listas, árvores e demais estruturas de dados estudadas.
Operador de atribuição
O operador de atribuição é utilizado para armazenar um valor em uma variável. Em linguagem C, esse operador é representado pelo símbolo =.
Ao executar uma atribuição, o valor da expressão localizada à direita do operador é calculado e armazenado na variável localizada à esquerda.
Alguns exemplos são apresentados na tabela abaixo.
| Instrução | Descrição |
|---|---|
idade = 20; |
A variável idade recebe o valor 20. |
nota = 8.5; |
A variável nota recebe o valor 8.5. |
x = y; |
O valor armazenado em y é copiado para x. |
x = y + 10; |
Primeiro calcula-se y + 10; em seguida o resultado é armazenado em x. |
Considere o programa abaixo:
int a = 10; int b = 20; a = b; printf("%d\n", a);
Após a atribuição, a variável a passa a armazenar o mesmo valor de b.
| Variável | Valor inicial | Valor final |
|---|---|---|
a |
10 | 20 |
b |
20 | 20 |
Observação. Não confunda o operador de atribuição (=) com o operador de comparação (==). O primeiro altera o valor de uma variável, enquanto o segundo verifica se dois valores são iguais.
|
Estruturas condicionais
Em muitos algoritmos é necessário executar determinadas instruções apenas quando uma determinada condição é satisfeita. Para isso utilizamos as estruturas condicionais.
Uma condição é uma expressão lógica cujo resultado pode ser apenas verdadeiro ou falso. Esse resultado é normalmente obtido através da combinação de operadores relacionais e lógicos.
A linguagem C oferece três estruturas condicionais principais.
| Estrutura | Aplicação |
|---|---|
if |
Executa um bloco de código somente quando uma condição é verdadeira. |
if...else |
Escolhe entre dois blocos de código. |
switch |
Seleciona uma entre diversas alternativas. |
Estrutura if
A estrutura if executa um bloco de instruções apenas quando uma determinada condição é verdadeira.
if (nota >= 6.0) { printf("Aprovado!\n"); }
Caso a condição seja falsa, nenhuma instrução será executada e o programa continuará normalmente após o bloco do if.
Estrutura if...else
Quando desejamos executar um bloco caso a condição seja verdadeira e outro bloco caso seja falsa, utilizamos a estrutura if...else.
if (nota >= 6.0) { printf("Aprovado!\n"); } else { printf("Reprovado!\n"); }
Observe que apenas um dos blocos será executado.
Estruturas condicionais aninhadas
É bastante comum que um teste dependa do resultado de outro teste. Nesses casos utilizamos estruturas condicionais aninhadas.
if (nota >= 6.0) { if (frequencia >= 75) { printf("Aprovado!\n"); } else { printf("Reprovado por frequência.\n"); } } else { printf("Reprovado por nota.\n"); }
Outra forma bastante utilizada consiste em escrever uma sequência de testes utilizando else if.
if (nota >= 9.0) { printf("Conceito A\n"); } else if (nota >= 7.5) { printf("Conceito B\n"); } else if (nota >= 6.0) { printf("Conceito C\n"); } else { printf("Conceito D\n"); }
Essa construção torna o código mais organizado do que utilizar diversos if independentes.
Indentação
Embora a linguagem C não exija indentação para funcionar corretamente, ela é uma das principais ferramentas para tornar um programa legível.
Um código mal indentado é difícil de compreender e aumenta significativamente a chance de erros durante o desenvolvimento e manutenção do programa.
Compare os dois exemplos abaixo.
Exemplo com indentação inadequada:
if (nota >= 6.0){ printf("Aprovado\n"); if(frequencia>=75){ printf("Frequência suficiente\n"); } else{ printf("Reprovado por frequência\n"); } }
Exemplo corretamente indentado:
if (nota >= 6.0) { printf("Aprovado\n"); if (frequencia >= 75) { printf("Frequência suficiente\n"); } else { printf("Reprovado por frequência\n"); } }
Observe que a indentação permite identificar facilmente quais instruções pertencem a cada bloco do programa.
Ao longo desta disciplina adotaremos sempre uma indentação consistente utilizando quatro espaços por nível de bloco.
Estrutura switch
Quando existem diversas alternativas baseadas no valor de uma variável inteira ou caractere, pode ser mais conveniente utilizar a estrutura switch.
switch (opcao) { case 1: printf("Inserir\n"); break; case 2: printf("Remover\n"); break; case 3: printf("Buscar\n"); break; default: printf("Opção inválida\n"); }
A instrução break interrompe a execução do switch. Caso ela seja omitida, o programa continuará executando os casos seguintes, comportamento conhecido como fall-through.
| Estrutura | Quando utilizar |
|---|---|
if |
Quando existe apenas uma condição a ser testada. |
if...else |
Quando existem duas alternativas. |
else if |
Quando há várias condições mutuamente exclusivas. |
switch |
Quando diversas alternativas dependem do valor de uma mesma variável. |
Boas práticas.
|
Estruturas de repetição
Diversos algoritmos precisam executar um mesmo conjunto de instruções várias vezes. Para isso utilizamos as estruturas de repetição, também chamadas de laços (loops).
A linguagem C oferece três estruturas principais de repetição:
| Estrutura | Quando utilizar |
|---|---|
while |
Quando não sabemos previamente quantas repetições serão necessárias. |
for |
Quando o número de repetições é conhecido ou pode ser determinado antes do início do laço. |
do...while |
Quando o bloco precisa ser executado pelo menos uma vez. |
Estrutura while
A estrutura while executa um bloco de código enquanto uma determinada condição permanecer verdadeira.
Ela é normalmente utilizada quando não sabemos previamente quantas vezes o laço será executado. Em outras palavras, o término da repetição depende de alguma condição que será verificada durante a execução do programa.
Considere o exemplo abaixo:
int numero = -1; while (numero != 0) { printf("Digite um número (0 para sair): "); scanf("%d", &numero); }
Nesse caso, não sabemos quantos números o usuário irá digitar. O laço continuará sendo executado até que o valor informado seja igual a zero.
Outro exemplo bastante comum consiste em percorrer uma estrutura de dados até encontrar um determinado elemento ou até que ela fique vazia. Situações como essas aparecem frequentemente nesta disciplina.
Estrutura for
A estrutura for é utilizada principalmente quando já sabemos quantas repetições deverão ser realizadas.
Ela reúne em uma única instrução três informações importantes:
- inicialização da variável de controle;
- condição de repetição;
- atualização da variável de controle.
Sua sintaxe geral é:
for (inicialização; condição; atualização) {
instruções;
}
Por exemplo:
for (int i = 0; i < 10; i++) { printf("%d\n", i); }
Nesse exemplo sabemos exatamente quantas vezes o laço será executado: dez vezes, uma para cada valor da variável i, de 0 até 9.
Em Estruturas de Dados, o comando for será utilizado principalmente para percorrer vetores e matrizes, onde normalmente conhecemos o número de elementos armazenados.
Estrutura do...while
A estrutura do...while é semelhante ao while, porém a condição é testada apenas após a execução do bloco de código.
Isso garante que o bloco será executado pelo menos uma vez.
int numero; do { printf("Digite um número: "); scanf("%d", &numero); } while (numero != 0);
Essa estrutura é bastante utilizada em menus e programas interativos, nos quais é necessário executar uma ação antes de decidir se o laço continuará.
Comparando as estruturas
| Estrutura | Quando utilizar | Exemplo típico |
|---|---|---|
while |
Não sabemos previamente quantas repetições serão necessárias. | Leitura de dados até o usuário informar um valor de parada. |
for |
Sabemos antecipadamente quantas repetições serão realizadas. | Percorrer um vetor de tamanho n. |
do...while |
O bloco deve ser executado pelo menos uma vez. | Menus e interfaces interativas. |
A principal diferença entre while e for não está na capacidade de resolver problemas — ambos podem implementar praticamente qualquer algoritmo iterativo —, mas na intenção do programador. Quando a repetição depende de um teste cujo número de execuções é desconhecido, normalmente utilizamos while. Quando a repetição é controlada por uma contagem conhecida, o for costuma produzir um código mais claro e organizado.
Boas práticas.
|
Vetores e Matrizes (Arrays)
Um vetor (ou array unidimensional) é uma estrutura de dados homogênea e estática que armazena uma sequência de elementos do mesmo tipo em posições consecutivas da memória.
Em C, a indexação de vetores começa sempre no índice 0 e termina no índice N-1, onde N é o tamanho total do vetor.
Declaração e Inicialização
Para declarar um vetor, especificamos o tipo de dados dos elementos, o nome do vetor e a quantidade de elementos entre colchetes:
int notas[5]; // Declara um vetor de inteiros com 5 posições
Também é possível inicializar os elementos do vetor no momento da declaração:
int primos[5] = {2, 3, 5, 7, 11};
Acesso e Modificação
O acesso e a modificação de elementos individuais são realizados através de seus índices correspondentes:
primos[0] = 2; // Acessa o primeiro elemento int x = primos[3]; // Lê o quarto elemento (valor 7)
Representação na Memória
Como os elementos de um vetor são armazenados de forma contígua, a distância física entre os elementos na memória é uniforme e depende diretamente do tamanho do tipo de dado do vetor.
Se um vetor de inteiros (onde cada int ocupa 4 bytes) começa no endereço E2000, seus elementos estarão localizados da seguinte forma na memória:
| Endereço | Tipo | Identificador | Valor |
|---|---|---|---|
E2000 |
int |
primos[0] |
2 |
E2004 |
int |
primos[1] |
3 |
E2008 |
int |
primos[2] |
5 |
E2012 |
int |
primos[3] |
7 |
E2016 |
int |
primos[4] |
11 |
Matrizes (Arrays Multidimensionais)
Uma matriz é um vetor de vetores, ideal para representar estruturas bidimensionais (como grades, tabelas ou imagens). A declaração de uma matriz em C requer a especificação do número de linhas e de colunas:
int matriz[3][4]; // Declara uma matriz de inteiros com 3 linhas e 4 colunas
Para percorrer ou manipular uma matriz, normalmente utilizamos laços de repetição aninhados (geralmente um laço for dentro de outro):
for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { matriz[i][j] = i + j; } }
Boas práticas e Cuidados importantes.
|
Strings
Uma string é um vetor do tipo char terminado pelo caractere nulo '\0' (null terminator). Esse caractere especial indica o fim da string, permitindo que as funções saibam onde o texto termina.
Declaração e Inicialização
Podemos declarar e inicializar strings de várias maneiras:
char nome1[10] = {'J', 'o', 'a', 'o', '\0'}; // Declaração explícita com terminador char nome2[10] = "Joao"; // O compilador adiciona o '\0' automaticamente char nome3[] = "Maria"; // Tamanho definido automaticamente (6 bytes)
Entrada e Saída de Strings
Para exibir uma string na tela, podemos utilizar a função printf() com o especificador de formato %s tranquilamente:
printf("Nome: %s\n", nome2);
No entanto, para a leitura de strings, a função scanf() tradicional apresenta limitações. Ao utilizar scanf("%s", nome), a função lê a entrada apenas até encontrar o primeiro caractere de espaço em branco (como espaço, tabulação ou quebra de linha). Se o usuário digitar "João Silva", apenas "João" será armazenado.
Para solucionar esse problema e ler strings contendo espaços, podemos utilizar as seguintes funções:
- gets(): Lê uma linha inteira da entrada padrão. Atenção: Esta função é considerada obsoleta e insegura (removida dos padrões modernos da linguagem C), pois não limita o número de caracteres lidos, o que pode causar estouro de buffer (buffer overflow).
- fgets(): A alternativa segura. Ela lê uma linha limitando a quantidade máxima de caracteres a serem lidos, evitando invasão de memória. Sua sintaxe é:
fgets(destino, tamanho_maximo, fluxo).
char nome[50]; printf("Digite seu nome completo: "); fgets(nome, 50, stdin); // Lê com segurança da entrada padrão (stdin)
A Biblioteca string.h
A biblioteca padrão <string.h> oferece um conjunto de funções prontas para a manipulação de strings. As principais funções são:
| Função | Descrição | Exemplo de Uso |
|---|---|---|
strlen() |
Retorna o comprimento da string (desconsiderando o terminador '\0'). |
int tam = strlen(nome); |
strcpy() |
Copia o conteúdo da string de origem para a string de destino. | strcpy(destino, origem); |
strcmp() |
Compara duas strings. Retorna 0 se forem iguais, um valor menor que 0 se a primeira for menor lexicograficamente, ou maior que 0 se for maior. | if (strcmp(senha, "123") == 0) |
strcat() |
Concatena (junta) a string de origem ao final da string de destino. | strcat(destino, origem); |
#include <stdio.h> #include <string.h> int main() { char str1[20] = "Ola "; char str2[10] = "Mundo"; // Concatenação strcat(str1, str2); printf("Concatenação: %s\n", str1); // Imprime "Ola Mundo" // Tamanho da string printf("Comprimento: %lu\n", strlen(str1)); // Comparação if (strcmp(str2, "Mundo") == 0) { printf("As strings são iguais!\n"); } return 0; }
Cuidados importantes.
|