Sintaxe da Linguagem C
Legal pessoal, conversamos sobre como é realizado o processo de compilação do nosso código e como um programa é convertido em um executável. Agora vamos dar uma olhada na sintaxe da linguagem C. Ela traz o conjunto de regras sobre como escrever código fonte em C. Vamos aproveitar e praticar com alguns exercícios sobre.
1. Sintaxe Básica do Ola Mundo
Section titled “1. Sintaxe Básica do Ola Mundo”Pessoal para iniciar nosso estudo de C, vamos escrever nosso primeiro programa e avaliar ele:
#include <stdio.h>#include <stdlib.h>
int main() { printf("Ola mundo!\n"); return 0;}Beleza, vamos entender o que cada elemento do nosso código faz:
#include <stdio.h>: Esta linha inclui a biblioteca padrão de entrada e saída do C, que nos permite usar funções comoprintfpara imprimir texto na tela.#include <stdlib.h>: Esta linha inclui a biblioteca padrão de utilidades do C, que nos permite usar funções comoexitemalloc. Nenhum deles foi utilizado neste exemplo, mas já vamos ver como utilizá-los.int main(): Esta linha define a função principal do programa, que é o ponto de entrada. Todo programa C deve ter uma funçãomain. A sua execução começa por aqui.{ ... }: As chaves delimitam o início e o fim do corpo da funçãomain. A linguagem C utiliza chaves para realizar agrupamentos de código, como funções, estruturas e blocos.printf("Ola mundo!\n");: Esta linha chama a funçãoprintfpara imprimir o texto “Ola mundo!” na tela. O\né um caractere de nova linha, que move o cursor para a linha seguinte. Vamos avaliar esse comando mais a fundo.return 0;: Esta linha indica que o programa terminou com sucesso. O valor0é retornado para o sistema operacional, indicando que não houve erros durante a execução.// ola.c: Este é o comentário de uma linha, que é ignorado pelo compilador. Comentários são usados para documentar o código e torná-lo mais legível. No C, comentários de uma linha começam com//, enquanto comentários de várias linhas são delimitados por/*e*/.
Legal, agora que avaliamos cada um dos elementos do nosso código, vamos compilar e executar ele. Para isso, no terminal, vamos utilizar o comando:
# Dentro do diretório onde está o arquivo ola.cgcc ola.c -o olaVamos compreender um pouco mais sobre este processo. Primeiro é preciso compreender o que está acontecendo aqui. Não temos a execução do nosso programa, mas sim a compilação dele. O comando gcc é o compilador de C, que converte o código fonte em um executável. O parâmetro -o ola especifica o nome do arquivo executável que será gerado. Durante o processo de compilação, o compilador verifica a sintaxe do código, realiza a análise semântica e gera o código objeto. Este código objeto é vinculado com as bibliotecas necessárias para criar o executável final. Este processo é chamado de “linkagem”. Após a sua finalização, o executável ola é criado no mesmo diretório.
Beleza, agora para executar o programa, basta rodar o comando:
./olaSe verificarmos no terminal, o arquivo ola é um arquivo executável. Podemos verificar isso com o comando file:
file olaLegal, agora vamos avançar um pouco com a sintaxe do C.
2. Sintaxe Básica do C
Section titled “2. Sintaxe Básica do C”Já vimos alguns pontos importantes da sintaxe do C:
- Todo programa em C deve possuir uma função
main, que é o ponto de entrada do programa; - A linguagem C é compilada, ou seja, o código fonte é convertido em um executável antes de ser executado;
- O C utiliza chaves
{}para delimitar blocos de código, como funções e estruturas.
Agora vamos para mais alguns detalhes importantes, vamos analisar o nosso segundo programa de exemplo:
#include <stdio.h>
int main(){ int valor_a; int valor_b, valorC = 10; printf("O valor das variaveis: %i, %d, %i\n", valor_a, valor_b, valorC); valor_a = 10; valor_b = valor_a*2 + valorC; printf("O valor das variaveis: %i, %d, %i\n", valor_a, valor_b, valorC); return 0;}Vamos compilar e executar este programa. O que mais interessa neste momento é a sintaxe do C e alguns conceitos que vamos analisar aqui.
gcc 01_programa_base.c -o 01_programa_base./01_programa_baseA saída do programa deverá ser algo parecido com:
O valor das variaveis: -47394720, 21870, 10O valor das variaveis: 10, 30, 10“Opa opa Murilão! A segunda linha está igual, mas a primeira está com valores diferentes, o que está acontecendo?”
Boa gente, vamos avaliar isso. C é uma linguagem estática, ou seja, as variáveis devem ser declaradas antes de serem utilizadas. No nosso exemplo, as variáveis valor_a e valor_b são declaradas, mas não inicializadas antes de serem utilizadas na primeira chamada do printf. Isso significa que elas contêm valores aleatórios da memória, o que resulta em uma saída inesperada.
Outro ponto importante de se observar, que as variáveis são posições de memória que nós utilizamos para armazenar valores. No C, as variáveis devem ser declaradas com um tipo específico, como int, float, char, etc. No nosso exemplo, valor_a e valor_b são declaradas como inteiros (int), enquanto valorC é inicializada com o valor 10. Os tipos que chamamos de primitivos são os tipos básicos da linguagem, como int, float, char, etc. Eles são usados para armazenar valores simples, como números inteiros, números de ponto flutuante e caracteres.
| Tipo | Descrição | Exemplo de declaração | Tamanho (bytes) |
|---|---|---|---|
_Bool | Valor booleano (0 ou 1), veio no C99. Incluir com <stdbool.h> | _Bool flag = 1; | 1 |
char | Caractere (geralmente ASCII) | char c = 'A'; | 1 |
signed char | Caractere com sinal, alcance típico −128 a 127 | signed char sc = -10; | 1 |
unsigned char | Caractere sem sinal, alcance 0 a 255 | unsigned char uc = 200; | 1 |
short | Inteiro curto com sinal | short s = 1000; | 2 |
unsigned short | Inteiro curto sem sinal | unsigned short us = 60000; | 2 |
int | Inteiro padrão com sinal | int i = 12345; | 4 |
unsigned int | Inteiro padrão sem sinal | unsigned int ui = 3000000000U; | 4 |
long | Inteiro longo com sinal | long l = 1234567890L; | 8 |
unsigned long | Inteiro longo sem sinal | unsigned long ul = 4000000000UL; | 8 |
long long | Inteiro muito longo com sinal | long long ll = 1234567890123LL; | 8 |
unsigned long long | Inteiro muito longo sem sinal | unsigned long long ull = 1000000000000000ULL; | 8 |
float | Ponto flutuante simples precision | float f = 3.14f; | 4 |
double | Ponto flutuante dupla precision | double d = 3.1415926535; | 8 |
long double | Ponto flutuante estendido | long double ld = 3.141592653589793L; | 16 |
Legal, agora temos os tipos das nossas variáveis vamos ver um pouco mais sobre o nosso programa. Vamos aproveitar para falar dos nomes das variáveis. No C, os nomes das variáveis devem seguir algumas regras:
- Devem começar com uma letra (a-z, A-Z) ou um sublinhado (_);
- Podem conter letras, números (0-9) e sublinhados;
- Não podem conter espaços ou caracteres especiais (como @, #, $, etc.);
- Não podem ser palavras reservadas da linguagem (como
int,return,if, etc.); - São sensíveis a maiúsculas e minúsculas (por exemplo,
valor_aeValor_Asão considerados diferentes).
Dos tópicos anteriores, acho que a mais interessante para analisar é a questão de palavras reservadas. Elas são reservadas pois são utilizadas pela linguagem para definir estruturas e comandos. Por exemplo, int é uma palavra reservada que define um tipo de dado inteiro, return é usada para retornar um valor de uma função, e if é usada para criar uma estrutura condicional. Não podemos utilizar essas palavras como nomes de variáveis, pois isso causaria conflitos e erros de compilação.
| Palavra-reservada | Categoria |
|---|---|
| auto | Storage-class |
| register | Storage-class |
| static | Storage-class |
| extern | Storage-class |
| typedef | Storage-class |
| _Thread_local | Storage-class (C11) |
| _Atomic | Qualificador/Atomicidade (C11) |
| const | Qualificador |
| volatile | Qualificador |
| restrict | Qualificador (C99) |
| void | Tipo |
| char | Tipo |
| short | Tipo |
| int | Tipo |
| long | Tipo |
| float | Tipo |
| double | Tipo |
| signed | Tipo (qualificador) |
| unsigned | Tipo (qualificador) |
| _Bool | Tipo (C99) |
| _Complex | Tipo (C99) |
| _Imaginary | Tipo (C99) |
| _Alignas | Align-spec (C11) |
| _Alignof | Align-spec (C11) |
| _Noreturn | Função (C11) |
| _Static_assert | Asserção (C11) |
| if | Controle condicional |
| else | Controle condicional |
| switch | Controle condicional |
| case | Controle condicional |
| default | Controle condicional |
| for | Laço |
| while | Laço |
| do | Laço |
| break | Desvio |
| continue | Desvio |
| goto | Desvio |
| return | Desvio |
| sizeof | Operador |
| inline | Especificador de função (C99) |
| enum | Construção de tipo |
| struct | Construção de tipo |
| union | Construção de tipo |
Legal agora também temos nossas palavras reservadas! Vamos ver um pouco mais ainda sobre este programa. As instruções em C são terminadas por ponto e vírgula (;). Isso indica o final de uma instrução. No nosso exemplo, cada linha dentro da função main termina com um ponto e vírgula, exceto as linhas que contêm chaves {} que delimitam blocos de código. Outro ponto interessante para observar é o que significa C ser uma linguagem de tipagem estática. Isso significa que os tipos das variáveis são verificados em tempo de compilação, e não em tempo de execução. Ou seja, o compilador verifica se as variáveis estão sendo usadas corretamente de acordo com seus tipos antes de gerar o executável. Isso ajuda a evitar erros comuns, como tentar realizar operações inválidas entre tipos diferentes.
Outro ponto interessante para observar aqui, o operador de atribuição = é utilizado para atribuir valores às variáveis. No nosso exemplo, valor_a é atribuído o valor 10, e valor_b é atribuído o resultado da expressão valor_a * 2 + valorC. O C também possui operadores aritméticos, como +, -, *, / e %, que são usados para realizar operações matemáticas. Mais sobre isso logo menos.
Agora vamos para nossa função de exibição, o printf(). Ela é uma função da biblioteca padrão do C que permite imprimir texto formatado na saída padrão (em geral, o console). Ela recebe como primeiro argumento uma string de formato, que diz como o texto deve ser exibido, e os argumentos seguintes são os valores a serem exibidos. No nosso exemplo, a string de formato é "O valor das variaveis: %i, %d, %i\n", onde %i e %d são especificadores de formato que indicam que os valores correspondentes devem ser exibidos como inteiros. O \n é um caractere de nova linha, que move o cursor para a linha seguinte.
| Especificador | Significado | Exemplo de uso |
|---|---|---|
%d / %i | inteiro decimal com sinal | int i = -42; printf("%i\n", i); |
%u | inteiro decimal sem sinal | unsigned u = 42U; printf("%u\n", u); |
%o | inteiro em octal sem sinal | unsigned u = 10; printf("%o\n", u); |
%x / %X | inteiro em hexadecimal (minúsculo/maiúsculo) | unsigned u = 255; printf("%X\n", u); |
%f / %F | ponto flutuante (notação decimal) | double d = 3.14; printf("%F\n", d); |
%e / %E | ponto flutuante em notação científica | double d = 0.00123; printf("%e\n", d); |
%g / %G | formato automático (%f ou %e, o que for menor) | double d = 123456.0; printf("%g\n", d); |
%a / %A | ponto flutuante em notação hexadecimal | double d = 10.5; printf("%a\n", d); |
%c | caractere | char c = 'Z'; printf("%c\n", c); |
%s | string (cadeia de char) | char *s = "Olá"; printf("%s\n", s); |
%p | endereço de ponteiro | int x; printf("%p\n", (void*)&x); |
%n | escreve em um inteiro o número de caracteres já impressos até aqui | int count; printf("abc%n\n", &count); /* count == 3 */ |
%% | imprime o literal % | printf("100%% concluído\n"); |
Legal, temos só mais um detalhes que são os caracteres de escape. Eles são utilizados para representar caracteres especiais dentro de strings. No nosso exemplo, usamos \n para representar uma nova linha. Outros caracteres de escape comuns incluem:
| Sequência | Descrição | Exemplo de uso |
|---|---|---|
\n | Nova linha (LF) | printf("Linha1\nLinha2\n"); |
\t | Tabulação horizontal | printf("Col1\tCol2\n"); |
\r | Retorno de carro (CR) | printf("12345\rAB\n"); /* imprime AB345 */ |
\a | Alerta (beep) | printf("Atenção!\a\n"); |
\b | Backspace | printf("ABC\bD\n"); /* imprime ABD */ |
\f | Form feed (avança para nova “página”) | printf("Página1\fPágina2\n"); |
\v | Tabulação vertical | printf("Linha1\vLinha2\n"); |
\\ | Barra invertida (\) | printf("C:\\Windows\\System32\n"); |
\' | Aspas simples (') | printf("It\'s OK\n"); |
\" | Aspas duplas (") | printf("Ela disse: \"%s\"\n", "Olá"); |
\? | Ponto de interrogação | printf("E se eu disser\?\n"); |
\nnn | Caractere pelo código octal (até 3 dígitos) | printf("\101\n"); /* ’A’ */ |
\xhh | Caractere pelo código hexadecimal (até 2 dígitos) | printf("\x41\n"); /* ’A’ */ |
Beleza! Agora vamos avançar um pouco mais.
3. Trabalhando com Variáveis
Section titled “3. Trabalhando com Variáveis”Aqui vamos escrever um programa que permite o usuário inserir dois números inteiros e vamos exibir a soma deles.
#include <stdio.h> int main(){ int val1, val2, res; printf("Informe dois valores:\n"); scanf("%d", &val1); scanf("%d", &val2); res = val1 + val2; printf("Resultado da soma: %d\n", res); return 0;}Aqui temos algumas novidades a tratar! Primeiro vamos da mais simples, a soma dos valores. O operador + faz parte dos operadores aritméticos do C, que são usados para realizar operações matemáticas. Outros operadores aritméticos incluem - (subtração), * (multiplicação), / (divisão) e % (módulo). No nosso exemplo, usamos o operador + para somar os valores de val1 e val2, e armazenamos o resultado na variável res.
| Operador | Descrição | Exemplo |
|---|---|---|
+ | Adição | c = a + b; |
- | Subtração | c = a - b; |
* | Multiplicação | c = a * b; |
/ | Divisão inteira (ou em ponto-flutuante) | c = a / b; |
% | Módulo (resto da divisão inteira) | c = a % b; |
++ | Incremento (pré ou pós) | ++a; ou a++; |
-- | Decremento (pré ou pós) | --a; ou a--; |
+= | Adição e atribuição (a = a + valor) | a += 5; |
-= | Subtração e atribuição (a = a - valor) | a -= 2; |
*= | Multiplicação e atribuição (a = a * valor) | a *= 3; |
/= | Divisão e atribuição (a = a / valor) | a /= 4; |
%= | Módulo e atribuição (a = a % valor) | a %= 6; |
Legal, agora vamos falar sobre a nossa entrada de dados do usuário, a função scanf(). Ela é usada para ler dados da entrada padrão (no nosso caso o teclado) e armazenar o resultado lido em um endereço de memória.
“Murilão, calma lá! Você falou errado! Era para dizer que ela lê dados de entrada e guarda o resultado em uma variável.”
Então, sim isso está correto. Mas tem um detalhe importante, nós precisamos que a função scanf() consiga modificar um dos parâmetros que passamos para ela. Isso traz um comportamento bastante interessante do C, a passagem por valor e por referência. Os parâmetros são passados para as funções por valor, ou seja, uma cópia do valor é passada para a função. No entanto, quando passamos o endereço de memória de uma variável (usando o operador &), estamos passando uma referência para a variável original. Isso permite que a função modifique o valor da variável original.
Os tipos de dados que utilizamos como texto formatador no scanf() são:
| Forma de leitura | Tipo de dado lido | Exemplo | Descrição |
|---|---|---|---|
%d | int | scanf("%d", &i); | Lê inteiro decimal com sinal. |
%i | int | scanf("%i", &i); | Lê inteiro em base automática (0x → hex, 0 → octal, senão decimal). |
%u | unsigned int | scanf("%u", &u); | Lê inteiro decimal sem sinal. |
%o | unsigned int | scanf("%o", &u); | Lê inteiro em base octal. |
%x / %X | unsigned int | scanf("%x", &u); | Lê inteiro em base hexadecimal. |
%f | float | scanf("%f", &f); | Lê ponto flutuante (simples precisão). |
%lf | double | scanf("%lf", &d); | Lê ponto flutuante dupla precisão. |
%e / %E | float / double | scanf("%e", &f); | Lê em notação científica (1.23e+02). |
%c | char | scanf(" %c", &c); | Lê um caractere; o espaço antes do %c faz scanf pular whitespace. |
%s | char[] (string) | scanf("%s", str); | Lê até o primeiro whitespace (não protege contra overflow). |
%[^\n] | char[] (scanset) | scanf("%[^\n]", line); | Lê tudo até encontrar \n (newline). |
%p | void* | scanf("%p", &ptr); | Lê um valor de ponteiro (endereço). |
%n | int* | scanf("%d%n", &i, &count); | Armazena em count o nº de caracteres consumidos até aqui. |
%*d | descarta (int) | scanf("%*d %d", &i); | O * suprime a atribuição: o primeiro inteiro é lido, mas não armazenado. |
%5d, %10s etc. | diversos | scanf("%5s", str); | O número entre % e o especificador limita o nº máximo de caracteres lidos. |
Legal, agora vamos falar sobre o operador de endereço &. Ele é usado para obter o endereço de memória de uma variável. No nosso exemplo, usamos &val1 e &val2 para passar o endereço de memória das variáveis val1 e val2 para a função scanf(). Isso permite que a função modifique o valor dessas variáveis diretamente na memória. Vamos trabalhar mais com este operador logo menos.
4. Estruturas de Decisão
Section titled “4. Estruturas de Decisão”As estruturas de controle são utilizadas para controlar o fluxo da execução de um programa. Elas permitem que o programa tome decisões para seguir diferentes fluxos de execução dado o estado atual do programa. A linguagem C possui algumas estruturas de decisão, vamos avaliar elas aqui.
#include <stdio.h>int main(){ int valor; printf("Informe um valor: "); scanf("%d", &valor);
if (valor > 0) { printf("O valor é positivo.\n"); } else if (valor < 0) { printf("O valor é negativo.\n"); } else { printf("O valor é zero.\n"); }
return 0;}Esse programa é bastante simples e bastante direto para sua utilização, depois de compilar e executar o programa, ele vai solicitar para o usuário informar um valor. Esse valor é armazenado na variável valor, que é um inteiro. Aqui vem a utilização das estruturas de decisão. A estrutura if verifica primeiro se o valor é maior que zero. Em caso afirmativo, o programa imprime “O valor é positivo.” na tela. Se não for, ele verifica se o valor é menor que zero. Se for, o programa imprime “O valor é negativo.”. Se nenhuma das condições anteriores for verdadeira, significa que o valor é zero, e o programa imprime “O valor é zero.”.
Aqui cabe destacar o seguinte comportamento, apenas uma das condições será executada. Isso ocorre porque as estruturas de decisão são avaliadas sequencialmente. Assim que uma condição é verdadeira, o bloco de código correspondente é executado e o restante das condições é ignorado. É importante conhecer a forma como as estruturas de decisão são avaliadas para conseguir utilizá-las da melhor forma possível.
Outro detalhe bastante importante: em C, não existe o conceito de “verdadeiro” e “falso” como em outras linguagens. Em vez disso, qualquer valor diferente de zero é considerado verdadeiro, enquanto zero é considerado falso. Isso significa que podemos usar expressões aritméticas diretamente nas condições das estruturas de decisão. Mas atenção, o ponto que é verdade é que o valor zero é falso, todo o restante é considerado verdadeiro.
Os operadores lógicos também são utilizados para combinar condições. Os operadores lógicos mais comuns são:
| Operador | Descrição | Exemplo |
|---|---|---|
< | Menor que | if (a < b) printf("sim"); |
> | Maior que | if (a > b) printf("sim"); |
<= | Menor ou igual a | if (a <= b) printf("sim"); |
>= | Maior ou igual a | if (a >= b) printf("sim"); |
== | Igual a | if (a == b) printf("sim"); |
!= | Diferente de | if (a != b) printf("sim"); |
Vamos ver um exemplo agora com mais uma estrutura de decisão, a switch. Ela é utilizada para avaliar uma expressão e executar um bloco de código correspondente ao valor da expressão. Vamos ver um exemplo:
#include <stdio.h>
int main() { int dia; printf("Informe um número de 1 a 7 para o dia da semana: "); scanf("%d", &dia);
switch (dia) { case 1: printf("Domingo\n"); break; case 2: printf("Segunda-feira\n"); break; case 3: printf("Terça-feira\n"); break; case 4: printf("Quarta-feira\n"); break; case 5: printf("Quinta-feira\n"); break; case 6: printf("Sexta-feira\n"); break; case 7: case 0: // Adicionando o caso 0 para o sábado também printf("Sábado\n"); break; default: printf("Número inválido! Informe um número de 1 a 7.\n"); }
return 0;}Pessoal aqui o ponto principal para se observar é a estrutura switch. Ela avalia a variável dia e executa o bloco de código correspondente ao valor dessa variável. Cada case representa um valor possível para a variável, e o bloco de código correspondente é executado se a variável tiver aquele valor. O break é usado para sair do switch após a execução do bloco de código correspondente. Se não houver um break, o programa continuará executando os blocos de código dos próximos case, até que ele encontre um break ou o final do switch, como é o caso do valor 7 e do valor 0 no nosso exemplo. O default é usado para tratar casos em que a variável não corresponde a nenhum dos valores especificados nos case. Ele é opcional, mas é uma boa prática incluí-lo para lidar com entradas inválidas.
5. Estruturas de Repetição
Section titled “5. Estruturas de Repetição”As estruturas de repetição são utilizadas para executar um bloco de código várias vezes, enquanto uma condição for verdadeira. Elas são úteis quando precisamos repetir uma ação várias vezes, como percorrer elementos de um array ou realizar cálculos iterativos. A linguagem C possui três estruturas de repetição principais: for, while e do-while. Vamos ver cada uma delas.
#include <stdio.h>int main() { int i; printf("Contagem de 1 a 10:\n"); for (i = 1; i <= 10; i++) { printf("%d ", i); } printf("\n"); return 0;}Legal, aqui temos um exemplo de utilização da estrutura for. Ela composta por 3 partes principais:
- Inicialização:
i = 1é executada uma vez antes do início do loop. É onde definimos a variável de controle do loop. - Condição:
i <= 10é verificada antes de cada iteração do loop. Se for verdadeira, o bloco de código dentro do loop é executado. Se for falsa, o loop termina. - Incremento:
i++é executado após cada iteração do loop. É onde atualizamos a variável de controle do loop.
Em geral, essa estrutura é utilizada quando sabemos o número de iterações que queremos realizar. No nosso exemplo, estamos contando de 1 a 10, então sabemos que o loop deve ser executado 10 vezes.
Agora vamos ver um exemplo com a estrutura while.
#include <stdio.h>
int main() { int i = 1; printf("Contagem de 1 a 10:\n"); while (i <= 10) { printf("%d ", i); i++; } printf("\n"); return 0;}Aqui pessoal, o nosso exemplo é semelhante ao anterior, mas utilizando a estrutura while. Aqui a diferença é que a estrutura tem o funcionamento de repetir sua execução, enquanto a condição de teste for verdadeira. No nosso exemplo, a variável i é inicializada com o valor 1, e o loop continua enquanto i for menor ou igual a 10. Dentro do loop, imprimimos o valor de i e incrementamos i em 1. Assim como no exemplo anterior, o loop será executado 10 vezes. Aqui uma observação importante, a variável de controle do loop pode ser alterada durante a sua execução, ele apenas será avaliada novamente quando o loop for executado novamente. Isso significa que se a condição de teste não for alterada, o loop pode se tornar infinito, ou seja, ele nunca vai terminar. Por isso é importante garantir que a condição de teste seja alterada dentro do loop, para evitar loops infinitos.
A estrutura while tem uma característica interessante, ela pode ter seu bloco de código nunca executado. Isso acontece quando a condição de teste já é falsa antes do loop ser executado. Vamos ver um exemplo disso:
#include <stdio.h>
int main() { int i = 11; // Inicializando i com um valor maior que 10 printf("Contagem de 1 a 10:\n"); while (i <= 10) { printf("%d ", i); // Este bloco nunca será executado i++; } printf("\n"); return 0;}Aqui no momento que a condição de teste é avaliada, ela já é falsa, então o bloco de código dentro do loop nunca será executado. Isso é importante para se ter em mente, pois pode levar a comportamentos inesperados se não for tratado corretamente. Agora, vamos alterar esse programa para já verificarmos o funcionamento da estrutura do-while.
#include <stdio.h>
int main() { int i = 11; // Inicializando i com um valor maior que 10 printf("Contagem de 1 a 10:\n"); do { printf("%d ", i); // Este bloco será executado pelo menos uma vez i++; } while (i <= 10); printf("\n"); return 0;}Repare que aqui temos quase o mesmo código do exemplo anterior, mas agora estamos utilizando a estrutura do-while. A diferença é que o bloco de código dentro do do será executado pelo menos uma vez, mesmo que a condição de teste seja falsa. Isso significa que, mesmo que i seja inicializado com um valor maior que 10, o programa ainda imprimirá o valor de i uma vez antes de verificar a condição de teste.
6. Funções
Section titled “6. Funções”Legal, até aqui falamos de programas bastante pequenos. Eles tem por objetivo nos auxiliar a compreender como a linguagem C funciona. Por que compreender isso é importante? Porque agora vamos ver um conceito que auxilia demais durante o desenvolvimento de programas mais complexos, ou ainda quando desejamos deixar nosso código mais modular e simples de se utilizar, as funções.
De novo, ainda vamos continuar vendo programas pequenos, mas meu objetivo ao apresentar o conceito das funções é deixar claro para vocês que elas nos trazem bastante flexibilidade e organização no código. Vamos ver o exemplo de um programa que realiza as quatro operações matemáticas básicas (adição, subtração, multiplicação e divisão) utilizando funções.
#include <stdio.h>// Função para somar dois númerosint somar(int a, int b) { return a + b;}// Função para subtrair dois númerosint subtrair(int a, int b) { return a - b;}// Função para multiplicar dois númerosint multiplicar(int a, int b) { return a * b;}// Função para dividir dois númerosdouble dividir(int a, int b) { if (b == 0) { printf("Erro: Divisão por zero!\n"); return 0; // Retorna 0 em caso de divisão por zero } return (double)a / b;}
int main() { int num1, num2; printf("Informe dois números inteiros:\n"); scanf("%d %d", &num1, &num2);
printf("Soma: %d\n", somar(num1, num2)); printf("Subtração: %d\n", subtrair(num1, num2)); printf("Multiplicação: %d\n", multiplicar(num1, num2)); printf("Divisão: %.2f\n", dividir(num1, num2));
return 0;}Aqui tem bastante coisa acontecendo! Fiquem comigo para que possamos estudar essa sintaxe em conjunto! Vamos lá!
Primeiro vamos compreender como é a estrutura de uma função em C. Ela possui um anatomia própria, que é composta por:
- Tipo de retorno: o tipo de dado que a função irá retornar. No nosso exemplo, as funções
somar,subtrairemultiplicarretornam umint, enquanto a funçãodividirretorna umdouble. - Nome da função: o nome que será utilizado para chamar a função. No nosso exemplo, as funções são chamadas
somar,subtrair,multiplicaredividir. - Parâmetros: os valores que serão passados para a função. No nosso exemplo, as funções recebem dois parâmetros do tipo
int, exceto a funçãodividir, que recebe dois parâmetros do tipointe retorna umdouble. - Corpo da função: o bloco de código que será executado quando a função for chamada. No nosso exemplo, cada função possui um corpo que realiza a operação matemática correspondente e retorna o resultado.
Aqui tem um ponto importante para se observar, as funções possuem uma forma específica de serem caracterizadas. Essa forma é chamada de assinatura da função. Analisando as assinaturas das funções que o compilador sabe que elas são diferentes, mesmo que os nomes sejam iguais. A assinatura de uma função é composta pelo nome da função, seu tipo de retorno e pelos tipos dos parâmetros.
As funções em C precisão ser declaradas antes de serem utilizadas. Isso significa que o compilador precisa conhecer a assinatura da função antes de encontrá-la no código. No nosso exemplo, as funções são declaradas antes da função main, o que permite que elas sejam chamadas dentro da função main. Também é possível declarar as funções após a função main, mas nesse caso precisamos utilizar uma declaração prévia da função, que é uma forma de informar ao compilador sobre a assinatura da função antes de sua definição. A declaração prévia é feita da seguinte forma:
// Declaração prévia da funçãoint somar(int a, int b);“Mas Murilão não é só declarar a função antes da
mainque já está tudo certo?”
Quando estamos trabalhando com apenas um arquivo fonte, é possível fazer isso. Mas quando trabalhamos com múltiplos arquivos fonte ou ainda quando estamos trabalhando com bibliotecas, é necessário utilizar a declaração prévia para informar ao compilador sobre a assinatura da função antes de sua definição. Isso é especialmente importante quando estamos trabalhando com cabeçalhos (.h) e arquivos de implementação (.c).
Outro ponto importante é que as funções podem ser chamadas dentro de outras funções. No nosso exemplo, a função main chama as funções somar, subtrair, multiplicar e dividir para realizar as operações matemáticas. Isso permite que o código seja modularizado e organizado, facilitando a manutenção e a leitura do código. Quando vamos utilizar uma função, é importante passar os parâmetros corretos para ela. No nosso exemplo, as funções recebem dois parâmetros do tipo int, que são os números que serão utilizados nas operações matemáticas. Quando chamamos a função, passamos os valores das variáveis num1 e num2 como argumentos.
Podemos sobrecarregar funções em C, ou seja, podemos ter várias funções com o mesmo nome, mas com assinaturas diferentes. Vamos ver um exemplo disso:
#include <stdio.h>// Função para somar dois inteirosint somar(int a, int b) { return a + b;}// Função para somar dois números de ponto flutuantedouble somar(double a, double b) { return a + b;}// Função para somar três inteirosint somar(int a, int b, int c) { return a + b + c;}int main() { int num1 = 5, num2 = 10, num3 = 15; double num4 = 2.5, num5 = 3.5;
printf("Soma de dois inteiros: %d\n", somar(num1, num2)); printf("Soma de dois números de ponto flutuante: %.2f\n", somar(num4, num5)); printf("Soma de três inteiros: %d\n", somar(num1, num2, num3));
return 0;}Aqui temos um exemplo de sobrecarga de funções. Temos três funções chamadas somar, mas cada uma tem uma assinatura diferente. A primeira função recebe dois parâmetros do tipo int, a segunda recebe dois parâmetros do tipo double e a terceira recebe três parâmetros do tipo int. Quando chamamos a função somar, o compilador escolhe a função correta com base nos tipos dos argumentos passados. Isso permite que possamos utilizar o mesmo nome de função para realizar operações diferentes, dependendo dos tipos dos parâmetros.
Vamos avançar mais com o uso de funções assim que avançarmos um pouco mais com a sintaxe do C.
7. Arrays
Section titled “7. Arrays”Legal, agora temos vários elementos base para construir nossos programas. Vamos analisar agora alguns outros elementos que vão permitir que nossos programas implementem alguns outros comportamentos mais complexos. Primeiro vamos analisar os arrays. Eles são utilizados como uma forma de armazenar diferentes valores do mesmo tipo na memória de forma contígua. Isso significa que os valores são armazenados em posições de memória adjacentes, o que permite acessar os valores de forma rápida e eficiente. Vamos ver um exemplo de como declarar e utilizar um array em C.
#include <stdio.h>int main() { int numeros[5]; // Declara um array de inteiros com 5 elementos printf("Informe 5 números inteiros:\n");
// Lê os valores do usuário e armazena no array for (int i = 0; i < 5; i++) { scanf("%d", &numeros[i]); }
// Exibe os valores armazenados no array printf("Os números informados foram:\n"); for (int i = 0; i < 5; i++) { printf("%d ", numeros[i]); } printf("\n");
return 0;}Aqui temos bastante a observar! Primeiro, a declaração do array numeros[5] cria um array de inteiros com 5 elementos. Os arrays em C são indexados a partir de zero, ou seja, o primeiro elemento do array está na posição 0, o segundo elemento está na posição 1, e assim por diante. Isso significa que o último elemento do array numeros está na posição 4, podemos dizer que a última posição de um array de N posições será N-1.
Agora vamos pensar sobre como acessar os elementos do array. No nosso exemplo, utilizamos um loop for para ler os valores do usuário e armazená-los no array. A variável i é usada como índice para acessar cada elemento do array. Quando chamamos numeros[i], estamos acessando o elemento na posição i do array numeros.
Os arrays podem ser multidimensionais, ou seja, podemos ter arrays com mais de uma dimensão. Isso é útil para representar matrizes ou tabelas de dados. Vamos ver um exemplo de como declarar e utilizar um array bidimensional em C.
#include <stdio.h>int main() { int matriz[3][3]; // Declara um array bidimensional de inteiros 3x3 printf("Informe os valores para a matriz 3x3:\n");
// Lê os valores do usuário e armazena na matriz for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("Elemento [%d][%d]: ", i, j); scanf("%d", &matriz[i][j]); } }
// Exibe os valores armazenados na matriz printf("A matriz informada é:\n"); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", matriz[i][j]); } printf("\n"); }
return 0;}Um prática bastante comum quando trabalhamos com arrays multidimensionais é utilizar loops aninhados para percorrer os elementos do array. No nosso exemplo, utilizamos dois loops for para ler os valores do usuário e armazená-los na matriz. O primeiro loop percorre as linhas da matriz, enquanto o segundo loop percorre as colunas. Quando acessamos um elemento da matriz, usamos a sintaxe matriz[i][j], onde i é o índice da linha e j é o índice da coluna.
Os arrays podem ser utilizados como um tipo de dados enviado para funções. Isso permite que possamos passar um array como argumento para uma função e manipular seus valores dentro da função. Vamos ver um exemplo de como fazer isso:
#include <stdio.h>// Função para calcular a média de um array de inteirosdouble calcular_media(int numeros[], int tamanho) { int soma = 0; for (int i = 0; i < tamanho; i++) { soma += numeros[i]; } return (double)soma / tamanho;}
int main() { int numeros[5]; printf("Informe 5 números inteiros:\n");
// Lê os valores do usuário e armazena no array for (int i = 0; i < 5; i++) { scanf("%d", &numeros[i]); }
// Calcula a média dos números informados double media = calcular_media(numeros, 5); printf("A média dos números informados é: %.2f\n", media);
return 0;}Aqui temos um exemplo de como passar um array como argumento para uma função. A função calcular_media recebe um array de inteiros e o tamanho do array como parâmetros. Dentro da função, utilizamos um loop for para calcular a soma dos elementos do array e, em seguida, retornamos a média dos valores. No main, lemos os valores do usuário e chamamos a função calcular_media passando o array numeros e o tamanho do array como argumentos.
Agora vamos ver um tipo de dado interessante para trabalhar com arrays, as strings.
8. Strings
Section titled “8. Strings”Em C, não existe um tipo de dado específico para strings como em outras linguagens. Em vez disso, as strings são representadas como arrays de caracteres (char). Uma string é um array de caracteres que termina com o caractere nulo ('\0'), que indica o final da string. Vamos ver um exemplo de como declarar e utilizar uma string em C.
#include <stdio.h>int main() { char nome[50]; // Declara uma string com tamanho máximo de 49 caracteres + 1 para o caractere nulo printf("Informe seu nome: "); scanf("%49s", nome); // Lê uma string do usuário, limitando a 49 caracteres
printf("Olá, %s!\n", nome); // Exibe a string informada pelo usuário
return 0;}Aqui temos um exemplo de como declarar e utilizar uma string em C. A string nome é declarada como um array de caracteres com tamanho máximo de 50, o que permite armazenar até 49 caracteres mais o caractere nulo ('\0'). Utilizamos a função scanf para ler a string do usuário, limitando a entrada a 49 caracteres para evitar overflow. Em seguida, exibimos a string informada pelo usuário utilizando o especificador %s. Outra forma de fazer essa leitura é utilizando a função fgets, que permite ler uma linha inteira, incluindo espaços em branco, até encontrar um caractere de nova linha ('\n') ou atingir o tamanho máximo especificado.
#include <stdio.h>int main() { char nome[50]; // Declara uma string com tamanho máximo de 49 caracteres + 1 para o caractere nulo printf("Informe seu nome: "); fgets(nome, sizeof(nome), stdin); // Lê uma linha inteira do usuário
printf("Olá, %s", nome); // Exibe a string informada pelo usuário
return 0;}Aqui utilizamos a função fgets para ler uma linha inteira do usuário, incluindo espaços em branco. A função fgets lê até o tamanho máximo especificado (neste caso, 50 caracteres) ou até encontrar um caractere de nova linha ('\n'). É importante lembrar que a função fgets inclui o caractere de nova linha na string lida, então, se necessário, podemos removê-lo manualmente.
#include <stdio.h>#include <string.h>int main() { char nome[50]; // Declara uma string com tamanho máximo de 49 caracteres + 1 para o caractere nulo printf("Informe seu nome: "); fgets(nome, sizeof(nome), stdin); // Lê uma linha inteira do usuário
// Remove o caractere de nova linha, se presente nome[strcspn(nome, "\n")] = '\0';
printf("Olá, %s!\n", nome); // Exibe a string informada pelo usuário
return 0;}Aqui utilizamos a função strcspn para encontrar a posição do caractere de nova linha na string e substituí-lo por um caractere nulo ('\0'), efetivamente removendo o caractere de nova linha da string. Outro ponto é a utiliza”cão da biblioteca <string.h>, que contém funções úteis para manipulação de strings, como strcpy, strlen, strcmp, entre outras.
| Função | Descrição | Exemplo de uso |
|---|---|---|
size_t strlen(const char *s) | Retorna o número de caracteres antes do '\0' em s. | char s[] = "Olá"; printf("%zu\n", strlen(s)); // 3\n |
char *strcpy(char *dest, const char *src) | Copia a string src (incluindo '\0') para dest. | char a[10]; strcpy(a, "Mundo"); // a == "Mundo"\n |
char *strncpy(char *dest, const char *src, size_t n) | Copia até n caracteres de src para dest (sem garantia de '\0' se src >= n). | char a[5]; strncpy(a, "ABCDEFG", 4); a[4] = '\0'; // "ABCD"\n |
char *strcat(char *dest, const char *src) | Concatena src ao fim de dest (deve haver espaço suficiente). | char a[20] = "Hello, "; strcat(a, "World!"); // a == "Hello, World!"\n |
char *strncat(char *dest, const char *src, size_t n) | Concatena até n caracteres de src ao fim de dest. | char a[20] = "123"; strncat(a, "456789", 3); // a == "123456"\n |
int strcmp(const char *s1, const char *s2) | Compara lexicograficamente s1 e s2; retorna <0, 0, >0. | printf("%d\n", strcmp("A", "B")); // \<0\n |
int strncmp(const char *s1, const char *s2, size_t n) | Compara até n caracteres de s1 e s2. | printf("%d\n", strncmp("ABC", "ABD", 2)); // 0\n |
char *strchr(const char *s, int c) | Retorna ponteiro para a primeira ocorrência de c em s, ou NULL. | char *p = strchr("banana", 'n'); // p aponta para "nana"\n |
char *strrchr(const char *s, int c) | Retorna ponteiro para a última ocorrência de c em s, ou NULL. | char *p = strrchr("banana", 'a'); // p aponta para "a"\n |
char *strstr(const char *hay, const char *ndl) | Retorna ponteiro para a primeira substring ndl em hay, ou NULL. | char *p = strstr("foobar", "oba"); // p aponta para "obar"\n |
9. Ponteiros
Section titled “9. Ponteiros”Legal, até aqui vimos funções e arrays, conceitos que já nos permitem modularizar e armazenar coleções de dados. Mas C oferece um recurso ainda mais poderoso: os ponteiros, que são variáveis cujo valor é um endereço de memória. Por que entender ponteiros é importante? Porque, com eles, podemos:
- Passar grandes estruturas (arrays, structs) para funções sem copiar todo o conteúdo.
- Manipular diretamente a memória e construir estruturas dinâmicas (listas encadeadas, árvores).
- Controlar a alocação dinâmica de memória com
mallocefree, criando buffers em tempo de execução.
De novo, vamos continuar com um programa pequeno, mas que ilustra vários usos de ponteiros. Veja um exemplo que troca valores, percorre um array com aritmética de ponteiros e imprime uma string literal:
#include <stdio.h>
// Função que troca dois inteiros via ponteirosvoid trocar(int *a, int *b) { int temp = *a; // lê valor apontado por 'a' *a = *b; // escreve valor apontado por 'b' em *a *b = temp; // grava temp em *b}
int main(void) { int x = 10, y = 20; int *px = &x; // px aponta para x int *py = &y; // py aponta para y
printf("Antes: x = %d, y = %d\n", x, y); trocar(px, py); // passando endereços de x e y printf("Depois: x = %d, y = %d\n", x, y);
// Percorrendo um array com ponteiro int arr[] = {1, 2, 3, 4, 5}; int *p = arr; // 'arr' decai para &arr[0] printf("Array: "); for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); // aritmética de ponteiro } printf("\n");
// Imprimindo string literal via ponteiro char *s = "Olá, Mundo!"; printf("String: %s\n", s);
return 0;}Aqui cada ponteiro tem um papel:
int *px = &x;eint *py = &y;Declaramos ponteiros parainte inicializamos com&, obtendo o endereço das variáveisxey.- Na função
trocar(int *a, int *b), os parâmetros são ponteiros. Dentro dela, usamos*ae*bpara ler e escrever nos endereços passados, alterandoxeyna chamada. int *p = arr;faz “decair” o array para um ponteiro ao primeiro elemento. Na expressão*(p + i), somamosiao ponteiro (avançamosielementos) e depois desreferenciamos com*.char *s = "Olá, Mundo!";aponta para a primeira posição de uma string literal. O%sdoprintfpercorre esse bloco até encontrar o caractere'\0'.
A sintaxe de ponteiros em C envolve:
- Declaração:
tipo *nome(por exemplo,int *p;). - Operador
&: obtém o endereço de uma variável (ex.:&x). - Operador
*: desreferencia um ponteiro, acessando o valor apontado (ex.:*p). - Aritmética de ponteiros:
p + nandanposições do tipo apontado porp.
Legal, falamos de alguns usos dos ponteiros, mas sempre acho que um exemplo visual facilita bastante nosso entendimento de como esses elementos são utilizados. Os ponteiros são tipos de variáveis especiais, pois o que eles armazenam não são outros valores, mas sim endereços de memória.
Quando observamos a imagem, podemos ver que as variáveis ptr, num1 e num2 ocupam as posições de memória 0500, 0600 e 0604, respectivamente. O ponteiro ptr armazena o endereço de memória da variável num1, que é 0600. Quando desreferenciamos o ponteiro ptr com *ptr, estamos acessando o valor armazenado na posição de memória 0600.
Um outro ponto bastante interessante de se observar é que os nomes das funções também são ponteiros. Quando declaramos uma função, o nome da função é um ponteiro para o endereço de memória onde a função está armazenada. Isso significa que podemos passar o nome da função como argumento para outra função, ou ainda podemos armazenar o endereço da função em um ponteiro de função. Vamos ver um exemplo disso:
#include <stdio.h>// Função que recebe um ponteiro para uma funçãovoid executar_funcao(void (*funcao)()) { funcao(); // Chama a função passada como argumento}// Função que será passada como argumentovoid minha_funcao() { printf("Executando minha_funcao!\n");}int main() { // Passa o nome da função como argumento executar_funcao(minha_funcao); return 0;}Aqui temos um exemplo de como passar o nome de uma função como argumento para outra função. A função executar_funcao recebe um ponteiro para uma função que não recebe parâmetros e não retorna valor (void (*funcao)()). Dentro da função, chamamos a função passada como argumento. No main, passamos o nome da função minha_funcao como argumento para executar_funcao, que executa a função.
Agora vamos ver uma utilização poderoza dos ponteiros, que a alocação dinâmica de memória. Ela permite que nossos programas solicitem memória para o sistema operacional em tempo de execução! Mas para isso, precisamos compreender primeiro alguns tipos de memória que nossos programas utilizam.
10. Alocação dinâmica de memória
Section titled “10. Alocação dinâmica de memória”Para compreendermos a alocação dinâmica, precisamos entender os diferentes tipos de memória que nossos programas utilizam. Em C, a memória é dividida em quatro áreas principais:
- Text Segment (ou Code Segment): Contém o código executável do programa (as instruções compiladas). Geralmente é somente-leitura e compartilhado entre processos que usam o mesmo binário.
- Data Segment:
- Initialized Data: armazena variáveis globais e estáticas que você inicializa explicitamente.
- Uninitialized Data (BSS - Block Store by Symbol): armazena variáveis globais e estáticas não inicializadas (ou inicializadas a zero).
- Heap: Área de memória usada para alocação dinâmica em tempo de execução. Cresce “para cima” (endereços aumentam) conforme você chama malloc(), calloc() ou realloc(). Você deve liberar esses blocos manualmente com free(), caso contrário ocorre memory leak.
- Stack: Área de memória usada para variáveis locais e contextos de chamada (frames de função). Cresce “para baixo” (endereços diminuem) cada vez que você entra em uma função, e é liberado automaticamente ao retornar. Tamanho limitado; ultrapassar chamando funções recursivas demais ou alocando arrays locais enormes pode causar stack overflow.
Legal, vamos bastante, mas cade um código para gente ver? Vamos lá!
Vamos ver um exemplo de alocação dinâmica de memória em C. Neste exemplo, vamos criar um programa que lê uma quantidade variável de números inteiros do usuário e os armazena em um array alocado dinamicamente.
#include <stdio.h>#include <stdlib.h> // Necessário para malloc e freeint main() { int n; printf("Quantos números você deseja informar? "); scanf("%d", &n);
// Aloca memória dinamicamente para um array de inteiros int *numeros = (int *)malloc(n * sizeof(int)); if (numeros == NULL) { printf("Erro ao alocar memória!\n"); return 1; // Retorna 1 em caso de erro }
// Lê os valores do usuário e armazena no array printf("Informe %d números inteiros:\n", n); for (int i = 0; i < n; i++) { scanf("%d", &numeros[i]); }
// Exibe os valores armazenados no array printf("Os números informados foram:\n"); for (int i = 0; i < n; i++) { printf("%d ", numeros[i]); } printf("\n");
// Libera a memória alocada free(numeros);
return 0;}Aqui temos um exemplo de alocação dinâmica de memória. Utilizamos a função malloc para alocar memória para um array de inteiros com tamanho n, que é lido do usuário. A função malloc retorna um ponteiro para o início da memória alocada, que armazenamos na variável numeros. É importante verificar se a alocação foi bem-sucedida, verificando se o ponteiro retornado é diferente de NULL. Se a alocação falhar, exibimos uma mensagem de erro e retornamos um código de erro.
Depois de ler os valores do usuário e armazená-los no array, exibimos os valores armazenados. Por fim, utilizamos a função free para liberar a memória alocada, evitando vazamentos de memória.
Um último ponto importante sobre a alocação dinâmica de memória é que ela não precisa alocar apenas arrays. Podemos alocar qualquer tipo de dado, incluindo structs e outros tipos definidos pelo usuário. Mas além disso, ela não precisa alocar memória contígua, ou seja, podemos alocar blocos de memória de tamanhos diferentes e armazenar os ponteiros para esses blocos em um array ou lista. Isso nos permite criar estruturas de dados dinâmicas, como listas encadeadas, pilhas e filas.
11. Structs
Section titled “11. Structs”Até aqui vimos como organizar dados simples (variáveis), coleções de dados homogêneos (arrays) e modularizar funções e acessar memória com ponteiros. Mas muitos problemas do mundo real exigem representar objetos compostos, com diversos campos de tipos diferentes — pense em um ponto 2D (x, y), em um aluno (nome, matrícula, notas) ou em um nó de lista encadeada (valor, próximo). É aí que entram as structs, que nos permitem agrupar múltiplos membros sob um único nome.
// Definição de uma struct sem typedefstruct Ponto { int x; int y;};
int main(void) { // Declaração de variável do tipo struct Ponto struct Ponto p1; p1.x = 10; p1.y = 20; printf("Ponto p1: (%d, %d)\n", p1.x, p1.y); return 0;}No exemplo acima, struct Ponto cria um novo tipo composto com dois campos int. Para simplificar a sintaxe, podemos usar typedef:
// Definindo um alias com typedeftypedef struct { int x; int y;} Ponto;
// Agora podemos declarar direto:int main(void) { Ponto p2 = { .x = 5, .y = 15 }; // inicialização por designador printf("Ponto p2: (%d, %d)\n", p2.x, p2.y); return 0;}Exemplos de uso
Section titled “Exemplos de uso”-
Array de structs Agrupar vários registros em um único vetor.
typedef struct {char nome[50];int idade;} Pessoa;Pessoa sala[3] = {{ "Alice", 20 },{ "Bruno", 22 },{ "Carla", 19 }};// Acessando:printf("%s tem %d anos\n", sala[1].nome, sala[1].idade); -
Ponteiro para struct e alocação dinâmica Útil quando o número de elementos só é conhecido em tempo de execução.
Pessoa *grupo = malloc(n * sizeof *grupo);if (!grupo) exit(1);// Preenchendo:strcpy(grupo[0].nome, "Diego");grupo[0].idade = 21;// …free(grupo); -
Struct aninhada Podemos ter structs dentro de structs para modelar hierarquias.
typedef struct {int dia, mes, ano;} Data;typedef struct {char titulo[100];Data lancamento;float nota;} Filme;Filme f = {"Meu Filme",{ .dia = 1, .mes = 7, .ano = 2025 },8.5f};printf("%s (%02d/%02d/%04d): %.1f\n",f.titulo, f.lancamento.dia, f.lancamento.mes,f.lancamento.ano, f.nota);
Com as structs, você organiza dados relacionados de forma elegante e sem perder a eficiência de C. O uso de typedef torna o código mais legível, eliminando a necessidade de escrever struct a todo momento. Em programas maiores, structs formam a base para criar listas encadeadas, árvores, hash tables e outras estruturas dinâmicas que habilitam sistemas complexos e modulares.
Quando você tem um ponteiro para uma struct, o operador -> é a forma mais direta de acessar seus campos, equivalendo a (*ponteiro).campo mas com sintaxe mais limpa.
#include <stdio.h>
typedef struct { int id; char nome[20];} Aluno;
int main(void) { Aluno a = { .id = 123, .nome = "Maria" }; Aluno *pa = &a; // ponteiro para a struct
// Acesso via operador '->' printf("Aluno %d: %s\n", pa->id, pa->nome); // equivale a: // printf("Aluno %d: %s\n", (*pa).id, (*pa).nome);
return 0;}- Declaração:
Aluno *pa = &a;fazpaapontar paraa. - Uso de
->:pa->idlêa.id;pa->nomelêa.nome.
Para criar instâncias de struct em tempo de execução, usamos o heap com malloc e liberamos com free. Assim, não ficamos restritos ao escopo de variáveis locais:
#include <stdio.h>#include <stdlib.h>#include <string.h>
typedef struct { int id; char nome[20];} Aluno;
int main(void) { // Aloca um único struct Aluno Aluno *pa = malloc(sizeof *pa); if (!pa) { perror("malloc falhou"); return 1; }
// Preenche via ponteiro e '->' pa->id = 456; strcpy(pa->nome, "João");
printf("Aluno alocado: %d - %s\n", pa->id, pa->nome);
free(pa); // devolve memória ao heap
// Alocando um vetor de 3 structs Aluno size_t n = 3; Aluno *vet = malloc(n * sizeof *vet); if (!vet) { perror("malloc falhou"); return 1; }
// Inicialização simples for (size_t i = 0; i < n; i++) { vet[i].id = i + 1; snprintf(vet[i].nome, sizeof vet[i].nome, "Aluno%zu", i + 1); }
// Impressão for (size_t i = 0; i < n; i++) { printf("vet[%zu]: %d - %s\n", i, vet[i].id, vet[i].nome); }
free(vet); // libera o array de structs return 0;}malloc(sizeof *pa): aloca exatamente o tamanho de*pa(mais seguro contra mudanças de tipo).free(pa): liberta a memória alocada.- No vetor, usamos
vet[i].campo; poderíamos também usar(vet + i)->campopara ilustrar novamente o->.
Com isso, você pode criar tanto uma única instância quanto coleções de structs em heap, controlando manualmente o tempo de vida dos objetos e evitando limitações de escopo de funções.
12. Arquivos
Section titled “12. Arquivos”Para finalizar nossa jornada, vamos falar sobre arquivos. Em C, podemos ler e escrever dados em arquivos usando funções da biblioteca <stdio.h>. Isso nos permite persistir dados entre execuções do programa, o que é essencial para muitas aplicações. Em C, todo acesso a arquivos em disco é feito via ponteiros do tipo FILE*. Isso nos permite ler, escrever e navegar em arquivos de texto ou binários, mantendo o código organizado e portátil.
- Abrindo e fechando arquivos
#include <stdio.h>
FILE *f = fopen("arquivo.txt", "r"); // abre para leituraif (!f) { perror("fopen"); return 1;}
/* … uso de f … */
fclose(f); // fecha e libera recursos-
Modos comuns de abertura:
"r"– leitura (erro se não existir)"w"– escrita (cria/trunca)"a"– escrita em modo append"r+"– leitura/escrita sem truncar- Acrescente
"b"para arquivos binários ("rb","wb", …).
- Escrevendo em arquivos de texto
#include <stdio.h>
int main(void) { FILE *fw = fopen("saida.txt", "w"); if (!fw) { perror("fopen"); return 1; }
fprintf(fw, "ID: %d\nNome: %s\n", 42, "Fulano"); fprintf(fw, "Nota1: %.1f, Nota2: %.1f\n", 7.5, 8.0);
fclose(fw); return 0;}fprintffunciona comoprintf, mas envia saída para oFILE*.
- Lendo de arquivos de texto
#include <stdio.h>
int main(void) { FILE *fr = fopen("saida.txt", "r"); if (!fr) { perror("fopen"); return 1; }
int id; char nome[50]; float n1, n2;
// lê valores formatados if (fscanf(fr, "ID: %d\nNome: %49[^\n]\n", &id, nome) == 2) { printf("Lido: %d - %s\n", id, nome); }
// pula até o próximo par de notas fscanf(fr, "Nota1: %f, Nota2: %f\n", &n1, &n2); printf("Notas: %.1f, %.1f\n", n1, n2);
fclose(fr); return 0;}fscanffunciona comoscanfmas lê de umFILE*.- Para ler linhas inteiras (inclusive espaços), use
fgets(buf, tamanho, fr).
- Arquivos binários
#include <stdio.h>#include <stdlib.h>
typedef struct { int id; float nota;} Registro;
int main(void) { Registro r = { 123, 9.5f };
// grava binário FILE *fb = fopen("dados.bin", "wb"); if (!fb) { perror("fopen"); return 1; } fwrite(&r, sizeof r, 1, fb); fclose(fb);
// lê binário FILE *fr = fopen("dados.bin", "rb"); if (!fr) { perror("fopen"); return 1; } Registro r2; if (fread(&r2, sizeof r2, 1, fr) == 1) { printf("Registro lido: id=%d, nota=%.1f\n", r2.id, r2.nota); } fclose(fr); return 0;}fwrite(ptr, tamanho, count, FILE*)efreadmanipulam blocos de bytes, ideal para structs ou buffers de dados.
- Movendo o cursor no arquivo
-
fseek(FILE *f, long offset, int whence): desloca o ponteiro de leitura/escrita.SEEK_SET(início),SEEK_CUR(posição atual),SEEK_END(final).
-
ftell(FILE *f): retorna posição atual (em bytes). -
rewind(FILE *f): volta ao início (fseek(f, 0, SEEK_SET)).
Com essas funções, você tem controle completo sobre como seus programas armazenam e recuperam dados de arquivos, seja em formato legível (texto) ou eficiente (binário), aproveitando alocação dinâmica e estruturas compostas conforme necessário.
13. Referências
Section titled “13. Referências”- The C Book: Link
- C Programming Language (Kernighan & Ritchie): Link
- Linguagem C, completa e descomplicada: Link
- Introdução à programação com a linguagem C: Link
- Use a cabeça! C: Link
14. Exercícios
Section titled “14. Exercícios”Pessoal, aqui vou colocar alguns exercícios para vocês praticarem tudo que conversamos até aqui! Eles vão de nível básico até um pouco mais avançado!
-
Soma de dois inteiros: Leia dois números inteiros do teclado e mostre sua soma.
-
Produto de float e int: Leia um número real (float) e um inteiro, calcule e exiba o produto.
-
Área de um círculo: Leia o raio (tipo double) de um círculo e calcule a área (π·r²), exibindo com duas casas decimais.
-
Par ou ímpar: Leia um inteiro e informe se ele é par ou ímpar (usando if/else).
-
Positivo, negativo ou zero: Leia um inteiro e diga se ele é positivo, negativo ou zero.
-
Maior de três números: Leia três inteiros e determine qual deles é o maior (apenas com if/else).
-
Conceito de nota: Leia uma nota (0–100) e atribua um conceito A/B/C/D/F segundo faixas predefinidas (ex.: ≥90 → A, ≥80 → B etc.).
-
Nome do mês: Leia um número de 1 a 12 e imprima o nome do mês correspondente (use switch).
-
Contagem até N: Leia um inteiro positivo N e imprima todos os números de 1 até N (laço for).
-
Soma de 1 a N: Leia N e calcule a soma de todos os inteiros entre 1 e N (pode usar while ou for).
-
Fatorial de N: Leia um inteiro N ≥ 0 e calcule seu fatorial usando um loop.
-
Série de Fibonacci: Leia um inteiro N e imprima os primeiros N termos da sequência de Fibonacci (0, 1, 1, 2, 3…).
-
Média de N valores: Leia N, depois leia N valores reais e exiba a média aritmética.
-
Tabuada de um número: Leia um inteiro X e imprima sua tabuada de 1 a 10.
-
Matriz e transposta: Leia o tamanho n (≤10), depois leia uma matriz quadrada n×n e mostre sua transposta.
-
Função max(a,b): Implemente uma função int max(int a, int b) que retorne o maior de dois inteiros e use-a para comparar dois valores lidos.
-
Função fatorial(n): Crie uma função long fatorial(int n) e utilize-a para mostrar o fatorial de um número lido.
-
Função recursiva de Fibonacci: Escreva uma função recursiva int fib(int n) e exiba o n-ésimo termo lido do usuário.
-
Verificador de número primo: Implemente uma função int eh_primo(int x) que retorne 1 se x for primo ou 0 caso contrário; depois, leia um valor N e liste todos os primos de 2 até N.
-
Utilizando ponteiros: Crie um programa que leia um número inteiro e utilize ponteiros para calcular o dobro e o triplo desse número, exibindo os resultados.
-
Alocação dinâmica de memória: Implemente um programa que leia N números inteiros, aloque dinamicamente um array para armazená-los e calcule a média desses números.
-
Structs: Defina uma struct para representar um aluno (com nome, matrícula e nota) e implemente um vetor para armazenar vários alunos, permitindo adicionar, remover e listar alunos.
A partir daqui os exercícios são um pouco mais robustos e exigem um pouco mais de reflexão, mas não se preocupe, você vai conseguir!
- Gerenciamento de Estoque de Loja com
structe Alocação Dinâmica
Você vai implementar em C um sistema de gerenciamento de estoque para uma pequena loja, utilizando struct para representar cada produto e um vetor dinâmico para armazenar esses produtos em memória. Seu programa deverá prover um menu interativo com as seguintes funcionalidades mínimas:
-
Definição das Estruturas
typedef struct { int codigo; // identificador único do produto char nome[50]; // nome do produto char categoria[30]; // categoria ou departamento float preco; // preço unitário int quantidade; // unidades em estoque int estoque_minimo; // nível mínimo para reabastecimento } Produto;- Use um vetor de
Produtoalocado commalloc/realloc, de forma que seu programa possa crescer conforme novos itens sejam cadastrados.
-
Menu de Operações Implemente um laço que exiba opções como:
1) Cadastrar novo produto2) Remover produto (por código)3) Atualizar estoque (entrada/saída de mercadorias)4) Listar todos produtos5) Listar produtos abaixo do estoque mínimo6) Consultar produto (por código ou nome)7) Calcular valor total do estoque0) SairCada opção deve chamar uma função específica, recebendo ponteiro para o vetor e seu tamanho atual, e retornar — se necessário — o novo tamanho (após remoções ou inclusões).
-
Requisitos Funcionais
-
Cadastrar produto: ler todos os campos e inserir no fim do vetor (use
reallocpara expandi-lo). -
Remover produto: localizar pelo
codigo, deslocar os elementos seguintes para “fechar o buraco” e reduzir o tamanho comrealloc. -
Atualizar estoque: pedir código, ler quantidade (+ para entrada, – para saída) e ajustar
quantidademantendo-a ≥ 0. -
Listagens:
- Todos os produtos: exibir tabela com
codigo,nome,categoria,preco,quantidade. - Estoque baixo: listar apenas itens em que
quantidade < estoque_minimo.
- Todos os produtos: exibir tabela com
-
Consulta: buscar por código (igual) ou nome (substring com
strstr), exibindo detalhes completos. -
Valor total do estoque: somar, para cada produto,
preco * quantidadee imprimir o resultado.
-
-
Persistência (opcional, nível avançado)
- Ao iniciar, ler um arquivo binário “estoque.dat” com a lista de produtos (
fread). - Ao sair, gravar o vetor atualizado em “estoque.dat” (
fwrite), garantindo que, ao reiniciar, os dados anteriores sejam restaurados.
- Ao iniciar, ler um arquivo binário “estoque.dat” com a lista de produtos (
-
Boas-práticas e Observações
- Modularize seu código em funções com assinatura clara (use
typedefpara apelidar ponteiros a funções, se desejar). - Verifique retornos de
malloc/realloce de operações de arquivo antes de usar os ponteiros. - Use
->apenas quando trabalhar com ponteiros paraProduto; para o vetor, use notaçãovet[i].campo. - Trate entradas inválidas (códigos não encontrados, quantidades negativas, limites de buffer em
scanf).
- Modularize seu código em funções com assinatura clara (use