前回自分のコードを見て少しがっかりした気持ちを書きましたが、大分時間がかかっているのも事実です。これには

 

訳があるのです。

 

といっても、最大の理由は、

 

自分自身の劣化

 

による「頭の回転」「記憶力の低下」「(特にコンソールプログラミングとユニコードに関わる)知見の乏しさ」等々の忸怩たる現実にあることは確かなんですが。とはいえ、

 

何とか目鼻がついた

 

ようなので、私の嵌ったドツボの紹介を兼ねて現状を報告しましょう。

 

1.出足は順調だった...が

最初にCALCクラスのプロトタイプを大雑把に決め、関連するメンバー関数(メソッド)の部品作りから始めたことはお話ししました。実際、10進数、2進数、16進数文字列の整数変換

 

class CALC
{
private:
...

    bool GetVal(wchar_t*, int&);                //10、2または16進数文字列から整数値を返す
    bool GetDec(wchar_t*, int&);            //10進数文字列から整数値を返す
    bool GetBin(wchar_t*, int&);            //2進数(0Bまたは0b)文字列から整数値を返す
    bool GetHex(wchar_t*, int&);            //16進数(0Xまたは0x)文字列から整数値を返す
};

//2、16または10進数文字列から整数値を返す
bool CALC::GetVal(wchar_t* str, int& sol) {

    if(GetBin(str, sol))
        return true;
    else if(GetHex(str, sol))
        return true;
    else if(GetDec(str, sol))
        return true;
    else
        return false;
}


//2進数(0Bまたは0b)文字列から整数値を返す
bool CALC::GetBin(wchar_t* str, int& sol) {

    int num = 0;                            
//値を求めるワーク変数
    int len = wcslen(str);                    //strの長さ-1(NULL文字非算入)
    //接頭指定が0Bまたは0bではない場合
    if(str[0] != L'0' || !(str[1] == L'B' || str[1] == L'b'))
        return false;
    for(int i = 2; i < len; i++) {
        if(str[i] < L'0' || str[i] > L'1')    
//2進数以外の文字があればエラー
            return false;
        num = num * 2 + (str[i] - L'0');
    }
    sol = num;
    return true;
}


//16進数(0Xまたは0x)文字列から整数値を返す
bool CALC::GetHex(wchar_t* str, int& sol) {

    int num = 0;                          
 //値を求めるワーク変数
    int len = wcslen(str);                    //strの長さ-1(NULL文字非算入)
    //接頭指定が0Xまたは0xではない場合
    if(str[0] != L'0' || !(str[1] == L'X' || str[1] == L'x'))
        return false;
    for(int i = 2; i < len; i++) {
        if(str[i] < L'0' || (str[i] > L'9' && str[i] < L'A') || (str[i] > L'F' && str[i] <L'a') || str[i] > L'f')
        {
            return false;
        }
        if(str[i] >= L'0' && str[i] <= L'9')
            num = num * 16 + (str[i] - L'0');
        else if(str[i] >= L'A' && str[i] <= L'F')
            num = num * 16 + (str[i] - L'A' + 10);
        else
            num = num * 16 + (str[i] - L'a' + 10);
    }
    sol = num;
    return true;
}


//10進数文字列から整数値を返す
bool CALC::GetDec(wchar_t* str, int& sol) {

    int num = 0;                            
//値を求めるワーク変数
    int len = wcslen(str);                    //strの長さ-1(NULL文字非算入)
    for(int i = 0; i < len; i++) {
        if(str[i] < L'0' || str[i] > L'9')    
//10進数以外の文字があればエラー
            return false;
        num = num * 10 + (str[i] - L'0');
    }
    sol = num;
    return true;
}

 

は順調に進み、似たようなものは纏め、ネーミングもC#経験を生かして、

 

    bool TryParse(int&, wchar_t*);            //10、2または16進数文字列から整数値を返す

 

に纏めました。(コードは纏めて最後に...)

 

2.文字列の扱いで凡ミスが続いた...

8bitの時代と違い、メモリーは潤沢にあるので、今回はせこく')'を'\0'で置き換えることなどせず、新しい仕様を導入しようとしました。が、...

 

(1)空白はループでスキップ...する筈が...

これは不具合というよりも、

あー面倒っ!

ということで、仕様変更に至った話です。

40年前は最初から空白文字を排除したのですが、今回はスキップで行こうと考え、非空白文字の先頭にサブポインターを置き、ループ中に空白文字になったら直前(非空白文字の最後)にサブポインターを置いて管理することを考えましたが、処理によって次の空白を更にスキップすることも必要となり、「変なフラグを導入して処理を複雑化するよりも」ということで、結局40年前と同じくクラスインスタンスの初期化段階で演算式の空白を総て除去した式文字列を保有することにしました。(

:と言っても、ユニコードの世界は甘くありません。「空白文字」と言っても実にたくさん有ります。localeを指定して日本語環境にし、"#include <cwctype>"で"std::iswspace(wchar_t)を使おうかとも思ったのですが、結局2進、10進、16進数と算術演算子、括弧のみを拾う形にしてみました。この処理の問題は不正な演算式が通ってしまうことですが(例:"12  34"等の演算子漏れが1234に解釈される)、取り敢えず大目に見てしまいました。(笑;↓を見れば許してくれるかも?)

<追記>

↑のように書いたのですが、矢張り気になります。(ムズムズ)矢張り気持ちが悪いのでこれは構文エラーかエラーとして値0を返すようにしたいですね。ということで、この後、

(1)演算式の初期化で「オペランドが連続する(演算子漏れ)」を検知する方法

(2)元の仕様に戻して、空白文字の有る演算式を処理する方法

の二つを試しています。

(1)もよいのですが、括弧(それも多重括弧)は、結局演算式分析と似たようなロジカルチェックを行うことになるので、(2)の方が良いかな、と考えています。2024年12月08日現在(2)のプロトタイプがキチンとエラーを出すようになりました。(下段は空白文字除去方のプロトタイプです。)

しかしまだ完全とはいいがたく、次の例のような不具合が発生し、まだ原因を突き止めておりません。

【最後の" - 9"が処理できずにエラーになる式】
(  2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9

(出力)「演算式が正しくありません。」(デバッグすると最後の'9'の所で整数変換できません。)

【最後の" - 9"が処理できる式】
(2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9

(出力)「演算式の答え:39」(最初の2前の空白2つを除去すると、完動します。)

なお、これを書いている今「空白一つならどうなるのかな?」と思い、実行したらほぼ暴走状態になりました。

ンンーーー。

 

(2)文字列の切り取り

【現象】正しい演算式を与えても「不正な演算式」として認識されるようになりました?

 

【原因】生まれて初めて「(CALC)クラス内(CPOS)クラス」を作り、「文字列の一部を切り取った文字列」を扱う部分文字列クラス(Class Part OString)を作り、スタートポインターとエンドポインターで切り出すことにしましたが、その際に使った関数がwcsncpyであることを忘れ、ヌル終端をつけ忘れたので、知らない間に「尾ヒレ(文字列)」がついて、不正とされてしまいました。

 

【対策】勿論、最後にヌル終端を明確に入れました。

 

(3)文字列配列の更新

【現象】動作テストもパスして、一応の目鼻がついたプログラムをヘッダーとテストプログラムに分離したら、落ちるようになりました。

【原因】自分でも気がついていなかったのですが、メンバー変数の演算式文字列ポインターを元々のプログラムでは"wchar_t m_formula= 0;"とヌル初期化していたのですが、ヘッダーにする際に「これ、いらないよね」と削除したみたいなんです。これがコンストラクターの(演算式文字列更新用の)"delete [] m_formula;"のご機嫌を損ね()、関係ないメモリーを開放しまくっていたようです。ヘッダーとテストプログラムの分離前はきちんと動いていたので見つけるまでに時間がかかっちゃいました。

ご参考

 

【対策】前と同様に演算式文字列ポインターをヌル初期化すればdeleteの方で勝手に回避してくれるのですが、ここはやはり明示的に対応すべきと判断し、演算式文字列ポインターをヌル初期化するとともに、deleteも「ヌルポインターでなければ」という条件式を入れました。(因みに私は旧い人なので、ヌルポインターを'0'で設定していましたが、C++11からはnullptr定数が出来たということなので、これに切り替えました。)

 

3.「DOS窓」を安易に考えて、知見が無かった...

今回この話はとても勉強になったのですが、(当たり前ですが)Windowsのコンソールウィンドウ(通称「DOS窓」)も矢張りウィンドウなんですね。従ってここに表示する言語や文字が(Shift-JISやUTF-8などで共通の)ANSI程度ならともかく、ユニコードを使おうと思ったら、きっちりと管理しないとだめそうです。例えばWindows 11を日本語で使っている方のDOS窓について、(ユニコードベースのプログラミングで)次の二つの関数を実行してみてください。

 

//ワイド文字を使用する
#define        UNICODE    1    //Unicode使用の為
#include    <wchar.h>    //Unicode使用の為
#include    <iostream>    //wcout、wcin使用の為

 

//コンソールによって使用される入力コード ページを取得
wcout << L"GetConsoleCP: " << GetConsoleCP() << endl;
//コンソールによって使用される出力コード ページを取得
wcout << L"GetConsoleOutputCP: " << GetConsoleOutputCP() << endl;

 

共に出力は"932"で"shift_jis    ANSI/OEM 日本語;日本語 (Shift-JIS)"であることが判ると思います。従ってUTF-16のwchar_tを表示する為には、(Chat-GPT様によれば)

 

    SetConsoleCP(CP_UTF8);                //入力コードページをUTF-8に設定(CP_UTF8はwindows.hで定義)
    //SetConsoleOutputCP(CP_UTF8);        //出力コードページをUTF-8に設定入れると何故か文字化けする)
    std::wcin.imbue(locale("japanese"));    //wcinにロケールを設定
    std::wcout.imbue(locale("japanese"));    //wcoutにロケールを設定
 

とするように言われましたが、↑に書いたように何故かSetConsoleOutputCP関数は使うと文字化けしてしまいます。(これは疲れるので余り追究しないようにしました。コメントアウトすると正しく表示され、大勢に影響がないので...)
 

4.コンソールプログラミングを甘く見ていた

私もWindowsプログラミングでアルゴリズムやプロトタイプをコンソールプログラムで試験して組み込むようにしていましたし、ユニコードもECCSkeltonを作ったので一定分かったつもりになっていたのですが、コンソールプログラムでのユニコード使用は経験不足が露呈しました。

 

先ず、エントリーポイントが

 

int main(int argc, char** argv)

 

ではなく、

 

int wmain(int argc, wchar_t **argv)

 

ことや、C由来の文字列操作関数が「wcsナンチャラ」(例:strlen(char*)→wcslen(wchar_t*)、wcsはwide character stringの略だろう)になることなどは一定知っていましたが、コンソール入出力のcoutとcinがwcoutやwcinとなり、文字列もwchar_t(WCHARはWindows用-Esent.hで定義)で定数表記はL"...."になることは知っていましたが、結構間違えたり、上記「3.「DOS窓」を安易に考えて、知見が無かった...」を理解するまで文字化けに悩まされました。しかし、

 

一番困惑したのはwcinの動作を知らなかった

 

事でした。ずーっと

 

wcin >> (文字列wchar_t配列);

 

でDOS窓に入力した文字列が入ると(wcout(cout)は空白入り文字列を出力できるので、wcin(cin)もそうだろうと勝手に)信じて疑わなかったのですが、テスト中に空白付の"1 + 2 + 3"を演算させると、答えが"1"になるバグで調べまくった挙句、"1 + 2 + 3"はwchar_t変数5つに夫々'1'、'+'、'2'、'+'、'3'を入れるしかない、と判明しました。

無駄にコードを舐めるように見返していた時間」がもったいなかったです。

仕様としてwcinは(cinも)「空白区切りで入力」するため、空白を含む文字列の入力は、

std::wstring s;    //wchar_tへの変換はc_str()メソッドを使う

std::getline(std::wcin, s);

を使え、ということのようです。

 

ということで、

 

現在のテストプログラムは以下の通り。

【TestCalc.cpp】

//ワイド文字を使用する
#define        UNICODE    1    //Unicode使用の為
#include    <wchar.h>    //Unicode使用の為

//テストプログラム用のヘッダー
#include    <windows.h>    //定数CP_UTF8使用の為
#include    <conio.h>    //getch()使用の為
#include    <iostream>    //wcout、wcin使用の為
#include    <string>    //getline使用の為

#include    "Calc.h"    //CALCクラス定義ヘッダーファイル

using namespace std;    //解説:これを忘れないでください。

//////////////////////////////
// TestCalc.cpp
// Copyright 2024 by Ysama
//////////////////////////////

#define    LONG_STR    512    //解説:元々はwcin用に"wchar_t formula[LONG_STR];"としていたためです。これは不要でした。

int wmain(int argc, wchar_t **argv) {    //Unicodeの場合、mainではない。(https://learn.microsoft.com/ja-jp/cpp/c-language/using-wmain?view=msvc-170)

    SetConsoleCP(CP_UTF8);                    //入力コードページをUTF-8に設定
    //SetConsoleOutputCP(CP_UTF8);            //出力コードページをUTF-8に設定入れると何故か文字化けする
    std::wcin.imbue(locale("japanese"));    //wcinにロケールを設定
    std::wcout.imbue(locale("japanese"));    //wcoutにロケールを設定

    wstring s;
    wchar_t *formula;

    do {
        wcout << L"整数の算術演算式を演算子(+, -, *, /, %)を使って入力してください(0-終了):";    //Unicodeの場合、coutではない。
        getline(wcin, s);
        formula = (wchar_t*)s.c_str();
        if(*formula == L'0')
                return 0;
        wcout << endl << L"確認:" << formula << endl;
        int result;
        //解説:CALCクラスは以下の3つの使い方があります。
        //使い方1
        CALC calc;
        if(calc.TryCalc(result, formula))
        //使い方2
        //CALC calc(formula);
        //if(calc.TryCalc(result))
        //使い方3
        //CALC calc;
        //calc = formula;
        //if(calc.TryCalc(result))

            wcout << L"演算式の答え:" << result << endl;
        else
            wcout << L"演算式が正しくありません。" << endl;
    } while(true);
    return 0;
}

 

まぁ、この他にも色々と「私の恥ずかしい経験」があるのですが、未だ「完全体のセル」ではないので、まだデバッグ分析、テスト試行等修行を重ねてゆきたいと思っています。

 

まぁ、頭を使うので「ボケ防止」(のおもちゃにする)にはもってこいです。(笑)

 

前回Calcの大雑把なイメージを書き、無駄話で部品作りから始めたことを書きました。

 

が、

 

結果的にこれは余りうまくなかったかなー、と現在少し反省しています。どういうことかというと、

 

先ず、

 

20代の時に作ったオリジナルの処理、即ち

 

(1)(当時の8bitコンピューターの主記憶空間は64KBしかなかったので)予め余計な空白文字当然Unicodeなどはなかった)は所与の算術式から除去する。

(2)そして基本的に二項式にして、Operand1とOpernd2、そしてOpecode(+-*/%)に分解し、

(3)先ずOperand1を整数値化(この時に単項式も処理する)し、

(4)次にOpecodeを読んで、これも整数値化したOperand2をOperand1にOpecodeに従って加減乗除する(これを繰り返す)というもので、

(5)算術順序は「加減式」処理を行って、Operand1、2の整数化で処理できない文字列を「乗除式」処理に渡す、それでも値が取得できない場合は(オリジナルでは更にラベル検索・登録値取得処理がありましたが)エラーとする。

(6)括弧はカッコ内の算術式をこの処理全体に再投入する(再帰処理-なお、投入文字列もコピーせずに、投入時に')'にNULL終端を入れる、という涙ぐましい処理を行っていました。

 

を思い出して、「それに近似したものを」(しかしメモリーはたっぷりあるのでけちけちせずに)再現しようとしたのが間違いだったようです。

 

現在のプロトタイプは何か冗長的で「美しさ」を感じさせません。

 

もう少し考えてみましょうか?

 

本日第二回目の【Calc】記事を書きましたが、文中で

 

漠然と「8bitのMZ-2500とBDS Cで書いた時の仕様」

 

等と書いています。これを少し説明すると、「【昔話】私の8ビット時代」(特に後半)のことです。今C++の学びなおし、コンソールプログラムのユニコード対応の学習やテストなどをしながら、少しづつCalcを書いていますが、それに比べると30代だった私の頭はもっともっと柔軟で、回転も速かったんだなぁー、と思い知らされます。(歳は取りたくないものです。)

 

断片的に当時のアルゴリズムを思い返しながら、現在はC++11準拠のフルセットのC++コンパイラーで書いているので、随分と楽が出来そうですが、逆に「あー、どうするんだっけ?」等と頭を抱えたり、時々ズルでChatGPT様にアドバイスをもらっています。しかし、当時のアッセンブラープログラム全体を100とすると、算術演算部分(単に2、10、16進数演算をするだけではなく、整数化できない文字列は、メモリーアドレスのラベルとして登録、検索、演算等も行っていたので、今回のCalcよりもずーっと優れものです)は10にも満たないものですが、それすらもスラスラとは書けません。

 

あ"~、情けないなぁ。

 

と長嘆息してしまいます。例えば今やっとクラスの大まかな構成、メンバー変数、メンバー関数を決めつつあるのですが、先ずは文字列から整数値へ変換することが大事と、

 

//2、16または10進数文字列から整数値を返す
bool CALC::TryParse(wchar_t* str, int& res) {

・・・・

}
 

を作り、(優先順位の為に「加減算処理」→「乗除算処理」→「括弧処理」でオペランド文字列を渡してゆけば自然とその逆に処理がなされて優先順位が決まるので)「加減算処理」で必要な、

 

//括弧()をスキップし、その長さを返す。(括弧内文字列先頭:m_start、末尾:m_end)
int CALC::SkipBraces(wchar_t* str) {

・・・・

}

 

//スペース、タブ、全角スペース等をスキップする(戻り値は次のアドレス)
wchar_t* CALC::SkipSpaces(wchar_t* str) {

・・・・

}

 

//+、 -、 *、 /、または%か否かを1-5、非該当を0で返す
int CALC::IsOperator(wchar_t c) {

・・・・

}

 

処理まで来たところです。(一応いずれもテスト上はパスしています。)

 

そしていよいよ算術式の評価に向かおうとしています。しかしここまででも「空白文字をスキップする」という当たり前の話で躓いています。

 

元々C(言語)には

 

int isspace(char)

解説:ANSI文字コードの空白文字(0x09~0x0d(タブや改行コード), 0x20(スペース))なら真を返す。

 

という関数があり、昔だったらこれを使うところ()ですが、ANSIとマルチバイト文字は今どき誰も使わないし、垂直タブや改行などは不要なこともあり、悩みました。

:20余年前のBCCSkeltonでは、CSTRクラスで次のような形で処理していますね。

    //スペース文字はスキップする
    while(isspace(*ptr) || (*ptr == -0x7F && *(ptr + 1) == 0x40) || *ptr == ',') {
        if(isspace(*ptr) || *ptr == ',')
            ptr++;                        //スキップスペース文字
        else {
            ptr++;    ptr++;                //全角" "もスキップ
        }
 

今回は(Localeが日本語環境の)ユニコードで書いているので、スペース、タブ、全角スペースをスキップするような関数(メソッド)を自作しようかな、と考えています。

 

では、wchar_t変数のユニコードの番号はいくつになるのか、とかどう表記するのか、とかでいちいち躓き、いちいちテストプログラムで確認しています。()例えば、

 

    //IsSpace用テスト
    wchar_t test[4] = {'\u0041', '\u0042', '\u0043', L'\0'};    //test = L"ABC";
    wcout << test << endl;
 (出力:"ABC")

 

の様に、です。

:真面目に考えればユニコードのスペースはたくさんあるのだそうです。しかし、日本語環境だけで考えれば一定絞った対応っでよいと思います。

 

なんか疲れますが、知らなかったことを

 

学習するって楽しいっ!

 

というのは間違いがないです。

 

さて、前回お話しした通り、私のC++プログラミング(注)は大分錆が浮いているので、リフレッシュ、リハビリが必要ですが、一方、プログラミングを進めるうえで重要な「成果物の仕様」を煮詰めてゆかなければなりません。

:私のC++プログラミング環境は、当時はべらぼうに値段が張ったVisual Studio(当時のVisual C++)ではなく、昔のBorland C++コンパイラー(bcc32.exe)の承継会社であるEmbarcadero C++コンパイラー(bcc32c.exe)になります。(時々これをECCと呼んだりしますのでご容赦を。)尚、これらは32bit コンパイラーですが、ECC(bcc32c.exe)はC++11に準拠しています。

 

前に書いた「整数算術式の演算プログラム」ですが、漠然と「8bitのMZ-2500とBDS Cで書いた時の仕様」を考えていますが、何せもう詳細や細部は忘れているので、改めてアウトラインを考えてゆきましょう。

 

1.算術演算

特にコンピューターの行う演算には大雑把に言って算術演算論理演算がありますが、(昔のVBに沿って書いているのでしょうが)大体はこんな感じでしょうか?少なくとも四則演算(+、-、*、/)に余り(%)位は計算してもらうつもりです。

 

2.整数

ECCでは符号付の32bit整数になるので、-2,147,483,648から2,147,483,647(十進数)までは計算できそうです。なお、十進数の他、2進数と16進数は使いたいですね。昔アッセンブラーを踏んだ時はニモニック表記に合わせて"1001B" とか"0AFH"とか書きましたが、今回はC++に敬意を表して接頭詞の"0B || 0b"と"0X || 0x"を使うつもりです。

 

3.優先順位

(1)左から右へ演算を行う。

(2)演算子の優先順位は「括弧内の式」>「*、/、%」>「+、-」

で始めてみようかと思います。

 

4.入力と出力

入力は「式の文字列」で行い、出力は整数値(オーバーフロー対策なし)で行いましょう。

 

別にこれをライブラリーにするつもりはありませんが、一応「整数式を演算するオブジェクト」のクラスとして記述するつもりです。

 

昨日の【無駄話】でまたまた世迷言を書きましたが、久々にC++でプログラムを書きたい、というのは本音で、今日から少しずつ進めて行こうかとアウトラインを書き始めました。名付けて

 

Calc

 

という「整数算術式の演算プログラム」です。

 

ところが、

 

いやはやC#にどっぷりと漬かっていたので最初からエラーの連発です。

 

先ず、整数算術式の演算を行うオブジェクトの「CALCクラス」を作ろうということで、

 

////////////////////////////////
// Calcクラス定義ファイル
//(整数算術式演算ライブラリー)
// Copyright (c) 2024 by Ysama
////////////////////////////////

class CALC
{
private:

public:
    CALC();                                  //コンストラクター
    ~CALC();                                //デストラクター
    int DoCalc(wchar_t*);              //演算制御を行う主関数
};                                               //解説:これがC#と違う

 

というところから始めたのは良いのですが、先ず入力文字列バッファーを確保しようとして、

 

char[512] formula;    //formulaという文字列変数

 

等と書いてしまい、コンパイラーに怒られました。(

:C#では”string”で済んでしまいますが、C++では1バイトのchar変数の配列を用意します。この際、C#なら"char[] formula = new char[512];"等と書くので、ごっちゃになりました。C++で書く場合は"char formula[512];"が正解です。

 

そんな覚束ないことをやっているのに、

 

「どうせやるならUnicodeで。」

 

と思ったために、更に

 

//ワイド文字を使用する
#define        UNICODE    1
#include    <wchar.h>    //Unicode使用の為

 

をつけたのは良いのですが、次のような形でエラーでまくりです。(

 

アウト     正解

WCHAR     wchar_t

cout      wcout

cin       wcin

strlen     wcslen

strcpy     wcscpy

main      wmain

:WCHARはWindowsで使うMicrosoftの型でした。正しくはISO/IEC 9899/AMD1:1995で定まったwchar_tとなります。又、私はUnicodeを使ったECCSkeltonを作りましたが、今回はコンソールベースにするので、そういう意味ではAnsiベースの関数のユニコード版については全くのトーシロであったことを思い知らされました。なお、"wcs"は("str"が"string"を意味したように)"wide character string"の意味かと。

 

トホホ...

 

ですが、まぁ、のんびりじっくりやりましょう!

(写真はCalcのテストプログラムで、先ずは2、16、10進数の整数変換を行う処理の実行画面)

 

このところWindowsベースのC#による安易なプログラミングが多く、はたと自分がC++のことを忘れていることに気が付きました。

 

初めてコンピューター(MSX PC-SONY HitBit)を買った時は、(長男の子育てで多忙でもあり、長男と神さんの目を盗んで)ROM Basicしか入っていないのにただプログラムを入力して、実行するだけで、それをTV(MSXはモニターがテレビだったんです)で眺めているだけで楽しかった。

 

しかし今は、(昔の親父が帰宅すると直ぐにTVにスイッチを入れる様に)日常的に64bit Windows 11に火を入れると、ルーティンのPUA検索・駆除ソフトを二つ実行し、Explorerを立ち上げて、取り敢えずChromeで世の動静を探り、動画や音楽を楽しむ毎日です。

 

いかんっ!!!

これでは本ブログの目的である「ボケ防止」にならんっ!!!

 

ということで、もう使えたり、役に立ったりするソフト(アプリ)を作るのではなく、

 

脳味噌を鍛えるような、早朝ウォーキングのようなプログラミング

 

をしようと思います。

 

そんなこんなを入浴しながら考えていた時に、

 

大昔、Sharp MZ2500でBDS-Cを使って、Z80用アセンブラーZASMを組んだなぁ

 

という記憶がよみがえり、

 

そういえば、あの時算術式をどうやって解釈するプログラムにしたっけ?

 

と思った時に愕然としました。

 

思い出せないっ!

 

ということで、当分「あの時なにしたっけ?」を題材に、

 

コンソールベースで(Cではなく)C++で再現に務め、出来上がったらそれをC#に移植でもして処理時間を比べてみよう。

 

というプロジェクトを組むことにしました。乞御期待、です。

 

昨日は全く問題なく起動、正常動作したMoPaiとTestMahjongTable01、本日は早速、MoPaiで通知が来て(しかし立ち上がりましたが、その後ファイルが強制的に削除されました)、「ならば」と起動したTestMahjongTable01で、またまた

 

 

がでました。

 

勿論、「Windows セキュリティ」でチェックし「問題なし。」、更にカスタムスキャンでMahjongフォールダーをチェックしましたが矢張り「現在の脅威はありません。」「処置は不要。」でした。

 

イライラしません?

 

MoPaiのプログラム自体を紹介すると、

 

.起動時に↓のような使用方法の説明があり、

 

.初期画面は、全部伏せ牌となります。(最下段が手牌13牌で、その上は河)

 

.ここで最下段の手牌にマウスカーソルを動かすと、最下段の手牌選択の赤枠が現れます。(河では選択の赤枠が出ず、手牌しか選択できません。)

 

.手配を「盲牌」して(一つ開いて)、「河」から同じものを探します。(手牌を開いた状態では、河の牌しか赤枠が出ず、手牌は再選択できません。)

 

.このペアの探索を執念深く繰り返し、最後の牌のペアを作ると、

 

.完了通知が出ます。

 

そう、ただそれだけ...

 

この後「ゲームを続けますか?」と訊かれますが、恐らく「終了」を選ぶでしょう。それ位結構exhaustedします。

 

カードの神経衰弱と対比してみましょう。

 

          カード版    麻雀牌版

総数          52      136

ペア対象数        4        4    (カードは番号だけ、麻雀牌は同一牌だけ)

ペア数         26       13

私の試行回数     80回位    130-150回位 (単に私がボケなのかも...)

 

カードの神経衰弱にしておく方が無難かも?

 

ps. ということで、PUAエラーも出ることだし(テンションダダ下がり)、ソ-スを公開すべきかどうか迷っています。

 

先日書いた【ちょっと為になる話】.Netのウィルス検知の後日譚です。

 

結局「PUA」エラーは出るわ、原因や対象が不明のまま、取り敢えず問題となる「Mahjong)」というフォールダーをDefenderでスキャンし、問題が無いので「Windows セキュリティ」の「ウィルスと驚異の防止」→「ウィルスと驚異の防止の設定」→「設定の管理」→「除外の追加または削除」で当該フォールダーをMicrosoft Windows Defender リアルタイム保護の対象外として作業をしていましたが、なんかスッキリしません。

:この中にmahjongtable.dllや、エラーダイアログが出たテストプログラムのTestMahjongTable01.exe、開発中のMoPai.exeが入っています。

 

「そういえばDefenderのログを見ていなかった!」ということで、webで調べてどうも「イベントビューワー()」でDefenderを調べるのだそうです。

:コントロールパネル→システムとセキュリティ→Windows ツールの中に入っています。

 

イベントビューワーも対象が広大なので、左のペインの「アプリケーションとサービス ログ」のツリービュー→Microsoft→Windowsと展開し、「Windows Defender」の"Operational"というログを開きます。(

:最初"Defender"だけで探して見つけられませんでした。又このツールは保秘性が高いのか、スクリーンプリントを取ろうとしても、反応せず撮れませんので以下は「テキストだけ」となります点、ご容赦ください。

 

すると、リストボックスに時系列的なログが表示され、多くの「レベル」列の「情報」レベルログの中に、「警告」レベルログが見つかりました。以下にそのサンプルをコピーした抜粋を載せますが、一番旧いものが、

 

ログの名前:      Microsoft-Windows-Windows Defender/Operational
ソース:          Microsoft-Windows-Windows Defender
日付:          
 2024/11/13 7:47:12
イベント ID:     1116
タスクのカテゴリ:なし
レベル:          
警告
キーワード:      
ユーザー:        SYSTEM
コンピューター:  Ysama-PC-2022
説明:

Microsoft Defender ウイルス対策 でマルウェアまたは他の望ましくない可能性のあるソフトウェアが検出されました。
 詳細については、次を参照してください:
https://go.microsoft.com/fwlink/?linkid=37020&name=Trojan:Win32/Wacatac.B!ml&threatid=2147735505&enterprise=0
     名前:
Trojan:Win32/Wacatac.B!ml
     ID: 2147735505
     重大度:
重大
     カテゴリ: トロイの木馬
     パス: file:_C:\Users\ysama\Programing\C# Programing\Samples\Games\Mahjong\MoPai.exe
     検出元の場所: ローカル コンピューター
     検出の種類: 高速パス
     検出元: リアルタイム保護
     ユーザー: Ysama-PC-2022\ysama
     プロセス名: C:\Windows\explorer.exe
     セキュリティ インテリジェンスのバージョン: AV: 1.421.251.0, AS: 1.421.251.0, NIS: 1.421.251.0
     エンジンのバージョン: AM: 1.1.24090.11, NIS: 1.1.24090.11


で、翌11月14日まで調査の為に何回か起動した際のエラーが記録されています。

 

ログの名前:   Microsoft-Windows-Windows Defender/Operational
ソース:         Microsoft-Windows-Windows Defender
日付:            
2024/11/14 9:26:45
イベント ID:  1116
タスクのカテゴリ:なし
レベル:        
警告
キーワード:      
ユーザー:      SYSTEM
コンピューター:Ysama-PC-2022
説明:

Microsoft Defender ウイルス対策 でマルウェアまたは他の望ましくない可能性のあるソフトウェアが検出されました。
 詳細については、次を参照してください:
https://go.microsoft.com/fwlink/?linkid=37020&name=Trojan:Win32/Wacatac.B!ml&threatid=2147735505&enterprise=0
     名前:
Trojan:Win32/Wacatac.B!ml
     ID: 2147735505
     重大度:
重大
     カテゴリ: トロイの木馬
     パス: file:_C:\Users\ysama\Programing\C# Programing\Samples\Games\Mahjong\MoPai.exe
     検出元の場所: ローカル コンピューター
     検出の種類: 高速パス
     検出元: リアルタイム保護
     ユーザー: Ysama-PC-2022\ysama
     プロセス名: C:\Windows\explorer.exe
     セキュリティ インテリジェンスのバージョン: AV: 1.421.275.0, AS: 1.421.275.0, NIS: 1.421.275.0
     エンジンのバージョン: AM: 1.1.24090.11, NIS: 1.1.24090.11

 

また、同様にテストプログラムもエラーが発生したので記録されていました。

 

ログの名前:   Microsoft-Windows-Windows Defender/Operational
ソース:         Microsoft-Windows-Windows Defender
日付:            
2024/11/14 8:35:41
イベント ID:  1116
タスクのカテゴリ:なし
レベル:        
警告
キーワード:      
ユーザー:      SYSTEM
コンピューター:Ysama-PC-2022
説明:

Microsoft Defender ウイルス対策 でマルウェアまたは他の望ましくない可能性のあるソフトウェアが検出されました。
 詳細については、次を参照してください:
https://go.microsoft.com/fwlink/?linkid=37020&name=Trojan:Win32/Wacatac.B!ml&threatid=2147735505&enterprise=0
     名前:
Trojan:Win32/Wacatac.B!ml
     ID: 2147735505
     重大度:
重大
     カテゴリ: トロイの木馬
     パス: file:_C:\Users\ysama\Programing\C# Programing\Samples\Games\Mahjong\TestMahjongTable01.exe
     検出元の場所: ローカル コンピューター
     検出の種類: 高速パス
     検出元: リアルタイム保護
     ユーザー: Ysama-PC-2022\ysama
     プロセス名: C:\Windows\explorer.exe
     セキュリティ インテリジェンスのバージョン: AV: 1.421.275.0, AS: 1.421.275.0, NIS: 1.421.275.0
     エンジンのバージョン: AM: 1.1.24090.11, NIS: 1.1.24090.11


なーんと、本当にトロイの木馬に感染していたのか!

 

ということで、(もう一度「除外設定」を削除し-エラーが出るようにして)再三再四「右クリック→その他のオプションを確認→Microsoft Defenderでスキャンする」や、「Windows セキュリティ」の「ウィルスと驚異の防止」にある「フォールダー毎のオフラインスキャン」やらを実行するのですが、何にも出ません。阿保みたいに、

 

「現在の脅威はありません。」

「処置は不要です。」

 

を繰り返すばかりです。

 

アーア、どうすりゃいいんだー!!!

 

と思っていたら、本日(11月15日)はMoPai.exe、TestMahjongTable01.exeのいずれのファイルを起動しても素直に立ち上がり、まったくエラーは出ません。

 

何なんだよっ!

 

とPCに毒づきましたが、仕方ないですよね?しっかし、この平穏はいつまで続くのやら?

 

ということで、なぜこうなったか、どうして警告が出なくなったのか、

 

ちょっとわかりませんでした。(アレクサ)

 

色々と寄り道をしましたが、cardtable.dllのソースコード(cardtable.cs)に続き、「神経衰弱(Concentration)」のソースコードを解説します。

 

【Concentration.cs】

//////////////////////////////////////////
// Concentration by CardTable.dll
// Copyright (c) by Ysama September, 2024
//////////////////////////////////////////
using System;
using System.Windows.Forms;
using System.Drawing;
using cardtable;                //CardTableクラスを利用する


namespace Concentration
{
    ///////////////////////////
    //エントリーポイントクラス
    ///////////////////////////

    class Memory : Form
    {
        //メンバーフィールド(変数)
        CardTable myTable;        //トランプ台
        int trials;                //試行回数
        bool ontheway;            //一枚開いた状態(解説:OnClickの一回目、二回目を判別します。)
        int firstNo;            //一枚目のカード番号
        int secondNo;            //二枚目のカード番号
        int pairs;                //対を開いた数

        [STAThread]
        public static void Main()
        {
            Application.Run(new Memory());
        }

        //コンストラクター
        public Memory()
        {
            this.Size = new Size(1012, 522);                    //調査値
            this.MinimumSize = new Size(640, 522);                //調査値
            this.MaximumSize = new Size(1012, 522);                //調査値
            this.BackColor = Color.Black;
            this.Text = "Concentration";
        //    this.SizeChanged += OnSizeChanged;                    //調査用
            myTable = new CardTable(988, 476, false);
            myTable.Location = new Point(0, 0);
            myTable.Size = new Size(ClientSize.Width, ClientSize.Height);
            myTable.Anchor = (AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right);
            myTable.Click += OnMyCardClick;
            this.Controls.Add(myTable);
            Init();
        }

        //private void OnSizeChanged(object sender, EventArgs e)//調査用
        //解説:親ウィンドウサイズを決定する為にSizeChangedイベントでウィンドウサイズを調べました。

        //{
        //    this.Text = "Width: " + Size.Width.ToString() + ", Height: " + Size.Height.ToString();
        //}


        //解説:以下はこのアプリで一番重要なマウスクリック時の処理です

        private void OnMyCardClick(object sender, EventArgs e)
        {
            //解説:左ボタンクリック時の処理です。

            if(myTable.msButton == MouseButtons.Left)
            {
                if(!ontheway)    //解説:一回目のクリックです。(trialの途中 - ontheway - の意味です。)
                {
                    firstNo = myTable.GeGridtNo(myTable.msX, myTable.msY);    //解説:一枚目のカード番号を取得
                    if(myTable.Deck[firstNo].Discarded)    return;    //解説:既に捨てられたカードの場合は何もしません。
                    //MessageBox.Show("No: " + firstNo.ToString(), "firstNo", MessageBoxButtons.OK, MessageBoxIcon.Information);    //解説:デバッグ用です。
                    if(firstNo >= 0 && firstNo < 52)    //解説:52枚のカードエリア内なら
                    {
                        myTable.TurnFace(firstNo, true);    //解説:表(true-裏false)にして
                        myTable.ShowCard(firstNo, firstNo % 13, firstNo / 13, true);    //解説:カードを表示します。

                        //解説:因みに第1引数はDeck配列の添字=カード番号、(13 x 4の)グリッドx、y座標、グリッド表示指定(true)です。
                        trials++;    //解説:試行回数を一つ増やします。
                        ontheway = !ontheway;    //解説:falseからtrueに変更します。
                    }
                }
                else    //解説:onthewayがtrue、即ち二回目です。
                {
                    secondNo = myTable.GeGridtNo(myTable.msX, myTable.msY);    //解説:二枚目のカード番号を取得。
                    if(myTable.Deck[secondNo].Discarded)    return;    //解説:捨てられたカードなら何もしません。
                    if(secondNo == firstNo)    //解説:一枚目と同じカードは選択できません。(見逃しやすいエラーです。)
                    {
                        MessageBox.Show("同じ札は選択できません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                        return;
                    }
                    //MessageBox.Show("No: " + secondNo.ToString(), "secondNo", MessageBoxButtons.OK, MessageBoxIcon.Information);    //解説:デバッグ用です。
                    if(firstNo >= 0 && firstNo < 52)    //解説:52枚のカードエリアなら
                    {
                        myTable.TurnFace(secondNo, true);    //解説:表にして
                        myTable.ShowCard(secondNo, secondNo % 13, secondNo / 13, true);    //解説:カードを表示します。
                        if(myTable.Deck[firstNo].Number == myTable.Deck[secondNo].Number)
                        {    //解説:以下は成功条件(同じ番号番号札)の処理です。
                            MessageBox.Show("同じ番号の札を開きました。", "正解!", MessageBoxButtons.OK, MessageBoxIcon.Information);
                            pairs++;    //解説:成功した数(=ペアの数)を一つ増やします。
                            this.Text = "Concentration (取得したペアの数:" + pairs.ToString() + ")";    //解説:進捗状況も表示
                            myTable.RemoveCard(firstNo, firstNo % 13, firstNo / 13, true);    //解説:1枚目を捨てます。
                            myTable.RemoveCard(secondNo, secondNo % 13, secondNo / 13, true);    //解説:2枚目も同様。
                            if(pairs == 26)    //解説:26ペア(52枚)すべて完了したならば
                            {
                                MessageBox.Show("ゲームが完了しました。あなたの試行回数は" + trials.ToString() + "でした。", "ゲーム完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
                                DialogResult res = MessageBox.Show("終了しますか?", "終了確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                                if(res == DialogResult.Yes)
                                    Close();    //解説:終了します。
                                else
                                {
                                    Init();        //再初期化(解説:ゲームは再開します。)
                                    return;
                                }
                            }
                        }
                        else
                        {    //解説:以下は失敗の場合です。
                            MessageBox.Show("残念!番号が違います。", "不正解!", MessageBoxButtons.OK, MessageBoxIcon.Information);
                            myTable.TurnFace(firstNo, false);    //解説:一番目のカードを裏にして
                            myTable.ShowCard(firstNo, firstNo % 13, firstNo / 13, true);    //解説:表示します。
                            myTable.TurnFace(secondNo, false);    //解説:二番目のカードも同様にします。
                            myTable.ShowCard(secondNo, secondNo % 13, secondNo / 13, true);
                        }
                        ontheway = !ontheway;    //解説:成功、失敗に関わらずonthewayフラグをfalseに戻します。
                    }
                }
            }
            else if(myTable.msButton == MouseButtons.Right)    //解説:右ボタンを押された場合のデバッグ用です。
            {
                MessageBox.Show("右ボタン:No = " + myTable.GeGridtNo(myTable.msX, myTable.msY).ToString(), "カード番号", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            else    //解説:中央ボタンが押された場合のデバッグ用です。
            {
                MessageBox.Show("中央ボタン:No = " + myTable.GeGridtNo(myTable.msX, myTable.msY).ToString(), "カード番号", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

        private void Init()    //解説:初期化メソッドです。
        {
            this.Text = "Concentration";    //解説:これは本来不要ですが、SizeChanged調査用に入れました。
            trials = 0;                //試行回数
            pairs = 0;                //対を開いた数
            ontheway = false;        //一枚開いた状態
            myTable.Shuffle(false);    //Jokerを使わない(解説:でシャッフルします。)
            DialogResult res = MessageBox.Show("小さいカードを使いますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);    //解説:カードサイズは大小を選べます。
            if(res == DialogResult.Yes)
                myTable.smallCards = true;
            else
                myTable.smallCards = false;
            for(int i = 0; i < 4; i++)    //解説:13枚x4行の全カード表示処理です。
            {
                for(int j = 0; j < 13; j++)
                {
                    myTable.TurnFace(i * 13 + j, false);    //解説:裏面にして
                    myTable.ShowCard(i * 13 + j, j, i, true);    //横(0 - 12) X 縦(0 - 5)のグリッド表示
                }
            }
        }
    }
}

 

cardtable.dllを使って書くと結構簡単に書けましたね。

 

実際に動かして、遊んでみましたが、結構その気にさせます。特段目新しい所はなく、「普通に神経衰弱で、普通に楽しい」()という感じです。

:カードの神経衰弱はweb上に多数無料ゲームがあり、それとこれとの機能的な違いは、...まぁ、無いです。

 

次回からは(既に先走って書いてますが)MahjongTableクラス、そのdllとアプリとしてのMoPaiを解説してゆこうと考えています。(その前にMopPaiを完成させることが先決なんですが...汗;)

 

ps. 昨日書いたこの問題、mahjongtable.dllを開発している時にテストプログラム(TestCardTable01.exe)を組みましたが、それもエラーに引っ掛かります。ということはアプリプログラムではなく、dll自体に(検知に引っ掛かるような)問題があるのかしら???大体、最初は全く反応せず、昨日から発生するようになるって言うのはどーよ?

ChatGPT様にお伺いを立てましたが、この問題のQ2、Q3に関連する情報としては矢張り

【CHatGPTの返答抜粋】

1. アンチウイルスソフトでの詳細な分析

アンチウイルスソフトウェアの検出ログを確認し、具体的にどのコードや機能が原因でPUAとして検出されているか調べます。PUAは次のような要素で検出されやすくなります:

  • 外部リソースやネットワークへのアクセス
  • 動的コード生成や実行
  • 未署名のコード

2. コードレビューとセキュリティ分析

C#のコード内で、以下のような処理が含まれていないか確認します。

  • ネットワーク通信:WebリクエストやAPIアクセス
  • ファイル操作:一部のアンチウイルスは、システムファイルやレジストリの変更操作をPUAとみなすことがあります。
  • 暗号化・難読化:アンチウイルスによっては、自己難読化コードもPUAと認識される可能性があります。

とのことで、身に覚えがないというしかないのですが???

 

【2024/11/13出稿直後に末尾追加】

まず初めに断っておきますが、私のプログラミング趣味は飽くまでスタンドアローンベースであったもので、インターネットを含むネットワークはユーザーとしてしかタッチしておらず、プログラミングはおろかセキュリティ関係には疎いです。

 

なので

 

前回「嵌った」と書いたMoPai

 

をコンパイルして実行しようとしたところ、これ↓

 

が出たのでびっくりしました。(例の「ウィルス感染詐欺」位にびっくりしましたね。私もあのピーピーギャーギャーうるさいやつに捕らわれて、再起動して回避したことが有ります。)

 

さて、いったいこれは何なのか?

 

と、また悪い癖で、横道にそれていってしまいます。(いつになったらConcentrationのソース説明に行くのやら。)

 

ご本家サイトで調べてみるとこんなこと↓のようです。

 

システム エラー コード (0 から 499)

曰く「ERROR_VIRUS_INFECTED 225 (0xE1)
ファイルにウイルスまたは望ましくない可能性のあるソフトウェアが含まれているため、操作が正常に完了しませんでした。

 

では、Q1「何が」、Q2「どういう条件で」、Q3「何を」検知したのでしょうか?(MoPaiはありふれた内容でURLも使っていませんし...)

 

MicrosoftのAI曰く「.Netで「ファイルにウィルスまたは望ましくないソフトウェア」エラーが検知されるのは、正常なプログラムがマルウェアや望ましくない可能性のあるアプリケーション(PUA)として誤って判断されたためです。これを誤検知や過検知といいます。
誤検知によってファイルが削除されたり、プロセスが終了されたり、特定のソフトウェアの動作がブロックされたりすることがあります。
誤検知を解除するには、Microsoft Windows Defender のリアルタイム保護を一時的に無効にすることができます。手順は次のとおりです。
PCの画面右下の通知領域から、Microsoft Windows Defender のアイコンをクリックする。
「ウィルスと脅威の防止」をクリックする。
「ウィルスと脅威の防止の設定」をクリックする。
「リアルタイム保護」の項目で「オン」をクリックしてオフの状態に変える。
」

 

また曰く「.Net ファイルにウイルスや望ましくないソフトウェアがないかを確認するには、Windows セキュリティの「ウイルスと脅威の防止」で除外を追加することができます。
手順
1. [スタート] を選択し、[設定] を開きます。
2. [プライベートとセキュリティ] を選択し、[ウイルスと脅威の防止] を選択します。
3. [ウイルスと脅威の防止の設定] で、[設定の管理] を選択します。
4. [除外] で、[除外の追加または削除] を選択します。
5. [除外を追加する] を選択し、ファイル、フォルダー、ファイルの種類、またはプロセスから選択します。

また、Windows セキュリティの「アプリとブラウザー コントロール」をオンにすると、望ましくない可能性のあるアプリを検知した場合に通知が表示され、対処を促されます。

 

Q1に関してはMicrosoft Windows Defender(有難う、君に恨みはないよ!)のリアルタイム保護のようです。Q2とQ3は(当たり前っちゃー、当たり前ですが)秘情報なのか何処にも描かれていませんね。

 

更に踏み込んでみると、私のような経験をする人も少なくないようです。

自作のプログラムがウイルスとして検出されてしまうのはなぜですか
 

また、この筋の専門家によれば矢張り「破る(攻める)」側と「守る」側のイタチごっっこのようですが、↓の記事はほとんど理解できませんでした。(ショック!!!)

 

 

いずれにせよ、

 

AI君のアドバイスに従い、MoPai.exeだけはDefenderのリアルタイム保護から外すようにしました。(従って、これを他の環境で実行しようとすると同じ症状が発生する可能性がありますね。ゴメンナサイ。でも、「何故引っかかるのか」は私ではわかりませんのでご容赦ください。)

 

ps. 書いた後に気が付いたのですが、cardtable.dllとmahjongtable.dllのソースにはURLをコメントに書いています。

<例>

///////////////////////////////////////////////////////////
// MahjongTable.cs
// Copyright (c) by Ysama September, 2024
// Free Images come from:
// https://www.ac-illust.com/main/search_result.php?word=
//         麻雀&search_word=麻雀牌&page=0#google_vignette
///////////////////////////////////////////////////////////

コメントはコンパイルの際に除去されるものと理解していました。事実cardtableではこんなことはありませんでした。が、可能性として考える必要があるかもしれませんね。