Aula 2
- Compilação
- Depuração
- help50 e printf
- debug50
- check50 e style50
- Tipos de Dados
- Memória
- Arrays
- Strings
- Argumentos da linha de comando
- Legibilidade
- Criptografia
Compilação
- Da última vez, aprendemos a escrever nosso primeiro programa em C. Aprendemos a sintaxe para a função
main
em nosso programa, a funçãoprintf
para imprimir no terminal, como criar strings com aspas duplas e como incluirstdio.h
para a funçãoprintf
. - Em seguida, compilamos com
clang hello.c
para poder executar./a.out
(o nome padrão) eclang -o hello hello.c
(passando um argumento da linha de comando para o nome da saída) para poder executar./hello
. - Se quiséssemos usar a biblioteca do CS50, via
#include <cs50.h>
, para strings e a funçãoget_string
, também temos que adicionar um sinalizador:clang -o hello hello.c -lcs50
. O sinalizador-l
vincula o arquivocs50
, que já está instalado no CS50 Sandbox, e inclui protótipos, ou definições de strings eget_string
(entre mais) que nosso programa pode então fazer referência e usar. - Escrevemos nosso código-fonte em C, mas precisamos compilá-lo em código de máquina, em binário, antes que nossos computadores possam executá-lo.
clang
é o compilador, emake
é um utilitário que nos ajuda a executar oclang
sem ter que indicar todas as opções manualmente.- "Compilar" o código-fonte em código de máquina é, na verdade, composto de etapas menores:
- pré-processamento
- compilação
- montagem
- linkagem
- Pré-processamento envolve olhar para as linhas que começam com um
#
, como#include
, antes de tudo. Por exemplo,#include <cs50.h>
dirá aoclang
que procure primeiro por esse arquivo de cabeçalho, pois ele contém conteúdo que queremos incluir em nosso programa. Então,clang
essencialmente substituirá o conteúdo desses arquivos de cabeçalho em nosso programa. -
Por exemplo …
#include <cs50.h> #include <stdio.h> int main(void) { string name = get_string("Name: "); printf("hello, %s\n", name); }
-
… será pré-processado em:
string get_string(string prompt); int printf(const char *format, ...); int main(void) { string name = get_string("Name: "); printf("hello, %s\n", name); }
-
Compilação pega nosso código-fonte, em C, e o converte em código assembly, que se parece com isto:
... main: # @main .cfi_startproc # BB#0: pushq %rbp .Ltmp0: .cfi_def_cfa_offset 16 .Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp .Ltmp2: .cfi_def_cfa_register %rbp subq $16, %rsp xorl %eax, %eax movl %eax, %edi movabsq $.L.str, %rsi movb $0, %al callq get_string movabsq $.L.str.1, %rdi movq %rax, -8(%rbp) movq -8(%rbp), %rsi movb $0, %al callq printf ...
-
Essas instruções são de nível inferior e estão mais próximas das instruções binárias que o CPU de um computador pode entender diretamente. Elas geralmente operam em bytes, ao contrário de abstrações como nomes de variáveis.
-
O próximo passo é pegar o código assembly e traduzi-lo em instruções em binário por montagem. As instruções em binário são chamadas de código de máquina, que a CPU de um computador pode executar diretamente.
- A última etapa é vinculação, onde o conteúdo de bibliotecas anteriormente compiladas que queremos vincular, como
cs50.c
, são realmente combinadas com o binário de nosso programa. Então, acabamos com um arquivo binário,a.out
ouhello
, que é a versão compilada dehello.c
,cs50.c
eprintf.c
.
Depuração
- Bugs são erros em programas que não pretendíamos cometer. E a depuração é o processo de encontrar e corrigir bugs.
help50 e printf
-
Digamos que escrevemos este programa,
buggy0.c
:int main(void) { printf("hello, world\n"); }
- Vemos um erro (em vermelho), quando tentamos
make
este programa, que estamosdeclarando implicitamente a função de biblioteca 'printf'
. Não entendemos muito isso, então podemos executarhelp50 make buggy0
, que nos dirá, no final, que podemos ter esquecido de escrever#include <stdio.h>
, que contémprintf
.
- Vemos um erro (em vermelho), quando tentamos
-
Podemos tentar novamente com
buggy1.c
:#include <stdio.h> int main(void) { string name = get_string("What's your name?\n"); printf("hello, %s\n", name); }
- Vemos muitos erros, e mesmo o primeiro não parece fazer muito sentido. Então, podemos executar novamente
help50 make buggy1
, que irá nos sugerir que precisamos decs50.h
poisstring
não está definido.
- Vemos muitos erros, e mesmo o primeiro não parece fazer muito sentido. Então, podemos executar novamente
-
Para limpar a janela do terminal (para que só possamos ver a saída do que quisermos executar em seguida), podemos pressionar
control + L
, ou digitarclear
como um comando na janela do terminal. -
Vamos dar uma olhada em
buggy2.c
:#include <stdio.h> int main(void) { for (int i = 0; i <= 10; i++) { printf("#\n"); } }
-
Hmm, pretendíamos ver apenas 10
#
, mas há 11. Se não soubéssemos qual é o problema (pois nosso programa está compilando sem nenhum erro, e agora temos um erro lógico), poderíamos adicionar outra linha de impressão para nos ajudar:#include <stdio.h> int main(void) { for (int i = 0; i <= 10; i++) { printf("i agora é %i: ", i); printf("#\n"); } }
-
Agora, vemos que
i
começou em 0 e continuou até chegar a 10, mas deveríamos interromper quando chegasse a 10, comi < 10
em vez dei <= 10
.
-
debug50
- Hoje também daremos uma olhada no IDE do CS50, que é como a Sandbox do CS50, mas com mais recursos. É um ambiente de desenvolvimento online, com um editor de código e uma janela de terminal, além de ferramentas para depuração e colaboração:
- No IDE do CS50, teremos outra ferramenta,
debug50
, para nos ajudar a depurar programas. - Abriremos
buggy2.c
e tentaremosmake buggy2
. Mas salvamosbuggy2.c
em uma pasta chamadasrc2
, então precisamos executarcd src2
para alterar nosso diretório para o correto. E o terminal do IDE do CS50 irá nos lembrar em que diretório estamos, com um prompt como~/src/ $
. (O~
indica o padrão ou diretório inicial). - Em vez de usar
printf
, também podemos depurar nosso programa interativamente. Podemos adicionar um ponto de interrupção, ou um indicador para uma linha de código em que o depurador deve pausar nosso programa. Por exemplo, podemos clicar à esquerda da linha 5 de nosso código, e um círculo vermelho aparecerá: - Agora, se executarmos
debug50 ./buggy2
, veremos o painel do depurador aberto à direita: - Vemos que a variável que criamos,
i
, está na seçãoVariáveis Locais
e vemos que há um valor de0
. - Nosso ponto de interrupção pausou nosso programa após a linha 5, logo antes da linha 7, pois é a primeira linha de código que pode ser executada. Para continuar, temos alguns controles no painel do depurador. O triângulo azul continuará nosso programa até atingirmos outro ponto de interrupção ou o final de nosso programa. A seta curva à sua direita irá "passar por cima" da linha, executando-a e pausar nosso programa novamente imediatamente depois.
- Então, usaremos a seta curva para executar a próxima linha e ver o que muda depois. Estamos na linha
printf
e pressionando a seta curva novamente, vemos um único#
impresso em nossa janela de terminal. Com outro clique da seta, vemos que o valor dai
à direita muda para1
. E podemos continuar clicando na seta para observar a execução do nosso programa, uma linha de cada vez. - Para sair do depurador, podemos pressionar
control + C
para interromper o programa. - Podemos economizar muito tempo no futuro investindo um pouco agora para aprender a usar o
debug50
!
check50 e style50
- Podemos executar um comando como
check50 cs50/problems/hello
, ondecheck50
é um programa que seguirá as instruções identificadas pelo argumentocs50/problems/hello
para carregar, executar e testar seu programa em servidores CS50. Isto checará a correção de seu programa.- Ao escrever software no mundo real, os desenvolvedores geralmente escreverão seus próprios testes para garantir que seu código funciona como esperado, especialmente quando mais funcionalidades são adicionadas ao mesmo código.
style50
é outro programa que checará nosso código em busca de questões estéticas, como espaço em branco, de forma que nosso código seja mais legível e fácil de manter. Por exemplo, podemos estar perdendo recuo. E o Guia de Estilo incluirá mais explicações sobre o que esperamos.- Podemos até usar a depuração de borracha de pato, um método no qual explicamos o que estamos tentando fazer para um pato de borracha, de forma que percebamos o que estamos tentando fazer e o que devemos corrigir.
- Também queremos escrever nosso código com bom design, onde não apenas resolvemos o problema corretamente, mas bem, onde fazemos escolhas razoáveis de como nosso programa é executado, e fazemos compensações entre tempo, custo de desenvolvimento e memória.
Tipos de Dados
- Em C, temos diferentes tipos de variáveis que podemos usar para armazenar dados:
- bool 1 byte
- char 1 byte
- int 4 bytes
- float 4 bytes
- long 8 bytes
- double 8 bytes
- string ? bytes
- Cada um destes tipos ocupa um determinado número de bytes por variável que criamos, e os tamanhos acima são o que a caixa de areia, IDE, e mais provavelmente seu computador usam para cada tipo em C.
Memória
- Dentro de nossos computadores, nós temos chips chamados RAM, memória de acesso aleatório, que armazena dados para uso a curto prazo. Nós podemos salvar um programa ou arquivo em nosso disco rígido (ou SSD) para armazenamento a longo prazo, mas quando nós o abrimos, ele é primeiramente copiado para a RAM. Embora a RAM seja muito menor e temporária (até que a energia seja desligada), ela é muito mais rápida.
- Nós podemos pensar em bytes, armazenados na RAM, como se eles estivessem em uma grade:
- Na realidade, há milhões ou bilhões de bytes por chip.
- Em C, quando nós criamos uma variável do tipo
char
, que terá o tamanho de um byte, ela será fisicamente armazenada em uma daquelas caixinhas na RAM. Um inteiro, com 4 bytes, ocupará quatro daquelas caixinhas. - E cada uma dessas caixinhas é rotulada com algum número, ou endereço, de 0 a 1, a 2 e assim por diante.
Arrays
-
Vamos dizer que queremos armazenar três variáveis:
#include <stdio.h> int main(void) { char c1 = 'H'; char c2 = 'I'; char c3 = '!'; printf("%c %c %c\n", c1, c2, c3); }
- Observe que usamos aspas simples para indicar um caractere literal e aspas duplas para vários caracteres juntos em uma string.
- Podemos compilar e executar isso para ver
H I !
.
-
E sabemos que os caracteres são apenas números, portanto, se alterarmos nossa formatação de string para
printf("%i %i %i\n", c1, c2, c3);
, podemos ver os valores numéricos de cada char impresso:72 73 33
.- Podemos converter ou lançar explicitamente cada caractere em um int antes de usá-lo com
(int) c1
, mas nosso compilador pode fazer isso implicitamente para nós.
- Podemos converter ou lançar explicitamente cada caractere em um int antes de usá-lo com
- E na memória, podemos ter três caixas rotuladas como
c1
,c2
ec3
, cada uma representando um byte de binário com os valores de cada variável. -
Vamos dar uma olhada em
scores0.c
:#include <cs50.h> #include <stdio.h> int main(void) { // Pontuações int score1 = 72; int score2 = 73; int score3 = 33; // Imprimir média printf("Média: %i\n", (score1 + score2 + score3) / 3); }
- Podemos imprimir a média de três números, mas agora precisamos criar uma variável para cada escore que queremos incluir e não podemos usá-las facilmente mais tarde.
-
Acontece que, na memória, podemos armazenar variáveis umas após as outras, consecutivamente. E em C, uma lista de variáveis armazenadas uma após a outra em um pedaço contíguo de memória é chamada de array.
- Por exemplo, podemos usar
int scores[3];
para declarar um array de 3 inteiros. -
E podemos atribuir e usar variáveis em um array com:
#include <cs50.h> #include <stdio.h> int main(void) { // Pontuações int scores[3]; scores[0] = 72; scores[1] = 73; scores[2] = 33; // Imprimir média printf("Média: %i\n", (scores[0] + scores[1] + scores[2]) / 3); }
- Observe que os arrays são indexados por zero, o que significa que o primeiro elemento, ou valor, tem índice 0.
-
E repetimos o valor 3, representando o comprimento de nosso array, em dois lugares diferentes. Portanto, podemos usar uma constante ou valor fixo para indicar que ele sempre deve ser o mesmo nos dois lugares:
#include <cs50.h> #include <stdio.h> const int N = 3; int main(void) { // Pontuações int scores[N]; scores[0] = 72; scores[1] = 73; scores[2] = 33; // Imprimir média printf("Média: %i\n", (scores[0] + scores[1] + scores[2]) / N); }
- Podemos usar a palavra-chave
const
para dizer ao compilador que o valor deN
nunca deve ser alterado por nosso programa. E por convenção, colocaremos nossa declaração da variável fora da funçãomain
e colocaremos seu nome em maiúsculas, o que não é necessário para o compilador, mas mostra a outros humanos que esta variável é uma constante e torna fácil de ver desde o começo.
- Podemos usar a palavra-chave
-
Com um array, podemos coletar nossas pontuações em um loop e acessá-las mais tarde em um loop também:
#include <cs50.h> #include <stdio.h> float average(int length, int array[]); int main(void) { // Obter o número de pontuações int n = get_int("Pontuações: "); // Obter pontuações int scores[n]; for (int i = 0; i < n; i++) { scores[i] = get_int("Pontuação %i: ", i + 1); } // Imprimir média printf("Média: %.1f\n", average(n, scores)); } float average(int length, int array[]) { int sum = 0; for (int i = 0; i < length; i++) { sum += array[i]; } return (float) sum / (float) length; }
- Primeiro, pediremos ao usuário o número de pontuações que ele possui, criaremos um array com
ints
suficientes para o número de pontuações que ele possui e usaremos um loop para coletar todas as pontuações. - Em seguida, escreveremos uma função auxiliar,
average
, para retornar umfloat
ou um valor decimal. Vamos passar o comprimento e um array deints
(que pode ser de qualquer tamanho) e usar outro loop dentro de nossa função auxiliar para adicionar os valores em uma soma. Usamos(float)
para convertersum
elength
em floats, portanto, o resultado que obtemos ao dividir os dois também é um float. - Por fim, quando imprimimos o resultado que obtemos, usamos
%.1f
para mostrar apenas um lugar após a vírgula decimal.
- Primeiro, pediremos ao usuário o número de pontuações que ele possui, criaremos um array com
-
Na memória, nosso array é armazenado assim, onde cada valor ocupa não um, mas quatro bytes:
Strings
- Strings são na verdade apenas matrizes de caracteres. Se tivermos uma string
s
, cada caractere pode ser acessado coms[0]
,s[1]
e assim por diante. - E acontece que uma string termina com um caractere especial, '\0', ou um byte com todos os bits definidos como 0. Esse caractere é chamado de caractere nulo ou caractere de terminação nulo. Portanto, precisamos de quatro bytes para armazenar nossa string “HI!”:
-
Agora vamos ver como quatro strings em um array podem se parecer:
nomes de string[4]; names[0] = "EMMA"; names[1] = "RODRIGO"; names[2] = "BRIAN"; names[3] = "DAVID"; printf("%s\n", nomes[0]); printf("%c%c%c%c\n", nomes[0][0], nomes[0][1], nomes[0][2], nomes[0][3]);
- Podemos imprimir o primeiro valor em 'nomes' como uma string, ou podemos obter a primeira string e obter cada caractere individual naquela string usando '[]' novamente. (Podemos pensar nisso como '(names[0])[0]', embora não precisemos dos parênteses.)
- E embora saibamos que o primeiro nome tinha quatro caracteres,
printf
provavelmente usou um loop para olhar cada caractere na string, imprimindo-os um de cada vez até chegar ao caractere nulo que marca o final da string. E, de fato, podemos imprimirnames[0][4]
como umint
com%i
e ver um0
sendo impresso.
-
Podemos visualizar cada caractere com seu próprio rótulo na memória:
-
Podemos tentar fazer experiências com
string0.c
:#include <cs50.h> #include <stdio.h> #include <string.h> int main(void) { string s = get_string("Entrada: "); printf("Saída: "); para (int i = 0; i < strlen(s); i++) { printf("%c", s[i]); } printf("\n"); }
- Podemos usar a condição
s[i] != '\0'
, onde podemos verificar o caractere atual e imprimi-lo apenas se ele não for o caractere nulo. - Também podemos usar o comprimento da string, mas primeiro precisamos de uma nova biblioteca,
string.h
, parastrlen
, que nos diz o comprimento de uma string.
- Podemos usar a condição
-
Podemos melhorar o design do nosso programa.
string0
foi um pouco ineficiente, pois verificamos o comprimento da string, após cada caractere ser impresso, em nossa condição. Mas como o comprimento da string não muda, podemos verificar o comprimento da string uma vez:#include <cs50.h> #include <stdio.h> #include <string.h> int main(void) { string s = get_string("Entrada: "); printf("Saída:\n"); para (int i = 0, n = strlen(s); i < n; i++) { printf("%c\n", s[i]); } }
- Agora, no início do nosso loop, inicializamos uma variável
i
en
e lembre-se do comprimento de nossa string emn
. Então, podemos verificar os valores a cada vez, sem ter que realmente calcular o comprimento da string. - E precisamos usar um pouco mais de memória para
n
, mas isso economiza algum tempo ao não ter que verificar o comprimento da string a cada vez.
- Agora, no início do nosso loop, inicializamos uma variável
-
Agora podemos combinar o que vimos, para escrever um programa que pode colocar letras em maiúsculas:
# include <cs50.h> # include <stdio.h> # include <string.h> int main(void) { string s = get_string("Antes: "); printf("Depois: "); for (int i = 0, n = strlen(s); i < n; i++) { if (s[i] >= 'a' && s[i] <= 'z') { printf("%c", s[i] - 32); } else { printf("%c", s[i]); } } printf("\n"); }
- Primeiro, obtemos uma string
s
. Então, para cada caractere na string, se for minúscula (seu valor está entre o dea
ez
), nós a convertemos para maiúscula. De outra forma, nós apenas a imprimimos. - Nós podemos converter uma letra minúscula em sua equivalente maiúscula, subtraindo a diferença entre seus valores ASCII. (Nós sabemos que letras minúsculas têm um valor ASCII maior do que letras maiúsculas, e a diferença é convenientemente a mesma entre as mesmas letras, então podemos subtrair essa diferença para obter uma letra maiúscula de uma letra minúscula.)
- Primeiro, obtemos uma string
-
Nós podemos utilizar as páginas do manual, ou manual do programador, para encontrar funções de biblioteca que podemos utilizar para realizar a mesma coisa:
# include <cs50.h> # include <ctype.h> # include <stdio.h> # include <string.h> int main(void) { string s = get_string("Antes: "); printf("Depois: "); for (int i = 0, n = strlen(s); i < n; i++) { printf("%c", toupper(s[i])); } printf("\n"); }
- Ao pesquisar as páginas do manual, vemos
toupper()
é uma função, entre outras, de uma biblioteca chamadactype
, que podemos utilizar.
- Ao pesquisar as páginas do manual, vemos
Argumentos de linha de comando
- Usamos programas como
make
eclang
, que aceitam palavras extras após seu nome na linha de comando. Acontece que programas próprios também podem aceitar argumentos de linha de comando. -
Em
argv.c
, mudamos como nossa funçãomain
se parece:#include <cs50.h> #include <stdio.h> int main(int argc, string argv[]) { if (argc == 2) { printf("hello, %s\n", argv[1]); } else { printf("hello, world\n"); } }
argc
eargv
são duas variáveis que nossa funçãomain
agora receberá quando nosso programa for executado a partir da linha de comando.argc
é a contagem de argumentos, ou o número de argumentos, eargv
é um array de strings que são os argumentos. E o primeiro argumento,argv[0]
, é o nome do nosso programa (a primeira palavra digitada, como./hello
). Neste exemplo, verificamos se temos dois argumentos e imprimimos o segundo se for o caso.- Por exemplo, se executarmos
./argv David
, veremoshello, David
impresso, já que digitamosDavid
como a segunda palavra em nosso comando.
-
Acontece que podemos indicar erros em nosso programa retornando um valor da nossa função
main
(como implicado peloint
antes da nossa funçãomain
). Por padrão, nossa funçãomain
retorna0
para indicar que nada deu errado, mas podemos escrever um programa para retornar um valor diferente:#include <cs50.h> #include <stdio.h> int main(int argc, string argv[]) { if (argc != 2) { printf("missing command-line argument\n"); return 1; } printf("hello, %s\n", argv[1]); return 0; }
- O valor de retorno de
main
em nosso programa é chamado de código de saída.
- O valor de retorno de
-
À medida que escrevemos programas mais complexos, códigos de erro como este podem nos ajudar a determinar o que deu errado, mesmo que não seja visível ou significativo para o usuário.
Legibilidade
- Agora que sabemos como trabalhar com strings em nossos programas, podemos analisar parágrafos de texto quanto ao seu nível de legibilidade, com base em fatores como quão longas e complicadas as palavras e as frases são.
Criptografia
- Se quiséssemos enviar uma mensagem a alguém, podemos querer criptografar, ou seja, embaralhar essa mensagem de alguma forma para que fosse difícil para outras pessoas lerem. A mensagem original, ou entrada do nosso algoritmo, é chamada de texto não criptografado e a mensagem criptografada, ou saída, é chamada de texto criptografado.
- Uma mensagem como
OI!
poderia ser convertida em ASCII,72 73 33
. Mas qualquer pessoa poderia converter isso de volta em letras. - Um algoritmo de criptografia geralmente requer outra entrada, além do texto não criptografado. É necessário uma chave e, às vezes, é simplesmente um número mantido em segredo. Com a chave, o texto não criptografado pode ser convertido, por meio de algum algoritmo, em texto criptografado e vice-versa.
- Por exemplo, se quiséssemos enviar uma mensagem como
EU AMO VOCÊ
, poderíamos primeiro convertê-la em ASCII:73 76 79 86 69 89 79 85
. Em seguida, podemos criptografá-la com uma chave de apenas1
e um algoritmo simples, no qual apenas adicionamos a chave a cada valor:74 77 80 87 70 90 80 86
. Então, alguém que converta esse ASCII de volta em texto veráJM PWF ZPV
. Para descriptografar isso, alguém precisará saber a chave. - Vamos aplicar esses conceitos em nosso conjunto de problemas!