Aula 9 - Ponteiros e Passagem por Referência
Na Aula 8, vimos que argumentos são sempre copiados para os parâmetros de uma função — inclusive quando o argumento é o endereço de uma variável. Nesta aula, vamos entender exatamente o que é esse "endereço", como manipulá-lo através de ponteiros, e como isso nos permite fazer uma função alterar de verdade uma variável de quem a chamou.
1. Motivação: por que precisamos de ponteiros?
Vimos, na Aula 8, o seguinte problema: a função trocar(int x, int y) não conseguia trocar os valores de duas variáveis da main(), porque x e y recebiam apenas cópias dos valores de a e b. Alterar as cópias não tem nenhum efeito sobre as variáveis originais.
Precisamos, então, de uma forma de dizer a uma função: "não me dê uma cópia do valor — me diga onde, na memória, esse valor está guardado, para que eu possa ir lá e alterá-lo diretamente." É exatamente isso que um ponteiro permite fazer: um ponteiro é uma variável que armazena, como seu valor, o endereço de memória de uma outra variável.
| Por que isso importa? Ponteiros são a base de praticamente tudo o que veremos daqui para frente: passar vetores e structs grandes para funções sem copiá-los inteiros, alterar variáveis de dentro de uma função, percorrer vetores de forma alternativa, e futuramente alocar memória dinamicamente e construir estruturas de dados como listas encadeadas. |
2. Revisão: variáveis na memória
Lembre-se, desde a Aula 1: toda variável, ao ser declarada, ocupa um espaço na memória principal do computador (a memória RAM). É lá que ficam guardadas as variáveis de um programa em execução, enquanto o processador (CPU) é quem executa as instruções do programa — lendo e escrevendo valores na RAM conforme o programa manda. Cada posição da RAM tem um endereço, guarda um valor, e esse valor é interpretado de acordo com o tipo da variável.
Endereços de memória normalmente são representados em hexadecimal (com o prefixo 0x). Para não misturar essa conversão de base com os conceitos de ponteiros que veremos agora, continuamos usando aqui a notação decimal simplificada da Aula 1: a letra E seguida de um número, como E1000 ("endereço 1000").
Podemos imaginar a memória como uma grande tabela, em que cada linha é uma "caixinha" com um identificador (o nome da variável), um endereço, um tipo, e o valor guardado ali:
| Endereço | Identificador (ID) | Tipo | Valor |
|---|---|---|---|
E1000 |
num |
int |
42 |
Até aqui, nada novo. A novidade é: o endereço de uma variável também é um valor, e esse valor pode, ele mesmo, ser guardado em uma outra variável — um ponteiro.
3. Três formas de acessar uma variável
Dado o identificador (ID) de uma variável, existem três formas de acessá-la, cada uma "lendo" um campo diferente da tabela de memória:
| Forma | O que acessa | Vale para |
|---|---|---|
ID |
O campo Valor da variável — o conteúdo da "caixinha". | Qualquer variável. |
&ID |
O campo Endereço da variável — onde a "caixinha" está localizada na memória. | Qualquer variável. |
*ID |
O "valor do valor": usa o Valor de ID como se fosse um Endereço, e busca o que está guardado naquele endereço. |
Apenas ponteiros — só faz sentido se o valor guardado em ID for, ele próprio, um endereço válido. |
Vamos ver essas três formas em ação com um exemplo completo, acompanhando a tabela de memória a cada linha do programa.
#include <stdio.h> int main() { int num = 42; // (1) declara "num" int *p = # // (2) declara "p" e guarda nele o ENDEREÇO de num printf("num = %d\n", num); // campo Valor de num -> 42 printf("&num = %p\n", &num); // campo Endereço de num -> E1000 (exemplo) printf("p = %p\n", p); // campo Valor de p -> E1000 (mesmo endereço!) printf("&p = %p\n", &p); // campo Endereço de p -> E2000 (exemplo) printf("*p = %d\n", *p); // "valor do valor de p" -> vai até E1000 e lê 42 return 0; }
Como imprimir um endereço de verdade. Para imprimir um ponteiro (ou o endereço de qualquer variável, com &), usamos o especificador %p no printf, como nos exemplos acima (printf("%p", &num);, printf("%p", p);). Diferente da notação E1000 que usamos aqui apenas para fins didáticos, o %p imprime o endereço real da variável em memória — e ele sempre imprime esse endereço em hexadecimal (com prefixo 0x), como 0x7ffe1234abcd. Se você rodar esses exemplos em um computador de verdade, é isso que verá na tela, não os endereços E... deste material.
|
Depois da linha int *p = #, a tabela de memória (com endereços fictícios, apenas para fins didáticos) fica assim:
| Endereço | ID | Tipo | Valor |
|---|---|---|---|
E1000 |
num |
int |
42 |
| ... | ... | ... | ... |
E2000 |
p |
int * |
E1000 |
As reticências entre as duas linhas representam o fato de que, entre os endereços E1000 e E2000, existe muito mais coisa na memória — outras variáveis, dados do próprio programa, ou espaço não utilizado. Os endereços aqui são apenas exemplos didáticos; na prática, duas variáveis quaisquer podem estar bem mais próximas ou bem mais distantes uma da outra do que sugerem estes números.
Repare que p é uma variável como qualquer outra — também tem seu próprio endereço (E2000) — mas o valor guardado dentro dela não é um número "comum", e sim o endereço de uma outra variável (E1000, o endereço de num). Por isso p é chamado de ponteiro: ele "aponta" para onde num está.
num→ 42 (campo Valor da linha denum)&num→E1000(campo Endereço da linha denum)p→E1000(campo Valor da linha dep— que por acaso é um endereço)&p→E2000(campo Endereço da linha dep)*p→ 42 ("valor do valor": pega o valor dep[E1000], vai até a linha cujo endereço éE1000, e lê o Valor de lá — que é o denum)
4. Declarando e usando ponteiros
A declaração de um ponteiro utiliza o operador * junto ao tipo:
Sintaxe genérica:
tipo *nome_do_ponteiro;
Exemplos:
int *p; // ponteiro para int float *pf; // ponteiro para float char *pc; // ponteiro para char
O tipo do ponteiro deve corresponder ao tipo da variável para a qual ele apontará — um int * só deveria apontar para um int, e assim por diante. Isso importa porque o tipo é o que diz ao compilador quantos bytes ler ao desreferenciar o ponteiro (*p), e quanto "andar" na memória em operações de aritmética de ponteiros (Seção 6).
O tamanho do ponteiro não depende do tipo apontado. Todo ponteiro guarda apenas um endereço — e, em um sistema de 64 bits, um endereço sempre ocupa o mesmo tamanho, não importa para que tipo o ponteiro aponte:
sizeof(char *) // 8 (em um sistema de 64 bits) sizeof(int *) // 8 sizeof(float *) // 8 sizeof(double *) // 8 sizeof(FILE *) // 8 (Aula 11)Todos ocupam o mesmo tamanho porque todos armazenam apenas um endereço — nenhum deles guarda, ele mesmo, o dado apontado. O que muda, de tipo para tipo, é o tamanho do objeto apontado, e não o do ponteiro: sizeof(char) // 1 sizeof(int) // geralmente 4 sizeof(double) // geralmente 8É exatamente essa diferença que sustenta a aritmética de ponteiros (Seção 6):
int *p;
p + 1
O ponteiro p continua ocupando 8 bytes (em um sistema de 64 bits) — isso nunca muda. Mas, ao calcular p + 1, o compilador não soma 1 byte ao endereço: ele usa o tipo apontado (int) para saber que deve avançar sizeof(int) bytes (normalmente 4) de uma vez, "pulando" para o próximo int, e não para o próximo byte.
|
Atenção: o * tem dois papéis diferentes. É comum confundir os dois usos do asterisco:
*x (operador unário de desreferenciação, com um único operando) com x * y (operador binário de multiplicação, com dois operandos) — é o mesmo símbolo *, mas com significados completamente diferentes dependendo de quantos operandos ele recebe.
|
Uma das aplicações mais importantes de ponteiros é modificar o valor de uma variável através do ponteiro que aponta para ela, usando *p do lado esquerdo de uma atribuição:
#include <stdio.h> int main() { int num = 10; int *p = # printf("Antes: %d\n", num); // 10 *p = 20; // vai até o endereço guardado em p (E1000) e escreve 20 lá printf("Depois: %d\n", num); // 20 (num mudou, mesmo sem mexer em "num" diretamente!) return 0; }
Ponteiros não inicializados e NULL. Um ponteiro recém-declarado, sem inicialização, guarda "lixo de memória" — um endereço qualquer, inválido. Desreferenciar esse ponteiro (*p) é um erro grave (pode travar o programa ou corromper memória). Se um ponteiro ainda não deve apontar para nada, inicialize-o com NULL, e sempre verifique if (p != NULL) antes de desreferenciá-lo.
|
5. Passagem por referência: ponteiros em funções
Agora podemos resolver, de verdade, o problema da Aula 8: fazer uma função alterar uma variável de quem a chamou. A ideia é simples: em vez de passar a variável como argumento (o que copia seu valor), passamos o endereço dela (que também é copiado — lembre-se, tudo é copiado — mas agora a cópia é do endereço, não do valor original).
#include <stdio.h> // Protótipo void incrementar(int *n); int main() { int num = 10; printf("Antes: %d\n", num); // 10 incrementar(&num); // passamos o ENDEREÇO de num, não o valor printf("Depois: %d\n", num); // 11 (agora sim, num mudou de verdade!) return 0; } // Implementação void incrementar(int *n) { *n = *n + 1; // desreferencia n: altera o valor de "num", lá na main() }
Compare com a versão da Aula 8, que não conseguia alterar a variável original:
| Passagem por valor (Aula 8) | Passagem por referência (com ponteiro) |
|---|---|
void incrementar(int n) |
void incrementar(int *n) |
incrementar(num); |
incrementar(&num); |
n recebe uma cópia do valor de num. Alterar n não afeta num. |
n recebe uma cópia do endereço de num. Alterar *n altera num de verdade. |
| Atenção: continua sendo "passagem por valor". Como vimos na Aula 8, tudo em C é passado por valor — inclusive endereços. A diferença é o que é copiado: um valor "comum" (que não permite alterar o original) ou um endereço (que permite, indiretamente, alterar o original através dele). Por isso dizemos que C simula uma passagem por referência usando ponteiros — tecnicamente, ainda é uma cópia (do endereço) sendo passada. |
6. Aritmética de ponteiros
Ponteiros suportam operações aritméticas (soma e subtração) que permitem "navegar" pela memória, elemento a elemento — isso é especialmente útil para percorrer vetores.
#include <stdio.h> int main() { int vetor[5] = {10, 20, 30, 40, 50}; int *p = vetor; // p aponta para o primeiro elemento (vetor[0]) printf("%d\n", *p); // 10 (vetor[0]) p++; // avança p para o PRÓXIMO elemento printf("%d\n", *p); // 20 (vetor[1]) p = p + 2; // avança mais 2 elementos printf("%d\n", *p); // 40 (vetor[3]) return 0; }
O ponto-chave: quando fazemos p++, o ponteiro não avança simplesmente 1 byte. Ele avança o tamanho do tipo apontado. Como p é um int *, e um int normalmente ocupa 4 bytes, p++ avança 4 bytes de uma vez — o suficiente para "pular" para o próximo int do vetor:
| Endereço | ID / posição | Tipo | Valor |
|---|---|---|---|
E1000 |
vetor[0] |
int |
10 |
E1004 |
vetor[1] |
int |
20 |
E1008 |
vetor[2] |
int |
30 |
E1012 |
vetor[3] |
int |
40 |
Ou seja: p começa valendo E1000; após p++, passa a valer E1004 (não E1001!); após p = p + 2 (a partir de E1004), passa a valer E1012. É por isso que o tipo do ponteiro importa tanto: ele diz ao compilador de quantos em quantos bytes "pular".
Cuidado: isso só é seguro dentro de um mesmo vetor. A aritmética de ponteiros "andar um elemento à frente" só tem garantia de funcionar quando as posições pertencem ao mesmo vetor (como vetor[0], vetor[1], vetor[2], que o compilador garante serem contíguas). Duas variáveis comuns, declaradas uma após a outra, não têm essa garantia — o compilador pode alocá-las em qualquer ordem, com qualquer espaçamento entre elas (ou até reordená-las, otimizar uma delas para ficar só em um registrador, etc.):
#include <stdio.h> int main() { int a = 10; int b = 20; printf("%d\n", *(&a + 1)); return 0; }Embora a e b tenham sido declaradas em sequência, não há garantia de que b esteja logo depois de a na memória — o resultado de *(&a + 1) não necessariamente será 20. Esse código tem comportamento indefinido (pode até "parecer funcionar" às vezes, por coincidência, mas não é algo em que se possa confiar). Só use aritmética de ponteiros para navegar dentro de um vetor de fato.
|
7. Vetores e strings em funções
Em C, o nome de um vetor, usado sozinho, já é um ponteiro para seu primeiro elemento. Explicitamente:
v == &v[0]
Ou seja, dado um vetor v: o próprio nome do vetor usado como valor (v) e o endereço explícito do seu primeiro elemento (&v[0]) apontam para o mesmo endereço. É por isso que podemos atribuir int *p = vetor; diretamente, sem precisar escrever &vetor[0].
E o &v (endereço do vetor inteiro)? Tecnicamente, v (um vetor) não é a mesma coisa que um ponteiro: o tipo de v é "vetor de N inteiros" (int[N]), não int *. Na grande maioria das expressões, porém, o compilador converte automaticamente um vetor para um ponteiro ao seu primeiro elemento — é por isso que v "funciona como" int * quase sempre.
Já &v é diferente: seu tipo é "ponteiro para vetor de N inteiros" (int (*)[N]), e não int *. Ele aponta para o mesmo endereço na memória que v e &v[0], mas com um tipo diferente — por isso, para comparar ou atribuir &v a um int *, seria necessário um cast explícito, como em (int *)&v. Considere, por exemplo:
int v[10]; v // tipo int * (convertido automaticamente) &v[0] // tipo int * &v // tipo int (*)[10] (ponteiro para o vetor inteiro)Os três apontam para o mesmo local de memória — mas apenas os dois primeiros têm o tipo int * diretamente. Este é um detalhe mais avançado; para o dia a dia da disciplina, basta lembrar que v == &v[0].
|
Da mesma forma, para qualquer posição i dentro do vetor, acessar por colchetes ou por aritmética de ponteiros dá exatamente no mesmo resultado:
v[i] == *(v + i)
#include <stdio.h> int main() { int vetor[5] = {10, 20, 30, 40, 50}; int *p = vetor; printf("%d\n", *p); // 10 -> acessa vetor[0] printf("%d\n", *(p + 1)); // 20 -> acessa vetor[1] printf("%d\n", p[1]); // 20 -> equivalente: p[1] == *(p + 1) return 0; }
Isso explica por que, quando passamos um vetor para uma função, na prática estamos sempre passando um ponteiro para seu primeiro elemento — nunca uma cópia do vetor inteiro. Por isso uma função pode alterar diretamente o conteúdo de um vetor recebido como parâmetro, mesmo sem usarmos & explicitamente na chamada:
#include <stdio.h> // Protótipo — "int v[]" e "int *v" são equivalentes aqui void dobrarTodos(int v[], int tamanho); int main() { int numeros[4] = {1, 2, 3, 4}; int i; dobrarTodos(numeros, 4); // não precisa de &: o próprio "numeros" já é um endereço for (i = 0; i < 4; i++) { printf("%d ", numeros[i]); // 2 4 6 8 (o vetor original foi alterado!) } printf("\n"); return 0; } // Implementação void dobrarTodos(int v[], int tamanho) { int i; for (i = 0; i < tamanho; i++) { v[i] = v[i] * 2; // altera direto o vetor de quem chamou } }
O mesmo raciocínio vale para strings: como vimos na Aula 5, uma string em C é apenas um vetor de char terminado em '\0'. Ao passar uma string para uma função, também estamos passando um ponteiro para seu primeiro caractere — por isso funções como strcpy, strcat e as suas próprias funções que recebem char texto[] conseguem alterar a string original.
Boas práticas com ponteiros.
|
Resumo
|
Exercícios
Nível fácil
-
Escreva um programa que declare uma variável
int idade = 25;e um ponteiroint *pque aponte para ela. Imprima, usandoprintf, o valor deidade, o endereço deidade, o valor depe o valor de*p, comentando o que cada saída representa. -
Escreva uma função
void dobrar(int *n)que dobre, por referência, o valor apontado porn. Chame-a a partir damain(), passando o endereço de uma variável, e imprima o valor antes e depois da chamada. -
Escreva um programa que declare um vetor
int notas[4] = {7, 8, 5, 9};e um ponteiroint *p = notas;. Usando apenas o ponteirop(com*p,*(p+1), etc., sem usar colchetes), imprima os quatro valores do vetor.
Nível médio
-
Escreva uma função
void trocar(int *x, int *y)que troque, de verdade, os valores de duas variáveis, usando ponteiros. Chame-a a partir damain()com duas variáveisaeb, e mostre que a troca funciona (diferente do que ocorria na Aula 8). -
Escreva uma função
void somarUm(int v[], int tamanho)que some 1 a cada elemento de um vetor de inteiros. Chame-a a partir de umamain()que declare um vetor, imprima-o antes e depois da chamada, e confirme que o vetor original foi alterado. -
Explique, sem executar o código, qual será a saída do programa abaixo, e por quê:
#include <stdio.h> int main() { int valores[3] = {100, 200, 300}; int *p = valores; p = p + 2; *p = 999; printf("%d %d %d\n", valores[0], valores[1], valores[2]); return 0; }
Nível difícil
-
Escreva uma função
int somaVetor(int *v, int tamanho)que calcule a soma dos elementos de um vetor, acessando os elementos apenas por aritmética de ponteiros (usando*(v + i), sem usar a notaçãov[i]). Utilize-a em um programa completo que leia um vetor do usuário e imprima a soma. -
O programa abaixo tenta usar um ponteiro sem inicializá-lo corretamente. Identifique o erro, explique por que ele é perigoso (mesmo que "pareça funcionar" ao rodar), e reescreva o programa corrigindo o problema:
#include <stdio.h> int main() { int *p; *p = 10; printf("%d\n", *p); return 0; }
-
Explique, sem executar o código, qual será a saída do programa abaixo (assuma
intcom 4 bytes e arquitetura little-endian, em que o byte menos significativo fica no primeiro endereço). O programa usa um ponteirochar *para "enxergar" um vetor de inteiros byte a byte, e altera diretamente o primeiro byte do primeiro elemento:#include <stdio.h> int main() { int valores[2] = {256, 500}; char *bp = (char *) valores; // bp "enxerga" a mesma memória, byte a byte bp[0] = 1; // altera só o primeiro BYTE do primeiro int printf("%d %d\n", valores[0], valores[1]); return 0; }
Explique também por que, se em vez dechar *bpusássemosint *bp, o resultado da mesma operação (bp[0] = 1;) seria bem diferente. -
O programa abaixo usa
gets()(função insegura, sem verificação de tamanho) para ler um nome em um vetor pequeno demais. Explique o que acontece com as variáveisidadeesaldose o usuário digitar um nome mais longo do que o vetornomecomporta, mesmo o programa nunca atribuindo diretamente a essas variáveis depois da leitura. Em seguida, reescreva o programa usandofgetsde forma seaura (como visto na Aula 5):#include <stdio.h> int main() { char nome[8]; int idade = 20; float saldo = 100.0; printf("Digite seu nome: "); gets(nome); // PERIGO: não verifica o tamanho do que foi digitado! printf("Nome: %s, idade: %d, saldo: %.2f\n", nome, idade, saldo); return 0; }
Sugestões de Respostas dos Exercícios
Exercício 1
#include <stdio.h> int main() { int idade = 25; int *p = &idade; printf("idade = %d\n", idade); // valor de idade printf("&idade = %p\n", &idade); // endereço de idade printf("p = %p\n", p); // valor de p (que é o endereço de idade) printf("*p = %d\n", *p); // "valor do valor de p": vai até o endereço e lê 25 return 0; }
Exercício 2
#include <stdio.h> void dobrar(int *n); int main() { int valor = 5; printf("Antes: %d\n", valor); dobrar(&valor); printf("Depois: %d\n", valor); return 0; } void dobrar(int *n) { *n = *n * 2; }
Exercício 3
#include <stdio.h> int main() { int notas[4] = {7, 8, 5, 9}; int *p = notas; printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3)); return 0; }
Exercício 4
#include <stdio.h> void trocar(int *x, int *y); int main() { int a = 3, b = 8; printf("Antes: a = %d, b = %d\n", a, b); trocar(&a, &b); printf("Depois: a = %d, b = %d\n", a, b); return 0; } void trocar(int *x, int *y) { int temp = *x; *x = *y; *y = temp; }
Agora a troca funciona: x e y continuam sendo cópias — mas cópias de endereços. Ao desreferenciá-los (*x, *y), a função acessa e altera diretamente as posições de memória de a e b, na main().
Exercício 5
#include <stdio.h> void somarUm(int v[], int tamanho); int main() { int numeros[4] = {10, 20, 30, 40}; int i; printf("Antes: "); for (i = 0; i < 4; i++) { printf("%d ", numeros[i]); } printf("\n"); somarUm(numeros, 4); printf("Depois: "); for (i = 0; i < 4; i++) { printf("%d ", numeros[i]); } printf("\n"); return 0; } void somarUm(int v[], int tamanho) { int i; for (i = 0; i < tamanho; i++) { v[i] = v[i] + 1; } }
Exercício 6
A saída será 100 200 999. O vetor valores ocupa três posições consecutivas na memória (por exemplo, E1000, E1004 e E1008, considerando int de 4 bytes). p começa apontando para valores[0] (E1000). A instrução p = p + 2 avança 2 elementos (não 2 bytes!), levando p a apontar para valores[2] (E1008). Em seguida, *p = 999; altera exatamente essa posição — ou seja, valores[2] — deixando valores[0] e valores[1] inalterados.
Exercício 7
#include <stdio.h> int somaVetor(int *v, int tamanho); int main() { int valores[5]; int i; printf("Digite 5 numeros:\n"); for (i = 0; i < 5; i++) { scanf("%d", &valores[i]); } printf("Soma: %d\n", somaVetor(valores, 5)); return 0; } int somaVetor(int *v, int tamanho) { int soma = 0; int i; for (i = 0; i < tamanho; i++) { soma = soma + *(v + i); } return soma; }
Exercício 8
O erro é usar o ponteiro p sem inicializá-lo: int *p; declara p, mas seu valor inicial é indeterminado ("lixo de memória") — pode ser qualquer endereço, inclusive um endereço inválido ou de uma região protegida do sistema. A instrução *p = 10; tenta escrever no endereço guardado em p, que não aponta para lugar nenhum conhecido; isso é comportamento indefinido — o programa pode travar imediatamente (o mais provável), pode corromper outra parte da memória silenciosamente, ou pode, por coincidência, "parecer funcionar" em um teste e falhar em outro computador ou execução.
#include <stdio.h> int main() { int valor; // variável de verdade, para onde p vai apontar int *p = &valor; // p inicializado com um endereço válido *p = 10; printf("%d\n", *p); return 0; }
Exercício 9
A saída será 1 500. O vetor valores ocupa 8 bytes na memória (dois int de 4 bytes cada). O ponteiro bp é do tipo char *, então cada posição bp[i] enxerga um único byte dessa mesma região de memória — diferente de um int *, que enxergaria 4 bytes de cada vez. Em uma arquitetura little-endian, o valor 256 (que em binário é 00000001 00000000 em 2 bytes, completando 4 bytes com zeros à esquerda) é armazenado com o byte menos significativo (0x00) no primeiro endereço. A instrução bp[0] = 1; substitui exatamente esse primeiro byte por 1, sem tocar nos outros 3 bytes do inteiro — o valor de valores[0] passa de 256 para 1 (256 já tinha o byte menos significativo zerado; ao trocá-lo por 1, o número final passa a ser só 1). O segundo elemento, valores[1], nem é tocado, e permanece 500.
Se usássemos int *bp em vez de char *bp, a instrução bp[0] = 1; escreveria 4 bytes de uma vez (o tamanho de um int) na posição de valores[0], substituindo o inteiro inteiro por 1 — não apenas um byte dele. O comportamento de "enxergar" a memória byte a byte só existe porque o ponteiro é char *; o tipo do ponteiro sempre determina de quantos em quantos bytes ele lê e escreve.
Exercício 10
O vetor nome tem espaço para apenas 8 caracteres na memória. A função gets() lê o que o usuário digitar sem nunca verificar se cabe nesse espaço — se o usuário digitar, por exemplo, 20 caracteres, os 12 caracteres que "sobram" são escritos além do fim do vetor nome, sobrescrevendo o que estiver logo depois dele na memória (nesse caso, possivelmente as variáveis idade e saldo, ou até outras informações internas do programa, dependendo de como o compilador organizou a memória). Isso acontece mesmo sem que o programa nunca tenha escrito idade = ... ou saldo = ... depois da leitura — a própria gets(), por dentro, está escrevendo byte a byte na memória, exatamente como fizemos manualmente no Exercício 9, só que sem controle nenhum sobre até onde vai. Esse tipo de falha é chamado de buffer overflow, e é uma das causas mais clássicas de bugs graves e falhas de segurança em programas C. É exatamente por isso que gets() foi removida do padrão da linguagem.
#include <stdio.h> #include <string.h> int main() { char nome[8]; int idade = 20; float saldo = 100.0; printf("Digite seu nome: "); fgets(nome, sizeof(nome), stdin); // respeita o tamanho de "nome" nome[strcspn(nome, "\n")] = '\0'; // remove o '\n' deixado pelo fgets printf("Nome: %s, idade: %d, saldo: %.2f\n", nome, idade, saldo); return 0; }