Por que incrementar indicadores?

25

Recentemente, comecei a aprender C ++ e, como a maioria das pessoas (de acordo com o que tenho lido), estou tendo dificuldades com indicadores.

Não no sentido tradicional, eu entendo o que são, e por que são usados, e como podem ser úteis; no entanto, não consigo entender como os ponteiros de incremento seriam úteis, alguém pode fornecer uma explicação de como o incremento de um ponteiro é um conceito útil e C ++ idiomático?

Essa pergunta surgiu depois que comecei a ler o livro A Tour of C ++, de Bjarne Stroustrup, fui recomendado por este livro, porque eu conheço bem o Java, e os caras do Reddit me disseram que seria um bom livro de 'troca' .

INdek
fonte
11
Um ponteiro é apenas um iterador
Charles Salvia
1
É uma das ferramentas favoritas para escrever vírus de computador que leem o que não devem ler. Esse também é um dos casos mais comuns de vulnerabilidade nos aplicativos (quando alguém passa um ponteiro pela área em que deveria, depois o lê ou grava)> Veja o bug do HeartBleed.
Sam
1
@vasile É isso que há de ruim nos ponteiros.
Cruncher
4
O bom / ruim do C ++ é que ele permite que você faça muito mais antes de chamar um segfault. Geralmente, você recebe um segfault ao tentar acessar a memória de outro processo, a memória do sistema ou a memória do aplicativo protegido. Qualquer acesso dentro das páginas usuais do aplicativo é permitido pelo sistema, e cabe ao programador / compilador / idioma impor limites razoáveis. C ++ praticamente permite que você faça o que quiser. Quanto ao openssl ter seu próprio gerenciador de memória - isso não é verdade. Apenas possui os mecanismos de acesso à memória C ++ padrão.
Sam
1
@INdek: Você só receberá um segfault se a memória que você está tentando acessar estiver protegida. A maioria dos sistemas operacionais atribui proteção no nível da página, para que você possa acessar normalmente qualquer coisa que esteja na página em que o ponteiro inicia. Se o sistema operacional usa um tamanho de página de 4K, é uma quantidade razoável de dados. Se o ponteiro começar em algum lugar da pilha, é possível adivinhar quantos dados você pode acessar.
TMN

Respostas:

46

Quando você tem uma matriz, pode configurar um ponteiro para apontar para um elemento da matriz:

int a[10];
int *p = &a[0];

Aqui paponta para o primeiro elemento de a, que é a[0]. Agora você pode incrementar o ponteiro para apontar para o próximo elemento:

p++;

Agora paponta para o segundo elemento a[1],. Você pode acessar o elemento aqui usando *p. Isso é diferente de Java, onde você precisaria usar uma variável de índice inteiro para acessar elementos de uma matriz.

Incrementar um ponteiro em C ++ em que esse ponteiro não aponta para um elemento de uma matriz é um comportamento indefinido .

Greg Hewgill
fonte
23
Sim, com o C ++, você é responsável por evitar erros de programação, como acesso fora dos limites de uma matriz.
Greg Hewgill
9
Não, incrementar um ponteiro que aponte para qualquer coisa, exceto um elemento da matriz, é um comportamento indefinido. No entanto, se você estiver fazendo algo de baixo nível e não portátil, incrementar um ponteiro geralmente nada mais é do que acessar a próxima coisa na memória, seja lá o que for.
Greg Hewgill 01/08/14
4
Existem algumas coisas que são ou podem ser tratadas como uma matriz; uma sequência de texto é, de fato, uma matriz de caracteres. Em alguns casos, um int longo é tratado como uma matriz de bytes, embora isso possa causar problemas facilmente.
AMADANON Inc.
6
Isso indica o tipo , mas o comportamento é descrito em 5.7 Operadores aditivos [expr.add]. Especificamente, 5.7 / 5 diz que ir a qualquer lugar fora da matriz, exceto um após o fim, é UB.
inútil
4
O último parágrafo é: se o operando do ponteiro e o resultado apontarem para elementos do mesmo objeto de matriz, a avaliação não produzirá um estouro; caso contrário, o comportamento é indefinido . Portanto, se o resultado não estiver na matriz nem no passado, você receberá UB.
Useless
37

Incrementar ponteiros é C ++ idiomático, porque a semântica de ponteiros reflete um aspecto fundamental da filosofia de design por trás da biblioteca padrão C ++ (baseada na STL de Alexander Stepanov )

O conceito importante aqui é que o STL é projetado em torno de contêineres, algoritmos e iteradores. Ponteiros são simplesmente iteradores .

Obviamente, a capacidade de incrementar (ou adicionar / subtrair) ponteiros remonta a C. Muitos algoritmos de manipulação de cordas C podem ser escritos simplesmente usando a aritmética dos ponteiros. Considere o seguinte código:

char string1[4] = "abc";
char string2[4];
char* src = string1;
char* dest = string2;
while ((*dest++ = *src++));

Esse código usa a aritmética do ponteiro para copiar uma cadeia C terminada em nulo. O loop termina automaticamente quando encontra o nulo.

Com o C ++, a semântica do ponteiro é generalizada para o conceito de iteradores . A maioria dos contêineres C ++ padrão fornece iteradores, que podem ser acessados ​​através das funções-membro begine end. Os iteradores se comportam como ponteiros, pois podem ser incrementados, desreferenciados e, às vezes, decrementados ou avançados.

Para iterar sobre um std::string, diríamos:

std::string s = "abcdef";
std::string::iterator it = s.begin();
for (; it != s.end(); ++it) std::cout << *it;

Nós incrementamos o iterador da mesma forma que incrementamos um ponteiro para uma string C simples. A razão pela qual esse conceito é poderoso é porque você pode usar modelos para escrever funções que funcionarão para qualquer tipo de iterador que atenda aos requisitos de conceito necessários. E este é o poder do STL:

std::string s1 = "abcdef";
std::vector<char> buf;
std::copy(s1.begin(), s1.end(), std::back_inserter(buf));

Este código copia uma string em um vetor. A copyfunção é um modelo que funcionará com qualquer iterador que ofereça suporte ao incremento (que inclui ponteiros simples). Poderíamos usar a mesma copyfunção em uma string C simples:

   const char* s1 = "abcdef";
   std::vector<char> buf;
   std::copy(s1, s1 + std::strlen(s1), std::back_inserter(buf));

Poderíamos usar copyem um std::mapou um std::setou qualquer contêiner personalizado que suporte iteradores.

Observe que os ponteiros são um tipo específico de iterador: iterador de acesso aleatório , o que significa que eles oferecem suporte para incrementar, decrementar e avançar com o operador +e -. Outros tipos de iteradores suportam apenas um subconjunto de semânticas de ponteiros: um iterador bidirecional suporta pelo menos incrementos e decrementos; um iterador direto suporta pelo menos incrementos. (Todos os tipos de iteradores suportam a desreferenciação.) A copyfunção requer um iterador que suporte pelo menos o incremento.

Você pode ler sobre diferentes conceitos de iteradores aqui .

Portanto, incrementar ponteiros é uma maneira idiomática do C ++ para iterar em um array C ou acessar elementos / deslocamentos em um array C.

Charles Salvia
fonte
3
Embora eu use ponteiros como no primeiro exemplo, nunca pensei nisso como um iterador, agora faz muito sentido.
Dyesdyes
1
"O loop termina automaticamente quando encontra o nulo." Este é um idioma aterrorizante.
Charles Wood
9
@CharlesWood, então eu acho que você deve encontrar C bastante aterrorizante
Siler
7
@CharlesWood: A alternativa é usar o comprimento da string como variável de controle de loop, o que significa percorrer a string duas vezes (uma vez para determinar o comprimento e outra para copiar os caracteres). Quando você está rodando em um 1MHz PDP-7, isso pode realmente começar a aumentar.
TMN
3
@ INdek: em primeiro lugar, C e C ++ tentam evitar a todo custo introduzir alterações de última hora - e eu diria que alterar o comportamento padrão dos literais de string seria uma modificação. Porém, o mais importante é que as strings com terminação zero são apenas uma convenção (facilitada pelo fato de que os literais são terminados com zero por padrão e que as funções da biblioteca esperam por eles), ninguém o impede de usar strings contadas em C - na verdade, várias bibliotecas C as utilizam (consulte, por exemplo, o BSTR do OLE).
Matteo Italia
16

A aritmética de ponteiro está em C ++ porque estava em C. A aritmética de ponteiro está em C porque é um idioma normal no assembler .

Existem muitos sistemas em que o "incremento de registro" é mais rápido que "carrega o valor constante 1 e adiciona ao registro". Além disso, muitos sistemas permitem que você "carregue DWORD em A do endereço especificado no registro B e adicione sizeof (DWORD) a B" em uma única instrução. Atualmente, você pode esperar que um compilador otimizador resolva isso para você, mas isso não era realmente uma opção em 1973.

Esta é basicamente a mesma razão pela qual as matrizes C não são verificadas por limites e as seqüências C não têm um tamanho incorporado: a linguagem foi desenvolvida em um sistema em que cada byte e cada instrução contavam.

pjc50
fonte