Objective-C para desarrolladores de C# (ii)–Punteros

Aaahhh… los punteros son una de las bestias negras del desarrollo. Desterrados de los dominios de los lenguajes orientados a objetos “modernos” como Java por ser demasiado “próclives a errores” los punteros se han convertido en una especie de ser mitológico, temido por muchos desarrolladores que tiemblan cuando ven un asterisco dando vueltas por ahí… Incluso C# los tiene medio apartados por ahí, rodeados de unsafes por todas partes.

¿Qué es un puntero?

Un puntero no es nada más que una variable normal y corriente pero que su valor en lugar de ser un entero, o un carácter, o un número decimal es una dirección de memoria:

int *a;

int i=10;

a=&i;

La primera línea declara un puntero a int. No hay un tipo de datos específico para puntero, en su lugar se pone un asterisco que sigue al tipo de datos.

Nota: El tipo de datos de la variable a (el puntero) es int*. Que en mi código haya un espacio entre el int y el * y ninguno entre el * y la a es porque el parser es así de majo y me deja hacerlo. Verás otra gente que teclea int* a; y es equivalente (y estrictamente hablando más cierto).

La segunda línea es obvia (declara un int) y la tercera línea utiliza el operador de referencia (&) para obtener la dirección de la variable i y asignarla al puntero a.

En este punto el valor de a es la dirección de memoria de la variable i. Si tengo un puntero puedo modificar el contenido de dicho puntero utilizando el operador de dereferencia (*):

*a=11;

En este punto el contenido del puntero a vale 11. O dicho de otra manera: el valor de posición de memoria a la que apuntaba el puntero ha pasado a valer 11. ¿Y qué había en esta posición de memoria? Pues la variable i. Así si ahora imprimo la variable i veré que vale 11.

O de forma más clara, *a es equivalente a i. Modificar el valor de *a implica modificar el valor de i (y a la inversa, modificar el valor de i implica modificar el valor de *a).

¿Por qué son peligrosos los punteros?

Per se, los punteros no son peligrosos. Son simplemente un alias para acceder a una posición de memoria. Pues entonces, ¿qué es lo que los hace peligrosos?

Pues su peligro reside en lo que se llama aritmética de punteros y que permite modificar no el contenido de un puntero si no el propio puntero en sí. Eso significa que un puntero tiene la capacidad de apuntar a direcciones arbitrarias de memoria:

int *a;

int i=10;

a=&i;

a = a + 0x300;

*a=11;

Fíjate en la penúltima línea: Le estamos sumando 0x300 (768) a la dirección de a. De forma que ahora a apunta 768 elementos más allá de la dirección de memoria de i. Cuando modifico el contenido de a, estoy modificando una dirección de memoria arbitraria y lo más normal es que:

image

El otro problema típico de los punteros, no es tanto de los punteros en sí, si no del runtime del lenguaje (me da igual si es C/C++ o Objective-C). En runtimes dotados de garbage collector (como el CLR), el propio sistema se encarga de destruir aquellos objetos que no se usan. ¿Y como sabe el runtime que nadie usa un objeto? Pues, porque no hay referencias que apunten a él. El hecho de tener un GC nos garantiza de que las referencias siempre apuntan a un objeto válido (ya que el GC no destruirá un objeto mientras tenga referencias que lo apunten). Pero, que ocurre si no tenemos GC? Pues que puedo tener dos referencias que apunten a un objeto y alguien puede destruir este objeto. Cuando luego más tarde uso una de esas referencias para acceder al objeto, me puede dar un error ya que en la dirección de memoria apuntada por esta referencia ya no está el objeto, hay otros datos. Este error viene a ser el contrario del NullReferenceException: En lugar de tener un error porque la referencia NO apunta a nada (vale null), tengo un error porque la dirección de memoria a la que apunta la referencia ya no contiene el objeto que contenía porque este ha sido destruído.

He estado usando en todo este párrafo la palabra referencia y no puntero, para poner de manifiesto que podríamos tener este error también si usáramos un lenguaje basado en referencias (y no en punteros) como C# si tuviesemos una gestión manual de memoria. La principal causa de errores de C/C++ es la gestión manual de memoria, no los punteros en sí. En los lenguajes que tienen gestión manual de memoria se denomina con el término dangling pointer a un puntero que apunta a una dirección de memoria donde ya NO hay el objeto que había.

No voy a entrar en las diferencias entre punteros y referencias (porque son muy tenues y depende un poco de cada lenguaje). Quédate con que tanto un puntero como una referencia apuntan a una dirección de memoria. Si tienes GC puedes estar seguro de que en esta dirección habrá un objeto válido. Si no tienes GC entonces NO puedes estar seguro de esto y ahí empiezan realmente tus problemas. La única característica que tienen los punteros y no tienen las referencias es la aritmética de punteros.

¿Cual es la posición de Objective-C?

Primero, Objective-C es un superconjunto de C (eso significa que todo el código C es código Objective-C válido). C tiene punteros y aritmética de punteros así que Objective-C también.

Lo que diferencia Objective-C de C es la gestión de memoria. En C (y en C++) la gestión es totalmente manual: Cuando quieres crear un objeto debes reservar la memoria una sola vez y luego liberarla una sola vez cuando ya nadie más vaya a usar el objeto.

Òbjective-C usa un contador de referencias para ello: El runtime mantiene un contador que indica cuantos punteros apuntan a un objeto en un momento dado. Cuando dicho contador llega a 0 el runtime destruye el objeto. Hay dos modelos de gestión de memoria dentro del runtime:

  1. Manual: Nosotros somos los encargados de indicarle al runtime cuando queremos que incremente y decremente dicho contador. Este modelo de memoria adolece de los mismos problemas que tiene C/C++ para gestionar la memoria (memory leaks si no decrementamos suficientes veces el contador o dangling pointers si lo decrementamos demasiadas veces).
  2. Automática (ARC): El encargado de decrementar o incrementar el contador es el compilador. La verdad es que ARC es muy sencillo de usar y nos permite desarrollar casi, casi, casi como si tuviesemos un GC.

Nota: El runtime de Objective-C soporta también el uso de Garbage Collector. Pero como en iOS no se puede usar, no hablaré del GC de Objective-C. Además su uso está marcado como obsoleto en favor de ARC.

En esta serie de posts veremos tanto la gestión manual (MRC) como la automática (ARC) de memoria.

Desde el punto de vista de un desarrollador de C# debes quedarte con lo siguiente:

  1. Si usas ARC la forma de desarrollar será muy parecida a C#: vamos a crear objetos y nos despreocuparemos de liberar la memoria. ARC lo hará por nosotros.
  2. A diferencia de C++ que admite el paso por valor de objetos, en Objective-C se admite tan solo paso por referencia a través de punteros. Eso lo hace más parecido a C# (cuando se pasa un objeto no se pasa por valor, si no que se pasa una referencia a dicho objeto).

Bueno… lo dejamos aquí por el momento. En el siguiente post de la serie, más 😉

Saludos a todos!

2 comentarios en “Objective-C para desarrolladores de C# (ii)–Punteros”

  1. Partiendo de que no tengo ni idea de Objective-C, y de que mis conocimientos sobre C/C++ están bastante oxidados, un par de dudas:

    1) ¿La gestión de memoria con ARC sería equivalente al uso de smart pointers en C++ (auto_ptr, shared_ptr y compañía)?

    2) ¿Qué ventajas tendría usar gestión de memoria manual? A simple vista parece que sólo son ganas de meterse en líos, a menos que tengas que reutilizar código escrito en C puro.

  2. Buenas Juanma!

    1) Pues sí. El uso de ARC es muy parecido, conceptualmente al concepto de smart pointers en C++. De hecho un shared_ptr proporciona un contador de referencias si no tengo mal entendido. La diferencia está en que ARC es como más “mágico”: usas punteros normales y dejas que el compilador haga la magia. Hay tan solo algunas consideraciones que debes tener presente y en general funciona bastante bien.

    2) En mi opinión ninguna. Yo la contaré porque me parece que es una buena ocasión de ver como funciona un contador de referencias manual. Pero, vamos, yo siempre uso ARC 🙂

    Saludos y gracias por comentar!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *