Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.2.2 Aritmética de punteros

§1 Sinopsis

La aritmética de punteros se limita a suma, resta, comparación y asignación. Las operaciones aritméticas en los punteros de tipoX (punteros-a-tipoX) tienen automáticamente en cuenta el tamaño real de tipoX. Es decir, el número de bytes necesario para almacenar un objeto tipoX [2]. Por ejemplo, suponiendo una matriz de double con 100 elementos, si ptr es un puntero a dicha matriz, la sentencia ptr++; supone incrementar el Rvalue de ptr en 6.400 bits, porque el tamaño de la matriz es precisamente 100x64 bits.

Nota: no confundir el puntero-a-matriz con un puntero a su primer elemento (que aquí sería puntero-a-double).

La aritmética realizada internamente en los punteros depende del modelo de memoria en uso y de la presencia de cualquier modificador superpuesto.

  Las operaciones que implican dos punteros exigen que sean del mismo tipo o se realice previamente un modelado apropiado .

§2 Operaciones permitidas

Sean ptr1, ptr2 punteros a objetos del mismo tipo, y n un tipo entero o una enumeración; las operaciones permitidas y los resultados obtenidos con ellas son:

Operación Resultado Comentario
pt1++ puntero Desplazamiento ascendente de 1 elemento
pt1-- puntero Desplazamiento descendente de 1 elemento
pt1 + n puntero Desplazamiento ascendente n elementos  [4]
pt1 - n puntero Desplazamiento descendente n elementos [4]
pt1 - pt2 entero Distancia entre elementos
pt1 == NULL booleano Siempre se puede comprobar la igualdad o desigualdad con NULL
pt1 != NULL booleano ( 3.2.1b)
pt1 <R> pt2 booleano <R> es una expresión relacional ( 4.9.12)
pt1 = pt2 puntero Asignación
pt1 = void puntero genérico Asignación


La comparación de punteros solo tiene sentido entre punteros a elementos de la misma matriz; en estas condiciones los operadores relacionales ( 4.9.12): ==!=, <>, <=, >=, funcionan correctamente.


§3 Homos señalado que cuando se realizan operaciones aritméticas con punteros, se tiene en cuenta el tamaño de los objetos apuntados, de modo que si un puntero es declarado apuntando-a-tipoX, añadirle un entero n (al puntero) supone hacerlo hace avanzar un número n de objetos tipoX. Si tipoX tiene un tamaño de 10 bytes, añadir 5 al puntero-a-tipoX lo hace avanzar 50 bytes en memoria (si se trata de punteros a elementos de una matriz, supone avanzar n elementos en la matriz [3]).

Del mismo modo, la diferencia entre dos punteros resulta ser el número de objetos tipoX que separa a dos punteros-a-tipoX. Por ejemplo, si ptr1 apunta al tercer elemento de una matriz, y ptr2 apunta al décimo elemento, el resultado ptr2-ptr1 es 7 (en realidad, la diferencia de dos punteros solo tiene sentido cuando ambos apuntan a la misma matriz).

observe que no está definida la suma entre punteros.

Si ptr es un puntero a un elemento de una matriz, desde luego no existe un elemento tal como: "uno después del último", pero se permite que ptr tenga dicho valor. Si ptr1 apunta al último elemento del array, ptr+1 es legal, pero ptr+2 es indefinido (lo que a efectos prácticos significa que devolverá basura, o un runtime, volcado de memoria, etc).

Si ptr apunta a uno después del último, ptr-1 es legal (puntero al último elemento). Sin embargo, aplicando el operador de indirección * a un puntero después del último conduce a una indefinición.

Informalmente puede pensarse en ptr + n como avanzar el puntero en (n * sizeof(tipoX)) bytes, siempre que ptr se mantenga en su rango legal (entre el primer elemento y uno después del último).

La resta de dos punteros a elementos de la misma matriz, ptr1-ptr2, produce un entero n del tipo ptrdiff_t definido en <stddef.h> Este número representa la diferencia entre los subíndices i y j de los dos elementos referenciados (n = i-j). Para esto es necesario que ptr1 y ptr2 apunten a elementos existentes, o uno después del último.

§4 Ejemplos:
N. Expresión Resultado
1. ip+10; Produce otro puntero; Si ip es un puntero al elemento m[j] de una matriz (dicho de otro modo: si *ip == &m[j] ), el nuevo puntero apunta a otro elemento m[j+10] de la misma matriz, con lo que *(ip+10) == &m[j+10]
2. y = *ip+10; Añade 10 al objeto *ip (objeto referenciado por ip) y lo asigna a y
3. *ip += 1; Equivale a: *ip = *ip + 1. Incrementa en 1 el valor (Rvalue) del objeto referenciado por ip.
4. ++*ip; Igual que el anterior: incrementa en 1 el valor del objeto referenciado por ip
5. ++ip; El resultado es otro puntero. Equivale a ip = ip+1, es decir, incrementa en 1 el valor del puntero, con lo que el nuevo puntero señala a otra posición (ver caso 1.)
6. ip++; Igual que el caso anterior. Incrementa en 1 el valor del puntero.
7. (*ip)++ Igual que el caso 4. Dado que el paréntesis tiene máxima precedencia y asocia de izquierda a derecha ( 4.9.0a), incrementa en 1 el valor del objeto referenciado por ip.

Observe que el paréntesis es necesario; sin él la expresión modifica la posición de memoria señalado por ip. Es decir, se realiza ip++;. Si ip es un puntero al elemento m[j] de una matriz, el resultado es un puntero al elemento m[j+1]. Después se tomaría la indirección, es decir, el resultado final sería el valor del elemento m[j+1].

Por tanto, la expresión *ip++ equivale a *(ip++)

8. *ip+1; El valor resultante es el de incrementar en 1 el valor del objeto apuntado por ip
§5  Ejemplos

Consideremos copiar una cadena NTBS ( 3.2.3f) definida por un puntero a su origen s, en un destino definido por un puntero p. Considerando que el final de la cadena está señalado por el carácter nulo, el proceso podría ser:

while (*s != 0) {
  *p = *s;     // copiar carácter
  s++ ;
  p++ ;
}

Teniendo en cuenta que estamos usando el postincremento ( 4.9.1) para s y p, y que cualquier <expresión> es cierta si <expresión> != 0. Lo anterior sería equivalente a:

while (*s ) {
  *p = *s ;
  s++;
  p++;
}

El incremento se puede expresar en la misma sentencia de asignación:

while (*s ) { *p++ = *s++ ; }

La razón es que *p++ indica el carácter apuntado por p antes que sea incrementado. El postincremento ++ no cambia p hasta que el carácter ha sido actualizado (primero se efectúa la asignación y después se incrementa). El orden de ejecución de las operaciones involucradas es:

1.-  Se asigna *p *s

2.-  Se incrementa s

3.-  Se incrementa p

La comparación puede hacerse en el mismo momento que la copia de caracteres:

while ( *p++ = *s++)    // §5.1
 ;

La razón es que después de la última asignación que correspondería al carácter '\0' de s, el resultado de la asignación sería justamente este valor (0 == falso) y se saldría del while.

Por la razón inversa, la expresión *++p indica el carácter apuntado por p después que ha sido incrementado. Ambos tipos de expresiones de asignación con punteros, simultaneadas con pre/post incrementos/decrementos, son muy frecuentes en los bucles que involucran punteros. De hecho, las expresiones:

*p++ = val;   // cargar la pila con val
val = *--p;   // sacar de la pila el último valor, asignarlo a val

son las sentencias estándar de cargar/descargar valores val de una pila LIFO [1] manejada por un puntero p.

Nota: esta capacidad de C++ para permitir formas de código tan extraordinariamente compactas, tiene fervientes defensores y acérrimos detractores. En mi opinión, el único problema es que sin un entrenamiento previo, la primera vez que se topa uno con una expresión como la §5.1 anterior, puede quedarse bastante perplejo. Aunque en realidad solo se trate de uno más de los "idioms" ( 4.13) de fondo de armario de cualquier programador C++ 

§6 Conversión de punteros

Un puntero de un tipo (tipoX) puede ser convertido a otro (tipoY) usando el mecanismo de conversión o modelado de tipos, que utiliza el operador (tipoY*).

Ejemplo:

char *str;         // str puntero a char
int *ip;           // ip puntero a int
str = (char *)ip;  // asignación str = ip (ip puntero a char)


De forma general, el operador-moldeador (tipoX *) puede convertir el operando (un puntero de cualquier tipo) a un puntero-a-tipoX.

Como ejemplo de lo insidioso que pueden llegar a ser algunos rincones de los compiladores C++, considere el siguiente ejemplo:

long strRchr (const String& str, char needle) {
   unsigned long stLen = str.len();
   for (register unsigned long i = stLen - 1; i>=0; --i) {
      if (*(str.cptr + i) == needle) return i;
   }
   return -1L;
}

Se trata de una función que proporciona la posición de la última ocurrencia de un carácter (needle) en un una cadena de caracteres (str).  En nuestro caso los objetos de la clase String albergan cadenas alfanuméricas. En concreto, el método len() proporciona la longitud de la cadena, y el miembro cptr es un puntero al primer carácter.  La función devuelve -1 si el carácter no se encuentre en la cadena. En caso contrario devuelve la posición, empezando a contar desde cero para el primer carácter.

Aunque compila sin dificultad, la rutina anterior, produce un extraño resultado negativo en las pruebas realizadas con el compilador BC++ 5.5, mientras que con GNU G++ 3.4.2-20040916-1 para Windows, produce un error fatal de runtime.  Después de perder un par de horas intentando diagnosticar el problema, y haber sopesado todas las posibilidades (incluyendo que el compilador estuviese "poseido" :-), resultó que estaba en el resultado de str.cptr + i  (suma del puntero con el incremento). Cuando i es un unsigned long, la suma con el puntero produce un resultado negativo!!.  Ningún otro tipo para i, producía error. Por ejemplo int o long.

Observe que este comportamiento anómalo se produce a pesar de lo señalado al respecto por el Estándar [4], y de que los unsigned long son tipos enteros.

Temas relacionados:
  • "Modelado de tipos con punteros" ( 4.2.1b)
  • "Modelado de tipos" ( 4.9.9), en especial los operadores dynamic_cast y reinterpret_cast para convertir un puntero al tipo deseado.

  Inicio.


[1]  LIFO: "Last in first out" El último en entrar es el primero en salir.

[2]  Incluyendo cualquier posible ajuste o alineación interna ( 4.6.1) realizada por el compilador.

[3]  Observe que en este tipo de operaciones no está garantizado que el puntero resultante señale a una posición dentro de los límites de la matriz. En C++ esta parte del problema (no salirse de los límites) es incumbencia exclusiva del programador.

[4]  El estándar señala que en estos casos, n debe ser un tipo numérico entero o enumeración ("integral or enumeration type"); que el puntero debe ser el operando de la izquierda, y que el resultado es del tipo del puntero.  Por tanto, no están permitidas expresiones como n + pt1 o n + pt1.