CS50x em Português - Aula 4 - Memória
CS50
Imagens feitas de zeros e uns 0:37
A aula começa mostrando obras de arte feitas por alunos com post-its verdes e roxos formando a imagem de um gato, usada para ilustrar como computadores armazenam imagens sob o capô. Toda imagem, seja uma foto de bolas antiestresse ou qualquer outra, tem resolução finita: um número de pontos na horizontal multiplicado pelo número de pontos na vertical, resultando em bytes, kilobytes ou megabytes. Ao dar zoom, aparecem pequenos pontos chamados pixels, cada um com uma cor específica. Num exemplo simplificado com apenas 1 bit por pixel, onde 0 representa preto e 1 representa branco, é possível esconder uma carinha sorridente dentro de um padrão de zeros e uns. Hoje em dia usam-se 16, 24 ou mais bits por cor para obter toda a gama de cores, mas o princípio é o mesmo: um arquivo de imagem nada mais é do que uma grade de pontos pretos e brancos, ou coloridos, interpretados por um programa como o aplicativo Fotos.
Cores em RGB e hexadecimal 4:00
Retomando o sistema RGB (vermelho, verde e azul) já visto na semana 0, a aula mostra o seletor de cores do Photoshop, onde o preto é representado como 000000 e o branco como FFFFFF, equivalente a 255 de vermelho, 255 de verde e 255 de azul. Vermelho puro é FF0000, verde é 00FF00 e azul é 0000FF. Essa notação usa o sistema hexadecimal, que tem 16 dígitos possíveis: de 0 a 9 e depois A, B, C, D, E, F, onde F representa o número 15. Contando em hexadecimal, depois de 0F vem 10, que corresponde ao número 16 em decimal, não a dez. FF em hexadecimal equivale a 255 em decimal. A vantagem do hexadecimal é que cada dígito representa exatamente 4 bits, então dois dígitos hexadecimais representam um byte inteiro de 8 bits, o que torna essa notação muito conveniente para descrever cores e endereços de memória. Para evitar ambiguidade, todo número em hexadecimal é escrito com o prefixo 0x, deixando claro que 0x10 significa 16 e não dez.
Onde as variáveis moram na memória 13:31
Usando um programa simples chamado addresses.c, que declara um inteiro n igual a 50 e o imprime, a aula mostra que esse valor precisa ocupar algum lugar físico na memória do computador, tipicamente 4 bytes, já que inteiros costumam ter 32 bits. Esse local tem um endereço, algo como 0x123 para fins didáticos, embora na prática os endereços reais sejam números bem maiores e menos intuitivos. Para descobrir esse endereço no código, usa-se o operador e comercial, &, colocado antes do nome da variável, como em &n. Para exibir esse endereço na tela em vez do valor da variável, troca-se o código de formatação %i por %p no printf. Ao rodar o programa, aparece um endereço real como 0x7FFFD3C34ECC, mostrando que o computador de fato guarda essa informação, mesmo que raramente seja útil aos humanos no dia a dia.
O que é um ponteiro 18:01
É introduzido o conceito de ponteiro, descrito como um dos tópicos mais difíceis da linguagem C, uma variável que armazena um endereço de memória em vez de um valor comum. A sintaxe para declarar um ponteiro usa um asterisco depois do tipo de dado, como em int estrela p, o que significa que p vai guardar o endereço de um inteiro, não o inteiro em si. Assim, é possível escrever int n = 50, depois int estrela p = &n, e imprimir p para ver o endereço de n. É mostrado também um erro comum: se o asterisco for esquecido na declaração de p, o compilador acusa incompatibilidade entre ponteiro e inteiro. Em resposta a perguntas da turma, fica esclarecido que o mesmo operador de endereço funciona para outros tipos de dados além de inteiros, como strings, e que por convenção um ponteiro ocupa 8 bytes, ou 64 bits, porque os computadores atuais têm memória na casa dos gigabytes e precisam de endereços capazes de contar além de 4 bilhões.
Abstraindo endereços com setas 25:33
Como programadores raramente se importam com o endereço exato onde uma variável está guardada, é comum abstrair esse detalhe e representar ponteiros com setas em diagramas, quadros brancos ou seções de estudo, indicando apenas que uma variável como p aponta para o valor armazenado por n. Para tornar essa ideia mais concreta, a aula recorre a uma metáfora física usando caixas de correio: uma caixa rotulada como p representa fisicamente a variável ponteiro, e dentro dela estaria guardado o endereço de outro valor, preparando o terreno para explorar como isso funciona na prática.
Ponteiros como caixas de correio 27:03
O professor retoma a analogia das caixas de correio para explicar endereços de memória: cada caixa pode conter um valor útil, como o número 50, ou pode conter o endereço de outra caixa. Um ponteiro é justamente essa segunda caixa, aquela que aponta para onde o valor real está guardado. Usando o operador de desreferência, o asterisco antes do nome da variável, é possível ir até o endereço armazenado em p e recuperar o valor 50 que está guardado em n, em vez de imprimir apenas o próprio endereço em hexadecimal.
Strings são arrays contíguos 32:30
O exemplo passa a usar a string s igual a HI!, mostrando que ela ocupa 4 bytes na memória, três para os caracteres H, I e ponto de exclamação, e um quarto para o caractere nulo que marca o fim da string. Esses caracteres ficam em endereços contíguos, um seguido do outro, e é por isso que uma string sempre foi, na prática, apenas um array de caracteres guardados lado a lado na memória.
A revelação sobre o tipo string 33:01
O verdadeiro valor guardado em s não é a palavra inteira, mas o endereço do primeiro caractere dela, aqui usado como exemplo o endereço 0x123. A partir disso o professor revela que o tipo string, ensinado desde a semana 1, é na verdade apenas um sinônimo para char*, criado pela biblioteca do CS50 através de um typedef, da mesma forma que antes foi criado o tipo person para a lista telefônica. Sem a biblioteca do CS50, o compilador não reconhece a palavra string, mas o programa volta a funcionar normalmente se ela for substituída diretamente por char*, porque é isso que ela sempre significou.
Aritmética de ponteiros e impressão 41:30
Como printf recebe apenas o endereço do primeiro caractere quando usa o formato %s, ele mesmo percorre a memória, caractere por caractere, até encontrar o terminador nulo, e é por isso que não é preciso desreferenciar s manualmente nesse caso. O professor mostra então que é possível somar números a um endereço, chamado de aritmética de ponteiros, de forma que s mais 1 aponta para o segundo caractere e s mais 2 para o terceiro, produzindo os mesmos resultados que a notação de array s colchete 0, s colchete 1 e s colchete 2, que é apenas uma forma mais agradável de escrever a mesma operação.
Comparando strings com strcmp 48:01
Comparar dois inteiros com o operador de igualdade funciona normalmente, mas o mesmo teste falha com strings, porque a variável não guarda os caracteres em si, apenas o endereço deles. Por isso a função strcmp, da biblioteca de strings, é necessária: ela compara caractere por caractere e retorna 0 quando as duas sequências são idênticas. O professor demonstra isso digitando hi! duas vezes, obtendo o resultado igual, e depois oi e tchau, obtendo diferente, deixando claro que cada chamada a GetString cria seu próprio pedaço de memória, mesmo que o texto digitado seja coincidentemente o mesmo.
Comparando strings por endereço versus conteúdo 54:01
Nesse ponto, s e t são dois ponteiros que apontam para pedaços diferentes da memória, cada um guardando sua própria string 'oi'. Quando se usa strcmp, a função recebe os endereços de s e t e percorre caractere por caractere, comparando o conteúdo real das strings. Já ao usar o operador ==, o que se compara são os próprios endereços, como perguntar se 0x123 é igual a 0x456, o que quase sempre é falso mesmo quando o texto digitado é idêntico. Ao imprimir os endereços de s e t com %p, fica claro que são diferentes, ainda que o usuário tenha digitado a mesma palavra duas vezes.
O problema de copiar uma string com igual 58:04
Ao criar um programa chamado copy.c que pede uma string s e tenta copiá-la para t apenas com t igual a s, e depois capitaliza o primeiro caractere de t, o resultado surpreende: tanto s quanto t aparecem capitalizados. Isso acontece porque t recebeu apenas uma cópia do endereço de s, então os dois ponteiros apontam para o mesmo bloco de memória. Mudar o primeiro caractere através de t altera exatamente o mesmo byte que s também aponta, por isso os colchetes de array e o acesso direto ao endereço funcionam de forma idêntica.
Alocando memória de verdade com malloc e free 1:01:00
Para copiar uma string de fato, é preciso pedir memória nova ao sistema operacional com malloc, informando quantos bytes são necessários, calculados como o comprimento de s mais 1 para o caractere nulo. Depois de incluir a biblioteca stdlib.h, um laço percorre cada posição de s até o comprimento total, copiando caractere por caractere para o novo bloco apontado por t, garantindo que o terminador nulo também seja copiado. Uma versão mais limpa do mesmo trabalho usa a função strcpy, que recebe primeiro o destino e depois a origem. Quando essa memória alocada não é mais necessária, ela deve ser devolvida ao computador com free; não fazer isso é chamado de vazamento de memória e pode deixar programas de longa duração, como servidores ou aplicativos de celular, cada vez mais lentos.
O valor nulo e verificações de erro 1:08:00
Funções como getString e malloc podem falhar e, nesses casos, retornam o valor especial NULL, que corresponde ao endereço de memória 0x0, reservado para nunca guardar nada de útil. Por isso, um programa cuidadoso deveria checar se s ou t são iguais a NULL antes de continuar, abortando com um código de erro caso algo tenha dado errado, por exemplo quando o texto digitado é maior do que a memória disponível. Também é preciso verificar se o comprimento da string é maior que zero antes de tentar capitalizar o primeiro caractere, já que uma string vazia não tem nada para colocar em maiúscula e tocar nela pode causar problemas. A regra prática é simples: tudo que foi alocado com malloc deve ser liberado com free, mas memória obtida através do getString do CS50 já é liberada automaticamente por ele.
Usando o Valgrind para caçar erros de memória 1:14:02
O programa memory.c aloca espaço para três inteiros usando malloc e sizeof, mas contém dois erros propositais: os índices usados são 1, 2 e 3 em vez de 0, 1 e 2, e a memória alocada nunca é liberada com free. Executando esse programa normalmente não mostra nenhum problema visível, mas a ferramenta Valgrind, ao rodar valgrind ./memory, revela uma escrita inválida de tamanho 4 na linha 11, apontando o erro de indexação, além de um resumo de vazamento indicando que 12 bytes em 1 bloco nunca foram liberados, remetendo à linha 8 onde a memória foi alocada. Corrigir os índices para começar em 0 e adicionar a chamada de free ao final resolve os dois problemas identificados.
Verificando erros do malloc 1:20:33
Retomando o exemplo de memória, o professor lembra que sempre que se chama malloc, ou funções como getString, é preciso verificar se o retorno é nulo antes de usar essa memória, já que malloc pode falhar por falta de espaço. Com essa verificação adicionada, o programa é recompilado e testado com o Valgrind, que agora mostra zero erros e nenhum vazamento de memória, confirmando que o bug foi corrigido.
Valores lixo na memória 1:23:01
Para mostrar o que são valores lixo, é criado um programa chamado garbage.c que aloca um array para 1024 inteiros mas nunca atribui valores a eles. Ao imprimir esse array, aparecem números aleatórios como zeros, 25, 32000 ou valores negativos, que são apenas resquícios de usos anteriores daquela memória, e por isso não se deve confiar em memória não inicializada.
Binky e os ponteiros perigosos 1:27:32
Um segundo programa mostra dois ponteiros, x e y, declarados mas não inicializados corretamente. Atribuir um valor através de x funciona porque x foi alocado com malloc, mas tentar desreferenciar y falha porque y nunca apontou para um endereço válido. O vídeo de claymation de Nick Parlante, de Stanford, com o personagem Binky ilustra exatamente esse erro e mostra que a solução é fazer y apontar para o mesmo endereço de x, e não desreferenciar um ponteiro sem destino definido.
Poder e risco da linguagem C 1:31:02
O professor observa que C tem desempenho altíssimo por dar controle direto sobre a memória, o que explica sua permanência ao longo das décadas, mas esse mesmo controle facilita erros graves, muitas vezes responsáveis por falhas de segurança em softwares reais. Linguagens mais modernas, como Python ou Java, sacrificam desempenho em troca de mais proteções contra esse tipo de erro.
A demonstração dos copos com Olivia 1:33:03
Uma voluntária chamada Olivia, do Canadá, participa de uma demonstração com dois copos de líquidos coloridos, azul e laranja, colocados nos copos errados. Para trocar os líquidos sem misturá-los, ela precisa de um terceiro copo vazio como espaço temporário, o que ilustra fisicamente a necessidade de uma variável temporária ao trocar dois valores em código.
O problema da troca por valor 1:36:02
O programa swap.c tenta aplicar essa lógica em C, com uma função que recebe dois inteiros a e b e troca seus valores usando uma variável temp. Mesmo copiando fielmente a lógica dos copos, ao executar o programa os valores de x e y na função main não são trocados, porque em C os argumentos são passados por valor, ou seja, a função recebe apenas cópias de x e y, não os originais.
Pilha, heap e memória do programa 1:39:32
O professor explica como a memória do computador é organizada: o código de máquina fica no topo, seguido pelas variáveis globais, depois o heap, onde o malloc alocado memória crescendo para baixo, e por fim a pilha, onde funções e variáveis locais são armazenadas crescendo para cima, como bandejas empilhadas. Cada chamada de função, como main chamando swap, ganha seu próprio espaço na pilha, chamado frame, que desaparece quando a função termina, deixando resquícios que mais tarde aparecem como valores lixo.
A solução com passagem por referência 1:44:32
Como a e b na função swap são apenas cópias de x e y, trocar a e b não afeta os valores originais em main. A solução é passar por referência, usando ponteiros: em vez de a e b serem inteiros, eles passam a ser endereços de inteiros, permitindo que a função vá diretamente ao endereço de x e de y para trocar os valores reais. O professor reconhece que o uso repetido do símbolo asterisco, para multiplicação, declaração de ponteiro e desreferenciação, é confuso, mas é a ferramenta disponível na linguagem.
Swap funcionando com ponteiros 1:46:00
O professor retoma o diagrama de memória para mostrar que, ao passar o endereço de x e o endereço de y para a função swap, a variável tmp recebe o valor apontado por a, depois o valor de b é copiado para o endereço de a, e por fim tmp é copiado para o endereço de b. Assim, quando swap termina de executar, os valores reais de x e y foram trocados, porque a função recebeu um mapa do tesouro até essas variáveis, não apenas cópias delas. Essa é a primeira resposta prática para a utilidade dos ponteiros: eles permitem resolver uma classe inteira de problemas que antes eram impossíveis.
Pilha e heap podem colidir 1:48:00
O vídeo explica que a pilha, usada pelas chamadas de função, e a heap, usada por malloc, crescem em direções opostas dentro da memória finita do computador, e podem eventualmente colidir. A solução prática é minimizar o uso de memória, alocando com malloc apenas o necessário e evitando chamadas de função repetidas sem retorno, como recursões sem caso base. Daí vêm os termos estouro de pilha e estouro de heap, e até a origem do nome do site Stack Overflow. Um problema relacionado, mais específico, é o estouro de buffer, quando um array pequeno recebe mais valores do que sua capacidade.
Substituindo get_int por scanf 1:51:01
Para entender por que a biblioteca CS50 é útil, o professor reescreve um programa simples sem usar get_int, usando em vez disso a função scanf, que lê algo do teclado e grava na memória. Como scanf precisa alterar o valor de uma variável, é necessário passar o endereço de n com o operador e comercial, exatamente como fizeram com swap. Depois de ajustar o protótipo e o corpo da função swap para receber ponteiros de fato, o programa passa a trocar os valores corretamente, confirmando que a chave é dar à função o endereço das variáveis, não seus valores.
O perigo de ler strings sem alocar espaço 1:57:33
Tentar substituir get_string por scanf revela um problema mais sério: a variável s, declarada apenas como char* sem inicialização, contém um endereço lixo, então scanf grava a string digitada em um local de memória desconhecido e potencialmente inválido. O programa pode parecer funcionar com entradas curtas, mas a ferramenta Valgrind revela vários erros de uso de valores não inicializados. As soluções possíveis são alocar um número fixo de bytes suficientes, usar um array de tamanho definido, ou evitar scanf para strings, já que não há como saber de antemão quantos caracteres o usuário vai digitar. É por isso que get_string, implementada chamando malloc repetidamente para crescer a memória conforme necessário, e também por isso que linguagens como Python, que a turma usará em duas semanas, tornam essa tarefa muito mais simples.
Criando arquivos persistentes com fopen 2:02:31
O vídeo introduz E/S de arquivos, a capacidade de criar, ler e salvar arquivos em disco rígido ou SSD, que são formas de armazenamento persistente, ao contrário da RAM, que é volátil e perde tudo sem energia. Usando fopen, fclose e fprintf, o professor cria um programa phonebook.c que grava nome e número num arquivo phonebook.csv. Ele mostra que abrir o arquivo com o modo w sempre sobrescreve o conteúdo anterior, enquanto o modo a anexa novos dados ao final, permitindo que a lista telefônica realmente persista entre execuções do programa. Ele também recomenda verificar se o retorno de fopen é nulo antes de continuar, já que qualquer uso de ponteiros exige cuidado, e explica que a manipulação de arquivos em C depende inteiramente de ponteiros porque é assim que a linguagem foi projetada.
O conceito de buffer em vídeos 2:10:31
Para encerrar, o professor relaciona a ideia de buffer, um pedaço de memória de tamanho finito, geralmente um array, que armazena bytes temporariamente, à experiência comum de assistir vídeos on-line. Quando a barra de carregamento avança mais rápido que a reprodução, os próximos bytes do vídeo já estão armazenados nesse buffer, mas se a conexão for lenta e os bytes acabarem antes de novos serem baixados, o vídeo trava e aparece a mensagem de buffering.
Construindo o programa cp em C 2:11:32
O professor propõe recriar o comando cp, usado para copiar arquivos, para mostrar mais recursos de entrada e saída de arquivos. O programa recebe dois argumentos de linha de comando, guardados em argv[1] e argv[2], sendo o primeiro o nome do arquivo de origem e o segundo o nome do arquivo de destino, já que argv[0] guarda apenas o nome do próprio programa. Com fopen, abre-se o arquivo de origem em modo leitura e o de destino em modo escrita, preparando tudo para a cópia byte a byte.
Copiando byte a byte com fread e fwrite 2:12:01
Como a linguagem C não tem um tipo pronto para representar um byte, ele cria com typedef um novo tipo chamado byte, definido como unsigned char, garantindo que os 8 bits nunca sejam interpretados como número negativo. Um laço então usa fread para ler um byte por vez do arquivo de origem e fwrite para escrever esse mesmo byte no arquivo de destino, repetindo até o retorno de fread ser zero. Depois, os dois arquivos são fechados para salvar a cópia. Ao testar com o arquivo phonebook.csv, a cópia gerada, copy.csv, contém exatamente os mesmos dados, mostrando que a técnica funciona.
Arquivos bitmap e filtros de imagem 2:17:02
Ele explica que a próxima tarefa envolverá arquivos BMP, que armazenam uma grade de pixels linha por linha, cada um com valores de vermelho, verde e azul. Usando os mesmos princípios de ponteiros e leitura byte a byte, será possível criar filtros como transformar uma foto em tons de cinza, aplicar um tom sépia, espelhar a imagem, borrá-la ou até detectar bordas para separar, por exemplo, uma ponte do céu ao fundo. A aula termina com uma piada de ciência da computação e o encerramento da semana 4.
AI-generated summary. It can be wrong or incomplete - check anything that matters against the original.
