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.
  • #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.

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

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:

  1. O tipo de dado que a função retorna.
  2. O nome da função.
  3. Os dados que a função recebe (parâmetros).
  4. 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:

  1. O tipo de dado que a variável armazena.
  2. 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ênciaDescrição
\aProduz um alerta audível ou visual
\nProduz uma quebra de linha
\\'Produz uma aspa simples

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

char ch = '''; // Erro

char ch = '\''; // Okay

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

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

Quando um 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

Tabelas de Tipos Fundamentais

Tipos Inteiros

Especificadores (palavras-chave, a ordem não importa)Tipo equivalenteDescrição
_Bool_BoolTipo booleano, armazena 1 ou 0
signed charsigned charCaractere armazenado como inteiro com sinal
unsigned charunsigned charCaractere armazenado como inteiro sem sinal
charcharSe comporta igual signed char ou unsigned char dependendo do sistema
short ou
short int ou
signed short ou
signed short int
short intTipo inteiro, menor ou igual a int
unsigned short ou
unsigned short int
unsigned short intVersão sem sinal de short
int ou
signed ou
signed int
intTipo inteiro, menor ou igual a long
unsigned ou
unsigned int
unsigned intVersão sem sinal de int
long ou
long int ou
signed long ou
signed long int
long intTipo inteiro, menor ou igual a long long
unsigned long ou
unsigned long int
unsigned long intVersão sem sinal de long
long long ou
long long int ou
signed long long ou
signed long long int
long long intMaior tipo inteiro exigido pelo padrão C
unsigned long long ou
unsigned long long int
unsigned long intVersão sem sinal de long long

Tipos flutuantes

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

Saída Básica

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

Função puts

A função puts recebe uma string (sequência de caracteres) e a exibe. Podemos exibir a string "Olá, Mundo!" simplesmente utilizando-a como argumento de puts:

puts("Olá, Mundo!"); // 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:

  1. %: Introduz uma especificação de conversão.
  2. 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:

EspecificadorSignificado
cUm char, exibido como um caractere
uUm unsigned int, exibido em base 10
fUm double, exibido com 6 casas decimais por padrão
sUma 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 %):

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

Diferente de printf, perceba que scanf utiliza a especificação %f para float e %lf para double. Você não deve utilizar %f no lugar de %lf ou %d lugar de %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çãoValorRaciocínio
10 % 3110 / 3 é igual a 3, 3 * 3 é igual a 9, e 10 - 9 é 1
10 % 2010 / 2 é igual a 5, 2 * 5 é igual a 10, e 10 - 10 é 0
40 % 7540 / 7 é igual a 5, 7 * 5 é igual a 35 e 40 - 35 é 5
-10 % 3-1-10 / 3 é igual a -3, 3 * -3 é igual a -9 e -10 -(-9) é -1

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

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 !=.

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

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 >.

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

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

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.

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

Assim, podemos utilizar várias expressões para produzir um valor lógico. Por exemplo: a < b && b < c só é true se a, b e c cada um 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 elses 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

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 floats. 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 ints 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:

  1. Uma ação inicial (e.g. definição e/ou inicialização de variáveis).
  2. Uma condição (assim como no while).
  3. 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:

  1. A definição e inicialização int i = 0.
  2. A condição i <= 10.
  3. O incremento i = i + 1.
  4. 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:

  1. sizeof(tipo): Equivale ao tamanho em bytes de um objeto desse tipo.
  2. sizeof expressão: Equivale a sizeof(tipo) com tipo sendo o tipo da expressão. Pode também ser escrito sizeof(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:

  1. Calcular o valor de sizeof array. array é do tipo int[10], portanto o valor é sizeof(int) * 10 (pois um array de 10 ints é 10 vezes maior que um int).
  2. Calcular o valor de sizeof array[0]. array[0] é um dos elementos do array, portanto seu tipo é int. Isso se torna sizeof(int).
  3. Dividindo sizeof(int) * 10 por sizeof(int) temos o resultado 10, que é o número de ints 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 chars {'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çoValor
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 ints de 4 bytes, o array seria disposto como na tabela abaixo.

EndereçoValor
X10
X + 45
X + 89
X + 122
X + 161

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 ints 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:

if
Valor11
Bytes01 00 00 0000 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.
    • Caso contrário:
      • O valor é convertido para unsigned int.
  • 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
  • Caso contrário, se um operando for do tipo double:

    • O outro operando é convertido para o tipo double
  • Caso contrário, se um operando for do tipo float:

    • O outro operando é convertido para o tipo float
  • 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.
    • Caso contrário (ex. int + long):
      • O tipo de menor rank é convertido para o tipo de maior rank.

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:

  1. O valor de b é copiado para um registrador.
  2. O valor desse registrador é copiado para a.

Se a variável b fosse float, o processo seria mais longo:

  1. O valor de b é copiado para um registrador.
  2. O valor desse registrador é convertido para um valor inteiro com sinal e copiado para um segundo registrador.
  3. 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.