CおよびC ++コンパイラーは、強制されないのに関数シグニチャーで配列長を許可するのはなぜですか?


131

これは私の学習期間中に私が見つけたものです:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

したがって、変数int dis(char a[1])では、 を使用できるため、[1]は何も実行せず、まったく機能しません。またはのように。配列名はポインタであり、配列を伝達する方法を知っているので、私のパズルはこの部分についてではありません。
a[2]int a[]char *a

私が知りたいのは、コンパイラがこの動作を許可する理由です(int a[1])。それとも私が知らない他の意味がありますか?


6
これは、実際に配列を関数に渡すことができないためです。
Ed S.

37
ここでの質問は、Cがパラメーターが配列型であることを宣言できるのに、ポインターとまったく同じように動作する理由であったと思います。
ブライアン

8
@ブライアン:これが動作の引数かどうかはわかりませんが、引数の型がtypedef配列型の場合にも当てはまります。したがって、引数型の「ポインタへの減衰」は、単なる構文の置き換え[]ではなく*、実際に型システムを通過します。これはva_list、配列タイプまたは非配列タイプで定義されるようないくつかの標準タイプに対して実際の結果をもたらします。
R .. GitHub ICE HELPING ICEの停止2014年

4
@songyuanyaoポインターを使用して、C(およびC ++)で完全に異なるものではないことを達成できますint dis(char (*a)[1])。次に、配列へのポインタを渡しますdis(&b)。C ++には存在しないCの機能を使用するつもりなら、void foo(int data[static 256])やのように言うこともできますが、それはint bar(double matrix[*][*])他のワームの缶です。
スチュアートオルセン

1
@StuartOlsenポイントは、どの標準を定義したのかではありません。重要なのは、それを定義した人がそのよう定義した理由です。
user253751 2014年

回答:


156

これは、関数に配列を渡すための構文の癖です。

実際には、Cで配列を渡すことはできません。配列を渡す必要があるように見える構文を記述した場合、実際に起こるのは、配列の最初の要素へのポインターが代わりに渡されることです。

ポインターには長さ情報が含まれていないため、[]関数仮パラメーターリストのの内容は実際には無視されます。

この構文を許可する決定は1970年代に行われ、それ以来ずっと混乱を引き起こしています...


21
C以外のプログラマーとして、この答えは非常にアクセスしやすいと思います。+1
アステリ2014年

21
「この構文を許可する決定は1970年代に行われ、それ以来ずっと混乱を引き起こしています...」
NoSenseEtAl

8
これは事実ですが、構文を使用してそのサイズの配列を渡すこともできvoid foo(int (*somearray)[20])ます。この場合、発信者サイトでは20が強制されます。
v.oddou 2014年

14
-1 Cプログラマーとして、私はこの答えが間違っていると思います。[]patの回答に示されているように、多次元配列では無視されません。したがって、配列構文を含める必要がありました。さらに、1次元配列であってもコンパイラーが警告を出すのを止めるものはありません。
user694733 14年

7
「あなたの[]の内容」とは、質問のコードのことです。この構文の癖はまったく必要ありませんでした。ポインター構文を使用して同じことを実現できます。つまり、ポインターが渡される場合、パラメーターをポインター宣言子にする必要があります。たとえば、patの例ではvoid foo(int (*args)[20]);、厳密に言うと、Cには多次元配列がありません。しかし、要素が他の配列である可能性がある配列があります。これは何も変更しません。
MM

143

最初の次元の長さは無視されますが、コンパイラがオフセットを正しく計算できるようにするには、追加の次元の長さが必要です。次の例では、foo関数に2次元配列へのポインターが渡されます。

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

最初の次元のサイズ[10]は無視されます。コンパイラーは最後からインデックスを作成することを妨げません(フォーマルでは10要素が必要ですが、実際には2つしか提供されないことに注意してください)。ただし、2番目の次元のサイズは[20]各行のストライドを決定するために使用されます。ここで、フォーマルは実際と一致する必要があります。この場合も、コンパイラーは、2番目の次元の終わりからインデックスを作成することを妨げません。

配列のベースから要素へのバイトオフセットは、次のようにargs[row][col]決定されます。

sizeof(int)*(col + 20*row)

の場合col >= 20、実際には次の行(または配列全体の最後)にインデックスを付けることに注意してください。

sizeof(args[0])80私のマシンに戻りますsizeof(int) == 4。ただし、sizeof(args)を取得しようとすると、次のコンパイラ警告が表示されます。

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

ここで、コンパイラーは、配列自体のサイズではなく、配列が減衰したポインターのサイズのみを提供することを警告しています。


非常に便利です。これとの一貫性も、1-dの場合の奇妙な理由としてもっともらしくあります。
jwg 2014年

1
1次元の場合と同じ考えです。CおよびC ++の2次元配列のように見えるのは、実際には1次元配列で、その各要素は別の1次元配列です。この場合、10要素の配列があり、各要素は「20整数の配列」です。私の投稿で説明したように、実際に関数に渡されるのはの最初の要素へのポインターですargs。この場合、argsの最初の要素は「20整数の配列」です。ポインタにはタイプ情報が含まれます。渡されるのは、「20整数の配列へのポインター」です。
MM

9
ええ、それがint (*)[20]タイプです。「20整数の配列へのポインター」。
2014年

33

問題とそれをC ++で克服する方法

問題はpatMatt によって広範囲に説明さています。コンパイラは基本的に、配列のサイズの最初の次元を無視し、渡された引数のサイズを事実上無視しています。

一方、C ++では、この制限を2つの方法で簡単に克服できます。

  • 参照の使用
  • 使用std::array(C ++ 11以降)

参考文献

関数が既存の配列の読み取りまたは変更のみを試みている(それをコピーしていない)場合は、参照を簡単に使用できます。

たとえば、intすべての要素を10に設定する10 の配列をリセットする関数が必要だとします0。これは、次の関数シグネチャを使用して簡単に実行できます。

void reset(int (&array)[10]) { ... }

だけでなく、これはしますうまく動作するが、それはまたになる配列の次元を施行します

テンプレートを利用して、上記のコードを汎用にすることもできます。

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

そして最後に、const正確さを利用できます。10要素の配列を出力する関数を考えてみましょう:

void show(const int (&array)[10]) { ... }

const修飾子を適用することで、可能な変更防止しています


配列の標準ライブラリクラス

上記の構文を醜いと不必要の両方で考えると、私がそうするように、それを缶に入れてstd::array代わりに使用できます(C ++ 11以降)。

これがリファクタリングされたコードです:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

素晴らしいですね。私が以前にあなたに教えた一般的なコードトリックは言うまでもなく、それでも動作します:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

それだけでなく、無料でセマンティックスをコピーおよび移動できます。:)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

何を求めている?使用してくださいstd::array


2
@kietz、提案された編集が拒否されてすみませんが、特に指定されていない限り、C ++ 11が使用されていると自動的に想定します。
2014年

これは本当ですが、与えられたリンクに基づいて、ソリューションがC ++ 11のみであるかどうかも指定することになっています。
trlkly 2014年

@trlkly、私は同意します。私はそれに応じて答えを編集しました。指摘してくれてありがとう。
2014年

9

これはCの楽しい機能で、傾斜が強い場合でも効果的に足を撃つことができます。

その理由は、Cがアセンブリ言語の1ステップにすぎないためだと思います。サイズチェック同様の安全機能が削除され、ピークパフォーマンスが可能になりました。これは、プログラマーが非常に勤勉である場合は問題ありません。

また、関数の引数にサイズを割り当てると、その関数が別のプログラマーによって使用されたときに、サイズの制限に気付く可能性があるという利点があります。ポインタを使用するだけでは、その情報は次のプログラマに伝わりません。


3
はい。Cは、コンパイラーよりもプログラマーを信頼するように設計されています。配列の最後に露骨にインデックスを付ける場合は、特別で意図的なことをしている必要があります。
John

7
私は14年前にCでのプログラミングに夢中になりました。私の教授が言ったすべての中で、他のすべてのものよりも私に不満を感じさせた1つのフレーズは、「Cはプログラマーのために、プログラマーのために書かれたものです」。言語は非常に強力です。(決まり文句の準備)ベンおじさんが私たちに教えたように、「大きな力には大きな責任が伴います」。
Andrew Falanga 14年

6

まず、Cは配列の境界をチェックしません。ローカル、グローバル、静的、パラメーターなど、何でもかまいません。配列の境界のチェックは、より多くの処理を意味し、Cは非常に効率的であると考えられているため、配列の境界のチェックは、必要に応じてプログラマーによって行われます。

第二に、配列を関数に値渡しすることを可能にするトリックがあります。関数から配列を値で返すこともできます。structを使用して新しいデータ型を作成するだけです。例えば:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

次のような要素にアクセスする必要があります:foo.a [1]。余分な ".a"は奇妙に見えるかもしれませんが、このトリックはC言語に優れた機能を追加します。


7
ランタイム境界チェックとコンパイル時の型チェックを混同している。
Ben Voigt 2014年

@Ben Voigt:元の質問と同様に、境界チェックについて話しているだけです。
user34814 2014年

2
@ user34814コンパイル時の境界チェックは、型チェックの範囲内です。いくつかの高水準言語がこの機能を提供しています。
Leushenko 2014年

5

myArrayが少なくとも10の整数の配列を指すことをコンパイラーに伝えるには、次のようにします。

void bar(int myArray[static 10])

優れたコンパイラは、myArray [10]にアクセスすると警告を表示するはずです。「static」キーワードがない場合、10は何も意味しません。


1
11番目の要素にアクセスし、配列に少なくとも 10個の要素が含まれている場合、コンパイラーが警告する必要があるのはなぜですか?
nwellnhof 2014年

おそらくこれは、コンパイラーが少なくとも 10個のエレメントを持っていることのみを強制できるためです。11番目の要素にアクセスしようとすると、それが存在するかどうかは確認できません(存在する場合でも)。
ディランワトソン

2
それは規格の正しい読みではないと思います。[static]あなたがいる場合、コンパイラは警告することができます呼び出し barint[5]内で 何にアクセスできるかは規定していませんbar。責任は完全に呼び出し側にあります。
タブ

3
error: expected primary-expression before 'static'この構文を見たことがない。これが標準のCまたはC ++になることはほとんどありません。
v.oddou 2014年

3
@ v.oddou、それはC99、6.7.5.2および6.7.5.3で指定されています。
Samuel Edwin Ward

5

C ++はCコードを正しくコンパイルすることになっているため、これはCのよく知られた「機能」であり、C ++に渡されます。

問題はいくつかの側面から発生します:

  1. 配列名はポインタと完全に同等であると想定されています。
  2. Cは高速であることが想定されており、元々は「高レベルのアセンブラー」(特に最初の「ポータブルオペレーティングシステム」を書くように設計されている:Unix)として開発されているため、「隠し」コードを挿入することは想定されていません。したがって、実行時の範囲チェックは「禁止」されています。
  3. 静的配列または動的配列(スタック内または割り当て済み)にアクセスするために生成されたマシンコードは、実際には異なります。
  4. 呼び出された関数は、引数として渡された配列の「種類」を認識できないため、すべてがポインタであると見なされ、そのように扱われます。

配列はCでは実際にはサポートされていないと言えます(これについては先ほど言ったように、実際には当てはまりませんが、これは適切な近似です)。配列は実際にはデータのブロックへのポインターとして扱われ、ポインター演算を使用してアクセスされます。Cにはどのような形式のRTTIもないため、関数プロトタイプで配列要素のサイズを宣言する必要があります(ポインター演算をサポートするため)。これは、多次元配列についても「より当てはまる」ものです。

とにかく、上記のすべては本当に本当ではありません:p

最近のほとんどのC / C ++コンパイラは行うサポート境界チェックが、基準は(下位互換性のために)それはデフォルトでオフにする必要があります。たとえば、合理的に最近のバージョンのgccは、「-O3 -Wall -Wextra」を使用してコンパイル時の範囲チェックを実行し、「-fbounds-checking」を使用して実行時の完全な境界チェックを実行します。


たぶん、C ++ 20年前にCコードをコンパイルするはずだったのですが、確かにそうでなく、長い間ありませんでした(少なくともC ++ 98?C99、これは新しいC ++標準によって「修正」されていません)。
hyde 14

@hydeちょっと厳しすぎるようです。Stroustrupを引用するには、「マイナーな例外を除いて、CはC ++のサブセットです。」(C ++ PL第4版、セクション1.2.1)。C ++とCの両方がさらに進化し、最新のCバージョンの機能が存在し、最新のC ++バージョンにはないが、全体的にはStroustrupの見積もりはまだ有効だと思います。
mvw 14

@mvwこのミレニアムで記述されたほとんどのCコードは、互換性のない機能を回避することでC ++互換性を意図的に維持していませんが、C99で指定された初期化構文(struct MyStruct s = { .field1 = 1, .field2 = 2 };)を使用して構造体を初期化します。その結果、ほとんどのCコードは構造体を初期化するため、最新のCコードは標準のC ++コンパイラによって拒否されます。
ハイド14

@mvw C ++はCと互換性があると考えられているため、ある程度の妥協が行われた場合に、CコンパイラとC ++コンパイラの両方でコンパイルされるコードを記述できます。ただし、C ++のサブセットだけでなく、CとC ++の両方のサブセットを使用する必要があります。
hyde 14

@hyde C ++コンパイル可能なCコードの量に驚くでしょう。数年前、Linuxカーネル全体がC ++コンパイル可能でした(それがまだ当てはまるかどうかはわかりません)。C ++コンパイラでCコードを定期的にコンパイルして、優れた警告チェックを取得します。Cモードでコンパイルされるのは、 "最適化"を絞るために "製品"のみです。
ZioByte

3

Cは、唯一のタイプのパラメータを変換しないであろうint[5]*int。宣言をtypedef int intArray5[5];指定すると、型のパラメータもに変換intArray5*intれます。奇妙ではありますが、この動作が役立つ状況がいくつかあります(特に、実装によっては配列としてva_list定義されているで定義されているような場合stdargs.h)。int[5](次元を無視して)として定義された型をパラメーターとして許可int[5]することはできませんが、直接指定することはできません。

配列型のパラメーターのCの処理は不合理であると思いますが、それはアドホック言語を採用する努力の結果であり、その大部分は特に明確に定義されていないか、考え抜かれておらず、行動を考え出そうとしています既存の実装が既存のプログラムに対して行ったことと一致する仕様。その観点から見ると、Cの癖の多くは理にかなっています。特に、それらの多くが発明されたとき、今日知っている言語の大部分はまだ存在していないと考えると、私が理解しているところでは、BCPLと呼ばれるCの前身では、コンパイラーは実際には変数の型を十分に追跡していませんでした。宣言int arr[5];はと同等int anonymousAllocation[5],*arr = anonymousAllocation;でした。割り当てが確保されたら。コンパイラーは、arrポインタまたは配列でした。arr[x]またはのいずれか*arrとしてアクセスされた場合、どのように宣言されたかに関係なく、ポインターと見なされます。


1

まだ答えられていないのは、実際の質問です。

すでに与えられた答えは、配列を値によってCまたはC ++の関数に渡すことはできないことを説明しています。また、宣言されたパラメータint[]はtypeのように扱われint *、typeの変数int[]をそのような関数に渡すことができることも説明しています。

ただし、配列の長さを明示的に指定してもエラーにならない理由は説明されていません。

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

これらのうち最後のものがエラーにならないのはなぜですか?

その理由は、typedefで問題が発生するためです。

typedef int myarray[10];
void f(myarray array);

関数パラメーターで配列の長さを指定するのがエラーである場合、関数パラメーターでmyarray名前を使用することはできません。また、一部の実装はなどの標準ライブラリタイプに配列タイプを使用しva_list、すべての実装はjmp_buf配列タイプを作成する必要があるため、これらの名前を使用して関数パラメーターを宣言する標準的な方法がないと、非常に問題になります。などの機能の移植可能な実装ではありませんvprintf


0

渡された配列のサイズが予期したものと同じであるかどうかをコンパイラが確認できるようになります。そうでない場合、コンパイラは問題を警告することがあります。

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