173

ポインターを渡すのではなく、C で構造体を値で渡すことの欠点はありますか?

構造体が大きい場合、大量のデータをコピーするというパフォーマンス上の側面があることは明らかですが、構造体が小さい場合は、関数にいくつかの値を渡すのと基本的に同じにする必要があります。

戻り値として使用すると、さらに興味深いものになるかもしれません。C では、関数からの戻り値は 1 つしかありませんが、多くの場合、複数の値が必要になります。したがって、簡単な解決策は、それらを構造体に入れて返すことです。

これに賛成または反対の理由はありますか?

ここで私が話していることは誰にも明らかではないかもしれないので、簡単な例を挙げます。

C でプログラミングしている場合、遅かれ早かれ次のような関数を書き始めるでしょう。

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

これは問題ではありません。唯一の問題は、すべての関数で同じ規則を使用するために、パラメーターの順序について同僚と同意する必要があることです。

しかし、同じ種類の情報を返したい場合はどうなるでしょうか? 通常、次のようなものが得られます。

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

これは問題なく機能しますが、はるかに問題があります。戻り値は戻り値ですが、この実装ではそうではありません。上記から、関数 get_data が len が指すものを見ることが許可されていないことを伝える方法はありません。また、値が実際にそのポインターを介して返されることをコンパイラーにチェックさせるものは何もありません。そのため、来月、他の誰かがコードを正しく理解せずに (ドキュメントを読んでいなかったために) コードを変更すると、気付かないうちにコードが壊れたり、ランダムにクラッシュしたりします。

だから、私が提案する解決策は単純な構造体です

struct blob { char *ptr; size_t len; }

例は次のように書き直すことができます。

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

なんらかの理由で、ほとんどの人は本能的に inspect_data に構造体 blob へのポインターを持たせると思いますが、その理由はわかりません。ポインターと整数は引き続き取得されますが、それらが一緒になることははるかに明確です。get_data の場合、長さの入力値がなく、長さが返される必要があるため、前に説明した方法で混乱することはありません。

4

10 に答える 10

221

小さな構造体 (point、rect など) の場合、値渡しは完全に受け入れられます。しかし、速度とは別に、値による大きな構造体の受け渡しに注意する必要があるもう 1 つの理由があります。スタック スペースです。

多くの C プログラミングは、メモリが貴重な組み込みシステム用であり、スタック サイズは KB またはバイト単位で測定される場合があります...構造体を値で渡したり返したりする場合、それらの構造体のコピーが配置されますスタック、このサイトの名前にちなんで名付けられた状況を引き起こす可能性があります...

スタックを過度に使用しているように見えるアプリケーションを見つけた場合、値渡しの構造体は最初に探すものの 1 つです。

于 2008-10-02T11:39:50.777 に答える
66

言及されていないこれを行わない理由の1つは、バイナリ互換性が重要になる問題を引き起こす可能性があることです。

使用するコンパイラーに応じて、コンパイラーのオプション/実装に応じて、構造体をスタックまたはレジスターを介して渡すことができます。

参照: http: //gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-freg-struct-return

2つのコンパイラが一致しない場合、事態は悪化する可能性があります。言うまでもなく、これを行わない主な理由は、スタックの消費とパフォーマンスの理由です。

于 2008-10-03T13:45:10.207 に答える
20

この質問に本当に答えるには、アセンブリの土地を深く掘り下げる必要があります。

(次の例では x86_64 で gcc を使用しています。誰でも MSVC、ARM などの他のアーキテクチャを追加できます。)

サンプルプログラムを見てみましょう:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

完全に最適化してコンパイルする

gcc -Wall -O3 foo.c -o foo

アセンブリを見てください:

objdump -d foo | vim -

これが得られるものです:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

noplパッドを除くと、give_two_doubles()27 バイトgive_point()ですが、29 バイトです。一方、give_point()よりも 1 つ少ない命令が生成されます。give_two_doubles()

興味深いのは、コンパイラがmovより高速な SSE2 バリアントmovapdmovsd. さらに、give_two_doubles()実際にはメモリの内外にデータを移動するため、処理が遅くなります。

明らかに、これらの多くは組み込み環境には適用できない可能性があります (現在、ほとんどの場合、C が活躍の場となっているのは組み込み環境です)。私は組み立てウィザードではないので、どんなコメントでも大歓迎です!

于 2010-07-28T17:18:17.913 に答える
16

ここにいる人々がこれまで言及するのを忘れていた (または私が見落としていた) ことの 1 つは、通常、構造体にはパディングがあるということです!

struct {
  short a;
  char b;
  short c;
  char d;
}

すべての char は 1 バイトで、すべての short は 2 バイトです。構造体の大きさは?いいえ、6 バイトではありません。少なくとも、より一般的に使用されているシステムではそうではありません。ほとんどのシステムでは 8 になります。問題は、アラインメントが一定ではなく、システムに依存することです。そのため、同じ構造体でも異なるシステムでは異なるアラインメントと異なるサイズになります。

パディングがスタックをさらに食い尽くすだけでなく、システムがどのようにパディングするかを知っていない限り、パディングを事前に予測できないという不確実性も追加され、アプリ内のすべての構造体を調べてサイズを計算します。それのための。ポインターを渡すには、予測可能な量のスペースが必要です。不確実性はありません。ポインターのサイズはシステムで認識されており、構造体がどのように見えるかに関係なく、常に同じであり、ポインターのサイズは常に整列され、パディングを必要としない方法で選択されます。

于 2008-10-02T13:04:04.383 に答える
15

簡単な解決策は、エラー コードを戻り値として返し、その他すべてを関数のパラメーターとして返すことです。
このパラメーターはもちろん構造体にすることができますが、これを値で渡すことに特別な利点は見られず、ポインターを送信しただけです。
構造体を値で渡すのは危険です。何を渡すのかには非常に注意する必要があります。C にはコピー コンストラクターがないことを覚えておいてください。構造体パラメーターの 1 つがポインターの場合、ポインター値がコピーされます。維持。

答えを完成させるために(Roddyへの完全な功績)、スタックの使用は構造を値で渡さないもう1つの理由です。スタックオーバーフローのデバッグは本当のPITAだと思います。

リプレイしてコメント:

構造体をポインターで渡すということは、一部のエンティティがこのオブジェクトの所有権を持ち、何をいつ解放する必要があるかを完全に把握していることを意味します。構造体を値で渡すと、構造体の内部データへの非表示の参照 (別の構造体へのポインターなど) が作成されます。これを維持するのは困難です (可能ですが、なぜですか?)。

于 2008-10-02T11:35:22.863 に答える
10

これは誰も言及していないものです:

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

a のメンバーは ですconst structconst、そのメンバーが ( のようなchar *)ポインターであるchar *const場合、実際に必要な ではなくconst char *なります。もちろん、これconstは意図の文書化であり、これに違反する人は誰でも悪いコードを書いていると想定できます (実際にそうです)。クラッシュ)。

別の方法として、a を作成してそれを使用することもできますが、これはややこしいことです。これは、 ing ポインターstruct const_blob { const char *c; size_t l }で発生したのと同じ命名方式の問題に陥ります。typedefしたがって、ほとんどの人は 2 つのパラメーターのみを使用することに固執します (または、この場合は文字列ライブラリを使用する可能性が高くなります)。

于 2011-09-26T04:47:00.483 に答える
9

パラメータと戻り値の両方として、(大きすぎない) 構造体を値で渡すことは、完全に正当な手法だと思います。もちろん、構造体が POD 型であるか、コピー セマンティクスが適切に指定されているかのいずれかに注意する必要があります。

更新: 申し訳ありませんが、私は C++ の思考を制限していました。関数から構造体を返すことが C で合法ではなかった時代を思い出しますが、これはおそらくその後変更されています。使用する予定のすべてのコンパイラがプラクティスをサポートしている限り、有効であると言えます。

于 2008-10-02T11:24:36.837 に答える
9

あなたの質問は物事をかなりうまくまとめていると思います。

構造体を値渡しするもう 1 つの利点は、メモリの所有権が明示的であることです。構造体がヒープからのものであるかどうか、およびそれを解放する責任を誰が持っているかについては、何の疑問もありません。

于 2008-10-02T11:26:20.653 に答える
5

http://www.drpaulcarter.com/pcasm/の PC Assembly Tutorial の 150 ページには、C で関数が構造体を返す方法が明確に説明されています。

C では、構造体型を関数の戻り値として使用することもできます。明らかに、EAX レジスターで構造体を返すことはできません。この状況の処理方法は、コンパイラによって異なります。コンパイラが使用する一般的な解決策は、構造体ポインターをパラメーターとして受け取る関数として関数を内部的に書き換えることです。ポインターは、呼び出されたルーチンの外部で定義された構造体に戻り値を入れるために使用されます。

上記のステートメントを確認するために、次の C コードを使用します。

struct person {
    int no;
    int age;
};

struct person create() {
    struct person jingguo = { .no = 1, .age = 2};
    return jingguo;
}

int main(int argc, const char *argv[]) {
    struct person result;
    result = create();
    return 0;
}

「gcc -S」を使用して、この C コードのアセンブリを生成します。

    .file   "foo.c"
    .text
.globl create
    .type   create, @function
create:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %ecx
    movl    $1, -8(%ebp)
    movl    $2, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -4(%ebp), %edx
    movl    %eax, (%ecx)
    movl    %edx, 4(%ecx)
    movl    %ecx, %eax
    leave
    ret $4
    .size   create, .-create
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    leal    -8(%ebp), %eax
    movl    %eax, (%esp)
    call    create
    subl    $4, %esp
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

create を呼び出す前のスタック:

        +---------------------------+
ebp     | saved ebp                 |
        +---------------------------+
ebp-4   | age part of struct person | 
        +---------------------------+
ebp-8   | no part of struct person  |
        +---------------------------+        
ebp-12  |                           |
        +---------------------------+
ebp-16  |                           |
        +---------------------------+
ebp-20  | ebp-8 (address)           |
        +---------------------------+

create を呼び出した直後のスタック:

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+
于 2011-04-21T15:19:06.450 に答える