Ir para conteúdo

Aritmética inteira e de ponteiros


fredericopissarra

Posts Recomendados

Aqui quero só apontar alguns conceitos que o estudante pode ter entendido mal. Um deles é a diferença entre tipos inteiros sinalizados e sem sinal. AMBOS são a mesma coisa, embora não pareçam... Existe, é claro uma diferença: A possibilidade de trabalhar com valores negativos. Mas, internamente, esses dois "tipos" comportam-se do mesmo jeito.

Por exemplo: Ao subtrair 1 de 0 obteremos, se estivermos trabalhando com tipos sinalizados, -1. Mas, para um tipo sem sinal, obteremos todos os bits do valor setados. Isso é fácil de ver:

#include <stdio.h>

void main ( void )
{
  int x = 0;
  unsigned int y = 0;

  x--;
  y--;
  printf ( "x=%1$d (%1$#x), y=%2$u (%2$#x)\n", x, y );
}

Isso imprimirá "x=-1 (0xffffffff), y=4294967295 (0xffffffff)", o que significa que int e unsigned int funcionam do mesmo jeito. De fato, ao subtrair 1 de 0, em binário, obteremos todos os bits setados e o flag CF também setado:

$ cat test.c
#include <stdio.h>

__attribute__((noinline))
int f(int x)
{
  __asm__ __volatile__(
    "subl $1,%0"
    : "=r" (x) : "0" (x)
  );

  return x;
}

void main ( void )
{
  printf ( "x=%1$d (%1$#x)\n", f(0) );
}

$ cc -g -O2 -o test test.c
$ gdb -q test
Reading symbols from test...done.
(gdb) b f
Breakpoint 1 at 0x6a0: file test.c, line 6.
(gdb) disas f
Dump of assembler code for function f:
   0x00000000000006a0 <+0>:	mov    %edi,%eax
   0x00000000000006a2 <+2>:	sub    $0x1,%eax
   0x00000000000006a5 <+5>:	retq   
End of assembler dump.
(gdb) r
Starting program: /mnt/vol2/Work/tmp/test 

Breakpoint 1, f (x=0) at test.c:6
6	  __asm__ __volatile__(
(gdb) n
12	}
(gdb) info reg
rax            0xffffffff	4294967295
rbx            0x0	0
rcx            0x5555555546b0	93824992233136
rdx            0x7fffffffdd08	140737488346376
rsi            0x7fffffffdcf8	140737488346360
rdi            0x0	0
rbp            0x5555555546b0	0x5555555546b0 <__libc_csu_init>
rsp            0x7fffffffdc08	0x7fffffffdc08
r8             0x7ffff7dd0d80	140737351847296
r9             0x7ffff7dd0d80	140737351847296
r10            0x2	2
r11            0x7	7
r12            0x555555554590	93824992232848
r13            0x7fffffffdcf0	140737488346352
r14            0x0	0
r15            0x0	0
rip            0x5555555546a5	0x5555555546a5 <f+5>
eflags         0x297	[ CF PF AF SF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) 

Troque 'int' por 'unsigned int' e você obterá a mesma coisa. O que o compilador faz é interpretar os tipos 'int' como representandos valores em complemento 2. Assim, o valor absoluto contido na variável tem apenas N-1 bits e depende do MSB (Bit de alta ordem)... Mas, repare, o valor contido no tipo continua sendo um grupo de N bits sem qualquer indicação de "sinal". O sinal aqui é uma representação, cuja semântica é obtida no programa e pelos flags.

Os flags, é claro, são usados para comparações. Com valores sinalizados usamos os flags SF e OF, bem como ZF. Com valores sem sinal, usamos os flags CF e ZF... No caso do x86, o flag SF é setado se o resultado da operação resultar num MSB=1 (como 0-1 resulta no bit MSB setado!), e o flag OF é uma indicação de overflow, ou seja, nos extremos das faixas sinalizadas... se passarmos de 0x7fffffff para 0x80000000, então OF=1 e o contrário também... Mas, note que isso não acontece se passarmos de 0 para 0xffffffff e vice versa (como pode ser visto acima). Isso seria um "underflow".

Toda comparação é feita por subtração. Se comparamos x com y, se x == y,  o ZF será 1, significando que a diferença entre eles é 0. Isso vale para os tipos sinalizados e não sinalizados, mas para comparações de inequalidades (menor que ou maior que), se os tipos forem sinalizados temos que usar AMBOS os flags SF e OF, e também ZF. Repare que, com 0-1 obtivemos SF=1, OF=0 e ZF=0. Uma comparação de "maior que" resulta sempre em SF!=OF e ZF=0. O processador tem um atalho para esse tipo de salto condicional: JL (Jump if less than)... Se quisermos comparar com "maior ou igual" a lógica fica SF!=OF ou ZF=1, que também existe um atalho: JLE. Para comparações "maior que", SF==OF.

No caso de valores sem sinal, precisamos apenas do CF e ZF. Como visto acima, se x < y, CF=1 e ZF=0, o processador tem um atalho: JB. Já se quisermos testar se x <= y podemos usar apenas CF, mesmo assim existe um "apelido" para JC: JBE.

Por que estou falando disso? Por causa de uma prática que vejo por ai com o uso de ponteiros... Alguns acreditam que existe uma diferença fundamental entre 'char' e 'unsigned char', onde o último representa 1 byte exatamente... AMBOS representam 1 byte exatamente, a diferença está apenas nas comparações entre variáveis desses tipos, mas a cópia e a aritmética inteira entre eles é exatamente a mesma. Declarar um ponteiro do tipo 'unsigned char *' ou 'char *', se não for fazer comparações entre os dados apontados, é exatamente a mesma coisa. O mesmo se aplica a 'short', 'int', 'long', 'long long'!

Repare que não há distinção, também, na aritmética com ponteiros:

char array[10];
char *p1 = array;
unsigned char *p2 = ( unsigned char * ) array;

p1++;  // aponta para array[1];
p2++  // também aponta para array[1]!

Nada te impede de fazer um casting na comparação, se precisar, quando tem arrays para char e quer tratar seus elementos como "bytes":

if ( ( unsigned )array[0] < array[1] )
...

Como a promoção de tipos encara o modificador 'unsigned' com maior prioridade do que o 'signed' (porque tem 1 bit de precisão extra), toda a expressão será promovida para 'unsigned'...

Outro detalhe é que ponteiros são, por definição, 'unsigned'. Ao comparar dois ponteiros o compilador preferirá usar instruções como JA, JB, JAE ou JBE, no caso de comparações de inequalidades (menor que, maior que e usando ou não o "ou igual"). Alguns podem pensar que a arquitetura x86-64 seja uma exceção a essa regra, já que os endereços têm que ser "canônicos" (ou seja, se o bit 47 do endereço for 1, todos os demais, até o MSB, tem que ser 1 também!). Mas, note que o endereço canônico não é uma representação em "complemento 2" e, por isso, as comparações continuam sendo feitas de maneira 'unsigned' (CF e ZF são usados, não SF e OF). Por exemplo:

$ cat test.c
int test( void *a, void *b ) { return a < b; }
$ cc -O2 -c -o test.o test.c
$ objdump -dM intel test.o
test.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <test>:
   0:	31 c0                	xor    eax,eax
   2:	48 39 f7             	cmp    rdi,rsi
   5:	0f 92 c0             	setb   al
   8:	c3                   	ret

Repare no 'setb'...

ATENÇÃO: Note que estou sendo enfático em dizer que a comparação entre tipos é diferente. Se você tentar comparar um tipo 'unsigned' com um 'signed', poderá obter resultados indesejados, se não souber o que está fazendo... Como eu disse antes, o compilador C promove o tipo de signed para unsigned... Assim, comparações como:

if ( x > -1 ) ...

Se x for 'unsigned', são perigosas... Esse -1 será promovido para 0xffffffff, que é o valor máximo de um 'int' (por exemplo) e x jamais poderá ser maior que isso... Ou seja, a expressão sempre será FALSA!

A indiferença dos tipos 'unsigned' e 'signed' aqui refere-se ao tamanho do armazenamento...

[]s
Fred

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...