Introdução
O que é C?
C é uma linguagem de programação amplamente utilizada que influenciou várias linguagens populares, como C++. É pequena, de baixo nível e fortemente focada em desempenho. Algumas de suas características podem ser vistas negativamente por alguns programadores, mas são vantagens em diversas situações.
Sua 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.
A linguagem 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 compiladores se comportavam de forma imprevisível. Isso foi resolvido nos próximos anos, onde um padrão de C foi aprovado pela Organização Internacional de Normalização (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 C17.
Introdução
O que é C?
C é uma linguagem de programação amplamente utilizada que influenciou várias linguagens populares, como C++. É pequena, de baixo nível e fortemente focada em desempenho. Algumas de suas características podem ser vistas negativamente por alguns programadores, mas são vantagens em diversas situações.
Sua 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.
A linguagem 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 compiladores se comportavam de forma imprevisível. Isso foi resolvido nos próximos anos, onde um padrão de C foi aprovado pela Organização Internacional de Normalização (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 C17.
Primeiro Programa
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:
Código-Fonte
Arquivo main.c
:
#include <stdio.h>
int main(void)
{
puts("Hello, World!");
return 0;
}
Esse programa 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
faz parte dele.
A primeira tarefa realizada em nosso main
é a linha puts("Hello, World!");
.
puts
é uma das funções de entrada e saída do stdio.h
, e ela exibe seu
argumento (em nosso caso, "Hello, World"
) na tela.
Logo após isso temos a linha return 0;
. return
termina a execução da função,
que nesse caso é main
, e 0
é um valor que será enviado ao sistema
operacional. 0
costuma significar que o programa foi executado corretamente.
Executando
O código-fonte acima está completo, mas ainda não é executável; precisamos utilizar um compilador C para transformar código-fonte 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. Ambos processos são realizados automaticamente em compiladores atuais (como GCC e Clang), portanto ainda não serão detalhados.
Para criar um executável utilizando o compilador GCC, por exemplo, você deve
fornecer como argumento o arquivo de extensão .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!
.
Referências
- Padrão C18 (ISO/IEC 9899:2018):
- 5.1.2.2 Hosted environment
- 6.8.6.4 The return statement
- 7.21 Input/output <stdio.h>
Diretiva #include
As diretivas de pré-processamento são processadas antes da compilação do programa e alteram o código-fonte que será compilado. Como compiladores sofisticados realizam essa tarefa automaticamente, você não verá o resultado dessa etapa; o código gerado será enviado diretamente ao compilador e descartado.
Exemplos
A diretiva #include
faz que o pré-processador inclua código externo no arquivo
a ser compilado. Imagine dois arquivos:
Arquivo externo.c
:
int a;
int b;
int c;
Arquivo main.c
:
#include "externo.c"
int main(void)
{
return 0;
}
Quando a compilação de main.c
for solicitada, o pré-processador enviará o
seguinte código para o compilador:
int a;
int b;
int c;
int main(void)
{
return 0;
}
Como pode ser observado, a diretiva #include
é substituída pelos conteúdos do
arquivo incluído. Aqui está mais um exemplo:
Arquivo externo.c
:
return 0;
Arquivo main.c
:
int main(void)
{
#include "externo.c"
}
Resultado do pré-processamento de main.c
:
int main(void)
{
return 0;
}
Variações
É possível incluir certo arquivo de duas formas:
#include <arquivo>
:- Deve ser utilizado com arquivos cuja localização é conhecida pelo
pré-processador; os arquivos da biblioteca padrão como
stdio.h
normalmente podem ser incluídos dessa forma. As localizações buscadas costumam ser definidas em uma variável de ambiente.
- Deve ser utilizado com arquivos cuja localização é conhecida pelo
pré-processador; os arquivos da biblioteca padrão como
#include "arquivo"
:- Deve conter o caminho para o arquivo, como
C:/Users/João/Documents/arquivo
. Normalmente esse caminho pode também ser relativo à localização atual. Caso o arquivo não seja encontrado, ele será buscado da forma anterior.
- Deve conter o caminho para o arquivo, como
Caso seu compilador não realize o pré-processamento automaticamente, aprenda a utilizar algum pré-processador adequado. Isso está fora do escopo desse conteúdo.
Referências
- Padrão C18 (ISO/IEC 9899:2018):
- 6.10.2 Source file inclusion
Funções
O que são?
Em C uma função é um bloco de código que pode ser executado quando necessário. O
ponto de entrada main
, utilizado até nos programas mais simples, é uma função.
Funções podem também receber e retornar dados, e isso é muito importante.
Um grande motivo para utilizar uma função é que basta utilizar o nome dela para executá-la. Não é necessário reescrever seu código, e isso facilita o desenvolvimento de qualquer programa.
Definição e Uso
Definir uma função é simples e consiste em especificar:
- O tipo de dado que a função retorna.
- O nome da função.
- Os dados que a função recebe (parâmetros).
- O código que deve ser executado pela função.
Vamos criar uma função simples chamada foo
. Funções que não retornam algum
valor possuem o tipo void
, então começamos com:
void foo
Agora, imagine que queremos que foo
receba um número inteiro que será chamado
num
. Inteiros podem ser representados com o tipo int
(outros tipos serão
mostrados mais à frente):
void foo(int num)
A função ainda não possui um corpo. Precisamos escrever algum código para ser
executado, então faremos foo
multiplicar num
por 2
:
void foo(int num)
{
num * 2;
}
Como a função não retorna nenhum valor, a operação num * 2
é realizada em vão.
Vamos fazer a função retornar o resultado da operação, e para isso basta trocar
void
por int
(número inteiro) e usar a palavra-chave return
, que faz a
função retornar o valor à sua frente (em nosso caso, num * 2
):
int foo(int num)
{
return num * 2;
}
A função está pronta. Para utilizá-la, basta chamá-la e fornecer um número:
foo(5);
Isso é uma expressão que resulta no valor 10
.
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 respectivo 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 a linha return 0;
existe apenas para informar ao sistema
operacional que o programa terminou de executar corretamente.
Variáveis
Definição e Inicialização
A maneira mais simples de armazenar dados em C é pelo uso de variáveis.
Variáveis simples podem ser criadas especificando:
- O tipo de dado que a variável armazena.
- Seu declarador, que nos casos mais simples é simplesmente o identificador (nome) da variável
Para armazenar um número inteiro podemos criar uma variável do tipo int
. Vamos
criar, por exemplo, a variável num
com a sintaxe <tipo> <declarador>;
:
int num;
Como a variável num
foi definida sem algum valor especificado, na maioria dos
casos ela é uma variável não inicializada—armazena um valor "lixo" que já estava
na memória. Para inicializá-la, basta especificar um inicializador (valor
inicial), tornando a sintaxe <tipo> <declarador> = <inicializador>;
:
int num = 62;
Assim, a variável armazena o número 62
e não algum valor aleatório. O 62
pode ser trocado por qualquer outro número representável como um int
, e.g.
-5
ou 0
.
Várias variáveis podem ser definidas de uma só vez, para isso basta utilizar a
sintaxe <tipo> <decl-init-lista>;
, sendo decl-init-lista
uma lista (separada
por vírgula) de 1 ou mais declaradores opcionalmente seguidos de
inicializadores. Seguem 3 exemplos:
int a = 62, b = 30, c = 49;
int d, e = 50, f;
int g = 32, h, i;
Uso
Uma variável substituir um valor em vários casos, como uma chamada de função. As
duas funções a seguir retornam o mesmo resultado, porém a primeira faz o uso de
uma variável n
:
int foo(void)
{
int n = 13;
return log(n);
}
int bar(void)
{
return log(13);
}
Variáveis, assim como o nome sugere, podem ter seus valores alterados durante a
execução do código. A função a seguir retorna o valor 5
, e não 99
:
int foo(void)
{
int n = 99;
n = 5;
return n;
}
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 ({}
}) a que pertence. No caso da seguinte variável n
, o escopo é o
mesmo que seria como parâmetro:
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: Esse n está contido apenas nesse bloco
}
}
Vale lembrar, também, que mesmo sendo o mesmo identificador, n
representa uma
entidade diferente em cada bloco que é redefinido:
int foo(void)
{
int n = 5;
{
int n = 10;
return n; // Isso retorna 10 e não 5, pois a redefinição de n torna o
// n anterior inacessível
}
return n; // Isso retorna 5 pois aqui o segundo n sai de escopo e o primeiro
// volta a estar acessível
}
Caso um identificador não seja redefinido em um bloco aninhado, sua definição anterior 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 um valor definido de acordo com seu tipo. 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.
Referências
- Padrão C18 (ISO/IEC 9899:2018):
- 6.2.1 Scopes of identifiers
- 6.2.5 Types
- 6.7 Declarations
- 6.7.9 Initialization
Tipos Fundamentais Básicos
Anteriormente vimos que funções, parâmetros e variáveis podem representar diferentes tipos de dados. Alguns tipos já estão embutidos como palavras-chave 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 minúscula)
ou ?
(ponto de interrogação). Os caracteres em C devem estar dentro de aspas
simples; "a"
não é um caractere, mas 'a'
é.
char bar = 'a'; // Okay
char foo = "a"; // Erro
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
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 char
é convertido para um int
, o resultado é o valor inteiro que
representa o caractere no conjunto de caracteres utilizado. Quando um int
é
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
Tipo _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
quebraria
vários programas antigos.
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 referia a
_Bool
e permite utilizar as palavras true
(verdadeiro) e false
(falso).
Veja o mesmo código que acima porém utilizando <stdbool.h>
:
bool verdadeiro = true;
bool falso = false;
Sempre que bool
, true
ou false
forem utilizados, considere que estamos
usando a diretiva #include <stdbool.h>
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 de uso do bool
são predicados—termo comum para funções que retornam
verdadeiro ou falso. 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
Exemplos de predicados existentes na biblioteca padrão são algumas funções do
<ctype.h>
, como isalpha
que retorna true
caso a função receba um caractere
alfabético, como uma letra (o comportamento pode variar conforme a localidade).
isalpha('a'); // true
isalpha('x'); // true
isalpha('L'); // true
isalpha('1'); // false
isalpha(','); // false
isalpha('?'); // false
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.
int i = 15.89; // i é igual a 15
Referências
- Padrão C18 (ISO/IEC 9899:2018):
- 5.2 Environmental considerations:
- 5.2.2 Character display semantics
- 5.2.4.2.1 Sizes of integer types <limits.h>
- 6.4.4.4 Character constants
- 5.2 Environmental considerations:
Tabelas de Tipos Fundamentais
Tipos Inteiros
Especificadores (palavras-chave, a ordem não importa) | Tipo equivalente | Descrição |
---|---|---|
_Bool | _Bool | Tipo booleano, armazena 1 ou 0 |
signed char | signed char | Caractere armazenado como inteiro com sinal |
unsigned char | unsigned char | Caractere armazenado como inteiro sem sinal |
char | char | Se comporta igual signed char ou unsigned char dependendo do sistema |
short oushort int ousigned short ousigned short int | short int | Tipo inteiro, menor ou igual a int |
unsigned short ouunsigned short int | unsigned short int | Versão sem sinal de short |
int ousigned ousigned int | int | Tipo inteiro, menor ou igual a long |
unsigned ouunsigned int | unsigned int | Versão sem sinal de int |
long oulong int ousigned long ousigned long int | long int | Tipo inteiro, menor ou igual a long long |
unsigned long ouunsigned long int | unsigned long int | Versão sem sinal de long |
long long oulong long int ousigned long long ousigned long long int | long long int | Maior tipo inteiro exigido pelo padrão C |
unsigned long long ouunsigned long long int | unsigned long int | 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!"); // Saída: Olá, Mundo!
Note o uso de aspas duplas. Diferente de um char
, uma string não deve ser
delimitada por aspas simples.
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");
/* Saída:
* Essa é a 1ª linha
* Essa é a 2ª linha
* Essa é a 3ª linha
*/
Várias chamadas seguidas de puts
com literais string (strings predefinidas,
entre aspas duplas) podem ser substituídas por apenas uma:
puts("Essa é a 1ª linha\n"
"Essa é a 2ª linha\n"
"Essa é a 3ª linha");
/* Saída:
* Essa é a 1ª linha
* Essa é a 2ª linha
* Essa é a 3ª linha
*/
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 sistema
operacional pode estar simplesmente 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('!');
// Saída: Oi!
Não esqueça que funções podem receber variáveis, não apenas valores manualmente
especificados. Imaginando que v
é uma variável do tipo char
, putchar(v);
exibirá seu valor.
Aqui está um exemplo de 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');
// Saída: Oi!
return 0;
}
Não se engane, a função ExibirDuo
é apenas um exemplo e não deve ser muito
útil em um projeto sério.
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, como %d
. Vamos dissecá-la:
- %: Introduz uma especificação de conversão.
- d: Um especificador de conversão. Indica um valor do tipo
int
em base 10.
Ao exibir, printf
substitui as especificações de conversão pelos valores dos
argumentos recebidos. Por exemplo:
// %d é substituído pelo argumento 5
// Saída: O valor de 5 é 5
printf("O valor de 5 é %d", 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)
// Saída: O valor de 5 é 5 e o valor de 9 é 9
printf("O valor de 5 é %d e o valor de 9 é %d", 5, 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 int , exibido em base 10 |
f | Um double , exibido com 6 casas decimais por padrão |
s | Uma string |
E alguns exemplos de uso:
// %s é substituído por "falar da".
// Saída: Você já ouviu falar da tragédia de Darth Plagueis, o sábio?
printf("Você já ouviu %s tragédia de Darth Plagueis, o sábio?", "falar da");
// %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 "+".
// Saída: 1 + 1
printf("1 %c 1", '+');
Diferente do que acontece com puts
, o repetido uso de printf
acima irá
exibir toda a saída em uma linha só. 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, por exemplo:
printf("Você já ouviu falar da tragédia de Darth Plagueis, o sábio?\n");
printf("Não");
// Alternativamente:
printf("Você já ouviu falar da tragédia de Darth Plagueis, o sábio?");
printf("\nNão");
Nos casos acima seria melhor utilizar printf
apenas uma vez, mas serve como
exemplo. Mesmo assim, aqui está uma forma de exibir com um printf
:
printf("Você já ouviu falar da tragédia de Darth Plagueis, o sábio?\n"
"Não");
// Alternativamente:
printf("Você já ouviu falar da tragédia de Darth Plagueis, o sábio?\nNão");
Na primeira opção acima as duas strings 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 é relativamente complexa,
portanto só é explicada em páginas mais avançadas.
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 função 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 | |
d | Um número inteiro, será armazenado em um int | |
l | d | Um número inteiro, será armazenado em um long int |
ll | d | Um número inteiro, será armazenado em um long long int |
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 %c
e vice-versa.
Para armazenar um valor lido com uma especificação de conversão, precisamos
especificar seu alvo (uma localização na memória). No nosso caso, utilizaremos o
operador &
(que será explicado depois) para obter o endereço de uma variável
na memória:
int n;
scanf("%d", &n);
A chamada de função 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 esse 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 código 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 o
scanf
não modifica a entrada (exceto por descartar caracteres white-space iniciais) e ela pode ser lida futuramente. - O número digitado pode ser grande demais para ser armazenado 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 duas variáveis int
e uma char
da
seguinte forma:
int n1,
n2;
char ch;
scanf("%d%c%d", &n1, &ch, &n2);
printf("n1: %d\n"
"n2: %d\n"
"ch: %c\n", n1, n2, ch);
O scanf
acima associa 163
ao primeiro %d
, armazena o valor em n1
e a
sequência p90
continua na entrada. O próximo caractere ('p'
nesse caso) se
associa ao %c
e é armazenado em ch
. O último %d
recebe o inteiro 90
que
é armazenado em n2
. 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, 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 do scanf
para.
printf("Quantos anos você tem? ");
int idade;
scanf("Eu tenho %d", &idade);
O scanf
acima só chega ao %d
se todos caracteres anteriores forem
correspondentes na 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 se encaixa em qualquer número
(inclusive zero) de 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 dito
no início, a especificação %d
automaticamente pula 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 digitar um número e ele ser lido, o scanf
não descarta
o caractere de 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 é '%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 é '%c'\n", ch);
Referências
- ISO/IEC JTC1/SC22/WG14 N2310
- 5.2.2 Character display semantics
- 6.4 Lexical elements
- 7.21.6.2 The fscanf function
Operadores Aritméticos Básicos
Virtualmente toda manipulação de dados em um programa C é feita por operadores, que são tokens que indicam ações a serem realizadas com seus operandos (valores que a operação recebe).
Na matemática, a soma é uma operação representada por +
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.
No C é possível separar os operadores em grupos de acordo com a aridade de cada um, e vamos fazer isso agora.
Operadores Binários
Todos os operadores binários no C recebem um operando de cada lado, no formato
<operando> <operador> <operando>
(o número de espaços não importa, podendo ser
até mesmo zero).
+
binário
O operador binário +
funciona igual na matemática: o valor da operação é a
soma dos dois operandos. 5 + 3
, por exemplo, é uma expressão de valor 8.
Como exemplo, aqui está um programa que soma dois números que o usuário digitar:
#include <stdio.h>
int main(void)
{
int a, b;
printf("Digite dois números: ");
scanf("%d%d", &a, &b); // "%d %d" pode ser mais legível
printf("%d\n", a + b);
return 0;
}
-
binário
O operador binário -
funciona de forma parecida ao operador +
binário, porém
o valor resultante é o valor do operando à esquerda subtraído pelo valor do
operando à direita. 5 - 3
, por exemplo, é uma expressão de valor 2.
*
O operador binário *
realiza a multiplicação de seus operandos. O valor de
5 * 3
é 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
inteiros o resultado é um inteiro (A parte fracionária é removida). Se algum dos
operandos fosse real, e.g. 5 / 3.
, o resultado seria a dízima infinita
1,666..., arredondada conforme seu sistema.
%
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 | Raciocínio |
---|---|---|
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 |
Operadores Unários
A maioria dos operadores unários ficam à esquerda do operando, no formato
<operador> <operando>
(o número de espaços geralmente não importa).
+
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.
Por enquanto, não se preocupe 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.
Como exemplo de uso do -
unário, 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 alguns operadores são executados antes dos outros, independente da ordem de escrita 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; // a = 7
int b = 10 + 2 / 2; // b = 11
float c = 1 + 3.f / 2; // c = 2.5
// Perceba o ".f" após o 3. Isso torna 3 um float
// para que a divisão produza um resultado real.
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 6.5 Expressions:
- 6.5.3.3 Unary arithmetic operators
- 6.5.5 Multiplicative operators
- 6.5.6 Additive operators
- 6.5 Expressions:
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 --
Esses operadores unários ficam à esquerda de seus operandos e os modificam,
incrementando (no caso de ++
) ou decrementando (no caso de --
) o valor em 1.
int n = 5;
++n; // Incrementa n em 1
printf("%d\n", n); // Exibe 6
--n; // Decrementa n em 1
printf("%d\n", n); // Exibe 5
Essas operações não só modificam o operando mas também possuem o valor dele.
Isso significa que o valor de ++n
é n + 1
e o valor de --n
é n - 1
.
int n = 5;
printf("%d\n", ++n); // Exibe 6
printf("%d\n", --n); // Exibe 5
Operadores de Sufixo ++
e --
Ao contrário dos operadores de prefixo, esses operadores ficam à direita do
operando. O comportamento é similar: ++
incrementa e --
decrementa, porém a
alteração no valor não ocorre imediatamente. A alteração ocorre durante o
próximo ponto de sequência. Pontos de sequência existem em vários lugares
diferentes no C, mas considerar todo ;
um ponto de sequência é uma heurística
razoável.
int n = 5;
printf("%d\n", n++); // Exibe 5
printf("%d\n", n); // Exibe 6 (pois o ponto de sequência já passou)
int o = n++;
printf("%d\n", o); // Exibe 6
printf("%d\n", n); // Exibe 7
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 5.1.2.3 Program execution
- 6.5 Expressions:
- 6.5.2.4 Postfix increment and decrement operators
- 6.5.3.1 Prefix increment and decrement operators
Operadores Lógicos e Relacionais
Operadores relacionais
Os operadores relacionais consistem na comparação de dois valores de várias formas diferentes.
Operadores ==
e !=
O operador ==
("igual a") produz o valor 1
(true
) quando seus operandos
tiverem 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 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
, com <op>
sendo 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 <
e >
O operador <
("menor que") produz o valor 1
quando o valor do operando à
esquerda for menor que o do valor à direita, e o operador >
("maior que")
produz o valor 1
quando o valor do operando à esquerda for maior que o
operando à direita.
Imaginemos a expressão A <op> B
, com <op>
sendo 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.
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
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 operandos possuem valor true
, enquanto
||
produz true
quando pelo menos um de seus operandos tiver 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 tiver 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, isso significa que cada número na sequência deve ser maior ou igual ao anterior.
#include <stdbool.h> // Para o tipo bool e os valores true/false
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.
Controle de Fluxo
Até agora vimos programas totalmente lineares, sem mais de um caminho possível. Nessa página vamos introduzir duas instruções que permitem ao programa decidir, durante a execução, qual caminho percorrer.
Instrução if
A instrução if
faz com que o programa execute uma instrução se uma condição
for verdadeira (true
). A condição deve estar entre parênteses após o if
, e
após o fechamento dos parênteses deve estar a instrução a ser executada:
// ⬐ Condição
if (a > b) // ⬐ Executado se a condição for true (verdadeira)
puts("a é maior que b");
Na maioria dos casos, uma instrução é um trecho de código entre dois ;
. Como o
if
apenas pode executar uma instrução, o segundo puts
do código abaixo é
executado incondicionalmente.
if (a > b) // ⬐ Executada se a condição for true
puts("a é maior que b");
puts("Não tenho nada a ver com isso");
// ⬑ Não faz parte do if, sempre será executada
Para que a instrução puts("Não tenho nada a ver com isso");
faça parte do
if
, precisamos transformar as duas instruções em uma instrução composta.
Fazemos isso colocando elas entre chaves:
if (a > b)
{ // ← Início da instrução composta
puts("a é maior que b");
puts("Agora tenho algo a ver com isso");
} // ← Fim da instrução composta
Agora, ambos puts
só são executados se a condição for verdadeira. Uma
instrução composta pode ser formada por qualquer número de instruções, até mesmo
compostas.
Não é necessário alinhar o código da mesma forma que nos trechos acima. Por sinal, um programa em C pode ser escrito em uma linha apenas (exceto por diretivas de pré-processamento). O código abaixo faz exatamente o mesmo que o código anterior.
if(a>b){puts("a é maior que b");puts("Agora tenho algo a ver com isso");}
Por questão de legibilidade, virtualmente todos os programadores optam por "embelezar" o código com espaços em branco desnecessários. Isso não é algo ruim, muito pelo contrário.
Vamos modificar um trecho de um programa apresentado anteriormente, que verifica se os números digitados estão em ordem crescente. O trecho antigo foi comentado:
/* Trecho antigo
printf("Os números estão ordenados? %d\n",
Ordenados(a, b, c, d, e));
*/
if (Ordenados(a, b, c, d, e))
puts("Os números estão ordenados.");
Agora, em vez de exibir 1
ou 0
, o programa é mais descritivo. Infelizmente,
ele não exibe nada se os números não estiverem ordenados. Para isso existe a
instrução else
.
Palavra-chave else
Formalmente, else
não é uma instrução e sim uma palavra-chave que produz uma
forma alternativa da instrução if
. A palavra-chave else
só pode aparecer
após o "corpo" (a instrução seguinte) de um if
. Assim como if
, else
condicionalmente executa uma instrução; porém apenas se a condição do if
precedente for falsa.
Uma forma simples de visualizar isso é adicionando um else
em nosso código
anterior:
if (Ordenados(a, b, c, d, e))
puts("Os números estão ordenados.");
else
puts("Os números não estão ordenados.");
O segundo puts
só executa se a condição Ordenados(a, b, c, d, e)
for falsa,
e assim nosso programa finalmente gera saídas apropriadas para ambos os casos.
Um if
, seu corpo e quaisquer else
s seguintes são considerados uma só
instrução, portanto é possível encadear vários if
e else
:
if (Ordenados(a, b, c, d, e))
puts("Os números estão em ordem crescente.");
else if (Ordenados(e, d, c, b, a))
puts("Os números estão em ordem decrescente.");
else
puts("Os números não estão ordenados.");
Analise com atenção o código acima, pois o uso de condicionais encadeadas é bastante comum em C. Em português, o funcionamento do programa é o seguinte.
- Se os valores formam uma sequência crescente:
- Exibe "Os números estão em ordem crescente."
- Caso contrário, se os valores formam uma sequência decrescente:
- Exibe "Os números estão em ordem decrescente."
- Caso contrário, os valores não estão em ordem crescente ou decrescente.
- Exibe "Os números não estão ordenados."
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 6.8 Statements and blocks:
- 6.8.2 Compound statement
- 6.8.3 Expression and null statements
- 6.8.4 Selection statements
- 6.8 Statements and blocks:
Arrays
Até agora vimos apenas tipos escalares—representam objetos conceitualmente
indivisíveis. Um int
é logicamente composto por um único inteiro, e não várias
entidades. Tipos compostos por múltiplas partes são chamados agregados, e arrays
são um exemplo disso.
Arrays representam sequências de objetos do mesmo tipo, e.g. 5 float
s. A
declaração de um array é parecida com a de uma variável comum, mas possui
colchetes após o identificador, e.g. int v[]
. Entre os colchetes especificamos
o comprimento do array, ou seja a quantidade de elementos que ele possui. Para
criar um array de 5 int
s chamado v
, usaríamos int v[5]
.
// Um array de 9 chars
char c[9];
// Um array de 2 ints
int i[2];
// Um array de 13 floats
float f[13];
Após criar um array, você pode acessar um de seus valores utilizando o operador
[]
(subscrito). Interessantemente, nesse operador um dos operandos fica entre
os colchetes.
// Acessa o 5º char do array c
c[4];
// Acessa o 1º int do array i
i[0]
// Acessa o 10º float do array f
f[9];
Como pode ter percebido com o trecho acima, os elementos de um array são contados a partir de 0 e não 1. Isso é uma fonte de confusões para vários programadores iniciantes, então esteja atento.
Arrays são comumente usados para armazenar informações fortemente relacionadas entre si, por exemplo, coordenadas em um plano 2D podem ser armazenadas em duas variáveis ou em um array. No segundo caso fica explícito no código que os valores do array fazem parte de um todo, enquanto o primeiro caso não explicita nenhuma relação entre as variáveis.
Aqui está um exemplo do armazenamento de coordenadas 2D em duas variáveis:
#include <stdio.h>
int main(void)
{
int y, x;
printf("Posição vertical: ");
scanf("%d", &y);
printf("Posição horizontal: ");
scanf("%d", &x);
}
E aqui o mesmo código, porém utilizando um array:
#include <stdio.h>
int main(void)
{
int coords[2];
printf("Posição vertical: ");
scanf("%d", &coords[0]);
printf("Posição horizontal: ");
scanf("%d", &coords[1]);
}
Assim como variáveis comuns, os valores de um array são indefinidos se não forem inicializados. Podemos inicializar um array com valores entre chaves:
char c[4] = {'a', 'c', 'j', '2'};
int i[2] = {9 + 5, 2};
float f[3] = {7.5, 1.333, 6.0 / 2};
Para alguns arrays, pode ser mais legível dividir seus inicializadores entre várias linhas:
int i[32] = {5, 9, 2, 1, 8, 0, 2, 5,
2, 2, 1, 3, 5, 6, 7, 5,
0, 7, 3, 4, 9, 9, 9, 8,
1, 8, 2, 3, 2, 6, 2, 6};
Uma vantagem de usar um inicializador, além de não causar valores indefinidos, é que o tamanho do array pode ser determinado automaticamente:
int a[]; // Erro: Qual o tamanho do array?
int b[] = {6, 2, 8}; // Ok: O tamanho é 3.
Se o inicializador de um array possuir menos valores que o array, os valores
excedentes são inicializados com 0
ou o valor NULL
(de <stddef.h>
) de
acordo com seus tipos. Para todos os tipos escalares vistos até agora, o valor é
0
.
// Todos os valores do array serão 0
int a[50] = {0};
Você pode estar se perguntando o que acontece ao tentar acessar, por exemplo, o décimo elemento em um array de comprimento 9. O comportamento resultante é indefinido—pode não acontecer nada e pode explodir o planeta.
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 6.2.5 Types
- 6.5.2.1 Array subscripting
- 6.7.9 Initialization
Instrução while
A instrução while
é muito parecida com a instrução if
, porém a instrução
seguinte será executada repetidamente até que a condição seja falsa. Nas
instruções de iteração, como while
, a instrução após a condição é chamada
loop body.
Um exemplo de uso do while
é um contador. Utilizando um loop body que exibe
o valor de uma variável e a incrementa, podemos gradualmente incrementar essa
variável até que a condição seja falsa. Você consegue imaginar a saída produzida
pelo código abaixo?
int contador = 0;
// ⬐ Enquanto contador for menor ou igual a 10
while (contador <= 10)
{ // ← Início do loop body
// Exibir o valor do contador
printf("%d ", contador);
// Incrementar o contador em 1
contador = contador + 1;
} // ← Fim do loop body
puts("e terminamos!");
Saída: 0 1 2 3 4 5 6 7 8 9 10 e terminamos!
Enquanto esse tipo de laço tem seus usos, eles são mais poderosos quando utilizados com arrays. Utilizando um laço que percorre um array, podemos realizar operações com todos seus elementos um por um. Que tal reduzir um array à soma de todos valores nele contidos?
int array[5] = {10, 5, 3, 5, 2};
int soma = 0; // Armazenamos a soma aqui
// Utilizamos esse "índice" para acessar cada elemento
int i = 0;
while (i < 5)
{
// Adicionamos o valor do elemento à soma
soma = soma + array[i];
// Incrementamos o índice
i = i + 1;
}
Após a execução do código acima, a variável soma
terá o valor 25
. A
instrução for
costuma ser mais apropriada para percorrer arrays. Ela será
introduzida em breve.
Outra utilidade do while
é fazer menus com opções. Vejamos um exemplo:
int opcao = 1;
while (opcao != 0)
{
// Exibir as opções
printf("0 - Sair\n"
"1 - Exibir \"Olá\"\n"
"2 - Exibir \"Mundo!\"\n"
"Opção: ");
// Ler a opção escolhida
scanf("%d", &opcao);
// Exibir a palavra escolhida
if (opcao == 1)
puts("Olá");
else if (opcao == 2)
puts("Mundo!");
else
puts("Opção inválida!");
}
Assim que a opção for 0, o laço será interrompido. Enquanto isso não acontecer, "Olá" ou "Mundo!" serão exibidos conforme as escolhas.
Você pode ter percebido que na primeira execução, verificar a condição é inútil
pois opcao
possui inicialmente o valor 1
. Para ignorar a condição na
primeira execução, basta utilizar a palavra-chave do
antes da instrução
composta e mover o while
com sua condição para após a instrução. Veja a versão
modificada para isso ficar mais claro:
int opcao = 1;
do // Substituímos o while (...) por do
{
printf("0 - Sair\n"
"1 - Exibir \"Olá\"\n"
"2 - Exibir \"Mundo!\"\n"
"Opção: ");
scanf("%d", &opcao);
if (opcao == 1)
puts("Olá");
else if (opcao == 2)
puts("Mundo!");
else
puts("Opção inválida!");
} while (opcao != 0); // O while (...) fica após a instrução
Assim, não fazemos mais a verificação desnecessária do valor de opcao
inicialmente.
É possível interromper um laço prematuramente usando a palavra-chave break
,
que para a execução do laço assim que executada. Podemos fazer um laço com uma
tautologia (condição sempre verdadeira) e utilizar break
para pará-lo:
while (true) // Lembre-se de incluir <stdbool.h>
{
printf("0 - Sair\n"
"1 - Exibir \"Olá\"\n"
"2 - Exibir \"Mundo!\"\n"
"Opção: ");
int opcao;
scanf("%d", &opcao);
// Parar o laço caso a opção seja 0
if (opcao == 0)
break;
if (opcao == 1)
puts("Olá");
else if (opcao == 2)
puts("Mundo!");
else
puts("Opção inválida!");
}
Repare que como a condição não utiliza mais a variável opcao
, ela pôde ser
movida para dentro da instrução composta, pois apenas lá ela é utilizada. Mesmo
com as mudanças, o código ainda aparenta se comportar da mesma forma.
Instrução for
A instrução de iteração for
é similar à while
, porém além de uma condição
várias funcionalidades podem ser embutidas entre os parênteses. Entre os
parênteses, podemos especificar três coisas:
- Uma ação inicial (e.g. definição e/ou inicialização de variáveis).
- Uma condição (assim como no
while
). - Uma ação intermediária (ocorre após cada iteração).
As expressões (ou, no caso de 1., definição) devem ser separadas por ponto e
vírgula. Vamos fazer um laço que conta de 0 a 10 com um for
, utilizando suas
três partes e um loop body:
- A definição e inicialização
int i = 0
. - A condição
i <= 10
. - O incremento
i = i + 1
. - O loop body que imprime o valor de
i
.
Isso é expresso da seguinte forma:
// ⬐ Definição ⬐ Incremento
for (int i = 0; i <= 10; i = i + 1)
// ⬑ Condição
printf("%d\n", i); // ← Loop body
Variáveis declaradas no for
são temporárias, ou seja, só podem ser acessadas
entre os parênteses e dentro do loop body.
Anteriormente, fizemos a soma dos valores de um array utilizando a instrução
while
. Vamos refazê-la, mas dessa ver usando for
:
int array[5] = {10, 5, 3, 5, 2};
int soma = 0;
for (int i = 0; i < 5; i = i + 1)
soma = soma + array[i];
Qualquer uma das três partes entre os parênteses de um for
pode ser omitida.
Caso a condição seja omitida, ela é substituída por alguma expressão true
.
Portanto o seguinte for
executa interminavelmente:
for (;;) // Equivalente a while (true)
puts("*");
O loop body de um for
ou while
pode ser uma instrução nula, i.e. um
simples ;
. Dessa forma, toda a lógica de um laço simples pode ser especificada
entre os parênteses da instrução:
int array[5] = {10, 5, 3, 5, 2};
int soma = 0;
for (int i = 0; i < 5; soma = soma + array[i], i = i + 1);
Isso soma os elementos do array assim como anteriormente, porém com loop body
nulo. Você pode ter percebido o uso da vírgula entre soma = soma + array[i]
e
i = i + 1
; o operador ,
(simplesmente "vírgula") é usado para compor uma
expressão a partir de duas, portanto soma = soma + array[i], i = i + 1
é
considerada uma só expressão e assim pode ser utilizada no for
. Na maioria dos
casos, fazer toda a lógica de um laço entre os parênteses piora a legibilidade
do código; faça apenas quando a concisão do código for crucial.
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 6.5.17 Comma operator
- 6.8.5 Iteration statements
Ponteiros
Um ponteiro é um tipo que se refere a outro objeto. Utilizar o símbolo *
em
uma declaração faz com que ela seja um ponteiro, por exemplo:
char *a; // a é um ponteiro para um char
int *b; // b é um ponteiro para um int
float *c; // c é um ponteiro para um float
float **d; // d é um ponteiro para um ponteiro para um float
Para fazer com que um ponteiro se refira a uma variável, precisamos adquirir o
endereço da variável na memória com o operador &
e atribuí-lo ao ponteiro:
char A;
int B;
float C;
float *D;
a = &A; // a agora se refere à variável A
b = &B; // b agora se refere à variável B
c = &C; // c agora se refere à variável C
d = &D; // d agora se refere à variável D
Operador de Indireção
O símbolo *
é comumente o operador "produto" e realiza multiplicação, mas
quando precede um ponteiro esse mesmo símbolo é o operador "indireção". A
indireção serve para acessar o objeto alvo do ponteiro, por exemplo:
int i;
int *p = &i; // p se refere a i
*p = 5; // A indireção acessa i
printf("%d\n", i); // Exibe 5
printf("%d\n", *p); // Exibe 5
i = 12;
printf("%d\n", i); // Exibe 12
printf("%d\n", *p); // Exibe 12
Como pode ser observado acima, a indireção de um ponteiro é análoga ao objeto a
que ele se refere. I.e., *p = 2
é efetivamente i = 2
.
Ponteiros como Parâmetros
Até agora vimos o que ponteiros fazem, mas não um motivo para usá-los. Um caso em que ponteiros são úteis é uma função que deve modificar o valor de seus argumentos, tome como exemplo uma função que incrementa um inteiro:
void incremento(int valor)
{
valor = valor + 1;
}
Como o escopo de valor
é o corpo da função, a atribuição não possui efeito no
código externo. Para que o resultado seja utilizado, nesse caso, é preciso
retorná-lo:
int incremento(int valor)
{
return valor + 1;
}
Porém essa função é contraintuitiva, incremento(num)
retorna num + 1
mas o
valor de num
é inalterado a não ser que o retorno seja utilizado:
num = incremento(num)
. Utilizando ponteiros, esse incômodo pode ser evitado:
void incremento(int *endereço)
{
*endereço = *endereço + 1;
}
A função acima recebe um ponteiro e incrementa o valor do inteiro a que ele se
refere. Embora o escopo de endereço
seja o corpo da função, o objeto alvo pode
estar em algum lugar externo. Essa versão é mais intuitiva que a anterior, pois
basta utilizar incremento(&num)
para num
ser incrementado, sem necessidade
de receber um valor de retorno.
Inicialização
Como vimos, ponteiros se referem a outros objetos. Quando um ponteiro não é inicializado, seu valor é indefinido e utilizar o operador de indireção causará comportamento perigoso e indesejado.
Uma maneira segura de especificar que um ponteiro não referencia algum objeto
válido é utilizar a constante NULL
definida em <stddef.h>
, e.g.
int *p = NULL;
. NULL
em uma condição é equivalente a false
, assim, antes
de realizar uma indireção no ponteiro, é possível verificar se ele é nulo:
void incremento(int *endereço)
{
if (endereço)
*endereço = *endereço + 1;
}
Se o ponteiro recebido for nulo, a condição endereço
será false
e a
instrução *endereço = *endereço + 1;
não será executada.
Em outras palavras, utilizar NULL
é uma convenção para que ponteiros inválidos
possuam um valor que os identifiquem. Funções de várias bibliotecas (incluindo a
biblioteca padrão) verificam se um ponteiro é nulo antes de tentar utilizá-lo.
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 6.5.3.2 Address and indirection operators
- 6.7.6 Declarators
Operador sizeof
O padrão C economiza palavras ao falar sobre os tamanhos de objetos de cada tipo
na memória. É possível que em uma plataforma um int
ocupe 2 bytes, e em outra
ocupe 8. Por isso, existe o operador sizeof
para descobrir o tamanho em bytes
de um tipo ou objeto.
A aplicação do operador sizeof
pode se dar de duas formas:
sizeof(tipo)
: Equivale ao tamanho em bytes de um objeto desse tipo.sizeof expressão
: Equivale asizeof(tipo)
comtipo
sendo o tipo da expressão. Pode também ser escritosizeof(expressão)
.
Ambas formas retornam um valor do tipo inteiro sem sinal size_t
(definido em
<stddef.h>
).
Usos Simples
Um exemplo de uso do operador sizeof
é não precisar digitar manualmente o
tamanho de um array para percorrê-lo em um laço for
. Os dois trechos abaixo
são equivalentes, porém utilizando sizeof
, ao alterar o array a condição não
precisa ser alterada.
int array[10];
/* ... */
// Percorre todos elementos do array
for (size_t i = 0; i < 10; i = i + 1)
/* ... */
int array[10];
/* ... */
// Percorre todos elementos do array
for (size_t i = 0; i < sizeof array / sizeof array[0]; i = i + 1)
/* ... */
A expressão sizeof array / sizeof array[0]
resulta no número de elementos no
array após 3 passos:
- Calcular o valor de
sizeof array
.array
é do tipoint[10]
, portanto o valor ésizeof(int) * 10
(pois um array de 10int
s é 10 vezes maior que umint
). - Calcular o valor de
sizeof array[0]
.array[0]
é um dos elementos do array, portanto seu tipo éint
. Isso se tornasizeof(int)
. - Dividindo
sizeof(int) * 10
porsizeof(int)
temos o resultado10
, que é o número deint
s no array.
sizeof(char)
é sempre 1
, portanto o número de elementos em um array de
char
pode ser calculado utilizando simplesmente sizeof(array)
. Utilize essa
versão reduzida com moderação pois pode causar erros caso o tipo armazenado no
array seja alterado por alguma mudança no código.
Quantos Bits Tem Um Byte?
Curiosamente, o padrão ISO não especifica quantos bits há em um byte. A
quantidade de bits em um byte no seu sistema é especificada no valor de
CHAR_BIT
(<limits.h>
). Atualmente é muito provável que no seu caso esse
valor seja 8
.
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 5.2.4.2.1 Sizes of integer types <limits.h>
- 6.5.3.4 The sizeof and _Alignof operators
- 7.19 Common definitions <stddef.h>
Aritmética de Ponteiros
Embora possa parecer estranho à primeira vista, ponteiros (exceto void *
)
suportam soma e subtração. Para entender o funcionamento, é necessário primeiro
compreender como arrays são armazenados na memória.
Somando e Subtraindo Inteiros
Imaginemos um array de char
s {'a', 'f', 'c', 'k', 'b'}
. O array seria
disposto na memória como na tabela abaixo. Cada endereço se refere a um byte.
Endereço | Valor |
---|---|
X | 'a' |
X + 1 | 'f' |
X + 2 | 'c' |
X + 3 | 'k' |
X + 4 | 'b' |
Para facilitar a compreensão, estamos supondo que um int
possui 4 bytes em
qualquer sistema.
No caso de um array int arr[] = {10, 5, 9, 2, 1};
com int
s de 4 bytes, o
array seria disposto como na tabela abaixo.
Endereço | Valor |
---|---|
X | 10 |
X + 4 | 5 |
X + 8 | 9 |
X + 12 | 2 |
X + 16 | 1 |
Perceba que os elementos são contíguos na memória, i.e. um vem imediatamente
após o outro. Na tabela acima 10
ocupa X até X + 3, 5
ocupa X + 4 até X + 7,
9
ocupa X + 8 até X + 11, etc. Isso significa que se um ponteiro aponta para o
endereço de 10
, esse endereço somado com 4 (sizeof(int)
) será o endereço
de 5
.
Dado o ponteiro Tipo *p = X
, p + 1
se refere a X + sizeof(Tipo)
. Isso
significa que dado int *p = &arr[0]
, p + 1
referencia arr[1]
. e p + 2
referencia arr[2]
. Ou seja, somar 1
a p
na verdade incrementa o endereço
em 4. Subtração funciona da mesma forma: Dado int *p = &arr[2]
, p - 2
referencia arr[0]
.
Voltemos para a tabela acima. Um ponteiro para X, somado com 1 apontará para X + 4 e somado com 2 apontará para X + 8. Isso pode parecer contraintuitivo a princípio, porém é conveniente: Para acessar o próximo elemento contíguo basta somar 1 ao ponteiro independentemente de seu tipo.
int arr[] = {0, 0, 9, 0, 0};
int *p = &arr[1];
*p = 10;
p = p + 2; // p agora se refere a arr[3]
*p = 8;
p = p + 1; // p agora se refere a arr[4]
*p = 7;
p = p - 4; // p agora se refere a arr[0]
*p = 11;
Após a execução do código acima, os elementos de arr
serão 11, 10, 9, 8, 7
.
Subtraindo Ponteiros
Embora inteiros possam ser somados a ponteiros, ponteiros não. Já a subtração
entre ponteiros é possível quando ambos ponteiros referenciam elementos do mesmo
array (ou a posição logo após o final do array), e resulta na diferença entre os
índices dos elementos apontados. O resultado é do tipo ptrdiff_t
de
<stddef.h>
, que é um inteiro com sinal. Veja abaixo, lembrando que o resultado
do operador unário &
é um ponteiro.
int arr[50];
// A especificação %td serve para ptrdiff_t
printf("%td\n", &arr[10] - &arr[20]); // Exibe "-10" (10 - 20)
printf("%td\n", &arr[25] - &arr[20]); // Exibe "5" (25 - 20)
int *p = &arr[10] + 20;
printf("%td\n", p - &arr[10]); // Exibe "20" (30 - 10)
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 6.5.6 Additive operators
- 7.19 Common definitions <stddef.h>
- 7.21.6.1 The fprintf function
Gerenciamento de Memória
Até agora não vimos gerenciamento manual de memória, apenas automático. Todas
variáveis auto
que criamos são destruídas automaticamente quando saem do
escopo onde foram declaradas. A duração estática de armazenamento evita essa
destruição, porém o objeto estático é compartilhado entre todas chamadas da
função e persiste durante toda a execução programa. Para ter controle total da
duração de um objeto na memória, é necessário utilizar as funções de
gerenciamento de memória malloc
e free
, de <stdlib.h>
.
Função malloc
A função malloc
recebe a quantidade de bytes a serem alocados e retorna um
ponteiro que se refere à memória alocada. Isso significa que para alocar e
utilizar um int
, basta fazer int *p = malloc(sizeof *p);
ou
int *p = malloc(sizeof(int));
. Todas as indireções no ponteiro acessarão o
objeto alocado.
int *AlocarInt(void)
{
return malloc(sizeof(int));
}
int main(void)
{
int *a = AlocarInt(),
*b = AlocarInt(),
*c = AlocarInt();
*a = 1;
*b = 2;
*c = 3;
// Exibe "1, 2, 3"
printf("%d, %d, %d\n",
*a, *b, *c);
return 0;
}
Funções em C não podem retornar arrays mas podem retornar ponteiros. Assim, uma
forma de simular o retorno de array é alocar um array com malloc
e retornar um
ponteiro para ele. Como exemplo, vejamos uma função abaixo.
// Retorna um array de qtd ints com valor val
int *FabricarArray(size_t qtd, int val)
{
int *array = malloc(sizeof(int[qtd]));
for (size_t i = 0; i < qtd; i = i + 1)
array[i] = val;
return array;
}
int main(void)
{
int *array = FabricarArray(9, 5);
// Exibirá "5 5 5 5 5 5 5 5 5 "
for (size_t i = 0; i < 9; i = i + 1)
printf("%d ", array[i]);
return 0;
}
Lembre-se que embora um ponteiro suporte o operador []
e simule um array, ele
não é exatamente um array. O operador sizeof
na variável array
do código
acima não resulta no tamanho do array retornado pela função e sim no tamanho do
ponteiro.
int array[100];
int *ponteiro = FabricarArray(100, 0);
printf("sizeof array: %zu\nsizeof ponteiro: %zu\n",
sizeof array, sizeof ponteiro);
Em um sistema específico, a execução do código acima exibiu sizeof array: 400
e sizeof ponteiro: 8
, embora ambos possam acessar 100 int
s com o operador
[]
.
Função free
Toda a memória alocada por malloc
continua alocada até ser manualmente
liberada. Isso é feito aplicando a função free
ao ponteiro que foi retornado
por malloc
. O ponteiro retornado pela nossa função FabricarArray
é alocado
por malloc
, portanto esse procedimento deve ser feito.
int main(void)
{
int *array = FabricarArray(9, 5);
/* Utilizar o array ... */
free(array);
return 0;
}
Após o free
, acessar o ponteiro com *
resulta em comportamento indefinido
até que um novo alvo válido seja dado a ele.
Referências
- ISO/IEC JTC1/SC22/WG14 N2310:
- 7.22.3.3 The free function
Conversões Implícitas
Com a intenção de simplificar o código, em algumas situações os compiladores C convertem valores para outros tipos mesmo sem um cast. Esse processo realmente facilita o desenvolvimento, mas é necessário compreendê-lo para que não ocorram imprevistos. Essas conversões possuem regras complexas, memorizá-las é preferível mas pode ser desnecessário para programas relativamente simples.
Conversões de Atribuição
Os tipos inteiros são claramente diferentes dos tipos de ponto flutuante. Essa diferença não se dá somente nos valores que podem assumir, mas também na representação dos mesmos na memória.
int i = 1;
float f = 1;
Por mais que i
e f
possuam o mesmo valor, ele pode ser representado de forma
diferente em cada objeto. O seguinte código exibe a representação hexadecimal
dos bytes que compõem esses objetos:
for (int x = 0; x < sizeof(i); ++x)
printf("%.2hhx ", ((unsigned char *)&i)[x]);
putchar('\n');
for (int x = 0; x < sizeof(f); ++x)
printf("%.2hhx ", ((unsigned char *)&f)[x]);
A seguinte tabela exibe as informações obtidas executando os trechos acima em certo sistema Linux x86-64:
i | f | |
---|---|---|
Valor | 1 | 1 |
Bytes | 01 00 00 00 | 00 00 80 3f |
Até atribuindo o valor de f
para i
com i = f
e executando o código
novamente, os resultados são os mesmos que na tabela. Como isso é possível?
Esse é o efeito das conversões de atribuição no C. Para não haver resultados
inesperados, o compilador gera código que converte o valor em ponto flutuante
para um valor inteiro mesmo que as representações não batam. Isso significa que
ao executar i = f
, a memória não é apenas copiada mas também convertida na
atribuição.
Promoções Inteiras
Todos os tipos inteiros possuem a propriedade abstrata quantitativa rank, e aqui
está a relação entre os ranks dos inteiros padrões: _Bool
< (char
e
signed char
) < short
< int
< long
< long long
(Um tipo sem sinal
possui o mesmo rank que sua contraparte com sinal). Essa propriedade define em
parte como as conversões entre tipos inteiros ocorrem.
Promoções inteiras são conversões implícitas de um valor de tipo inteiro para
int
ou unsigned int
, com as seguintes regras:
- Se o tipo do valor possuir rank menor que o de
int
:- Se
int
puder representar qualquer valor desse tipo:- O valor é convertido para
int
.
- O valor é convertido para
- Caso contrário:
- O valor é convertido para
unsigned int
.
- O valor é convertido para
- Se
- Caso contrário:
- O valor mantém seu tipo.
Os casos em que promoções inteiras ocorrem são bem especificados no padrão ISO
mas não entraremos em muitos detalhes aqui. Por agora, basta saber que essas
promoções ocorrem nos operadores unários +
e -
, nas conversões aritméticas
usuais, e também na passagem de argumentos para funções variádicas—funções que
recebem um número arbitrário de argumentos de qualquer tipo—ou sem protótipo.
Por em isso sistemas em que int
consegue armazenar qualquer valor char
(virtualmente todos sistemas e até placas Arduino) a função printf
(que é
variádica) exibe corretamente um char
mesmo com a string de formato "%d"
.
Afinal, esse char
é convertido para int
antes da função recebê-lo.
char a,
b = 2,
c = 3;
// b e c são convertidos para int (ou unsigned) e somados, e o resultado é
// convertido de volta para char e armazenado em a.
a = b + c;
// Em sistemas onde int pode representar qualquer char, o valor de a é
// convertido para int e passado para printf, que deve exibir "a == 5"
printf("a == %d", a);
// Em raros sistemas onde int não pode representar qualquer char, o valor de a é
// convertido para unsigned int e passado para printf, que deve exibir "a == 5"
printf("a == %u", a);
Conversões Aritméticas Usuais
As conversões aritméticas usuais ocorrem quando um operador aritmético é aplicado a operandos de diferentes tipos, portanto há a necessidade de pelo menos um deles ser convertido. Esse processo segue várias regras bem estabelecidas no padrão C ISO. Resumidamente:
-
Se um operando for do tipo
long double
:- O outro operando é convertido para
long double
- O outro operando é convertido para
-
Caso contrário, se um operando for do tipo
double
:- O outro operando é convertido para o tipo
double
- O outro operando é convertido para o tipo
-
Caso contrário, se um operando for do tipo
float
:- O outro operando é convertido para o tipo
float
- O outro operando é convertido para o tipo
-
Caso contrário, as promoções inteiras ocorrem onde aplicável.
-
Se após isso os operandos ainda forem de diferentes tipos:
- Se um tipo possuir sinal e outro não (ex.
int + unsigned long
):- Se o rank do tipo sem sinal for maior ou igual que o outro:
- O tipo com sinal é convertido para o tipo sem sinal.
- Caso contrário:
- Se o tipo com sinal puder representar qualquer valor do tipo sem sinal:
- O tipo sem sinal é convertido para o tipo com sinal.
- Caso contrário:
- Ambos operandos são convertidos para a versão sem sinal do tipo com sinal.
- Se o tipo com sinal puder representar qualquer valor do tipo sem sinal:
- Se o rank do tipo sem sinal for maior ou igual que o outro:
- Caso contrário (ex.
int + long
):- O tipo de menor rank é convertido para o tipo de maior rank.
- Se um tipo possuir sinal e outro não (ex.
Quando consideramos tipos complexos e imaginários as regras são similares, porém tipos complexos só se convertem para tipos complexos e tipos imaginários só se convertem para tipos imaginários, enquanto tipos reais continuam sendo reais.
Considerações sobre Desempenho
Quando seu objetivo for atingir desempenho máximo em um programa, tenha em mente
o custo da conversão entre tipos. Em um certo sistema x86-64 a atribuição da
variável int b
à variável int a
é feita com duas instruções:
- O valor de
b
é copiado para um registrador. - O valor desse registrador é copiado para
a
.
Se a variável b
fosse float
, o processo seria mais longo:
- O valor de
b
é copiado para um registrador. - O valor desse registrador é convertido para um valor inteiro com sinal e copiado para um segundo registrador.
- O valor do segundo registrador é copiado para
a
.
E com b
do tipo long double
, dez instruções são necessárias!
Embora os números acima sejam alarmantes, não faça otimizações sem necessidade. Em muitos casos a legibilidade do código é mais importante que seu desempenho, portanto saiba quando priorizar a manutenibilidade do seu código.
Referências
- Padrão C18 (ISO/IEC 9899:2018):
- 6.2.5 Types
- 6.2.6 Representations of types
- 6.3 Conversions:
- 6.3.1.8 Usual arithmetic conversions
- 6.5.16 Assignment operators
Seleção Genérica
Funções Genéricas Tradicionais
Funções genéricas são um ponto fraco do C. Quando queremos que uma função
funcione com parâmetros de qualquer tipo, é comum recebê-los com o tipo
void *
. Como passar um argumento como void *
remove as informações sobre seu
tipo original, muitas vezes a função "genérica" precisa também receber outra
função que sabe lidar com o tipo original do argumento. Aqui está a
implementação de uma função Max
genérica:
// Recebe o endereço de dois valores e uma função comparadora, e retorna o
// endereço do maior entre os dois valores utilizando o comparador.
void *Max(void *lhs, void *rhs, int cmp(void *lhs, void *rhs))
{
return cmp(lhs, rhs) < 0 ? rhs : lhs;
}
Interpretar essa declaração de Max
deve ser simples para programadores
experientes, mas para chamar a função é necessário ter um comparador definido e
isso pode ser contraproducente. É possível que por falta de conhecimento
múltiplos comparadores equivalentes sejam definidos no mesmo programa,
introduzindo redundância. Aqui está um exemplo onde há dois comparadores
equivalentes:
int CmpInt1(void *lhs, void *rhs)
{
return *(int *)lhs - *(int *)rhs;
}
int CmpInt2(void *lhs, void *rhs)
{
return *(int *)lhs - *(int *)rhs;
}
Chamar Max
com qualquer um desses comparadores terá o mesmo resultado,
portanto ter mais de um é desnecessário. Uma maneira de evitar esse problema é
criar um wrapper de Max
específico para comparar dois int
da mesma forma que
os comparadores acima.
// Comparador interno para MaxInt
static int MaxIntCmp(void *lhs, void *rhs)
{
return *(int *)lhs - *(int *)rhs;
}
int MaxInt(int lhs, int rhs)
{
return *(int *)Max(&lhs, &rhs, MaxIntCmp);
}
O problema de redundância foi resolvido. Não há mais necessidade de criar
comparadores e MaxInt
recebe e retorna valores int
diretamente, mas temos
outros problemas: O excesso de ponteiros e indireções pode impactar
consideravelmente o desempenho da função, e agora temos duas funções que fazem
essencialmente a mesma coisa: Max
e MaxInt
. Nesse ritmo, teremos também
MaxShort
, MaxUnsignedShort
, MaxUnsigned
, MaxLong
etc. e digitar um nome
diferente para realizar a mesma operação em cada tipo não agrada muitos
desenvolvedores.
Essa é uma situação comum no desenvolvimento de software: A solução de um problema causar mais problemas. Existe, afinal, alguma forma padrão de amenizar todos os problemas acima?
Palavra-chave _Generic
A palavra-chave _Generic
foi adicionada no padrão C11 e recebe uma expressão
controladora seguida de uma lista de associações. Uma associação possui a
sintaxe tipo: expressão
e o compilador escolhe qual expressão avaliar com base
no tipo da expressão controladora. Talvez você tenha morrido por dentro ao ler
isso, mas é mais simples do que parece:
int v;
// O compilador transformará isso em puts("v é um int");
_Generic(v, char: puts("v é um char"),
int: puts("v é um int"),
float: puts("v é um float"),
default: puts("v não é char, int, ou float"));
No código acima temos a expressão controladora v
e três associações. Se a
expressão controladora v
possuir o tipo char
, a seleção genérica será
substituída pela expressão puts("v é um char");
. A palavra-chave default
define uma associação para tipos que não se encaixam em nenhuma associação
anterior.
Você talvez tenha notado que a seleção genérica se comporta de forma similar à
instrução switch
, porém resolvida durante a compilação e baseada em um tipo e
não em um valor. Esse é um ponto de vista válido, mas a seleção genérica
apresenta um detalhe importante: Sua expressão controladora não é avaliada. Isso
significa que a expressão controladora puts("Olá, Mundo!")
não exibirá nada na
tela, e a expressão controladora exit(0)
não finalizará o programa. O
compilador deve saber previamente tipo de retorno dessas funções, portanto não
há necessidade de executá-las para realizar a seleção.
Enfim, como utilizar isso para resolver os problemas de nossas funções Max
e
MaxInt
?
#define MAX(lhs, ...) _Generic(lhs, int: MaxInt,\
default: Max)(lhs, __VA_ARGS__)
void *Max(void *lhs, void *rhs, int cmp(void *lhs, void *rhs))
{
return cmp(lhs, rhs) < 0 ? rhs : lhs;
}
int MaxInt(int lhs, int rhs)
{
return lhs < rhs ? rhs : lhs;
}
Agora não precisamos utilizar Max
ou MaxInt
explicitamente. Basta utilizar o
macro MAX
com os parâmetros desejados, e o compilador selecionará a função
mais apropriada:
// Comparador para floats
int CmpFloat(void *lhs, void *rhs)
{
return *(float *)lhs == *(float *)rhs ? 0 :
*(float *)lhs < *(float *)rhs ? -1 :
1;
}
int main(void)
{
int ai = 5,
bi = 3;
float af = 5.f,
bf = 3.f;
// Isso se torna int vi = MaxInt(ai, bi);
int vi = MAX(ai, bi);
// Isso se torna float vf = *(float *)Max(&af, &bf, CmpFloat);
float vf = *(float *)MAX(&af, &bf, CmpFloat);
// vi agora vale 5 e vf agora vale 5.0
return 0;
}
Como o tipo float
não possui nenhuma associação explícita na seleção genérica,
a função Max
é selecionada e precisamos utilizar ponteiros (eliminando a
possibilidade de utilizar rvalues). Para resolver isso, basta criar uma função
para comparar valores float
:
#define MAX(lhs, ...) _Generic(lhs, int: MaxInt,\
float: MaxFloat,\
default: Max)(lhs, __VA_ARGS__)
void *Max(void *lhs, void *rhs, int cmp(void *lhs, void *rhs))
{
return cmp(lhs, rhs) < 0 ? rhs : lhs;
}
int MaxInt(int lhs, int rhs)
{
return lhs < rhs ? rhs : lhs;
}
float MaxFloat(float lhs, float rhs)
{
return lhs < rhs ? rhs : lhs;
}
Agora podemos utilizar MAX
com valores float
sem ponteiros:
float vf = MAX(af, bf)
. O uso de rvalues também se torna possível:
float vf = MAX(5.f, 3.f)
.
Há vários meios de melhorar nossa seleção genérica. As funções MaxInt
e
MaxFloat
podem ser criadas com macros, pois têm a mesma estrutura. Também há
como aninhar palavras-chave _Generic
para que MAX
não precise de ponteiros
na associação default
. As possibilidades são incontáveis e não é difícil
exagerar ao ponto de tornar o código virtualmente ilegível, mas se usadas
corretamente, seleções genéricas são uma forma padronizada de lidar com diversos
problemas.