Não tenho certeza, mas acho que a resposta é não, por razões bastante sutis. Eu perguntei em Ciência da Computação Teórica há alguns anos e não obter uma resposta que vai além do que eu vou apresentar aqui.
Na maioria das linguagens de programação, você pode simular uma máquina de Turing:
- simulando o autômato finito com um programa que usa uma quantidade finita de memória;
- simulando a fita com um par de listas vinculadas de números inteiros, representando o conteúdo da fita antes e depois da posição atual. Mover o ponteiro significa transferir o cabeçalho de uma das listas para a outra lista.
Uma implementação concreta em execução em um computador ficaria sem memória se a fita ficasse muito longa, mas uma implementação ideal poderia executar o programa da máquina de Turing fielmente. Isso pode ser feito com caneta e papel ou comprando um computador com mais memória e um compilador visando uma arquitetura com mais bits por palavra e assim por diante, se o programa ficar sem memória.
Isso não funciona em C porque é impossível ter uma lista vinculada que pode crescer para sempre: sempre há algum limite no número de nós.
Para explicar o porquê, primeiro preciso explicar o que é uma implementação em C. C é realmente uma família de linguagens de programação. O padrão ISO C (mais precisamente, uma versão específica deste padrão) define (com o nível de formalidade que o inglês permite) a sintaxe e a semântica de uma família de linguagens de programação. C tem muitos comportamentos indefinidos e comportamentos definidos pela implementação. Uma "implementação" de C codifica todo o comportamento definido pela implementação (a lista de itens a serem codificados está no apêndice J da C99). Cada implementação de C é uma linguagem de programação separada. Observe que o significado da palavra “implementação” é um pouco peculiar: o que realmente significa é uma variante de linguagem, pode haver vários programas de compilação diferentes que implementam a mesma variante de linguagem.
Em uma determinada implementação de C, um byte possui valores possíveis de CHAR_BIT . Todos os dados podem ser representados como uma matriz de bytes: um tipo tem no máximo
2 CHAR_BIT × sizeof (t) valores possíveis. Esse número varia em diferentes implementações de C, mas para uma determinada implementação de C, é uma constante.2CHAR_BITt
2CHAR_BIT×sizeof(t)
Em particular, os ponteiros podem ter no máximo . Isso significa que existe um número máximo finito de objetos endereçáveis.2CHAR_BIT×sizeof(void*)
Os valores de CHAR_BIT
e sizeof(void*)
são observáveis; portanto, se você ficar sem memória, não poderá simplesmente continuar executando o programa com valores maiores para esses parâmetros. Você estaria executando o programa em uma linguagem de programação diferente - uma implementação em C diferente.
Se os programas em uma linguagem só podem ter um número limitado de estados, a linguagem de programação não é mais expressiva que os autômatos finitos. O fragmento de C restrito ao armazenamento endereçável permite apenas no máximo onde n é o tamanho da árvore de sintaxe abstrata do programa (representando o estado do fluxo de controle); programa pode ser simulado por um autômato finito com tantos estados. Se C é mais expressivo, deve ser através do uso de outros recursos.n×2CHAR_BIT×sizeof(void*)n
C não impõe diretamente uma profundidade máxima de recursão. É permitido que uma implementação tenha um máximo, mas também não é permitido. Mas como nos comunicamos entre uma chamada de função e seu pai? Os argumentos não são bons se forem endereçáveis, porque isso indiretamente limitaria a profundidade da recursão: se você tiver uma função int f(int x) { … f(…) …}
, todas as ocorrências de x
quadros ativos f
terão seu próprio endereço e, portanto, o número de chamadas aninhadas será limitado pelo número de endereços possíveis para x
.
O programa CA pode usar armazenamento não endereçável na forma de register
variáveis. As implementações “normais” podem ter apenas um número pequeno e finito de variáveis que não têm um endereço, mas, em teoria, uma implementação pode permitir uma quantidade ilimitada de register
armazenamento. Em tal implementação, você pode fazer uma quantidade ilimitada de chamadas recursivas para uma função, desde que seu argumento seja register
. Mas, como os argumentos são register
, você não pode fazer um ponteiro para eles e, portanto, precisa copiar explicitamente os dados deles: só é possível transmitir uma quantidade finita de dados, não uma estrutura de dados de tamanho arbitrário feita de ponteiros.
Com profundidade de recursão ilimitada e a restrição de que uma função pode obter apenas dados do chamador direto ( register
argumentos) e retornar dados ao chamador direto (o valor de retorno da função), você obtém o poder dos autômatos de empilhamento determinístico .
Não consigo encontrar um caminho a percorrer.
(É claro que você pode fazer com que o programa armazene o conteúdo da fita externamente, através das funções de entrada / saída de arquivo. Mas você não perguntaria se C é Turing-completo, mas se C mais um sistema de armazenamento infinito é Turing-completo, para qual a resposta é um chato "sim". Você também pode definir o armazenamento como um oráculo de Turing - chamada fopen("oracle", "r+")
, fwrite
o conteúdo inicial da fita e fread
o conteúdo final da fita.)