Pourquoi augmenter les pointeurs?

25

J'ai récemment commencé à apprendre le C ++, et comme la plupart des gens (selon ce que j'ai lu), j'ai du mal avec les pointeurs.

Pas dans le sens traditionnel, je comprends ce qu'ils sont, pourquoi ils sont utilisés et comment peuvent-ils être utiles, mais je ne peux pas comprendre comment l'incrémentation des pointeurs serait utile, quelqu'un peut-il expliquer comment l'incrémentation d'un pointeur est un concept utile et idiomatique C ++?

Cette question est venue après que j'ai commencé à lire le livre A Tour of C ++ de Bjarne Stroustrup, on m'a recommandé ce livre, parce que je connais assez bien Java, et les gars de Reddit m'ont dit que ce serait un bon livre de `` basculement '' .

INdek
la source
11
Un pointeur n'est qu'un itérateur
Charles Salvia
1
C'est l'un des outils préférés pour écrire des virus informatiques qui lisent ce qu'ils ne devraient pas lire. C'est aussi l'un des cas de vulnérabilité les plus courants dans les applications (quand on incrémente un pointeur au-delà de la zone où ils sont censés, puis le lit ou l'écrit)> Voir le bogue HeartBleed.
Sam
1
@vasile C'est ce qui est mauvais avec les pointeurs.
Cruncher
4
La bonne / mauvaise chose à propos de C ++ est qu'il vous permet de faire beaucoup plus avant d'appeler un segfault. Habituellement, vous obtenez une erreur de segmentation lorsque vous essayez d'accéder à la mémoire d'un autre processus, à la mémoire système ou à la mémoire d'application protégée. Tout accès à l'intérieur des pages d'application habituelles est autorisé par le système, et c'est au programmeur / compilateur / au langage d'appliquer des limites raisonnables. C ++ vous permet à peu près de faire ce que vous voulez. Quant à openssl ayant son propre gestionnaire de mémoire - ce n'est pas vrai. Il a juste les mécanismes d'accès à la mémoire C ++ par défaut.
Sam
1
@INdek: vous n'obtiendrez un défaut de segmentation que si la mémoire à laquelle vous essayez d'accéder est protégée. La plupart des systèmes d'exploitation attribuent une protection au niveau de la page, vous pouvez donc généralement accéder à tout ce qui se trouve sur la page sur laquelle votre pointeur démarre. Si le système d'exploitation utilise une taille de page 4K, c'est une bonne quantité de données. Si votre pointeur commence quelque part dans le tas, tout le monde peut deviner combien de données vous pouvez accéder.
TMN

Réponses:

46

Lorsque vous avez un tableau, vous pouvez configurer un pointeur pour pointer vers un élément du tableau:

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

Indique ici ple premier élément de a, qui est a[0]. Vous pouvez maintenant incrémenter le pointeur pour pointer vers l'élément suivant:

p++;

Maintenant , les ppoints au deuxième élément, a[1]. Vous pouvez accéder à l'élément ici en utilisant *p. C'est différent de Java où vous devez utiliser une variable d'index entier pour accéder aux éléments d'un tableau.

L'incrémentation d'un pointeur en C ++ où ce pointeur ne pointe pas vers un élément d'un tableau est un comportement non défini .

Greg Hewgill
la source
23
Oui, avec C ++, vous êtes responsable d'éviter les erreurs de programmation telles que l'accès en dehors des limites d'un tableau.
Greg Hewgill
9
Non, incrémenter un pointeur qui pointe vers n'importe quoi sauf un élément de tableau est un comportement non défini. Cependant, si vous faites quelque chose de bas niveau et non portable, incrémenter un pointeur n'est généralement rien de plus que d'accéder à la prochaine chose en mémoire, quelle qu'elle soit.
Greg Hewgill
4
Il y a quelques éléments qui sont ou peuvent être traités comme un tableau; une chaîne de texte est en fait un tableau de caractères. Dans certains cas, un long int est traité comme un tableau d'octets, bien que cela puisse facilement vous causer des ennuis.
AMADANON Inc.
6
Cela vous indique le type , mais le comportement est décrit dans 5.7 Opérateurs additifs [expr.add]. Plus précisément, 5.7 / 5 dit qu'aller n'importe où en dehors du tableau, sauf un-dernier-bout, c'est UB.
inutile
4
Le dernier paragraphe est le suivant: si l'opérande pointeur et le résultat pointent tous les deux vers des éléments du même objet tableau, l'évaluation ne doit pas produire de débordement; sinon, le comportement n'est pas défini . Donc, si le résultat n'est ni dans le tableau ni après la fin, vous obtenez UB.
Inutile
37

L'incrémentation des pointeurs est idiomatique en C ++, car la sémantique des pointeurs reflète un aspect fondamental de la philosophie de conception derrière la bibliothèque standard C ++ (basée sur la STL d'Alexander Stepanov )

Le concept important ici est que la STL est conçue autour de conteneurs, d'algorithmes et d'itérateurs. Les pointeurs sont simplement des itérateurs .

Bien sûr, la possibilité d'incrémenter (ou d'ajouter / de soustraire) des pointeurs remonte à C. De nombreux algorithmes de manipulation de chaîne C peuvent être écrits simplement en utilisant l'arithmétique des pointeurs. Considérez le code suivant:

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

Ce code utilise l'arithmétique du pointeur pour copier une chaîne C terminée par un caractère nul. La boucle se termine automatiquement lorsqu'elle rencontre le null.

Avec C ++, la sémantique des pointeurs est généralisée au concept d' itérateurs . La plupart des conteneurs C ++ standard fournissent des itérateurs, accessibles via les fonctions membres beginet end. Les itérateurs se comportent comme des pointeurs, en ce sens qu'ils peuvent être incrémentés, déréférencés et parfois décrémentés ou avancés.

Pour parcourir un std::string, nous dirions:

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

Nous incrémentons l'itérateur comme nous incrémenterions un pointeur sur une chaîne en C simple. La raison pour laquelle ce concept est puissant est que vous pouvez utiliser des modèles pour écrire des fonctions qui fonctionneront pour tout type d'itérateur qui répond aux exigences de concept nécessaires. Et c'est la puissance de la STL:

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

Ce code copie une chaîne dans un vecteur. La copyfonction est un modèle qui fonctionnera avec tout itérateur qui prend en charge l'incrémentation (qui inclut des pointeurs simples). Nous pourrions utiliser la même copyfonction sur une chaîne C simple:

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

Nous pourrions utiliser copysur un std::mapou un std::setou tout conteneur personnalisé prenant en charge les itérateurs.

Notez que les pointeurs sont un type spécifique d'itérateur: itérateur à accès aléatoire , ce qui signifie qu'ils prennent en charge l'incrémentation, la décrémentation et l'avancement avec l' opérateur +et -. D'autres types d'itérateurs ne prennent en charge qu'un sous-ensemble de sémantique de pointeur: un itérateur bidirectionnel prend en charge au moins l'incrémentation et la décrémentation; un itérateur direct prend en charge au moins l'incrémentation. (Tous les types d'itérateurs prennent en charge le déréférencement.) La copyfonction nécessite un itérateur qui prend au moins en charge l'incrémentation.

Vous pouvez lire sur les différents concepts de iterator ici .

Ainsi, l'incrémentation de pointeurs est une façon idiomatique C ++ d'itérer sur un tableau C ou d'accéder à des éléments / décalages dans un tableau C.

Charles Salvia
la source
3
Bien que j'utilise des pointeurs comme dans le premier exemple, je n'y ai jamais pensé en tant qu'itérateur, cela a beaucoup de sens maintenant.
dyesdyes
1
"La boucle se termine automatiquement lorsqu'elle rencontre le null." Ceci est un idiome terrifiant.
Charles Wood
9
@CharlesWood, alors je suppose que vous devez trouver C assez terrifiant
Siler
7
@CharlesWood: L'alternative consiste à utiliser la longueur de la chaîne comme variable de contrôle de boucle, ce qui signifie parcourir la chaîne deux fois (une fois pour déterminer la longueur et une fois pour copier les caractères). Lorsque vous utilisez un PDP-7 à 1 MHz, cela peut vraiment commencer à s'additionner.
TMN
3
@INdek: tout d'abord, le C et le C ++ essaient d'éviter à tout prix d'introduire des changements de rupture - et je dirais que modifier le comportement par défaut des littéraux de chaîne serait tout à fait une modification. Mais plus important encore, les chaînes terminées par zéro ne sont qu'une convention (rendue facile à suivre par le fait que les littéraux de chaîne sont terminés par défaut par zéro et que les fonctions de bibliothèque les attendent), personne ne vous empêche d'utiliser des chaînes comptées en C - en fait, plusieurs bibliothèques C les utilisent (voir par exemple le BSTR d'OLE).
Matteo Italia
16

L'arithmétique du pointeur est en C ++ parce qu'elle était en C. L'arithmétique du pointeur est en C parce que c'est un idiome normal dans l' assembleur .

Il existe de nombreux systèmes où "increment register" est plus rapide que "load constant value 1 and add to register". De plus, plusieurs systèmes vous permettent de "charger DWORD dans A à partir de l'adresse spécifiée dans le registre B, puis d'ajouter sizeof (DWORD) à B" dans une seule instruction. De nos jours, vous pourriez vous attendre à ce qu'un compilateur d'optimisation trie cela pour vous, mais ce n'était pas vraiment une option en 1973.

C'est essentiellement la même raison pour laquelle les tableaux C ne sont pas vérifiés et que les chaînes C n'ont pas de taille intégrée: le langage a été développé sur un système où chaque octet et chaque instruction comptent.

pjc50
la source