Autor Tema: Artículo: Memoria y punteros  (Leído 9055 veces)

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Artículo: Memoria y punteros
« en: 20 de Marzo de 2011, 20:10:56 pm »
Esto pretende ser un artículo grande escrito íntegramente por mí sobre los punteros y los direccionamientos en un ordenador de 32 bits.
No está completo, pero lo estoy intentando completar poco a poco. He preferido introducirlo poco a poco en el foro para que lo vayais leyendo con tranquilidad
Si alguien lo referencia en otra web por favor respeten autores y pongan este post como cita.
Siento los reservados pero son capitulos largos y esta mejor tenerlo todo organizado!

Os aviso: Altas dosis de "teoría" y poco código. Si quieres aprender mas de C/C++ y de ordenadores y programación en general seguro te sirve!

Índice

0. Introducción. Direcciones de memoria.
1. Tipos en la memoria.
2. Definición de puntero. Tipos de punteros.
3. Aritmética de punteros
4. Transformación de punteros.
5. Memoria dinámica.
« Última modificación: 18 de Abril de 2011, 02:02:32 am por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Comunidad PHPeros

Artículo: Memoria y punteros
« en: 20 de Marzo de 2011, 20:10:56 pm »

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #1 en: 20 de Marzo de 2011, 20:11:14 pm »
0. Introducción. Direcciones de memoria.

Este artículo supone que ya posees algo de conocimientos acerca del lenguaje C y de la programación en general, pero no necesitas un nivel muy elevado para leer esto. En realidad es tan básico que solo necesitas conocer los tipos de datos en C (int, char, float...) y las tablas (que en realidad no es mas que conjuntos de los anteriores, básicamente).

Partiendo de esa base, y sabiendo la diferencia entre cada uno de esos tipos de datos, es necesario saber como se disponen las variables/datos en la memoria de tu ordenador.

El lenguaje C es un lenguaje compilado, y por lo tanto las instrucciones de tu archivo de código fuente (por ejemplo main.c) se compilan y generan un conjunto de instrucciones en código máquina, es decir, genera un ejecutable. Estas instrucciones en código maquina son un conjunto de 0 y 1 que el procesador puede entender directamente. Es lo que se llama el lenguaje binario. Cuando ejecutas un programa (un ejecutable) las instrucciones en lenguaje binario que contiene se cargan en la memoria RAM, y se leen desde ahí.

A diferencia de C, el lenguaje binario es dependiente de la plataforma en la que se ejecute. Mientras que un código fuente en C es universal y debería compilar en cualquier ordenador, el archivo ejecutable que se crea como producto de esa compilación no funcionará en todas las máquinas. Por lo tanto, si yo compilo en mi ordenador un programa en C, eso no significa que vaya a funcionar también en tu ordenador (a no ser que tengan una arquitectura igual o similar).

Cuando nos referimos a "arquitectura" nos estamos refiriendo al tipo de compu.tador principalmente, en particular al procesador y a cómo lee la memoria
este procesador. Por lo tanto, al existir distintos tipos de procesadores y distintas formas de leer de la memoria, también deben existir distintos tipos de ejecutables, que organizaran su código binario interno de una forma u otra, dependiendo de cómo debe almacenarse en la memoria para que el procesador al que va destinado pueda entenderlo.
Hay arquitecturas de 32 bits y arquitecturas de 64 bits. Nosotros vamos a referirnos siempre a las de 32 bits.

Ya hemos dicho que las instrucciones de los ejecutables se almacenan en la memoria en código binario (0 y 1), pero no solo las instrucciones. En la memoria se almacena todo en código binario, tanto instrucciones, como datos. Por ejemplo: 00001011 en binario, es un 11 en el sistema decimal, que es el que usamos normalmente.

Cada 0 o 1 se almacena en la unidad mínima de información, el bit. Un bit puede ser o bien 0, o bien 1, pero siempre uno de los dos. Corresponde al estado Apagado, o al estado Encendido. Si lo quieres ver de otra manera mas real, un 1 correspondería a un pulso eléctrico (que al fin y al cabo es lo que entienden las máquinas) y un 0 a que no hay ningún pulso eléctrico.

A lo que resulta de unir 8 bits se le denomina octeto o también byte. El número de arriba, 00001011 es un octeto o un byte que corresponde al número 11 como hemos dicho antes.
La memoria de un ordenador se organiza en bytes, uno detrás de otro hasta que se agota la memoria. A cada byte se le denomina posición de memoria.

00001011 -> Primer byte
01101010 -> Segundo byte
10110001 -> Tercer byte
.....             ->  ......

Haciendo traducciones, 1 KiloByte (KB) serían 1024 bytes, a su vez 1 MegaByte (MB) serían 1024 kb o 2^20 bytes (^ hace referencia a "elevado a"), 1 GigaByte (GB) serían 1024 MB, o 2^20 KB, o 2^30 byte. Por lo tanto, si en nuestro sistema tenemos instalados 2GB de memoria RAM, lo que tenemos son 2*(2^30) posiciones de memoria, es decir, 2^31 posiciones de memoria.
La arquitectura de 32 bits que estudiamos nos limita a tener 2^32 posiciones de memoria como máximo, luego explicaré el por qué.

Estas posiciones están numeradas de forma parecida a como he puesto arriba. Cada posición tiene un numero en formato hexadecimal que indica su posición (primera, segunda, tercera...) A este numero se le denomina dirección de memoria.
Esta numero suele tener este formato: 0x203AD2FB. El 0x del principio no significa nada, es simplemente unos caracteres que se ponen delante de una cadena para explicar que los números y letras que vienen a continuación están en base hexadecimal. La base hexadecimal esta compuesta por 16 caracteres (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F) La A corresponde al 10, la B al 11, la C al 12, la D al 13, la E al 14 y la F al 15. Mientras que la base binaria necesita muchos caracteres para expresar un numero pequeño...
Ejemplo: 100110110111011 en binario -> 19899 en decimal
La base hexadecimal suele expresar con pocos caracteres números muy grandes
Ejemplo: 203AD2FB en hexadecimal -> 540726011 en decimal

Si aún no conocen como pasar de binario a hexadecimal, de hexadecimal a binario y demás, os dejo a ustedes la tarea de buscar en google como se hace, pues la verdad es que es una tarea muy sencilla.

Si las posiciones se numeran desde el 0x00000000 hasta el 0xFFFFFFFF, eso nos da 4294967296 posiciones como máximo, que son 2^32 posiciones. ¿Casualidad? Si recordais lo de antes, 2^32 posiciones era el máximo permitido en la arquitectura de 32 bits. Si traducimos una dirección de memoria, por ejemplo la 0xF00AD2AB, al traducirlo a binario tenemos 11110000000010101101001010101011. ¡Que curioso! 32 bits.
Para que nos entendamos, y sin entrar en detalles, para que el procesador lea de las direcciones de memoria, tendremos que pasarle mediante algún medio físico todos esos bits uno a uno, para que luego el procesador los pueda leer y unir para tener el numero final F00AD2AB. Ese medio físico lo puedes pensar como una maraña de 32 cables llamado bus, donde en cada cable viaja un solo bit. ¿Entiendes ahora por qué no se puede referenciar mas de 0xFFFFFFFF direcciones? Si solo tenemos 32 cables, ese es el máximo valor que podemos obtener.
« Última modificación: 22 de Marzo de 2011, 01:54:18 am por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #2 en: 20 de Marzo de 2011, 20:13:19 pm »
1. Tipos en la memoria

Ya deberíamos saber que hay varios tipos de variables en C: Esta el int, el unsigned int, el short int, el char, el float, el double...
En general, se pueden distinguir de varias formas: los tipos y los modificadores.
Tipos son: int, char, float, double...
Modificadores serían: unsigned, short, long, long long...
En general los tipos hacen referencia a QUE se guarda en la memoria, y los modificadores a COMO se guarda en la memoria.

Comencemos viendo cuales son los distintos tipos de codificación de la información. Cuando decimos codificación de la información nos referimos a como se mezclan esos 1 y 0 que la memoria puede albergar, para representar un número.
Antes hemos visto que un numero se puede expresar en binario clásico, como por ejemplo:
011001 en binario es 25 en decimal.
Si quieres saber mas sobre el binario clásico visita: http://personales.unican.es/togoresr/lisp/BINARIO.htm y http://es.wikipedia.org/wiki/Sistema_binario

El problema es que con el binario clásico solo pueden expresarse números positivos (¿no lo habías pensado?). Con la necesidad de tener también en cuenta números negativos se invento otro sistema de codificación llamado Complemento a Dos.
Básicamente el complemento a dos utiliza el bit mas significativo (BMS, el de mas a la izquierda) para decir si el numero es negativo o no (1 -> negativo, 0 -> positivo).
En caso de que lo sea se invierten todos los bits y se le suma uno para obtener el valor absoluto del número. Luego a este valor habrá que añadirle el signo. Algunos ejemplos:
1101 -> BMS 1 -> Negativo -> Se invierten el resto de bits: 010 -> Se le suma uno -> 011 -> -3
0111 -> BMS 0 -> Positivo -> Se lee tal cual -> 7
Mas info: http://es.wikipedia.org/wiki/Complemento_a_dos
Este sistema nos da una ventaja: podemos usar números negativos, pero nos da también una desventaja: el numero mas grande que podemos con 4 bits ya no será el 1111
si no que ahora será el 0111 (recuerda que en complemento a dos el 1111 es un numero negativo porque el BMS es 1).

En resumen, teniendo en cuenta que usamos un numero N de bits, el sistema binario clásico nos permite expresar números mas grandes con esos bits, pero sin embargo no podríamos expresar números negativos. El complemento a dos nos permite expresar números negativos, pero sin embargo los números que podemos expresar son mas pequeños en valor absoluto que con el binario clásico. ¿Cual es mejor? Ninguno de los dos, depende de para qué se use.

Hay otro sistema de codificación que no comentaré demasiado que se llama mantisa. Se utiliza para codificar números en notación científica. Por ejemplo el 5x10^23
En este sistema se codifica por un lado el signo, por otro lado el exponente y por otro la mantisa. Es un sistema algo mas complejo que los anteriores pero nos permite codificar números muy grandes perdiendo precisión.

Con eso será suficiente sobre la codificación de la información.

Ahora bien, respecto a cómo se organiza la memoria del ordenador, antes dijimos que cada posición de memoria era 1 byte, y que este byte tenia 8 bits. Si solo tengo 8 bits quiere decir que el numero mas grande que puedo expresar en codificación binaria clasica seria el 11111111, que es el 255 (2^numero_bits - 1). En complemento a dos, el numero mas grande que puedo expresar es: 01111111, que es 127 (2^(numero_bits-1) - 1).
Puesto que 8 bits solo nos permite expresar números pequeños (como máximo 255 o 127, dependiendo de si es binario clásico o complemento a dos), la memoria puede organizarse de forma que un mismo numero se guarde en varias posiciones de memoria, combinando los bits de esta para así formar números mas grande.

Por hablar de datos en concreto y cosas reales, un int ocupa 4 posiciones de memoria, que son 4 bytes, y 4*8=32 bits. Además, un int por defecto usa el sistema complemento a dos, por lo tanto el valor mas grande que puede obtener es:
01111111111111111111111111111111 en binario -> 2147483647 en decimal. ¡Probadlo!

El caso es que nosotros podemos controlar con nuestro código fuente cuantos bytes queremos que ocupen nuestras variables. Cada tipo tiene asociado un numero de bytes, que dependen de la maquina donde se ejecuta el programa, y que podemos averiguar con sizeof(). Si colocamos entre los paréntesis de sizeof() un tipo, esta función nos devuelve el numero de tipos char que caben en ese tipo. Puede parecer lioso, pero no lo es.
Por lo general un char ocupa 1 byte. Por lo tanto, si un int ocupa 4 bytes, sizeof(int) nos devolverá un 4, que quiere decir 4 veces lo que ocupa un char, es decir: 4*1 byte= 4 bytes.

Algunos modificadores nos dejan cambiar cuanto ocupa una variable: short indica que esa variable debe ocupar menos memoria (limitando el número mas grande y mas pequeño que podemos almacenar ahí) y long y long long nos ampliaría la memoria que ocuparía dicha variable, aumentando el número máximo. Por ejemplo, mientras que un int ocupa 4 byte y el numero mas grande que almacena en complemento a dos es 2147483647, un short int ocupa 2 bytes, y el numero mas grande que almacena en complemento a dos es 32767.

Ademas de estos modificadores hay otros que indican que tipo de codificación se usará para almacenar ese número, por lo general solo dos: signed y unsigned.
signed es lo mismo que no poner nada, es decir: int y signed int es lo mismo. Simplemente quiere decir que ese numero se almacenara en complemento a dos, mientras que si colocamos un unsigned delante diremos que ese numero queremos que se codifique en binario clasico (aumentando el numero mas grande que podemos obtener, pero eliminando números negativos).

Dicho esto ya sabemos como se guardan en la memoria todos los números posibles. ¿Pero como se guardan los caracteres?
Los caracteres se guardan con una correspondencia Numero->Caracter. Es decir, se crea un convenio. Es un sistema muy simple: se dice, por ejemplo,
que el 1 será la A, el 2 será la B, el 3 será la C.. y así con el resto. Cuando se hace con todos los caracteres se crea lo que se conoce como tabla de caracteres.
La tabla que los ordenadores usan por regla general se llama tabla ASCII: http://es.wikipedia.org/wiki/ASCII
En la tabla ASCII, por ejemplo, se guarda el carácter 'A' como un 65 decimal, o un 01000001 en binario. Todos los caracteres se pueden representar en números positivos con 8 bits, esa es la razón de que un char solo ocupe 1 byte.
El bit mas significativo, el numero 8, siempre sobra. Por lo tanto, como el bit mas significativo siempre es 0, da lo mismo que un char sea signed u sea unsigned, puesto que si se codifica en complemento a dos el MBS siempre será 0, y por lo tanto no se invertirán los bits y los 7 bits restantes serán leídos en binario clásico.

Sabiendo ahora que la memoria solo almacena números con estos métodos, y que puedo variar el numero de bytes que ocupa cada tipo, no será muy difícil entender que aunque yo cree una variable así:
char pruebaCaracter = 'A';
En pruebaCaracter realmente hay guardados un conjunto de 0 y 1 que representan al numero decimal 65 como dijimos antes. Por lo tanto, si yo leo pruebaCaracter como un char, el procesador leera una A, si leo pruebaCaracter como un int, el procesador leera un 65 y si leo pruebaCaracter como un unsigned int, el procesador leera un 65 también (el MBS es 0).
« Última modificación: 22 de Marzo de 2011, 11:30:11 am por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #3 en: 20 de Marzo de 2011, 20:13:22 pm »
2. Definicion de puntero. Tipos de punteros.

Puede que los capítulos anteriores sean demasiado teóricos y quizás un poco aburrido para algunos, pero puedo asegurar que ayudara mucho comprender todos esos conceptos.
Al fin y al cabo, todo eso que he explicado en los temas anteriores es culturilla general sobre informática, pero vamos al tema de los punteros en sí, entrando mas en el asunto.

¿Que es un puntero?
Para empezar, ya deberíamos saber que un puntero y cualquier cosa en un ordenador son solo conjuntos de 0 y 1, y por lo tanto es un número.
Un puntero no es mas que un numero que representa una dirección de memoria.
Normalmente suele expresarse en formato hexadecimal, con 8 dígitos hexadecimales, así: 0xbff23adf

En C los punteros se declaran con el carácter * así:
Código: [Seleccionar]
int *p;
char *p2;
float *p3;

Estas declaraciones son muy fáciles de leer. * Quiere decir que es un puntero, es decir, que almacena una direccion de memoria. El tipo que precede al * quiere decir al tipo de variable que apunta esa dirección.
Es decir, que si yo tengo una variable así:
int prueba;
Y esta variable vive en la dirección 0xbff23adf puedo hacer esto:
int *p = 0xbff23adf;
El 0x como hemos dicho ya antes solo quiere decir que los dígitos que vienen a continuación son hexadecimales.
Así se declararía un puntero a int, es decir, que p contiene un numero (En este caso 0xbff23adf). Ese numero es una dirección de memoria. Si vas a esa dirección de memoria lo que vas a encontrar ahí es un int.

Ahora bien, ¿como puedes saber tu donde va a vivir la variable prueba? No puedes saberlo, de hecho es imposible.
Cuando declaras una variable, el SO es el que se encarga de darle un hueco en la memoria para que se albergue. Osea, que si declaras un int, se le pide al SO que busque un hueco libre en la memoria RAM de 4 bytes, para asignárselos a la variable prueba. Por supuesto el hueco libre que encuentre no siempre es el mismo, de hecho debemos considerar que siempre es distinto, por lo tanto en un puntero para nosotros estará prohibido hacer algo así:
int *p = 0xbff23adf;
Ya que no podemos saber que tipo de contenido hay en la dirección de memoria 0xbff23adf

¿Como lo hacemos entonces? Usando el caracter &.
El carácter & en C es un operador que devuelve la dirección donde vive la variable en la que se coloca. Por lo tanto, &prueba devolvería la dirección de memoria donde se alberga la variable prueba.
Los humanos lo podemos leer como "dirección de" por lo tanto &prueba sería "dirección de la variable prueba".
Además de & falta otro operador especial por comentar, que es el *. Hay algunos problemas con este operador, pues la gente no sabe que con el mismo carácter (*) se refieren a dos cosas diferentes.
Si yo coloco el carácter * delante de un puntero, este carácter me devuelve el contenido de la dirección de memoria que hay en la variable. Los humanos lo leemos como "contenido de la variable apuntada por"

Es decir, que si yo tengo esto:
Código: [Seleccionar]
int prueba = 4;
int *p = &prueba;
printf("%d", *p);

Lo que imprimimos es "el contenido de la variable apuntada por p". Puesto que el puntero p contiene la dirección de la variable prueba. Lo que estamos imprimiendo es el contenido de la variable prueba, es decir, eso imprimiría un 4. ¿Un poco lioso? Leelo con calma.

Dije que el operador * trae problemas porque se utiliza tanto a la hora de declarar un tipo como a la hora de desreferenciar variables (Acceder al contenido de una variable apuntada por un puntero se le denomina desreferenciar). Diferenciar los dos casos es muy fácil!! Cuando el * tiene un tipo primitivo a la izquierda, nos referimos a un tipo de puntero, y cuando el * tiene únicamente un nombre de variable a la

derecha nos referimos al operador desreferenciar.
*p -> desreferenciar
int *p -> puntero a entero de nombre p
Además, cuando ponemos el nombre del puntero sin ningún operador, por ejemplo: p hacemos referencia a la dirección que guarda p

Vamos a poner algunos ejemplos para que se intente quedar la idea clara

Código: [Seleccionar]
int num = 6;
int *pnum = # // Se crea un puntero que apunta a int, y se le hace apuntar a la variable num
*pnum = 4; // Ahora num vale 4
num = 2; // Ahora *pnum vale 2, y pnum sigue valiendo lo mismo (la dirección de num)

Código: [Seleccionar]
char c1 = 'A';
char c2 = 'B';
char *p = &c1; // p apunta a la variable c1
*p = 'C'; // como p apunta a c1, el contenido de p (*p) que es c1 ahora vale 'C'
p = &c2; // p apunta a la variable c2
*p = 'D'; // ahora c2 vale 'D'

Otra característica interesante de los punteros es que hay que recordar que una dirección (bff23adf) solo es otra forma de representar un número, solo que en lugar de binario se expresa en hexadecimal porque es mas corto y mas fácil de leer para los humanos pero en la memoria ese numero se guarda en formato binario, exactamente así: 10111111111100100011101011011111. Ya hemos dicho que en una maquina

de 32 bits la memoria va desde la 0x00000000 hasta la 0xFFFFFFFF por lo tanto en binario la dirección mas alta es 11111111111111111111111111111111, es decir, que una dirección ocupa siempre 32 bits, que son 4 bytes.

Ya tenemos otra característica de los punteros: Siempre ocupan 4 bytes.
La pregunta es: si todos los punteros ocupan lo mismo y todos van en formato binario clásico ¿por qué hay distintos tipos? ¿por qué existe char *, int *, float * y no un único tipo de puntero?
Bueno, la respuesta tiene que ver en como el compilador hace operaciones matemáticas con los punteros, es decir, en la aritmética se marca la diferencia.
Lo veremos en el próximo capitulo.
« Última modificación: 23 de Marzo de 2011, 11:02:31 am por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #4 en: 20 de Marzo de 2011, 20:13:27 pm »
3. Aritmética de punteros

La respuesta a la pregunta del capitulo anterior, como ya dijimos, son las matemáticas.
En todos los tipos de datos que hemos visto hasta ahora (quizás ignorando los de coma flotante, ya que son números muy grandes), si tu haces la operación:
variable = variable + 1;
A esa variable se le sumará uno en su cantidad, sea del tipo que sea. Incluso en los char.
Si tienes:
char c1 = 'A';
Y haces:
c1 = c1 + 1;
c1 ahora valdrá 'B'. ¿Por qué? Porque como dijimos antes, en la tabla ASCII el caracter A vale 65 en decimal, y da la casualidad de que la B vale 66 en decimal, por eso todo ese resultado. De hecho las letras mayusculas y minúsculas en la tabla ASCII están todas agrupadas de forma consecutiva, pero sin embargo hay una separación entre las mayúsculas y las minúsculas, por lo que 'y' + 1 = 'z', pero sin embargo 'z' + 1 != 'A'. Ya sabéis que si queréis mas información sobre esto solo tenéis que buscar la tabla ASCII en google.

Sin embargo, en los punteros no funciona igual, ya que el número que le sumas a un puntero no se considera un número, si no que se considera una cantidad de posiciones de memoria relativas.
¿Por qué relativas? Por qué depende del tipo de puntero al que le estemos sumando. Puede resultar complicado, me explico:
Código: [Seleccionar]
int as = 0;
int *p = &as;
p = p + 1;

Puesto que p es un puntero de tipo int, y que apunta a la dirección de la variable as (vamos a suponer que la direccion es la 0xbfff23a2) al sumar 1 al puntero p, lo que en realidad estamos haciendo es sumar 1*sizeof(int). Es decir, que sumamos 1 vez el tamaño de un int. Como ya he dicho, normalmente sizeof(int) vale 4, así que en realidad estaríamos sumando 4. La dirección apuntada por p sería ahora 0xbfff23a6.
Si fuera así:
Código: [Seleccionar]
char s = 'C';
char *p = &s;
p = p + 2;
Aquí estaríamos sumando 2*sizeof(char), como sizeof(char) siempre vale 1, en realidad estamos sumando 2. Suponiendo que p antes valía 0xbfff23a2 como antes, ahora valdría 0xbfff23a4.

Como ven, aunque en el código pueda parecer que sumamos 1 en el primer caso, o 2 en el segundo, en realidad estamos sumando cantidades diferentes (4 en el primero, 2 en el segundo).
En general, si tenemos esto:
Código: [Seleccionar]
tipo var = 0;
tipo *p = &var;
p = p + c;

Aunque parezca que la ultima linea suma c unidades a la dirección, en realidad esta sumando c*sizeof(tipo). Tenedlo en cuenta.

¿Por qué pasa esto así? Por una sencilla razón, para mantener la concordancia con el código. Esto se ve muy fácilmente con las tablas.
Una tabla no es mas que una serie de variables puestas consecutivamente en memoria. Por lo tanto int var[3]; serían 3 variables de tamaño int, como un int ocupa 4 bytes, la tabla en total ocupará 3*4=12 bytes.
Si sabemos algo de tablas, deberíamos saber que var[0] es el primero elemento de la tabla, y var[2] es el ultimo elemento. Todos los elementos son del tipo int, desde var[0] hasta var[2]
Ahora bien, se debe saber una cosa sobre las tablas. La notación var[num] (donde num es el indice del elemento al que queremos acceder) no es mas que una abreviación que usa el compilador.
var[num] puede traducirse a *(var + num); Que quiere decir, contenido del puntero (var+num). Lo coloco entre paréntesis para hacer notar que la operación de suma se realiza antes de aplicar el operador *.

De ahí podemos sacar dos cosas: Para empezar podemos deducir que si var[0] es int, var es un puntero a int. Donde además *var = var[0] (Es el caso en el que num vale 0).
Si miramos la memoria, tendríamos esto:

Código: [Seleccionar]
[--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------]
|''''''''''''''''var[0]''''''''''''''''||''''''''''''''''var[1]''''''''''''''''||''''''''''''''''var[2]''''''''''''''''|
<- var

Cada - es un bit, cada [--------] son 8 bits, y por lo tanto 1 byte. Cada byte tiene asignada una posición de memoria y cada 4 bytes tenemos un int, por lo tanto un valor de la tabla.
Supongamos que var vale 0xbbbb0000. var[0] ocupa los 4 primeros bytes, var[1] los 4 siguientes y var[2] los 4 últimos. var (sin corchetes) siempre es un puntero que apunta al primer byte de la tabla.
Por lo tanto, al hacer var[1] lo traducimos a *(var + 1). Como var es un puntero a int, en realidad a var se le suman 1*sizeof(int), osea, que se le suma 4.
Estaríamos haciendo: *(0xbbbb0004), si miramos el cuadro, y teniendo en cuenta que 0xbbbb0000 es el primer byte, estaríamos accediendo al cuarto byte, que es donde vive la variable var[1], el segundo int.
Hay que aclarar que en todo este proceso el valor de var no se ve modificado, puesto que no hay ninguna asignación (no hay ningun operador = )

Al hacer var[2] ocurre lo mismo. Se traduciría a *(var + 2), en realidad se sumaría 2*sizeof(int) que en total es 8. Tendríamos 0xbbbb0008 y accederíamos a var[2].

Solo tenemos asignados 3 variables en la tabla, desde la 0 hasta la 2. ¿Que pasa si accedo a var[3]?
En realidad puedes hacerlo, el compilador no te pondrá ningún problema. De hecho compilará y solo te mostrará una warning que dice:
warning: array subscript is above array bounds
Básicamente, te dice que estas fuera de los límites del array (de la tabla). Pero sintácticamente es correcto y puede funcionar, así que lo compila.

¿Que pasa en realidad?
Estas accediendo a memoria no reservada, es decir, tu no has reservado nada para esa posición, por lo tanto no sabes que hay en la memoria var[3], ni siquiera sabes el tipo de datos.
Pero el compilador de todas formas accede, y te muestra lo que había. Probablemente sean cosas sin sentido como números negativos muy grandes (es lo habitual), y además en cada ejecución ese número tiene muchas posibilidades de cambiar, ya que serán posiciones de memoria diferentes cada vez que ejecutes el programa.

Incluso puede darse el caso de que no estés leyendo nada coherente. Me explico, imagina que detrás de tu tabla tienes un char, y luego un int, de esta forma:

Código: [Seleccionar]
[--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------][--------]
|''''''''''''''''var[0]''''''''''''''''||''''''''''''''''var[1]''''''''''''''''||''''''''''''''''var[2]''''''''''''''''||'''CD'''||'''''''''''int desconocido''''''''''''|

CD es una forma de abreviar Carácter Desconocido. Al hacer var[3] estarías accediendo a la posición var+4*3 (Recordamos que la tabla es de int, y cada int son 4 bytes) y además, aunque en realidad en la posición que estas leyendo hay un char (el CD), el compilador piensa que es un int, ya que tu puntero es un puntero a int, con lo que estarías leyendo 4 bytes, 1 del char desconocido, y 3 del int desconocido por lo cual te da como resultado algo muy extraño.

Con estas cosas hay que tener mucho cuidado, porque puede molestar mucho a la hora de probar el programa. Además los errores de este tipo con punteros a veces no dan problemas hasta que pasa X circunstancia, y por lo tanto a veces es difícil localizarlos y parchearlos.

En conclusión: cuidado con las sumas y las restas en los punteros, pueden llevarte a zonas de memoria que no conoces, y tener un efecto inesperado en el programa.
« Última modificación: 24 de Marzo de 2011, 13:26:55 pm por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #5 en: 20 de Marzo de 2011, 20:13:41 pm »
4. Transformación de punteros

Visto lo visto, y si lo habéis entendido llegaréis conmigo a la conclusión de que en realidad no necesitas los tipos. Es decir, imaginando que existiera de forma hipotética un puntero sin tipo, es decir, que solo contubiera una dirección de memoria en 4 bytes, nosotros podríamos hacer todo lo que quisiéramos con ese puntero. No hay necesidad de nada más. Si tenemos un dato de 3 bytes y ese puntero lo apunta, leemos 3 bytes y paramos. Si tenemos una tabla de 4 elementos y de 2 bytes cada uno, y nuestro puntero "hipotético" apuntara al principio de la tabla, solo nos

haría falta sumar 2 direcciones a nuestro puntero cada vez que quisiéramos pasar al siguiente elemento de la tabla. Esta bien, "no necesitas los tipos" es una exageración. Los tipos en C le dan solidez y estructura al lenguaje, sin ellos lo que tendríamos sería un pequeño caos.
De hecho, aportan mucha comodidad al programador, ya que no tenemos que preocuparnos de cuanto ocupa la información, ni de como se codifica. Tampoco nos preocupamos de averiguar la dirección efectiva (la real) en memoria cuando hacemos tabla[1] o tabla[22]. Todo son

comodidades. Aunque lo cierto es, que el supuesto puntero "hipotético" en realidad existe. Es el ultimo tipo de puntero que queda por ver, y es un tanto especial, su nombre es void*, que quiere decir "puntero a elemento sin tipo".

Como bien dice, solo almacena una dirección y no tiene en cuenta que tipo de dato contiene, por lo tanto todo el tema del capitulo 3, la aritmética de punteros, se va al garete. ¿Como va a saber cuanto sumar o restar a un puntero si no conoce la longitud en bytes del tipo que almacena?
La solución es que en lugar de sumar de la forma que se explica en el punto 3, ahora, al no conocer el tipo, en lugar de usar aritmética de punteros usa sumas y restas normales y corrientes.
De hecho, si quieres puedes probar a sumar algo a un puntero sin tipo, por ejemplo:

Código: [Seleccionar]
#include <stdio.h>
 
int main() {
        int num = 234;
        void *p = &num;
        printf("Vale %p al principio\n", p);
        p = p + 2;
        printf("Despues de sumarle dos vale: %p\n", p);
        return 0;
}

%p muestra la dirección de memoria del a variable, lo que dará como resultado algo del estilo de:
Vale 0xbfa02044 al principio
Después de sumarle dos vale: 0xbfa02046

Sin embargo, si recordamos del capítulo tres, si en lugar de usar un void* usamos un int* la cosa cambia:

Código: [Seleccionar]
#include <stdio.h>
 
int main() {
        int num = 234;
        int *p = &num;
        printf("Vale %p al principio\n", p);
        p = p + 2;
        printf("Despues de sumarle dos vale: %p\n", p);
        return 0;
}

Este, en cambio, devuelve algo del estilo:
Vale 0xbf8a3704 al principio
Después de sumarle dos vale: 0xbf8a370c

Esto es por lo que explicábamos antes, en realidad no se hace p = p + 2, si no que se hace p = 2*sizeof(tipo_de_variable_a_la_que_apunta). Como en el segundo caso es int, y sizeof(int) es 4, en realidad se suma 8 a la dirección, y por lo tanto 0xbf8a3704 + 8 = 0xbf8a370c.
Recordad que en hexadecimal c = 12, así que 4 + 8 = c.

Para el primer caso, en realidad el compilador le asigna al tipo void el tamaño 1, y así al hacer la operacion que se explica antes, se convierte en una suma normal y corriente. Recordemos:
Código: [Seleccionar]
tipo *p = &var;
p = p + c;
Aunque parezca que la ultima linea suma c unidades a la dirección, en realidad esta sumando c*sizeof(tipo). Como tipo es void, y sizeof(void) es igual a 1 c*sizeof(void) = c, así que p = p + c.

Además, te conviene conocer algo llamado transformación forzada. La transformación o conversión forzada es una operación por la cual una variable de un tipo pasa a ser de otro tipo durante una instrucción, pero el tipo de esa variable no se ve modificado al salir de ella.
Se hace añadiendo entre paréntesis el tipo antes de la instrucción: (int)my_var. Si el tipo nuevo es mas pequeño que el tipo antiguo, el tipo nuevo se queda solo con los bits menos significativos del tipo antiguo. Es decir, que si un int lo convertimos forzosamente a un char, este char

tendrá un byte (como todos los chars) que justamente será el byte menos significativo del int.

Algunas veces las conversiones forzadas no son necesarias, puesto que el compilador las hace sin necesidad de indicarlo nosotros, es el caso del int y el char, el float y el int y demás tipos que no son punteros y no son demasiados complejos. Nosotros podemos asignar un char a un int,

un int a un char, un float a un int y cosas por el estilo sin problemas, y no necesitamos de las conversaciones forzadas. Es decir:
Código: [Seleccionar]
int n = 42;
char c = n; // Correcto, no hay problema.

Sin embargo, aunque ahí no vale para mucho, en el tema de los punteros se puede utilizar para otras cosas, por ejemplo... podríamos crear una tabla de int, y usarla en lugar de con un puntero a un int, con un puntero a void, a base de transformaciones forzadas para aprovechar la

aritmética de punteros en lugar de la aritmética matemática normal.

Por ejemplo:

Código: [Seleccionar]
#include <stdio.h>
 
int main() {
int i = 0;

/* Creamos la tabla, a partir de ahora tabla es un puntero a int
que apunta al comienzo de nuestra tabla */
int tabla[5] = {0, 1, 2, 3, 4};

/* Declaramos puntero a void, y le asignamos la dirección que hay en tabla
así ahora tenemos la dirección de comienzo a la tabla en p. */
void *p = tabla;

/* Como p no tiene tipo, al imprimir el contenido de p tendremos que decir
mediante transformación forzada que p en realidad es un puntero a tipo int, y luego
del resultado de eso, usaremos el * para acceder a su valor */
printf("El elemento numero 0 de la tabla es: %d\n", *((int*)p));

/* Pero acceder a los siguientes elementos ya no es tan fácil como hacer p[1] o
p[2], puesto que sizeof(void) no coincide con sizeof(int). Si un int vale 4 bytes y
sizeof(void) vale uno, puesto que p es void*, al hacer p[2] en realidad estaríamos
haciendo *(p+2*sizeof(void)), es decir, que estaríamos accediendo al tercer byte del
primer int de la tabla, y no al tercer elemento de la tabla, para acceder al tercer
elemento de la tabla habrá que hacer algo del estilo: */
p = (int*)p + 2;
printf("El elemento numero 2 de la tabla es: %d\n", *((int*)p));

/* Es decir, con (int*)p + 2 lo que decimos es que trate al puntero p como un puntero
a int, y por lo tanto al hacer 2*sizeof(tipo) hará 2*sizeof(int) en lugar de
2*sizeof(void) y los cálculos serán correctos */
 
/* Mostramos el resto de elementos */
for(i = 3; i < 5; i++) {
p = (int*)p + 1;
printf("El elemento numero %d de la tabla es: %d\n", i, *((int*)p));
}
 
return 0;
}

Espero que haya quedado claro con ese gran ejemplo. La conclusión es que podemos usar la conversión/transformación forzada de tipos para variar tanto la aritmética de punteros como el método para leer la memoria guardada en una posición de memoria.

En realidad, todo esto no suele usarse demasiado. El tema de convertir punteros de un tipo a otro no es algo que tenga demasiada utilidad, pero es cierto que se verá brevemente en el siguiente capitulo, la reserva de memoria dinámica.
De todas formas merece la pena pararse un poco a pensar como funciona C y como maneja sus datos, porque sin duda aumenta mucho la capacidad de aprender sobre el lenguaje.
« Última modificación: 28 de Marzo de 2011, 00:46:43 am por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #6 en: 20 de Marzo de 2011, 20:13:52 pm »
5. Memoria dinámica
La memoria dinámica es un tipo de memoria que se almacena en uno de los sectores en los que se divide la memoria ram de un programa. Este sector se denomica "heap". Realmente, el uso de la memoria dinámica es lo que hace que un programa consuma diferente cantidad de memoria a lo largo del tiempo. En algunos programas podréis observar (con el administrador de tareas de Windows por ejemplo) como la RAM consumida por un proceso crece y decrece de vez en cuando. Esto es debido a que se libera y se reserva memoria de este sector. El heap crece hacia posiciones de memoria mas grandes, es decir, que si reservo dinamicamente 2 bytes en el heap (primero uno, y luego el siguiente), el segundo byte reservado tiene una posición de memoria mas elevada que el primer byte reservado.

La reserva dinamica de memoria es un proceso muy importante y muy útil en un programa escrito en C. Es algo a tener en cuenta sin ninguna duda.
Nos permite reservar memoria justo para lo que necesitamos, ni más ni menos. Por ejemplo, imaginemos que queremos crear una tabla (array o arreglo) de X numeros aleatorios, para luego ordenarlos de mayor a menor en esa misma tabla.
El usuario nos indicará por la linea de comandos cuantos numeros aleatorios desea, es decir, una cosa así:
./generador 6
Eso significaría que quiere que generemos 6 numeros aleatorios. Ese numero 6 (codificado como un char, es decir, que esta en ASCII) se guardaría en argv[1]. Si no saben de que estoy hablando, probablemente deban echar un vistazo en google sobre "argc y argv en C".

Sin la memoria dinámica, no tendríamos mas remedio que declarar una tabla con un número que nosotros elijamos, y que seleccionariamos como tope, y luego rellenar en esa tabla tantos numeros como nos indique el usuario. Es decir, que si decidimos hacer una tabla de 18 int's, así:
int numeros[18];
y luego el usuario nos dice que solo quiere 6 numeros (como en nuestro ejemplo), entonces ocupamos las 6 primeras posiciones de la tabla, pero el resto quedan vacías, por lo tanto estamos perdiendo memoria y haciendo nuestro programa menos eficiente. En este caso estaríamos reservando 18-6: 12 ints, * 4 bytes cada uno: 48 bytes de memoria.

Con la memoria dinámica, sin embargo, podemos reservar en tiempo de ejecución una tabla de tantos miembros como nos indique el usuario.
¿ Como se hace ? Muy sencillo: Malloc y calloc.

Malloc y calloc son unas funciones que se encuentran en stdlib.h, y que nos permiten reservar memoria dinámicamente.
malloc devuelve un puntero del tipo void y acepta un argumento, que es el numero de bytes a reservar (se debe usar sizeof para esto).
calloc es parecido, devuelve un puntero del tipo void, acepta dos argumentos, el primero es el numero de elementos que queremos reservar, y el segundo es el tamaño de un solo elemento.
Además, calloc inicializa la memoria que hemos reservado a 0, y malloc esto no lo hace. (¡Cuidado con esto!)

Parece lógico que devuelvan un puntero (ya que lo que devuelve es una dirección de memoria al sector del heap que explicabamos antes). Este puntero puede ser o bien un puntero a un único elemento, o bien un puntero a una tabla de elementos, y nos moveriamos en estos elementos haciendo *(puntero+X) o bien puntero[X] donde X es el numero del elemento a acceder, empezando por cero (Esto ya lo hemos explicado en otro capítulo).

La razón de que devuelva un puntero de tipo void es que malloc y calloc no saben que tipo de información vas a usar en esa zona, y por lo tanto tú tendras que convertir ese puntero a void en un puntero a otro tipo (int, char o lo que sea) para poder usarlo mas adelante, con la conversión forzado que explicabamos en un capítulo anterior.

Por lo tanto, el ejemplo de antes del generador, con memoria dinámica podemos sustituir
int numeros[18];
Por:
int *numeros;
numeros = (int*)calloc( atoi(argv[1]), sizeof(int) );

Y con eso tendríamos una tabla con el tamaño que nos especifique el usuario por la linea de comandos.

No hay mucho mas que explicar sobre esto. Cuando las tablas sean fijas usa una tabla común, y si el tamaño de una tabla depende de una variable que se introduce en tiempo de ejecución, entonces usa memoria dinámica. La memoria dinámica tambien puede usarse para reservar memoria para variables, o estructuras, en tiempo de ejecución.

Eso es todo, si necesitas mas información sobre calloc, malloc, el heap o la memoria dinámica, no dudes en consultar google, o en consultarme a mí!
Un saludo y gracias por leer este sencillo intento de paper sobre punteros :)
« Última modificación: 18 de Abril de 2011, 02:02:08 am por TLX »
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #7 en: 20 de Marzo de 2011, 20:13:57 pm »
6. Reservado
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado Physlet

  • PHPero Experto
  • *****
  • Mensajes: 822
  • Karma: 41
  • Sexo: Masculino
  • Todo es posible con esfuerzo, dedicación e interés
    • Ver Perfil
    • PanamaDev
Re:Artículo: Punteros
« Respuesta #8 en: 20 de Marzo de 2011, 21:13:15 pm »
Me encanta el simple hecho que lo estés redactando tú.
Y quiero que tengan algo claro, la teoría es indispensable para todo lo que hagan.

Desconectado HostingUnEuro

  • PHPerit@
  • *
  • Mensajes: 31
  • Karma: 0
  • Nuev@ PHPer@
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #9 en: 20 de Marzo de 2011, 23:03:41 pm »

Excelente, espero que sigas desarrollándolo. Me parece muy interesante y muy generoso por tu parte por querer ampliar tanto este punto dentro del clamoroso mundo del C o C++

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #10 en: 22 de Marzo de 2011, 01:55:13 am »
Acabo de escribir el segundo punto.
Espero que alguien tenga la paciencia de leerlo jajaja aunque ciertamente puede resultar muy tedioso y incluso aburrido si no te gusta este mundo!

En fin! Si alguien encuentra un error o tiene alguna duda o sugerencia... estoy aquí para cualquier cosa.

Un saludo
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado Physlet

  • PHPero Experto
  • *****
  • Mensajes: 822
  • Karma: 41
  • Sexo: Masculino
  • Todo es posible con esfuerzo, dedicación e interés
    • Ver Perfil
    • PanamaDev
Re:Artículo: Punteros
« Respuesta #11 en: 22 de Marzo de 2011, 05:44:23 am »
Está quedando genial, esto me ha servido para aclarar unas cuantas dudas que tenía sobre los tipos de datos primitivos y lo de complemento a dos. Entonces el "complemento a uno", ¿vendría siendo el binario clásico? Pasa que a nosotros nos explicaron el año pasado sobre esto de complemento a dos, a uno y todo pero para resolver operaciones matemáticas con diferentes sistemas numéricos y nos tocaron el tema de complemento a dos sólo para realizar las restas, pero no nos explicaron realmente por qué debería ser así.

De veras que te felicito, por cierto corrige esto:
Citar
Ademas de estos modificadores ahí hay otros que indican que tipo de codificación se usará para almacenar ese número, por lo general solo dos: signed y unsigned.

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #12 en: 22 de Marzo de 2011, 11:27:20 am »
Está quedando genial, esto me ha servido para aclarar unas cuantas dudas que tenía sobre los tipos de datos primitivos y lo de complemento a dos. Entonces el "complemento a uno", ¿vendría siendo el binario clásico? Pasa que a nosotros nos explicaron el año pasado sobre esto de complemento a dos, a uno y todo pero para resolver operaciones matemáticas con diferentes sistemas numéricos y nos tocaron el tema de complemento a dos sólo para realizar las restas, pero no nos explicaron realmente por qué debería ser así.

De veras que te felicito, por cierto corrige esto:

Uy! Que fallo mas tonto! Gracias por avisar jeje =)

El complemento a uno y el binario clásico no es lo mismo no. El binario clásico no permite mostrar números negativos, mientras que el complemento a uno sí.
Se podría decir que el complemento a uno es como la versión anterior al complemento a dos. En el complemento a uno el bit mas significativo también indica el signo del número.
Si el BMS es 1, entonces significa que el número es negativo y en este caso se invierten todos los bits y al número que resulte de esta operación se le añade el signo negativo.
La diferencia es que en el complemento a dos, cuando un número es negativo, después de invertir todos los bits, a ese resultado se le sumaba uno.

Esto se cambió porque el complemento a uno tenía un fallo muy gordo, y es que había un numero que se podía representar de dos formas diferentes: el 0.
En complemento a uno (por ejemplo en 4 bits) es tanto 0000 como 1111.
Sin embargo en complemento a dos el 0 sería 0000, y el 1111 sería:
1111 -> BMS 1 -> Negativo -> Se invierten los bits: 0000 -> Se suma uno -> 1 -> Se añade el signo de antes -> -1

De esta forma, en el complemento a dos no se puede representa de distintas formas ningún número, lo que aumenta en 1 la cantidad de números negativos que podemos expresar.
Fijate que en complemento a uno, el número mas pequeño que podemos representar (con 4 bits, para que se mas fácil) es el 1000 que sería el -7, mientras que en complemento a dos el número mas pequeño sería también el 1000, solo que en este caso vale -8. Al invertir los bits y añadirle uno nos queda de nuevo 1000 que en decimal clásico es 8.

Saludos!
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es

Desconectado Physlet

  • PHPero Experto
  • *****
  • Mensajes: 822
  • Karma: 41
  • Sexo: Masculino
  • Todo es posible con esfuerzo, dedicación e interés
    • Ver Perfil
    • PanamaDev
Re:Artículo: Punteros
« Respuesta #13 en: 22 de Marzo de 2011, 18:42:53 pm »
Uy! Que fallo mas tonto! Gracias por avisar jeje =)

El complemento a uno y el binario clásico no es lo mismo no. El binario clásico no permite mostrar números negativos, mientras que el complemento a uno sí.
Se podría decir que el complemento a uno es como la versión anterior al complemento a dos. En el complemento a uno el bit mas significativo también indica el signo del número.
Si el BMS es 1, entonces significa que el número es negativo y en este caso se invierten todos los bits y al número que resulte de esta operación se le añade el signo negativo.
La diferencia es que en el complemento a dos, cuando un número es negativo, después de invertir todos los bits, a ese resultado se le sumaba uno.

Esto se cambió porque el complemento a uno tenía un fallo muy gordo, y es que había un numero que se podía representar de dos formas diferentes: el 0.
En complemento a uno (por ejemplo en 4 bits) es tanto 0000 como 1111.
Sin embargo en complemento a dos el 0 sería 0000, y el 1111 sería:
1111 -> BMS 1 -> Negativo -> Se invierten los bits: 0000 -> Se suma uno -> 1 -> Se añade el signo de antes -> -1

De esta forma, en el complemento a dos no se puede representa de distintas formas ningún número, lo que aumenta en 1 la cantidad de números negativos que podemos expresar.
Fijate que en complemento a uno, el número mas pequeño que podemos representar (con 4 bits, para que se mas fácil) es el 1000 que sería el -7, mientras que en complemento a dos el número mas pequeño sería también el 1000, solo que en este caso vale -8. Al invertir los bits y añadirle uno nos queda de nuevo 1000 que en decimal clásico es 8.

Saludos!
Ah ok.. Entonces otra pregunta, si almaceno un long, ¿estaré usando 8 posiciones de memoria diferentes? Tomando en cuenta que un long int ocupa 8 bytes

Desconectado CarlosRdrz

  • Moderador Global
  • PHPero Master
  • *****
  • Mensajes: 2.505
  • Karma: 131
  • Sexo: Masculino
  • A.k.a. TLX
    • Ver Perfil
Re:Artículo: Punteros
« Respuesta #14 en: 22 de Marzo de 2011, 19:01:12 pm »
Ah ok.. Entonces otra pregunta, si almaceno un long, ¿estaré usando 8 posiciones de memoria diferentes? Tomando en cuenta que un long int ocupa 8 bytes

Si haces sizeof(long int) y eso devuelve 8 quiere decir que ocupa 8 unidades de char, normalmente 8 bytes.
Es decir, que sí, ocuparía 8 posiciones de memoria distintas. Todas ellas consecutivas. En total serían 8posiciones*8 bits = 64 bits.
Con esos 64 bits podrías representar 2^64 numeros diferentes. En complemento a dos (signed) el número mas pequeño que puedes representar sería el -((2^64)/2) y el número mas alto el ((2^64)/2)-1.
En binario clasico (unsigned) el numero mas alto sería el (2^64)-1 y el numero mas pequeño el 0.
La dedicación de mi respuesta sera directamente proporcional a la dedicación de tu pregunta.
Hacer códigos que entiendan las máquinas es fácil, lo difícil y realmente útil es hacer códigos que entiendan las personas.
http://twitter.com/CarlosRdrz
http://www.carlosrdrz.es