Os cinco tipos básicos de dados e seus modificadores
Em C temos os tipos de dados básicos char (caractere), int (inteiro), float (ponto flutuante),
double (ponto flutuante de dupla precisão) e void (sem valor).
O tamanho que cada tipo ocupa na memória varia muito para cada plataforma, mas
normalmente um caractere ocupa 1 byte e um inteiro 2 bytes.
Modificando os Tiupos Básicos
Um modificador é usado para adaptar um tipo básico de dados para funcionar de maneira
diferente. Lista de modificadores:
● signed
● unsigned
● long
● short
Por exemplo, um char ocupa 8 bits na memória e corresponde aos números -127 à 127.
Se declararmos uma variável como sendo unsigned char, esta ainda terá 8 bits porém
representará valores de 0 à 255.
Se o código declarar uma variável do tipo inteira com sinal, o compilador analisa o bit mais
significativo, se este for 0 o número é positivo, se for 1 o número é negativo. Logo, dos 16 bits,
poderemos usar somente 15 para representar o número:
número 32.767:
01111111
11111111
Entendendo o complemento de 2
Por que usar o complemento de 2?
R: Simples, o computador faz apenas somas. Para subtrair, ele aplica o complemento de dois
ao número e depois efetua a soma. A divisão é uma sucessão de subtrações. A multiplicação é
uma sucessão de somas, e claro a soma é só somar.
Observação de desempenho: dividir é a operação que exige maior consumo de processamento, para
evitar esse problema, usa-se o shift que veremos mais a frente.
Por exemplo, tendo o seguinte binário com sinal (em vermelho indica positivo):
00000011
=
3
Qual será o binário correspondente ao -3?
1° Passo: invetemos o número 3:
11111100
2° Passo: Somamos 1 ao número invertido
11111100+1
=
11111101
=
- 125
Logo o binário correspondente ao -3 é: 1 1 1 1 1 1 0 1
Prova real: Somamos 3 (0000011) de -3 ( 1111101)
=
10000000
Ignoramos o primeiro bit, afinal não faz parte dos 7 que representam o número, e teremos:
0000000 = 0 que é a subtração de 3 - 3.
Nomes de Identificadores
Identificadores são nomes de variáveis, funções, rótulos e outros objetos definidos pelo
usuário. Eles podem variar de 1 a vários caracteres, sendo o primeiro uma letra ou um
sublinhado (underline), e os demais devem ser letras, números ou sublinhados. Por exemplo:
Certo
count
teste
high_balance
Errado
1count
hi!teste
high...balance
O tamanho máximo para um nome de identificador, se formos trabalhar com link edição, é de
8 caracteres, do contrário podemos usar até 31. Se usarmos mais caracteres, os demais (não
significativos) serão ignorados pelo compilador.
C é case-sensitive, ou seja, um caractere maiúsculo é diferente de um minúsculo:
count != COUNT != Count
O identificador não pode ser igual a uma palavra-chave da linguagem e não pode ter o mesmo
nome que funções definidas pelo programa ou em alguma biblioteca C.
Variáveis
O que é?
R: É um endereço para uma posição da memória com um nome
Por que usar?
R: Como diria o professor Simão: “Porque decorar o endereço da memória é f...”
Antes de usarmos uma variável em C, precisamos declará-la, usando a sintaxe:
tipo lista_de_variáveis;
Tipo deve ser um tipo de dado válido em C, e a lista de variáveis pode consistir em um ou mais
nomes identificadores separados por vírgulas, por exemplo:
int i, j, k
short int number;
unsigned int unsigned_number;
double balance, loss;
Onde são declaradas?
Dentro de funções, na definição dos parâmetros das funções e fora de todas as funções. Sendo
variáveis locais, parâmetros formais e variáveis globais, respectivamente.
Variáveis Locais (escopo local)
São declaradas dentro de funções. Só podem ser referenciadas dentro do bloco em que se
encontram. Elas são criadas quando o bloco de código em que se encontram é executado e em
seguida são destruídas. Por exemplo:
/**
* x e y são parâmetros formais. Devemos ter certeza de quais valores queremos
* receber na hora de definir estas variáveis.
* as três variáveis (x, y e z) são criadas ao iniciar a função e em seguida,
* quando a função termina, são destruídas
*/
int soma(int x, int y)
{
int z;
z = x + y;
return z;
}
/**
* esta variável z não tem nenhuma relação com o z da função soma.
*/
int test(void)
{
int z;
z = 10;
return z;
}
Por padrão, variáveis locais são armazenadas na pilha, que é uma área dinâmica e mutável.
Por esse motivo elas não podem armazenar valores entre as chamadas.
Variáveis Globais (escopo global)
Estas são reconhecidas por qualquer lugar no programa, e guardam valores por toda a
execução do programa, logo, consomem memória o tempo todo. Para definir uma variável
global, basta declará-la fora de qualquer função:
// soma é uma variável global
int soma;
void somar(int x, int y);
soma = x + y);
}
Elas são úteis quando a informação que contém é usada em várias partes do programa.
Porém, devem ser usadas com cautela, afinal em uma manutenção o programador pode
esquecer que a variável é global e alterando seu valor pode causar erros inesperados no
programa.
Modificadores de Tipo de Acesso
const : Variáveis com este modificador são inicializadas na sua declaração e não podem ser
alteradas na execução do programa.
Esse modificador pode ser usado para garantir que o valor apontado por um parâmetro que
recebe um ponteiro em uma função, não seja alterado:
#include “stdio.h”
void sp_to_dash(const char *str);
void main(void)
{
sp_to_dash(“isso é um teste”);
}
void sp_to_dash( const char *str)
{
while(*str){
if ( * str == ‘ ‘ ) printf ( “c%”, ‘-’);
else printf(“%c”, *str);
str++;
}
}
Se tentarmos modificar o valor para o qual *str aponta, teríamos um erro.
volatile
Para garantir que o compilador analise sempre o valor de uma variável, usamos este
modificador. Isso é útil, pois muitos compiladores C tem rotinas que otimizam certas
expressões.
valatile indica ao compilador que não necessariamente o programador irá modificar o valor da
variável em questão. Um exemplo disto é o valor de memória que corresponda à uma porta
externa, por exemplo: 0x378 que é a LPT1. Neste caso podemos usar também o modificador
const para garantir que o programanão irá modificar o valor da variável:
const volatile unsigned char *lpt1 = 0x378;
Expecificadores de Tipo de Classe de Armazenamento
Estes epecificadores informam ao compilador como as variáveis serão armazenadas. Eles
precedem o resto da declaração da varíavel.
extern
C permite que módulos de um programa sejam compilados separadamente. Neste caso, se
arquivos fonte de diferentes módulos possuim o mesmo nome para funções globais, podemos
ter um problema. É importante informarmos ao linkeditor sobre isso, do contrário, ele irá alegar
variáveis com mesmo nome, ou simplesmente escolher uma e usar.
Arquivo 1
Arquivo2
int x, y;
extern int x, y;
main(void)
test(void)
{
{
x = 1;
x = y + 10;
}
}
static
São variáveis permantentes, que mantém seus valores entre as chamadas. Os efeitos são
diferntes para variáveis locais e globais.
static local
Definida em escopo local, ela se comporta semelhante à global, ou seja, retém seu valor nas
chamadas do bloco, porém só é reconhecida naquele escopo. Um exemplo de uso seria um
gerador de uma série numérica:
series (void)
{
static int series_num;
series_num = series_num + 23;
return (series_num);
}
series_num armazena o valor da última chamada, e pode ser usada para novas chamadas,
garantindo assim um número sempre diferente para aquela execução do programa.
Se dado um valor de inicialização para a variável estática, este será atribuído a ela somente
uma vez:
series(void)
{
static int series_num = 10;
series_num = series_num + 23;
return (series_num);
}
static global
Neste caso o compilador irá criar uma variável global, porém que será reconhecida apenas no
arquivo qual foi declarada. Isso faz com que ela não fique sujeita a efeitos colaterais, como
alteração de seu valor em algum lugar indevido.
Para entender melhor, vamos a um exemplo do livro:
// Usar isto em arquivo separado
static int series_num;
void series_start(int seed);
int seires(void);
int series(void)
{
series_num = series_num + 23;
return series_num;
}
/** Inicializa series_num */
void series_start(int seed)
{
series_num = seed;
}
Para iniciar o gerador de números, precisamos chamar series_start e passar por argumento
algum número inteiro. Feito isso, cada chamada de series retornará um valor diferente.
Lembrando: Nomes de variáveis static locais são conhecidos apenas na função ou bloco em
que foram declaradas. Nomes de variáveis static globais são reconhecidos apenas no arquivo
em que elas estão. Então podemos colocar estas funções em uma biblioteca e usá-las sem
problemas, porém não poderemos acessar diretamente o valor da variável series_num, pois
esta está escondida do resto do programa.
register
Originalmente register só funcionava com dados int e char. Com o padrão ANSI ele se tornou
possível para todos os tipos de dados. Usando este especificador de armazenamento, o
compilador armazena o valor nos registradores da CPU, fazendo com que não haja perca
de tempo lendo estes valores na memória RAM. Logo, a velocidade de acesso é muito mais
rápida.
Agora que qualquer tipo de dado pode ser register, o padrão ANSI diz que “o acesso ao objeto
é o mais rápido possível”, na prática ele só tem efeitos para variáveis do tipo inteiro e caractere.
Apenas variáveis locais e parâmetros formais podem receber o especificador register, veja o
exemplo:
int_pwr(int m, register int e)
{
register int temp;
temp = 1;
for (; e; e--) temp = temp * m;
return temp;
}
Inicialização de Variáveis
A maioria das variáveis pode ser inicializada com algum valor no momento em que são
declaradas. Exemplos:
char ch = ‘a’;
int first = 0;
float balance = 123.23;
Constantes
São valores que não são alterados pelo programa. Podem ser qualquer um dos cinco tipos de
dados básicos.
Constantes Hexadecimais e Octais
Podemos definir constantes hexadecimais e octais. Sendo que um valor em exadecimal
sempre é precedido de 0x e um valor octal de 0:
// 128 em decimal ou em binário: 1000 0000 (separamos de 4 em 4 para hexadecimal)
int hex = 0x80;
// 10 em decimal ou em binário: 00 001 010 (separamos de 3 em 3 para octal)
int oct = 012;
Constantes String
Uma string é um conjunto de caracteres. Lembrando que uma constante de um único caractere
é definida com aspas simples, já uma string é definida com aspas duplas:
// Isto é uma constante caractere
char caractere = ‘a’;
// Isteo é uma constante string
char string = “a”;
Constantes Caractere de Barra Invertida
Alguns caracteres não podem ser digitados pelo teclado, são exemplos destes o retorno de
carro (CR), Retrocesso (BS) entre outros:
Código
\b
\f
\n
\r
\t
\”
\’
\0
\\
\v
\a
\N
\xN
Significado
Retrocesso (BS)
Alimentação de formulário (FF)
Nova linha (LF)
Retorno de carro (CR)
Tabulação horizontal (HT)
Aspas duplas “
Aspas simples ‘
Nulo
Barra invertida
Tabulação vertical
Alerta (beep)
Constante octal (onde N é uma constante octal)
Constante hexadecimal (onde N é uma constante hexadecimal)
Operadores
C define quatro tipos de operadores: aritméticos, relacionais, lógicos e bit a bit.
Operador de atribuição
Sempre usamos a seguinte sintaxe:
nome_da_variável = expressão;
Onde expressão pode ser qualquer valor aceito pelo tipo da variável à esquerda.
Conversão de Tipos em Atribuições
Por regra, o valor do lado direito (o lado da expressão - rvalue) é convertido no tipo do lado
esquerdo (a variável de destino - lvalue), por exemplo:
char ch;
int x;
void func(void)
{
ch = x
}
//
linha1
Na linha1 os bits mais significativos do valor inteiro x são ignorados, deixando ch com os 8 bits
menos significativos. Exemplo:
x = 1025 = 00000100 00000001
ch = x
ch = 1 = 00000001
Exemplos assumindo uma palavra de 16bits:
Tipo destino
Tipo da Expressão
signed char
char
char
short int
char
int
char
long int
int
long int
int
float
float
double
double
long double
Possível Informação Perdida
Se valor > 127, o destino é negativo
Os 8 bits mais significativos
Os 8 bits mais significativos
Os 24 bits mais significativos
Os 16 bits mais significativos
A parte fracionária e possivelmente mais
Precisão, o resultado é arredondado
Precisão, o resultado é arredondado
Atribuições múltiplas
Podemos fazer várias atribuições de valores em um único comando:
x = y = z = 0;
Operadores Aritméticos
Operador
Ação
Subração, também menos unário
+
Adição
*
Multiplicação
/
Divisão
%
Módulo da divisão (resto)
-Decremento
++
Incremento
Os operadores -, + , * e / trabalham semelhante a outras linguagens. Quando / é aplicado a um
inteiro ou caractere, qualquer resto será truncado, por exemplo 5/2 = 2. O operador % retorna o
resto de uma divisão inteira, e não pode ser usado em dados com ponto flutuante.
Com o operador unário, qualquer número precedido por um sinal de menos, troca de sinal.
Incremento e Decremento
++ irá somar 1 ao seu operando e -- subtrair:
x = 1;
x++; // x = 2
x--; // x = 1;
Se usarmos o operador antes do operando temos um efeito interessante:
x = 10;
// x = 10
y = ++x;
// y = 11, x = 11
x = 10;
y = x++;
// x = 10
// y = 10, x = 11
Por gerar código-objeto ao usarmos estes operadores, x = x + 1 é mais lento que x++.
Precedência:
maior
++
-*
/
%
menor
+
-
Operadores Relacionais e Lógicos
A ideia de verdadeiro e falso está por trás dos conceitos dos operadores lógicos e relacionais.
Em C, qualquer coisa diferente de 0 é verdadeiro, e 0 é falso. As expressões que usamo
operadores relacionais sempre retornarão verdadeiro ou falso.
Tabela verdade de operadores:
p
q
p&&q
0
0
0
0
1
0
1
1
1
1
0
0
p||q
0
1
1
1
!p
1
1
0
0
Podemos fazer combinações: 10 > 5 && !(10 < 9) || 3 <= 4. Isto resultará verdadeiro.
O livro informa que C não tem nenhum operador lógico xor. Caso queira usar o ou exclusivo
(xor), use a seguinte função:
xor (int a, int b)
{
return (a || b) && !(a &&b);
}
Precedência:
maior
!
>
&&
menor
||
>=
<
<=
A expressão !0 && 0 || 0 retorna falso. Com a adição dos parênteses !(0 && 0) || 0 ela retornará
verdadeiro.
Operadores Bit a Bit
Como foi projetada para substituir a linguagem assembly, Csuporte um conjunto completo de
operadores bit a bit. As operações bit não podem ser usadas em float, double, long double e
void.
C não possui o operador lógico XOR, mas possui o operador bit a bit XOR, confira na tabelaverdade:
p
0
1
1
0
q
0
0
1
1
p^q
0
1
0
1
No operador XOR, o resultado é verdadeiro se e somente se um dos operandos for verdadeiro.
Muito útil para uso de máscaras em circuitos elétricos, por exemplo:
BIT1 = 00001000
BIT2 = 00010000
STATUS_ENVIO = 00000000
Deseja-se ativar apenas o BIT1, então faz-se um OR entre o STATUS_ENVIO e o BIT1,
resultando o STATUS_ENVIO em 00001000.
Deseja-se desativar apenas o BIT1, então aqui vem o segredo, aplica-se o XOR entre o BIT1 e
o STATUS_ENVIO, resultando em: 00000000.
Caso o BIT2 fosse ativado em meio a este processo não haveria problema algum, pois o ou
exclusivo (XOR) cuidaria disto. Usei estas regras em projetos de automação. Pouparam-me um
bom tempo e ficou muito fácil de entender o que estava acontecendo.
Lembrando: Operadores lógicos sempre produzem um resultado que é 0 ou 1. Já operadores
bit a bit produzem um valor de acordo com a operação específica. Se x = 7, então x && 8 é
verdadeiro, enquanto que x & 8 é falso.
Os operadores de deslocamento, >> e << movem todos os bits para a direita ou para esquerda
o número de veses especificado. Exemplo de uso:
#include <stdio.h>
int main(void)
{
unsigned char test = 0xff;
//
11111111
=
255
printf(“valor de char: %d\n”, test);
test = test << 7;
//
10000000
=
128
printf(“valor de char depois da operacao: %d\n”, test);
test = test >> 7;
//
00000001
=
1
printf(“valor de char depois da segunda operacao: %d\n”, test);
}
Um deslocamento não é uma rotação, então o que sair por um lado não entrará pelo outro.
Como defini a variável sendo unsigned char (0 à 255) o que sair pela esquerda é perdido, por
isso ocorreu esta “perca” de informações ao “desfazer” o deslocamento.
Divisão e multiplicação usando deslocamento de bits
Mais simples do que eu mesmo imaginava, aprendi isso agora hehe.
Para dividir deslocamos um bit à direita e para multiplicar por 2, deslocamos um bit à esquerda.
Confira:
char x;
x = 10;
x = x << 1;
x = x << 1;
x = x << 1;
x = x >> 3;
x = x >> 1;
x = x >> 1;
x = x << 2;
x a cada execução da sentença
00001010
00010100
00101000
01010000
00001010
00000101
00000010
00001000
Valor de x
10
20
40
80
10
5
2
8
Note que se perdeu informação quando deslocamos muito à direita.
Operadore bit a bit são frequentemente usados em rotinas de criptografia. Funções simples
para criptografia que invertem os bits usando o operador de complemento a um (~):
char encode(char ch) { return (~ch); }
char decode(char ch) { return (~ch); }
Operador ?
O operador ternário é muito útil onde precisa-se usar if else. Sua sintaxe é simples:
Exp1 ? Exp2 : Exp3 (se Exp1 for verdadeiro então Exp2, senão Exp3):
x = 10;
y = x > 9 ? 100: 200; // lê-se: se x maior que 9 então y recebe 100, senão recebe 200.
Os Operadores de Ponteiros & e *
Um ponteiro é um endereço de uma variável na memória. Ponteiros tem 3 grandes funções em
C:
● fornecem uma maneira rápida de referenciar elementos de uma matriz;
● permitem que as funções modifiquem seus parâmetros de chamada;
● suportam listas encadeadas e outras estruturas dinâmicas de dados;
O operador & retorna o endereço de memória de seu operando. Lembrando que um operador
unário requer apenas um operando. Por exemplo:
m = &count;
// lê-se: m recebe o endereço de count
O código acima insere o endereço na memória da variável count em m. Esse endereço é a
posição interna da variável no computador.
O operador * é o complemento de &. Ele retorna o valro da variável localizada no endereço que
o segue. Exemplo:
m = &count;
q = *m;
// lê-se: q recebe o valor da variável apontada por m
Apesar de serem iguais, & e * não tem nada a ver com o AND e a multiplicação,
respectivamente.
Ao usarmos variáveis de tipo ponteiro, devemos declará-las do mesmo tipo de dados para qual
apontam. Por exemplo se ch for um ponteiro para um char, devemos declarar:
char *ch;
/** lê-se: ch é um ponteiro que aponta para um endereço de memória de uma
variável do tipo caractere **/
Esse programa irá imprimir o valor 10:
#include <stdio.h>
void main(void)
{
int target, source, *m;
source = 10;
m = &source;
target = *m;
printf(“%d”, target);
}
O operador em Tempo de Compilação sizeof
O operador sizeof é um operador unário que retorna o tamanho, em bytes, da variável que
recebe. Ele pode ajudar muito a gerar códigos portáveis que dependam do tamanho dos tipos
de dados internos de C.
O operador Vírgula
É usado para encadear diversas expressões. Pode ser lido como faça isso e isso:
x = (y = 3, y + 1);
Primeiro atribui o valor 3 para y e, em seguida, atribui o valor 4 a x. É necessário usar
parênteses, pois o perador vírgula tem precedência menor que o operador de atribuição.
Os operadores Ponto (.) e Seta (->)
Estes operadores referenciam elementos individuiais de estrutura e uniões. Estrutura e uniões
são tipos de dados compostos que podem ser referenciados por um único nome (veremos mais
a frente isso). Por exemplo:
#include <stdio.h>
struct employee{
char name[80];
int age;
float wage;
} emp;
struct employee *p = &emp;
int main(void)
{
// Acesso via ponto
emp.wage = 123.23;
printf("wage: %f\n", emp.wage);
// Acesso via seta
p->wage = 321.22;
printf("wage new: %f\n", p->wage);
}
Parênteses e Colchetes como Operadores
Em C, parênteses são operadores que aumentam a precedência das operações dentro deles.
Colchetes fazem indexação de matrizes e serão discutidos mais adiante. Exemplo:
#include <stdio.h>
char s[80];
int main(void)
{
s[3] = ‘X’;
printf(“%c”, s[3]);
}
Aqui atribuímos X ao quarto elementro da matriz (elas começam sempre em 0) e depois
imprimimos este valor.
Resumo das Precedências
Ordem de procedência de operadores em C:
Maior
()
[]
->
! ~ ++ -- - (tipo) * & sizeof
*
/
%
+
<<
>>
<
<=
>
>=
==
!=
&
^
|
&&
||
?
Menor
=
+=
-=
*=
/=
Expressões
Operadores, constantes e variáveis são os elementos que constituem as expressões. Uma
expressão em C é qualquer combinação válida desses elementos.
Ordem de avaliação
Como o padrão ANSI não especifica ordem de avaliação das expressões, cada compilador é
livre para implementar da maneira que quiser, por exemplo:
x = f1() + f2();
Não temos garantia que f1 será executada antes que f2, a menos que usemos parênteses para
definir uma ordem.
Conversão de Tipos em Expressões
Quando constantes e variáveis são misturadas em uma expressão, elas são convertidas a um
mesmo tipo, seguindo as seguintes regras:
SE um operando é long double
ENTÃO o segundo é convertido para long double.
SENÃO, SE um operando é double
ENTÃO o segundo é convertido para double.
SENÃO, SE um operando é float
ENTÃO o segundo é convertido para float.
SENÃO, SE um operando é unsigned long
ENTÃO o segundo é convertido para unsigned long.
SENÃO, SE um operando é long
ENTÃO o segundo é convertido para long.
SENÃO, SE um operando é unsigned
ENTÃO o segundo é convertido para unsigned.
Se um operando é long e o outro é unsigned, e se o valor de unsigned não pode ser
representado por um long, os dois operandos são convertidos para unsigned long.
Exemplo:
char ch;
int i;
float f;
double d;
result = (ch / i)
+
(f * d)
-
(f + i);
Primeiro, o caractere ch é convertido para int, e float f é convertido para um double. Em
seguida, o resultado de ch / i é convertido para double porque f * d é double. O resultado final é
double porque, neste momento, os dois operandos são double.
Casts
Podemos forçar uma expressão a ser de algum tipo específico usando uma construção
chamada cast. Seu uso:
(tipo) expressão
(float) x / 2 Garante que o valor da divisão de x por 2 será um float.
Exemplo de uso:
#include <stdio.h>
int main(void)
{
int i;
for (i = 1; i <= 100; ++i)
printf(“%d / 2 e: %d\n”, i, (float) i / 2);
}
Sem o (float), uma divisão inteira seria efetuada, o cast neste caso está assegurando que a
parte fracionária da resposta seja mostrada.
Espacejamento e Parênteses
É muito importante que nossos códigos sejam legíveis tanto quanto possível. Então procure
sempre usar espaços e tabulações à vontade. Siga ou crie um padrão próprio para isso, mas
use um padrão. As expressões abaixo são iguais:
x=10/y~(127/x);
x = 10 y ~(127 / x);
O uso de parênteses não deixa mais lenta a execução do programa, então devemos sempre
deixar claro a ordem de avaliação de uma expressão, por exemplo:
x=y/2-34*temp&127;
x = ( y/3 ) - ( ( 34*temp ) & 127 );
Avreviações C
Podemos apreviar o uso de certos comandos em C, por exemplo:
x = x + 10
x += 10;
// pode se escrito:
// lê-se: x recebe x + 10
Programas profissionais usam muito essas abreviações, então devemos nos familiarizar com
elas.
Minha reflexão
No começo parecia que seria impossível em 7 horas ver um capítulo inteiro, mas vi que não é
tão difícil quanto parece. Ler, entender, e escrever de uma forma que mais pessoas possam
compreender isso é algo muito gratificante. Mesmo que ninguém além de mim leia, sinto que a
missão está cumprida.
Download

Os cinco tipos básicos de dados e seus