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.
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:
Tipo de sua saída (dado que a função retorna).
Nome da função.
Tipos e nomes de seus parâmetros (entradas—dados que a função recebe).
Corpo (código) que deve ser executado pela função.
Agora definiremos as funções descritas acima.
-
foo
- <saída>:
int
O tipoint
representa um número inteiro. - <nome>:
foo
- <params>:
int n
A entradan
deve ser um valor inteiro. - <corpo>:
{ return n * 2; }
O corpo da função deve estar entre chaves. Apenas precisamos calcular o dobro den
(n * 2
, pois*
é o operador de multiplicação em C) e retorná-lo com a palavra-chavereturn
.return
também finaliza a execução da função.
int foo(int n) { return n * 2; }
- <saída>:
-
bar
- <saída>:
int
- <nome>:
bar
- <params>:
void
O tipovoid
representa a ausência de um valor. - <corpo>:
{ return 3; }
int bar(void) { return 3; }
- <saída>:
-
baz
- <saída>:
void
- <nome>:
baz
- <params>:
int n
- <corpo>:
{}
Um corpo vazio, que não executa nada.
void baz(int n) {}
- <saída>:
Uso
Para utilizar as funções foo
, bar
e baz
definidas acima, utilizamos a
sintaxe
Expressão7 que denota uma função, e.g. o nome de uma função.
Lista com zero ou mais argumentos (expressões) separados por
vírgula, e.g. 5, pi + 3, 'z', 49
.
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
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ência | Descrição |
---|---|
\a | Produz um alerta audível ou visual |
\n | Produz 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.
Valor | Caractere | Valor | Caractere | Valor | Caractere |
---|---|---|---|---|---|
32 | (espaço) | 65 | A | 97 | a |
48 | 0 | 66 | B | 98 | b |
49 | 1 | 67 | C | 99 | c |
50 | 2 | 68 | D | 100 | d |
51 | 3 | 69 | E | 101 | e |
52 | 4 | 70 | F | 102 | f |
53 | 5 | 71 | G | 103 | g |
54 | 6 | 72 | H | 104 | h |
55 | 7 | 73 | I | 105 | i |
56 | 8 | 74 | J | 106 | j |
57 | 9 | 75 | K | 107 | k |
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 |
---|---|
_Bool | Tipo booleano, armazena 1 ou 0 |
signed char | Representa um subconjunto dos valores representáveis em short |
unsigned char | Versão sem sinal de signed char |
char | Se comporta igual signed char ou unsigned char dependendo do sistema |
short | Representa um subconjunto dos valores representáveis em int |
unsigned short | Versão sem sinal de short |
int | Representa um subconjunto dos valores representáveis em um long |
unsigned | Versão sem sinal de int |
long | Representa um subconjunto dos valores representáveis em um long long |
unsigned long | Versão sem sinal de long |
long long | Maior tipo inteiro exigido pelo padrão C |
unsigned long long | Versão sem sinal de long long |
Tipos flutuantes
Especificadores (palavras-chave, a ordem não importa) | Descrição |
---|---|
float | Representa números reais |
double | Representa números reais com precisão maior ou igual a float |
long double | Representa 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 tipoint
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:
Especificador | Significado |
---|---|
c | Um char , exibido como um caractere |
u | Um unsigned , exibido em base 10 |
f | Um double , exibido com 6 casas decimais por padrão |
s | Uma 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 %
):
Modificador | Especificador | Significado |
---|---|---|
c | Um caractere, será armazenado em um char | |
h | d | Um número inteiro, será armazenado em um short |
d | Um número inteiro, será armazenado em um int | |
l | d | Um número inteiro, será armazenado em um long |
ll | d | Um número inteiro, será armazenado em um long long |
f | Um número real, será armazenado em um float | |
l | f | Um número real, será armazenado em um double |
L | f | Um 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:
Uma expressão que resulta em um valor.
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
int
s 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ção | Valor | Explicação |
---|---|---|
10 % 3 | 1 | 10 / 3 é igual a 3 , 3 * 3 é igual a 9 , e 10 - 9 é 1 |
10 % 2 | 0 | 10 / 2 é igual a 5 , 2 * 5 é igual a 10 , e 10 - 10 é 0 |
40 % 7 | 5 | 40 / 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
!=
.
A | op | B | Resultado |
---|---|---|---|
10 | == | 10 | true |
10 | != | 10 | false |
10 | == | 25 | false |
10 | != | 25 | true |
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
>
.
A | op | B | Resultado |
---|---|---|---|
0 | < | 15 | true |
0 | > | 15 | false |
15 | < | 15 | false |
15 | > | 15 | false |
15 | < | 0 | false |
15 | > | 0 | true |
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
.
A | op | B | Resultado |
---|---|---|---|
false | || | false | false |
false | && | false | false |
true | || | false | true |
true | && | false | false |
true | || | true | true |
true | && | true | true |
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.