Explicações Adicionais
A linguagem C possui,
além dos operadores aritméticos + - * / %
e relacionais == != < <= > >=
,
os operadores lógicos && || !
("e", "ou" e "não", respectivamente),
que servem para criar condições compostas.
Assim, por exemplo, suponha que desejamos escrever um programa que leia do usuário um número inteiro e que em seguida imprima na tela se esse número está ou não no intervalo de 1 a 10. Certamente, isso poderia ser feito usando apenas o comando "if":
#include <stdio.h> int main (void) { int n; printf("Digite um número inteiro: "); scanf("%d", &n); if (n < 1) { printf("O número não está no intervalo de 1 a 10.\n"); } else { if (n > 10) { printf("O número não está no intervalo de 1 a 10.\n"); } else { printf("O número está no intervalo de 1 a 10.\n"); } } // else } // mainPorém, é mais conveniente escrever esse programa usando o operador lógico
&&
("e")...
#include <stdio.h> int main (void) { int n; printf("Digite um número inteiro: "); scanf("%d", &n); if (n >= 1 && n <= 10) { printf("O número está no intervalo de 1 a 10.\n"); } else { printf("O número não está no intervalo de 1 a 10.\n"); } } // main... ou o operador lógico
||
("ou"):
#include <stdio.h> int main (void) { int n; printf("Digite um número inteiro: "); scanf("%d", &n); if (n < 1 || n > 10) { printf("O número não está no intervalo de 1 a 10.\n"); } else { printf("O número está no intervalo de 1 a 10.\n"); } } // main
O operadorOBSERVAÇÃO: os operadores
&&
e||
só avaliam a condição da direita se a avaliação da condição da esquerda não for suficiente para dar o resultado da expressão completa.Assim, por exemplo, uma expressão da forma
A && B
só é verdadeira se tanto a condiçãoA
quanto a condiçãoB
forem verdadeiras, masA
é sempre avaliada primeiro, e, casoA
seja falsa, já se sabe queA && B
também é falsa, e por isso a condiçãoB
não é sequer avaliada.De forma análoga, uma expressão da forma
A || B
é verdadeira seA
ouB
forem verdadeiras (incluindo o caso em que ambas são verdadeiras), masA
é sempre avaliada primeiro, e, casoA
seja verdadeira, já se sabe que a expressãoA || B
também é verdadeira, e por isso a condiçãoB
não é sequer avaliada.
!
("não") também pode ser utilizado no
programa acima:
#include <stdio.h> int main (void) { int n; printf("Digite um número inteiro: "); scanf("%d", &n); if (!(n >= 1 && n <= 10)) { printf("O número não está no intervalo de 1 a 10.\n"); } else { printf("O número está no intervalo de 1 a 10.\n"); } } // mainOs operadores lógicos podem ser encadeados para criar condições compostas de várias condições menores. Você também pode usar parênteses para garantir a correta estrutura da expressão ou para melhorar a legibilidade do código-fonte. Assim, por exemplo,
A && B || C
é o mesmo que
(A && B) || C
, pois &&
tem
maior
precedência que
||
, e nesse caso os parênteses são opcionais (mas
você pode escolher escrevê-los para deixar a condição mais clara);
entretanto, se a expressão desejada for
A && (B || C)
, então o uso dos parênteses é
obrigatório, para garantir a correta estrutura da expressão.
Em C, =
é o
operador de atribuição,
que serve para atribuir um valor a uma variável:
#include <stdio.h> int main (void) { int n; // Valor de "n" desconhecido a priori, mas pode ser impresso na tela. printf("n: %d\n", n); n = 5; printf("n: %d\n", n); // Agora, "n" vale 5. n = 8; printf("n: %d\n", n); // Agora, "n" vale 8. n = (n + 2)/2 - n; printf("n: %d\n", n); // Agora, "n" vale (8+2)/2 - 8, ou seja, -3. }
O operador =
também serve para
inicializar uma variável, isto é,
atribuir a ela um valor inicial.
#include <stdio.h> int main (void) { int n = 9; // Inicialização: "n" já começa valendo 9. printf("n: %d\n", n); n = 5; // Atribuição: agora, "n" vale 5. printf("n: %d\n", n); int m = (10*n)/2; // Inicialização: "m" começa valendo (10*5)/2, ou seja, 25. printf("n: %d, m: %d\n", n, m); }
Ao utilizar a operação de complemento de 2, "2k − N", é importante lembrar que o número N da fórmula é o número natural correspondente à sequência de bits do número cujo inverso aditivo é buscado (e portanto que N não é o próprio número cujo inverso aditivo é buscado, no caso de esse número ser negativo).
Exemplo: desejamos descobrir o número representado por "11101". Como o primeiro bit é 1, então trata-se de um número negativo "−y". Como y = −(−y), nós podemos descobrir "y" calculando o complemento de 2 de "11101". Isso pode ser feito de 2 maneiras:
Como o número natural N cuja representação binária é "11101" é 29, então y = 25 − 29 = 32 − 29 = 3.
Invertendo os bits de "11101", obtemos "00010". Adicionando 1, obtemos "00011", que é a representação binária de 3.
Nós descobrimos, portanto, que "11101" representa o número −3. Entretanto, observe agora: se "11101" representa −3, então, quando nós utilizamos a operação de complemento de 2 sobre "11101", nós desejávamos descobrir o inverso aditivo de −3; porém, seria errôneo realizar o cálculo 25 − (−3) para isso. O erro consiste em utilizar −3 no cálculo; conforme explicado acima, deve ser utilizado o número 29, que é o número natural correspondente à sequência "11101".
A regra de que o inverso aditivo de um número é obtido por meio da operação "2k − N" tem uma exceção: o caso em que a representação do número é "1000...000".
Exemplo: qual é o número representado por "10000"? Como o primeiro bit é 1, então trata-se de um número negativo. Porém, se tentarmos calcular o complemento de 2 de "10000", eis o resultado: invertendo os bits, obtemos "01111"; adicionando 1, obtemos "10000", que é a própria sequência original!
O número representado por uma sequência "1000...000" pode ser calculado de maneira indireta. Retomando, por exemplo, o caso da sequência "10000":
Digamos que o número representado por "10000" é −y.
Adicionando 1 a −y, obtemos a sequência "10001", que nós diremos que representa um certo número −z.
Para descobrir o valor de "z", basta fazer complemento de 2: "10001" → "01110" → "01111" → 15.
Como −y + 1 = −z = −15, então −y = −16.
Na representação por complemento de 2, a sequência "1000...000" representa sempre o número negativo de maior valor absoluto: –2k−1. Dentre as 3 representações de inteiros em C (sinal-e-magnitude, complemento de 1 e complemento de 2), esse é o único caso em que um número −y é representável e o número "y" não o é.
Você deve lembrar que "1000...000" é a exceção da regra do complemento de 2. Além disso, para lembrar que o número representado por essa sequência é –2k−1, você pode:
Fazer o cálculo indireto ilustrado acima para "10000".
Lembrar que, para "k" bits, há 2k números representáveis: metade não-negativos e metade negativos. Mais especificamente, os 2k−1 números não-negativos são 0, 1, 2, ..., 2k−1 − 1 (começando de 0), e os 2k−1 negativos são −1, −2, ..., −2k−1 (começando de −1).
int
Conforme estudamos, uma variável do tipo int
pode armazenar valores num intervalo limitado.
Os limites desse intervalo estão nas constantes INT_MIN
e
INT_MAX
, definidas em
<limits.h>
.
Para o tipo unsigned int
, a aritmética é modular, e
não existe erro em se incrementar uma variável que esteja armazenando
o maior valor representável.
Já para o tipo int
, a situação é diferente:
se o resultado de uma expressão cai fora do intervalo dos números
representáveis, trata-se de uma situação de
transbordo (overflow),
e o comportamento resultante do programa é
indefinido.
Isso significa que, em princípio,
o programa poderia até mesmo terminar com um erro.
Na prática, porém, um comportamento comum para transbordo de inteiros é que nenhum erro ocorra, e que a aritmética típica seja aplicada aos bits da expressão. Assim, por exemplo, se uma variável inteira possuísse apenas 4 bits, estivesse armazenando o valor 7 ("0111") e fosse incrementada, poderia então vir a assumir o valor −8 ("1000"), supondo-se a representação por complemento de 2.
int
e unsigned int
Em C, é possível atribuir um valor do tipo int
a uma variável do tipo unsigned int
,
e vice-versa, com as seguintes garantias:
Se o valor pode ser representado no tipo de destino, então o valor não é alterado.
Esse é o caso dos valores não-negativos que admitem
representação num int
:
int i = 30000; unsigned int u = i; // u == 30000, como esperado. u = 20000; i = u; // i == 20000, como esperado.
Se o valor atribuído não pode ser armazenado no tipo de destino, então:
Caso o tipo de destino seja unsigned int
,
então o valor sendo atribuído é negativo.
Nesse caso, esse valor é somado com 2k
(sendo "k" o número de bits da representação),
e o resultado (positivo) será armazenado na variável de destino.
Observe que, caso o tipo int
utilize a representação de
complemento de 2 (que é o caso típico hoje em dia),
a explicação acima significa que os bits da expressão
int
serão simplesmente copiados para a variável
unsigned int
, onde passarão a representar um valor
positivo.
Caso o tipo de destino seja int
,
então o valor sendo atribuído é um número positivo maior do que um
int
pode armazenar,
e nesse caso o resultado depende da arquitetura.
Na prática, um resultado possível (e típico?) é que, novamente, os bits da expressão original serão simplesmente copiados para a variável de destino, onde passarão a representar um valor negativo.
Assim como existem funções que não possuem parâmetros e portanto não
recebem argumentos, existem também funções que não retornam valores;
elas são definidas especificando-se o tipo de retorno como
void
:
#include <stdio.h> void boas_vindas (void) // Sem retorno, sem parâmetros. { printf("Olá! Este programa calcula o quadrado de um racional.\n\n"); } double ler_num (void) // Com retorno, sem parâmetros. { printf("Digite um número racional: "); double x; scanf("%lg", &x); return x; } void imp_quad (double x) // Sem retorno, com parâmetros. { printf("O quadrado de %g é %g.\n", x, x*x); } int main (void) // Com retorno, sem parâmetros. { boas_vindas(); double num = ler_num(); imp_quad(num); }
Como ilustrado acima, a maneira de chamar uma função que não retorna valor é a mesma de chamar uma função que retorna valor, com uma diferença importante: enquanto a chamada de uma função que retorna valor denota o valor retornado pela função, a chamada de uma função que não retorna valor não denota valor algum, e portanto não pode ser utilizada num contexto onde um valor é esperado:
double num1 = ler_num(); // Correto (esperado um double). double num2 = boas_vindas(); // Incorreto (esperado um double). double num3 = imp_quad(3.1); // Incorreto (esperado um double).
Em C, além das conversões implícitas que ocorrem quando os operandos
dos operadores aritméticos (+ − * / %
) e
relacionais (> >= <
etc) têm tipos diferentes,
também ocorrem conversões implícitas nos seguintes contextos:
Atribuição e Inicialização:
double d = 3.14; // ok: 3.14 tem tipo double int i = 7; // ok: 7 tem tipo int i = (int) d; // ok: "(int) d" tem tipo int d = (double) i; // ok: "(double) i" tem tipo double i = 3.14; // ok: 3.14 implicitamente convertido para int d = 3; // ok: 3 implicitamente convertido para double double d2 = 3; // ok: 3 implicitamente convertido para double
Atenção: lembre que tais conversões são utilizadas apenas para efeito do aproveitamento do valor de um termo numa expressão; elas não alteram os tipos das variáveis (os quais, na realidade, nunca mudam).
Inicialização de Parâmetros:
double ao_quadrado (double x) { return x*x; } ... double q = ao_quadrado(3); // ok: 3 implicitamente convertido para double
Retorno de Funções:
int sempre_3 (void) { return 3.14; // ok: 3.14 implicitamente convertido para int }
Em C, as funções
malloc,
calloc e
realloc
permitem a alocação de memória em tempo de execução,
e a função
free
permite a devolução dessa memória ao sistema;
todas estão declaradas em <stdlib.h>
.
O retorno das 3 funções de alocação acima segue a mesma regra:
Se a alocação foi bem-sucedida,
é retornado um ponteiro (de tipo void*
)
apontando para o início da memória que foi alocada.
Se a alocação não pôde ser realizada (sem memória suficiente), então é retornado um ponteiro nulo.
Deve-se sempre testar se o ponteiro retornado pelas funções de alocação é nulo ou não. Além disso, a memória alocada com essas funções deve ser posteriormente desalocada, seja via "free" ou "realloc".
As funções acima são explicadas a seguir:
A função "malloc" simplesmente aloca uma porção de memória, sem nela realizar qualquer tipo de inicialização. Essa função recebe um único argumento: o número de bytes a serem alocados – o qual normalmente é obtido de forma genérica por meio do operador sizeof.
double *pd = malloc( sizeof(double) ); // Aloca um único double double *pvd = malloc( 3*sizeof(double) ); // Aloca um vetor de 3 double's int *pvi = malloc( 3*sizeof(int) ); // Aloca um vetor de 3 int's
A função "calloc" aloca um vetor e inicializa todos os bytes dele com zero. Ela recebe dois argumentos: (1) o número de elementos do vetor, e (2) o número de bytes de cada elemento.
int t = 1000; double *vd = calloc( t, sizeof(double) ); // Aloca um vetor de "t" double's int *vi = calloc( t, sizeof(int) ); // Aloca um vetor de "t" int's
A função "realloc" serve quando queremos aumentar ou diminuir alguma porção de memória alocada anteriormente, preservando os dados lá gravados. Ela recebe dois argumentos: (1) um ponteiro para a memória alocada anteriormente, e (2) o novo número de bytes desejado.
Caso o pedido seja por uma porção maior de memória, a realocação funciona ou (1) adicionando à área já alocada os bytes adicionais necessários (se eles estiverem disponíveis), ou (2) alocando uma nova região com o tamanho desejado, copiando para ela o conteúdo da região já alocada e então desalocando esta última (tal desalocação só acontece se a alocação da nova região for bem-sucedida). Em ambos os casos, os bytes adicionais não são inicializados.
int t = 500; int *v1 = malloc( t*sizeof(int) ); // Aloca um vetor de 500 int's if (v1 != 0) { for (int i = 0; i < t; ++i) { v1[i] = i; } } int *v2 = realloc( v1, 1000*sizeof(int) ); // Aumenta o vetor para 1000 int's int *v3 = realloc( v2, 200*sizeof(int) ); // Diminui o vetor para 200 int's
A função "free" simplesmente desaloca uma região de memória alocada por meio das funções acima. Ela recebe um único argumento: um ponteiro para o início da região em questão. Se o ponteiro fornecido para "free" for nulo, a função nada faz; se, porém, o ponteiro não for nulo, então ele tem que ser um ponteiro anteriormente retornado pelas funções de alocação e ainda não desalocado.
int *v = malloc( 500*sizeof(int) ); // Aloca um vetor de 500 int's free(v); // Se v != 0, desaloca o vetor free(v); // ERRADO SE v != 0