CS50x em Português - Aula 2 - Arrays
CS50
Leituras dos voluntários e níveis de leitura 0:45
A aula começa com três voluntários lendo trechos diferentes em voz alta: Lea lê 'Um peixe, dois peixes, peixe vermelho, peixe azul' e a turma identifica seu nível como jardim de infância, por causa das frases curtas e simples. Maria lê um trecho com rima e um pouco mais de complexidade, avaliado como nível de terceira série. Omar lê um parágrafo de 1984, de George Orwell, com frases e vocabulário mais elaborados, classificado como nível dez. O professor explica que essa avaliação será feita, ao longo da semana, por uma medida científica que analisa matematicamente um texto para estimar seu nível de leitura.
Representar texto e o problema da criptografia 5:30
Para calcular esse nível de leitura, será preciso representar o texto na memória do computador de forma mais detalhada do que apenas imprimir strings inteiras, como se fazia na semana anterior. Outro tema central da aula é a criptografia, a arte de embaralhar informações para que uma mensagem só possa ser lida por quem tem permissão, mesmo que seja interceptada no caminho. O professor mostra na tela uma mensagem já criptografada, com uma pista de que, ao final da semana, ela poderá ser decifrada e revelará a frase 'CS50'.
A origem da palavra bug 8:03
Antes de entrar em exemplos práticos, o professor fala sobre depuração, o processo de encontrar e remover erros do código. Ele credita à doutora Grace Hopper, contra-almirante da Marinha e uma das programadoras do computador Harvard Mark I, a popularização do termo 'bug': sua equipe encontrou literalmente uma mariposa presa na fiação da máquina e anotou no diário de bordo o 'primeiro caso real de bug encontrado'.
Corrigindo erros de sintaxe em bug.c 9:03
No VS Code, o professor cria de propósito um programa chamado bug.c cheio de falhas para ensinar a corrigi-las junto com a turma. Os erros incluem esquecer de incluir o arquivo de cabeçalho stdio.h, faltar um ponto e vírgula, esquecer a barra invertida com o caractere de nova linha, confundir stdio com studio, não declarar o tipo de uma variável como string e esquecer de incluir o cabeçalho cs50.h, necessário para usar recursos como get_string. Cada mensagem de erro do compilador é lida e interpretada com a turma, mostrando como o compilador aponta a linha e o tipo de problema, mas não entende a intenção do programador.
Erros lógicos e o uso de printf para depurar 19:04
Depois dos erros de sintaxe, o professor mostra um erro lógico: um programa compila normalmente, mas imprime 'Olá' seguido de um espaço em vez do nome digitado, porque faltou usar o especificador de formato %s e a vírgula com o nome da variável. Em seguida, com um laço for que deveria imprimir três tijolos usando o símbolo #, o programa imprime quatro, porque o laço vai de zero até menor ou igual a três. Para investigar, o professor usa printf temporariamente para exibir o valor da variável i a cada repetição, revelando que o laço passa pelos valores 0, 1, 2 e 3, quatro valores ao todo, e corrige trocando a condição para 'menor que três'.
Funções auxiliares e a ferramenta debug50 22:32
O professor reorganiza o código criando uma função auxiliar chamada print_column, que recebe a altura desejada e imprime a coluna de tijolos, sendo chamada dentro da função main. Isso exige declarar um protótipo de função antes do main, informando nome, entradas e saída da função, para que o compilador saiba o que ela significa antes de encontrar sua definição completa mais abaixo. Com o código mais modularizado, porém mais longo, o professor observa que adicionar e remover printf repetidamente se torna cansativo, e apresenta o debug50, um comando do CS50 que aciona um depurador padrão da indústria, permitindo pausar e percorrer o código linha por linha para observar o que está acontecendo durante a execução.
Usando o depurador debug50 26:30
O professor mostra como usar o comando debug50 para percorrer o código linha por linha, algo que exige antes definir um ponto de interrupção clicando à esquerda do número da linha desejada. Ao rodar debug50 bug, a execução para dentro da função main, mostrando no painel esquerdo o valor das variáveis na memória, mesmo antes de qualquer linha ser executada. Esse valor inicial, como 32764, é chamado de valor de lixo, resultado de memória RAM usada anteriormente por outro processo e que ainda guarda um padrão aleatório de bits.
Step over e step into 31:04
Os ícones do depurador permitem continuar a execução normalmente, avançar uma linha por vez com step over, ou entrar dentro de uma função chamada com step into. Ao aplicar isso ao programa que imprimia quatro cerquilhas em vez de três, o professor acompanha a variável i dentro da função printColumn e percebe que a condição do laço usava menor ou igual a três, permitindo uma repetição extra. A correção é trocar esse operador por um simples menor que, resolvendo o bug.
Dúvidas sobre pontos de interrupção e loops 34:31
Em resposta a perguntas, o professor explica que um ponto de interrupção apenas indica onde a execução deve pausar, podendo ser colocado em qualquer função, não só em main. Sobre os valores de lixo, esclarece que o sistema operacional organiza a memória de forma que sobras de uso anterior aparecem ali, o que também representa um risco de segurança se dados sensíveis ficarem expostos. Também revisa a ordem de operações de um laço for: primeiro a inicialização, depois a condição booleana, em seguida o código dentro das chaves, depois a atualização, e a condição é checada de novo antes de cada repetição.
Depuração com pato de borracha e uso da IA 38:04
O professor apresenta a técnica de depuração com pato de borracha, que consiste em explicar o problema em voz alta a um objeto inanimado, o que muitas vezes ajuda a perceber a falha de raciocínio sem precisar de outra pessoa. Ele observa que, na era da inteligência artificial, existe também um pato virtual em CS50.ai dentro do VS Code de CS50.dev, útil para tirar dúvidas pontuais, mas recomenda não colar o código inteiro pedindo a solução, já que o objetivo do curso é desenvolver a própria habilidade de resolver problemas.
Do código fonte ao código de máquina 40:32
O professor retoma o modelo de compilação: o código fonte escrito em C precisa ser traduzido para código de máquina, feito de zeros e uns, e essa tradução é feita por um compilador chamado Clang, sendo make apenas um comando que automatiza a chamada ao Clang. Ele mostra que compilar diretamente com clang hello.c gera um arquivo chamado a.out em vez de hello, e que usar funções da biblioteca CS50, como getString, exige o argumento adicional lcs50 para o compilador encontrar essa biblioteca. Também explica os argumentos de linha de comando, que são as palavras digitadas depois do nome do programa no terminal, e como o argumento -o permite escolher o nome do arquivo de saída.
As quatro etapas da compilação 48:32
O termo compilar, explica o professor, esconde na verdade quatro processos separados que ocorrem em sequência automaticamente. O primeiro é o pré-processamento, no qual o compilador localiza arquivos de cabeçalho como cs50.h ou stdio.h no disco do servidor e copia e cola seu conteúdo, incluindo protótipos de funções como getString e printf, diretamente no código do programador, evitando que ele precise escrever esses protótipos manualmente. A etapa seguinte, a compilação propriamente dita, converte esse código pré-processado em código assembly, uma forma de programação anterior ao surgimento de linguagens como C, Python ou Java, quando os programadores ainda escreviam instruções em cartões perfurados.
Da montagem à ligação do programa 52:02
O compilador transforma o código C em instruções de assembly, que são os comandos de baixo nível que a CPU realmente entende, como move, push, xor e call. Em seguida, o processo de montagem converte esse código assembly em zeros e uns, e por fim a etapa de ligação combina esses zeros e uns com o código de bibliotecas escritas por outras pessoas, como a biblioteca do CS50 e a STDIO, que já vêm compiladas de antemão. Foi por isso que, quando faltou a flag lcs50 num exemplo anterior, o compilador reclamou de um erro de ligação, já que não sabia onde encontrar esse código pronto no disco. Na prática, o mundo da programação costuma chamar todo esse processo de pré-processamento, compilação, montagem e ligação simplesmente de compilar.
Por que existem CPUs diferentes 58:03
Cada tipo de CPU, seja de iPhone, Android, Mac ou PC, exige um compilador diferente, porque o mesmo código C gera instruções de assembly distintas para cada arquitetura. É por isso que um programa gravado para Mac não rodava num PC antigamente. Dividir a compilação em quatro etapas separadas também permite que equipes diferentes trabalhem em cada parte, permitindo que todos continuem escrevendo na mesma linguagem em nível mais alto mesmo que o resultado final varie por dispositivo.
Por que não dá para descompilar 1:01:00
Compilar transforma código fonte em código de máquina, mas fazer o caminho inverso, ou seja, descompilar zeros e uns para recuperar o código original, é extremamente difícil e seria uma ameaça real à propriedade intelectual se fosse fácil. Mesmo sabendo a arquitetura da CPU, decifrar manualmente o que cada padrão de bits significa levaria um tempo absurdo, provavelmente mais do que simplesmente reimplementar um programa como o Microsoft Word do zero. Além disso, coisas como a escolha entre um loop for ou um loop while geram exatamente o mesmo padrão de zeros e uns, então nunca dá para saber com certeza como o código fonte original foi escrito. A exceção fica por conta de linguagens como JavaScript, cujo código fonte é enviado dos servidores para os navegadores e pode ser visto diretamente em qualquer site.
Quanto espaço cada tipo ocupa 1:02:00
Um bool ocupa 1 byte inteiro, mesmo precisando só de um bit, porque é mais eficiente trabalhar com bytes completos. Um int usa 4 bytes, permitindo contar até cerca de 4 bilhões, ou 2 bilhões para cada lado se incluir negativos. Um long usa 8 bytes, chegando a quatrilhões de possibilidades. Um float usa 4 bytes e um double usa o dobro disso para mais precisão, embora a imprecisão de ponto flutuante continue sendo um problema em áreas como cálculos financeiros e científicos. Um char ocupa 1 byte usando o código ASCII, e uma string varia de tamanho conforme o número de caracteres armazenados. Esses valores ficam fisicamente guardados em pentes de memória, chips com bilhões de bytes endereçáveis um após o outro.
O problema da divisão de inteiros 1:06:02
Ao escrever um programa para calcular a média de três notas de conjuntos de problemas, usando as pontuações 72, 73 e 33, a divisão por inteiros trunca o resultado e mostra 59 em vez de 59 e um terço. A solução é garantir que pelo menos um dos números envolvidos na divisão seja um float, seja convertendo as variáveis para float, escrevendo o divisor como 3.0, ou convertendo explicitamente o número três para float entre parênteses. Qualquer uma dessas abordagens recupera a parte decimal perdida na divisão.
Arrays substituem variáveis repetidas 1:12:34
Criar uma variável separada para cada pontuação funciona para três notas, mas se torna impraticável para trinta ou trezentas. A solução é usar um array, um bloco de memória contíguo que agrupa vários valores do mesmo tipo sob um único nome. Com a sintaxe int scores[3], é possível reservar espaço para três inteiros de uma vez e acessar cada posição por um índice que começa em zero, como scores[0], scores[1] e scores[2]. Isso reduz três variáveis a uma só, e os valores continuam ocupando 4 bytes cada, lado a lado na memória, exatamente como antes, mas agora de forma organizada e escalável. O próprio VS Code ajuda a completar a sintaxe dos colchetes automaticamente durante a digitação. Ao final, a função get_int da biblioteca do CS50 é usada para tornar o programa interativo, pedindo as três pontuações diretamente ao usuário em vez de fixá-las no código.
Usando laço for para preencher o array 1:17:04
O professor mostra que repetir três linhas quase idênticas para pedir cada pontuação é um mau design. A solução é usar um laço for que percorre as posições do array de pontuações, chamando get_int dentro do laço e guardando cada valor na posição correta usando os colchetes. Assim, em vez de três linhas repetidas, uma única estrutura de repetição preenche as posições zero, um e dois do array automaticamente.
O problema do número mágico 1:19:02
Mesmo com o laço for, o código ainda tem uma falha: o número três aparece codificado em dois lugares diferentes, no tamanho do array e na contagem do laço. Isso é arriscado, porque basta errar um dos dois números para gerar um bug. A correção é criar uma variável chamada n para guardar esse valor uma única vez e usá-la nos dois lugares. Por convenção de estilo, quando essa variável não deve mudar, ela é declarada como constante, usando a palavra const e escrevendo o nome em letra maiúscula, para deixar visualmente claro que é um valor fixo.
Criando a função average 1:20:30
Para calcular a média das pontuações, o professor cria uma função chamada average que recebe um array de números e devolve um float, evitando perder a parte decimal do resultado. Dentro da função, uma variável sum começa em zero e um laço for soma cada valor do array. Como em C não é possível perguntar ao array qual é o seu tamanho, como se faz em Java ou Python, a função precisa receber também um segundo parâmetro chamado length, informando quantos números existem. A média é calculada dividindo a soma pelo comprimento, e esse valor é devolvido pela função com a palavra return, em vez de ser impresso diretamente dentro dela.
O erro da divisão de inteiros 1:25:06
Ao testar a função com as notas 72, 73 e 33, o resultado aparece como 59, sem a parte decimal esperada. O erro é a divisão de inteiro por inteiro, que trunca qualquer resto. A correção é converter a soma ou o comprimento para float antes de dividir, usando uma conversão de tipo, o que permite obter o resultado correto, 59.3333. O professor reforça que, ao criar uma função que recebe um array, é preciso sempre passar também o comprimento do array, e que, ao declarar o parâmetro do array na função, usa-se colchetes vazios, sem número dentro.
Caracteres como números na memória 1:29:34
Usando a grade de memória, o professor mostra que três variáveis do tipo char, guardando as letras H, I e o sinal de exclamação, ficam lado a lado na memória, cada uma ocupando um padrão de oito bits. Ao imprimir essas variáveis com o código de formatação de inteiro em vez do de caractere, aparecem os números 72, 73 e 33, provando que um caractere é, na prática, apenas um número entre 0 e 255 da tabela ASCII. Ele também explica por que números com ponto decimal, como 3.0, são tratados como double por padrão, e observa que a relação entre os códigos de formatação do printf e os tipos de dados em C nem sempre é consistente, sendo isso uma herança histórica da linguagem.
Strings como arrays de caracteres 1:38:32
O professor demonstra que uma string, como a palavra hi, é internamente um array de caracteres, cada um acessível pelos colchetes, s do zero, s do um e assim por diante. Ao imprimir uma posição além do fim da string, aparece um valor zero, que na verdade é o caractere nulo, um padrão especial de oito bits usado pelo compilador para marcar automaticamente o fim de toda string escrita entre aspas duplas. Por isso, uma string de três letras ocupa na verdade quatro bytes na memória. Esse caractere nulo é o que permite ao printf saber onde parar de imprimir, e ele corresponde à posição da tabela ASCII que os alunos já haviam visto brevemente antes.
Arrays de strings 1:42:06
Por fim, o professor cria um array chamado words capaz de guardar duas strings, hi e bye, cada uma ocupando uma posição do array. Ele mostra que a sintaxe para criar e imprimir esse array de palavras é idêntica à usada para arrays de números, reforçando a ideia de que um array de palavras é, na memória, uma sequência de arrays de caracteres, já que cada palavra em si já é um array.
Indexação dupla em arrays de strings 1:44:02
Você pode combinar dois conjuntos de colchetes para acessar letras individuais dentro de palavras guardadas num array, escrevendo algo como palavra em colchetes e depois a letra em outro colchete. Isso serve apenas para demonstrar que uma string é, na prática, um array, já que basta indexar a palavra e depois o caractere desejado dentro dela. O importante aqui é fixar a ideia central do dia: para todos os efeitos práticos, strings são arrays.
Criando e inicializando arrays 1:46:00
Um array é criado especificando nome, tamanho e tipo, e a partir daí você acessa qualquer posição com colchetes. Também é possível inicializar um array inteiro numa única linha, usando chaves com os valores separados por vírgula, como 72, 73, 33, sem precisar nem informar o tamanho, porque o compilador conta quantos valores existem e reserva o espaço certo sozinho.
Contando o comprimento de uma string manualmente 1:48:02
Para descobrir quantos caracteres tem uma string, você pode criar uma variável n começando em zero e usar um loop while que pergunta, posição por posição, se o caractere ali é o caractere nulo, incrementando n enquanto a resposta for não. Como a função get string sempre coloca um caractere nulo no fim da entrada do usuário, mesmo quando a pessoa não digita nada, o programa consegue medir corretamente o tamanho da palavra, inclusive obtendo zero para uma entrada vazia.
A função strlen e a biblioteca string.h 1:52:01
Em vez de reinventar essa contagem manual, existe a biblioteca padrão string.h, com a função strlen, que já faz exatamente esse trabalho. Isso permite simplificar bastante o código, eliminando a variável auxiliar e chamando diretamente strlen dentro do printf. A partir daí, um novo programa percorre uma string caractere por caractere usando um loop for que compara o índice com o valor retornado por strlen, mas essa comparação repetida dentro da condição do loop é ineficiente, porque strlen é recalculada a cada volta mesmo que o comprimento da string não mude. A correção é guardar o valor numa segunda variável, declarada junto com a variável de índice na própria inicialização do loop, desde que ambas sejam do mesmo tipo.
Transformando texto em maiúsculas com ctype.h 1:59:33
Para converter letras minúsculas em maiúsculas, dá para comparar cada caractere com 'a' e 'z' e subtrair 32, aproveitando que na tabela ASCII as letras minúsculas ficam exatamente 32 posições acima das maiúsculas correspondentes. Mas a biblioteca ctype.h já oferece funções prontas para isso, como islower e toupper, que verificam e convertem o caractere automaticamente, dispensando a matemática manual e deixando o código bem mais enxuto.
Argumentos de linha de comando em main 2:06:31
A função main pode ser escrita de duas formas: com void entre parênteses, indicando que o programa não recebe palavras depois do seu nome no terminal, ou com int argc e string argv em colchetes, permitindo receber esses argumentos. Argc conta quantas palavras foram digitadas após o nome do programa, e argv é o array com essas palavras. Usando essa segunda forma, um programa chamado greet pode cumprimentar o usuário lendo o nome diretamente de argv, em vez de pedir com get string, embora seja preciso manter a inclusão da biblioteca CS50, já que é nela que o tipo string é definido.
Argumentos na linha de comando: argv e argc 2:11:33
O professor mostra como o programa greet usa argv, um vetor de strings que guarda as palavras digitadas depois do nome do programa no terminal. A posição zero de argv sempre contém o nome do próprio programa, o que é útil quando se quer exibir mensagens ou documentação que dependam do nome do arquivo. Para evitar erros quando o usuário não digita nada, ele usa argc, a contagem de argumentos, verificando se argc é igual a dois antes de imprimir o nome da pessoa, e caindo num texto padrão caso contrário.
O valor de retorno da função main 2:14:35
Ele explica que o int antes de main representa o chamado status de saída, um número que indica se o programa terminou com sucesso ou com falha, por convenção zero significa sucesso e qualquer outro valor indica um tipo de erro. Isso é comparado a códigos como o erro 1132 do Zoom ou o 404 da web, que só fazem sentido para quem tem a documentação certa. No programa status.c, se argc não for igual a dois, o programa imprime um aviso e retorna um, caso contrário imprime a saudação e retorna zero, e ele mostra como o comando echo com cifrão e ponto de interrogação revela esse valor de retorno no terminal, o mesmo mecanismo usado pelo Check50 para saber se um código está correto. Também menciona o programa cowsay, que aceita argumentos como o nome de um bicho e a frase a ser dita, apenas como exemplo divertido do mesmo uso de argc e argv.
Introdução à criptografia e à cifra de César 2:20:32
Para encerrar a aula, ele apresenta a criptografia como a arte de embaralhar informações para comunicação segura, transformando um texto simples em texto cifrado por meio de uma cifra que depende de uma chave secreta compartilhada entre as partes. Usa o exemplo de deslocar cada letra em uma posição, de modo que HI se torne IJ com a chave um, e explica que descriptografar significa apenas inverter esse deslocamento. Cita o Rot13, que desloca treze posições e é usado informalmente para escondar espoilers online, e observa que o Rot26 não funciona porque devolve a letra original. Esse tipo de deslocamento é historicamente chamado de cifra de César, e ele nota que hoje é fácil quebrá-la por força bruta testando todas as chaves possíveis, o que serve de ponte para algoritmos mais sofisticados que a disciplina vai explorar depois.
AI-generated summary. It can be wrong or incomplete - check anything that matters against the original.

