Cの関数から `struct`を返します


171

今日、私は2人の友人にC structの使い方を教えていました。それらの1つはstruct、関数からを返すことができるかどうかを尋ねましたが、「いいえ!代わりに、動的にmalloc編集されたstructs へのポインターを返します。」

主にC ++を行う人から来て、struct値でs を返すことができないと思っていました。C ++ではoperator =、オブジェクトのをオーバーロードでき、値によってオブジェクトを返す関数を持つことは完全に理にかなっています。ただし、Cではそのオプションがないため、コンパイラーが実際に何をしているのかを考えさせられました。以下を検討してください。

struct MyObj{
    double x, y;
};

struct MyObj foo(){
    struct MyObj a;

    a.x = 10;
    a.y = 10;

    return a;
}        

int main () {

    struct MyObj a;

    a = foo();    // This DOES work
    struct b = a; // This does not work

    return 0;
}    

なぜstruct b = a;動作しないのか理解しています- operator =データ型のオーバーロードはできません。a = foo();うまくコンパイルできるのはなぜですか?それ以外の意味struct b = a;ですか?多分尋ねるべき質問は次のとおりです:署名returnに関連する声明は正確に何を=しますか?

[編集]:さて、指摘されたのstruct b = aは構文エラーです-それは正しいです、私はばかです!しかし、それはそれをさらに複雑にします!使用struct MyObj b = aは確かに機能します!ここで何が欠けていますか?


24
struct b = a;構文エラーです。あなたがしようとした場合はどうなりますstruct MyObj b = a;か?
Greg Hewgill、2012年

2
@GregHewgill:あなたはまったく正しい。しかし、興味深いことに、struct MyObj b = a;機能するようです:)
mmirzadeh

回答:


200

関数から構造体を返す(または=演算子を使用する)ことができます。それは言語の明確に定義された部分です。唯一の問題struct b = aは、完全な型を提供しなかったことです。 struct MyObj b = aうまくいきます。構造体関数に渡すこともできます。構造体は、パラメーターの受け渡し、戻り値、および割り当ての目的で、組み込み型とまったく同じです。

3つすべてを実行する簡単なデモプログラムを次に示します。構造体をパラメータとして渡し、関数から構造体を返し、構造体を代入ステートメントで使用します。

#include <stdio.h>

struct a {
   int i;
};

struct a f(struct a x)
{
   struct a r = x;
   return r;
}

int main(void)
{
   struct a x = { 12 };
   struct a y = f(x);
   printf("%d\n", y.i);
   return 0;
}

次の例はほとんど同じですがint、デモ目的で組み込み型を使用しています。2つのプログラムは、パラメーターの受け渡しや割り当てなどの値渡しの点で同じ動作をします。

#include <stdio.h>

int f(int x) 
{
  int r = x;
  return r;
}

int main(void)
{
  int x = 12;
  int y = f(x);
  printf("%d\n", y);
  return 0;
}

14
面白いですね。私はいつもあなたがこれらのための指針を必要とするという印象の下にありました。私は間違っていました:)
mmirzadeh

8
確かにポインタは必要ありません。つまり、ほとんどの場合、それらを使用する必要があります-値によってフリング構造が発生する暗黙的なメモリコピーは、メモリ帯域幅は言うまでもなく、CPUサイクルの無駄になります。
Carl Norum

10
@CarlNorumコピーがmalloc + freeよりもコストがかかることを構造が取得するにはどのくらいの大きさが必要ですか?
josefx 2012年

7
@josefx、単一のコピー?おそらく巨大です。問題は、通常、値で構造体を渡す場合は、それらを大量にコピーすることです。とにかくそれはそれほど単純ではありません。ローカルまたはグローバルな構造を迂回している可能性があります。その場合、割り当てコストはほとんど無料です。
Carl Norum、2012年

7
コンパイル時に値に割り当てられたメモリの量がわからない場合は、関数本体の外で返される値のポインタとメモリの割り当てが必要です。これは構造体用なので、C関数は問題なく戻ります。
reinierpost 2015

33

などの呼び出しを行うとa = foo();、コンパイラは結果構造体のアドレスをスタックにプッシュし、それをfoo()関数への「隠し」ポインタとして渡すことがあります。効果的には、次のようになります。

void foo(MyObj *r) {
    struct MyObj a;
    // ...
    *r = a;
}

foo(&a);

ただし、これの正確な実装は、コンパイラやプラットフォームによって異なります。Carl Norumが指摘しているように、構造が十分に小さい場合は、レジスタで完全に渡されることさえあります。


11
それは完全に実装依存です。たとえば、armccは、通常のパラメータ渡し(または戻り値)レジスタで十分に小さい構造体を渡します。
Carl Norum

それはローカル変数へのポインターを返しませんか?返された構造体のメモリをfooスタックフレームの一部にすることはできません。の帰還後も存続できる場所でなければなりませんfoo
アンダースアベル

@AndersAbel:Gregが意味するところは、コンパイラがメイン関数の変数へのポインタを受け取り、それを関数に渡すことだと私は思いますfoo。関数内でfooは、割り当てを行うだけです
mmirzadeh

4
@AndersAbel:*r = a最後に、ローカル変数を呼び出し側の変数に(事実上)コピーします。コンパイラがRVOを実装してローカル変数をa完全に削除する可能性があるため、「効果的に」と言います。
Greg Hewgill、2012年

3
これは直接質問に答えるものではありませんが、これが多くの人々がgoogleを介してここに落ちる理由c return structです。彼らはcdecl eaxが値によって返され、構造体は一般に内部に収まらないことを知っていeaxます。これは私が探していたものです。
Ciro Santilli郝海东冠状病六四事件法轮功

14

struct bそれは構文エラーだから行は動作しません。タイプを含めるように展開すると、うまく機能します

struct MyObj b = a;  // Runs fine

ここでCが行っていることは、本質的にmemcpyはソース構造体から宛先への構造です。これは、struct値の代入と戻りの両方に当てはまります(実際には、Cの他のすべての値)。


+1、実際、多くのコンパイラはmemcpy、この場合、実際にはへのリテラル呼び出しを発行します-少なくとも、構造がかなり大きい場合。
Carl Norum、2012年

したがって、データ型の初期化中に、memcpy関数は機能しますか?
bhuwansahni 2012年

1
@bhuwansahniここで何を求めているのかよくわかりません。もう少し詳しく説明してもらえますか?
JaredPar

4
@JaredPar -コンパイラはしばしばん文字通り呼び出すmemcpy構造の状況で機能します。たとえば、簡単なテストプログラムを作成して、GCCがそれを実行するのを確認できます。発生しない組み込み型の場合-それらは、その種の最適化をトリガーするのに十分な大きさではありません。
Carl Norum、2012年

3
私が取り組んでいるプロジェクトにはmemcpyシンボルが定義されていないので、それを実現することは間違いなく可能です。そのため、コンパイラーが独自に1つを吐き出すと、「未定義のシンボル」リンカーエラーが発生することがよくあります。
Carl Norum、2012年

9

はい、構造体を渡して構造体を返すことも可能です。あなたは正しかったが、実際にはこの構造体MyObj b = aのようなデータ型を渡さなかった。

実際、ポインタやグローバル変数を使用せずに、関数に対して複数の値を返すより良い解決策を見つけようとしたときも知りました

次に、同じ例を示します。平均値に関する生徒の成績の偏差を計算します。

#include<stdio.h>
struct marks{
    int maths;
    int physics;
    int chem;
};

struct marks deviation(struct marks student1 , struct marks student2 );

int main(){

    struct marks student;
    student.maths= 87;
    student.chem = 67;
    student.physics=96;

    struct marks avg;
    avg.maths= 55;
    avg.chem = 45;
    avg.physics=34;
    //struct marks dev;
    struct marks dev= deviation(student, avg );
    printf("%d %d %d" ,dev.maths,dev.chem,dev.physics);

    return 0;
 }

struct marks deviation(struct marks student , struct marks student2 ){
    struct marks dev;

    dev.maths = student.maths-student2.maths;
    dev.chem = student.chem-student2.chem;
    dev.physics = student.physics-student2.physics; 

    return dev;
}

5

私の知る限り、Cの最初のバージョンでは、プロセッサレジスタに収まる値しか返せませんでした。つまり、構造体へのポインタしか返せませんでした。関数の引数に適用される同じ制限。

最近のバージョンでは、構造体などのより大きなデータオブジェクトを渡すことができます。この機能は80年代または90年代前半にはすでに一般的だったと思います。

ただし、配列は引き続きポインターとしてのみ受け渡しできます。


構造体内に配置すると、値で配列を返すことができます。値で返すことができないのは可変長配列です。
2012年

1
はい、構造体の中に配列を入れることはできますが、たとえばtypedef char arr [100]と書くことはできません。arr foo(){...}サイズがわかっていても、配列を返すことはできません。
ジョルジオ

反対投票者は反対投票の理由を説明できますか?私の回答に誤った情報が含まれている場合は、修正させていただきます。
ジョルジオ

4

君は Ca = b; で構造体を割り当てるができます。これは有効な構文です。

機能しないタイプの一部(structタグ)を行に残さないだけです。


4

構造体を返すことに問題はありません。値で渡されます

しかし、構造体にローカル変数のアドレスを持つメンバーが含まれている場合はどうなりますか

struct emp {
    int id;
    char *name;
};

struct emp get() {
    char *name = "John";

    struct emp e1 = {100, name};

    return (e1);
}

int main() {

    struct emp e2 = get();

    printf("%s\n", e2.name);
}

ここで、ここでe1.nameには、関数get()にローカルなメモリアドレスが含まれています。get()が戻ると、nameのローカルアドレスは解放されます。SO、呼び出し側でそのアドレスにアクセスしようとすると、解放されたアドレスを試しているため、セグメンテーション違反が発生する可能性があります。それは悪いです。

e1.idは、値がe2.idにコピーされるため、完全に有効です。

そのため、関数のローカルメモリアドレスを返さないようにする必要があります。

mallocされたものは、必要なときにいつでも返すことができます。


2
struct emp {
    int id;
    char *name;
};

struct emp get() {
    char *name = "John";

    struct emp e1 = {100, name};

    return (e1);
}

int main() {

    struct emp e2 = get();

    printf("%s\n", e2.name);
}

新しいバージョンのコンパイラで正常に動作します。idと同じように、名前の内容は割り当てられた構造変数にコピーされます。


1
さらにシンプル:struct emp get(){return {100、 "john"}; }
クリス・リード

1

struct var e2アドレスが引数として呼び出し先スタックにプッシュされ、そこに値が割り当てられます。実際、get()はe2のアドレスをeax regに返します。これは、参照渡しのように機能します。

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