Trả về một `struct` từ một hàm trong C


171

Hôm nay tôi đã dạy cho một vài người bạn cách sử dụng C structs. Một trong số đó hỏi nếu bạn có thể trả về một structtừ một chức năng, mà tôi trả lời: "Không Bạn muốn trở gợi ý để tự động malloced struct. S thay vì"

Đến từ một người chủ yếu làm C ++, tôi đã hy vọng không thể trả về structs theo các giá trị. Trong C ++, bạn có thể quá tải operator =cho các đối tượng của mình và hoàn toàn có ý nghĩa để có một hàm trả về đối tượng của bạn theo giá trị. Tuy nhiên, trong C, bạn không có tùy chọn đó và vì vậy nó khiến tôi suy nghĩ trình biên dịch đang thực sự làm gì. Hãy xem xét những điều sau đây:

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;
}    

Tôi hiểu tại sao struct b = a;không nên hoạt động - bạn không thể quá tải operator =cho loại dữ liệu của mình. Làm thế nào mà nó a = foo();biên dịch tốt? Nó có nghĩa gì khác struct b = a;không? Có lẽ câu hỏi cần đặt ra là: Chính xác thì returntuyên bố kết hợp để =ký là gì?

[sửa]: Ok, tôi chỉ là struct b = a là một lỗi cú pháp - điều đó đúng và tôi là một thằng ngốc! Nhưng điều đó làm cho nó thậm chí còn phức tạp hơn! Sử dụng struct MyObj b = athực sự làm việc! Tôi đang thiếu gì ở đây?


23
struct b = a;là một lỗi cú pháp. Nếu bạn cố gắng struct MyObj b = a;thì sao?
Greg Hewgill

2
@GregHewgill: Bạn hoàn toàn đúng. Tuy nhiên, khá thú vị là struct MyObj b = a;dường như không hoạt động :)
mmirzadeh

Câu trả lời:


199

Bạn có thể trả về một cấu trúc từ một hàm (hoặc sử dụng =toán tử) mà không gặp vấn đề gì. Đó là một phần được xác định rõ của ngôn ngữ. Vấn đề duy nhất struct b = alà bạn đã không cung cấp một loại hoàn chỉnh. struct MyObj b = asẽ làm việc tốt Bạn cũng có thể truyền các cấu trúc cho các hàm - một cấu trúc hoàn toàn giống với bất kỳ loại tích hợp nào cho các mục đích truyền tham số, trả về giá trị và gán.

Đây là một chương trình trình diễn đơn giản thực hiện cả ba - truyền cấu trúc làm tham số, trả về cấu trúc từ hàm và sử dụng cấu trúc trong các câu lệnh gán:

#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;
}

Ví dụ tiếp theo khá giống nhau, nhưng sử dụng kiểu dựng sẵn intcho mục đích trình diễn. Hai chương trình có cùng một hành vi liên quan đến giá trị truyền qua cho việc truyền tham số, gán, v.v.:

#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
Điều đó khá thú vị. Tôi luôn có ấn tượng bạn cần con trỏ cho những điều này. Tôi đã sai :)
mmirzadeh

8
Bạn chắc chắn không cần con trỏ. Điều đó nói rằng, hầu hết thời gian bạn muốn sử dụng chúng - các bản sao bộ nhớ ngầm diễn ra các cấu trúc xung quanh theo giá trị có thể là một sự lãng phí thực sự của các chu kỳ CPU, chưa kể đến băng thông bộ nhớ.
Carl Norum

10
@CarlNorum một cấu trúc phải lớn đến mức nào mà một bản sao có giá cao hơn malloc + miễn phí?
josefx

7
@josefx, một bản sao? Có lẽ là rất lớn. Vấn đề là, thông thường nếu bạn chuyển các cấu trúc xung quanh theo giá trị thì bạn đang sao chép chúng rất nhiều . Dù sao nó không thực sự đơn giản như thế. Bạn có thể đi qua các cấu trúc địa phương hoặc toàn cầu, trong trường hợp đó, chi phí phân bổ của bạn khá miễn phí.
Carl Norum

7
Bạn cần con trỏ và phân bổ bộ nhớ cho giá trị được trả về bên ngoài thân hàm ngay khi lượng bộ nhớ được phân bổ cho một giá trị không được biết đến tại thời điểm biên dịch. Nó dành cho các cấu trúc, vì vậy các hàm C không có vấn đề gì khi trả về chúng.
Revierpost

33

Khi thực hiện một cuộc gọi như a = foo();, trình biên dịch có thể đẩy địa chỉ của cấu trúc kết quả trên ngăn xếp và chuyển nó dưới dạng con trỏ "ẩn" cho foo()hàm. Thực tế, nó có thể trở thành một cái gì đó như:

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

foo(&a);

Tuy nhiên, việc thực hiện chính xác điều này phụ thuộc vào trình biên dịch và / hoặc nền tảng. Như Carl Norum lưu ý, nếu cấu trúc đủ nhỏ, nó thậm chí có thể được chuyển trở lại hoàn toàn trong một thanh ghi.


11
Đó là hoàn toàn phụ thuộc thực hiện. Ví dụ, armcc sẽ vượt qua các cấu trúc đủ nhỏ trong các thanh ghi chuyển tham số thông thường (hoặc giá trị trả về).
Carl Norum

Điều đó sẽ không trả lại một con trỏ đến một biến cục bộ? Bộ nhớ cho cấu trúc được trả về không thể là một phần của fookhung stack. Nó phải ở một nơi còn sót lại sau khi trở về foo.
Anders Abel

@AndersAbel: Tôi nghĩ ý nghĩa của Greg là trình biên dịch sẽ đưa một con trỏ tới biến trong hàm chính và truyền nó cho hàm foo. Bên trong chức năng foo, bạn chỉ cần thực hiện nhiệm vụ
mmirzadeh

4
@AndersAbel: *r = aCuối cùng (một cách hiệu quả) sẽ thực hiện một bản sao của biến cục bộ sang biến của trình gọi. Tôi nói "hiệu quả" bởi vì trình biên dịch có thể thực hiện RVO và loại bỏ ahoàn toàn biến cục bộ .
Greg Hewgill

3
Mặc dù điều này không trả lời trực tiếp câu hỏi, nhưng đây là lý do tại sao nhiều người sẽ rơi vào đây thông qua google c return struct: họ biết rằng trong cdecl eaxđược trả về theo giá trị và nói chung các cấu trúc không phù hợp với bên trong eax. Đây là những gì tôi đang tìm kiếm.
Ciro Santilli 郝海东 冠状 病 事件

14

Các struct bdòng không làm việc vì đó là một lỗi cú pháp. Nếu bạn mở rộng nó ra để bao gồm loại nó sẽ hoạt động tốt

struct MyObj b = a;  // Runs fine

Những gì C đang làm ở đây về cơ bản là memcpytừ cấu trúc nguồn đến đích. Điều này đúng cho cả việc gán và trả về các structgiá trị (và thực sự là mọi giá trị khác trong C)


+1, trên thực tế, nhiều trình biên dịch sẽ thực sự phát ra một cuộc gọi theo nghĩa đen memcpytrong trường hợp này - ít nhất, nếu cấu trúc có kích thước hợp lý.
Carl Norum

Vì vậy, trong quá trình khởi tạo một kiểu dữ liệu, hàm memcpy hoạt động ??
bhuwansahni

1
@bhuwansahni Tôi không chắc bạn đang hỏi gì ở đây. Bạn có thể xây dựng một chút?
JaredPar

4
@JaredPar - trình biên dịch thường theo nghĩa đen gọi các memcpychức năng cho các tình huống cấu trúc. Bạn có thể thực hiện một chương trình thử nghiệm nhanh và xem GCC làm điều đó, ví dụ. Đối với các loại tích hợp sẽ không xảy ra - chúng không đủ lớn để kích hoạt loại tối ưu hóa đó.
Carl Norum

3
Chắc chắn có thể thực hiện được - dự án tôi đang thực hiện không có memcpybiểu tượng được xác định, vì vậy chúng tôi thường gặp phải lỗi liên kết "biểu tượng không xác định" khi trình biên dịch quyết định tự mình thực hiện.
Carl Norum

9

vâng, có thể chúng ta cũng có thể vượt qua cấu trúc và trả về cấu trúc. Bạn đã đúng nhưng thực tế bạn không vượt qua kiểu dữ liệu giống như cấu trúc này MyObj b = a.

Thật ra tôi cũng đã biết khi tôi đang cố gắng tìm ra một giải pháp tốt hơn để trả về nhiều hơn một giá trị cho hàm mà không cần sử dụng con trỏ hoặc biến toàn cục.

Bây giờ dưới đây là ví dụ cho cùng, tính toán độ lệch của điểm trung bình của một học sinh.

#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

Theo như tôi có thể nhớ, các phiên bản đầu tiên của C chỉ được phép trả về giá trị có thể vừa với thanh ghi bộ xử lý, điều đó có nghĩa là bạn chỉ có thể trả về một con trỏ cho một cấu trúc. Các hạn chế tương tự được áp dụng cho các đối số chức năng.

Các phiên bản gần đây hơn cho phép vượt qua các đối tượng dữ liệu lớn hơn như cấu trúc. Tôi nghĩ rằng tính năng này đã phổ biến trong những năm tám mươi hoặc đầu những năm chín mươi.

Mảng, tuy nhiên, vẫn có thể được thông qua và trả về chỉ là con trỏ.


Bạn có thể trả về một mảng theo giá trị nếu bạn đặt nó bên trong một cấu trúc. Những gì bạn không thể trả về theo giá trị là một mảng có độ dài thay đổi.
han

1
Có, tôi có thể đặt một mảng bên trong một cấu trúc, nhưng tôi không thể ví dụ viết typedef char Array [100]; Array foo () {...} Một mảng không thể được trả về, ngay cả khi kích thước đã biết.
Giorgio

Downvoter có thể giải thích lý do của downvote? Nếu câu trả lời của tôi chứa thông tin không chính xác, tôi sẽ vui lòng sửa nó.
Giorgio

4

Bạn có thể gán cấu trúc trong C.a = b; là cú pháp hợp lệ.

Bạn chỉ cần bỏ đi một phần của loại - thẻ struct - trong dòng của bạn không hoạt động.


4

Không có vấn đề trong việc trả lại một cấu trúc. Nó sẽ được thông qua bởi giá trị

Nhưng, nếu struct chứa bất kỳ thành viên nào có địa chỉ của biến cục bộ

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);
}

Bây giờ, ở đây e1.name chứa một địa chỉ bộ nhớ cục bộ cho hàm get (). Khi get () trả về, địa chỉ tên cục bộ sẽ được giải phóng. Vì vậy, trong trình gọi nếu chúng tôi cố gắng truy cập địa chỉ đó, nó có thể gây ra lỗi phân đoạn, vì chúng tôi đang thử một địa chỉ được giải phóng. Thật tệ..

Khi e1.id sẽ hoàn toàn hợp lệ vì giá trị của nó sẽ được sao chép vào e2.id

Vì vậy, chúng ta nên luôn luôn cố gắng tránh trả về địa chỉ bộ nhớ cục bộ của hàm.

Bất cứ điều gì malloced có thể được trả lại như và khi muốn


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);
}

hoạt động tốt với các phiên bản mới hơn của trình biên dịch. Giống như id, nội dung của tên được sao chép vào biến cấu trúc được gán.


1
Thậm chí đơn giản hơn: struct emp get () {return {100, "john"}; }
Chris Reid

1

địa chỉ struct var e2 được đẩy dưới dạng arg sang callee stack và các giá trị được gán ở đó. Trong thực tế, get () trả về địa chỉ của e2 trong reg reg. Điều này hoạt động như cuộc gọi bằng cách tham khảo.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.