前回自分のコードを見て少しがっかりした気持ちを書きましたが、大分時間がかかっているのも事実です。これには
訳があるのです。
といっても、最大の理由は、
自分自身の劣化
による「頭の回転」「記憶力の低下」「(特にコンソールプログラミングとユニコードに関わる)知見の乏しさ」等々の忸怩たる現実にあることは確かなんですが。とはいえ、
何とか目鼻がついた
ようなので、私の嵌ったドツボの紹介を兼ねて現状を報告しましょう。
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 Of String)を作り、スタートポインターとエンドポインターで切り出すことにしましたが、その際に使った関数が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;
}
まぁ、この他にも色々と「私の恥ずかしい経験」があるのですが、未だ「完全体のセル」ではないので、まだデバッグ分析、テスト試行等修行を重ねてゆきたいと思っています。
まぁ、頭を使うので「ボケ防止」(のおもちゃにする)にはもってこいです。(笑)













