web-dev-qa-db-ja.com

Cポインター:固定サイズの配列を指す

この質問は、Cの達人に伝えられます。

Cでは、次のようにポインターを宣言できます。

char (* p)[10];

..基本的に、このポインターは10文字の配列を指していると述べています。このようなポインターを宣言することの良い点は、異なるサイズの配列のポインターをpに割り当てようとすると、コンパイル時エラーが発生することです。また、単純なcharポインターの値をpに割り当てようとすると、コンパイル時エラーが発生します。これをgccで試しましたが、ANSI、C89、C99で動作するようです。

このようにポインターを宣言することは非常に便利であるように見えます-特に、関数にポインターを渡す場合。通常、人々はこのような関数のプロトタイプを次のように書きます。

void foo(char * p, int plen);

特定のサイズのバッファが必要な場合は、単にplenの値をテストします。ただし、pを渡した人が実際にそのバッファ内の有効なメモリ位置を確保することを保証することはできません。この関数を呼び出した人が正しいことをしていることを信頼する必要があります。一方:

void foo(char (*p)[10]);

..指定されたサイズのバッファを呼び出し元に強制的に与えます。

これは非常に便利に思えますが、これまでに出会ったコードでこのように宣言されたポインターを見たことはありません。

私の質問は次のとおりです。人々がこのようなポインタを宣言しない理由はありますか?明らかな落とし穴はありませんか?

105
figurassa

AndreyTの答えに追加したいと思います(誰かがこのページでつまずいてこのトピックの詳細を探している場合):

これらの宣言でさらに遊び始めると、Cには(明らかにC++にはないように)それらに関連する大きなハンディキャップがあることに気付きます。書き込み元のバッファーへのconstポインターを呼び出し元に渡したい状況が発生することはよくあります。残念ながら、Cでこのようなポインターを宣言する場合、これは不可能です。つまり、C標準(6.7.3-パラグラフ8)は、次のようなものと矛盾しています。


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

この制約はC++には存在しないため、これらのタイプの宣言ははるかに便利です。ただし、Cの場合、固定サイズのバッファーへのconstポインターが必要な場合は常に、通常のポインター宣言にフォールバックする必要があります(バッファー自体がconstとして宣言されていない限り)。このメールスレッドで詳細を確認できます。 link text

これは私の意見では厳しい制約であり、Cで通常このようなポインターを宣言しない主な理由の1つである可能性があります。もう1つは、このようなポインターをAndreyTは指摘しています。

9
figurassa

あなたの投稿で言っていることは絶対に正しいです。すべてのC開発者は、C言語のある程度の習熟度に達すると、まったく同じ発見とまったく同じ結論に達すると思います。

アプリケーション領域の詳細が特定の固定サイズの配列を呼び出す場合(配列サイズはコンパイル時の定数です)、そのような配列を関数に渡す唯一の適切な方法は、配列へのポインターパラメーターを使用することです

void foo(char (*p)[10]);

(C++言語では、これは参照でも行われます

void foo(char (&p)[10]);

)。

これにより、言語レベルの型チェックが有効になり、正確に正しいサイズの配列が引数として提供されるようになります。実際、多くの場合、人々はこの手法を暗黙のうちに使用しますが、気づかないうちに、typedef名の後ろに配列型を隠します

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

さらに、上記のコードは、配列またはstructであるVector3d型に関連して不変であることに注意してください。 Vector3dの定義はいつでも配列からstructに切り替えたり、その逆に切り替えたりできます。関数宣言を変更する必要はありません。どちらの場合でも、関数は「参照による」集約オブジェクトを受け取ります(これには例外がありますが、この説明の文脈ではこれは正しいです)。

しかし、あまりにも多くの人々がかなり複雑な構文に混乱し、それらを適切に使用するためのC言語のそのような機能に単純に十分に満足できないため、この配列受け渡しの方法が明示的に頻繁に使用されることはありません。このため、平均的な実生活では、配列を最初の要素へのポインタとして渡す方がより一般的なアプローチです。見た目は「シンプル」です。

しかし、実際には、配列の受け渡しに最初の要素へのポインターを使用することは非常にニッチな手法であり、非常に特定の目的に役立つトリックです:その唯一の目的は異なるサイズ (つまり、実行時サイズ)。実行時サイズの配列を本当に処理する必要がある場合、そのような配列を渡す適切な方法は、追加のパラメーターによって提供される具体的なサイズを持つ最初の要素へのポインターによるものです。

void foo(char p[], unsigned plen);

実際、多くの場合、実行時サイズの配列を処理できることは非常に有用であり、メソッドの人気にも貢献しています。多くのC開発者は、固定サイズの配列を処理する必要性に出会うことも(認識もすることもありません)、適切な固定サイズの手法を無視しています。

それにもかかわらず、配列サイズが固定されている場合、要素へのポインタとして渡す

void foo(char p[])

は、技術レベルの大きなエラーであり、残念ながら最近ではかなり普及しています。このような場合には、配列へのポインター手法がはるかに優れたアプローチです。

固定サイズの配列を渡す手法の採用を妨げる可能性のあるもう1つの理由は、動的に割り当てられた配列のタイピングに対する単純なアプローチの優位性です。たとえば、プログラムがchar[10]型の固定配列を呼び出す場合(例のように)、平均的な開発者は次のような配列をmallocします

char *p = malloc(10 * sizeof *p);

この配列は、次のように宣言された関数に渡すことはできません

void foo(char (*p)[10]);

これは平均的な開発者を混乱させ、固定サイズのパラメータ宣言をそれ以上考えずに放棄させます。しかし実際には、問題の根本は単純なmallocアプローチにあります。上記のmalloc形式は、実行時サイズの配列用に予約する必要があります。配列型のサイズがコンパイル時の場合、mallocへのより良い方法は次のようになります

char (*p)[10] = malloc(sizeof *p);

もちろん、これは上記のfooに簡単に渡すことができます

foo(p);

コンパイラは適切な型チェックを実行します。繰り返しますが、これは準備のできていないC開発者にとっては非常に紛らわしいため、「典型的な」平均的な日常のコードではあまり見かけません。

158
AnT

明らかな理由は、このコードがコンパイルされないことです:

_extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}
_

配列のデフォルトの昇格は、非修飾ポインターになります。

この質問 も参照してください。foo(&p)を使用すると動作します。

4
Keith Randall

このソリューションはお勧めしません

typedef int Vector3d[3];

これは、Vector3Dに知っておく必要のある型があるという事実を曖昧にするためです。プログラマは通常、同じ型の変数が異なるサイズを持つことを期待しません。考慮してください:

void foo(Vector3d a) {
   Vector3D b;
}

ここで、sizeof a!= sizeof b

1
Per Knytt

また、この構文を使用して、より多くの型チェックを有効にします。

しかし、ポインタを使用する構文とメンタルモデルが単純で覚えやすいことにも同意します。

ここに私が出会ったいくつかの障害があります。

  • 配列にアクセスするには、(*p)[]を使用する必要があります。

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }
    

    代わりに、charへのローカルポインターを使用するのは魅力的です。

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }
    

    しかし、これは正しい型を使用する目的を部分的に無効にします。

  • 配列のアドレスを配列へのポインターに割り当てるときは、アドレス演算子を使用することを忘れないでください:

    char a[10];
    char (*p)[10] = &a;
    

    アドレス演算子は、&aの配列全体のアドレスを取得し、pに割り当てるための正しい型を取得します。演算子を使用しない場合、aは、異なる型を持つ&a[0]と同じように、配列の最初の要素のアドレスに自動的に変換されます。

    この自動変換はすでに行われているので、&が必要であることにいつも戸惑います。他の型の変数での&の使用と一貫していますが、配列は特別であり、アドレスが正しい型であっても&を取得する必要があることを覚えておく必要があります値は同じです。

    私の問題の理由の1つは、80年代にK&R Cを学んだことです。これにより、配列全体で&演算子をまだ使用できませんでした(ただし、一部のコンパイラはそれを無視または構文を許容しました)。ちなみに、これは配列へのポインターを採用するのが難しいもう1つの理由かもしれません。ANSICと&演算子の制限がそれらを考慮するもう1つの理由であるため、正しく機能するだけです。ぎこちない。

  • typedefnotを使用して(一般的なヘッダーファイル内の)配列へのポインターの型を作成する場合、配列へのグローバルポインターには、より複雑なexternファイル間で共有するための宣言:

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
    
1
Orafu

簡単に言えば、Cはそのようなことをしません。タイプTの配列は、配列内の最初のTへのポインターとして渡されますが、それだけです。

これにより、次のような式で配列をループするなど、いくつかのクールでエレガントなアルゴリズムが可能になります。

*dst++ = *src++

欠点は、サイズの管理があなた次第だということです。残念ながら、これを誠実に行わないと、Cコーディングの数百万のバグ、および/または悪意のある悪用の機会につながります。

Cでの要求に近いのは、struct(値による)または1へのポインター(参照による)を渡すことです。この操作の両側で同じ構造体タイプが使用されている限り、参照を渡すコードとそれを使用するコードの両方が、処理されるデータのサイズについて一致しています。

構造体には、必要なデータを含めることができます。明確に定義されたサイズの配列を含めることができます。

それでも、あなたまたは無能なまたは悪意のあるコーダーがキャストを使用してコンパイラを欺いて、構造体を異なるサイズの1つとして扱うことを妨げるものはありません。この種のことを行うためのほとんど拘束されていない能力は、Cの設計の一部です。

1
Carl Smotricz

文字の配列はさまざまな方法で宣言できます。

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

値で配列を受け取る関数のプロトタイプは次のとおりです。

void foo(char* p); //cannot modify p

または参照:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

または配列構文によって:

void foo(char p[]); //same as char*
1
s1n