第77章 リテラル文字列

 前回のプログラム、cout の部分を switch 文で分岐して書きましたが、何か無駄に見えますよね。今回はそういうときに便利な文字列の配列と、文字列での初期化についての話を行います。


 では、今回の要点です。


 では、いってみましょう。


 さて、前回のプログラムで switch 文で分岐した cout がありました。しかし、処理は全て同じなのですから、ここを何とか1つの cout ですむようにはできないものでしょうか?

 そこで考えられる方法といえば、「表示する文字列を配列にする」というのがあるでしょう。たとえば result という文字列の配列があるとすると、switch 文の所は

cout << result[Compare(a, b)] << endl;

ですむわけですね。(もちろん、Compare が ECompare として正しい値を返すように保証しなければいけません!)

 しかし、このような文字列の配列などはどのようにすればいいのでしょうか?


 先ず、文字列は単なる文字コードの配列なので、文字列の配列は「配列の配列」つまり第61章でやった「2次元配列」で実現できそうです。ではやってみましょう。

char result[][] = {
    "前者は後者より小さいです。",
    "両者は等しいです。",
    "前者は後者より大きいです。",
};

 と、ちょっと待って下さい。第62章で「2番目以降の要素数を省略できない」と言いました。ということは、これはエラーになるはずです。コンパイルしてみると...やっぱりエラーになりますね。

error C2087: '<Unknown>' : 添字が不正です。
error C2117: '前者は後者より小さいです。' : 指定された配列には、初期化子が多すぎます。
error C2117: '両者は等しいです。' : 指定された配列には、初期化子が多すぎます。
error C2117: '前者は後者より大きいです。' : 指定された配列には、初期化子が多すぎます。

 パッと見よく分からないエラーですが、まぁ、これが2つとも省略したときのエラーです。エラーは機械的に判定するだけなので、よく分からないエラーを出すことがよくあります。なので、エラーの表面的な意味よりも、実際どういうときにどういうエラーが出るのかを覚えておいた方がいいでしょう

 となると、いちいち文字列の大きさを確かめて、配列の2番目の要素数を指定しなければならないのです。これはとても面倒ですね。ということで、この案は没です。(明らかに2番目の要素数を決められるような場合など、これはこれで利用価値のあることもあるんですけどね。)


 次の案です。第72章で言った通り、配列へのポインタは単にその要素の型へのポインタで構いません。ということで、文字列へのポインタの配列を作ってみてはどうでしょうか?

 ポインタはただの変数です。ただの変数の配列ということは [ ] は1つで、となるとここに大きさを指定しなくて構わなくなります。イメージとしては、

char* result[] = {
    "前者は後者より小さいです。",
    "両者は等しいです。",
    "前者は後者より大きいです。",
};

という感じです。しかし、文字列のアドレスで初期化する必要がありますが、これは面倒ではないのでしょうか?

 実はそんなことはありません。とても簡単に初期化することができます。何と、上のイメージ通りで何の問題もないのです!

 ということで、Enum1.cpp は次のように無事簡単になりました。めでたしめでたし。

プログラム実行結果例
// Enum1.cpp
#include <iostream.h>

enum ECompare
{
    LESSTHAN    = 0,
    EQUALTO     = 1,
    GREATERTHAN = 2,
};

ECompare Compare(int a, int b)
{
    return (a < b) ? LESSTHAN :
           (a > b) ? GREATERTHAN : EQUALTO;
}

bool Compare()
{
    const char* pszResult[] = {
        "前者は後者より小さいです。",
        "両者は等しいです。",
        "前者は後者より大きいです。",
    };

    int a, b;

    cout << "数字を2つ入力して下さい > " << flush;
    cin >> a >> b;

    if(a == -1)
        return false;

    cout << pszResult[Compare(a, b)] << endl;

    return true;
}

int main()
{
    while(Compare());

    return 0;
}
数字を2つ入力して下さい > 0 1
前者は後者より小さいです。
数字を2つ入力して下さい > 1 1
両者は等しいです。
数字を2つ入力して下さい > 1 0
前者は後者より大きいです。
数字を2つ入力して下さい > -1 0

 しかし、なぜこれで初期化できるのでしょうか? 「初期化できるようになっているからできる」でも構わないのですが、いつまでもそんな理解では困ります。ということで、この章ではその秘密に迫ってみます。

 pszResult は配列でした。しかし、別に配列でなくてもこの初期化は可能です。

const char* str = "文字列どす";

 そして、これは別に初期化でなくても可能です。

const char* str;
str = "文字列どす";

 これを見れば分かると思いますが、この " " は何やらアドレスを返すみたいです。

 この " " で囲まれた文字列のことはリテラル文字列と呼ばれます。これは静的なデータです。メモリ上の静的な領域のどこかにこの文字列が置かれます。

 そして、これをポインタに代入する式は、ポインタにそのアドレスを渡す式になるのです。よく考えてみれば、文字列を受ける関数

例)void Func(const char* str);

に " " で囲まれた文字列を渡したことが何度もあると思います。その時の str の値は何なのか考えたことはあるでしょうか? まさにそれがリテラル文字列へのアドレスなのです。

 このように、ポインタにリテラル文字列を代入すると、ポインタにはそのリテラル文字列へのアドレスが代入されるのです。

 ここで注意することは、リテラル文字列へのアドレスが入っているので、str[0] = 0; などをするとリテラル文字列を直接書き換えることになるということです。環境によるのですが、リテラル文字列を書き換えようとするとエラーになることが多いです。昔の環境ではエラーにならないこともありますが、そういう場合でもリテラル文字列を書き換えてはいけません

 なので、リテラル文字列を受けるポインタには const を必ず付けましょう

 また、リテラル文字列は静的なデータなので、寿命はプログラムが始まってから終わるまでです。例えば

return "文字列を返しちゃうよ";

としても、何の問題もありません。

 また、上の pszResult も関数に入る毎に毎回初期化するのは無駄なので、

static const char* result[] = {
    "前者は後者より小さいです。",
    "両者は等しいです。",
    "前者は後者より大きいです。",
};

と、静的にしてしまうといいでしょう。


 と、リテラル文字列での初期化の話をしました。では、配列をリテラル文字列で初期化したとき、例えば

char str[] = "This is string.";

としたとき、str はリテラル文字列を直接指しているのでしょうか? 答はノーです。

 str という配列は "This is string." という配列で初期化されるだけです。つまりは、

char str[] = {
    'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 's', 't', 'r', 'i', 'n', 'g', '.', 0,
};

と同じ事になるわけです。

 str は普通の配列であり、書き換えても問題ありません。str が自動変数なら str の寿命はそのブロック内に限られ、そのブロックを抜けると値は保証されません。なので、自動変数 str を関数から返すということは絶対にしてはなりません。

 このことは、初期化というのは普通の式とはちょっと違うのだということを表しています。初期化と代入式との違いというのは他にもあります。初期化と代入式は厳密には区別されるものなのです。

 第67章で言ったように「自動変数と静的変数では随分アドレスに差がある」ので、これらのことを自分で確かめてみるといいでしょう。


 それでは、今回の要点です。


 2番目の要点と4番目の要点が重なるようですが、これは初期化と代入は違うということを意識した結果です。くどいかもしれませんが、講座で間違ったことを書くわけにもいきませんからね。(既にいろいろ間違えてることもあるかも知れませんが(汗)。)


 もう第1部も終わりに近づいてきました。キリがいいので80章までやる予定なのですが、凝った話は第3部にまわすつもりなので、もうやることがなくなってきました(笑)。ピンチです。

 残り3章がかなりどうでもいい話に感じたら、そういういきさつがあるものだと思って見逃して下さい。それでは、次回まで。


第76章 列挙子 | 第78章 逃げの一手

Last update was done on 2000.7.31

この講座の著作権はロベールが保有しています