TL; DR
C herdou os operadores !
e ~
de outro idioma. Ambos &&
e ||
foram adicionados anos depois por uma pessoa diferente.
Resposta longa
Historicamente, C se desenvolveu a partir dos idiomas iniciais B, que eram baseados no BCPL, que era baseado no CPL, que era baseado no Algol.
Algol , o bisavô de C ++, Java e C #, definiu true e false de uma maneira que pareceu intuitiva aos programadores: “valores de verdade que, considerados como um número binário (true correspondente a 1 e false a 0), são o mesmo que o valor integral intrínseco ”. No entanto, uma desvantagem disso é que lógica e bit a bit não podem ser a mesma operação: em qualquer computador moderno, ~0
é igual a -1 em vez de 1 e ~1
é igual a -2 em vez de 0. (Mesmo em um mainframe de sessenta anos, onde ~0
representa - 0 ou INT_MIN
, ~0 != 1
em todas as CPUs já fabricadas, e o padrão da linguagem C exige isso há muitos anos, enquanto a maioria das linguagens filhas nem se dá ao trabalho de suportar sinal e magnitude ou complemento de alguém.)
Algol contornou isso, tendo diferentes modos e interpretando operadores de maneira diferente nos modos booleano e integral. Ou seja, uma operação bit a bit era uma em tipos inteiros e uma operação lógica era uma em tipos booleanos.
O BCPL tinha um tipo booleano separado, mas um único not
operador , tanto para bits quanto lógicos, não. A maneira como esse precursor de C fez esse trabalho foi:
O Rvalue de true é um padrão de bits inteiramente composto por uns; o valor de false é zero.
Observe que true = ~ false
(Você observará que o termo rvalue evoluiu para significar algo completamente diferente nas linguagens da família C. Nós hoje chamaríamos isso de "a representação do objeto" em C.)
Essa definição permitiria lógico e bit a bit não usar a mesma instrução de linguagem de máquina. Se C seguisse esse caminho, diriam os arquivos de cabeçalho em todo o mundo #define TRUE -1
.
Mas a linguagem de programação B era de tipo fraco e não possuía tipos de pontos booleanos ou mesmo de ponto flutuante. Tudo era equivalente int
em seu sucessor, C. Isso tornou uma boa idéia para a linguagem definir o que aconteceu quando um programa usava um valor diferente de verdadeiro ou falso como valor lógico. Ele primeiro definiu uma expressão de verdade como "diferente de zero". Isso foi eficiente nos minicomputadores em que era executado, que tinham um sinalizador de CPU zero.
Havia, na época, uma alternativa: as mesmas CPUs também tinham uma flag negativa, e o valor de verdade do BCPL era -1; portanto, B poderia ter definido todos os números negativos como verdade e todos os números não negativos como falsidade. (Existe um remanescente dessa abordagem: o UNIX, desenvolvido pelas mesmas pessoas ao mesmo tempo, define todos os códigos de erro como números inteiros negativos. Muitas chamadas do sistema retornam um dos vários valores negativos diferentes na falha.) Portanto, seja grato: ele Poderia ter sido pior!
Mas definir TRUE
como 1
e FALSE
como 0
em B significava que a identidade true = ~ false
não era mais válida, e abandonara a forte digitação que permitia a Algol desambiguar entre expressões bit a bit e expressões lógicas. Isso exigiu um novo operador lógico-não, e os designers escolheram !
, possivelmente porque já não era igual a !=
, que parece uma barra vertical através de um sinal de igual. Eles não seguiram a mesma convenção &&
ou ||
porque ainda não existia.
Indiscutivelmente, eles deveriam ter: o &
operador em B está quebrado como projetado. Em B e C, 1 & 2 == FALSE
embora 1
e 2
são ambos valores truthy, e não há nenhuma maneira intuitiva para expressar a operação lógica em B. Esse foi um erro C tentou retificar em parte, adicionando &&
e ||
, mas a principal preocupação na época era a finalmente, faça um curto-circuito para funcionar e torne os programas mais rápidos. A prova disso é que não existe ^^
: 1 ^ 2
é um valor verdadeiro, embora ambos os operandos sejam verdadeiros, mas não pode se beneficiar de um curto-circuito.