A verdadeira razão se resume a uma diferença fundamental na intenção entre C e C ++, por um lado, e Java e C # (por apenas alguns exemplos), por outro. Por razões históricas, grande parte da discussão aqui fala sobre C e não sobre C ++, mas (como você provavelmente já sabe) C ++ é um descendente bastante direto de C, então o que diz sobre C se aplica igualmente a C ++.
Embora eles sejam amplamente esquecidos (e sua existência às vezes até negada), as primeiras versões do UNIX foram escritas em linguagem assembly. Grande parte (se não apenas) do objetivo original de C era a porta UNIX da linguagem assembly para uma linguagem de nível superior. Parte da intenção era escrever o máximo possível do sistema operacional em um idioma de nível superior - ou examiná-lo de outra direção, para minimizar a quantidade que precisava ser escrita em linguagem assembly.
Para conseguir isso, C precisava fornecer quase o mesmo nível de acesso ao hardware que a linguagem assembly. O PDP-11 (por exemplo) mapeou os registros de E / S para endereços específicos. Por exemplo, você leu um local de memória para verificar se uma tecla foi pressionada no console do sistema. Um bit foi definido nesse local quando havia dados aguardando para serem lidos. Você leu um byte de outro local especificado para recuperar o código ASCII da tecla que foi pressionada.
Da mesma forma, se você quiser imprimir alguns dados, verifique outro local especificado e, quando o dispositivo de saída estiver pronto, escreva seus dados em outro local especificado.
Para oferecer suporte à gravação de drivers para esses dispositivos, C permitiu especificar um local arbitrário usando algum tipo de número inteiro, convertê-lo em um ponteiro e ler ou gravar esse local na memória.
Obviamente, isso tem um problema muito sério: nem todas as máquinas na Terra têm sua memória distribuída de forma idêntica a um PDP-11 do início dos anos 70. Portanto, quando você pega esse número inteiro, converte-o em um ponteiro e, em seguida, lê ou escreve através desse ponteiro, ninguém pode fornecer nenhuma garantia razoável sobre o que você obterá. Apenas para um exemplo óbvio, a leitura e a gravação podem mapear para separar os registros no hardware; portanto, você (ao contrário da memória normal) se escreve algo e tenta lê-lo novamente, o que lê pode não corresponder ao que escreveu.
Eu posso ver algumas possibilidades que restam:
- Defina uma interface para todo o hardware possível - especifique os endereços absolutos de todos os locais que você pode querer ler ou gravar para interagir com o hardware de qualquer forma.
- Proibir esse nível de acesso e decretar que quem quiser fazer essas coisas precisa usar a linguagem assembly.
- Permita que as pessoas façam isso, mas deixe que leiam (por exemplo) os manuais do hardware que eles estão alvejando e escreva o código para se ajustar ao hardware que estão usando.
Destes, 1 parece suficientemente absurdo que dificilmente vale mais discussão. 2 é basicamente jogar fora a intenção básica da linguagem. Isso deixa a terceira opção como essencialmente a única que eles poderiam razoavelmente considerar.
Outro ponto que aparece com bastante frequência é o tamanho dos tipos inteiros. C assume a "posição" que int
deve ter o tamanho natural sugerido pela arquitetura. Portanto, se estou programando um VAX de int
32 bits, provavelmente deve ter 32 bits, mas se estou programando um Univac de 36 bits, int
provavelmente deve ter 36 bits (e assim por diante). Provavelmente não é razoável (e talvez nem seja possível) gravar um sistema operacional para um computador de 36 bits usando apenas tipos que garantem múltiplos de 8 bits. Talvez eu esteja apenas sendo superficial, mas parece-me que se eu estivesse escrevendo um sistema operacional para uma máquina de 36 bits, provavelmente desejaria usar uma linguagem que suporte um tipo de 36 bits.
Do ponto de vista da linguagem, isso leva a um comportamento ainda mais indefinido. Se eu pegar o maior valor que caberá em 32 bits, o que acontecerá quando eu adicionar 1? No hardware típico de 32 bits, ele será rolado (ou possivelmente causará algum tipo de falha no hardware). Por outro lado, se estiver rodando em hardware de 36 bits, apenas adicionará um. Se o idioma oferecer suporte à escrita de sistemas operacionais, você não poderá garantir nenhum dos dois comportamentos - basta permitir que o tamanho dos tipos e o comportamento do estouro variem de um para outro.
Java e C # podem ignorar tudo isso. Eles não pretendem oferecer suporte a sistemas operacionais de gravação. Com eles, você tem algumas opções. Uma é fazer com que o hardware suporte o que eles exigem - já que eles exigem tipos de 8, 16, 32 e 64 bits, apenas construa um hardware que suporte esses tamanhos. A outra possibilidade óbvia é que o idioma seja executado apenas em cima de outro software que forneça o ambiente desejado, independentemente do hardware subjacente.
Na maioria dos casos, isso não é realmente uma opção de escolha. Em vez disso, muitas implementações fazem um pouco de ambos. Você normalmente executa o Java em uma JVM em execução no sistema operacional. Na maioria das vezes, o sistema operacional é escrito em C e a JVM em C ++. Se a JVM estiver sendo executada em uma CPU ARM, é bem provável que a CPU inclua as extensões Jazelle da ARM, para adaptar o hardware mais de perto às necessidades de Java, portanto, menos precisa ser feito em software e o código Java é executado mais rapidamente (ou menos de qualquer maneira).
Sumário
C e C ++ têm comportamento indefinido, porque ninguém definiu uma alternativa aceitável que lhes permita fazer o que pretendem fazer. C # e Java adotam uma abordagem diferente, mas essa abordagem se encaixa mal (se é que existe) com os objetivos de C e C ++. Em particular, nenhum deles parece fornecer uma maneira razoável de escrever software de sistema (como um sistema operacional) na maioria dos hardwares escolhidos arbitrariamente. Ambos geralmente dependem das instalações fornecidas pelo software do sistema existente (geralmente escrito em C ou C ++) para realizar seus trabalhos.