Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

C言語で2次元配列(文字列)の動的確保

スポンサードリンク

f:id:fa11enprince:20200522081020j:plain

C++版はこちら
C++で2次元配列(文字列)の動的確保 - Gobble up pudding

文字列(char型)を扱っているときに、2次元配列で動的確保したい時があります。
int型など文字でないものは1次元配列で代用できるのですが、
例えば、住所など複数レコード格納したいといった場合、利用します。
下記のようなデータの行数・1行あたりの文字数を調べてから確保したいなんていった時に使うテクニックが2次元配列の動的確保です(今回のサンプルでは行数と1行あたりの文字数を調べるところは書きません)。

北海道でっかいどう1-2-3
東京都ほげほげ1-2-3
神奈川県あふあふ2-3-4
……
沖縄県ちんすこう9-9-9

といったようなのを可変長レコードで管理したいといったときに2次元配列の動的確保が
必要になります。

C言語で2次元配列を動的に割り当てる4つの方法 - FLYING
を参考に3つパターンを示します(int型の2次元配列動的確保は上記URLのほうが詳しいです。)

gccで動作確認しています。
VC++の場合は先頭に

#define _CRT_SECURE_NO_WARNINGS

を書けばコンパイルが通ります(sprintfのかわりにsprintf_sというマイクロソフト独自の関数を使えっておこられるあれを無視するやつです)。
ちなみに、Visual Studioですと拡張子を.cにしてやると自動的にCコンパイラが選ばれるようになっています。
C++でコンパイルしようとするとmallocの戻り値をキャストしてくださいと怒られる可能性があります。

どんな行列でもいけちゃう方法

プログラムを楽に書く都合上、
SIZEYとSIZEXのサイズを固定でdefineしていますが、
本来は動的に与えられるものと考えてください。
ただし、最後の例だけはSIZEXは固定になります。

char arr[64][8]の配列を作る。つまり1行あたり8文字の64行確保です。
よく参考書にのっているやり方でchar arr[y][x]の領域を確保します。
不連続な領域になってしまうので実用性はあるのか謎です。
ガタガタな構造でメモリ節約なんてことをやろうと思うとこの方法しかありません。
ただガタガタ構造を作るためには1行1行文字数をバラバラに指定してmallocを呼ばなきゃなりません。
ちなみに8文字というのはNULL文字含めて8文字なことに注意してください(実際には7文字)。

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define SIZEY 64   /* 行数 */
#define SIZEX 8    /* 1行あたりの文字数 */

int main() {
    char** arr;
    int    x, y;
    char  tmp[8] = {0};
    arr = malloc(sizeof(char *) * SIZEY);
    for (y = 0; y < SIZEY; y++) {
        arr[y] = malloc(sizeof(char)* SIZEX);
    }

    for (y = 0; y < SIZEY; y++) {
        sprintf(tmp, "%02d", y + 1);
        strcpy(arr[y], tmp);
        printf("%s\n", arr[y]);  // 01 02 ... 64
    }

    for (y = 0; y < SIZEY; y++) {
        free(arr[y]);
    }
    free(arr);
    return 0;
}

とりあえず連続領域で確保する方法

連続領域で確保する場合のやり方です。上の例よりちょっとましな方法ですが…。
これも使うの?と言った感じがします。
このやり方だと1行あたりの文字数が固定で確保することになります。

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define SIZEY 64   /* 行数 */
#define SIZEX 8    /* 1行あたりの文字数 */

int main() {
    char** arr;
    char*  base_arr;
    int    x, y;   
    char  tmp[8] = {0};

    arr = malloc(sizeof(char *) * SIZEY );
    base_arr = malloc(sizeof(char) * SIZEY * SIZEX);
    // 確保した相対アドレスを入れていく
    for ( y = 0; y < SIZEY; y++ ) {
        arr[y] = base_arr + (y * SIZEX); 
    }

    for ( y = 0; y < SIZEY; y++ ) {
        sprintf(tmp, "%02d", y + 1);  // 01 02 ... 64
        strcpy(arr[y], tmp);
        printf( "%s\n", arr[y] );
    }

    free(base_arr);
    free(arr);
    return 0;
}

多分一番使える方法

最後に配列へのポインタを利用する方法です。
文字列の長さが固定ならこれがもっとも実用的だと思われます。
純粋なCを使っているときは結構よく使いました。これも当然連続領域となります。
しかし、配列へのポインタの宣言がきもいです(笑)

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define SIZEY 64   /* 行数 */
#define SIZEX 8    /* 1行あたりの文字数 */

int main() {
    char (*arr)[SIZEX];   // arrはchar[SIZEX]へのポインター
    int    y;
    char  tmp[8] = { 0 };

    // y*x分を確保、char[SIZEX]へのポインターなので、
    // SIZEY * char[SIZEX]分確保
    arr = malloc(sizeof(char) * SIZEY * SIZEX);

    for (y = 0; y < SIZEY; y++) {
        sprintf(tmp, "%02d", y + 1);
        strcpy(arr[y], tmp);
        printf("%s\n", arr[y]);  // 01 02 ... 64
    }

    free(arr);
    return 0;
}

補足

VC++でC++でコンパイルしているけどもmalloc使いたい場合はキャストが必要です。
なお、最後の例でのキャストはこうなります。

arr = (char (*)[SIZEX])malloc(sizeof(char) * SIZEY * SIZEX);

と非常にキ○いことになっております。

あといつも混乱するのが、どんな解説見ても、「配列へのポインタ」と「ポインタの配列」の区別がごっちゃになる。
やっぱり文法(仕様?)が混乱をきたしているとしか言いようがない。
これは私が頭が悪いというより、文法が複雑というか無理やり感があるせいなのだと思う。
例えば次の矢印の3つ目で書ければベスト…。

char (*arr)[8] -> declare (*) char arr[8] -> Ptr<Array<char>[8]> arr  -> char(8)の配列のポインタ
char *arr[8]   -> declare arr char*[8]    -> Array<Ptr<char>>[8] arr  -> charへのポインタの配列