ポインタの解説が続きます。今回はポインタを使っての文字列操作です。意外と使う機会が多いのと、うっかりすると間違った使い方やメモリ破壊バグを生みかねない部分もあります。今回はそういうダメな例も挙げながら解説していきます。
--
いきなりここに飛んで来ちゃった人は、よろしければ下記からご覧ください。


  • 文字列が格納されているアドレス
以前、配列でこのような初期化方法を説明しました。

char str[] = "ABC";

この結果、str 配列は要素数 4 となりそれぞれ…

str[0] = 'A'; str[1] = 'B'; str[2] = 'C'; str[3] = 0;
このような内容で初期化されてました。では、そもそもその初期化に使った "ABC" という文字列はどこにあるのでしょうか。初期化する文字列ですので、メモリのどこかに格納されているはずです。そこで、こんな事をしてみます。

char* pStr = "ABC";
これ、昔の C言語だとコンパイルが通ったのですが、VS2022 だとエラーが出てコンパイルが通りません。
定数の文字列ポインタは普通のポインタ変数に代入できません。
"ABC" は const char[4] (定数)だから、それを使ってポインタを初期化してはダメだというエラーです。だから内容の書き換えをしないという保証がある const char* (定数ポインタ)ならアドレスを受け取れますね。では、定数として値を受け取って、状態を確認してみます。

const char* pStr = "ABC"; printf("%p : %s\n", pStr, pStr);
アドレスと文字列が表示された
無事、アドレスと文字列が表示されました。では、少し戻って先程の str 配列を pStr に入れて同じように確認してみます。

char str[] = "ABC"; char* pStr = str; printf("%p : %s\n", pStr, pStr);
今度は const がなくてもエラーが出ませんでした。ポインタが指し示す先が変数たる配列であれば、アドレスの受け取りは許容されますが、定数の場合は const を付けて中身の改変禁止を保証しなければいけないのです。

では、ここでちょっと危ないことをしてみます。

for (int i = 0; i < 2; ++i) { char* pStr = (char*)"ABC"; printf("%s\n", pStr); pStr[2] = 'A'; printf("%s\n\n", pStr); }
キャストをすれば、定数アドレスを通常のポインタに入れることが出来ます。そして、pStr のポインタが指し示す場所の C を A に置き換えて、同じ二度ループをしてみます。すると…
書き込みアクセス違反発生
VS2022 はちゃんと定数文字列の書き換えを検知して、処理を停止してしまいました。実はこれ、そのまま書き換えてしまうことのほうが私の経験では多かったのです😓 エラーが出るのはなんとお優しい事か…。このまま定数値を書き換えて二度目のループでは、ほら、初期値が変わったでしょと説明しようと思ってました。

まあ、VS2022 が禁止してしまうように、定数値の書き換えは大変危険な行為です。その後の動作が保証できなくなってしまいます。
※ なんて尻窄みな説明なんだ。おのれVS2022…

なお、Release ビルドにするとチェックが外れるので動作はしますが、以前と異なり C が A に置き換わることはありませんでした。
定数の書き換えはリリースビルドでも禁止される
安全になっているんですね、今のコンパイラは…
※ モニタアーム、どうせ買うなら最初から最高のものを選びたいです。激安品と価格差は数倍もありますが、少なくともこの製品なら後悔はしません。

  • 文字列配列
文字列を配列で扱う際は、ポインタ配列を使うのが一般的です。これは文字列定数の場所を示す配列になるため、メモリ消費のムダが省けるためです。char 配列で管理すると、最も長い文章サイズでメモリ確保する必要があるので、場合によっては大変無駄になったのですが、

const char* msg[] = { "空母", "戦艦", "巡洋艦1", "巡洋艦2", "駆逐艦" };

このようなポインタ配列で文字列を管理した場合は、msg にはポインタアドレスだけが格納されるため、メモリ使用効率が上がります。
※ 今回のサンプルのような短い文字列の場合はメモリ消費が大きくなる事があります。

この msg 配列から文字を表示するのは簡単で

printf("%s\n", msg[4]);

このように記述すれば、そのまま画面に駆逐艦と表示されます。配列の範囲を超えた添字を指定すると想定外の動作となります。Debug ビルドだと例外が発生し、Release ビルドだと (null) と表示されます。
配列範囲外なので表示がnullになった
さて、固定文字列を扱うにはポインタが便利と書きましたが、では…

I'm NAITO. Nice to meet you.

このように、文章内に scanf_s で入力させた自分の名前(この例では NAITO )を表示させる、文字列の結合はどうすれば良いでしょうか。方法はいくつかあります。

const char* msg0 = "I'm "; const char* msg1 = ". Nice to meet you."; char name[20]; scanf_s("%s", name, (int)sizeof(name)); printf("%s%s%s\n", msg0, name, msg1);

名前を入力する前後に分離したメッセージを用意して、結合しながら表示する方法です。こういう事もできるという例で紹介しましたが、この方法は保守性が低くなるのでオススメできません。

次の方法は本当に文字列を結合させるやり方です。この文字列操作は C言語では便利な関数がいくつも用意されています。文字列操作関数の多くは #include <string.h> とすることで使用可能となります。…が、実は私のオススメは sprintf_s です。これは画面の表示の代わりに、結果を配列に格納するという優れものです。
※ scanf_s は省きます。

char name[20], msg[256]; sprintf_s(msg, sizeof(msg), "I'm %s. Nice to meet you.", name); printf("%s\n", msg);

msg 配列に最終的に表示する文字列が出来上がります。とても便利なので少なくとも存在は覚えておくと良いでしょう。ちなみに昔はこんなワザが使えました。

const char* msg = "I'm %s. Nice to meet you."; char name[20]; printf("%s\n", msg, name);

%s で文字列が msg に置き換えられるのですが、ポインタはその差し替えられたメッセージを追いかけて %s を見つけて、次の引数である name を表示するという処理です。便利だったんですが、禁止されてしまいました。まあ、システムメモリの書き換えに繋がるので禁止やむなしとは思っています。今はとことん防止されていますねぇ…。

もう少し簡単に文字列結合をする場合はこんなのは如何でしょうか。

const char* msg = "I'm \0. Nice to meet you."; char name[20]; printf("%s%s\n", name, msg + printf("%s", msg) + 1);
printf の中に printf が入っていて理解しづらいのであれば、分解すると下記のような流れとなります。

int len = printf("%s", msg); printf("%s%s\n", name, msg + len + 1);

msg を2つに分解せず1つの文字列として、名前を入れたい場所に \0 を入れてあるのがミソです。最初に msg を表示すると \0 まで画面に表示します。これで I'm と画面に表示されます。printf の返却値は表示した文字数なので、len は \0 の位置が入ります。その後、name を表示してから、続いて最初に表示した文字数 + 1 だけポインタを進めた位置から再表示します。すると、Nice to meet you. が表示されるというわけです。

printf の中に printf が入っている状態だと可読性が落ちる場合は、

/// <summary> /// 文字列の中に文字列を入れて表示する /// </summary> /// <param name="msg">元となるメッセージ</param> /// <param name="insert">差し込むメッセージ</param> void PrintInsText(const char* msg, const char* insert) { printf("%s%s\n", insert, msg + printf("%s", msg) + 1); }

このように関数化してしまえば良いと思います。関数の引数に const を指定しているのは、この関数内では値の書き換えは行ないませんという宣言になります。呼び出す側もこれで安心です。
※ めっちゃスタイリッシュ!これを使ったらそりゃもう気分は最高です!

  • 【VS2022】Debug ビルドと Release ビルド
デバッグを行う際の実行開始ボタンの左側に Debug と書いてあるドロップダウンがあります。
Debugビルド状態
このドロップダウンを展開すると
ドロップダウンリストを展開した
いつもの Debug の他に Release という選択肢があるのが見えるかと思います。この違い、なんだか分かりますでしょうか。Debug とは文字通り開発中にデバッグを行うためのモードです。ステップトレースはこの Debug じゃないと正しく行われません。また、本来なら落ちるようなバグが出たとしても、Debug ではきちんと停止して、その問題解決のための表示を行ってくれます。

但し、この Debug 状態は常に VS2022 の管理下で動かすため、本来の実行速度は出ていません(らしい)。また、Debug で作られた exe は、公開してはいけないと規約に記載されています。そのため、最終的には最低でも Release モードでビルドした exe で動作確認が必要です。

以前はたまにあったのですが、Debug ビルドだと正常動作して、Release だと落ちるというバグが出たりします。これは Debug ビルドでは、メモリの初期化を必要以上に行っているためです。Release ビルドでは最小限しか初期化は行われないので、初期のメモリ状態が変化して実行結果が異なってしまうのです。

なので、時々は Release ビルドで動作確認したほうが良いですよーというお話でした。
Releaseビルド状態
※ VESAに準拠していればエルゴトロンに限らず使える(はず)のクイックリリースブラケット。裏側の配線とか取り付けとかかなり面倒ですが、これがあればワンタッチで脱着可能。年に数回しか使わないと思いますが、この便利さを体験すると手放せません!