No estoy seguro, pero creo que la respuesta es no, por razones bastante sutiles. Me pregunté en la informática teórica hace unos años y no conseguir una respuesta que va más allá de lo que voy a presentar aquí.
En la mayoría de los lenguajes de programación, puede simular una máquina de Turing de la siguiente manera:
- simulando el autómata finito con un programa que usa una cantidad finita de memoria;
- simulando la cinta con un par de listas enlazadas de enteros, que representan el contenido de la cinta antes y después de la posición actual. Mover el puntero significa transferir el encabezado de una de las listas a la otra lista.
Una implementación concreta que se ejecuta en una computadora se quedaría sin memoria si la cinta se alarga demasiado, pero una implementación ideal podría ejecutar el programa de la máquina Turing fielmente. Esto se puede hacer con lápiz y papel, o comprando una computadora con más memoria, y un compilador dirigido a una arquitectura con más bits por palabra y así sucesivamente si el programa alguna vez se queda sin memoria.
Esto no funciona en C porque es imposible tener una lista vinculada que pueda crecer para siempre: siempre hay un límite en la cantidad de nodos.
Para explicar por qué, primero necesito explicar qué es una implementación de C. C es en realidad una familia de lenguajes de programación. El estándar ISO C (más precisamente, una versión específica de este estándar) define (con el nivel de formalidad que permite el inglés) la sintaxis y la semántica de una familia de lenguajes de programación. C tiene muchos comportamientos indefinidos y comportamientos definidos por la implementación. Una "implementación" de C codifica todo el comportamiento definido por la implementación (la lista de cosas para codificar se encuentra en el apéndice J para C99). Cada implementación de C es un lenguaje de programación separado. Tenga en cuenta que el significado de la palabra "implementación" es un poco peculiar: lo que realmente significa es una variante de lenguaje, puede haber múltiples programas compiladores diferentes que implementan la misma variante de lenguaje.
En una implementación dada de C, un byte tiene valores posibles de CHAR_BIT . Todos los datos pueden representarse como una matriz de bytes: un tipo tiene como máximo
2 CHAR_BIT × sizeof (t) valores posibles. Este número varía en diferentes implementaciones de C, pero para una implementación dada de C, es una constante.2CHAR_BITt
2CHAR_BIT×sizeof(t)
En particular, los punteros solo pueden tomar como máximo . Esto significa que hay un número máximo finito de objetos direccionables.2CHAR_BIT×sizeof(void*)
Los valores de CHAR_BIT
y sizeof(void*)
son observables, por lo que si se queda sin memoria, no puede simplemente continuar ejecutando su programa con valores más grandes para esos parámetros. Estaría ejecutando el programa bajo un lenguaje de programación diferente, una implementación de C diferente.
Si los programas en un lenguaje solo pueden tener un número limitado de estados, entonces el lenguaje de programación no es más expresivo que los autómatas finitos. El fragmento de C que está restringido al almacenamiento direccionable solo permite como máximo estados del programa donde n es el tamaño del árbol de sintaxis abstracta del programa (que representa el estado del flujo de control), por lo tanto esto El programa puede ser simulado por un autómata finito con tantos estados. Si C es más expresivo, tiene que ser a través del uso de otras características.n×2CHAR_BIT×sizeof(void*)n
C no impone directamente una profundidad máxima de recursión. Se permite que una implementación tenga un máximo, pero también se permite que no tenga uno. Pero, ¿cómo nos comunicamos entre una llamada de función y su padre? Los argumentos no son buenos si son direccionables, porque eso limitaría indirectamente la profundidad de la recursividad: si tiene una función int f(int x) { … f(…) …}
, todas las apariciones de x
los marcos activos f
tienen su propia dirección, por lo que el número de llamadas anidadas está limitado por el número de posibles direcciones para x
.
El programa de CA puede usar almacenamiento no direccionable en forma de register
variables. Las implementaciones "normales" solo pueden tener un número pequeño y finito de variables que no tienen una dirección, pero en teoría una implementación podría permitir una cantidad ilimitada de register
almacenamiento. En dicha implementación, puede realizar una cantidad ilimitada de llamadas recursivas a una función, siempre que su argumento sea register
. Pero dado que los argumentos son register
, no puede hacer un puntero a ellos, por lo que debe copiar sus datos explícitamente: solo puede pasar una cantidad finita de datos, no una estructura de datos de tamaño arbitrario que esté hecha de punteros.
Con una profundidad de recursión ilimitada y la restricción de que una función solo puede obtener datos de su llamador directo ( register
argumentos) y devolver datos a su llamador directo (el valor de retorno de la función), obtiene el poder de los autómatas deterministas .
No puedo encontrar una manera de ir más allá.
(Por supuesto, puede hacer que el programa almacene el contenido de la cinta externamente, a través de las funciones de entrada / salida de archivos. Pero no se preguntará si C es Turing completo, sino si C más un sistema de almacenamiento infinito es Turing completo, para cuya respuesta es un aburrido "sí". También podría definir el almacenamiento como un oráculo de Turing: llamada fopen("oracle", "r+")
, fwrite
el contenido inicial de la cinta y fread
el contenido final de la cinta).