函数返回结构体的内幕

在刚接触C语言编程时,无论是前辈还是教科书,都反复告诫我们两件事:

①函数的参数是值传递(意味着在函数中对参数本身的修改无法“传回”);

②不要返回函数体内局部变量的地址,因为函数结束时栈会回收,局部变量也随之销毁(如果局部变量为类对象,其析构函数会被自动调用),但可以返回局部变量本身。

 

那么如果一个函数的返回值为结构体类型,其返回值是如何“返回”的呢?

是通过“值传递”吗?我们知道函数参数的值传递,就是将参数入栈(因此参数有自己的一份拷贝,函数中对参数的操作其实操作的就是这份拷贝)。而函数结束时要回收堆栈,那么返回值如何进行“值传递”呢?

是通过寄存器吗?我们知道,对X86来说,一般情况下,函数的返回值通过eax(或者以及edx)返回,但结构体的size太大,无法通过寄存器来返回。

 

这两种方法都行不通,那只能曲线救国了。

下文通过分析div()函数的汇编代码,来看看如何曲线救国。

 

1.函数原型

<stdlib.h>
div_t div(int num,int denom);

返回值为结构体类型(包含商和余数)

typedef struct {
    int  quot;
    int  rem;
} div_t;

div()函数的实现如下:

div_t div(int num, int denom)
{
    div_t r;
 
    r.quot =num / denom;
    r.rem =num % denom;
    if (num>= 0 && r.rem < 0) {
       r.quot++;
       r.rem-= denom;
    }
    return(r);
}


2.汇编代码分析

我们来写一段代码,调用div()函数,如下:

void testdiv()
{
    ...
    int iMinutes = 1000;
    const intMinutesPerHour = 60;
    div_t HourAndMinutes = div(iMinutes, MinutesPerHour);
    ...
}

来看看对应的汇编代码:  

    ;...
    mov     [esp+1CH], 3E8h        ;[esp+1CH] = 1000  即iMinutes
    mov     [esp+18H], 3Ch         ;[esp+18H] =60  即MinutesPerHour
    lea     eax, [esp+1Ch]         ;eax = esp + 1CH
    mov     [esp+8h], 3Ch           ;[esp + 8] = 3CH  第3个参数 即60
    mov     edx, [esp+2Ch+var_10]   ;edx = [esp + 1CH]  即1000
    mov     [esp+2Ch+denom], edx    ;[esp + 4] = 1000 即第2个参数 iMinutes
    mov     [esp+2Ch+numer], eax    ;[esp] = esp + 1CH 即存放结果结构体的地址
    call    _div
    sub     esp, 4                  ;div通过retn 4返回,栈平衡被破坏,所以这里调整下esp
    ;...

函数的参数从右至左依次入栈。在call _div指令之前,[esp]中存放函数的第一参数,但这里并不是iMinutes(1000)而是testdiv()函数堆栈中某个地址,而原本是第一参数的iMinutes和第二个参数的MinutesPerHour分别变成了第二个([esp + 4])和第三个参数([esp + 8])了。可见原本只有两个参数的div()函数,编译后有三个参数,第一个参数是编译器附加的,为一指针,指向“存放返回的结构体”的地址,且该地址是在caller的堆栈上分配的。

调用call _div之后,为什么要将esp4呢?看完div()函数的汇编代码后再回过头来分析。总结下调用div()函数的过程,用伪代码表示如下:

拨动esp为div_t结构体HourAndMinutes在栈上分配空间;
MinutesPerHour入栈;              //原本第2个参数变成第3个参数
iMinutes入栈;                    //原本第1个参数变成第2个参数
HourAndMinutes在栈上的地址入栈;  //“附加”一个指针参数
call div;            
sub esp, 4;                      //堆栈调整

div()函数对应的汇编代码如下:

public div                                   
arg_0          = dword ptr  8                              
arg_4          = dword ptr  0Ch                            
arg_8          = dword ptr  10h                            
               push    ebp                                 
               mov     ebp, esp                            
               lea     esp, [esp-8]                        
               mov     [esp], esi                          
               mov     [esp+4], edi                        
               mov     esi, [ebp+arg_4]                    
               mov     edi, [ebp+arg_8]                    
               mov     edx, esi                            
               mov     eax, esi                            
               sar     edx, 1Fh                            
               mov     ecx, [ebp+arg_0]  ;第一个参数即结构体的地址赋给ecx
               idiv    edi                                 
               test    esi, esi                            
               js      short loc_2A61D                     
               test    edx, edx                            
               jns     short loc_2A61D                     
               lea     eax, [eax+1]                        
               sub     edx, edi                            
loc_2A61D:
               mov     [ecx], eax    ;与下一行一起将计算结果保存到结构体对应的地址空间中
               mov     [ecx+4], edx  ;
               mov     eax, ecx      ;函数的返回值由eax返回,可见返回值其实为结构体的地址
               mov     esi, [esp]                          
               mov     edi, [esp+4]                        
               mov     esp, ebp                            
               pop     ebp                                  
               retn    4   ;因为div函数原型中只有两个参数,编译成汇编之后有3个参数,编译器有点
                           ;过意不去,在返回时除了将返回地址出栈,第一个参数也出栈,这样该函数返
                           ;回后,在调用者的堆栈中,编译器附加的第一个参数(结构体的地址)便失
                           ;效了,这就是retn后面的4的作用。如此一来,函数调用者的堆栈可能就不
                           ;平衡了,需要重新调整
div            endp 

参考注释。可见div()函数的返回值(通过eax返回)其实为结构体的地址;而其语义上的返回值(返回结构体对象)却是通过附加的指针参数“回传”的。

我们知道,函数一般通过ret指令返回。因为call指令暗含了“将函数的返回地址入栈的操作”,因此对应的ret指令暗含了“弹出函数返回地址的操作”,这一入栈、一弹出栈就平衡了。但div()函数却是通过“retn 4”返回,即在“弹出函数的返回地址”之后又弹出4个字节,将div()函数的第一个参数即附加的参数也弹出了。至于为什么要这么干,注释中的说明有些戏虐的成分,应该是与调用规范有关。总之,通过“retn 4”返回到caller中(testdiv()函数中),由于多执行一次“出栈”操作,导致caller的栈不平衡了,因此在caller中通过“sub     esp,4”又将堆栈扩展4个字节,恢复栈平衡。

 3.结论

可见,函数的返回值为结构体类型,其返回值既不是“值传递”也不是通过“寄存器”回传。编译器在编译此类函数时,为其附加了一个指针参数(指向的地址在caller的堆栈上),且作为函数的第一个参数(函数本身的参数依次后移),函数语义上的返回值通过该附加的指针参数回传,而函数真正的返回值就是该指针。

<think>我们正在讨论C语言中函数如何返回结构体。根据引用[1]和引用[2],有两种主要方法:1.返回结构体变量本身(值返回):函数可以直接返回一个结构体变量。这种情况下,结构体会被复制一份返回给调用者。这种方法适用于较小的结构体,因为复制不会带来太大的性能开销。2.返回结构体指针:函数可以返回指向结构体的指针。这种方法通常用于较大的结构体,以避免复制整个结构体的开销。但需要注意,返回的指针所指向的内存必须是有效的(例如,使用malloc分配的内存,或者指向静态存储期或调用者提供的内存)。引用[1]展示了返回结构体指针的方法:```cstructPoint*createPoint(intx,inty){structPoint*p=malloc(sizeof(structPoint));p->x=x;p->y=y;returnp;//返回结构体指针}```引用[2]展示了直接返回结构体变量的方法:```cs_outadd(float*datain,intlength){s_outdataout={0};//声明结构体变量//...对dataout进行赋值returndataout;//返回结构体变量本身}```另外,引用[3]提到了结构体内嵌函数函数指针)的概念,但这与函数返回结构体没有直接关系,所以这里不展开。引用[4]则展示了一个使用结构体数组和函数调用的例子,其中函数通过指针参数修改结构体数组,而不是返回结构体。因此,总结两种返回结构体的方法:###方法1:直接返回结构体(值返回)-适用于较小的结构体。-在函数内部定义结构体变量,然后直接返回该变量。-调用者将获得该结构体的一个副本。-示例:```c#include<stdio.h>structPoint{intx;inty;};structPointcreatePoint(intx,inty){structPointp;p.x=x;p.y=y;returnp;}intmain(){structPointmyPoint=createPoint(10,20);printf("Point:(%d,%d)\n",myPoint.x,myPoint.y);return0;}```###方法2:返回结构体指针-适用于较大的结构体或需要避免复制的场景。-在函数内部使用动态内存分配(如`malloc`)创建结构体,然后返回指针。-调用者在使用完毕后需要释放内存,否则会导致内存泄漏。-示例:```c#include<stdio.h>#include<stdlib.h>structPoint{intx;inty;};structPoint*createPoint(intx,inty){structPoint*p=(structPoint*)malloc(sizeof(structPoint));if(p==NULL){//处理内存分配失败returnNULL;}p->x=x;p->y=y;returnp;}intmain(){structPoint*myPoint=createPoint(10,20);if(myPoint!=NULL){printf("Point:(%d,%d)\n",myPoint->x,myPoint->y);free(myPoint);//释放内存}return0;}```###注意事项-直接返回结构体时,注意结构体的大小,避免因复制大结构体导致性能下降。-返回指针时,必须明确内存管理责任:由谁负责释放内存。通常,由调用者负责释放。-返回指向局部变量的指针是危险的,因为局部变量在函数返回后会被销毁。因此,返回指针时应指向堆内存(通过`malloc`等分配)或静态存储期的变量(但静态变量可能导致重入性问题)。根据上述内容,我们可以生成相关问题。</think>### C语言函数返回结构体的方法 在C语言中,函数返回结构体主要有两种实现方式: #### 方法1:直接返回结构体变量(值返回) ```c #include <stdio.h> // 定义结构体类型 typedef struct { int x; int y; } Point; // 函数直接返回结构体实例 Point createPoint(int x, int y) { Point p = {x, y}; // 创建局部结构体变量 return p; // 返回结构体副本 } int main() { Point myPoint = createPoint(3, 4); // 接收返回结构体 printf("坐标: (%d, %d)\n", myPoint.x, myPoint.y); return 0; } ``` **特点**: - 适用于小型结构体(复制开销小) - 自动内存管理(无需手动释放) - 返回的是结构体的副本而非原对象 - 语法简洁直观 #### 方法2:返回结构体指针(动态内存) ```c #include <stdio.h> #include <stdlib.h> typedef struct { float data[10]; int length; } DataSet; // 返回指向结构体的指针 DataSet* createDataSet(int size) { DataSet* ds = malloc(sizeof(DataSet)); // 堆内存分配 ds->length = size; for(int i=0; i<size; i++) { ds->data[i] = i * 0.5f; } return ds; // 返回指针 } int main() { DataSet* myData = createDataSet(5); printf("数据集大小: %d\n", myData->length); free(myData); // 必须手动释放内存 return 0; } ``` **特点**: - 适用于大型结构体(避免复制开销) - 需要手动管理内存(malloc/free) - 返回的是原对象的地址 - 效率更高但风险更大(可能内存泄漏) #### 关键注意事项: 1. **内存管理**:返回指针时必须确保内存有效性(堆内存或静态变量) 2. **结构体大小**:超过几十字节的结构体建议使用指针返回 3. **返回值优化**:现代编译器会对值返回进行优化(RVO) 4. **作用域风险**:绝对不要返回指向局部变量的指针 ```c // 危险示例(悬空指针) Point* createPoint() { Point local; // 函数结束即销毁 return &local; // 错误! } ``` #### 性能对比 | 方法 | 小结构体(<32B) | 大结构体(>100B) | 内存管理 | |--------------|----------------|-----------------|----------| | 值返回 | ★★★ 高效 | ★ 低效 | 自动 | | 指针返回 | ★★ 中等 | ★★★ 高效 | 手动 | 实际选择应根据结构体大小和使用场景决定[^1][^2]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值