【C言語】
論理否定演算子(ビックリマーク!)の基本的な使い方と紛らわしい使い方

■この記事の概要

この記事は、C言語の否定演算子「!」の使い方を、基本的な例とド・モルガンの法則を用いた応用的な使い方とともに解説しています。

簡単なサンプルコードを通じて、否定演算子のトリッキーな使い方や、条件式のリファクタリングに役立つ技術を紹介しています。

又、この記事は、C言語における論理否定演算子!の誤用によるエラー例を解説しています。

特にif(!x == 0)のようなコードの誤解を避けるための適切な書き方や、演算子の結合順序による問題とその対策を示しています。


論理否定演算子の基本的な使い方

//論理否定演算子ビックリマークの使い方
#include <stdio.h>
#include <ctype.h>
int main(void){
    char s[] = "ABC123def456\n";

    printf("もし、英字でけれなば %s ➡",s);
    for(int i = 0 ;s[i] != '\0';i++){
        if(!isalpha(s[i])){
            putchar(s[i]);
        }
    }
    
    printf("もし、数字でなければ %s ➡",s);
    for(int i = 0 ;s[i] != '\0';i++){
        if(!isdigit(s[i])){
            putchar(s[i]);
        }
    }
}
./a.out
もし、英字でけれなば ABC123def456
 ➡123456
もし、数字でなければ ABC123def456
 ➡ABCdef

■否定演算子でド・モルガンの法則を検証する

#include <stdbool.h>
#include <stdio.h>
//ド・モルガンの法則
void DeMorgan(bool x,bool y)
{
    bool    d11     =  (!x && !y);
    bool    d12     = !( x ||  y);
    puts(d11 == d12 ? "同じ" : "違う");
    
    bool    d21     =  (!x || !y);
    bool    d22     = !( x &&  y); 
    puts(d21 == d22 ? "同じ" : "違う");
}  
int main(void)
{
    DeMorgan(false,false);
    DeMorgan(false,true);
    DeMorgan(true,false);
    DeMorgan(true,true);
}
./a.out
同じ
同じ
同じ
同じ
同じ
同じ
同じ
同じ

複雑な条件式をリファクタリング(動作を変えずにコードをわかりやすくする事)する際、
ド・モルガンの法則はよく使用されます。


論理否定演算子の二重否定によるbool型変換

//論理否定演算子の二重否定によるbool型変換
#include <stdio.h>
int main(void){
    int x;
    x = 0;      printf("x = 0    ➡ %d\n",!!x);
    x = 1;      printf("x = 1    ➡ %d\n",!!x);
    x = 2;      printf("x = 2    ➡ %d\n",!!x);
    
    x = 0xFF;   printf("x = 0xFF ➡ %d\n",!!x);
}
./a.out
x = 0    ➡ 0
x = 1    ➡ 1
x = 2    ➡ 1
x = 0xFF ➡ 1

任意の値を真偽値(0/1)に変換したい時の技です。

どんな値でも!!と2重否定すると
必ず0か1になります。

非ゼロの時⇒!非ゼロは0⇒!0⇒1
ゼロの時 ⇒!ゼロは1⇒!1⇒0


■if(!x == 0)は誤読しやすい

warning: logical not is only applied to the left hand side of this comparison

警告:!の結合力は==よりも強い
[-Wlogical-not-parentheses]

#include <stdio.h>
void f(int x){
    //推奨しない書き方
    if(!x == 0) puts("1");
    if((!x)==0) puts("2");
    if(!(x==0)) puts("3");
    
    //推奨する書き方
    if(x == 0)  puts("4");
    if(x != 0)  puts("5");
}
int main(void){
    f(0);
    puts("★");
    f(1);
}
./a.out
4
★
1
2
3
5

if(!x == 0)のコードの意図が
if(x== 0)なのか
if(x != 0)なのか、よくわかりません。

if((!x)==0)や
if(!(x==0))のように丸括弧をつけても
分かりにくさは改善されません。


■論理演算とビット演算の混在はバグの臭い

warning: suggest parentheses around operand of ‘!’ or change ‘&’ to ‘&&’ or ‘!’ to ‘~’

警告:否定演算子!はビット演算子&より結合力が強い
[-Wparentheses]

#include <stdio.h>
void f(int x,int y) {
    printf("★x=%d,y=%d\n",x,y);
    
    //論理算子とビット演算の混在はバグの臭い
    printf("%08x\n",!x &  y);        
    
    //プログラマの意図がわからないので修正できない
    printf("%08x\n",!x && y);       //両方論理演算
    printf("%08x\n",~x &  y);       //両方ビット演算
    printf("%08x\n",(!x) & y);      //結合力を明示
    printf("%08x\n",!(x & y));      //結合力を明示
    printf("%08x\n",(x==0) & y);    //!の代わりに==0
    printf("%08x\n",(x & y) == 0);  //!の変わりに==0
}
int main(void){
    f(0,0);
    f(0,1);
    f(1,0);
    f(1,1);
}

論理演算とビット演算を混在させると、
プログラマが何をしたいか
分からなくなるので避けましょう。

原作
if(!x & y)

修正案
if(!x && y)//両方論理演算子
if(~x & y)//両方ビット演算子
if((!x) & y)//結合力を明示する
if(!(x & y))//結合力を明示する
if((x==0) & y)//!の代わりに==0
if((x & y) == 0)//!に代わりに==0


■if(!strcpy(p1,p2))はバグ

int f(char *p1,char *p2){
    if(!strcpy(p1,p2))  
        return  1;
    return  0;
}

多分strcmpの間違い
strcpyは第一引数をオーム返しするだけなので
第一引数にNULLはあり得ない。


■if(!strcmp(p1,p2) == 0) は多分バグ

int f(char *p1,char *p2){
    if(!strcmp(p1,p2) == 0)  
        return  1;
    return  0;
}

多分否定演算子!が不要

参考:

EXP20-C. 成功、真偽、等価を判定するには明示的な検査を行う