topazの遊び場

いろいろやってみる

そろそろC++やるぞパート8 配列2 ~ スタックとヒープ ~

そろそろC++やります

そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。

という感じでやっています。

※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。

内容

  • Microsoft配列をベースに、スタックの配列や、ヒープの配列について、深く見ていきます。

スタック配列

素数は、整数リテラルや定数式として、コンパイラが割り当てる領域を認識している必要があります。実行時に計算される値を使用することはできません。

// 整数リテラル
int arCount = 23;
int ar[arCount];
// 定数式
int brCount = 13 + 57;
int br[brCount];

こちらの記事や、こちらの記事、こちらの記事でも、配列のサイズは定数でなくてはならない、と書いてありました。
しかし、これらの記事で紹介されている、scanfcinを用いた外部入力による配列の要素数の決定でも、自分の環境ではエラーが出ず、動的に配列サイズを変えて配列が作れました。

// エラーが出ずに実行ができる。
int num;
scanf("%d", &num);
int a[num];

int n;
cin >> n;
int array[n];

自分のコンパイル環境はこちらです。

$ g++ --version
Apple clang version 14.0.3 (clang-1403.0.22.14.1)
Target: arm64-apple-darwin22.3.0
Thread model: posix

エラーは出ずに使えたのですが、ドキュメントにダメと書いてあるため、今後使うのは止めることとします。
配列に規定値を定めない場合は、そのメモリの位置にあるランダムな値が入ります。

int ar[3];
for (int i = 0; i < 3; i++)
{
    cout << ar[i] << endl;
}
// Output:
// -427810655
// 2620700
// 1

配列の最初の要素は0番で、最後の要素はn-1番です。

スタックベースの配列は、ヒープベースの配列(次に紹介)よりも割り当てとアクセスが高速です。ただし、スタックの要素数は制限され過剰に使用することはできません。過剰さはプログラムによって異なります。

ヒープ配列

ヒープ配列を用いるのは、

  • スタック配列に割り当てるには要素数が多すぎる。
  • コンパイル時にサイズが不明ある。

場合に使います。
使用方法としては、ポインターを定義し、new []による初期化を行います。

int *arHeap;
arHeap = new int[4];

arHeapはスタック領域におかれ、arHeap[0] ~ arHeap[3]の実態はヒープ領域におかれます。

1. ポインタ宣言時はStack領域

arHeapを宣言した時は、スタック領域にarHeapのメモリが確保されます。この時点では変数の宣言のみなので、メモリの中身は未定状態になっています。

int *arHeap;

2. new[]での配列の割り当てはHeap領域

new []を用いて、配列の割り当てを行った時に、Heap側に配列の要素のメモリ確保が行われます。そして、new []演算子は、最初の要素へのポインターを返すため、スタック上のarHeapの中身がarHeap[0]ポインター(アドレス)に置き換わります。

arHeap = new int[4];

これがメモリを動的に確保できる原理になっています。

配列を取り扱う上で気をつける点として、

  • 後からメモリの削除を行えるように、アドレスを保存する。
  • 配列にアクセスする際に、配列の範囲を越えないようにする。

ことが、あります。

一度new []してヒープ領域のメモリを確保した際にdelete[]でメモリの開放を行う必要があります。

int *arHeap;
arHeap = new int[2];
arHeap[0] = 13;
arHeap[1] = 16;
cout << hex << &arHeap[1] << endl;
cout << dec << arHeap[1] << endl;
delete[] arHeap;
cout << hex << &arHeap[1] << endl;
cout << dec << arHeap[1] << endl;
// Output:
// 0x128e068e4
// 16
// 0x128e068e4
// 0

delete[] arHeap;でメモリの開放を行っており、その後arHeap[1]にアクセスした時に、中身が0で初期化されています。

最後に

最後に、メモリの位置確認用のスクリプトを載せておきます。観察してみると、配列の要素だけ、メモリが別の場所にあることがわかると思います。

int main()
{
    int a = 100;
    cout << "a address      :";
    cout << hex << &a << endl;

    int *arHeap;
    arHeap = new int[4];
    arHeap[0] = 13;
    arHeap[1] = 16;
    arHeap[2] = 56;
    arHeap[3] = 25;

    cout << "arHeap address :";
    cout << hex << &arHeap << endl;
    cout << "arHeap value   :";
    cout << hex << arHeap << endl;

    int b = 100;
    cout << "b address      :";
    cout << hex << &b << endl;

    for (int i = 0; i < 4; i++)
    {
        cout << "==== arHeap[" << i << "] ====" << endl;
        cout << dec << arHeap[i] << endl;
        cout << hex << &arHeap[i] << endl;
    }
    return 0;
}
a address      :0x16b99edc8
arHeap address :0x16b99edc0
arHeap value   :0x145e068e0
b address      :0x16b99edbc
==== arHeap[0] ====
13
0x145e068e0
==== arHeap[1] ====
16
0x145e068e4
==== arHeap[2] ====
56
0x145e068e8
==== arHeap[3] ====
25
0x145e068ec

参考リンク

感想

スタックの配列からヒープの配列まで行いました。同じ配列という枠組みではありますが、メモリの部分までみると、全く別のことをしていて、スタックの配列とヒープの配列は別物だ、という印象を持ちました。
素数に制約があるけど、高速でアクセスできるスタックか、自由に可変的に使えるヒープか、使い分ける必要がありそうとも思いました。

次回は、配列の初期化や、多次元配列を行います。