web-dev-qa-db-ja.com

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

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

_#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]_は何もしないようで、で動作しません
all、私は_a[2]_を使用できるため。 _int a[]_または_char *a_と同じです。配列名はポインターであり、配列の伝達方法を知っているので、私のパズルはこの部分に関するものではありません。

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

129
Fanl

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

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

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

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

152
M.M

最初の次元の長さは無視されますが、コンパイラーがオフセットを正しく計算できるようにするには、追加の次元の長さが必要です。次の例では、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])は、sizeof(int) == 4であるマシン上で_80_を返します。ただし、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.
_

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

142
pat

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

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

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

  • 参照を使用する
  • std::arrayを使用(C++ 11以降)

参照資料

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

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

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

これは うまく動作する だけでなく、 配列の次元を強制する にもなります。

templatesを使用して上記のコードを作成することもできます generic

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

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

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

const修飾子を適用することにより、 可能な変更を防ぐ になります。


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

上記の構文をIいものと不必要なものの両方であると考える場合、私はそれを缶に入れて、代わりに 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 を使用してください。

33
Shoe

これは[〜#〜] c [〜#〜]の楽しい機能です。この機能を使用すると、自分が傾いている場合に効果的に自分の足で撃つことができます。

その理由は、[〜#〜] c [〜#〜]がアセンブリ言語の1ステップ上にあるためだと思います。 サイズチェックおよび同様の安全性機能が削除され、ピークパフォーマンスが可能になりました。これはプログラマーにとっては悪いことではありません非常に勤勉です。

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

8
bill

まず、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言語に素晴らしい機能を追加します。

6
user34814

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

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

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

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

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

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

5
ZioByte

MyArrayが少なくとも10のintの配列を指していることをコンパイラーに伝えるには:

void bar(int myArray[static 10])

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

5
gnasher729

Cはint[5]型のパラメーターを*intに変換するだけではありません。宣言typedef int intArray5[5];が与えられると、タイプintArray5のパラメーターも*intに変換されます。この振る舞いは奇妙ではありますが、いくつかの状況があります(特に、一部の実装が配列として定義するva_listで定義されるstdargs.hのような場合)。 int[5](ディメンションを無視)として定義された型をパラメーターとして許可することは非論理的ですが、int[5]を直接指定することはできません。

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

3
supercat

まだ回答されていないことの1つは、実際の質問です。

既に与えられた答えは、配列をCまたはC++の関数に値で渡すことはできないことを説明しています。また、int[]として宣言されたパラメーターはint *型を持つものとして扱われ、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などの関数の移植可能な実装ができません。

1
user743382