TL;DR
C inherited the !
and ~
operators from another language. Both &&
and ||
were added years later by a different person.
Long Answer
Historically, C developed out of the early languages B, which was based on BCPL, which was based on CPL, which was based on Algol.
Algol, the great-granddaddy of C++, Java and C#, defined true and false in a way that came to feel intuitive to programmers: “truth values which, regarded as a binary number (true corresponding to 1 and false to 0), is the same as the intrinsic integral value”. However, one disadvantage of this is that logical and bitwise not cannot be the same operation: On any modern computer, ~0
equals -1 rather than 1 and ~1
equals -2 rather than 0. (Even on some sixty-year-old mainframe where ~0
represents -0 or INT_MIN
, ~0 != 1
on every CPU ever made, and the C language standard has required it for many years, while most of its daughter languages don’t even bother to support sign-and-magnitude or one’s-complement at all.)
Algol worked around this by having different modes and interpreting operators differently in boolean and integral mode. That is, a bitwise operation was one on integer types, and a logical operation was one on boolean types.
BCPL had a separate boolean type, but a single not
operator, for both bitwise and logical not. The way this early forerunner of C made that work was:
The Rvalue of true is a bit pattern entirely composed of ones; the Rvalue of false is zero.
Note that true = ~ false
(You’ll observe that the term rvalue has evolved to mean something completely different in C-family languages. We would today call that “the object representation” in C.)
This definition would allow logical and bitwise not to use the same machine-language instruction. If C had gone that route, header files the world over would say #define TRUE -1
.
But the B programming language was weakly-typed, and had no boolean or even floating-point types. Everything was the equivalent of int
in its successor, C. This made it a good idea for the language to define what happened when a program used a value other than true or false as a logical value. It first defined a truthy expression as “not equal to zero.” This was efficient on the minicomputers on which it ran, which had a CPU zero flag.
There was, at the time, an alternative: the same CPUs also had a negative flag, and BCPL’s truth value was -1, so B might have instead defined all negative numbers as truthy and all non-negative numbers as falsy. (There is one remnant of this approach: UNIX, developed by the same people at the same time, defines all error codes as negative integers. Many of its system calls return one of several different negative values on failure.) So be thankful: it could have been worse!
But defining TRUE
as 1
and FALSE
as 0
in B meant that the identity true = ~ false
no longer held, and it had dropped the strong typing that allowed Algol to disambiguate between bitwise and logical expressions. That required a new logical-not operator, and the designers picked !
, possibly because not-equal-to was already !=
, which looks sort of like a vertical bar through an equal sign. They didn’t follow the same convention as &&
or ||
because neither one yet existed.
Arguably, they should have: the &
operator in B is broken as designed. In B and in C, 1 & 2 == FALSE
even though 1
and 2
are both truthy values, and there is no intuitive way to express the logical operation in B. That was one mistake C tried to partly rectify by adding &&
and ||
, but the main concern at the time was to finally get short-circuiting to work, and make programs run faster. The proof of this is that there is no ^^
: 1 ^ 2
is a truthy value even though both its operands are truthy, but it cannot benefit from short-circuiting.
~~
then not have been more consistent for logical NOT, if you follow the pattern that the logical operator is a doubling of the bitwise operator?!!foo
is a not-uncommon (not not common?) idiom. It normalizes a zero-or-nonzero argument to0
or1
.