Aritmética de ponteiros em C [RESOLVIDO]

1. Aritmética de ponteiros em C [RESOLVIDO]

Bell Coutinho
BellCoutinho

(usa Arch Linux)

Enviado em 12/12/2018 - 09:53h

Pessoal, como eu posso percorrer uma matriz dinamicamente alocada com aritmética de ponteiros?

Desde já Obrigado


  


2. MELHOR RESPOSTA

Fernando
phoemur

(usa Debian)

Enviado em 12/12/2018 - 20:47h

Quanto à pergunta original do tópico, você faria algo assim:
#include <iostream>

int main()
{
int** matrix = new int*[5];
for (int i = 0; i < 5; ++i)
*(matrix+i) = new int[5];

for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < 5; ++j)
{
std::cout << *(*(matrix+i)+j) << " ";
}
std::cout << "\n";
}

for (int i = 0; i < 5; ++i)
delete[] *(matrix+i);

delete[] matrix;

return 0;
}


Eu acho esquisito, porém pior do que isso, é também perigoso.

Por exemplo se ao invés de usar
 std::cout << *(*(matrix+i)+j) << " "; 

você usasse:
 std::cout << *(matrix+i+j) << " "; 

Coisas terríveis aconteceriam...rs

______________________
https://github.com/phoemur

3. Re: Aritmética de ponteiros em C

Paulo
paulo1205

(usa Ubuntu)

Enviado em 12/12/2018 - 16:26h

O jeito mais simples é usando os operadores de indexação, que são os colchetes ([ e ]).

Se p é um ponteiro para um tipo de dados X (diferente de void) e n é um inteiro qualquer, o acesso ao (n-1)-ésimo elemento do tipo X a partir do endereço indicado por p pode ser feito tanto com “p[n]” (mais comum) quanto com “n[p]” (incomum, mas válido; não use, porque só usa quem quer obscurecer o código), e ambos são sinônimos de “*(p+n)” e de “*(n+p)”. Se você quiser os endereços de cada elemento, em vez de os elementos em si, pode usar “p+n”, “n+p”, “&p[n]” ou “&n[p]”.

Observe que em C um array decai automaticamente para ponteiro em quase todas as expressões (as exceções são no momento da declaração e quando o array é passado como argumento dos operadores sizeof ou &). Sendo assim, o operador de indexação sempre atua sobre ponteiros e índices inteiros, mesmo que o ponteiro seja obtido pelo decaimento de um nome que designe um array.

Isso é inicialmente estranho para quem está acostumado com outras linguagens de programação que não abrem detalhes de ponteiros ou sobre arrays, mas é internamente coerente. Por exemplo, assim como você não pode usar aritmética de ponteiros sobre ponteiros para void, também não pode ter arrays de void (nem dados escalares do tipo void).

Se você por acaso tiver ponteiros para void, tem de convertê-lo em algum outro tipo de ponteiro, antes de poder indexá-lo ou de aceder ao seu conteúdo.


4. Re: Aritmética de ponteiros em C

Paulo
paulo1205

(usa Ubuntu)

Enviado em 12/12/2018 - 18:44h

Um comentário adicional (baseado em opinião minha).

A “aritmética de ponteiros” perdeu relevância na medida em que os programadores em C passaram a adotar práticas de programação mais seguras, e que os próprios compiladores passaram a reclamar mais de práticas antigas e que hoje são consideradas inseguras e perigosas.

Essa discussão era muito mais importante, e alvo de confusão, numa época — que já vai remota — em que ponteiros e variáveis inteiras eram usadas de modo intercambiável. Ninguém em são consciência faz isso gratuitamente hoje em dia (e, quando o faz, geralmente cerca o código dos devidos cuidados sintáticos e visuais).

O problema acontecia no passado em código semelhante ao que vai abaixo, que penso que pode ilustrar o problema.
id(p){ return p; }

main(){
int v[]={1, 2, 3};
int *a, *b, *c, *d;
a=v+1; /* Aritmética de ponteiro. OK. */
b=id(v)+1; /* Ponteiro se perde. Aritmética com dois inteiros. */
c=id(v+1); /* Aritmética de ponteiros antes da função. Depois o ponteiro se perde. */
d=id(v)+sizeof *v; /* Ponteiro se perde. Valor certo do endereço obtido manualmente. */
printf("%d %d %d %d %d\n", &v[1], a, b, c, d);
printf("%d %d %d %d %d\n", v[1], *a, *b, *c, *d);
}

Esse código é obsoleto de várias maneiras mas, em particular, a função id(), que supostamente retorna a mesma coisa que recebe como argumento, não especifica nem o tipo de dados do argumento nem o tipo do dado retornado. Nesse caso, o que acontece é que ambos são assumidos como sendo int. E como, em muitas máquinas antigas (e ainda algumas máquinas atuais), frequentemente int e ponteiros têm a mesma representação interna (que tipicamente coincidia com o tipo dos registradores de uso comum no processador), essa coincidência era assumida como líquida e certa e até como desejável. O compilador não reclamava, e todos eram felizes.

Exceto quando tinha que acontecer alguma operação aritmética com os dados. Com arrays e ponteiros, somar 1 (ou n) significa ir para o próximo elemento (ou para n posições depois do início) e, nos casos em que o tamanho de cada elemento é maior do que um byte, isso provocava um deslocamento diferente do que ocorreria com a soma pura entre dois inteiros.

Os cuidados com a aritmética de ponteiros eram, então, uma abordagem para lembrar ao usuário que, embora ponteiros e inteiros pudessem ser usados de modo intercambiável em geral, ele deveria ter cuidado com suas operações se um dos operandos fosse um ponteiro, porque o resultado poderia ser diferente do que ele esperaria em uma soma com dois inteiros. Ao mesmo tempo, ele deveria lembrar também que se ele precisasse operar com um inteiro que viesse a se tornar ponteiro no futuro, os deslocamentos teriam de ser multiplicados pelos tamanhos dos dados que viessem a ser apontados, caso contrário, ele poderia acabar ficando com um valor que, quando usado como endereço (ponteiro), seria inválido.

Veja alguns resultados, num PC com 32 e com 64 bits (o PC aceita endereçar inteiros em endereços que não sejam múltiplos de sizeof(int)), numa SUN UltraSparc em 32 e 64 bits (que não aceita endereços que não sejam múltiplos do tamanho do dado) e num IBM Power em 32 e 64 bits (que também aceita dados em endereços que não são múltiplos do tamanho do dado).

$ ./linuxpc32 ; ./linuxpc64
-3798332 -3798332 -3798335 -3798332 -3798332
2 2 33554432 2 2
-755092352 -755092352 -755092355 -755092352 -755092352
Segmentation fault (core dumped)

$ ./solaris32 ; ./solaris64
-4195232 -4195232 -4195235 -4195232 -4195232
Bus Error (core dumped)
2147482408 2147482408 2147482405 2147482408 2147482408
Bus Error (core dumped)

$ ./aix32 ; ./aix64
804398476 804398476 804398473 804398476 804398476
2 2 256 2 2
-2972 -2972 -2975 -2972 -2972
Segmentation fault (core dumped)


No PC e no Power, as versões em 32 bits funcionaram de modo parecido: como inteiros e ponteiros têm o mesmo tamanho, o programa rodou até o fim, mas produziu endereços diferentes para o ponteiro b de todos os demais, e d só ficou com o valor certo porque fizemos a computação manual. Na impressão dos valores, o obtido através de b ficou diferente dos demais (no PC, cujos dados são com byte menos significativos primeiro, o valor impresso foi o equivalente a 0x02000000, onde o “02” é o primeiro byte do segundo elemento, e os três “00” são os três últimos bytes do primeiro; no AIX, em que o byte mais significativo vem primeiro, o valor impresso foi o equivalente a 0x00000100, em que “000001” corresponde aos três últimos bytes do primeiro elemento, e o “00” vem do primeiro byte do segundo elemento). No processador Sparc, dados não-alinhados provocam erro de barramento, de modo que nenhum dado foi impresso (o programa capota ao tentar fazê-lo), mas apenas os endereços (antes de capotar), com discrepâncias entre si semelhantes às vistas no PC e no Power.

Em 64 bits, acontece um problema maior: inteiros e ponteiros não têm mais tamanhos coincidentes (inteiros têm 32 bits, ponteiros têm 64 bits), de modo que possivelmente a conversão de ponteiro para inteiro provoca truncamento de informação, e a conversão de inteiros para ponteiros pode produzir endereços completamente inválidos para o programa. Por isso, PC e Power caíram com falha de segmentação (endereços inválidos após as conversões). No Sparc, novamente tivemos erro de barramento — possivelmente, por uma fortuita coincidência, o endereço não usava os bits de 33 em diante, cabendo nos 32 bits. Mas embora o endereço não tenha sido perdido, o valor calculado para b, sem aritmética de ponteiro, continua desalinhado, e portanto inválido.


A antiga coincidência de representação interna entre ponteiros e inteiros induzia a seu uso intercambiável, e as pessoas eram instadas a prestar atenção no contexto para saber qual tipo de aritmética usar. Com a mudança de tecnologia, que fez mais comuns ponteiros e inteiros que não são mais intercambiáveis, a própria linguagem passou a ser menos tolerante com o uso intercambiável. Isso levou o uso intercambiável a ficar mais e mais raro com o tempo. Hoje, só alguém que parou no tempo, que ainda esteja usando livros obsoletos e compiladores da década de 1990 (infelizmente ainda existe, especialmente em faculdades brasileiras do tipo Zé-das-Couves) é que ainda confunde ponteiros com inteiros. A aritmética de ponteiros é hoje mais uma curiosidade que só aparece quando você faz força para ver “como é um ponteiro por dentro” do que uma preocupação do dia-a-dia.

E isso me faz ficar curioso: por que você fez a pergunta?


----

Recomendo a leitura de um artigo sobre a história do C, desde priscas eras, escrito por seu próprio criador, Dennis M. Ritchie (já falecido): https://www.bell-labs.com/usr/dmr/www/chist.html.


5. Re: Aritmética de ponteiros em C [RESOLVIDO]

Fernando
phoemur

(usa Debian)

Enviado em 12/12/2018 - 20:29h

Paulo como sempre dando show de conhecimento. Saiba que eu admiro muito suas explicações aqui no VOL.
Obrigado.

Atualmente, que eu me lembre assim de pronto, eu costumo utilizar aritmética de ponteiros ainda atualmente quando vou usar algoritmos da STL do C++ sobre arrays comuns do C.

Um exemplo bobo:
#include <algorithm>
#include <iostream>
#include <functional>
#include <numeric>

int main()
{
int arr[5];
std::iota(arr, arr+5, 1);

int fatorial5 = std::accumulate(arr, arr+5, 1, std::multiplies<int>{});

std::cout << fatorial5 << std::endl;

return 0;
}


Naturalmente eu poderia utilizar também assim que é a mesma coisa:

std::iota(&arr[0], &arr[5], 1);


ou simplesmente - o que seria a melhor forma:

std::iota(std::begin(arr), std::end(arr), 1);


Contudo esse &arr[5] (apesar de ser a mesma coisa que arr + 5) além de ter uma verbosidade maior, parece meio estranho ao indicar o acesso ao elemento depois do último do array. Parece que acende uma luz amarela toda vez que leio isso.

Forte abraço
______________________
https://github.com/phoemur


6. Re: Aritmética de ponteiros em C

Bell Coutinho
BellCoutinho

(usa Arch Linux)

Enviado em 12/12/2018 - 21:28h

paulo1205 escreveu:

Um comentário adicional (baseado em opinião minha).

A “aritmética de ponteiros” perdeu relevância na medida em que os programadores em C passaram a adotar práticas de programação mais seguras, e que os próprios compiladores passaram a reclamar mais de práticas antigas e que hoje são consideradas inseguras e perigosas.

Essa discussão era muito mais importante, e alvo de confusão, numa época — que já vai remota — em que ponteiros e variáveis inteiras eram usadas de modo intercambiável. Ninguém em são consciência faz isso gratuitamente hoje em dia (e, quando o faz, geralmente cerca o código dos devidos cuidados sintáticos e visuais).

O problema acontecia no passado em código semelhante ao que vai abaixo, que penso que pode ilustrar o problema.
id(p){ return p; }

main(){
int v[]={1, 2, 3};
int *a, *b, *c, *d;
a=v+1; /* Aritmética de ponteiro. OK. */
b=id(v)+1; /* Ponteiro se perde. Aritmética com dois inteiros. */
c=id(v+1); /* Aritmética de ponteiros antes da função. Depois o ponteiro se perde. */
d=id(v)+sizeof *v; /* Ponteiro se perde. Valor certo do endereço obtido manualmente. */
printf("%d %d %d %d %d\n", &v[1], a, b, c, d);
printf("%d %d %d %d %d\n", v[1], *a, *b, *c, *d);
}

Esse código é obsoleto de várias maneiras mas, em particular, a função id(), que supostamente retorna a mesma coisa que recebe como argumento, não especifica nem o tipo de dados do argumento nem o tipo do dado retornado. Nesse caso, o que acontece é que ambos são assumidos como sendo int. E como, em muitas máquinas antigas (e ainda algumas máquinas atuais), frequentemente int e ponteiros têm a mesma representação interna (que tipicamente coincidia com o tipo dos registradores de uso comum no processador), essa coincidência era assumida como líquida e certa e até como desejável. O compilador não reclamava, e todos eram felizes.

Exceto quando tinha que acontecer alguma operação aritmética com os dados. Com arrays e ponteiros, somar 1 (ou n) significa ir para o próximo elemento (ou para n posições depois do início) e, nos casos em que o tamanho de cada elemento é maior do que um byte, isso provocava um deslocamento diferente do que ocorreria com a soma pura entre dois inteiros.

Os cuidados com a aritmética de ponteiros eram, então, uma abordagem para lembrar ao usuário que, embora ponteiros e inteiros pudessem ser usados de modo intercambiável em geral, ele deveria ter cuidado com suas operações se um dos operandos fosse um ponteiro, porque o resultado poderia ser diferente do que ele esperaria em uma soma com dois inteiros. Ao mesmo tempo, ele deveria lembrar também que se ele precisasse operar com um inteiro que viesse a se tornar ponteiro no futuro, os deslocamentos teriam de ser multiplicados pelos tamanhos dos dados que viessem a ser apontados, caso contrário, ele poderia acabar ficando com um valor que, quando usado como endereço (ponteiro), seria inválido.

Veja alguns resultados, num PC com 32 e com 64 bits (o PC aceita endereçar inteiros em endereços que não sejam múltiplos de sizeof(int)), numa SUN UltraSparc em 32 e 64 bits (que não aceita endereços que não sejam múltiplos do tamanho do dado) e num IBM Power em 32 e 64 bits (que também aceita dados em endereços que não são múltiplos do tamanho do dado).

$ ./linuxpc32 ; ./linuxpc64
-3798332 -3798332 -3798335 -3798332 -3798332
2 2 33554432 2 2
-755092352 -755092352 -755092355 -755092352 -755092352
Segmentation fault (core dumped)

$ ./solaris32 ; ./solaris64
-4195232 -4195232 -4195235 -4195232 -4195232
Bus Error (core dumped)
2147482408 2147482408 2147482405 2147482408 2147482408
Bus Error (core dumped)

$ ./aix32 ; ./aix64
804398476 804398476 804398473 804398476 804398476
2 2 256 2 2
-2972 -2972 -2975 -2972 -2972
Segmentation fault (core dumped)


No PC e no Power, as versões em 32 bits funcionaram de modo parecido: como inteiros e ponteiros têm o mesmo tamanho, o programa rodou até o fim, mas produziu endereços diferentes para o ponteiro b de todos os demais, e d só ficou com o valor certo porque fizemos a computação manual. Na impressão dos valores, o obtido através de b ficou diferente dos demais (no PC, cujos dados são com byte menos significativos primeiro, o valor impresso foi o equivalente a 0x02000000, onde o “02” é o primeiro byte do segundo elemento, e os três “00” são os três últimos bytes do primeiro; no AIX, em que o byte mais significativo vem primeiro, o valor impresso foi o equivalente a 0x00000100, em que “000001” corresponde aos três últimos bytes do primeiro elemento, e o “00” vem do primeiro byte do segundo elemento). No processador Sparc, dados não-alinhados provocam erro de barramento, de modo que nenhum dado foi impresso (o programa capota ao tentar fazê-lo), mas apenas os endereços (antes de capotar), com discrepâncias entre si semelhantes às vistas no PC e no Power.

Em 64 bits, acontece um problema maior: inteiros e ponteiros não têm mais tamanhos coincidentes (inteiros têm 32 bits, ponteiros têm 64 bits), de modo que possivelmente a conversão de ponteiro para inteiro provoca truncamento de informação, e a conversão de inteiros para ponteiros pode produzir endereços completamente inválidos para o programa. Por isso, PC e Power caíram com falha de segmentação (endereços inválidos após as conversões). No Sparc, novamente tivemos erro de barramento — possivelmente, por uma fortuita coincidência, o endereço não usava os bits de 33 em diante, cabendo nos 32 bits. Mas embora o endereço não tenha sido perdido, o valor calculado para b, sem aritmética de ponteiro, continua desalinhado, e portanto inválido.


A antiga coincidência de representação interna entre ponteiros e inteiros induzia a seu uso intercambiável, e as pessoas eram instadas a prestar atenção no contexto para saber qual tipo de aritmética usar. Com a mudança de tecnologia, que fez mais comuns ponteiros e inteiros que não são mais intercambiáveis, a própria linguagem passou a ser menos tolerante com o uso intercambiável. Isso levou o uso intercambiável a ficar mais e mais raro com o tempo. Hoje, só alguém que parou no tempo, que ainda esteja usando livros obsoletos e compiladores da década de 1990 (infelizmente ainda existe, especialmente em faculdades brasileiras do tipo Zé-das-Couves) é que ainda confunde ponteiros com inteiros. A aritmética de ponteiros é hoje mais uma curiosidade que só aparece quando você faz força para ver “como é um ponteiro por dentro” do que uma preocupação do dia-a-dia.

E isso me faz ficar curioso: por que você fez a pergunta?


----

Recomendo a leitura de um artigo sobre a história do C, desde priscas eras, escrito por seu próprio criador, Dennis M. Ritchie (já falecido): https://www.bell-labs.com/usr/dmr/www/chist.html.



Foi exatamente isso, curiosidade. Meu professor fez um dia uma aula e mostrou aritmética de ponteiros mas eu não me lembrava como era.


7. Re: Aritmética de ponteiros em C [RESOLVIDO]

Paulo
paulo1205

(usa Ubuntu)

Enviado em 13/12/2018 - 11:25h

Eu escrevi à beça, mas acho que não falei o que interessava. Eu me detive muito no aspecto histórico e na confusão decorrente dos abusos e das práticas obsoletas, e não no que era e continua sendo útil.


Sempre existiram e ainda existem operações aritméticas revelantes sobre ponteiros, e elas são as seguintes:

  • incremento (++), que faz o ponteiro apontar para o elemento seguinte;
  • decremento (--), faz com que o ponteiro aponte para o elemento imediatamente anterior;
  • deslocamento para frente (+, +=), que move o ponteiro para frente n elementos em relação à posição atual (sendo n inteiro);
  • deslocamento para trás (-, -=), que move o ponteiro n posições para trás (sendo n inteiro); e
  • medida de distância entre elementos de um mesmo array (- com dois operandos ponteiros), de modo que, sendo p1 e p2 ponteiros do mesmo tipo de dados e apontando para elementos do mesmo array, a expressão p2-p1 produz um valor inteiro n tal que satisfaria as operações inversas p1+n==p2 e p2-n==p1.

Essas operações são muito úteis e muito utilizadas (principalmente incremento), e são preferíveis ao uso de arrays em muitas situações. Por exemplo, compare as seguintes duas possíveis implementações de strcmp().
// Exemplo 1: usando notação de arrays.
int strcmp(const char *s1, const char *s2){
int i, diff;
i=0;
do
diff=s2[i]-s1[i];
while(diff==0 && s1[i] && s2[i++]);
return diff;
}

// Exemplo 2: usando aritmética de ponteiros.
int strcmp(const char *s1, const char *s2){
int diff;
do
diff=*s2-*s1;
while(diff==0 && *s1++ && *s2++);
return diff;
}







Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts