Tarefa 0 - Introdução à linguagem C

C para programadores de Python

por Carl Burch, Hendrix College, agosto de 2011
traduzido para a disciplina de MC202, junho de 2018

CC

C para programadores de Python por Carl Burch está licenciado sob uma Licença Creative Commons Atribuição-Compartilha Igual 3.0 dos Estados Unidos .

Baseado em um trabalho em www.toves.org/books/cpy/ .

Conteúdo

Nos anos setenta, nos laboratórios da Bell, Ken Thompson projetou a linguagem de programação C para ajudar no desenvolvimento do sistema operacional UNIX. Seguindo a uma série de eventos históricos, poucos intencionais, o UNIX cresceu de um pequeno projeto de pesquisa para um sistema operacional popular de força industrial. E, junto com o sucesso do UNIX, veio a linguagem C, já que o sistema operacional foi projetado para que os programas C pudessem acessar todos os seus recursos. À medida em que mais programadores adquiriram experiência com C, eles começaram a usá-lo também em outras plataformas, de modo que ele se tornou uma das principais linguagens para o desenvolvimento de software no final dos anos 80.

Enquanto hoje C não desfruta do amplo domínio que já teve, sua influência é tão grande que muitas outras linguagens foram projetadas para se parecer com ele, incluindo C++, C#, Objective-C, Java, JavaScript, PHP e Perl. Saber C é em si uma coisa boa — é um excelente ponto de partida para se relacionar mais diretamente com o que um computador faz. Mas aprender C também é um bom ponto de partida para se familiarizar com todos esses outros idiomas.

Este documento é direcionado a pessoas que aprenderam programação em Python e que desejam aprender sobre C. “A influência de C em Python é considerável”, nas palavras do inventor de Python, Guido van Rossum (“Uma Introdução a Python para Programadores UNIX/C”, 1993). Então, aprender Python é um bom primeiro passo para aprender C.

1. Construindo um programa simples

Começaremos com vários princípios gerais, trabalhando para construir um programa em C completo — mas limitado — até o final da Seção 1.

1.1. Compiladores versus interpretadores

Uma grande diferença entre C e Python é simplesmente como você executa programas escritos nos dois idiomas. Com programas em C, você geralmente usa um compilador quando está pronto para que um programa em C execute. Em contraste, com Python, você normalmente usa um interpretador. Um compilador gera um arquivo contendo a tradução do programa no código nativo da máquina. O compilador na verdade não executa o programa; em vez disso, você primeiro executa o compilador para criar um executável nativo e, em seguida, executa o executável gerado. Assim, depois de escrever um programa em C, executá-lo é um processo de duas etapas.

me@computer:~$ gcc meu_programa.c
me@computer:~$ ./a.out
GCD: 8

No primeiro comando (gcc meu_programa.c), invocamos o compilador, chamado gcc . O compilador lê o arquivo meu_programa.c, no qual salvamos nosso código C e gera um novo arquivo chamado a.out contendo uma tradução desse código no código binário usado pela máquina. No segundo comando (./a.out), dizemos ao computador para executar este código binário. Quando está executando o programa, o computador não tem idéia de que a.out acabou de ser criado a partir de algum programa em C: ele simplesmente executa cegamente o código encontrado no arquivo a.out, da mesma forma que executa cegamente o código encontrado no arquivo gcc arquivo em resposta ao primeiro comando.

Em contraste, um interpretador lê o programa escrito pelo usuário e o executa diretamente. Isso remove uma etapa do processo de execução, mas um compilador tem a vantagem de gerar um executável que pode ser executado da mesma forma que a maioria dos outros aplicativos da máquina e isso pode levar a tempos de execução consideravelmente mais rápidos.

Ser compilada ou interpretada tem algumas consequências importantes no projeto de uma linguagem de programação. C é projetado de tal forma que o compilador possa deduzir tudo aquilo de que precisa para traduzir o programa C sem realmente executar o programa.

1.2. Declarações de Variáveis

Entre as informações de que C precisa que os programadores forneçam ao compilador, um dos exemplos mais importantes ​​é que C requer declarações de variáveis, informando para o compilador cada uma das variáveis antes que elas sejam realmente usadas. Isso é típico de muitas linguagens de programação proeminentes, particularmente entre aquelas que devem ser compiladas antes de serem executadas.

Em C, a declaração da variável define o tipo da variável, especificando se ela é um número inteiro (int), um número de ponto flutuante (double), um caractere (char) ou algum outro tipo que estudaremos mais tarde. Uma vez que você declara que uma variável é de um tipo particular, você não pode mudar seu tipo: se a variável x é declarada como do tipo double e você realiza a atribuição x = 3, então x irá guardar o valor de ponto flutuante 3.0 ao invés do inteiro 3. Você pode, se desejar, imaginar que a variável x é uma caixa que é capaz de guardar apenas valores double; se você tentar colocar algo diferente (como o valor inteiro 3), o compilador converterá esse valor em um valor double de forma com que ele caiba na caixa.

Declarar uma variável é bastante simples: você insere o tipo da variável, alguns espaços em branco, o nome da variável e um ponto e vírgula:

double x

Em C, as declarações de variáveis devem ficar no topo da função em que são usadas.

Se você se esquecer de declarar uma variável, o compilador se recusará a compilar o programa, i.e., ele não vai compilar um programa em que uma variável é usada, mas não é declarada. A mensagem de erro indicará a linha dentro do programa, o nome da variável e uma mensagem como “símbolo não declarado”.

Para um programador Python, parece difícil incluir essas declarações de variáveis ​​em um programa, embora isso fique mais fácil com mais prática. Os programadores de C tendem a sentir que as declarações de variáveis compensam esse pequeno trabalho extra. A grande vantagem é que o compilador irá identificar automaticamente sempre que um nome de variável for digitado incorretamente e apontar diretamente para a linha onde o nome está escrito incorretamente. Isso é muito mais conveniente do que executar um programa e descobrir que ele está errado porque em algum lugar o nome da variável contém erros ortográficos.

1.3. Espaço em branco

Em Python, os espaços em branco como tabs e quebras de linhas são importantes: você separa suas instruções colocando-as em linhas separadas e indica a extensão de um bloco (como o corpo de uma instrução while ou if) usando o recuo. Esses usos do espaço em branco são idiossincrasias de Python. (Na verdade, não são muitas as linguagens que usam espaços para separar blocos.)

Como a maioria das linguagens de programação, C não usa espaço em branco, exceto para separar palavras. A maioria das instruções é terminada com um ponto e vírgula ; e blocos de instruções são indicados usando um par de chaves, { e }. Aqui está um fragmento de exemplo de um programa em C, com seu equivalente em Python.

Figura 1: fragmento C e equivalente em Python

Fragmento em C

disc = b * b - 4 * a * c;
if (disc < 0) {
    num_sol = 0;
} else {
    t0 = -b / a;
    if (disc == 0) {
        num_sol = 1;
        sol0 = t0 / 2;
    } else {
        num_sol = 2;
        t1 = sqrt(disc) / a;
        sol0 = (t0 + t1) / 2;
        sol1 = (t0 - t1) / 2;
    }
}

Equivalente em Python

disc = b * b - 4 * a * c
if disc < 0:
    num_sol = 0
else:
    t0 = -b / a
    if disc == 0:
        num_sol = 1
        sol0 = t0 / 2
    else:
        num_sol = 2
        t1 = disc ** 0.5 / a
        sol0 = (t0 + t1) / 2
        sol1 = (t0 - t1) / 2

O programa C à esquerda é como eu o escreveria. No entanto, o espaço em branco é insignificante, então o computador ficaria tão feliz quanto se eu tivesse escrito o seguinte.

disc=b*b-4*a*c;if(disc<0){
num_sol=0;}else{t0=-b/a;if(
disc==0){num_sol=1;sol0=t0/2
;}else{num_sol=2;t1=sqrt(disc/a;
sol0=(t0+t1)/2;sol1=(t0-t1)/2;}}

Enquanto o computador pode estar feliz com isso, nenhum humano sensato preferiria essa versão. Portanto, um programador competente tende a ser muito cuidadoso com os espaços em branco para indicar a estrutura do programa.

(Há algumas exceções à regra de ignorar o espaço em branco: o espaço é frequentemente importante para separar palavras de símbolos. O fragmento intmain é diferente do fragmento int main; da mesma forma, o fragmento a++ + 1 é diferente do fragmento a+ + +1.)

1.4. A função printf()

Um ingrediente importante para escrever programas C úteis é exibir os resultados para o usuário ver, o que você realizaria usando print em Python. Em C, você usa em printf(). Essa é na verdade uma função — que uma das funções mais úteis da biblioteca padrão de funções da linguagem C.

A maneira com que os parâmetros de printf() funcionam é um pouco complicada, mas é também bastante conveniente: o primeiro parâmetro é uma string que especifica o formato do texto que se deseja imprimir e os seguintes parâmetros indicam os valores a serem impressos. A maneira mais fácil de entender isso é olhar para um exemplo.

printf("# solns: %d\n", num_sol);

Esta linha diz para imprimir usando "#solns: %d\n" como a string de formatação. A função printf() percorre essa string de formato, imprimindo os caracteres "# solns: " antes de chegar ao caractere de porcentagem '%'. O caractere de porcentagem é considerado especial para a função printf(): ele diz para imprimir um valor especificado em um parâmetro subsequente. Nesse caso, uma letra minúscula d acompanha o caractere de porcentagem, indicando para exibir o parâmetro do tipo int na forma decimal. (O d representa decimal.) Então, quando printf() atinge "%d", ele olha para o valor do seguinte parâmetro (vamos imaginar num_sol vale 2 neste exemplo) e exibe esse valor. Em seguida, continua pela string de formatação, neste caso, exibindo um caractere de quebra de linha. Portanto, o usuário vê a seguinte linha de saída:

# solns: 2

Como Python, C permite incluir caracteres de escape em uma string usando uma barra invertida. A sequência '\n' representa o caractere de quebra de linha — isto é, o caractere que representa uma quebra de linha. Da mesma forma, '\t' representa o caractere de tabulação, '\"' representa o caractere de aspas duplas e '\\' representa o caractere de barra invertida. Esses caracteres de escape fazem parte da sintaxe C e não fazem parte da função printf(). (Isso é, a string que a printf() função recebe na verdade contém uma quebra de linha, não uma barra invertida seguida por um n. Assim, a natureza da barra invertida é fundamentalmente diferente do caractere de porcentagem, que printf() veria e interpretaria em tempo de execução.)

Vamos ver outro exemplo.

printf("# solns: %d", num_sol);
printf("solns: %f, %f", sol0, sol1);

Vamos supor que num_sol tenha valor 2, sol0 tenha valor 4 e sol1 tenha valor 1. Quando o computador executar essas duas chamadas à função printf(), ele primeiro executará a primeira linha, que exibe # solns: 2 e, em seguida, a segunda, que exibe solns: 4.0, 1.0, conforme ilustrado abaixo.

# solns: 2solns: 4,0, 1,0

Observe que printf() exibe apenas o que é dito a ele, sem adicionar espaços extras ou quebras de linha. Se quisermos que uma quebra de linha seja inserida entre as duas partes da saída, precisaríamos incluir \n no final da primeira string de formatação.

A segunda chamada printf() neste exemplo ilustra como a função pode imprimir vários valores de parâmetro. Na verdade, neste caso não há nenhuma razão para não termos combinado as duas chamadas a printf() em uma.

printf("# solns: %dsolns: %f, %f",
    num_sol, sol0, sol1);

Aliás, a função printf() exibe 4.0 ao invés de simplesmente 4, porque a string de formato usa %f, que diz para interpretar os parâmetros como números de ponto flutuante. Se você quiser exibir apenas 4, você pode se sentir tentado a usar “%d. Mas isso não funcionaria, porque printf() iria interpretar a representação binária usada para um número de ponto flutuante como uma representação binária para um número inteiro, e esses tipos são armazenados de maneiras completamente diferentes! No meu computador, substituir cada %f por %d leva à seguinte saída:

# solns: 2solns: 0, 1074790400

Há diversos caracteres que podem seguir o caractere de porcentagem na string de formatação.

Você também pode incluir um número entre o caractere de porcentagem e o descritor de formato como em %10d, que informa printf() para justificar à direita um número inteiro decimal em dez colunas.

1.5. Funções

Ao contrário de Python, todo o código em C deve estar aninhado dentro das funções e as funções não podem ser aninhadas umas nas outras. Assim, a estrutura geral de um programa em C é tipicamente muito direta: é uma lista de definições de funções, uma após a outra, cada uma contendo uma lista de instruções a serem executadas quando a função é chamada.

Aqui está um exemplo simples de uma definição de função:

double expon(double b, int e) {
    if (e == 0) {
        return 1.0;
    } else {
        return b * expon(b, e - 1);
    }
}

Uma função em C é definida nomeando o tipo de retorno (double neste exemplo, porque a função devolve um resultado de tipo ponto flutuante), seguido pelo nome da função (expon), seguido por um par de parênteses listando os parâmetros. Cada parâmetro é descrito incluindo o tipo do parâmetro e o nome do parâmetro. Depois da lista de parâmetros entre parênteses, deve vir um par de chaves, no qual você aninha o corpo da função.

Se você tiver uma função que não tenha nenhum valor de retorno, você deve escrever void no lugar do tipo de retorno.

Programas têm uma função especial chamada main, cujo tipo de retorno é um inteiro. Esta função é o “ponto de partida” do programa: o computador chama essencialmente a função main do programa quando quiser executar o programa. O valor de retorno inteiro é pode ser ignorado em grande parte das vezes; assim, sempre retornaremos 0 ao invés de nos preocuparmos sobre como o valor de retorno pode ser usado.

Estamos agora em posição de apresentar um programa C completo, juntamente com seu equivalente em Python.

Figura 2: Um programa C completo e um equivalente em Python

Programa em C

int gcd(int a, int b) {
  if (b == 0) {
    return a;
  } else {
    return gcd(b, a % b);
  }
}

int main() {
  printf("GCD: %d\n",
    gcd(24, 40));
  return 0;
}

Programa em Python

def gcd(a, b):
  if b == 0:
    return a
  else:
    return gcd(b, a % b)

print("GCD: " + str(gcd(24, 40)))

Como você pode ver, o programa C consiste em duas definições de função. Em contraste com o programa Python, onde a linha que contém print existe fora de qualquer definição de função, o programa C requer printf() esteja na função main do programa, já que é onde colocamos todo o código de nível superior que deve ser concluído quando o programa é executado.

2. Construções de nível de instrução

Agora que vimos como construir um programa completo, vamos aprender o universo do que podemos fazer dentro de uma função C, através das diversas formas de escrever instruções na linguagem de programação C.

2.1. Operadores

Um operador é algo que podemos usar em expressões aritméticas, como um sinal de mais ‘+’ ou um teste de igualdade ‘==’. Os operadores em C parecerão familiares, uma vez que o criador de Python, Guido van Rossum, escolheu começar a partir da lista de operadores de C; mas existem algumas diferenças importantes.

Figura 3: Principais operadores em C e Python

Precedência de operador em C

++ -- (postfix)
+ - ! (operadores unários)
* / %
+ - (operadores unários)
< > <= >=
== !=
&&
||
= += -= *= /= %=

Precedência de operador em Python

**
+ - (operadores unários)
* / % //
+ - (operadores binários)
< > <= >= == !=
not
and
or

Algumas distinções importantes:

2.2. Tipos básicos

A lista de tipos básicos de C é bastante restrita.

int para um inteiro
char para um único caractere
float para um número de ponto flutuante de precisão simples
double para um número de ponto flutuante de precisão dupla

O tipo int é direto. Você também pode criar outros tipos de inteiros, usando os tipos long e short. Um variável long reserva pelo menos tantos bits quanto um tipo int, enquanto uma variável short reserva menos bits do que um int (ou o mesmo número). A linguagem não garante o número de bits para cada um, mas a maioria dos compiladores atuais usa 32 bits para um int, o que permite números de até 2,15 × 109 . Isso é suficiente para a maioria dos propósitos e, de todo modo, muitos compiladores também usam 32 bits para long. Assim, tipicamente as pessoas usam int em seus programas.

O tipo char também é simples: representa um único caractere, como um símbolo de letra ou pontuação. Você pode representar um caractere individual em um programa colocando o caractere entre aspas simples: digit0 = '0'; guardaria o caractere de dígito zero na variável variável digit0 de tipo char.

Entre os dois tipos de ponto flutuante existentes, float e double, a maioria dos programadores de hoje utilizam quase que exclusivamente o tipo double. Esses tipos são para números que podem ter valores decimais neles, como 2,5, ou para números maiores que um int pode conter, como 6,02 × 1023 . Os dois tipos diferem no fato de que um float normalmente utiliza apenas 32 bits de armazenamento, enquanto double normalmente utiliza 64 bits. A técnica de armazenamento de 32 bits permite um intervalo de números mais estreito (de −3,4 × 1038 a 3,4 × 1038 ) e — mais problemático numericamente — com cerca de 7 dígitos significativos. Assim, uma variável do tipo float não pode guardar precisamente o número 281.421.906 (que foi a população dos EUA em 2000 segundo o censo), porque representar esse número requer nove dígitos significativos; se atribuíssemos esse número a uma variável float, obteríamos apenas uma aproximação, como 281,421,920. Por outro lado, a técnica de armazenamento de 64 bits permite uma faixa mais ampla de números (−1,7 × 10308 a 1,7 × 10308 ) e aproximadamente 15 dígitos significativos. Isso é mais adequado para situações diversas e o gasto extra de 32 bits de armazenamento raramente é relevante, portanto utilizar double é quase sempre preferível.

C não possui um tipo booleano para representar valores verdadeiro/falso. Isso tem implicações importantes para uma instrução como if, onde você precisa de um teste para determinar se deve executar um grupo de instruções. A abordagem utilizada em C é tratar o inteiro 0 como falso e todos os outros valores inteiros como verdadeiros. O seguinte seria um programa C válido.

int main() {
    int i = 5;
    if (i) {
        printf("in if\n");
    } else {
        printf("in else\n");
    }
    return 0;
}

Este programa compilaria e imprimiria in if quando executado, já que o valor da expressão i vale 5, o que é diferente de 0 e, portanto, o teste da instrução if é bem-sucedida.

Operadores em C cujos resultados devem ser valores booleanos (como ==, && e ||), na verdade, calculam valores int. Em particular, esses operadores devolvem 1 para representar verdadeiro e 0 para representar falso.

Esta peculiaridade — de que C considera todos os números inteiros diferentes de zero como valor verdadeiro — é geralmente considerada um erro. Assim, a maioria dos programadores experientes nunca realizam operações aritméticas (soma, subtração, etc.) com o resultado de expressões condicionais (operações booleanas, comparações, etc.)

2.3. Chaves

Várias instruções, como a instrução if, incluem um corpo que pode conter várias outras instruções. Normalmente, o corpo é cercado por chaves (’{’ e ‘}’) para indicar sua extensão. Mas quando o corpo contém apenas uma instrução, as chaves são opcionais. Assim, poderíamos encontrar o máximo de dois números sem usar chaves, desde que o corpos tanto de if quanto de else contenham apenas uma única instrução, cada.

if (first > second)
    max = first;
else
    max = second;

(Também poderíamos incluir chaves em apenas um dos dois corpos, contanto que esse corpo contenha apenas uma instrução.)

Os programadores C usam isso com bastante frequência quando querem que um de vários teste de if seja executado. Um exemplo disso foi dado no código da fórmula quadrática acima. Podemos calcular o número de soluções da seguinte forma:

disc = b * b - 4 * a * c;
if (disc < 0) {
    num_sol = 0;
} else {
    if (disc == 0) {
        num_sol = 1;
    } else {
        num_sol = 2;
    }
}

Observe que a cláusula else aqui contém apenas uma instrução (uma instrução if...else), então podemos omitir as chaves ao redor dela. Podemos então escrever:

disc = b * b - 4 * a * c;
if (disc < 0) {
    num_sol = 0;
} else
    if (disc == 0) {
        num_sol = 1;
    } else {
        num_sol = 2;
    }

Mas essa situação surge com freqüência suficiente para que os programadores C sigam uma regra especial para recuar nesse caso — uma regra que permite que todos os casos sejam escritos no mesmo nível de recuo.

disc = b * b - 4 * a * c;
if (disc < 0) {
    num_sol = 0;
} else if (disc == 0) {
    num_sol = 1;
} else {
    num_sol = 2;
}

Como isso é viável usando as regras de chaveamento de C, C não inclui um paralelo da cláusula elif que você encontra em Python. Você pode juntar quantas combinações else if desejar.

Além desta situação em particular, eu recomendo que você sempre inclua as chaves, mesmo que só haja uma instrução. À medida em que você continua trabalhando em um programa, geralmente descobre que deseja incluir instruções adicionais no corpo de um if e ter as chaves lá já lhe poupa o trabalho de adicioná-las mais tarde. E torna mais fácil manter o controle das chaves, já que cada nível de indentação requer uma chave direita de fechamento.

2.4. Instruções

Vimos quatro tipos de instruções, três das quais correspondem de perto com as de Python.

  1. int x;

    Nós já discutimos declarações de variáveis ​​na Seção 1.2 . Elas não têm paralelo em Python.

  2. x = y + z; ou printf("%d", x);

    Você pode ter uma expressão como uma instrução. Tecnicamente, a expressão poderia ser x + 3, mas tal instrução não serve para nada: pedimos ao computador para adicionar x e 3, mas não pedimos que nada aconteça com esse resultado. Quase sempre, as expressões têm uma das duas formas acima: uma forma é um operador que altera o valor de uma variável, como o operador de atribuição (x = 3;), o operador de atribuição de adição += ou o operador de incremento ++. A outra forma de expressão que você vê como uma instrução é uma chamada de função, como uma instrução que simplesmente chama a função printf().

  3. if (x < 0) { printf("negative"); }

    Você pode ter uma instrução if, que funciona de maneira muito semelhante à instrução if de Python. A única grande diferença é a sintaxe: em C, a condição if de uma instrução deve estar entre parênteses, não há dois pontos após a condição e o corpo tem um par de chaves que a encerra.

    Como já vimos, C não possui uma cláusula elif como em Python; em vez disso, os programadores C usam a regra de chave opcional e escrevem else if.

  4. return 0;

    Você pode ter uma instrução return para sair de uma função com um determinado valor de retorno. Ou, para uma função sem valor de retorno (a que tem tipo de retorno void), você escreveria simplesmente return;.

Existem mais três tipos de instruções que se correlacionam de perto com os equivalentes de Python.

  1. while (i >= 0) { i--; }

    A instrução while funciona de maneira idêntica à de Python, embora a sintaxe seja diferente da mesma maneira que a sintaxe if é diferente.

    while (i >= 0) {
        printf("%d\n", i);
        i--;
    }
    

    Novamente, a expressão de teste requer um par de parênteses em torno dela, não há dois pontos e usamos chaves para cercar o corpo do laço.

  2. break;

    Como em Python, a instrução break sai imediatamente do laço mais interno em que ela é encontrada. Claro, a instrução tem um ponto e vírgula a seguir.

  3. continue;

    Também como em Python, a instrução continue pula para o final do laço mais interno em que é encontrado e testa se deve repetir o laço novamente. Ele tem um ponto-e-vírgula também.

E existem dois tipos de instruções que não possuem um bom paralelo em Python.

  1. for (i = 0; i < 10; i++) {...

    Embora Python também tenha uma instrução for, seu propósito e sua sintaxe têm pouca semelhança com a instrução for de C. Em C, a palavra-chave for é seguida por um par de parênteses que contém três partes separadas por ponto e vírgula.

    for (inicialização; teste; atualização)
    

    A intenção do laço for de C é permitir passar uma variável por uma série de números, como contar de 0 a 9. A parte antes do primeiro ponto e vírgula (inicialização) é executada assim que a instrução for é atingida; é para inicializar a variável que contará. A parte entre os dois pontos e vírgulas (teste) é avaliada antes de cada iteração para determinar se a iteração deve ser repetida. E a parte seguinte ao último ponto e vírgula (atualização) é avaliada no final de cada iteração para atualizar a variável de contagem para a iteração a seguir.

    Na prática, os laços for são usados ​​com mais freqüência para contar n iterações. O idioma padrão para isso é o seguinte.

    for (i = 0; i < n; i++) {
        corpo
    }
    

    Aqui temos uma variável de contador i cujo valor começa em 0. Com cada iteração, testamos se i alcança n ou não; e, se não, então nós executamos o corpo for da instrução e então executamos a atualização i++ de forma que i vá para o inteiro seguinte. O resultado é que o corpo é executado para cada valor de i, começando em 0 até n - 1.

    Mas você também pode usar um laço for para outros propósitos. No exemplo a seguir, exibimos as potências de 2 até 512. Observe como a parte de atualização da instrução for foi alterada para p *= 2.

    for (p = 1; p <= 512; p *= 2) {
        printf("%d\n", p);
    }
    
  2. switch (grade) { case 'A':...

    A instrução switch não tem nenhum equivalente em Python, mas é essencialmente equivalente a uma forma particular de uma instrução da forma if... elif... elif... else onde cada um dos testes são feitos para diferentes valores de uma mesma variável.

    Uma instrução switch é útil quando você tem vários blocos de código possíveis, um dos quais deve ser executado com base no valor de uma expressão específica. Aqui está um exemplo clássico de instrução switch:

    switch (letter_grade) {
    case 'A':
        gpa += 4;
        credits += 1;
        break;
    case 'B':
        gpa += 3;
        credits += 1;
        break;
    case 'C':
        gpa += 2;
        credits += 1;
        break;
    case 'D':
        gpa += 1;
        credits += 1;
        break;
    case 'W':
        break;
    default:
        credits += 1;
    }
    

    Dentro dos parênteses após a palavra-chave switch, temos uma expressão cujo valor deve ser um caractere ou inteiro. O computador avalia essa expressão e desce para uma das palavras-chaves case dependente do valor dessa expressão. Se o valor é o caractere A, então o primeiro bloco é executado (gpa += 4; credits += 1;); se for B , então o segundo bloco é executado; etc. Se o valor não for nenhum dos caracteres (como um F), então o bloco após a palavra-chave default é executado.

    A instrução break no final de cada bloco é um detalhe crucial: se a instrução break for omitida, o computador continuará no seguinte bloco. Em nosso exemplo acima, se omitimos todas as instruções break, uma nota A levaria o computador a executar não apenas o caso A, mas também o B, C, D, W e caso default. O resultado seria que gpa aumentaria em 4 + 3 + 2 + 1 = 10 , enquanto credits aumentaria em 5. Ocasionalmente, você realmente deseja que o computador continue para o próximo caso (chamado de “fall-through”) e assim você omite uma instrução break; mas na prática você quase sempre quer uma instrução break no final de cada caso.

    Há uma exceção importante em que fall-through é bastante comum: às vezes, você deseja que o mesmo código seja aplicado a dois valores diferentes. Por exemplo, se quiséssemos que nada acontecesse se a nota fosse P ou W , então poderíamos incluir case 'P': um pouco antes de case 'W':, sem nenhum código intermediário.

2.5. Vetores

Python suporta muitos tipos que combinam os tipos atômicos básicos em um grupo: tuplas, listas, cadeias de caracteres, dicionários, conjuntos.

O suporte de C é muito mais rudimentar: O único tipo composto é o vetor, que é semelhante à lista de Python, exceto que um vetor em C não pode crescer ou encolher — seu tamanho é fixado no momento da criação. Você pode declarar e acessar um vetor da seguinte maneira.

double pops[50];
pops[0] = 897934;
pops[1] = pops[0] + 11804445;

Neste exemplo, criamos um vetor contendo 50 variáveis do tipo double. As variáveis são indexados de 0 a 49.

C não tem suporte para acessar o tamanho de um vetor depois de criado; ou seja, não há nada análogo ao len(pops) de Python ou ao pops.length de Java.

Um ponto importante em relação aos vetores: o que acontece se você acessar um índice de vetor fora do vetor, como acessar pops[50] ou pops[-100]? Com Python ou Java, isso terminará o programa com uma mensagem amigável apontando para a linha com a falha e dizendo que o programa foi além dos limites do vetor. C não é tão amigável. Quando você acessa além dos limites de um vetor, ele o faz cegamente.

Isso pode levar a um comportamento peculiar. Por exemplo, considere o programa a seguir.

int main() {
    int i;
    int vals[5];

    for (i = 0; i <= 5; i++) {
        vals[i] = 0;
    }
    printf("%d\n", i);
    return 0;
}

Alguns sistemas (incluindo uma instalação do Linux que encontrei) guardariam i na memória logo após o vetor vals; assim, quando i atinge 5 e o computador executa vals[i] = 0, ele de fato redefine a memória correspondente a i para 0. Como resultado, o laço for foi reinicializado e o programa passa pelo laço novamente, e novamente, repetidamente. O programa nunca alcança a chamada de função printf e o programa nunca termina.

Em programas mais complicados, a falta de checagem de limites de vetor pode levar a bugs muito difíceis, onde o valor de uma variável muda misteriosamente em centenas de funções e você como programador deve determinar onde um índice do vetor foi acessado fora dos limites. Este é o tipo de bug que leva muito tempo para descobrir e reparar.

É por isso que você deve considerar as mensagens de erro fornecidas por Python (ou Java) como extraordinariamente amigáveis: não apenas informa a causa de um problema, mas também informa exatamente qual linha do programa estava com defeito. Isso economiza muito tempo de depuração.

De vez em quando, você verá uma falha no programa em C, com uma mensagem como “falha de segmentação” ou “erro de barramento”. Não será útil incluir qualquer indicação de qual parte do programa é a culpa: tudo que você consegue são aquelas essas duas palavras. Esses erros geralmente significam que o programa tentou acessar um local de memória inválido. Isso pode indicar uma tentativa de acessar um índice de vetor inválido, mas normalmente o índice precisa estar bem fora dos limites para que isso ocorra. (Em geral, isso indica uma tentativa de fazer referência a um ponteiro não inicializado ou a um ponteiro NULL, que discutiremos mais adiante.)

2.6. Comentários

No projeto original de C, todos os comentários começam com uma barra seguida por um asterisco (/*) e terminam com um asterisco seguido por uma barra (*/). O comentário pode abranger várias linhas.

/* gcd - returns the greatest common
 * divisor of its two parameters */
int gcd(int a, int b) {

(O asterisco na segunda linha é ignorado pelo compilador. No entanto, a maioria dos programadores o incluiria, porque parece mais bonito e também porque indica a um leitor humano que o comentário está sendo continuado a partir da linha anterior.)

Embora este comentário em múltiplas linhas tenha sido o único comentário incluído originalmente em C, o C++ introduziu um comentário em uma única linha que provou ser tão útil que a maioria dos compiladores C atuais também o suportam. Começa com dois caracteres de barra (//) e vai para o final da linha.

int gcd(int a, int b) {
  if (b == 0) {
    return a;
  } else {
    // recurse if b != 0
    return gcd(b, a % b);
  }
}

3. Bibliotecas

Tendo discutido as funções internas, agora nos voltamos para discutir questões relacionadas a funções e separar um programa em vários arquivos.

3.1. Protótipos de função

Em C, uma função deve ser declarada acima do local onde você a usa. No programa C da Figura 2, definimos a função gcd() primeiro e depois a função main(). Isso é significativo: se trocássemos as funções gcd() e main(), o compilador iria reclamar em main() que a função gcd() não está declarada. Isso é porque C presume que um compilador lê um programa de cima para baixo: no momento em que chega a main(), ele não foi informado sobre uma função gcd() e, portanto, acredita que não existe tal função.

Isso gera um problema, especialmente em programas maiores que abrangem vários arquivos, em que as funções em um arquivo precisarão chamar funções em outro. Para contornar esse problema, C fornece a noção de um protótipo de função, onde escrevemos o cabeçalho da função mas omitimos a definição do corpo.

Por exemplo, digamos que queremos quebrar nosso programa C em dois arquivos: o primeiro arquivo, math.c, conterá a função gcd() e o segundo arquivo, main.c, conterá a função main(). O problema com isso é que, quando compilarmos main.c, o compilador não saberá sobre a função gcd() que está tentando chamar.

Uma solução é incluir um protótipo de função em main.c.

int gcd(int a, int b);

int main() {
    printf("GCD: %d\n",
        gcd(24, 40));
    return 0;
}

A linha int gcd... é o protótipo da função. Você pode ver que ela começa da mesma forma que uma definição de função começa, mas nós simplesmente colocamos um ponto-e-vírgula onde o corpo da função normalmente estaria. Ao fazer isso, estamos declarando que a função será eventualmente definida, mas ainda não a definimos. O compilador aceita isso e obedientemente compila o programa sem reclamações.

3.2. Arquivos de cabeçalho

Programas maiores que abrangem vários arquivos freqüentemente contêm muitas funções que são usadas muitas vezes em muitos arquivos diferentes. Seria doloroso repetir cada protótipo de função em todos os arquivos que usam a função. Então, criamos um arquivo — chamado de arquivo de cabeçalho — que contém cada protótipo escrito apenas uma vez (e possivelmente algumas informações compartilhadas adicionais) e então podemos nos referir a esse arquivo de cabeçalho em cada código-fonte que deseja os protótipos. O arquivo de protótipos é chamado de arquivo de cabeçalho, pois contém as “cabeças” de várias funções. Convencionalmente, os arquivos de cabeçalho usam o prefixo .h, ao invés do prefixo .c usado para os códigos-fontes C.

Por exemplo, poderíamos colocar o protótipo para a nossa função gcd() em um arquivo de cabeçalho chamado math.h.

int gcd(int a, int b);

Podemos usar um tipo especial de linha começando com #include para incorporar esse arquivo de cabeçalho no topo da main.c.

#include <stdio.h>
#include "math.h"

int main() {
    printf("GCD: %d\n",
        gcd(24, 40));
    return 0;
}

Este exemplo em particular não é muito convincente, mas imagine uma biblioteca que consiste em dezenas de funções, que são usadas em dezenas de arquivos: de repente, a economia de tempo de ter apenas um único protótipo para cada função em um arquivo de cabeçalho começa a fazer sentido.

A linha #include é um exemplo de uma diretiva para o pré-processador de C, para o qual o compilador C envia cada programa antes de compilá-lo. Um programa pode conter comandos (diretivas) informando ao pré-processador para manipular o texto do programa que o compilador realmente processa. A diretiva #include informa ao pré-processador para substituir a linha #include pelo conteúdo do arquivo especificado.

Você deve ter notado que colocamos stdio.h entre colchetes angulares, enquanto math.h está entre aspas duplas. Os colchetes angulares são para arquivos de cabeçalho padrão — arquivos especificados junto com a especificação da linguagem C. As aspas são para arquivos de cabeçalho personalizados que podem ser encontrados no mesmo diretório dos códigos-fontes.

3.3. Constantes

Outra diretiva de pré-processador particularmente útil é a diretiva #define. Ela diz ao pré-processador para substituir todas as ocorrências futuras de algum palavra por outra.

#define PI 3.14159

Neste fragmento, dizemos ao pré-processador que, para o resto do programa, ele deveria substituir cada ocorrência de PI por 3.14159. Suponha que mais tarde no programa exista a seguinte linha:

printf("area: %f\n", PI * r * r);

Vendo isso, o pré-processador iria traduzi-lo no seguinte texto para o compilador C processar:

printf("area: %f\n", 3.14159 * r * r);

Essa substituição acontece nos bastidores, para que o programador não veja a substituição.

Estes são os princípios básicos de escrever programas em C, dando-lhe o suficiente para poder escrever programas razoavelmente úteis. Mas, para ser um programador proficiente em C, você precisaria saber sobre ponteiros — um tópico que adiaremos a outro momento.