La verdadera razón se reduce a una diferencia fundamental en la intención entre C y C ++ por un lado, y Java y C # (por solo un par de ejemplos) por el otro. Por razones históricas, gran parte de la discusión aquí habla sobre C en lugar de C ++, pero (como probablemente ya sepa) C ++ es un descendiente bastante directo de C, por lo que lo que dice sobre C se aplica igualmente a C ++.
Aunque en gran parte se olvidan (y su existencia a veces incluso se niega), las primeras versiones de UNIX se escribieron en lenguaje ensamblador. Gran parte (si no únicamente) del propósito original de C era el puerto UNIX del lenguaje ensamblador a un lenguaje de nivel superior. Parte de la intención era escribir la mayor cantidad posible del sistema operativo en un lenguaje de nivel superior, o mirarlo desde la otra dirección, para minimizar la cantidad que tenía que escribirse en lenguaje ensamblador.
Para lograr eso, C necesitaba proporcionar casi el mismo nivel de acceso al hardware que el lenguaje ensamblador. El PDP-11 (por ejemplo) asignó registros de E / S a direcciones específicas. Por ejemplo, leería una ubicación de memoria para verificar si se presionó una tecla en la consola del sistema. Se estableció un bit en esa ubicación cuando había datos esperando ser leídos. Luego leería un byte de otra ubicación especificada para recuperar el código ASCII de la tecla que se había presionado.
Del mismo modo, si quisiera imprimir algunos datos, verificaría otra ubicación especificada y, cuando el dispositivo de salida estuviera listo, escribiría sus datos en otra ubicación especificada.
Para admitir la escritura de controladores para dichos dispositivos, C le permitió especificar una ubicación arbitraria utilizando algún tipo de entero, convertirlo en un puntero y leer o escribir esa ubicación en la memoria.
Por supuesto, esto tiene un problema bastante serio: no todas las máquinas en la tierra tienen su memoria idéntica a una PDP-11 de principios de los años setenta. Entonces, cuando tomas ese número entero, lo conviertes en un puntero y luego lees o escribes a través de ese puntero, nadie puede proporcionar ninguna garantía razonable sobre lo que vas a obtener. Solo por un ejemplo obvio, la lectura y la escritura pueden correlacionarse con registros separados en el hardware, por lo que usted (al contrario de la memoria normal) si escribe algo, intente leerlo de nuevo, lo que lea puede no coincidir con lo que escribió.
Puedo ver algunas posibilidades que deja:
- Defina una interfaz para todo el hardware posible: especifique las direcciones absolutas de todas las ubicaciones que desee leer o escribir para interactuar con el hardware de cualquier manera.
- Prohibir ese nivel de acceso y decretar que cualquiera que quiera hacer tales cosas necesita usar lenguaje ensamblador.
- Permita que la gente haga eso, pero deje que lean (por ejemplo) los manuales del hardware al que apuntan y escriban el código para que se ajuste al hardware que están utilizando.
De estos, 1 parece lo suficientemente absurdo como para que no valga la pena seguir discutiéndolo. 2 es básicamente tirar la intención básica del lenguaje Eso deja a la tercera opción como esencialmente la única que podrían considerar razonablemente.
Otro punto que surge con bastante frecuencia es el tamaño de los tipos enteros. C toma la "posición" que int
debería ser el tamaño natural sugerido por la arquitectura. Entonces, si estoy programando un VAX de 32 bits, int
probablemente debería tener 32 bits, pero si estoy programando un Univac de 36 bits, int
probablemente debería tener 36 bits (y así sucesivamente). Probablemente no sea razonable (y puede que ni siquiera sea posible) escribir un sistema operativo para una computadora de 36 bits utilizando solo tipos que garanticen que sean múltiplos de 8 bits. Tal vez solo estoy siendo superficial, pero me parece que si estuviera escribiendo un sistema operativo para una máquina de 36 bits, probablemente querría usar un lenguaje que admitiera un tipo de 36 bits.
Desde el punto de vista del lenguaje, esto conduce a un comportamiento aún más indefinido. Si tomo el valor más grande que cabe en 32 bits, ¿qué sucederá cuando agregue 1? En el hardware típico de 32 bits, se va a dar la vuelta (o posiblemente arroje algún tipo de falla de hardware). Por otro lado, si se ejecuta en hardware de 36 bits, solo ... agregará uno. Si el lenguaje va a admitir la escritura de sistemas operativos, no puede garantizar ninguno de los dos comportamientos: solo tiene que permitir que tanto el tamaño de los tipos como el comportamiento del desbordamiento varíen de uno a otro.
Java y C # pueden ignorar todo eso. No están destinados a admitir la escritura de sistemas operativos. Con ellos, tienes un par de opciones. Una es hacer que el hardware admita lo que exigen, ya que exigen tipos de 8, 16, 32 y 64 bits, solo construya hardware que admita esos tamaños. La otra posibilidad obvia es que el lenguaje solo se ejecute sobre otro software que proporciona el entorno que desean, independientemente de lo que el hardware subyacente pueda desear.
En la mayoría de los casos, esto no es realmente una opción o una opción. Más bien, muchas implementaciones hacen un poco de ambas. Normalmente ejecuta Java en una JVM que se ejecuta en un sistema operativo. La mayoría de las veces, el sistema operativo se escribe en C y la JVM en C ++. Si la JVM se ejecuta en una CPU ARM, es muy probable que la CPU incluya las extensiones Jazelle de ARM, para adaptar el hardware más de cerca a las necesidades de Java, por lo que hay que hacer menos en el software y el código Java se ejecuta más rápido (o menos lentamente, de todos modos).
Resumen
C y C ++ tienen un comportamiento indefinido, porque nadie ha definido una alternativa aceptable que les permita hacer lo que deben hacer. C # y Java adoptan un enfoque diferente, pero ese enfoque se ajusta mal (si es que lo hace) con los objetivos de C y C ++. En particular, ninguno de los dos parece proporcionar una manera razonable de escribir software de sistema (como un sistema operativo) en la mayoría del hardware elegido arbitrariamente. Ambos suelen depender de las instalaciones proporcionadas por el software del sistema existente (generalmente escrito en C o C ++) para hacer su trabajo.