0

How the logical NOT operator ! actually works in c? How it turns all non-zero int into 0 and vice-versa?

For example:

#include <stdio.h>

void main() {
    if(!(-76))
        printf("I won't print anything");

    if(!(2))
        printf("I will also not print anything");
}

doesn't print anything, which could mean -76 and 2 was turned into zero...

So, I tried this:

#include <stdio.h>

void main() {
    int x = !4;
    printf("%d", x);
}

which indeed printed 0

and now I don't get how, is it flipping all the bits to 0 or what?

coco
  • 19
  • 6
  • 2
    *"How it turns all non-zero int into 0 and vice-versa?"* because that's what it's defined to do. – dbush Jan 27 '23 at 22:15
  • 1
    Maybe you are thinking of bitwise NOT (`~`)? – 001 Jan 27 '23 at 22:21
  • @johnny-mopp nope – coco Jan 27 '23 at 22:27
  • 1
    logic not for variable `x` works like `if (x != 0) then 0 else 1;` – Iłya Bursov Jan 27 '23 at 22:30
  • At the level of the C language, there's no explanation. It's the responsibility of each C implementation to ensure that the semantics of the `!` operator are correct. C programmers can rely on it just working. If you're looking for a lower-level explanation, then you should focus your question more clearly on that, and apply a more appropriate tag. – John Bollinger Jan 27 '23 at 23:30
  • `!x` is equivalent to `x ? 0 : 1`. That should tell you all you need to know. – Tom Karzes Jan 28 '23 at 00:36
  • I assume you already understand that, when used in a truth context, a value is considered false if it is zero and true if it is non-zero. In the case of pointers, zero means the null pointer, so the null pointer is false and any other pointer is true. – Tom Karzes Jan 28 '23 at 00:50

2 Answers2

6

Most CPU architectures include an instruction to compare to zero; or even check the result against zero for most operations run on the processor. How this construct is implemented will differ from compiler to compiler.

For example, in x86, there are two instructions: JZ and JNZ: jump zero and jump not zero, which can be used if your test is an if statement. If the last value looked at was zero, jump (or don't jump) to a new instruction.

Given this, it's trivial to implement int x = !4; at assembly level as a jump if 4 is zero or not, though this particular example would be likely calculated at compile time, since all values are constant.

Additionally, most versions of the x86 instruction set support the SETZ instruction directly, which will set a register directly to 1 or 0, based on whether the processors zero flag is currently set. This can be used to implement the logical NOT operation directly.

Max
  • 10,701
  • 2
  • 24
  • 48
  • In a related question, here's a question about checking whether a register is zero in x86-64 assembly: https://stackoverflow.com/questions/29762012/how-to-check-if-a-register-is-zero-in-x86-64-assembly – Max Jan 27 '23 at 22:19
  • I get the point of JZ and JNZ, though it's still unclear to me how (not why) `!4` is evaluating to false at a low level, like I was asking "....flips all the bits or what?" – coco Jan 27 '23 at 22:25
  • By comparing to the processors `zero` flag, either using something like `JZ`, or `SETZ`. When 4 is loaded into a register, the `Z` flag is unset; then the compiler would either use `SETZ` (or similar) directly to set X to 0; or use `JNZ` to jump to a piece of code that will set X to zero. – Max Jan 27 '23 at 22:27
  • That is, on most modern architectures, pretty much everything you do will change the processor's Zero flag, and you make decisions/change values/skip code based on that. – Max Jan 27 '23 at 22:28
  • okay, `SETZ` and all of this seems reasonable for the decision making, but the value of x is also changing, so, I feel like looking into the implementation details of logical not, do you have any resources on it? – coco Jan 27 '23 at 22:44
  • 1
    There is nothing bit wise happening. Literally everything you do is being compared against zero all the time at the processor level and as another user says above, the most basic way to compile it is to compile it the same way as you would: `if (value) {x = 0;} else {x=1;}` It is nothing more complicated than that, other than SETZ is an optimization that can do it in one instruction. You can use something godbolt to see the assembly generated. – Max Jan 27 '23 at 22:47
2
6.5.3.3 Unary arithmetic operators

Constraints

1 The operand of the unary + or - operator shall have arithmetic type; of the ~ operator, integer type; of the ! operator, scalar type.

Semantics
...
5 The result of the logical negation operator ! is 0 if the value of its operand compares unequal to 0, 1 if the value of its operand compares equal to 0. The result has type int. The expression !E is equivalent to (0==E).

C 202x Working Draft

So, that's what language definition says should happen; if the expression x evaluates to non-zero, then the expression !x should evaluate to zero; if x evaluates to zero, then !x should evaluate to 1. The bits of the operand are not affected.

How that's accomplished in the machine code is up to the specific implementation; it depends on the available instruction set, the compiler, and various other factors such that no one answer works everywhere. It could translate to a branch statement, it could take advantage of specialized instructions, etc.

John Bode
  • 119,563
  • 19
  • 122
  • 198