C動的に成長する配列


126

ゲーム内のエンティティの「生の」リストを読み取るプログラムがあり、さまざまなものを処理するために、エンティティの数が不定のインデックス番号(int)を保持する配列を作成するつもりです。そのようなインデックスを維持するためにメモリやCPUを使いすぎないようにしたい...

私がこれまでに使用した迅速で汚い解決策は、メインの処理関数(ローカルフォーカス)で、最大ゲームエンティティのサイズの配列と、リストに追加された数を追跡する別の整数を宣言することです。すべてのリストが3000個以上の配列を保持しているため、これは満足のいくものではありませんが、それは無駄ではありませんが、さまざまな関数に6〜7個のリストのソリューションを使用できるので無駄です。

これを実現するためのC(C ++またはC#ではない)固有のソリューションは見つかりませんでした。ポインターを使用することはできますが、ポインターを使用するのは少し怖いです(唯一の可能な方法でない限り)。

配列が変更された場合、配列はローカル関数のスコープを離れません(関数に渡され、その後破棄されます)。

ポインターが唯一の解決策である場合、リークを回避するためにポインターを追跡するにはどうすればよいですか?


1
これはCの(非常に小さな)問題ですが、C ++とC#のすべてのソリューションをどのようにして見逃しましたか?
イグナシオバスケスエイブラムス

11
「ポインターが唯一の解決策である場合、リークを回避するためにポインターを追跡するにはどうすればよいですか?」ケア、注意、そしてvalgrind。これこそ、そもそもCだと人々が恐れる理由です。
Chris Lutz、

27
ポインターを使用せずにCを効果的に使用することはできません。恐れるな。
qrdl 2010

:大きなLIBSも構造体などのためにすべてのための唯一の機能がないstackoverflow.com/questions/3456446/...
user411313

6
ポインターなしでCを使用することは、燃料なしで車を使用することに似ています。
martinkunev 2017

回答:


210

ポインターは使用できますが、使用するのが少し怖いです。

動的配列が必要な場合、ポインターをエスケープすることはできません。なんで怖いの?彼らは噛みません(あなたが注意している限り)。Cには組み込みの動的配列はありません。自分で作成する必要があります。C ++では、組み込みstd::vectorクラスを使用できます。C#と他のほぼすべての高水準言語にも、動的配列を管理する類似のクラスがあります。

独自に作成する予定がある場合は、次のように始めます。ほとんどの動的配列の実装は、いくつかの(小さい)デフォルトサイズの配列から始めて、新しい要素を追加するときにスペースが足りなくなるたびに、配列のサイズ。以下の例でわかるように、それほど難しくはありません(簡潔にするために、安全チェックは省略しています)。

typedef struct {
  int *array;
  size_t used;
  size_t size;
} Array;

void initArray(Array *a, size_t initialSize) {
  a->array = malloc(initialSize * sizeof(int));
  a->used = 0;
  a->size = initialSize;
}

void insertArray(Array *a, int element) {
  // a->used is the number of used entries, because a->array[a->used++] updates a->used only *after* the array has been accessed.
  // Therefore a->used can go up to a->size 
  if (a->used == a->size) {
    a->size *= 2;
    a->array = realloc(a->array, a->size * sizeof(int));
  }
  a->array[a->used++] = element;
}

void freeArray(Array *a) {
  free(a->array);
  a->array = NULL;
  a->used = a->size = 0;
}

使い方は簡単です:

Array a;
int i;

initArray(&a, 5);  // initially 5 elements
for (i = 0; i < 100; i++)
  insertArray(&a, i);  // automatically resizes as necessary
printf("%d\n", a.array[9]);  // print 10th element
printf("%d\n", a.used);  // print number of elements
freeArray(&a);

1
サンプルコードをありがとう。私は大きな配列を使用して特定の機能を実装しましたが、これを使用して他の同様のものを実装し、それを制御した後、他の機能を元に戻します:)
Balkania

2
コードをたくさんありがとう。removeArray最後の要素を取り除く方法もきちんとだろう。許可する場合は、コードサンプルに追加します。
ブリムボリウム2013年

5
%dとsize_t ...ノーノーのビット。C99以降を使用している場合、%zの追加を利用できます
ランディハワード

13
メモリの割り当てと再割り当てによる安全性チェックを省略しないでください。
Alex Reynolds

3
これはパフォーマンスのトレードオフです。毎回2倍にすると、オーバーヘッドが100%、平均で50%になることがあります。3/2は、50%が最悪、25%が標準です。これは、フィビオナッチ数列の有効なベース(ファイ)にも近く、「指数2ですが、ベース2よりもはるかに弱くない」という特徴があり、計算が容易です。+8は、適度に小さい配列が、あまり多くのコピーを実行しないことを意味します。乗法の項を追加して、サイズに関係がない場合に配列をすばやく拡大できるようにします。スペシャリストの使用では、これは調整可能でなければなりません。
Dan Sheppard

10

私が考えることができるいくつかのオプションがあります。

  1. リンクリスト。リンクされたリストを使用して、動的に成長する配列を作成できます。しかしarray[100]1-99最初にウォークスルーしなければ、これを行うことはできません。そして、あなたが使うのもそれほど便利ではないかもしれません。
  2. 大きな配列。すべてのために十分なスペース以上のアレイを作成するだけです
  3. 配列のサイズを変更しています。サイズがわかったらアレイを再作成するか、マージンを確保してスペースが不足するたびに新しいアレイを作成し、すべてのデータを新しいアレイにコピーします。
  4. リンクリスト配列の組み合わせ。単純に固定サイズの配列を使用し、スペースがなくなったら、新しい配列を作成してそれにリンクします(構造体の配列と次の配列へのリンクを追跡することをお勧めします)。

どのオプションがあなたの状況に最適であるかを言うのは難しいです。もちろん、単純に大きな配列を作成することは最も簡単な解決策の1つであり、実際に大きくない限り、それほど大きな問題にはなりません。


3264整数の7つの配列は、現代風の2Dゲームでどのように聞こえますか?私が偏執的である場合、解決策は大きな配列になります。
バルカニア2010

3
ここの#1と#4の両方で、ポインタと動的メモリ割り当てを使用する必要があります。realloc#3と一緒に使用することをお勧めします-配列を通常のサイズに割り当て、不足するたびに配列を大きくしてください。 realloc必要に応じてデータのコピーを処理します。メモリー管理に関するOPの質問についてmallocは、最初にfree1回、最後にrealloc1回、そして部屋を使い果たすたびに必要です。悪くないです。
ボレアリド2010

1
@バルカニア:3264整数の7つの配列は100KB未満の髪です。それはまったくメモリではありません。
ボレアリド2010

1
@バルカニア:の7 * 3264 * 32 bitように聞こえ91.39 kilobytesます。最近の標準ではそれほどではありません;)
10

1
ときにどうするか、完全に明らかにされていませんので、この特定の省略は、恥であるreallocリターンNULLa->array = (int *)realloc(a->array, a->size * sizeof(int));...は、おそらくそれが最善のように書かれてただろう:int *temp = realloc(a->array, a->size * sizeof *a->array); a->array = temp;...起こる必要が起こらないものは何でもあることは明らかであろうそのように前をNULL値がに割り当てられているa->array(それがすべてである場合)。
2015

10

当初よりも恐ろしいように見えるすべてのものと同様に、最初の恐怖を乗り越える最善の方法は は、未知の不快感に身浸す!結局のところ、それは私たちが最もよく学ぶような時です。

残念ながら、制限があります。関数の使い方をまだ学習している間は、たとえば教師の役割を引き受けないでください。よくある落とし穴であるにもかかわらず、エラー処理省略していると偽って、使い方を知らないように見える人からの回答realloc(つまり、現在受け入れられている回答!)言及する必要があります。正しく使用する方法を説明する回答です。答えは、エラーチェックを実行するために、戻り値を別の変数に格納することですrealloc

関数を呼び出すたび、および配列を使用するたびに、ポインターを使用しています。変換は暗黙的に行われています。これは、恐らくもっと恐ろしいはずです。ほとんどの問題を引き起こすのは、目に見えないものだからです。たとえば、メモリリーク...

配列演算子はポインター演算子です。array[x]は本当にのショートカットであり*(array + x)、次のように分解できます:*および(array + x)。それが*あなたを混乱させるものである可能性が最も高いです。我々はさらに仮定して問題から付加を排除することができるxことが0、したがって、array[0]となる*array追加ため0値を変更しません...

...したがって、これは*arrayと同等であることがわかりarray[0]ます。どちらか一方を使いたい場所で使用でき、その逆も可能です。配列演算子はポインター演算子です。

mallocreallocそして友達はあなたがずっと使ってきたポインタの概念を発明していません。これは、これを使用して他の機能を実装するだけです。これは、異なる形式のストレージ期間であり、サイズの大幅で動的な変更が必要な場合に最適です。

現在受け入れられている答えがStackOverflowに関する他の非常に根拠のあるアドバイスの粒度に反すると同時に、このユースケースにぴったりとなるあまり知られていない機能を導入する機会を逃してしまうのは残念です:フレキシブル配列メンバー!それは実際にはかなり壊れた答えです... :(

を定義するときは、上限なしで、構造の最後struct配列宣言します。例えば:

struct int_list {
    size_t size;
    int value[];
};

これにより、の配列をintと同じ割り当てに統合するcountことができ、このようにバインドすることは非常に便利です。

sizeof (struct int_list)valueサイズが0であるかのように動作するため、リストが空の構造体のサイズがわかりますreallocリストのサイズを指定するには、渡されたサイズに追加する必要があります。

もう1つの便利なヒントは、とrealloc(NULL, x)同等であることを覚えておくことmalloc(x)です。これを使用してコードを簡略化できます。例えば:

int push_back(struct int_list **fubar, int value) {
    size_t x = *fubar ? fubar[0]->size : 0
         , y = x + 1;

    if ((x & y) == 0) {
        void *temp = realloc(*fubar, sizeof **fubar
                                   + (x + y) * sizeof fubar[0]->value[0]);
        if (!temp) { return 1; }
        *fubar = temp; // or, if you like, `fubar[0] = temp;`
    }

    fubar[0]->value[x] = value;
    fubar[0]->size = y;
    return 0;
}

struct int_list *array = NULL;

struct int_list **最初の引数として使用することを選択した理由はすぐには明らかではないように思われるかもしれませんが、2番目の引数について考えるとvalue、内部から行われた変更は、push_back呼び出し元の関数からは見えません。同じことが最初の引数にも当てはまりますarrayここだけなく、他の関数でもを変更できるようにする必要があります...

array何も指さないところから始まります。空のリストです。それを初期化することは、それに追加することと同じです。例えば:

struct int_list *array = NULL;
if (!push_back(&array, 42)) {
    // success!
}

PS それが終わったら忘れないでくださいfree(array);


array[x]本当に、のショートカットです*(array + x)[...]」よろしいですか???? それらのさまざまな動作の説明を参照してください:eli.thegreenplace.net/2009/10/21/…
C-Star-W-Star

1
残念ながら、@ C-Star-Puppy、リソースにまったく言及されていないように見える参照の1つはC標準です。これは、コンパイラーがCコンパイラーを合法的に呼び出すために準拠しなければならない仕様です。リソースが私の情報とまったく矛盾していないようです。それにもかかわらず、標準は、実際のようないくつかの例があるこの宝石、それは明らかにしていますarray[index]実際にptr[index]変装して... 「添字演算子の定義[]それがされるE1[E2]と同じです(*((E1)+(E2)))あなたはSTD反論することはできません
自閉症

:C-スター子犬@、このデモを試してみてくださいint main(void) { unsigned char lower[] = "abcdefghijklmnopqrstuvwxyz"; for (size_t x = 0; x < sizeof lower - 1; x++) { putchar(x[lower]); } }...あなたはおそらくする必要があります#include <stdio.h>し、<stddef.h>...あなたは、私が書いた方法を見てくださいx[lower](とxいうより整数型ですか)lower[x]?はと*(lower + x)同じ値*(x + lower)でありlower[x]、前者であるのでCコンパイラは気にしませんx[lower]。後者は後者です。これらの式はすべて同等です。それらを試してみてください...私の言葉を
自閉症

...そしてもちろん、この部分もありますが、これは私が自分で強調したものですが、実際には強調せずに引用全体を読む必要があります。「それがsizeof演算子、_Alignof演算子、または単項&演算子、または配列の初期化に使用される文字列リテラルである場合、「型の配列」型の式は、配列の初期要素を指す「型へのポインター」型の式に変換されますオブジェクトであり、左辺値ではありません。配列オブジェクトにレジスタストレージクラスがある場合、動作は未定義です。関数についても同じことが言えます。
自閉症の

おお、そして最後の注意として、@ C-Star-Puppyで、Microsoft C ++はCコンパイラではなく、ほぼ20年もの間使用されていません。C89モード(suuuure)を有効にできますが、1980年代後半以降のコンピューティングは進化しています。そのトピックの詳細については、私は読んでお勧めこの記事を ...そしてなどの実際のCコンパイラに切り替えるgccか、clangあなたはC99を備えて採用しているので、多くのパッケージがある見つけることができますので、あなたのCのコンパイルのすべてのために...
自閉症の

3

上に構築さマッテオFurlans彼が言ったときに、デザインの「新しい要素を追加する際に最もダイナミックな配列の実装はいくつかの(小)デフォルトサイズの配列をオフに開始することで動作し、その後、あなたは、配列のサイズを2倍、スペースが不足したときに」。以下の「作業中」の違いは、サイズが2倍ではなく、必要なものだけを使用することです。また、簡単にするために安全チェックも省略しています... ブリムボリウムのアイデアに基づいて、コードに削除機能を追加しようとしました...

storage.hファイルは次のようになります...

#ifndef STORAGE_H
#define STORAGE_H

#ifdef __cplusplus
extern "C" {
#endif

    typedef struct 
    {
        int *array;
        size_t size;
    } Array;

    void Array_Init(Array *array);
    void Array_Add(Array *array, int item);
    void Array_Delete(Array *array, int index);
    void Array_Free(Array *array);

#ifdef __cplusplus
}
#endif

#endif /* STORAGE_H */

storage.cファイルは次のようになります...

#include <stdio.h>
#include <stdlib.h>
#include "storage.h"

/* Initialise an empty array */
void Array_Init(Array *array) 
{
    int *int_pointer;

    int_pointer = (int *)malloc(sizeof(int));

    if (int_pointer == NULL)
    {       
        printf("Unable to allocate memory, exiting.\n");
        free(int_pointer);
        exit(0);
    }
    else
    {
        array->array = int_pointer; 
        array->size = 0;
    }
}

/* Dynamically add to end of an array */
void Array_Add(Array *array, int item) 
{
    int *int_pointer;

    array->size += 1;

    int_pointer = (int *)realloc(array->array, array->size * sizeof(int));

    if (int_pointer == NULL)
    {       
        printf("Unable to reallocate memory, exiting.\n");
        free(int_pointer);
        exit(0);
    }
    else
    {
        array->array = int_pointer;
        array->array[array->size-1] = item;
    }
}

/* Delete from a dynamic array */
void Array_Delete(Array *array, int index) 
{
    int i;
    Array temp;
    int *int_pointer;

    Array_Init(&temp);

    for(i=index; i<array->size; i++)
    {
        array->array[i] = array->array[i + 1];
    }

    array->size -= 1;

    for (i = 0; i < array->size; i++)
    {
        Array_Add(&temp, array->array[i]);
    }

    int_pointer = (int *)realloc(temp.array, temp.size * sizeof(int));

    if (int_pointer == NULL)
    {       
        printf("Unable to reallocate memory, exiting.\n");
        free(int_pointer);
        exit(0);
    }
    else
    {
        array->array = int_pointer; 
    } 
}

/* Free an array */
void Array_Free(Array *array) 
{
  free(array->array);
  array->array = NULL;
  array->size = 0;  
}

main.cは次のようになります...

#include <stdio.h>
#include <stdlib.h>
#include "storage.h"

int main(int argc, char** argv) 
{
    Array pointers;
    int i;

    Array_Init(&pointers);

    for (i = 0; i < 60; i++)
    {
        Array_Add(&pointers, i);        
    }

    Array_Delete(&pointers, 3);

    Array_Delete(&pointers, 6);

    Array_Delete(&pointers, 30);

    for (i = 0; i < pointers.size; i++)
    {        
        printf("Value: %d Size:%d \n", pointers.array[i], pointers.size);
    }

    Array_Free(&pointers);

    return (EXIT_SUCCESS);
}

続く建設的な批判を楽しみにしています...


1
あなたが求める建設的な批判であれば、Code Reviewに投稿してください。とはいえ、いくつかの提案:malloc()割り当てを使用する前に、コードがへの呼び出しの成功を確認することが不可欠です。同じように、割り当てrealloc()られた元のメモリへのポインタに結果を直接割り当てるのは誤りです。場合はrealloc()失敗し、NULL返され、コードがメモリリークが残されています。一度に1つのスペースを追加するよりも、サイズ変更時にメモリを2倍にする方がはるかに効率的realloc()です。
ex nihilo 2018

1
私は「建設的な批判」と言ったとき、私はちょうど冗談を言っていた、私は引き裂か取得するつもりだった知っていた...アドバイスをありがとう...

2
誰かを引き離そうとするのではなく、建設的な批評を提供するだけです。これは、あなたの気楽な近さなしでも近々行われる可能性があります;)
ex nihilo

1
デビッド、私はあなたのコメントについて考えていました「一度に1つのスペースを追加するよりも、サイズを変更するときにメモリを2倍にする方がはるかに効率的です:realloc()への呼び出しが少ない」。詳細について教えてください。タスクに必要な量だけを割り当てるよりも、メモリを2倍に割り当てて使用しないほうがよいため、メモリを浪費する方がよいのはなぜですか。realloc()の呼び出しについてあなたが言っていることがわかりますが、なぜ問題が発生するたびにrealloc()を呼び出すのですか?それは、メモリを再割り当てするためのものではありませんか?

1
厳密な倍加は最適ではないかもしれませんが、一度に1バイト(または1バイトintなど)ずつメモリを増やすよりは確かに優れています。倍増は典型的な解決策ですが、すべての状況に適合する最適な解決策があるとは思いません。倍増が良い考えである理由はここにあります(1.5のような他の要素も同様に良いでしょう):妥当な割り当てから始めれば、まったく再割り当てする必要がないかもしれません。より多くのメモリが必要になると、合理的な割り当てが2倍になります。このようにすると、への呼び出しが1つまたは2つで済みますrealloc()
ex nihilo 2018

2

あなたが言っているとき

不確定な数のエンティティのインデックス番号(int)を保持する配列を作成する

基本的には「ポインター」を使用していると言っていますが、これはメモリー全体のポインターではなく、配列全体のローカルポインターです。概念的にはすでに「ポインタ」(つまり、配列内の要素を参照するID番号)を使用しているので、通常のポインタ(つまり、最大の配列内の要素を参照するID番号:メモリ全体)を使用しないのはなぜですか)。

オブジェクトにリソースID番号を格納する代わりに、オブジェクトにポインタを格納させることができます。基本的には同じですが、「配列+インデックス」を「ポインタ」に変換することを避けるため、はるかに効率的です。

ポインタは、メモリ全体の配列インデックスと見なすと怖くない(これが実際のポインタです)。


2

あらゆる種類の無制限のアイテムの配列を作成するには:

typedef struct STRUCT_SS_VECTOR {
    size_t size;
    void** items;
} ss_vector;


ss_vector* ss_init_vector(size_t item_size) {
    ss_vector* vector;
    vector = malloc(sizeof(ss_vector));
    vector->size = 0;
    vector->items = calloc(0, item_size);

    return vector;
}

void ss_vector_append(ss_vector* vec, void* item) {
    vec->size++;
    vec->items = realloc(vec->items, vec->size * sizeof(item));
    vec->items[vec->size - 1] = item;
};

void ss_vector_free(ss_vector* vec) {
    for (int i = 0; i < vec->size; i++)
        free(vec->items[i]);

    free(vec->items);
    free(vec);
}

そしてそれを使用する方法:

// defining some sort of struct, can be anything really
typedef struct APPLE_STRUCT {
    int id;
} apple;

apple* init_apple(int id) {
    apple* a;
    a = malloc(sizeof(apple));
    a-> id = id;
    return a;
};


int main(int argc, char* argv[]) {
    ss_vector* vector = ss_init_vector(sizeof(apple));

    // inserting some items
    for (int i = 0; i < 10; i++)
        ss_vector_append(vector, init_apple(i));


    // dont forget to free it
    ss_vector_free(vector);

    return 0;
}

このベクトル/配列は、あらゆるタイプのアイテムを保持でき、サイズは完全に動的です。


0

ええと、要素を削除する必要がある場合は、除外する要素を省略した配列のコピーを作成します。

// inserting some items
void* element_2_remove = getElement2BRemove();

for (int i = 0; i < vector->size; i++){
       if(vector[i]!=element_2_remove) copy2TempVector(vector[i]);
       }

free(vector->items);
free(vector);
fillFromTempVector(vector);
//

、、およびが一時ベクトルを処理するための補助メソッドgetElement2BRemove()であるcopy2TempVector( void* ...)と想定しfillFromTempVector(...)ます。


これが実際に提起された質問への回答なのか、それともコメントなのかは不明です。

これは「方法」に対する意見であり、誰かがより良いアイデアを持っている場合、私は確認を求めています(私は間違っていますか?)。;)
JOSMAR BARBOSA

私はあなたの最後の文を理解していないと思います。SOはスレッドフォーラムではないため、このような未解決の質問に対する回答は奇妙に見えます。

1
私はあなたの最後の文章をあなたが言いたいと思うものに修正しました。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.