Do 'Hello, World!' ao Gerenciamento de Memória e Estruturas de Dados
Autoria (Autoria Discente)
Este material foi produzido como parte integrante da Atividade de Extensão do Curso Superior de Tecnologia em Análise e Desenvolvimento de Sistemas da UNIP.
Autores :
● GUILHERME BORGES (G03IHI3)
● IGOR VIEIRA MAGALHÃES (G0610I3)
● KELWIN ALEXANDRE ASSENCIO MANGINI (G9936J5)
● LEONARDO MONTEIRO MARQUES (R1885B9)
● MIGUEL DOS SANTOS GALVÃO (R110IF1)
● RAFAEL ARLES SOUZA DANTAS (R0796I0)
● YGOR MOREIRA RIBAS (G9048H0)
● DIEGO YUGI UEHARA(G9936G0)
Prefácio
Bem-vindo ao mundo do ASP.NET Core, a poderosa plataforma da Microsoft para o desenvolvimento de aplicações web modernas!
Este e-book foi criado com o propósito de guiar você em uma jornada prática e estruturada, do primeiro contato com o ecossistema .NET até a construção de aplicações reais e profissionais.
Nos últimos anos, a demanda por desenvolvedores back-end qualificados cresceu exponencialmente. O ASP.NET Core se consolidou como uma das principais tecnologias para criação de APIs, microsserviços e sistemas corporativos, unindo alto desempenho, segurança e flexibilidade multiplataforma. No entanto, para muitos iniciantes, o universo .NET pode parecer complexo à primeira vista. Foi pensando nisso que este projeto nasceu: tornar o aprendizado do ASP.NET Core acessível, direto e voltado para a prática.
Ao longo dos capítulos, você encontrará explicações claras, exemplos de código comentados e exercícios que reforçam o aprendizado. Cada seção foi cuidadosamente elaborada para ajudá-lo a compreender não apenas como o framework funciona, mas por que ele é estruturado dessa forma — conectando a teoria à prática de desenvolvimento moderna.
Justificativa e Motivação
Em um mercado de tecnologia em constante evolução, compreender os fundamentos e padrões do ASP.NET Core tornou-se essencial para quem deseja atuar no desenvolvimento web de alto nível.
O .NET não é apenas uma tecnologia: é um ecossistema completo, capaz de entregar soluções escaláveis, performáticas e seguras.
Este e-book surgiu da necessidade de preencher uma lacuna — a falta de material em português que apresente o ASP.NET Core de maneira didática, acessível e moderna. Nosso objetivo é permitir que estudantes, profissionais e entusiastas possam dar seus primeiros passos nesse universo com confiança e clareza. Mais do que um guia técnico, este material busca ser uma ponte entre o ambiente acadêmico e o mercado de trabalho, preparando o leitor para os desafios reais da programação back-end em 2025 e além.
Objetivos do Projeto
Objetivo Geral
Capacitar o leitor a desenvolver aplicações web back-end utilizando o framework ASP.NET Core, promovendo o aprendizado prático e a autonomia no uso de ferramentas e padrões modernos do ecossistema .NET.
Objetivos Específicos
Apresentar os conceitos fundamentais do .NET SDK e CLI
Explorar os principais padrões de arquitetura do framework, como MVC, Razor Pages e Web API.
Demonstrar o uso do Entity Framework Core para acesso a dados de forma prática e segura.
Integrar conhecimentos sobre autenticação e segurança com ASP.NET Identity.
Disponibilizar o e-book como uma referência pública e gratuita para toda a comunidade interessada em desenvolvimento web.
Público-Alvo
Este e-book é voltado para todos aqueles que desejam iniciar ou aprimorar seus conhecimentos em desenvolvimento back-end com .NET, especialmente:
Iniciantes em programação que buscam uma introdução guiada ao ASP.NET Core.
Estudantes e profissionais de tecnologia que desejam ampliar suas competências em arquitetura de software moderna.
Pessoas em transição de carreira interessadas em ingressar no mercado de desenvolvimento web.
Entusiastas e membros da comunidade local que desejam adquirir novas habilidades digitais com foco em empregabilidade.
Sobre Este Livro
O e-book "ASP.NET Core: O Guia Completo" foi estruturado para conduzir você passo a passo no aprendizado dessa plataforma robusta e versátil. Começaremos pelos fundamentos do .NET SDK e da CLI, explorando a estrutura de projetos e os conceitos que sustentam o framework. Em seguida, mergulharemos em arquiteturas como MVC e Razor Pages, construiremos APIs RESTful, e trabalharemos com bancos de dados usando o Entity Framework Core
Também abordaremos práticas essenciais como injeção de dependência, autenticação e autorização, e boas práticas de desenvolvimento que garantem performance e segurança.
Ao final da jornada, você aplicará todos esses conceitos em um projeto prático: a criação de um blog completo, consolidando seu aprendizado. Prepare-se para descobrir o potencial do ASP.NET Core e elevar suas habilidades de programação a um novo nível! Bem-vindo
ao Mundo do C!
Este ebook é um guia abrangente e prático para a linguagem de programação C, projetado para levá-lo de um iniciante completo a um programador C proficiente.
Cobrimos desde os conceitos mais básicos, como variáveis e controle de fluxo, até tópicos avançados como ponteiros, gerenciamento de memória dinâmica e estruturas de dados complexas. Com explicações claras, exemplos de código detalhados e exercícios práticos em cada capítulo, você construirá uma base sólida que não apenas o capacitará a escrever código C robusto, mas também a entender profundamente como os sistemas de software funcionam. Prepare-se para desvendar o poder e a elegância da linguagem C!
Sumário
Capítulo 1: Primeiros Passos com a Linguagem C
Capítulo 2: Variáveis, Tipos de Dados e Operadores
Capítulo 3: Controle de Fluxo
Capítulo 4: Funções
Capítulo 5: Arrays e Strings
Capítulo 6: O Coração do C: Ponteiros
Capítulo 7: Gerenciamento Dinâmico de Memória
Capítulo 8: Estruturas, Uniões e Enumerações
Capítulo 9: Entrada e Saída de Arquivos (File I/O)
Capítulo 10: O Pré-processador e Múltiplos Arquivos
Capítulo 11: Projeto Final: Agenda de Contatos em Linha de Comando
Capítulo 12: Glossário e Próximos Passos
Capítulo 1: Primeiros Passos com a Linguagem
C
Bem-vindo ao ponto de partida da sua jornada na programação C! Neste capítulo, vamos explorar a rica história da linguagem, entender por que ela continua sendo uma ferramenta fundamental no desenvolvimento de software e configurar nosso ambiente para escrever e executar seu primeiro programa.
1.1 História e Importância da Linguagem C
A linguagem C foi desenvolvida por Dennis Ritchie nos laboratórios Bell entre 1969 e 1973. Criada inicialmente para desenvolver o sistema operacional UNIX, o C rapidamente ganhou popularidade devido à sua eficiência, flexibilidade e capacidade de interagir diretamente com o hardware. Desde então, o C se tornou uma das linguagens de programação mais influentes de todos os tempos.
Muitos dos sistemas operacionais modernos (incluindo Linux, macOS e Windows), bancos de dados, compiladores, e até mesmo outras linguagens de programação (como C++, C#, Java, JavaScript e Python) foram escritos ou influenciados pelo C. Sua
importância reside na sua capacidade de oferecer controle de baixo nível sobre a memória e o hardware, sem abrir mão de construções de alto nível que facilitam a programação estruturada.
1.2 Por que Aprender C em um Mundo de Linguagens de Alto Nível?
Em uma era dominada por linguagens de alto nível com gerenciamento automático de memória e abstrações ricas, por que alguém se dedicaria a aprender C? As razões são várias e convincentes:
• Desempenho: C permite escrever código extremamente rápido, pois oferece controle direto sobre a memória e os recursos do sistema, resultando em programas otimizados e eficientes.
• Entendimento Profundo: Aprender C força você a entender como um computador realmente funciona – como a memória é organizada, como as variáveis são armazenadas e como o processador executa instruções.
• Sistemas Embarcados e Kernels: É a linguagem de escolha para programação de sistemas embarcados, drivers de dispositivo e kernels de sistemas operacionais, onde o desempenho e o controle de hardware são cruciais.
• Base para Outras Linguagens: Muitas linguagens populares são implementadas em C (ou C++). Entender C facilita a compreensão de como essas linguagens funcionam "por baixo do capô".
• Portabilidade: Embora ofereça controle de baixo nível, o C é surpreendentemente portátil. O código C pode ser compilado e executado em uma vasta gama de plataformas, desde microcontroladores até supercomputadores.
1.3 Configurando o Ambiente: Instalando um Compilador
Para escrever e executar programas em C, você precisará de um compilador. Um compilador é um programa que traduz seu código-fonte C (escrito em texto legível por humanos) em código de máquina (instruções que o computador pode executar). O compilador mais popular e amplamente utilizado é o GCC (GNU Compiler Collection).
Instalação do GCC:
• Linux: O GCC geralmente vem pré-instalado ou é facilmente instalável.
Abra um terminal e execute:
• # Para distribuições baseadas em Debian/Ubuntu
• sudo apt update
• sudo apt install build-essential # Isso instala o GCC e outras f erramentas
•
• # Para distribuições baseadas em Fedora/RHEL
sudo yum install gcc # ou dnf install gcc
• macOS: O GCC é fornecido como parte das "Command Line Tools" do Xcode. Abra um terminal e execute:
• Windows: A maneira mais fácil de obter o GCC é instalando o MinGW (Minimalist GNU for Windows) ou o TDM-GCC.
1. Acesse o site MinGW-w64 ou TDM-GCC.
2. Baixe e execute o instalador.
3. Certifique-se de adicionar o diretório bin da sua instalação do MinGW/TDM-GCC (ex: C:\MinGW\bin) à variável de ambiente PATH do seu sistema. Isso permitirá que você execute gcc a partir de qualquer diretório no prompt de comando.
Para verificar se o GCC foi instalado corretamente, abra um terminal (ou prompt de comando no Windows) e digite:
1.4 O Processo de Compilação
Entender o processo de compilação é fundamental para depurar e otimizar seus programas C. Ele pode ser dividido em quatro etapas principais:
1. Pré-processamento: O pré-processador (cpp) expande as diretivas do préprocessador (como #include para incluir cabeçalhos e #define para macros). Ele gera um arquivo intermediário com extensão .i.
2. Compilação: O compilador (cc1 no caso do GCC) traduz o código C préprocessado em código assembly. Ele gera um arquivo com extensão .s
Siga as instruções para instalar as ferramentas.
Você deverá ver a versão do GCC instalada.
3. Montagem (Assembly): O montador (as) traduz o código assembly em código de máquina (linguagem binária) específico para a arquitetura do processador. Ele gera um arquivo objeto com extensão .o (ou .obj no Windows).
4. Linkagem (Linking): O linker (ld) combina um ou mais arquivos objeto com as bibliotecas necessárias (como a biblioteca padrão C, libc) para criar o programa executável final.
Normalmente, você executa todas essas etapas com um único comando gcc, que coordena as ferramentas internas automaticamente.
1.5 Seu Primeiro Programa: "Hello, World!"
É uma tradição que o primeiro programa em qualquer nova linguagem seja o "Hello, World!". Ele é simples, mas demonstra os componentes essenciais de um programa C.
Crie um arquivo chamado hello.c e adicione o seguinte código:
#include <stdio.h> // Inclui a biblioteca padrão de entrada/saída
intmain(void) // Função principal onde o programa começa a execução
{ printf("Hello, World!\n"); // Imprime a string "Hello, World!" na tela
return0; // Indica que o programa terminou com sucesso
}
Análise da Estrutura:
• #include <stdio.h>: Esta é uma diretiva do pré-processador. Ela instrui o compilador a incluir o conteúdo do arquivo de cabeçalho stdio.h no seu código. stdio.h (Standard Input/Output) contém declarações para funções como printf, que usamos para imprimir texto na tela.
• int main(void): Esta é a declaração da função principal.
o int: Indica que a função main retornará um valor inteiro. Por convenção, 0 significa que o programa executou com sucesso, e outros valores indicam um erro. omain: É o ponto de entrada do programa. Quando você executa um programa C, o sistema operacional procura e começa a executar a função main o(void): Indica que a função main não
aceita nenhum argumento. Você também pode ver (int argc, char *argv[]) para programas que aceitam argumentos da linha de comando, que abordaremos mais tarde.
• { }: As chaves delimitam o corpo da função main, contendo as instruções a serem executadas.
• printf("Hello, World!\n");:
o printf: É uma função da biblioteca padrão usada para imprimir saída formatada na console. o"Hello, World!\n": É a string de texto a ser impressa. O \n é um caractere de nova linha (newline), que move o cursor para a próxima linha após a impressão. o ;: O ponto e vírgula marca o final de uma instrução em C. É crucial e sua ausência resultará em um erro de compilação.
• return 0;: Esta instrução termina a execução da função main e retorna o valor 0 ao sistema operacional, indicando sucesso.
Compilando e Executando:
1. Salve o arquivo como hello.c
2. Abra seu terminal (ou prompt de comando) e navegue até o diretório onde você salvou o arquivo.
3. Compile o programa usando o GCC: gcc hello.c -o hello
Este comando compila hello.c e cria um arquivo executável chamado hello (ou hello.exe no Windows).
4 Execute o programa :
# No Linux/macOS
./hello
# No Windows hello.exe
Você deverá ver a saída:
Hello, World!
Exercício 1.1: Modificando seu Primeiro Programa
1. Abra o arquivo hello.c.
2. Modifique a string dentro da função printf para imprimir uma mensagem diferente, como "Olá, Mundo da Programação C!" ou "Meu nome é [Seu Nome] e estou aprendendo C!".
3. Salve as alterações.
4. Compile e execute o programa novamente para ver sua nova mensagem.
Parabéns! Você acabou de dar seus primeiros passos no mundo da programação C. Nos próximos capítulos, aprofundaremos em conceitos mais complexos, mas igualmente fundamentais.
Capítulo 2: Variáveis, Tipos de Dados e Operadores
Neste capítulo, mergulharemos nos blocos de construção fundamentais de qualquer programa C: variáveis, tipos de dados e operadores. Entender como armazenar e manipular dados é essencial para criar programas funcionais e eficientes.
2.1 O que são Variáveis?
No contexto da programação, uma variável é um nome simbólico para um local de armazenamento na memória do computador. Ela é usada para guardar um valor que pode ser alterado durante a execução do programa. Imagine uma variável como uma caixa nomeada onde você pode guardar dados; o nome da caixa é a variável e o conteúdo é o seu valor.
Para usar uma variável em C, você precisa primeiro declarar ela, especificando seu tipo de dado e seu nome. O tipo de dado informa ao compilador quanto espaço na memória deve ser reservado para a variável e que tipo de valores ela pode armazenar (números inteiros, caracteres, números de ponto flutuante, etc.).
2.2 Tipos de Dados Fundamentais
C possui um conjunto de tipos de dados básicos que são a base para todos os outros tipos. Os mais comuns são:
• int: Usado para armazenar números inteiros (sem casas decimais). Geralmente ocupa 2 ou 4 bytes de memória, dependendo do sistema, e pode armazenar valores positivos e negativos.
• char: Usado para armazenar um único caractere (letras, símbolos, dígitos). Geralmente ocupa 1 byte e pode ser interpretado como um pequeno número inteiro.
• float: Usado para armazenar números de ponto flutuante (com casas decimais), também conhecidos como números reais. Oferece precisão simples (geralmente 4 bytes).
• double: Também usado para números de ponto flutuante, mas com precisão dupla (geralmente 8 bytes), o que significa que pode armazenar números maiores e com mais casas decimais do que float.
Modificadores de Tipo:
Os tipos de dados fundamentais podem ser modificados para atender a necessidades específicas de armazenamento. Os modificadores comuns são:
• short: Sugere que o tipo inteiro deve ter um tamanho menor. Por exemplo, short int.
• long: Sugere que o tipo deve ter um tamanho maior. Pode ser usado com int (long int ou apenas long) e double (long double).
• signed: (Padrão para int, short, long) Indica que a variável pode armazenar valores positivos e negativos.
• unsigned: Indica que a variável só pode armazenar valores não negativos (zero ou positivos), dobrando o valor máximo que pode ser armazenado.
Ex: unsigned int, unsigned char
Exemplo de declaração e uso de tipos:
#include <stdio.h>
#include <limits.h> // Para INT_MAX, etc.
#include <float.h> // Para FLT_MAX, DBL_MAX, etc.
int main(void) {
int idade = 30;
char inicial = 'Y';
float altura = 1.75f; // 'f' para indicar que é um float literal double pi = 3.1415926535;
unsigned int populacao = 1000000000U; // 'U' para unsigned long long big_number = 123456789012345LL; // 'LL' para long long
printf("sizeof(double): %zu bytes (max: %e)\n", sizeof(double), DB L_MAX);
return 0; }
Nota: Os especificadores de formato (%d, %c, %f, %u, %lld) em printf são cruciais para indicar ao compilador como interpretar e imprimir os valores das variáveis. Usar um especificador incorreto pode levar a resultados inesperados ou erros.
2.3 Declarando e Inicializando Variáveis
Você pode declarar uma variável e inicializá-la (atribuir um valor inicial) na mesma linha ou separadamente:
int numero; // Declaração: 'numero' existe, mas seu valor é indefinido
char letra = 'A'; // Declaração e Inicialização: 'letra' armazena 'A'
numero = 10; // Atribuição de valor a uma variável já declarad a
float preco = 9.99f; // Declarando e inicializando outro tipo
Cuidado: Variáveis não inicializadas em C contêm "lixo de memória" (valores arbitrários). Sempre inicialize suas variáveis para evitar comportamentos imprevisíveis.
2.4 Constantes: #define e const
Constantes são valores que não podem ser alterados durante a execução do programa. C oferece duas maneiras principais de defini-las:
2.4.1 O Pré-processador #define
A diretiva #define é processada antes da compilação real. Ela instrui o préprocessador a substituir todas as ocorrências de um identificador por um valor literal no código. Isso é conhecido como uma macro.
#include <stdio.h>
#define PI 3.14159 // Define PI como uma constante simbólica
#define TAMANHO_MAXIMO 100
int main(void) {
float raio = 5.0f;
float area = PI * raio * raio; // PI será substituído por 3.14159
printf("A área do círculo é: %.2f\n", area);
printf("Tamanho máximo: %d\n", TAMANHO_MAXIMO);
return 0; }
Vantagens de #define:
• Não ocupa espaço na memória do programa executável, pois a substituição ocorre antes da compilação.
• Pode ser usado para definir macros mais complexas (com argumentos).
Desvantagens:
• Não possui verificação de tipo (type checking).
• Pode levar a efeitos colaterais inesperados em macros complexas.
2.4.2 A Palavra-chave const
A palavra-chave const é uma forma mais moderna e segura de definir constantes. Ela define uma variável cujo valor não pode ser alterado após a inicialização. Ao contrário de #define, as constantes const são processadas pelo compilador, não pelo préprocessador.
#include <stdio.h>
int main(void) {
const float PI_CONST = 3.14159f; // Constante de ponto flutuante
const int MAX_VALOR = 200; // Constante inteira
float raio = 5.0f;
float area = PI_CONST * raio * raio;
printf("A área do círculo (const) é: %.2f\n", area);
printf("Valor máximo (const):\%d n", MAX_VALOR);
// PI_CONST = 3.0f; // Isso causaria um erro de compilação!
return0; }
Vantagens de const :
• Possui verificação de tipo, tornando o código mais seguro.
• Pode ser declarada com escopo, o que é útil em funções.
• É preferível para a maioria das constantes, especialmente as tipadas.
2.5 Operadores
Operadores são símbolos que realizam operações em uma ou mais variáveis ou valores (operandos). C possui uma rica variedade de operadores.
2.5.1 Operadores Aritméticos
Usados para realizar operações matemáticas básicas:
• +: Adição
• -: Subtração
• *: Multiplicação
• /: Divisão (divisão inteira para operandos inteiros)
• %: Módulo (resto da divisão inteira)
• ++: Incremento (a++ ou ++a)
• : Decremento (a-- ou --a)
int a = 10, b = 3;
printf("a + b = %d\n", a + b); // 13
printf("a / b = %d\n", a / b); // 3 (divisão inteira)
printf("a % b = %d\n", a % b); // 1
printf("a++ = %d\n", a++); // 10 (valor de 'a' antes do incremento, 'a ' agora é 11)
printf("++b = %d\n", ++b); // 4 (valor de 'b' depois do incremento)
2.5.2 Operadores Relacionais
Usados para comparar dois valores; o resultado é um valor booleano (verdadeiro ou falso, representados por 1 ou 0 em C).
• ==: Igual a
• !=: Diferente de
• >: Maior que
• <: Menor que
• >=: Maior ou igual a
• <= Menor ou igual a :
int x = 5, y = 10;
printf("x == y: %d\n", x == y); // 0 (falso)
printf("x < y: %d\n", x < y); // 1 (verdadeiro)
2.5.3 Operadores Lógicos
Usados para combinar ou inverter condições relacionais. Também retornam 1 (verdadeiro) ou 0 (falso).
• &&: AND lógico (ambas as condições devem ser verdadeiras)
• ||: OR lógico (pelo menos uma condição deve ser verdadeira)
char* status = (idade >= 18) ? "Maior de idade" : "Menor de idade";
printf("Status: %s\n", status); // Saída: Status: Maior de idade
Exercício 2.1: Crie um programa que calcula a área de um círculo
Escreva um programa em C que faça o seguinte:
1. Declare uma constante para o valor de PI (use const double PI = 3.1415926535;).
2. Declare uma variável raio do tipo double
3. Atribua um valor para raio (por exemplo, 10.0).
4. Calcule a área do círculo usando a fórmula area = PI * raio * raio;.
5. Imprima o valor do raio e da área na tela, formatando a área com duas casas decimais.
6. Modifique o programa para que ele solicite ao usuário o valor do raio (use scanf("%lf", &raio); - o %lf é para double, e o & será explicado no capítulo de ponteiros).
// Exemplo de como usar scanf para ler um double (parte do exercício)
#include <stdio.h>
int main(void) {
const double PI = 3.1415926535; double raio; double area; printf("Digite o valor do raio do círculo: "); scanf("%lf", &raio); // O '&' antes de 'raio' é essencial para sca nf
area = PI * raio * raio;
printf("Raio: %.2f\n", raio);
printf("Area: %.2f\n", area);
return 0; }
Capítulo 3: Controle de Fluxo
A habilidade de um programa tomar decisões e repetir ações é o que o torna útil. Neste capítulo, exploraremos as estruturas de controle de fluxo da linguagem C, que permitem aos seus programas reagir a diferentes condições e automatizar tarefas repetitivas.
3.1 Estruturas de Decisão: if, else if, else
As estruturas if, else if e else permitem que seu programa execute diferentes blocos de código com base em condições booleanas.
3.1.1 O comando if
O if é a estrutura de decisão mais básica. Ele executa um bloco de código se uma condição for verdadeira.
#include <stdio.h>
int main(void) {
int numero = 10;
if (numero > 0) {
printf("O número é positivo.\n"); }
return 0;
}
Se houver apenas uma instrução no bloco if, as chaves {} são opcionais, mas é uma boa prática sempre usá-las para evitar erros e melhorar a legibilidade.
3.1.2 O comando if-else
O if-else permite que você forneça um bloco de código alternativo para ser executado se a condição if for falsa.
#include <stdio.h>
int main(void) {
int numero = -5;
if (numero > 0) {
printf("O número é positivo.\n");
} else {
printf("O número é zero ou negativo.\n");
} return 0;
}
3.1.3 O comando if-else if-else
Para múltiplas condições, você pode encadear cláusulas else if
#include <stdio.h>
int main(void) {
int nota = 75;
if (nota >= 90) {
printf("Conceito A\n");
} else if (nota >= 80) {
printf("Conceito B\n");
} else if (nota >= 70) {
printf("Conceito C\n");
} else {
printf("Reprovado\n");
} return 0; }
3.2 O Operador Ternário
Vimos o operador ternário brevemente no Capítulo 2. Ele é uma forma concisa de expressar um if-else simples em uma única linha, útil para atribuições condicionais.
mensagem = (idade >= 18) ? "Você é maior de idade.": "Você é meno r de idade.";
printf("%s\n", mensagem); return 0; }
3.3 A Estrutura
switch -case
A estrutura switch-case é ideal para situações onde você precisa escolher entre múltiplas opções com base no valor de uma única variável (geralmente um inteiro ou caractere).
#include <stdio.h>
int main(void) {
int dia = 3; // 1=Domingo, 2=Segunda, etc.
switch (dia) {
case 1: printf("Domingo\n"); break; // Sai do switch
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: printf("Sábado\n"); break; default: printf("Número de dia inválido.\n"); break; } return 0; }
Importante: A palavra-chave break é essencial para sair do bloco switch após um case ser correspondido. Se você omitir o break, o programa continuará executando os blocos de código dos cases seguintes (comportamento conhecido como "fallthrough") até encontrar um break ou o final do switch.
3.4 Laços de Repetição: while, do-while e for Laços permitem executar um bloco de código repetidamente.
3.4.1 O laço while
O while executa um bloco de código enquanto uma condição for verdadeira. A condição é testada antes de cada iteração.
#include <stdio.h>
int main(void) {
int contador = 0;
while (contador < 5) {
printf("Contador: %d\n", contador);
contador++; // Não esqueça de atualizar a condição para evitar um loop infinito!
} return 0;
}
3.4.2 O laço do-while
O do-while é similar ao while, mas garante que o bloco de código seja executado pelo menos uma vez, pois a condição é testada depois da primeira iteração.
#include <stdio.h>
int main(void) {
int num;
do {
printf("Digite um número positivo: "); scanf("%d", &num);
} while (num <= 0);
printf("Número digitado: %d\n", num); return 0; }
3.4.3 O laço for
O for é ideal para laços onde o número de iterações é conhecido ou pode ser determinado no início. Ele consolida a inicialização, a condição de término e a atualização em uma única linha.
Sintaxe: for (inicialização; condição; atualização) { // bloco de código }
#include <stdio.h>
int main(void) {
// Imprime números de 0 a 4
for (int i = 0; i < 5; i++) {
printf("Iteração %d\n", i);
} return 0;
}
3.5 Comandos de Desvio: break e continue
Esses comandos permitem alterar o fluxo normal de um laço.
3.5.1 O comando break
O break sai imediatamente do laço mais interno (for, while, do-while) ou da estrutura switch.
#include <stdio.h>
int main(void) {
for (int i = 0; i < 10; i++) { if (i == 5) {
printf("Encontrei o 5, saindo do loop.\n"); break; // O loop terminará aqui
printf("%d\n", i);
}
printf("Fora do loop.\n");
return 0; }
3.5.2 O comando continue
O continue pula a iteração atual do laço e avança para a próxima iteração, testando a condição novamente.
#include <stdio.h>
int main(void) {
for (int i = 0; i < 5; i++) {
if (i == 2) {
printf("Pulando a iteração 2.\n");
continue; // Pula o restante do código para i=2 e vai para a próxima iteração
}
printf("Iteração %d\n", i);
} return 0; }
Exercício 3.1: Crie um programa que imprime a tabuada de um número fornecido pelo usuário.
Seu programa deve:
1. Solicitar ao usuário que digite um número inteiro.
2. Usar um laço for para calcular e imprimir a tabuada desse número de 1 a 10.
3. Exemplo de saída para o número 7:
4. Tabuada do 7:
5. 7 x 1 = 7
6. 7 x 2 = 14
7. ...
7 x 10 = 70
Capítulo 4: Funções
Funções são blocos de código que realizam uma tarefa específica. Elas são a espinha dorsal da programação modular em C, permitindo organizar seu código, reutilizá-lo e torná-lo mais legível e fácil de manter.
4.1 A Importância da Modularização
Modularizar um programa significa dividi-lo em partes menores e gerenciáveis, cada uma com uma responsabilidade bem definida. As funções são a principal ferramenta para alcançar essa modularização em C. As vantagens incluem:
• Reutilização de Código: Escreva um código uma vez e chame-o de vários lugares.
• Organização: Programas grandes se tornam mais fáceis de entender quando divididos em funções lógicas.
• Manutenção: Erros podem ser isolados e corrigidos mais facilmente em funções específicas.
• Colaboração: Múltiplos desenvolvedores podem trabalhar em diferentes funções simultaneamente.
• Abstração: Você pode usar uma função sem precisar saber os detalhes internos de sua implementação.
4.2 Declarando, Definindo e Chamando Funções
Um processo de três etapas para usar funções em C:
1. Declaração (Protótipo): Informa ao compilador sobre a existência de uma função, seu tipo de retorno, nome e os tipos de seus parâmetros. É como uma "promessa" ao compilador. Geralmente, as declarações ficam no início do arquivo ou em arquivos de cabeçalho (.h).
2. Definição: Contém o corpo real da função, ou seja, as instruções que a função executa.
3. Chamada: Invoca a função para que ela execute sua tarefa.
Exemplo:
#include <stdio.h>
// 1. Declaração da função (protótipo)
int somar(int a, int b);
int main(void) {
int resultado;
// 3. Chamada da função
resultado = somar(5, 3);
printf("A soma é: %d\n", resultado);
printf("A soma de 10 e 20 é: %d\n", somar(10, 20));
return0;
} // 2. Definição da função
intsomar(inta, intb) {
returna+b; }
Se a definição da função vier antes de sua primeira chamada no código (como a
função main geralmente faz), a declaração (protótipo) é opcional.
4.3 Parâmetros e Argumentos: Passagem por Valor
• Parâmetros: São as variáveis listadas na declaração e definição da função (ex: a e b em int somar(int a, int b)).
• Argumentos: São os valores reais passados para a função quando ela é chamada (ex: 5 e 3 em somar(5, 3)).
Em C, os argumentos são passados para as funções por valor por padrão. Isso significa que uma cópia do valor do argumento é passada para o parâmetro da função. Qualquer alteração feita no parâmetro dentro da função não afeta a variável original fora da função.
#include <stdio.h>
void modificarValor(int x) {
printf("Dentro da função: x antes = %d\n", x);
x = 100; // Altera a CÓPIA de 'valorOriginal'
printf("Dentro da função: x depois = %d\n", x);
intmain(void) {
intvalorOriginal= 10;
printf("Antes da função: valorOriginal = %d\n", valorOriginal);
modificarValor(valorOriginal); // Passa uma CÓPIA de valorOriginal
printf("Depois da função: valorOriginal = %d\n", valorOriginal); / / Continua sendo 10
return0;
Para alterar variáveis fora da função, precisamos usar ponteiros (passagem por
referência), que será abordado em detalhes no Capítulo 6.
4.4 O Valor de Retorno e o Tipo void
• Valor de Retorno: Uma função pode retornar um único valor ao chamador usando a palavra-chave return, seguido do valor. O tipo do valor retornado deve corresponder ao tipo de retorno especificado na declaração da função.
• Tipo void: Se uma função não retorna nenhum valor, seu tipo de retorno deve ser void. Funções void geralmente realizam uma ação (como imprimir algo na tela) em vez de calcular e retornar um valor.
#include <stdio.h>
// Função que retorna um int
int multiplicar(int a, int b) {
return a * b;
// Função que não retorna nada (void)
voidimprimirMensagem(void) {
printf("Esta é uma mensagem de uma função void.\n");
} intmain(void) {
intprod= multiplicar (4, 6);
printf("Produto: \%dn", prod);
imprimirMensagem (); return0;
4.5Escopo
de Variáveis: Locais, Globais e Estáticas
O escopo de uma variável determina onde no seu programa ela pode ser acessada.
• Variáveis Locais:
o Declaradas dentro de uma função ou bloco de código (delimitado por {}). o Só podem ser acessadas de dentro do bloco onde foram declaradas. o Sua vida útil começa quando o bloco é executado e termina quando o bloco é finalizado (são alocadas na pilha).
• void minhaFuncao(void) {
• int varLocal = 10; // Variável local para minhaFuncao
• printf("Dentro da função: %d\n", varLocal);
• }
• int main(void) {
• // printf("%d\n", varLocal); // Erro! varLocal não é acessív el aqui
• return 0; }
• Variáveis Globais:
o Declaradas fora de todas as funções, geralmente no início do arquivo fonte. o Podem ser acessadas de qualquer lugar no programa (por qualquer função). o Existem durante toda a vida do programa (alocadas na seção de dados do executável).
• int varGlobal = 20; // Variável global
•
• void outraFuncao(void) {
• printf("Dentro de outraFuncao: %d\n", varGlobal); // Acessív el
• }
• int main(void) {
• printf("Dentro de main: %d\n", varGlobal); // Acessível
• outraFuncao();
• return 0; }
Cuidado: O uso excessivo de variáveis globais pode tornar o código difícil de rastrear, depurar e testar, pois qualquer função pode alterá-las. Prefira variáveis locais e passagem de parâmetros sempre que possível.
• Variáveis Estáticas (static):
o A palavra-chave static tem dois significados dependendo do contexto:
1. Variáveis locais estáticas: São declaradas dentro de uma função, mas mantêm seu valor entre as chamadas da função. Elas são inicializadas apenas uma vez, na primeira vez que a função é chamada, e existem durante toda a vida do programa (como variáveis globais), mas seu escopo é restrito à função onde foram declaradas.
2. Variáveis globais estáticas / Funções estáticas: Limitam o escopo da variável ou função ao arquivo fonte em que foram declaradas, evitando conflitos de nome em projetos com múltiplos arquivos. Abordaremos isso em Capítulo 10.
• #include <stdio.h>
•
• void contarChamadas(void) {
• static int contador = 0; // Inicializada uma única vez
• contador++;
• printf("A função foi chamada %d vezes.\n", contador);
• }
• • int main(void) {
• contarChamadas(); // Saída: 1
• contarChamadas(); // Saída: 2
• contarChamadas(); // Saída: 3
• return 0; }
4.6 Recursão
Recursão é uma técnica de programação onde uma função chama a si mesma para resolver um problema. Para que uma função recursiva funcione corretamente, ela deve ter:
• Caso Base: Uma condição de parada que não faz uma chamada recursiva, evitando um loop infinito.
• Passo Recursivo: A função chama a si mesma com um conjunto de parâmetros que se aproximam do caso base.
Um exemplo clássico é o cálculo do fatorial de um número.
#include <stdio.h>
// Declaração do protótipo da função recursiva
long long fatorial(int n);
int main(void) {
int num = 5;
printf("Fatorial de %d é %lld\n", num, fatorial(num)); // Saída: 1 20
num = 10;
printf("Fatorial de %d é %lld\n", num, fatorial(num)); // Saída: 3 628800
return 0;
}
// Definição da função recursiva
long long fatorial(int n) {
// Caso base: fatorial de 0 ou 1 é 1
if (n == 0 || n == 1) {
return 1; } // Passo recursivo: n * fatorial(n-1)
else {
return n * fatorial(n - 1); } }
Cuidado com a Recursão: Embora elegante, a recursão pode ser ineficiente em termos de uso de memória (pilha de chamadas) e tempo de execução se o caso base não for alcançado ou se a profundidade da recursão for muito grande. Para muitos problemas, uma solução iterativa (com laços) é preferível.
Exercício 4.1: Crie uma função para calcular o fatorial de um número.
Você já tem um exemplo acima, mas tente implementá-lo sem olhar. Seu programa deve:
1. Definir uma função chamada long long calcularFatorial(int n)
2. Essa função deve calcular o fatorial de n de forma recursiva.
3. Na função main, peça ao usuário para inserir um número.
4. Chame a função calcularFatorial com o número inserido.
5. Imprima o resultado.
Capítulo 5: Arrays e Strings
Até agora, lidamos principalmente com variáveis que armazenam um único valor. Mas e se precisarmos armazenar uma coleção de valores do mesmo tipo? É aí que entram os arrays e, especificamente, as strings em C.
5.1 O que são Arrays?
Um array é uma coleção de elementos do mesmo tipo de dados, armazenados em posições de memória contíguas. Cada elemento é acessado por um índice.
Em C, os índices de arrays começam em 0
5.1.1 Declarando e Inicializando Arrays Unidimensionais
Sintaxe para declarar um array: tipo nome_array[tamanho];
printf("numeros[%d] = %d\n", i, numeros[i]);
Cuidado: C não realiza verificação de limites de arrays (bounds checking). Acessar um índice fora do tamanho do array (ex: numeros[5] ou numeros[-1]) levará a um comportamento indefinido (segmentation fault ou dados corrompidos).
5.1.2 Arrays Multidimensionais
Arrays multidimensionais são arrays de arrays. O mais comum é o array bidimensional (matriz), que pode ser visualizado como uma tabela com linhas e colunas.
Sintaxe: tipo nome_array[linhas][colunas]; int matriz[3][3]; // Matriz 3x3 de inteiros
Em C, não existe um tipo de dado nativo "string". As strings são tratadas como arrays de caracteres. Uma característica crucial é que toda string em C deve ser terminada com um caractere nulo, '\0'.
5.3 A Importância do Caractere Nulo \0
O caractere nulo ('\0', que tem o valor ASCII 0) marca o fim de uma string. Funções que trabalham com strings (como printf, strlen, etc.) usam \0 para saber onde a string termina. Se uma string não for terminada em nulo, as funções podem continuar lendo na memória até encontrar um \0 aleatório ou causar um erro.
// Tentando armazenar uma string maior que o array (buffer overflo w)
// char pequena[5];
// strcpy(pequena, "Banana"); // CUIDADO: Isso sobrescreveria a me mória!
// Lendo uma string do usuário com fgets (mais seguro que scanf)
char sobrenome[30];
printf("Digite seu sobrenome: "); fgets(sobrenome, sizeof(sobrenome), stdin);
printf("Seu sobrenome é: %s", sobrenome); // fgets inclui a nova l inha, se houver espaço return 0;
Dica de Segurança: Ao ler strings com scanf("%s", array), há o risco de buffer overflow se a entrada do usuário for maior que o array. Use fgets, que permite especificar o tamanho máximo a ser lido, ou scanf("%19s", nome) (para um array de 20, deixando espaço para \0).
5.4 Funções Úteis da Biblioteca
<string.h>
A biblioteca padrão <string.h> fornece várias funções para manipular strings.
#include <stdio.h>
#include <string.h> // Para funções de string
int main(void) {
char str1[50] = "Olá";
char str2[50] = "Mundo";
char str3[50] = "Olá";
char destino[100]; // Array grande o suficiente para cópia/concate nação
// strcpy(destino, str1); - Copia str1 para destino
Funções Seguras: Funções como strcpy e strcat não verificam o tamanho do buffer de destino, o que pode levar a buffer overflows. Para maior segurança, use suas variantes n-safe: strncpy, strncat. Para leitura, use fgets
Exercício 5.1: Crie um programa que lê um nome e o imprime ao contrário.
Seu programa deve:
1. Declarar um array de caracteres para armazenar um nome (com tamanho suficiente, ex: 50).
2. Solicitar ao usuário que digite um nome. Use fgets para ler o nome com segurança.
3. Se a string lida por fgets incluir um caractere de nova linha ('\n') no final, remova-o. Dica: use strlen e substitua '\n' por '\0'
4. Use um laço for (ou outra técnica) para iterar sobre a string do fim para o começo e imprima cada caractere. // Exemplo de como remover o \n de fgets
#include <stdio.h>
#include <string.h>
int main(void) {
char nome[50];
int len; printf("Digite seu nome: "); fgets(nome, sizeof(nome), stdin);
Ponteiros são o recurso mais poderoso, mas também um dos mais desafiadores, da linguagem C. Eles permitem que você trabalhe diretamente com endereços de memória, o que é fundamental para manipulação de memória dinâmica, passagem de parâmetros por referência, e construção de estruturas de dados avançadas. Compreender ponteiros é essencial para dominar C.
6.1 O que é um Endereço de Memória?
A memória do computador é como uma longa sequência de bytes, cada um com um endereço único. Quando você declara uma variável, o sistema operacional aloca um ou mais desses bytes para armazenar o valor da variável e associa um endereço a esse local. Um endereço de memória é um número que identifica a localização de uma variável na RAM.
Em C, podemos obter o endereço de uma variável usando o operador endereço de (&).
intmain(void) {
intidade= 30;
doublesalario= 5500.75 ;
charletra= 'A'; printf("Valor de idade: %d, Endereço de idade: %p \n", idade, &idad e);
printf("Valor de salario: %.2f, Endereço de salario: %p \n", salari o, &salario);
printf("Valor de letra: %c, Endereço de letra: %p \n", letra, &letr a); return0; }
O especificador de formato %p é usado para imprimir endereços de memória, geralmente em formato hexadecimal.
6.2 O que são Ponteiros? Declarando Ponteiros.
Um ponteiro é uma variável cujo valor é um endereço de memória. Em outras palavras, um ponteiro "aponta" para a localização de outra variável na memória. Sintaxe para declarar um ponteiro: tipo *nome_ponteiro; #include <stdio.h>
• tipo: O tipo de dado da variável para a qual o ponteiro apontará (ex: int para um int, char para um char). Isso é crucial para a aritmética de ponteiros.
• *: O asterisco indica que a variável que está sendo declarada é um ponteiro.
• nome_ponteiro: O nome da variável ponteiro.
#include <stdio.h>
intmain(void) {
intnumero= 10;
int*ptr; // Declara um ponteiro para um inteiro
ptr= &numero ; // Atribui o endereço de 'numero' ao ponteiro 'ptr'
printf("Valor de numero: %d \n", numero; )
printf("Endereço de numero: %p\n", &numero);
printf("Valor do ponteiro ptr (endereço que ele armazena): %p \n", ptr); return0;
6.3 Operadores de Ponteiro: & (endereço de) e * (desreferência )
• & (Operador Endereço de): Retorna o endereço de memória de uma variável. Já vimos isso.
• * (Operador Desreferência ou Indireção): Quando usado com um ponteiro, retorna o valor armazenado no endereço para o qual o ponteiro aponta. É como "seguir" o ponteiro até o valor.
int main(void) {
int valor = 42;
int *p = &valor; // p agora armazena o endereço de 'valor'
printf("Valor de 'valor': %d\n", valor);
printf("Endereço de 'valor': %p\n", &valor);
printf("Valor de 'p' (endereço): %p \n", p);
printf("Valor apontado por 'p' (*p): \%dn", *p); // Desreferencia 'p' para obter o valor de 'valor'
*p = 99; // Alterando o valor através do ponteiro
printf("Novo valor de 'valor': %d \n", valor); // 'valor' agora é 9 9
printf("Novo valor apontado por 'p': \%dn", *p); return0;
6.4 Ponteiros e Arrays: Uma Relação Íntima
Em C, o nome de um array (sem colchetes) frequentemente se comporta como um ponteiro para o primeiro elemento do array.
#include <stdio.h>
int main(void) { #include <stdio.h>
int numeros[5] = {10, 20, 30, 40, 50};
int*p;
p = numeros ; // 'p' aponta para o primeiro elemento de 'numeros' ( endereço de numeros[0])
printf("p[2] = %d \n", p[2]); // Notação de array com ponteiro
return0; }
Isso mostra a grande equivalência entre a notação de array ( array[i] ) e a notação de ponteiro ( *(array + )i).
6.5 Aritmética de Ponteiros
Você pode realizar operações aritméticas com ponteiros, mas com uma particularidade crucial: a aritmética de ponteiros é dimensionada pelo tipo de dado para o qual o ponteiro aponta. Por exemplo, se int *p aponta para um inteiro, p++ não adiciona 1 ao endereço de memória, mas sim sizeof(int) bytes, movendo o ponteiro para o próximo inteiro na memória.
#include <stdio.h>
int main(void) {
int numeros[3] = {10, 20, 30};
int *ptr = numeros; // ptr aponta para numeros[0]
printf("Endereço de numeros[0]: %p, Valor: %d\n", ptr, *ptr);
ptr++; // Avança para o próximo inteiro (numeros[1])
printf("Endereço de numeros[1]: %p, Valor: %d\n", ptr, *ptr);
ptr += 1; // Avança para o próximo inteiro (numeros[2])
printf("Endereço de numeros[2]: %p, Valor: %d\n", ptr, *ptr);
return 0; }
6.6 Ponteiros para Ponteiros
Um ponteiro para ponteiro é uma variável que armazena o endereço de outro ponteiro. É declarado com dois asteriscos (**).
#include <stdio.h>
int main(void) { int valor = 123; int *ptr_simples; // Ponteiro para int int **ptr_para_ptr; // Ponteiro para um ponteiro de int
ptr_simples = &valor; // ptr_simples armazena o endereço de 'valor'
ptr_para_ptr = &ptr_simples; // ptr_para_ptr armazena o endereço d e 'ptr_simples'
printf("Valor: %d\n", valor);
printf("Valor de ptr_simples: %p (endereço de valor)\n", ptr_simpl es; )
printf("Valor apontado por ptr_simples: %d \n", *ptr_simples );
printf("Valor de ptr_para_ptr: %p (endereço de ptr_simples) \n", pt r_para_ptr );
printf("Valor apontado por ptr_para_ptr (*ptr_para_ptr): %p (ptr_s imples)\n", *ptr_para_ptr );
printf("Valor apontado por **ptr_para_ptr: %d (valor)\n", **ptr_pa ra_ptr); // Dupla desreferência **ptr_para_ptr = 456; // Altera 'valor' através do ponteiro para p onteiro
printf("Novo valor de 'valor': %d\n", valor);
return0; }
Ponteiros para ponteiros são úteis em situações como arrays de strings ( char
printf("Endereço de valor: %p\n", &valor); **argv em main) ou alocação dinâmica de arrays bidimensionais.
6.7 Ponteiros e Funções: Passagem por Referência
No Capítulo 4, vimos que C passa argumentos para funções por valor. Para que uma função possa modificar o valor de uma variável passada como argumento, você deve passar o endereço da variável (um ponteiro para ela) e, em seguida, desreferenciar o ponteiro dentro da função.
#include <stdio.h>
// Função para trocar o valor de duas variáveis usando ponteiros
voidswap(int*a, int*b) {
inttemp= *a; // Lê o valor apontado por 'a'
*a = *b; // Atribui o valor apontado por 'b' ao endereço apont ado por 'a'
*b = temp; // Atribui 'temp' (valor original de 'a') ao endereç o apontado por 'b'
} intmain(void) {
intx = 10;
inty = 20;
printf("Antes da troca: x = %d, y = %d \n", x, y);
swap& ( x, &y); // Passa os ENDEREÇOS de x e y
printf("Depois da troca: x = %d, y = %d \n", x, y);
return0; }
Esta é a técnica de passagem por referência, fundamental para que as funções
possam modificar dados fora de seu próprio escopo.
6.8 Ponteiro Nulo (NULL)
Um ponteiro nulo é um ponteiro que não aponta para nenhum local de memória válido. Ele é representado pela macro NULL (definida em <stdio.h>, <stdlib.h>, etc.) e tem o valor de 0 (ou (void*)0).
É uma boa prática inicializar ponteiros com NULL se eles não forem atribuídos a um endereço válido imediatamente. Isso ajuda a evitar "ponteiros selvagens" (ponteiros que apontam para locais de memória desconhecidos ou inválidos) e permite verificar se um ponteiro é válido antes de desreferenciá-lo.
#include <stdio.h>
int main(void) {
int *ptr = NULL; // Inicializa o ponteiro como nulo
if (ptr != NULL) {
printf("Valor apontado: %d\n", *ptr); // Isso não será executa do
} else {
printf("O ponteiro é NULL. Não é seguro desreferenciá-lo.\n");
} int valor= 77;
ptr = &valor; // Agora ptr aponta para um endereço válido
if (ptr != NULL) {
printf("O ponteiro aponta para um valor válido: %d \n", *ptr);
} return0;
}
Exercício 6.1: Crie uma função swap que troca o valor de duas variáveis usando ponteiros.
Você já tem o código do exemplo 6.7. Agora, tente implementar essa função sem olhar o exemplo e teste-a na função main com diferentes valores.
1. Declare duas variáveis inteiras na main
2. Imprima seus valores antes da troca.
3. Chame a função swap, passando os endereços das variáveis.
4. Imprima os valores das variáveis novamente para confirmar a troca.
Capítulo 7: Gerenciamento Dinâmico
de Memória
Até agora, as variáveis que criamos tinham um tempo de vida e um tamanho fixos, determinados no momento da compilação. O gerenciamento dinâmico de memória permite que seu programa aloque e libere memória em tempo de execução, conforme necessário. Isso é crucial para lidar com dados de tamanho variável ou desconhecido até o programa ser executado.
7.1 Memória Stack vs. Heap
Para entender o gerenciamento de memória, é importante conhecer as duas principais áreas de memória que um programa C utiliza:
• Stack (Pilha):
o Usada para armazenar variáveis locais de funções, parâmetros de funções e endereços de retorno. o A alocação e desalocação são automáticas e gerenciadas pelo compilador. É rápida.
o Segue uma estrutura LIFO (Last-In, First-Out). o Possui um tamanho limitado; exceder o limite pode causar um "Stack Overflow".
• int soma(int a, int b) {
• int resultado = a + b; // 'a', 'b' e 'resultado' são alocado s na stack
• return resultado; }
• Heap (Montículo):
o Usada para alocação dinâmica de memória.
o A alocação e desalocação são manuais e gerenciadas pelo programador (via funções como malloc, free). É mais lenta que a stack.
o Não segue uma estrutura LIFO rígida. o Possui um tamanho muito maior que a stack, limitado pela memória física do sistema. o A memória
permanece alocada até ser explicitamente liberada ou o programa terminar.
Quando você precisa de memória para um objeto cujo tamanho só é conhecido em tempo de execução, ou que precisa persistir além da vida de uma função, você usa a heap
7.2 Alocação Dinâmica
com malloc, calloc e realloc
Essas funções estão declaradas em <stdlib.h>. 7.2.1
malloc() (Memory Allocation)
void *malloc(size_t size);
Aloca um bloco de size bytes na heap. Retorna um ponteiro do tipo void * (um ponteiro genérico) para o início do bloco alocado. Se a alocação falhar, retorna NULL
Você deve fazer um cast para o tipo de ponteiro desejado.
Aloca um bloco de memória para num elementos, cada um com size bytes. A principal diferença para malloc é que calloc inicializa todos os bytes do bloco alocado com zero
// Exemplo de calloc
int*ptr_calloc ;
intn = 3;
ptr_calloc= (int*) calloc(n, sizeof(int));
// ... verificar NULL e usar ...
// Todos os 3 inteiros em ptr_calloc são inicializados com 0.
free(ptr_calloc);
7.2.3 realloc() (Re-allocation)
void *realloc(void *ptr, size_t new_size);
Altera o tamanho do bloco de memória apontado por ptr para new_size bytes. Ele pode mover o bloco para um novo local se não houver espaço suficiente no local atual. Retorna um ponteiro para o novo bloco de memória (que pode ser o mesmo que o original). Se a alocação falhar, retorna NULL (e o bloco original não é liberado).
// Exemplo de realloc
int *arr = NULL;
int count = 0;
int capacity = 2;
// Aloca espaço inicial para 2 inteiros
arr = (int *) malloc(capacity * sizeof(int));
if (arr == NULL) return 1;
arr[0] = 10;
arr[1] = 20;
count = 2; // Precisa de mais espaço, realloc para 4 inteiros
int *temp = (int *) realloc(arr, (capacity + 2) * sizeof(int)); if (temp == NULL) {
printf("Erro de realloc!\n"); free(arr); // Libera o bloco original se realloc falhar
return 1;
}
arr = temp;
capacity += 2;
arr[2] = 30;
arr[3] = 40; count = 4;
printf("Array após realloc: "); for (int i = 0; i < count; i++) {
printf("%d ", arr[i]);
}
printf("\n"); free(arr);
7.3 Liberando Memória com free
void free(void *ptr);
free libera o bloco de memória apontado por ptr, que deve ter sido alocado por malloc, calloc ou realloc. É crucial liberar a memória que não é mais necessária para evitar vazamentos de memória.
Regra de Ouro: Para cada malloc, calloc ou realloc bem-sucedido, deve haver uma chamada correspondente a free
7.4 Perigos Comuns: Vazamentos de Memória e Ponteiros Pendentes
7.4.1 Vazamentos de Memória (Memory Leaks)
Ocorrem quando a memória é alocada na heap, mas nunca é liberada com free. O programa perde a capacidade de acessar essa memória, mas o sistema operacional ainda a considera em uso pelo programa. Com o tempo, vazamentos de memória podem consumir toda a memória disponível, levando a falhas no sistema ou degradação de desempenho.
// Exemplo de Memory Leak
void funcaoComVazamento(void) {
int *dados = (int *) malloc(sizeof(int));
if (dados == NULL) return;
*dados = 10;
// free(dados); // Esqueceu de liberar! -> Memory leak
} int main(void) {
for (int i = 0; i < 1000; i++) { funcaoComVazamento(); // Cada chamada vaza memória }
printf("Programa com vazamento de memória executado. Observe o con sumo de RAM.\n");
return0; }
7.4.2Ponteiros Pendentes (Dangling Pointers )
Um ponteiro pendente ocorre quando um ponteiro ainda aponta para um bloco de memória que já foi liberado. Desreferenciar um ponteiro pendente leva a um comportamento indefinido (acesso a memória inválida), o que pode causar falhas (segmentation faults) ou bugs difíceis de rastrear.
// Exemplo de Ponteiro Pendente
int main(void) {
int *ptr = (int *) malloc(sizeof(int));
if (ptr == NULL) return 1;
*ptr = 100;
printf("Antes de free: %d\n", *ptr); free(ptr);
// ptr agora é um ponteiro pendente, pois a memória para onde ele apontava foi liberada.
// Acesso a memória inválida - comportamento indefinido!
printf("Depois de free (potencialmente perigoso): %d\n", *ptr);
return 0;
}
Sempre defina um ponteiro para NULL após liberá-lo com free para mitigar o problema de ponteiros pendentes.
Exercício 7.1: Crie um programa que pede ao usuário o tamanho de um array, aloca-o dinamicamente, preenche-o e depois o libera.
Seu programa deve:
1. Solicitar ao usuário o número de elementos para o array.
2. Alocar dinamicamente um array de inteiros desse tamanho usando malloc
3. Verificar se a alocação foi bem-sucedida. Se não, imprimir uma mensagem de erro e sair.
4. Preencher o array com valores, por exemplo, de 1 a num_elementos.
5. Imprimir todos os elementos do array.
6. Liberar a memória alocada usando free e definir o ponteiro para NULL.
Capítulo 8: Estruturas, Uniões e Enumerações
Até agora, lidamos com tipos de dados primitivos (int, char, etc.) e coleções deles (arrays). Mas e se precisarmos agrupar dados de diferentes tipos em uma única unidade lógica? C fornece ferramentas para criar seus próprios tipos de dados personalizados: estruturas (structs), uniões (unions) e enumerações (enums).
8.1 Criando Tipos de Dados Personalizados com struct
Uma estrutura (struct) é uma coleção de variáveis de diferentes tipos de dados sob um único nome. Ela permite agrupar dados relacionados logicamente para tratá-los como uma única unidade. Por exemplo, você pode criar uma struct para representar um "Ponto" com coordenadas X e Y, ou um "Livro" com título, autor e ISBN.
Sintaxe para declarar uma struct:
struct NomeDaEstrutura {
tipo1 membro1;
tipo2 membro2;
// ... };
// Imprimindo os membros da struct
printf("Nome: %s \n", p1.nome);
printf("Idade: \%dn", p1.idade);
Exemplo destructPessoa :
printf("Altura: %.2f\n", p1.altura);
#include <stdio.h>
#include <string.h> // Para strcpy
// Inicialização na declaração com lista de membros (C99 em diante )
A expressão ptr_livro->titulo é açúcar sintático para (*ptr_livro).titulo .
8.3 Arrays de Structs
Podemos criar arrays de structs, assim como arrays de tipos primitivos. Isso é útil para gerenciar coleções de registros, como uma lista de alunos ou produtos.
#include <stdio.h>
#include <string.h>
struct Produto { char nome[30]; float preco;
int quantidade;
// Declarando e inicializando um array de structs Produto
Uma união (union) é semelhante a uma struct, mas com uma diferença crucial: todos os seus membros compartilham o mesmo espaço de memória. O tamanho de uma union é o tamanho do seu maior membro.
Você deve usar union quando precisar armazenar diferentes tipos de dados em um mesmo local de memória, mas apenas um deles por vez. Isso é útil para economizar memória quando você sabe que apenas um tipo de dado será ativo em um determinado momento.
#include <stdio.h> #include
<string.h> union
ValorGenerico {
float f; char s[20];
};
int main(void) {
union ValorGenerico val;
val.i = 10;
printf("Inteiro: %d\n", val.i);
val.f = 22.5f; // Isso sobrescreve o valor inteiro
printf("Float: %.2f\n", val.f);
printf("Inteiro (depois do float): %d (valor corrompido ou inesper ado)\n", val.i);
strcpy(val.s, "Hello Union!"); // Isso sobrescreve o float
printf("String: %s\n", val.s);
printf("Tamanho da união: %zu bytes (tamanho de seu maior membro, a string s)\n", sizeof(union ValorGenerico));
return 0;
}
Uso com cautela: O acesso a um membro de uma união que não foi o último a ser atribuído resultará em comportamento indefinido. União é uma ferramenta de baixo nível e deve ser usada com cuidado, geralmente em conjunto com um "discriminador" (um membro de uma struct que indica qual tipo de dado está ativo na união).
8.5 Tipos Enumerados com enum
Uma enumeração (enum) é um tipo de dado que permite definir um conjunto de constantes inteiras nomeadas. Isso melhora a legibilidade do código, substituindo números "mágicos" por nomes significativos.
Por padrão, o primeiro enumerador tem o valor 0, o segundo 1, e assim por diante.
Você pode atribuir valores explícitos.
#include <stdio.h>
// Declaração da enumeração para os dias da semana
enum DiaSemana {
DOMINGO, // 0
SEGUNDA, // 1
TERCA, // 2
QUARTA, // 3
QUINTA, // 4
SEXTA = 5, // Valor explícito
SABADO, // 6 (continua a partir do valor anterior)
FERIADO = 10 // Outro valor explícito
};
int main(void) {
enum DiaSemana hoje = QUARTA;
enum DiaSemana amanha = SEXTA;
}
printf("Hoje é o dia número: \%dn", hoje); // Saída: 3
printf("Amanhã será o dia número: \%dn", amanha); // Saída: 5
if (hoje == QUARTA) {
printf("Estamos na quarta-feira.\n");
}
printf("Valor de SABADO: %d \n", SABADO); // Saída: 6
printf("Valor de FERIADO: %d \n", FERIADO); // Saída: 10 return0;
8.6 Usando typedefpara Criar Apelidos para Tipos
A palavra-chave typedef permite criar um apelido (alias) para um tipo de dado existente. Isso é particularmente útil para simplificar a sintaxe ao usar structs e unions, tornando o código mais limpo e legível.
#include <stdio.h> #include <string.h>
// Antes do typedef: struct Coordenada { int x; int y;
// struct Coordenada p1; // Declaração normal
// Usando typedef para a struct
typedef struct Coordenada2D { int x; int y;
} Ponto; // Ponto é agora um apelido para struct Coordenada2D
// Usando typedef para uma união
typedef union Data { int i; float f;
} DataUnion;
// Usando typedef para um enum typedef enum Status { SUCESSO, FALHA
} OpStatus; int main(void) { Ponto p1 = {10, 20};
printf("Ponto p1: (%d, %d)\n", p1.x, p1.y);
DataUnion d1 ;
d1.i = 100;
printf("Data Union i: \%dn", d1.i);
OpStatus s= SUCESSO ;
printf("Status da operação:\%dn", s);
return0;
}
É uma prática comum em C usar typedefpara estruturas e enumerações para simplificar a sintaxe.
Exercício 8.1: Crie uma struct Aluno para armazenar nome, matrícula e notas, e crie um programa para gerenciar uma lista de alunos.
Seu programa deve:
1. Definir uma struct Aluno com os seguintes membros: ochar nome[50];oint matricula;ofloat notas[3]; (para 3 notas) ofloat media;
2. Use typedef para criar um apelido Aluno para esta estrutura.
3. Na função main, declare um array de Alunos (ex: para 2 alunos).
4. Preencha os dados para cada aluno (nome, matrícula, 3 notas). Você pode usar fgets para o nome e scanf para os números.
5. Para cada aluno, calcule a média das notas e armazene-a no campo media.
6. Imprima os dados de cada aluno, incluindo a média.
Exemplo de leitura de nome com fgets e remoção de '\n': printf("Digite o nome do aluno: "); fgets(alunos[i] nome, sizeof(alunos[i] nome), stdin);
// Remover o '\n' se ele foi lido size_t len = strlen(alunos[i].nome); if (len > 0 && alunos[i].nome[len - 1] == '\n') { alunos[i].nome[len - 1] = '\0';
}
Capítulo 9: Entrada e Saída de Arquivos (File I/O)
Até agora, nossos programas interagem principalmente com o console (entrada padrão e saída padrão). Para criar aplicações mais persistentes e úteis, precisamos ler e escrever dados em arquivos no disco. Este capítulo aborda as funções de Entrada/Saída (I/O) de arquivos em C.
9.1 O que são Streams?
Em C, a entrada e saída (I/O) são tratadas através do conceito de streams (fluxos). Um stream é uma sequência abstrata de bytes que fluem de uma fonte para um destino. Pode ser um arquivo no disco, um dispositivo de rede, a tela do console ou o teclado.
• Streams de texto: Contêm caracteres que são interpretados de acordo com o ambiente local (por exemplo, tradução de caracteres de nova linha).
• Streams binários: Contêm bytes brutos, sem qualquer interpretação ou tradução.
A biblioteca padrão <stdio.h> define três streams padrão, que já usamos:
• stdin: Entrada padrão (geralmente o teclado).
• stdout: Saída padrão (geralmente a tela do console).
• stderr: Saída de erro padrão (geralmente a tela do console, mas separada para redirecionamento de erros).
9.2 Trabalhando com Arquivos: FILE *
Para interagir com um arquivo, você precisa declará-lo como um ponteiro para um tipo FILE (definido em <stdio.h>). Este ponteiro atua como um "handle" ou "cabo" para o arquivo, permitindo que as funções de I/O de arquivo saibam com qual arquivo trabalhar.
FILE *fp; // Declara um ponteiro para um objeto FILE
A função fopen abre um arquivo especificado por filename no modo especificado por mode. Se o arquivo for aberto com sucesso, ela retorna um ponteiro FILE *; caso contrário, retorna NULL (indicando um erro).
Modos de abertura de arquivo comuns:
• "r": Abre um arquivo para leitura. O arquivo deve existir.
• "w": Abre um arquivo para escrita. Se o arquivo existir, seu conteúdo é truncado (apagado); se não existir, um novo arquivo é criado.
• "a": Abre um arquivo para adição (append). Os dados são escritos no final do arquivo. Se o arquivo não existir, um novo é criado.
• "r+": Abre um arquivo para leitura e escrita. O arquivo deve existir.
• "w+": Abre um arquivo para leitura e escrita. Se o arquivo existir, seu conteúdo é truncado; se não existir, um novo é criado.
• "a+": Abre um arquivo para leitura e adição.
Você pode adicionar b ao modo (ex: "rb", "wb") para abrir o arquivo em modo binário, que é importante para lidar com dados que não são texto.
9.3.2 fclose() (File Close) int fclose(FILE *stream);
A função fclose fecha o arquivo associado ao ponteiro stream. Isso libera os recursos do sistema e garante que todos os dados em buffer sejam gravados no disco. Retorna 0 em caso de sucesso, EOF em caso de erro.
#include <stdio.h>
#include <stdlib.h> // Para exit()
int main(void) { FILE *fp;
char data[50] = "Hello from C File I/O!"; // Abrir arquivo para escrita (cria ou sobrescreve)
fp = fopen("exemplo.txt", "w");
if (fp == NULL) {
perror("Erro ao abrir arquivo para escrita"); // Imprime mensa gem de erro exit(1); }
fprintf(fp, "%s\n", data); // Escreve no arquivo fclose(fp);
printf("Dados escritos em exemplo.txt\n");
// Abrir arquivo para leitura
fp = fopen("exemplo.txt", "r");
if (fp == NULL) {
perror("Erro ao abrir arquivo para leitura"); exit(1); } char buffer[100];
if (fgets(buffer, sizeof(buffer), fp) != NULL) { printf("Dados lidos do arquivo: %s", buffer);
fclose(fp); return 0;
perror(): A função perror (de <stdio.h>) imprime uma mensagem de erro na saída de erro padrão, baseada no valor da variável global errno, que é definida quando ocorre um erro de sistema.
9.4 Lendo e Escrevendo Caracteres (fgetc, fputc) e Strings