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


119

この質問は、そこにいるCの教祖に向けられます。

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

char (* p)[10];

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

このようにポインタを宣言することは、特にポインタを関数に渡す場合に非常に便利です。通常、このような関数のプロトタイプは次のように記述します。

void foo(char * p, int plen);

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

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

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

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

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


3
注:C99配列はタイトルで示されているように固定サイズである必要はないため10、スコープ内の任意の変数で置き換えることができます
MM

回答:


173

あなたの投稿で言っていることは完全に正しいです。すべての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);

さらに、上記のコードは、Vector3d型が配列またはであることに関連して不変であることに注意してくださいstruct。の定義はVector3dいつでも配列からaに切り替えることができ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開発者にとっては非常に混乱します。そのため、「典型的な」平均的な日常のコードではあまり見られません。


2
答えは、sizeof()がどのように成功するか、頻繁に失敗する方法、および常に失敗する方法について、非常に簡潔で有益な説明を提供します。ほとんどのC / C ++エンジニアの理解が理解されていないため、彼らが理解していると彼らが考える何かを行うことは、私がしばらく見てきたより予言的なことの1つであり、そのベールは、説明している正確さと比較して何もありません。真剣に、サー。素晴らしい答え。
WhozCraig

私はブラボー、この回答に基づいていくつかのコードをリファクタリングし、QとAの両方に感謝
ペリー

1
constこの手法でプロパティを処理する方法を知りたいです。const char (*p)[N]引数はポインタとの互換性は思えないchar table[N];対照的に、簡単なchar*との互換性を保つPTR const char*引数。
シアン、

4
配列の要素にアクセスするには、そうする必要があるのでは(*p)[i]なく、そうする必要があることに注意してください*p[i]。後者は配列のサイズによってジャンプしますが、これはほぼ確実に望んでいることではありません。少なくとも私にとって、この構文を学ぶことは間違いを引き起こしたのではなく、引き起こした。float *を渡すだけで正しいコードをより速く取得できただろう。
Andrew Wagner

1
はい、@ mickey、あなたが提案したのはconst可変要素の配列へのポインタです。そして、はい、これは不変要素の配列へのポインタとは完全に異なります。
シアン

11

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であると宣言されていない限り)、通常のポインター宣言にフォールバックする必要があります。あなたはこのメールスレッドでより多くの情報を見つけることができます:リンクテキスト

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


2
これはコンパイラ固有の問題のようです。gcc 4.9.1を使用して複製することができましたが、clang 3.4.2は非constバージョンからconstバージョンに問題なく移行できました。私はC11仕様(私のバージョンのp 9 ...互換性のある2つの修飾型について話している部分)を読み、これらの変換が違法であると思われることに同意します。ただし、実際には、警告なしに常にchar *からchar const *に自動的に変換できることがわかっています。IMO、clangはこれを許可する際にgccよりも一貫していますが、仕様ではこれらの自動変換は禁止されているようです。
Doug Richardson

4

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

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

配列のデフォルトの昇格は、修飾されていないポインタです。

参照してくださいこの質問を使用して、foo(&p)動作するはずです。


3
もちろんfoo(p)は機能しません。fooは10要素の配列へのポインタを要求しているので、配列のアドレスを渡す必要があります...
ブライアンR.ボンディ

9
それは「明白な理由」ですか?関数を呼び出す適切な方法はであることは明らかですfoo(&p)
AnT 2009年

3
「自明」というのは間違った言葉だと思います。「最も簡単」を意味しました。この場合のpと&pの違いは、平均的なCプログラマには非常に不明瞭です。ポスターが提案したことを行おうとする誰かが、私が書いたものを書き、コンパイル時エラーを受け取り、あきらめるでしょう。
キースランダル

2

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

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

ここに私が遭遇したいくつかの障害があります。

  • アレイにアクセスするには、以下を使用する必要があります(*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;

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

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

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

  • typedefが配列へのポインターの型の作成に使用されていない場合(共通のヘッダーファイル内)、配列へのグローバルポインターは、externファイル間で共有するために、より複雑な宣言が必要です。

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

1

まあ、簡単に言うと、Cはそのように動作しません。タイプの配列は、配列Tの最初Tの配列へのポインターとして渡されます。これですべてです。

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

*dst++ = *src++

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

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

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

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


1

文字の配列を宣言する方法はいくつかあります。

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*

2
固定サイズの配列はとして動的に割り当てることもできることを忘れないでくださいchar (*p)[10] = malloc(sizeof *p)
AnT 2009年

char array []とchar * ptrの違いの詳細については、こちらをご覧ください。stackoverflow.com/questions/1807530/...
t0mm13b

1

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

typedef int Vector3d[3];

Vector3Dが知っておくべき型を持っているという事実を覆い隠すからです。プログラマは通常、同じ型の変数が異なるサイズであることを期待しません。考慮してください:

void foo(Vector3d a) {
   Vector3D b;
}

ここで、sizeof a!= sizeof b


彼はこれを解決策として提案していませんでした。彼は単にこれを例として使用していました。
figurassa 2009年

うーん。なぜsizeof(a)同じではないのsizeof(b)ですか?
sherrellbc

0

多分私は何かが足りないかもしれませんが...配列は定数ポインターなので、基本的にそれはそれらへのポインターの周りを渡す意味がないことを意味します。

使ってみvoid foo(char p[10], int plen);ませんか?


4
配列は定数ポインターではありません。アレイに関するFAQを読んでください。
AnT 2009年

2
ここで重要なこと(パラメーターとしての1次元配列)の場合、定数ポインターに減衰するというのが事実です。面倒をなくす方法についてのFAQを読んでください。
Fortran 2009年

-2

私のコンパイラ(vs2008)ではchar (*p)[10]、Cファイルとしてコンパイルした場合でも、括弧がないかのように文字ポインタの配列として扱われます。コンパイラはこの「変数」をサポートしていますか?もしそうならそれを使用しない主な理由です。


1
-1間違い。vs2008、vs2010、gccでは問題なく動作します。特に、この例はうまく機能します:stackoverflow.com/a/19208364/2333290
kotlomoy 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.