3
#include <iostream> 
class B { 
public: 
 B () : b(bCounter++) {} 
int b; 
static int bCounter;  
}; 
int B::bCounter = 0; 
class D : public B { 
public: 
D () : d(bCounter) {} 
int d; 
}; 
const int N = 10; 
B arrB[N]; 
D arrD[N]; 
int sum1 (B* arr) { 
    int s = 0; 
    for (int i=0; i<N; i++) 
         s+=arr[i].b; 
    return s; 
} 
int sum2 (D* arr) { 
     int s = 0; 
     for (int i=0; i<N; i++) s+=arr[i].b+arr[i].d; 
     return s; 
} 
int main() { 
    std::cout << sum1(arrB) << std::endl; 
    std::cout << sum1(arrD) << std::endl; 
    std::cout << sum2(arrD) << std::endl; 
    return 0; 
}

問題は main 関数の 2 行目です。sum1() 関数が引数 arrD(Derived クラス オブジェクトの配列) で呼び出された場合、単に D::d を「切り取る」と予想していましたが、この場合は arrD の順序を再配置し、合計は次のようになります: 10+11+11+12+12+13+13+14+14+15 arrD[i] の b フィールドと d フィールドが交互になっているようで、b フィールドのみを合計する必要があります。 . 誰かが理由を説明してもらえますか? 前もって感謝します。

4

3 に答える 3

7

あなたは不運にも、完全に無効なコードをコンパイルできる型システムのスイート スポットの 1 つに到達しました。

この関数int sum1 (B* arr)B、署名に従って引数としてオブジェクトへのポインターを取りますが、意味的には、実際にはオブジェクトの配列Bへのポインターを取ります。呼び出すときは、オブジェクトの配列ではなくオブジェクトの配列sum1(arrD)渡すことで、その契約に違反しています。それらはどのように異なりますか?ポインタ演算はポインタの型のサイズに基づいて行われ、オブジェクトとオブジェクトはサイズが異なります。BDBD

の配列はの配列でDはありませんB

一般に、派生型のコンテナーは基本型のコンテナーではありません。考えてみると、 のコンテナの契約はオブジェクトをD保持することですが、 のコンテナが のコンテナである場合、オブジェクトを追加できます (引数が拡張されている場合は、オブジェクトの追加-- これも !から派生)。DDBBD1B

生の配列の代わりに高次の構造を使用していた場合、コンパイラのように、 aの代わりに astd::vectorを渡すことはブロックされていましたが、配列の場合はなぜ停止しなかったのでしょうか?std::vector<D>std::vector<B>

の配列が の配列でDない場合B、プログラムがコンパイルされたのはなぜですか?

これに対する答えは、C++ よりも前のものです。C では、関数へのすべての引数は値によって渡されます。pass-by-pointerもできると考える人もいますが、それはポインタを値渡しするだけです。しかし、配列は大きく、配列を値で渡すのは非常にコストがかかります。同時に、メモリを動的に割り当てるときはポインタを使用しますが、概念的には、10 を malloc するときは配列int割り当てています。int. C 言語の設計者はこれを考慮し、値渡しのルールに例外を設けました。配列を値渡ししようとすると、最初の要素へのポインターが取得され、そのポインターが配列の代わりに渡されます (同様のルールが関数に対して存在する場合、関数をコピーすることはできないため、関数を渡すと暗黙的に関数へのポインターが取得され、代わりにそれが渡されます)。最初から同じルールが C++ にありました。

さて、次の問題は、型システムが要素へのポインターと配列の一部である要素へのポインターを区別しないことです。そして、これには結果があります。は のベースであるため、オブジェクトへのポインタは へDのポインタに暗黙的に変換できます。OO プログラミングのオブジェクト全体は、ベース オブジェクトの代わりに派生型を使用できます (つまり、ポリモーフィズムの目的で)。BBD

元のコードに戻ると、 を書くsum1( arrD )と、がrvaluearrDとして使用されます。これは、配列が最初の要素へのポインターに減衰することを意味するため、効果的に に変換されます。部分式はポインターであり、ポインターは単なるポインターです...へのポインターを受け取り、 へのポインターは暗黙的に へのポインターに変換可能であるため、コンパイラーは喜んでその変換を行います: . 関数がポインターを取得して単一の要素として使用した場合、 aの代わりに aを渡すことができるので問題ありませんが、コンパイラーが渡すことを許可したとしても、の配列は ...の配列ではありません。そのままです。sum1( &arrD[0] )&arrD[0]sum1BDBsum1( static_cast<B*>(&arrD[0]) )DBDB

于 2012-08-22T00:46:40.707 に答える
2

あなたarrはタイプですB*、これが意味するのはそれであるarr[i]か、メモリ内で(arr + i)前進するでしょう。sizeof(B) * iメモリは次のようになります。

10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20

そして、forループは追加します

10 11 11 12 12 13 13 14 14 15

sizeof(D) * iこれは、あなたが望むように前進するのではなく、まさにメモリの最初の要素です。

于 2012-08-21T23:25:11.453 に答える
2

のサイズは のサイズBよりも小さいですD。したがって、いつポインタをsum1反復処理しているかは、配列の 2 番目の要素であると見なされているものを指しています。これは、実際には 1 番目の要素の真ん中にあります。arrarr[1]BD

したがって(パディングarrDがない場合)、次のようなレイアウトになります。

arrD: | 2 ints    | 2 ints    | 2 ints    | ...

ただし、a を設定するB *arrと、sum1B の配列であると見なされます。したがってsum1、パラメーターには次のようなレイアウトがあると見なされます。

arr:  | int | int | int | int | int | int | ...

つまり、arr[1]実際には のdメンバーですarrD[0]

于 2012-08-21T23:17:40.453 に答える