久々にC++でプログラムを書きたくなったので始めたCalc。
プログラムイメージは直ぐにできたのですが、直ぐに古希を迎えた「脳体力」の限界を感じ始めます。
一応は書いてみたのですが、プログラムイメージにしっくりこない。「あーでもない、こーでもない」と試行錯誤を繰り返して、そうこうするうちに一応の形を見ますが、同時に
(1)演算式の文字列にU+0001Hという制御コードが混入し、
(2)文字列の最終にヌル終端(L'\0'-U+0000H)を置いていたのに、それが知らないうちに空白(U+0020H)に変わるという、
を経験します。自分でも解析し、プログラムコードに致命的欠陥がないので、Chat-GPT様にも相談しますが、原因が分からないまま、一応の対処療法を施しました。
あー、一安心
と思ったのもつかの間、(これは疑いもなく私のミスですが)「整数化できない入力ミス」によってスタックオーバーフローが生じ、プログラムが落ちるというバグを発見したことから、「ユーザー入力ミス対応が脆弱」であることが判明しました。しかし、これが
怪我の功名
でバグフィックス中に「newでメモリーに確保した文字列(wchar_t配列)のデータが知らない間に書き換えられる瞬間」を確認することが出来ました。
↑の出力画像の上の赤枠は括弧内の演算式文字列「'-' (45),' ' (32), '9' (57),'' (0)」(即ち"... - 9")で、"38035334"のメモリーアドレスにあります。この後括弧内の演算式(紫枠)の評価処理があり、戻ってきて'-'を読んだ後が"38035336"(wchar_tは1文字2バイト)のメモリーアドレスにある下の赤枠の文字列になりますが、「' ' (32), '9' (57), '' (1),'' (0)」とあるのがお判りでしょうか?
これはキャラクターコンタミだと思っていたU+0001Hコードであり、ここでハタと「キャラクターコンタミ」ではなく、
L'\0'がL' '(空白)に変わったように、L'\0'がL'\1'(と続くL'\0')に書き換えられた
のだ、ということが判りました。要すれば全ての奇怪現象の原因は知らない間のメモリー書き換えという
真実はひとつ
だった、と考えられ、且つ考えること合理性があるということです。
では、それはどこで発生したのか?と考えた時にそれは「クラス内クラスで定義したCPOSクラスの仕業」に違いない!
////////////////////////////////
//クラス内クラス Part Of String
//st - ed 間の文字列を保有し返す
////////////////////////////////
class CPOS
{
private:
wchar_t* m_partstr = nullptr; //解説:切り取り文字列用ポインターです。(初期値はnullptr-ゼロ)
public:
CPOS() { //解説:コンストラクターです。
m_partstr = new wchar_t[1]; //解説:1文字(2バイト)の配列を作成(メモリーを確保)
*m_partstr = L'\0'; //解説:そこにヌル終端を置く。
}
CPOS(wchar_t* st, wchar_t* ed) { //解説:引数付きコンストラクターの場合はSet関数を呼ぶ。
Set(st, ed);
}
~CPOS() { //解説:デストラクターです。
delete [] m_partstr; //解説:newで確保したメモリーはdeleteで確実に開放する。
}
void Set(wchar_t* st, wchar_t* ed) { //解説:文字列の切り取り処理です。
if(!m_partstr) //解説:nullptrであれば、
delete [] m_partstr; //解説:既成の文字列を開放します。
if(ed >= st) { //引数順序st (<=) ed(解説:使用はstが開始文字、edが終了文字です)
int len = ed - st + 1; //解説:文字列の長さです。
m_partstr = new wchar_t[len + 1]; //解説:+1しているのはヌル終端分が必要だからです。
wcsncpy(m_partstr, st, len); //解説:開始文字から終了文字までコピーし、
m_partstr[len] = L'\0'; //解説:最後にヌル終端を入れます。
}
else { //引数順序間違い対応
//解説:エラー対応としてstとedを間違えても大丈夫なように入れ替えています。)
int len = st - ed + 1;
m_partstr = new wchar_t[len + 1];
wcsncpy(m_partstr, ed, len);
m_partstr[len] = L'\0';
}
}
wchar_t* Get() { //解説:切り取った文字列を取得します。
return m_partstr;
}
};
見た限りにおいて全く問題が無いように見えます
が、
試しに、このプログラムのnewする長さを「+1(2バイト)」増やし、ヌルを追加するようにした所、総ての
奇怪現象
が消えました。矢張り元々はm_formulaという配列をnewで確保し、そこから都度CPOSインスタンスで文字列を一部newでコピー文字列を作成して解釈処理を行っていましたが、このローカル変数用メモリー達が最後の所でオーバーラップ(上書き)していたということです。
この「ヌル二重化対応」で行こうと思いましたが、Chat-GPT様が「手動でポインタを操作してメモリを管理する方法から、標準ライブラリ(例: std::wstringやstd::string)への移行を検討してください。これにより、ポインタの解放忘れやオーバーランといったメモリ関連のバグを大幅に削減できます。」というので完全に書き換えました。
////////////////////////////////
//クラス内クラス Part Of String
//st - ed 間の文字列を保有し返す
////////////////////////////////
class CPOS
{
private:
wstring m_partstr; //解説:可変長のUnicocde(ワイド文字)用文字列クラス
public:
CPOS() : m_partstr(L"") {}
CPOS(const wchar_t* st, const wchar_t* ed) {
Set(st, ed);
}
void Set(const wchar_t* st, const wchar_t* ed) {
if(ed >= st) //引数順序st (<=) ed
m_partstr = wstring(st, ed + 1);
else //引数順序間違い対応
m_partstr = wstring(ed, st + 1);
}
const wchar_t* Get() const {
return m_partstr.c_str();
}
};
矢張りChat-GPT様の言われる通り、wstringクラスを使うようになってメモリーオーバーラップ現象は消失しました。
イヤー、疲れた(><)
というのが本音ですが、当初の目的は達成したといえます。
ps. 一応「お詫び」で紹介したバグは全てフィクスしました。序にWindows版のテストプログラムも作りました。
こいつのよい所は、(医療費控除などで行う)卓上計算機が出来ない一部のキーミスを修正して長い加減算を再実行できることです。
何れ紹介しましょう。


