配列とポインタ 解答ページ | Programming Place Plus 新C++編

トップページ新C++編配列とポインタ

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

次の各定義の違いを説明してください。

  1. char[] s = "abc";
  2. const char[] s = "abc";
  3. char* s = "abc";
  4. const char* s = "abc";
  5. const char* const s = "abc";


すべて "abc" で初期化しており、定義しようとしている s の型だけがおのおの違っています。その結果、それぞれが異なる意味合いとなっています。

"abc" は、const修飾された要素数4 の char型の配列です(本編解説)。末尾に隠れた終端文字があることを忘れないでください。つまり、const char[4] という型で表現できます。この前提を踏まえて、1~5の意味を考えていきます。

まず1番は、[] を使っているので、char型の配列を定義しています。要素数の指示がないので、初期化子の内容から判定されることになります。"abc" の要素数は 4 ですから、s の要素数も 4 になります。「文字列リテラルを配列に変換している」などと考えるのは不適切です。文字列リテラルはそもそも配列ですから変換の必要など最初からありません。char[] s = "abc"; という初期化の記法は、s の各要素に与える文字を 1つずつ指定しているにすぎず、char[] s = {'a', 'b', 'c', '\0'}; と書くのと本質的に同じことです。

2番も配列を定義していますが、今度は const修飾されています。そのため、s に与えた初期値はあとから書き換えられません。1番との違いはそこだけです。なお、const修飾する場合、初期値を与えることは必須事項です(本編解説)。

3番は * を使っているので、配列ではなく、ポインタを定義しています。"abc" は配列ですが、配列はポインタに暗黙的に変換できるため、ここでもポインタ型に変換しようとします。しかし、"abc" の型は const char[4] であり、これをポインタに変換すると const char* という constポインタになります(本編解説)。一方、s の型は char* であり constポインタではありません。constポインタを、const でないポインタへ変換することはできないため(本編解説)、3番の定義はコンパイルエラーとなります。

4番は、const char* s となっており、これは constポインタを定義しようとしています。3番の解説と同じ流れになりますが、今度は constポインタのまま受け取れるため、コンパイルできます。constポインタなので、定義された s が指し示す先にあるものを書き換えることはできませんが、s そのものを書き換えることはできます(たとえば、s = "Hello"; のようにして、別のものを指し示すように変更できる)。

5番は、4番に対して const が追加されています。変数宣言時、* の手前に const が置かれた場合は constポインタを意味しますが、* の後ろ側に const が置かれる場合は、宣言しようとしている変数自体が const であることを意味します(本編解説)。今回は両方ともに const が置かれているので、constポインタでありつつ、s 自体が const だということになります。constポインタなので、4番と同じく、文字列リテラルを使って初期化できます。そして、定義された s が指し示す先にあるものも、s そのものも書き換えられません。

問題2 (基本★★)

大きさが 1バイト、2バイト、4バイト、8バイトの整数型の配列をバイト列として出力してみて、メモリ上にどのようにバイトが並ぶか確認してください。


バイト列として出力するには、対象の配列の要素を指す unsigned char型のポインタを使います。unsigned char型は必ず 1バイトなので、ポインタ演算を使って 1バイトずつアクセスできます。各型の配列から unsigned char型のポインタを得るには、reinterpret_cast を用います(本編解説)。

#include <iomanip>
#include <iostream>

void print_byte_string(const unsigned char* bytes, std::size_t size)
{
    for (std::size_t i = 0; i < size; ++i) {
        std::cout << std::setw(2) << std::setfill('0') << std::hex << std::uppercase
            << static_cast<unsigned int>(bytes[i]) << " ";
    }
    std::cout << "\n";
}

int main()
{
    char array1[] {1, 0, 100, -1};
    short array2[] {1, 0, 100, -1};
    int array3[] {1, 0, 100, -1};
    long long array4[] {1, 0, 100, -1};

    print_byte_string(reinterpret_cast<const unsigned char*>(array1), sizeof(array1));
    print_byte_string(reinterpret_cast<const unsigned char*>(array2), sizeof(array2));
    print_byte_string(reinterpret_cast<const unsigned char*>(array3), sizeof(array3));
    print_byte_string(reinterpret_cast<const unsigned char*>(array4), sizeof(array4));
}

実行結果:

01 00 64 FF
01 00 00 00 64 00 FF FF
01 00 00 00 00 00 00 00 64 00 00 00 FF FF FF FF
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 00 00 00 00 00 00 00 FF FF FF FF FF FF FF FF

1バイトの配列を char型で、2バイトの配列を short型で、4バイトの配列を int型で、8バイトの配列を long long型で作っています。char型以外は、処理系によって大きさが異なる可能性があるので、自身の環境に合わせて変更してください。

また、16進数で出力するには std::hex を、a~f を大文字で表記するには std::uppercase を、桁数を揃えるには std::setw を、足らない桁を特定の文字で埋めるには std::setfill をそれぞれ用います。これらを総称してマニピュレータと呼んでいます(本編解説)。


1バイトの配列では、要素の値がそのまま出力されているのが分かります(10進数の 100 は、16進数で 64 です)。負数がどのように表現されるについてはまだ解説していませんが、-1 は FF と表現されている様子がみえます。

2バイト、4バイト、8バイト単位の配列で同じ値を出力してみると、足らないバイトが 00 で埋められていますが、登場するバイトは変わらず、0164FF のままです。

問題は足らないバイトの埋められ方で、たとえば 2バイトの配列で 1 を出力した結果は 01 00 となっています。00 01 のほうが自然ないように思えますが、この処理系では、リトルエンディアンという方式が採用されているため、下位のバイトほど、メモリでは上位側に来るように並べられた結果です(本編解説)。ビッグエンディアンを採用している環境では、00 01 のように並びます。こうした並び順のルールは、2バイト以上を使ってデータを表現する場合に影響するため、char型の配列のときには関係のない話ですが、short、int、long long を使っているほか3つの配列では重要になります。

問題3 (応用★★)

char型の配列の内容を逆順にする関数を作成してください。


たとえば次のように作成できます。

#include <cstring>
#include <iostream>
#include <utility>

// 文字列を逆順にする
char* str_reverse(char* str)
{
    std::size_t len = std::strlen(str);
    for (std::size_t i = 0; i < len / 2; ++i) {
        std::swap(str[i], str[len - i - 1]);
    }
    return str;
}

int main()
{
    char str[] = "Hello";

    std::cout << str_reverse(str) << "\n";
}

実行結果:

olleH

関数に配列を直接渡すことはできませんが、ポインタに変換されたうえでならば渡せます(本編解説)。つまり、仮引数を char* とすればいいということになります。

文字列の先頭と末尾を入れ替えることを、文字列の半分のところまで繰り返せば、逆順になります。入れ替えには std::swap関数を使っています(「プリプロセス」の練習問題より)。

str_reverse関数の仮引数は char* なので、const な char型配列や、constポインタを渡せないことにも注意しておきましょう。配列の中身を逆順に並び替える、つまり要素を変更しようとする関数なので、const な配列をターゲットにはできません。そのため、次のコードはコンパイルエラー(Visual Studio 2015/2017 ではコンパイルできてしまうが、未定義動作)です。

    std::cout << str_reverse("Hello") << "\n";  // 文字列リテラルは const な配列なので、char* には変換できない

同じ "Hello" なのに、char型配列に入っていたら問題なくて、そのまま渡そうとするとうまくいかないというのは、正確に理解しておくべき部分です。練習問題① をよく見直してください。

str_reverse関数は、仮引数str を戻り値として返していますが、こういう手法はよく見られるものです。つまり、処理の対象にしたものを返しているわけですが、このおかげで std::cout << str_reverse(str) << "\n"; が機能します。


ところで、逆順に並び替えるという処理は、比較的ありふれたものなので、標準ライブラリにも std::reverse関数1 2というものが用意されています。

#include <algorithm>
#include <cstring>
#include <iostream>

// 文字列を逆順にする
char* str_reverse(char* str)
{
    std::reverse(str, str + std::strlen(str));
    return str;
}

int main()
{
    char str[] = "Hello";

    std::cout << str_reverse(str) << "\n";
}

実行結果:

olleH


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る