2014年5月5日月曜日

C/C++ データの扱い その1

何度かコンピュータの基本はCPU+メモリ+I/Oという話をしましたが、前回までの「処理の流れ」がCPUの部分で、今回のデータがメモリの部分と言うことになります。

データの保存場所とスコープと種類


データを扱う時には以下の4つを考える必要があります。
  • 保存場所
  • スコープ
  • 初期化と終了処理
  • 種類(型、タイプ)

保存場所


保存場所には、大きく動的なデータと静的なデータがあり、更に動的なデータにはレジスタ、スタック、ヒープがあり、静的なデータには、ゼロで初期化されたデータ、ゼロ以外で初期化されたデータ、読み取り専用データの種類があります。
  • レジスタ(動的)
  • スタック(動的)
  • ヒープ(動的)
  • ゼロで初期化されるデータ(静的)
  • ゼロ以外で初期化されるデータ(静的)
  • 読み取り専用のデータ(静的)
静的というのはコンパイル時に作られるデータで、プログラムが実行されてから終了するまで、メモリに存在するデータです。動的というのはプログラム実行中にメモリに作ったり削除したりできるデータです。

一般的に静的なデータの方が速度が早く、動的なデータの方がメモリを割り当てたり解放したりする必要があるため、速度が遅いです。しかし、メモリを常に使っていることになるのでメモリ使用の効率が良くないです。

ゼロ以外で初期化されるデータはコンパイルしてできた実行ファイルに含まれ、プログラムが実行時にと実行ファイルからメモリに読み込まれます。ゼロで初期化されるデータは実行ファイルに含まれず、実行時にメモリ領域が作成され、ゼロに初期化されます。

読み取り専用のデータは、ゼロ以外で初期化されるデータとほぼ同じです(OSのセキュリティ機構で書き込みが出来ないよう保護することが可能です)。

動的なデータとしては、速度は一般的にレジスタ > スタック > ヒープ となっています。 その代わり速いほど大きなデータは扱えません。

レジスタはCPU上にある記憶領域で、通常のメモリとは異なります。大抵はメモリ上のデータをCPU上のレジスタにデータを読み込んでから計算します。レジスタはCPUによって数が決まっており少量です。

スタックは、プロセスやスレッド毎に作成されるメモリ領域です。そのサイズはOSやコンパイラによって違います。スタックに複数のデータが追加されたり、削除されたりしますが、順番が決まっています(最後に追加されたデータから削除しないといけない)。また、スタックのサイズは一度プロセスなどを作成してから変更できず、そのサイズを越えて追加が行われるとメモリ違反エラーになるなどの問題が起きます。

このスタックには通常のデータだけでなく、関数の引数やリターンアドレス等のデータも一緒に含まれます。

ヒープはOS等に必要なメモリサイズを要求して、取得されるメモリです。大きいサイズが取得でき、またサイズを変えることができるなど自由度が高いです。大きすぎる場合には取得時にOSがエラーを返すので、エラー処理を記述する必要があることや、解放が必要なため、メモリーリークが起きやすいなど、扱いが難しい部分もあります。


スコープ


保存場所がコンピュータの仕組み的な区別なのに対して、スコープはプログラミング手法での扱い(読み取りや書き込みのアクセス手段)の区別です。例えば以下があります。
  • グローバル(どこからでもアクセス可能)
  • 同一ファイル内からのみアクセス可能
  • 関数内からのみアクセス可能
  • 同じ名前空間内からのみアクセス可能(C++のみ)
  • 同じクラスからのみアクセス可能(C++のみ)
データのスコープが広い(色々な場所からアクセス可能)であるほど、使い勝手はいいです。しかし、その分プログラムの問題が起きやすく、また問題が起きた時に原因を見つけるのが大変です。そのため、少しでもデータのスコープを狭くすることが重要です。


初期化と終了処理



C/C++言語ではデータに値が入っているとは限りません。これはどんな値が入っているかわからないという意味で、不定値と言います。

C/C++言語では初期化を行う必要があるのですが、暗黙的に初期化が行われたり、明示的に初期化が行われたり、その書き方が様々です。

他の言語では不定値を持つことは少ないのですが、C/C++言語は速度を犠牲にしないために、初期化のタイミングを自由にできるようにするために不定値を持ちます。しかし、不定値は原因が特定しにくい不具合を生みやすいです。

終了処理も場合によっては行う必要があり、行うのを忘れると不要になったメモリがずっと使われている状態(メモリリークと言う)の問題を起こすことがあります。


種類(型、タイプ)



C/C++言語は、データの種類であるデータ型を意識してプログラムする必要があります。プログラミング言語によっては、型を意識することが少ない言語もあるのですが、C/C++言語は強い型付けがある言語です(C言語よりC++言語の方が強い)。

型は、例えば整数なのか、画像なのか、どのようなデータを持っているのかを表すという目的と、データで行われる処理が正しいかをチェックする目的があります。

型により適切なデータ量のメモリを使用することになります。

それと、例えば、整数と画像を足し算すると言うような処理は異常ですが、そのような間違えた処理が行われた場合に、静的に(コンパイル時に)チェックを行い、間違いを知らせることができます。


色々なデータの宣言方法



C/C++言語では、型を宣言することで、データを使用することが出来るようになります。その宣言方法で、「保存場所/スコープ/初期化と終了」が変わってきます。

以下にその宣言方法の例と、「保存場所/スコープ/初期化と終了」について記述します。型については数が多いので、一般的な整数型であるint型を使用して、その他の型については後述とします。

int              dataA;
int              dataB = 1;
static int       dataC;
const int        dataD = 2;
const static int dataE = 3;

void function()
{
        int              dataF;
        int              dataG = 4;
        static int       dataH;
        const int        dataI = 5;
        const static int dataJ = 6;
        registor int     dataK;
        auto     int     dataL;

        dataG += 1;

        {
                int dataM;
        }

        int dataN;

        for(int dataO = 0; dataO < 10; dataO++) {


        }

        int *dataP = (int i*) malloc(sizeof(int));
        int *dataQ = new int;

        free(dataP);
        delete dataQ;
}


dataAからdataQまで値の定義をしていますが、これらは「保存場所/スコープ/初期化と終了」が以下のようになります。

保存場所スコープ初期化と終了備考
dataA静的グローバル0で初期化
dataB静的グローバル指定した値(1)で初期化
dataC静的同一ファイル内0で初期化初期値指定も可。C++ではstaticでなく名前空間(namespace)を奨励です。
dataD静的(読み取り専用)グローバル指定した値(2)で初期化C++では初期値指定必須。
dataE静的(読み取り専用)同一ファイル内指定した値(3)で初期化C++では初期値指定必須。C++ではstaticでなく名前空間(namespace)を奨励です。
dataFレジスタまたはスタック同一関数内不定値
dataGレジスタまたはスタック同一関数内指定した値(4)で初期化
dataH静的同一関数内0で初期化
dataI静的(読み取り専用)またはスタック同一関数内指定した値(5)で初期化C++では初期値指定必須
dataJ静的
(読み取り専用)
同一関数内指定した値(6)で初期化C++では初期値指定必須
dataKレジスタまたはスタック同一関数内不定値初期値指定も可。レジスタ優先だが、あまり意味はないので通常registorは使用しない。
dataLレジスタまたはスタック同一関数内不定値初期値指定も可。あまり意味はなく、通常autoは使用しない。C++11以降はautoは別の意味で使用される。
dataMレジスタまたはスタック}で閉じるまで不定値その他、初期値指定、static指定など関数初めの宣言と同じ指定も可。
dataNレジスタまたはスタック同一関数内不定値C95以前は、関数や演算以降は宣言不可。C99以降、C++ではその他、初期化、static指定など関数初めの宣言と同じ指定も可。
dataOレジスタまたはスタックforの{}内指定した値(0)で初期化C95以前は、for内で宣言不可
*dataPヒープdataPと同じ不定値。free()関数で終了するまでメモリを使用する。
*dataQヒープdataQと同じコンストラクタで初期化。deleteで終了するメモリを使用する。C++のみで可能で、C言語では不可
dataPレジスタまたはスタック同一関数内指定した値(maloc()関数の返り値)で初期化データの先頭アドレスが保存される。ここではdataMのように指定したが、グローバルにすることもstaticやconstにすることも可能。
dataQレジスタまたはスタック同一関数内指定した値(newの返り値)で初期化データのアドレスが保存される。ここではdataMのように指定したが、グローバルにすることもstaticやconstにすることも可能


  • 静的なデータは初期化されると考えてください。
  • constは定数の意味ですが、C/C++言語では読み取り専用と同等の意味で考えて良いです。
  • レジスタかスタックかは通常はコンパイラが決めるため、指定の必要はないです。
  • ヒープに関してはポインタの知識が必要ですが、ポインタについての説明は後日予定です。
  • ヒープ以外はスコープから抜けるとメモリが解放されますが、ヒープは明示的にメモリを解放する必要があります。
  • for分で使っている10などのリテラルの値は静的(読み取り専用)となります。
  • 同一ファイル内のスコープとするためにstaticが使われますが、意味が不明(staticは静的の意味)のため、C++では同様の目的で名前空間を使用するのが奨励です。名前空間についての説明もまた後日予定です。
  • C言語のバージョン C95以前には、型宣言の場所が先頭のみとなっており、関数や演算の後には型宣言が出来ません。dataMのように{}内であれば、その先頭で型宣言が可能です。



0 件のコメント:

コメントを投稿