Prefácio

Motivação

Muitas pessoas que buscam aprender programação em C no Brasil percebem uma escassez de material nacional de qualidade sobre a linguagem. Eu anseio pelo fim desse problema, e este livro é uma forma de contribuir para isso.

Metas

Algumas das principais metas deste livro são:

  • Apresentar algumas "armadilhas" da linguagem C e como evitá-las;
  • Demonstrar usos dos mais diversos recursos da linguagem C;
  • Incentivar boas práticas de programação;
  • Introduzir conceitos gerais de programação e Ciência da Computação;
  • Ser claro e de fácil leitura.

Inspirações

Este livro utiliza e modifica parte do codinStruct-content (licença CC BY-SA 4.0), que faz parte de um projeto de faculdade para qual contribuí e que existe pelo mesmo motivo que este.

A iniciativa livrocpp, que é bastante similar, porém focada em C++, também foi uma forte inspiração.


Creative Commons License

A prosa escrita deste trabalho está licenciada sob os termos da licença Creative Commons Attribution-ShareAlike 4.0 International.

Introdução

Esse capítulo introduz os conceitos mais básicos para trabalhar com C, mas antes disso começaremos com um resumo da história dessa linguagem.

História

Desenvolvida na década de 70 por Dennis Ritchie, Ken Thompson e outros na Bell Laboratories, C foi criada como uma extensão de B—linguagem que fez parte do desenvolvimento do sistema operacional UNIX, que foi depois reescrito em C.

C evoluiu com o tempo e começou a ser usada em diversos outros projetos, porém ainda não tinha um padrão formal bem estabelecido e seus vários compiladores se comportavam de formas diferentes. Isso foi resolvido nos próximos anos, onde um padrão de C foi ratificado pelo ANSI e futuramente adotado pela ISO, como ISO/IEC 9899:1990. Esse padrão é comumente chamado C89, e atualmente existem várias revisões, como as informalmente chamadas C99, C11 e C18.

Primeiro programa

Código-Fonte

O primeiro programa que várias pessoas costumam escrever consiste em exibir "Hello, World!" ("Olá, Mundo!" em inglês). Um simples código C para essa tarefa é o seguinte:

#include <stdio.h>

int main(void)
{
    puts("Hello, World!");
    return 0;
}

Códigos-fonte C costumam estar em arquivos com extensão "c", portanto digamos que o código acima é o conteúdo do arquivo "main.c".

Esse código simples é composto por várias partes. Primeiro, <stdio.h> é o arquivo que fornece as principais funções de entrada e saída no C. A linha #include <stdio.h> essencialmente permite ao programa usar essas funções.

main é o ponto de entrada do programa, ou seja, onde começa a execução do código. As chaves { e } representam o corpo do main, e o código entre elas será executado.

A primeira tarefa realizada em nosso main é a linha puts("Hello, World!");. puts é uma das funções de entrada e saída fornecidas em <stdio.h>, e ela exibe seu argumento—"Hello, World!"—na tela.

Logo após isso temos a linha return 0;, que termina a execução da função. Em main, retornar 0 indica que o programa executou corretamente.

Execução

O código-fonte acima está completo, mas ainda precisamos utilizar um compilador C para transformá-lo em código objeto. Esse processo é chamado compilação e, por si só, não é suficiente para produzir um executável. Antes da compilação deve ocorrer o pré-processamento e, após a compilação, a ligação (linkage). Ambos os processos são realizados automaticamente em compiladores atuais (como GCC e Clang), portanto ainda não serão detalhados.

Uma forma simples de criar um executável utilizando o compilador GCC é fornecer como argumento o caminho para o arquivo (em nosso caso main.c) contendo o código-fonte. No caso do programa acima, o comando seria gcc main.c. O executável comumente será chamado "a.exe" ou "a.out", mas o nome pode ser alterado fornecendo a opção -o seguida pelo caminho de destino. Para criar o executável "Programa" usaríamos o comando gcc main.c -o Programa.

Se tudo der certo, a execução do programa exibirá "Hello, World!".

Funções

O que são funções?

Em C uma função é um bloco de código que pode ser executado sempre que necessário. O ponto de entrada main, visto anteriormente, é uma função. Um grande motivo para utilizar funções é que basta utilizar seus nomes para executá-las. Não é necessário reescrever o código, e isso facilita o desenvolvimento até de pequenos programas.

Entrada e saída

Uma função pode ter entrada (recebe dados) e/ou saída (retorna dados). Para exemplificarmos as possibilidades, imaginemos algumas funções simples:

  • foo, que recebe um número n e retorna seu dobro;
  • bar, que não recebe nada e sempre retorna o número 3;
  • baz, que recebe um número n e não retorna nenhum valor.
flowchart LR
    f1i[/n/]-->|Entrada|f1[[foo]]
    f1-->|Saída|f1o[/"2×n"/]

    f2[[bar]]--->|Saída|f2o[/3/]

    f3i[/n/]--->|Entrada|f3[[baz]]

Mesmo que uma função não produza saída, ela pode alterar o estado do programa, causando efeitos colaterais. Por isso, até funções sem saída podem ser úteis.

Definição

Para utilizar uma função em C, é necessário declará-la e defini-la—não faz sentido utilizar uma função se o compilador não a conhece.

A definição de uma função utiliza a seguinte sintaxe:

saída1 nome2 (params3) corpo4

1

Tipo de sua saída (dado que a função retorna).

2

Nome da função.

3

Tipos e nomes de seus parâmetros (entradas—dados que a função recebe).

4

Corpo (código) que deve ser executado pela função.

Agora definiremos as funções descritas acima.

  • foo

    • <saída>: int
      O tipo int representa um número inteiro.
    • <nome>: foo
    • <params>: int n
      A entrada n deve ser um valor inteiro.
    • <corpo>: { return n * 2; }
      O corpo da função deve estar entre chaves. Apenas precisamos calcular o dobro de n (n * 2, pois * é o operador de multiplicação em C) e retorná-lo com a palavra-chave return. return também finaliza a execução da função.
    int foo(int n) { return n * 2; }
    
  • bar

    • <saída>: int
    • <nome>: bar
    • <params>: void
      O tipo void representa a ausência de um valor.
    • <corpo>: { return 3; }
    int bar(void) { return 3; }
    
  • baz

    • <saída>: void
    • <nome>: baz
    • <params>: int n
    • <corpo>: {}
      Um corpo vazio, que não executa nada.
    void baz(int n) {}
    

Uso

Para utilizar as funções foo, bar e baz definidas acima, utilizamos a sintaxe

expr5 (args6)

5

Expressão7 que denota uma função, e.g. o nome de uma função.

6

Lista com zero ou mais argumentos (expressões) separados por vírgula, e.g. 5, pi + 3, 'z', 49.

7

Um trecho de código que calcula um valor, ou se refere a um objeto/função, ou gera efeitos colaterais no programa, ou realiza qualquer combinação desses comportamentos. Uma expressão que produz um valor pode ser utilizada em alguns contextos em que se espera um valor, e.g. a expressão bar() pode ser usada como um valor do tipo int.

Dizemos que uma função é "chamada" quando é utilizada dessa forma. Uma chamada de função é uma expressão que possui o valor retornado pela função no final de sua execução; isso significa que a expressão bar() (chamada da função bar) é aproximadamente equivalente à expressão 3.

A função bar2 abaixo retornará o valor 6 (3 + 3):

int bar2(void) { return bar() + bar(); }

Como chamadas de função são expressões, podemos utilizá-las como argumentos para funções. foo(bar()) resulta em foo(3), que resulta em 6.

Funções na biblioteca padrão

Antes de criar uma função, verifique se uma equivalente já não existe na biblioteca padrão do C. Para calcular o logaritmo natural de um número, por exemplo, basta utilizar a função log do arquivo <math.h>.

Um programa que calcula o logaritmo natural de 5 pode ser feito assim:

#include <math.h>

int main(void)
{
    log(5);

    return 0;
}

Embora <math.h> seja parte da biblioteca padrão, seu código pode não ser ligado automaticamente ao programa. Para solicitar a ligação no compilador GCC, adicione o argumento -lm na compilação (e.g. gcc main.c -lm). Em outros compiladores o procedimento pode ser diferente.

Lembre-se também que no exemplo acima, o valor de log(5) não é utilizado ou exibido—a exibição de valores numéricos será introduzida posteriormente.

Variáveis

Declaração e inicialização

A maneira mais simples de armazenar e acessar dados em C é pelo uso de objetos, que são regiões na memória que podem representar valores de diversos tipos. Uma forma simples de criar um objeto consiste em nomeá-lo e especificar seu tipo, com a seguinte sintaxe:

tipo nome

Assim, após a declaração int x;, x irá se referir a um objeto do tipo int.

Como a variável x foi definida sem algum valor especificado, na maioria dos casos ela é uma variável não inicializada, ou seja, armazena um valor "lixo" que já estava na memória. Para inicializá-la, basta especificar um inicializador (valor inicial), utilizando a sintaxe:

tipo nome = expr1

1

Expressão que produz um valor.

Substituindo int x; por int x = 5;, x passa a possuir o valor inicial especificado.

Uso

O valor de uma variável pode ser acessado e modificado durante a execução do programa.

Aqui está um código e um diagrama que representa as alterações no valor x durante sua execução:

int main(void)
{
    int x = 5;

    x = x + 5;
    x = x - 9;
    x = x + x + x;

    return 0;
stateDiagram
    Direction LR
    5-->10: x = x + 5
    10-->1: x = x - 9
    1-->3: x = x + x + x

O identificador de uma variável é uma expressão que produz o valor armazenado em seu respectivo objeto, portanto após a expressão x = 10, a expressão x produz o valor 10; nesse caso x + 3 é o mesmo que 10 + 3.

Escopo

Todos os identificadores, como nomes de variáveis e funções, possuem um escopo que determina onde podem ser acessados.

Escopo de Bloco

Os parâmetros de uma função podem ser acessados apenas em seu corpo, isso significa que n pode ser acessado em foo mas não em bar:

int foo(int n)
{
    return n; // Okay
}

int bar(void)
{
    return n; // Erro: n não existe nesse contexto
}

Isso se chama escopo de bloco, ou seja, o identificador é acessível dentro do bloco ({}) envolvente. No caso da seguinte variável n, seu escopo inicia em sua declaração e termina no final do bloco.

int foo(void)
{
    int n;

    return n; // Okay
}

int bar(void)
{
    return n; // Erro: n não existe nesse contexto
}

Um identificador também não pode ser definido duas vezes no mesmo bloco, mas blocos podem ser aninhados:

int foo(void)
{
    int n;

    int n; // Erro: n já foi definido nesse bloco
}

int bar(void)
{
    int n;

    {
        int n; // Okay: Este n está contido apenas nesse bloco
    }
}

Vale lembrar que mesmo sendo o mesmo identificador, n representa uma entidade diferente em cada escopo em que é declarado:

int foo(void)
{
    int n = 5;

    {
        int n = 10;

        return n; // Isso retorna 10 e não 5, pois a redeclaração de n torna o n
                  // anterior inacessível
    }

    return n; // Isso retorna 5 pois o escopo do segundo n termina e o primeiro
              // volta a estar acessível
}

Caso um identificador não seja redeclarado em um bloco aninhado, sua declaração original será acessada:

int foo(void)
{
    int n = 5;

    {
        return n; // Isso retorna 5
    }
}

Escopo de Arquivo

Uma variável declarada fora de um bloco possui escopo de arquivo—pode ser acessada em qualquer lugar do arquivo após sua declaração.

int n = 5;

int foo(void)
{
    return n; // Retorna 5
}

int bar(void)
{
    return n; // Retorna 5
}

Diferente de variáveis com escopo de bloco, variáveis com escopo de arquivo são inicializadas com valores definidos de acordo com seus tipos. Se o inicializador fosse removido do código acima n armazenaria 0, enquanto se n tivesse escopo de bloco não haveria nenhuma garantia de seu valor.

A sequência //, desde o padrão C99, transforma o resto de uma linha em um comentário—trecho que será ignorado.

Trechos iniciados em /* e terminados em */ são comentários que podem abranger múltiplas linhas.

Tipos fundamentais

Anteriormente vimos que funções, parâmetros e variáveis podem representar diferentes tipos de dados. Alguns tipos já estão embutidos na linguagem, e serão chamados de tipos fundamentais. Esse grupo inclui os tipos inteiros e os tipos flutuantes, que podem ser vistos abaixo.

Tipos inteiros

Tipo int

Representa um número inteiro, como -30 ou 529. O menor e maior número representável em um int não está definido no padrão C, mas um int representará, no mínimo, qualquer número inteiro no intervalo [-32767,32767].

Tipo char

Representa um caractere, como ' ' (espaço em branco), 's' (letra "s") ou ? (ponto de interrogação). Os caracteres em C devem estar dentro de aspas simples; "a" é uma string.

char foo = 'a'; // Okay

char bar = "a"; // Erro

Sequências de escape

Alguns caracteres não podem ser simplesmente digitados, portanto são representados utilizando sequências de escape, nesse caso uma barra invertida \ seguida de um caractere. Na tabela abaixo estão algumas sequências de escape.

SequênciaDescrição
\aProduz um alerta audível ou visual
\nProduz uma quebra de linha
\'Produz uma aspa simples

Tentar armazenar uma aspa simples em um char pode ser complicado, pois em char ch = '''; o compilador procura um caractere contido entre o primeiro par de aspas, porém não há nada dentro. Nesse caso devemos utilizar a sequência de escape \': char ch = '\'';.

char ch = '''; // Erro

char ch = '\''; // Okay

Conversões para outros tipos inteiros

Valores char são internamente representados por valores inteiros, e o valor de cada caractere depende do sistema. Muitos sistemas utilizam o conjunto de caracteres ASCII, parcialmente mostrado na tabela abaixo.

ValorCaractereValorCaractereValorCaractere
32(espaço)65A97a
48066B98b
49167C99c
50268D100d
51369E101e
52470F102f
53571G103g
54672H104h
55773I105i
56874J106j
57975K107k

Quando um valor char é convertido para outro tipo inteiro (e.g. int), o resultado é o valor inteiro que representa o caractere no conjunto de caracteres utilizado. Quando um tipo inteiro é convertido para char, o resultado é o caractere que representa o valor inteiro no conjunto de caracteres utilizado.

char ch = 65; // ch é igual a 'A' se o conjunto de caracteres for ASCII

int i = 'A'; // i é igual a 65 se o conjunto de caracteres for ASCII

Caracteres entre aspas simples, como 'a', possuem tipo int. Esse detalhe não costuma ser problemático pois um valor int pode ser implicitamente convertido para char.

Tipo _Bool

Em C, _Bool é equivalente ao bool de outras linguagens de programação, mas possui outro nome pois esse tipo foi adicionado no padrão C99 e usar o nome bool poderia quebrar programas antigos.

O tipo _Bool serve para armazenar um de dois valores: verdadeiro ou falso.

Aqui está um exemplo de duas variáveis _Bool, com valores que indicam, respectivamente, verdade e falsidade:

_Bool verdadeiro = 1;
_Bool falso = 0;

Utilizar a palavra-chave _Bool pode não ser intuitivo. Por conveniência, é recomendado incluir o arquivo <stdbool.h>, que faz com que bool se refira a _Bool e permite utilizar as palavras true (verdadeiro) e false (falso).

Veja o mesmo código que no exemplo anterior acima porém utilizando <stdbool.h>:

bool verdadeiro = true;
bool falso = false;

Sempre que bool, true ou false forem utilizados neste livro, considere que a diretiva #include <stdbool.h> está presente mesmo que em alguns exemplos ela possa estar omitida por conveniência. É útil lembrar, também, que true se refere ao valor 1 e false ao valor 0.

Um exemplo do uso de bool são predicados (termo comum para funções que retornam true ou false). Suponhamos que a função Paridade seja um predicado que verifica a paridade de um número, retornando true caso ele seja par e false caso contrário.

Paridade(1); // false
Paridade(3); // false
Paridade(5); // false

Paridade(2); // true
Paridade(4); // true
Paridade(6); // true

Tipos flutuantes

Tipo double

Representa um número real, como -30.52 ou 529.0023. Ao ser convertido para um inteiro a parte fracionária é descartada, portanto 15.89 se torna 15. Se o valor for alto/baixo demais para ser representado por um int, o comportamento é indefinido.

double d = 2.5; // d é igual a 2.5

int i = 2.5; // i é igual a 2

Tipo float

Representa um número real assim como double, mas os valores representáveis em um float são um subconjunto dos valores representáveis em um double. Isso significa que um float pode ter menos precisão e/ou não conseguir representar valores da mesma magnitude que um double.

Um f ou F após um valor flutuante (e.g. 1.5f) especifica que o valor é do tipo float.

float f = 2.5f; // f é igual a 2.5

Tipo long double

Representa um número real assim como double, mas os valores representáveis em um long double são um superconjunto dos valores representáveis em um double. Isso significa que um long double pode ter mais precisão e/ou conseguir representar valores de maior magnitude que um double.

Um l ou L após um valor flutuante (e.g. 1.5l) especifica que o valor é do tipo long double.

long double ld = 2.5l; // ld é igual a 2.5

Tipos flutuantes podem não representar todos os valores com precisão, mesmo que estejam entre o valor mínimo e o valor máximo permitidos.

Em geral, quanto mais dígitos um valor possuir, menos precisa será sua representação. O valor 1.00000001f, por exemplo, pode se tornar 1.f em algumas implementações.

Tabelas

Tipos Inteiros

Especificadores (palavras-chave, a ordem não importa)Descrição
_BoolTipo booleano, armazena 1 ou 0
signed charRepresenta um subconjunto dos valores representáveis em short
unsigned charVersão sem sinal de signed char
charSe comporta igual signed char ou unsigned char dependendo do sistema
shortRepresenta um subconjunto dos valores representáveis em int
unsigned shortVersão sem sinal de short
intRepresenta um subconjunto dos valores representáveis em um long
unsignedVersão sem sinal de int
longRepresenta um subconjunto dos valores representáveis em um long long
unsigned longVersão sem sinal de long
long longMaior tipo inteiro exigido pelo padrão C
unsigned long longVersão sem sinal de long long

Tipos flutuantes

Especificadores (palavras-chave, a ordem não importa)Descrição
floatRepresenta números reais
doubleRepresenta números reais com precisão maior ou igual a float
long doubleRepresenta números reais com precisão maior ou igual a double

Saída básica

As funções de saída do C permitem ao programa interagir com o usuário exibindo informações. Algumas funções para isso são puts, putchar e printf—todas incluídas em <stdio.h>.

Função puts

A função puts recebe uma string (sequência de caracteres) e a exibe.

Podemos exibir a string "Olá, Mundo!" simplesmente utilizando-a como argumento de puts:

puts("Olá, Mundo!");

Olá, Mundo!

Diferente de um char, uma string não deve ser delimitada por aspas simples; uma sequência de caracteres entre aspas simples é um int e não uma string.

Sequências de escape em C são interpretadas como caracteres únicos.

A função puts automaticamente insere uma quebra de linha (\n) na saída após a string fornecida, portanto a próxima operação de saída ocorrerá na linha seguinte.

puts("Essa é a 1ª linha");
puts("Essa é a 2ª linha");
puts("Essa é a 3ª linha");

Essa é a 1ª linha
Essa é a 2ª linha
Essa é a 3ª linha

Várias chamadas seguidas de puts com literais string podem ser substituídas por apenas uma.

puts("Essa é a 1ª linha\n"
     "Essa é a 2ª linha\n"
     "Essa é a 3ª linha");

Essa é a 1ª linha
Essa é a 2ª linha
Essa é a 3ª linha

Literais string são strings especificadas diretamente em código, entre aspas duplas. "a", "Hello, World!" e "123.917" são exemplos de literais string.

Note que as três strings passadas para puts não estão separadas por vírgula, portanto o compilador as mescla em uma só (processo que ocorre com literais string). O resultado final é o mesmo que ao utilizar a string "Essa é a 1ª linha\nEssa é 2ª linha\nEssa é a 3ª linha", porém dividir as linhas torna o código mais compreensível. Utilize essa funcionalidade para uma melhor legibilidade de código.

Tentar separar as três strings utilizando vírgulas resultará em um erro, pois serão consideradas três argumentos e a função puts deve receber apenas um.

// Erro: Passando três argumentos para uma função que recebe apenas um
puts("Essa é a 1ª linha\n",
     "Essa é a 2ª linha\n",
     "Essa é a 3ª linha");

Se os caracteres é e/ou ª não forem exibidos corretamente, seu terminal pode estar utilizando um conjunto de caracteres que não os suporta.

Função putchar

A função putchar é similar à função puts, porém exibe apenas um caractere e não insere uma quebra de linha. Seu uso é simples, basta fornecer um caractere.

putchar('O');
putchar('i');
putchar('!');

Oi!

Não esqueça que o identificador de uma variável é uma expressão, portanto pode ser utilizado como argumento. Imaginando que v denota uma variável, putchar(v); exibirá o caractere correspondente a seu valor.

putchar recebe um int, porém um char será implicitamente convertido para int nesse contexto.

Aqui está um programa simples, com uma função (ExibirDuo) que recebe dois caracteres e os exibe com uma exclamação no final:

#include <stdio.h>

void ExibirDuo(char primeiro, char segundo)
{
    putchar(primeiro);
    putchar(segundo);
    putchar('!');
}

int main(void)
{
    ExibirDuo('O', 'i');

    return 0;
}

Oi!

Função printf

A função printf, diferente de puts, tem a capacidade de formatar os dados antes de exibi-los. O primeiro parâmetro da função é uma string de formato, que informa à função a série de operações de saída a serem realizadas.

A string "Hello, World!", ao ser usada como string de formato, faz com que printf simplesmente a exiba. Para realizarmos operações de saída mais complexas, utilizamos especificações de conversão—e.g. %d.

  • %: Introduz uma especificação de conversão.
  • d: Um especificador de conversão que indica um valor do tipo int em base 10.

Ao exibir, printf substitui as especificações de conversão pelos valores dos argumentos recebidos.

// %d é substituído pelo argumento 5
printf("O valor de 5 é %d", 5);

O valor de 5 é 5

Quando há várias especificações de conversão, a enésima especificação é substituída pelo enésimo argumento após a string de formato.

// O 1º %d é substituído pelo 1º argumento após a string de formato (5)
// O 2º %d é substituído pelo 2º argumento após a string de formato (9)
printf("O valor de 5 é %d e o valor de 9 é %d", 5, 9);

O valor de 5 é 5 e o valor de 9 é 9

Para os diversos tipos há diversos especificadores de conversão. Vejamos alguns deles:

EspecificadorSignificado
cUm char, exibido como um caractere
uUm unsigned, exibido em base 10
fUm double, exibido com 6 casas decimais por padrão
sUma string
// %s é substituído por "World".
printf("Hello, %s!", "World");

Hello, World!

// %f é substituído pelo valor da expressão acos(-1), função de <math.h>.
// Isso exibirá um valor aproximado de π com 6 casas decimais, e pode variar
// conforme o seu sistema. Teste você mesmo.
printf("O valor de pi é %f", acos(-1));
// %c é substituído pelo caractere "+".
printf("1 %c 1", '+');

1 + 1

Diferente do que acontece com puts, o repetido uso de printf acima irá exibir toda a saída em uma linha apenas. Para iniciar uma nova linha utilize a sequência de escape \n no final da string de formato anterior ou no início da string de formato atual.

printf("Hello, %s!\n", "World");
printf("Goodbye, %s!", "World");

Ou, alternativamente:

printf("Hello, %s!", "World");
printf("\nGoodbye, %s!", "World");

Nos exemplos acima seria melhor utilizar printf apenas uma vez. Aqui estão duas formas de exibir duas linhas com apenas um printf:

printf("Hello, %s!\n"
       "Goodbye, %s!",
       "World", "World");
printf("Hello, %s!\nGoodbye, %s!", "World", "World");

Na primeira opção acima os primeiros dois literais string se tornam um argumento só, efetivamente causando o mesmo resultado que a segunda opção. Esse processo foi explicado em Função puts.

O especificador de conversão f irá funcionar até quando o argumento correspondente for float e o especificador d irá funcionar até quando o argumento correspondente for short. A razão disso é um assunto mais avançado.

Entrada básica

As funções de entrada do C permitem ao programa interagir com o usuário lendo informações. A função principal que usaremos pra isso é a scanf de <stdio.h>.

Função scanf

Assim como printf, a função scanf recebe uma string de formato; a diferença é que nesse caso a string determina não o que será exibido, mas o que será lido.

Os modificadores de comprimento e especificadores de conversão na string de formato determinam o tipo do valor que será lido. Vejamos algumas especificações (todas devem iniciar com %):

ModificadorEspecificadorSignificado
cUm caractere, será armazenado em um char
hdUm número inteiro, será armazenado em um short
dUm número inteiro, será armazenado em um int
ldUm número inteiro, será armazenado em um long
lldUm número inteiro, será armazenado em um long long
fUm número real, será armazenado em um float
lfUm número real, será armazenado em um double
LfUm número real, será armazenado em um long double

Diferente de printf, perceba que scanf utiliza a especificação %f para float e %lf para double. Você não deve utilizar %f no lugar de %lf ou %d lugar de %hd e vice-versa.

Para armazenar um valor lido com uma especificação de conversão, precisamos especificar seu alvo (a localização de um objeto na memória).

Neste caso, utilizaremos o operador & unário (address-of, ou endereço-de) para obter o endereço do objeto representado pela variável n:

int n;
scanf("%d", &n);

A chamada de função no exemplo acima lê um número inteiro da entrada e armazena seu valor em n por meio de seu endereço na memória. A especificação %d descarta quaisquer caracteres white-space da entrada até encontrar outro tipo de caractere, portanto essa scanf funciona com qualquer número de caracteres white-space precedendo o número na entrada.

Antes de continuar é importante definir o que é um caractere white-space. Essa expressão se refere a qualquer caractere da lista abaixo.

  • ' ' (espaço)
  • '\t' (tabulação horizontal)
  • '\v' (tabulação vertical)
  • '\n' (quebra de linha)
  • '\f' (quebra de página)

Retomando nosso foco, já podemos fazer um simples programa com entrada e saída:

#include <stdio.h>

int main(void)
{
    printf("Digite um número inteiro: ");
    int num;
    scanf("%d", &num);

    printf("O número digitado foi %d.\n", num);

    return 0;
}

Execute o exemplo acima e tente fazê-lo produzir um resultado incorreto. A leitura do número pode dar errado de várias formas, incluindo:

  • A entrada não é um número. Nesse caso scanf não modifica a entrada (exceto por descartar caracteres white-space iniciais) e ela pode ser lida futuramente.
  • O número digitado pode não ser representável em um int. Nesse caso o comportamento do programa é indefinido.
  • O número digitado pode conter casas decimais, e nesse caso o separador decimal e todos dígitos seguintes serão ignorados.

Com a especificação %d a sequência de dígitos será lida até um caractere de outro tipo (ex. uma letra) ser encontrado. Com a entrada 163p90, scanf associará 163 ao %d e p90 continuará na entrada para ser lido futuramente.

Podemos decompor a entrada 163p90 em dois objetos int e um char da seguinte forma:

// A ordem de definição das variáveis não importa
int  a;
char b;
int  c;

scanf("%d%c%d", &a, &b, &c);

printf("a: %d\n"
       "b: %c\n"
       "c: %d\n",
       a, b, c);

a: 163
b: p
c: 90

No exemplo acima a scanf associa 163 ao primeiro %d, armazena o valor em a e a sequência p90 continua na entrada. O próximo caractere ('p' nesse caso) se associa ao %c e é armazenado em b. O último %d recebe o inteiro 90 que é armazenado em c. Depois, os valores são exibidos com printf.

Quando um caractere da string de formato não faz parte de uma especificação de conversão, scanf verificará se esse caractere é igual ao próximo caractere da entrada. Se sim, o caractere da entrada é descartado e prosseguimos com a string de formato, caso contrário a execução de scanf para.

printf("Quantos anos você tem? ");
int idade;
scanf("Eu tenho %d", &idade);

No exemplo acima a scanf só chega ao %d se os caracteres anteriores corresponderem à entrada. Se a entrada for Eu tenho 5, o valor de idade será 5, mas a entrada Eu tinha 5 não armazena nada em idade e seu valor é indeterminado. Um espaço na string de formato corresponde a zero ou mais caracteres white-space na entrada, então a entrada Eutenho5 também funciona corretamente.

A string de formato "Eu tenho%d" também funciona corretamente, pois como supracitado, a especificação %d faz com que scanf descarta qualquer espaço em branco até encontrar um caractere non-white-space (caracteres não white-space, como letras e dígitos).

Não se esqueça que após um número ser digitado e lido, scanf não descarta a quebra de linha (\n) do final da linha de entrada. Isso pode fazer com que esse caractere se associe a uma futura especificação %c e isso pode ser indesejado. Para descartar esse caractere de uma forma simples, utilize um espaço antes da especificação %c e isso pulará a quebra de linha.

Aqui está um código em que a quebra de linha na entrada pode ser prejudicial:

int num;
scanf("%d", &num);

char ch;
scanf("%c", &ch); // Isso lerá uma quebra de linha se o usuário tiver digitado
                  //  um número e pressionado ENTER.

printf("O caractere lido foi '%c'\n", ch);

E aqui está uma versão que se previne disso:

int num;
scanf("%d", &num);

char ch;

//     ↓
scanf(" %c", &ch); // O usuário pode inserir espaços e pressionar ENTER o quanto
                   // quiser. Apenas um caractere non-white-space se associará.

printf("O caractere lido foi '%c'\n", ch);

Alternativamente, o espaço pode estar após a especificação %d: scanf("%d ", &num).

Operadores aritméticos básicos

Virtualmente toda a manipulação de dados em C é feita por operadores, que indicam operações ações a serem realizadas com seus operandos (valores que a operação recebe).

Na matemática, a soma é uma operação representada pelo operador "+" e se aplica a dois valores, i.e. uma operação binária. A aridade de uma função/operação é o número de operandos que ela recebe; uma operação é unária quando tem aridade 1 e binária quando tem aridade 2.

Em C é possível separar os operadores em grupos de acordo com a aridade de cada um. Abaixo são apresentados alguns dos operadores aritméticos.

Binários

A maioria dos operadores binários no C recebem um operando de cada lado com a seguinte sintaxe:

operando1 op2 operando

1

Uma expressão que resulta em um valor.

2

Um operador aritmético binário, como +, - etc.

+ binário

O operador binário + funciona igual na matemática: o resultado da operação é a soma dos dois operandos. 5 + 3, por exemplo, é uma expressão de valor int 8.

Aqui está um programa que soma dois números que o usuário digitar e exibe o resultado:

#include <stdio.h>

int main(void)
{
    int a, b;
    printf("Digite dois números: ");
    scanf("%d%d", &a, &b); // "%d %d" é equivalente
    printf("Resultado: %d\n", a + b);

    return 0;
}

- binário

O operador binário - funciona de forma parecida ao operador + binário, porém realiza subtração ao invés de adição. 5 - 3, por exemplo, é uma expressão de valor int 2.

* binário

O operador binário * realiza a multiplicação de seus operandos. O resultado de 5 * 3 é um int 15.

/

O operador binário / realiza a divisão do valor do operando à esquerda pelo valor do operando à direita. O valor de 5 / 3 é 1. Como ambos operandos são ints o resultado é um int (A parte fracionária é perdida). Se algum dos operandos fosse de tipo flutuante, e.g. 5 / 3. (3. é um double), o resultado seria a dízima infinita 1,666..., que no caso de 5 / 3. seria um double.

%

O operador binário % resulta no resto da divisão (inteira) do operando à esquerda pelo operando à direita. O valor de 5 % 3, e.g., é 2, pois resultado da divisão inteira é 1 e o resto é 2.

Mais alguns exemplos:

OperaçãoValorExplicação
10 % 3110 / 3 é igual a 3, 3 * 3 é igual a 9, e 10 - 9 é 1
10 % 2010 / 2 é igual a 5, 2 * 5 é igual a 10, e 10 - 10 é 0
40 % 7540 / 7 é igual a 5, 7 * 5 é igual a 35 e 40 - 35 é 5
-10 % 3-1-10 / 3 é igual a -3, 3 * -3 é igual a -9 e -10 -(-9) é -1

Unários

A maioria dos operadores unários ficam à esquerda do operando, como no seguinte formato:

op operando

+ unário

O operador unário + é quase sempre um no-op—uma operação que não faz nada.

Apenas em alguns casos, o operador unário + irá converter seu operando para outro tipo. Esse processo é chamado promoção inteira, que será detalhado bem depois.

Não se preocupe muito com esse operador, pois é raro encontrar um motivo legítimo para usá-lo.

- unário

Diferente do + unário, esse operador raramente é um no-op. Ele inverte o sinal de seu operando, transformando 50 em -50, -25 em 25 etc.

Ele pode ser no-op quando seu operando possui valor zero, mas em alguns sistemas é possível distinguir entre zero positivo e zero negativo. Não se preocupe muito com isso, pois o sinal do zero raramente altera o comportamento de um programa.

Aqui está um programa que inverte o número que o usuário digitar:

#include <stdio.h>

int main(void)
{
    int num;

    printf("Digite um número: ");
    scanf("%d", &num);
    printf("%d\n", -num);

    return 0;
}

Precedência

Assim como na matemática, aqui temos o conceito de precedência de operadores. Isso significa que algumas operações são agrupadas independentemente da ordem em que aparecem em uma expressão.

Os operadores * e / possuem maior precedência que os operadores binários + e -, portanto, a expressão a + b / 2 é o mesmo que a + (b / 2). As versões unárias de + e - possuem maior precedência que todos os operadores acima, portanto a + b * -c é o mesmo que a + (b * (-c)).

Aqui estão mais alguns exemplos da precedência desses operadores:

int   a = 1 + 2 * 3;   // 7
int   b = 10 + 2 / 2;  // 11
float c = 1 + 3.f / 2; // 2.5f

Operadores de incremento e decremento

Incrementar ou decrementar algum número por 1 é muito comum, portanto existem operadores para fazer isso de forma concisa.

Operadores de prefixo ++ e --

Estes operadores unários ficam à esquerda de seus operandos e os modificam, incrementando (++) ou decrementando (--) o valor em 1.

Aqui está um diagrama ilustrando as alterações que estes operadores causam no valor de uma variável:

stateDiagram
    Direction LR
    [*]-->5: n = 5
    3-->4: ++n
    4-->5: ++n
    5-->6: ++n
    6-->7: ++n
    7-->6: --n
    6-->5: --n
    5-->4: --n
    4-->3: --n

    note left of 3
        ...
    end note

    note right of 7
        ...
    end note
int n = 5;
++n; // n agora vale 6
--n; // n agora vale 5
--n; // n agora vale 4
++n; // n agora vale 5

As operações acima não só modificam o operando como também produzem seu novo valor. Isso significa que a expressão ++n produz o valor n + 1 e a expressão --n produz n - 1.

int n = 5;

printf("%d\n", ++n);
printf("%d\n", --n);

6
5

Operadores de sufixo ++ e --

Ao contrário dos operadores de prefixo, estes operadores ficam à direita do operando. O comportamento é similar: ++ incrementa e -- decrementa, porém a expressão produz o valor original e o incremento/decremento não ocorre imediatamente mas sim durante ou antes do próximo ponto de sequência. Pontos de sequência existem em vários lugares diferentes, e.g. todo ; é um ponto de sequência.

int n = 5;

printf("%d\n", n++); // n++ produz 5 e depois incrementa n
printf("%d\n", n);

5
6

Quando, sem um ponto de sequência entre as expressões, o valor de um objeto for incrementado/decrementado várias vezes, ou incrementado/decrementado e acessado, o comportamento do programa é indefinido.

int n = 0;
printf("%d %d", n, ++n); // Comportamento imprevisível: n incrementado e
                         // acessado sem um ponto de sequência entre as
                         // expressões

O programa acima pode exibir qualquer saída ou até travar, pois o valor de n pode ser acessado antes/durante/após seu incremento. Isso será detalhado posteriormente.

Operadores de atribuição

Anteriormente vimos como modificar um objeto com os operadores ++ e --, porém esses operadores só incrementam ou decrementam por 1. Para modificar um objeto de forma mais complexa, é necessário utilizar operadores de atribuição. Assim como o nome sugere, operadores de atribuição atribuem um novo valor a um objeto.

Atribuição simples

O operador = (atribuição simples) atribui ao operando à esquerda o valor da expressão à direita.

int x = -3;
printf("%d\n", x);

x = 5; // x agora vale 5
printf("%d\n", x);

x = x + x; // x agora vale 10 (5 + 5)
printf("%d\n", x);

-3
5
10

Atribuição composta

Os operadores de atribuição composta existem para simplificar atribuições do formato x = x op expr tal que expr seja uma expressão e op seja um dos operadores binários *, /, %, +, -, <<, >>, &, ^ ou |. Com atribuição composta x = x op expr pode ser reescrito como x op= expr.

Isso quer dizer que a expressão a = a + b pode ser reescrita como a += b, e x = x * (25 + y) como x *= 25 + y.

int x = -3;
printf("%d\n", x);

x += 8; // x agora vale 5 (-3 + 8)
printf("%d\n", x);

x *= 2; // x agora vale 10 (5 * 2)
printf("%d\n", x);

-3
5
10

Os operadores de atribuição possuem precedência menor que todos os outros operadores vistos até agora, portanto a + b = c - d é equivalente a (a + b) = (c - d).

Associatividade de operadores

Quando várias operações têm a mesma precedência, geralmente as agrupamos da esquerda para a direita, i.e. são associativas-à-esquerda.

Associatividade à esquerda

Como os operadores binários + e - possuem a mesma precedência e são associativos-à-esquerda, ambas as expressões abaixo são equivalentes.

a + b + c - d - e
(((a + b) + c) - d) - e

Os operadores binários *, / e % possuem a mesma precedência e também associatividade à esquerda, portanto a * b / c é o mesmo que (a * b) / c.

Operações são primeiro agrupadas de acordo com a precedência, e depois de acordo com a associatividade. Por isso a + b * c / d é equivalente a a + ((b * c) / d) e não ((a + b) * c) / d.

Associatividade à direita

Todos os operadores de atribuição possuem associatividade à direita. Isso significa que a = b = c é equivalente a a = (b = c).

Como os operadores de atribuição possuem a mesma precedência, a = b *= c += d é equivalente a a = (b *= (c += d)).

Operadores de igualdade, lógicos, e relacionais

Operadores de igualdade

Os operadores de igualdade verificam a equivalência entre os valores de dois objetos.

Operadores == e !=

O operador == ("igual a") produz o valor 1 (true) quando seus operandos possuem valores equivalentes. A expressão 5 == 5 tem valor 1, enquanto a expressão 5 == 6 tem valor 0 (false).

O operador != ("não igual a") é o oposto de ==. Quando ambos os operandos possuem valores equivalentes a expressão tem valor 0, caso contrário 1. 7 != 9 resulta em 1, e 7 != 7 resulta em 0.

Imaginemos a expressão A <op> B, sendo <op> um dos operadores == ou !=.

AopBResultado
10==10true
10!=10false
10==25false
10!=25true

Se A == B for true, A != B é necessariamente false.

Operadores relacionais

Operadores < e >

O operador < ("menor que") produz o valor 1 quando o valor do operando à esquerda for menor que o valor à direita, e o operador > ("maior que") produz o valor 1 quando o valor do operando à esquerda for maior que o valor à direita. Nos demais casos, o resultado é 0.

Imaginemos a expressão A <op> B, sendo <op> um dos operadores < ou >.

AopBResultado
0<15true
0>15false
15<15false
15>15false
15<0false
15>0true

Se ambos A > B e A < B forem false, então A == B é true e vice-versa.

A afirmação anterior não se aplica caso pelo menos uma das expressões isnan(A) e isnan(B) (de <math.h>) for diferente de false. Um objeto de tipo flutuante pode possuir um valor NaN, que representa um número indefinido ou irrepresentável.

Operadores <= e >=

Os operadores <= ("menor que ou igual a") e >= ("maior que ou igual a") são similares aos operadores acima. A <= B é true quando A for menor ou igual a B, e A >= B é true quando A for maior ou igual a B.

É possível que tanto A <= B quanto A >= B sejam true, nesse caso A e B possuem valores equivalentes.

Operadores lógicos

Para todos os fins relacionados aos operadores lógicos, qualquer valor diferente de 0 (false) é considerado true.

Operador !

O operador ! ("NÃO lógico") inverte o valor lógico de uma expressão—true se torna false e false se torna true.

Se a expressão <expr> for true, a expressão !(<expr>) é necessariamente false. ! tem precedência maior que todos os operadores apresentados nessa página.

Operadores && e ||

Os operadores && ("E lógico") e || ("OU lógico") são simples. O resultado da aplicação de && é true quando ambos os operandos possuem valor true, enquanto || produz true quando pelo menos um de seus operandos possuir valor true.

AopBResultado
false||falsefalse
false&&falsefalse
true||falsetrue
true&&falsefalse
true||truetrue
true&&truetrue

Assim, podemos utilizar várias expressões para produzir um valor lógico. Por exemplo: a < b && b < c só é true se a, b e c cada um possuir um valor maior que o anterior. A precedência dos operadores lógicos E e OU é menor do que a dos operadores relacionais, portanto a expressão anterior é equivalente a (a < b) && (b < c).

A precedência do operador || é menor do que a de &&, portanto a || b || c && d || e é equivalente a a || b || (c && d) || e.

Vamos utilizar os operadores que vimos para fazer uma função que verifica se vários números estão ordenados—cada número na sequência é maior ou equivalente ao anterior.

bool Ordenados(int a, int b, int c, int d, int e)
{
    return a <= b && b <= c && c <= d && d <= e;
}

A função Ordenados retorna true com os argumentos 1, 2, 3, 4, 5, mas retorna false com os argumentos 1, 2, 3, 4, 3. Vamos utilizá-la em um programa interativo:

int main(void)
{
    int a, b, c, d, e;

    printf("Digite 5 inteiros separados por vírgula: ");
    scanf("%d ,%d ,%d ,%d ,%d",
           &a, &b, &c, &d, &e);

    // Isso exibirá "1" (true) ou "0" (false)
    printf("Os números estão ordenados? %d\n",
           Ordenados(a, b, c, d, e));
}

O posicionamento das vírgulas no scanf acima pode ser contraintuitivo, mas lembre-se de um detalhe que vimos sobre a string de formato: um espaço em branco faz o scanf pular zero ou mais caracteres white-space na leitura, portanto ele funciona corretamente até se a vírgula estiver logo após o número. A especificação %d também pula caracteres white-space caso existam, assim até a entrada 1 , 2,3, 4, 5 funcionaria corretamente.

Valores bool se tornam int ao serem passados para printf, por isso a especificação %d funciona corretamente.

Associatividade

Os operadores de igualdade, lógicos, e relacionais são associativos-à-esquerda, exceto o operador !.

Instruções

O comportamento de um programa em C é ditado por instruções. Instruções são fragmentos de código executados em sequência, e indicam as ações a serem realizadas. Há várias categorias de instruções, e os códigos-fonte apresentados anteriormente utilizam diversas delas para funcionarem. Algumas dessas categorias serão devidamente apresentadas neste capítulo.

Instruções de expressão

Instruções de expressão são instruções compostas por expressões completas.

Vejamos um código de exemplo:

int main(void)
{
    // Instrução de expressão, pois chamadas de funções são expressões.
    printf("Digite um número inteiro: ");

    // Declaração, não é uma instrução
    int num;

    // Instrução de expressão, pois chamadas de funções são expressões.
    scanf("%d", &num);

    // Instrução de expressão. Contém a expressão completa num = num * 2
    num = num * 2;

    // Instrução de expressão, pois chamadas de funções são expressões.
    printf("O dobro do número digitado é %d.\n", num);

    // Instrução return
    return 0;
}

Uma expressão completa é uma expressão que não está contida em outra.

a = 2 * 8 / 3;

No trecho acima a expressão a = 2 * 8 / 3 é completa, enquanto as demais não. A expressão 2 não é completa pois está contida em 2 * 8, a expressão 2 * 8 não é completa pois está contida em 2 * 8 / 3, e a expressão 2 * 8 / 3 não é completa pois está contida em a = 2 * 8 / 3.

O diagrama a seguir demonstra visualmente a composição da mesma expressão:

flowchart LR
    exp0[a]-----exp6["a = 2 * 8 / 3"]
    exp1[2]---exp4[2 * 8]
    exp2[8]---exp4
    exp4---exp5
    exp3[3]----exp5[2 * 8 / 3]
    exp5---exp6

Para saber qual expressão está contida em qual, utilizamos as regras de precedência e associatividade de operadores: a = 2 * 8 / 3 é o mesmo que ((a) = (((2) * (8)) / (3))), e a partir disso basta notar quais parênteses estão contidos entre outros parênteses.