`
sipgreen
  • 浏览: 26649 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
最近访客 更多访客>>
社区版块
存档分类
最新评论

函数如何返回struct或class对象

    博客分类:
  • C++
 
阅读更多

所有的C、C++教科书都警告我们:不要通过函数来返回struct或 class对象,否则会造成内存复制以及复制构造函数的调用,降低性能。相信这句话已经成为了一个常识,大家都能牢记于心。然而,有时候我们不得不违反这个警告,例如,通过函数获取一个std::string对象(以个人的经验而言,这种情况是很常见的,我经常要通过函数创建一个新的对象)。不知道从什么时候起,当我面对这种情况的时候会通过引用来获取这个对象,像这样:

1
2
std::string GetString();
std::string& str = GetString();

这样子给我的感觉会好一点,让我觉得对象的复制次数少了。然而这只是一种凭空猜想,没有经过任何证实。为了弄清楚这样做究竟会不会带来性能的提升,我决定研究一下函数是如何返回struct或class对象的。最好的研究途径当然是反汇编编译器生成的机器码了。

 

我的实验环境是Visual Studio 2010,所有代码都是Debug版本的,因为这样生成的机器码是最原始的,没有经过任何优化,可以显示出真实的情况。而Release版本的机器码经过了优化,已经是“面目全非”,所以本文不考虑该版本。另外,对于struct来说,Visual Studio 2010 的C编译器和C++编译器生成的代码是一样的,所以本文所有代码都通过C++编译器来编译。注意,使用不同的编译器可能会有不同的结果!

 

如何返回struct对象

首先来看一下函数如何返回struct对象。分两种情况:第一种情况是struct的大小是1字节、2字节或4个字节,可以放到al、ax或eax寄存器中;第二种情况是struct的大小不是上面提到的三个值,不能放到寄存器中(包括3个字节的)。要注意,这里所说的“大小”是指在内存中经过对齐后的大小,而不是定义的大小。如果没有特别说明,下文提到的大小也是指经过对齐后的大小。

 

第一种情况:struct可以放到寄存器中

下面是第一种情况的典型例子,struct的大小是4个字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct S {
    int Value;
};
  
S GetS(int value) {
  
    S s;
    s.Value = value;
  
    return s;
}
  
int wmain() {
  
    S s = GetS(10);
}

 

下面是GetS函数的部分汇编代码:

1
2
3
4
5
6
;s.Value = value;
mov         eax,dword ptr [value]  
mov         dword ptr [s],eax
    
;return s;
mov         eax,dword ptr [s]

可以看到,s是直接通过eax来返回的,因为它的大小恰好可以放进eax寄存器中。

 

下面是S s = GetS(10);的汇编代码:

1
2
3
4
5
6
push        0Ah                       ;参数10入栈
call        GetS (8D1019h)            ;调用GetS函数  
add         esp,4                     ;释放参数空间
mov         dword ptr [ebp-0D4h],eax  ;将返回值保存到临时空间
mov         eax,dword ptr [ebp-0D4h]  ;从临时空间里取出返回值
mov         dword ptr [s],eax         ;将返回值保存到s中

这些代码都很好理解,唯一让人疑惑的地方是,返回值不是直接保存到s中,而是先放到一块临时空间里(ebp-0D4h),然后再从这块临时空间转移到s中。为什么编译器要如此多此一举呢?这是因为存在“不接收返回值”的函数调用,例如:GetS(10);,它返回的struct不会保存到局部变量里,而是只保存到那块临时空间中。

 

上面的汇编代码确实验证了那句警告,即使struct可以像一个普通的int那样通过eax返回,也会稍微降低性能,因为执行了两条“多余”的指令,但我认为这样的开销还是可以接受的。对于大小为1个字节或2个字节的struct来说,生成的汇编代码跟上面的几乎一样,只不过返回值是通过al或ax来返回的。

 

第二种情况:struct不能放到寄存器中

下面是第二种情况的典型例子,struct的大小为12字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct S {
    int Value1;
    int Value2;
    int Value3;
};
  
S GetS(int value) {
  
    S s;
    s.Value1 = value;
    s.Value2 = value * 2;
    s.Value3 = value * 3;
  
    return s;
}
  
int wmain() {
  
    S s = GetS(10);
}

 

下面是GetS函数的部分汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;s.Value1 = value;
mov         eax,dword ptr [ebp+0Ch]  
mov         dword ptr [ebp-14h],eax  
  
;s.Value2 = value * 2;
mov         eax,dword ptr [ebp+0Ch]  
shl         eax,1  
mov         dword ptr [ebp-10h],eax  
  
;s.Value3 = value * 3;
mov         eax,dword ptr [ebp+0Ch]  
imul        eax,eax,3  
mov         dword ptr [ebp-0Ch],eax  
  
;return s;
mov         eax,dword ptr [ebp+8]    ;取出第一个参数的值
mov         ecx,dword ptr [ebp-14h]  ;取出s.Value1
mov         dword ptr [eax],ecx      ;将s.Value1放到eax所指的内存中
mov         edx,dword ptr [ebp-10h]  ;取出s.Value2
mov         dword ptr [eax+4],edx    ;将s.Value2放到eax+4所指的内存中
mov         ecx,dword ptr [ebp-0Ch]  ;取出s.Value3
mov         dword ptr [eax+8],ecx    ;将s.Value3放到 eax+8所指的内存中
mov         eax,dword ptr [ebp+8]    ;将第一个参数作为返回值

 

重点看return s;这一句的汇编代码,它将局部变量s(ebp-14h)复制到了第一个参数(ebp+8)所指的内存中,然后将第一个参数作为返回值。等等,GetS不是只有一个参数吗?而且这个参数只是一个数值,而不是地址,这样做的话肯定会出错。再往上看看那几条赋值语句的汇编代码,或许就明白了:GetS的参数value实际上是ebp+0Ch,而不是ebp+8,也就是说,GetS实际上有两个参数!

 

再来看一下S s = GetS(10);这一句的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
push        0Ah                  ;参数10入栈
lea         eax,[ebp-0E8h]       ;取出临时空间的地址 
push        eax                  ;将临时空间的地址入栈
call        GetS (51019h)        ;调用GetS
add         esp,8                ;释放参数空间
  
;接下来的6条指令是将返回的struct(ebp-0E8h)复制到另一块临时空间(ebp-0FCh)中
mov         ecx,dword ptr [eax]  
mov         dword ptr [ebp-0FCh],ecx  
mov         edx,dword ptr [eax+4]  
mov         dword ptr [ebp-0F8h],edx  
mov         eax,dword ptr [eax+8]  
mov         dword ptr [ebp-0F4h],eax
  
;接下里的6条指令将临时空间(ebp-0FCh)中的数据复制到局部变量s(ebp-14h)中  
mov         ecx,dword ptr [ebp-0FCh]  
mov         dword ptr [ebp-14h],ecx  
mov         edx,dword ptr [ebp-0F8h]  
mov         dword ptr [ebp-10h],edx  
mov         eax,dword ptr [ebp-0F4h]  
mov         dword ptr [ebp-0Ch],eax

可以看到,GetS除了value这个显式定义的参数之外,还有一个隐含的参数,该参数是一个指向一块临时空间(ebp-0E8h)的地址,在GetS内部将要返回的struct复制到了这块临时空间中,然后再通过eax返回这块临时空间的地址。这样,通过两方的协作,完成了struct的返回。

 

接下来的指令仍然是在做“多余”的事情:将返回值复制到另一块临时空间(ebp-0FCh)中,再从临时空间复制到局部变量s(ebp-14h)中。综上所述,为了从函数中返回一个struct,需要三块内存空间:一块用来接收返回值,一块“多余”的临时空间,一块是局部变量的空间。另外还需要进行三次内存复制:一次是被调用函数复制返回值,另外两次是“多余”的复制。由此看出,返回一个不能容纳于寄存器中的struct,不仅浪费时间,也浪费空间!

 

如何返回class对象

虽然在C++中struct和class本质上是一样的,但为了加以区别,在下文中规定,class泛指含有复制构造函数的struct或class,而struct 泛指没有复制构造函数的struct或class(希望不会给你带来混乱)。你会看到,有没有复制构造函数会造成很大的不同。

 

返回class对象的行为比返回struct的行为简单得多,不论class的大小如何,处理方式都是一样的。下面是例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class C {
public:
    C() { }
    C(const C& rhs) {
        Value1 = rhs.Value1;
        Value2 = rhs.Value2;
        Value3 = rhs.Value3;
    }
  
    int Value1;
    int Value2;
    int Value3;
};
  
C GetC(int value) {
  
    C c;
    c.Value1 = value;
    c.Value2 = value * 2;
    c.Value3 = value * 3;
  
    return c;
}
  
int wmain() {
  
    C c = GetC(10);
}

 

下面是C c = GetC(10);的汇编代码:

1
2
3
4
5
push        0Ah      ;参数10入栈
lea         eax,[c]  ;取得局部变量c的地址
push        eax      ;将c的地址入栈
call        GetC     ;调用GetC
add         esp,8    ;释放参数空间

看上去清爽得多了。这里同样是将局部变量的地址作为隐含参数传递给被调用函数,但最后少了内存复制的操作。

 

下面是GetC的部分汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;C c;
lea         ecx,[c]  
call        C::C                    ;调用默认构造函数
  
;c.Value1 = value;
mov         eax,dword ptr [value]  
mov         dword ptr [c],eax  
  
;c.Value2 = value * 2;
mov         eax,dword ptr [value]  
shl         eax,1  
mov         dword ptr [ebp-0Ch],eax  
  
;c.Value3 = value * 3;
mov         eax,dword ptr [value]  
imul        eax,eax,3  
mov         dword ptr [ebp-8],eax  
  
;return c;
lea         eax,[c]  
push        eax  
mov         ecx,dword ptr [ebp+8]  
call        C::C                    ;调用复制构造函数
mov         eax,dword ptr [ebp+8]

重点还是在return c;这条语句上,它的汇编代码非常简洁,仅仅是调用传递进来的C对象的复制构造函数!假如复制构造函数中只进行一次内存复制的话,那么从函数中返回一个class对象只需要进行一次内存复制,也只需要一块内存空间,即局部变量所需的空间。也就是说,返回一个class对象基本上只需要调用一次复制构造函数即可。

 

下面再来看一种特殊情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class C {
public:
    C(int value) {
        Value1 = value;
        Value2 = value;
        Value3 = value;
    }
  
    C(const C& rhs) {
        Value1 = rhs.Value1;
        Value2 = rhs.Value2;
        Value3 = rhs.Value3;
    }
  
    int Value1;
    int Value2;
    int Value3;
};
  
C GetC(int value) {
  
    return C(value);
}
  
int wmain() {
  
    C c = GetC(10);
}

 

在GetC函数中,直接在return语句中构造一个C对象并返回。可以猜想,这样的话只需要调用一次构造函数就可以返回class对象了。下面是GetC的部分汇编代码:

1
2
3
4
5
6
;return C(value);
mov         eax,dword ptr [value]  
push        eax  
mov         ecx,dword ptr [ebp+8]  
call        C::C                    ;调用构造函数
mov         eax,dword ptr [ebp+8]

果然如此,这种做法的效率更高,跟创建一个新的对象几乎没有什么区别(当然,函数调用的开销还是存在的)。

 

由此可以看出,通过函数来返回一个class对象比返回一个struct对象开销要小得多,不需要多余的内存空间,也不需要多余的复制内存操作。

 

通过引用来获取对象真的高效率吗?

好了,上面通过对函数如何返回struct或class对象进行了比较全面研究,是时候来回答本文开头提到的问题了。下面分别是通过引用来获取struct和class的语句产生的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
;S& s = GetS(10);
push        0Ah  
lea         eax,[ebp-0F4h]  
push        eax  
call        GetS 
add         esp,8  
  
;下面6条指令将返回值(ebp-0F4h)复制到第一块临时空间(ebp-108h)
mov         ecx,dword ptr [eax]  
mov         dword ptr [ebp-108h],ecx  
mov         edx,dword ptr [eax+4]  
mov         dword ptr [ebp-104h],edx  
mov         eax,dword ptr [eax+8]  
mov         dword ptr [ebp-100h],eax
  
;下面6条指令将第一块临时空间(ebp-108h)的数据复制到第二块临时空间(ebp-20h)
mov         ecx,dword ptr [ebp-108h]  
mov         dword ptr [ebp-20h],ecx  
mov         edx,dword ptr [ebp-104h]  
mov         dword ptr [ebp-1Ch],edx  
mov         eax,dword ptr [ebp-100h]  
mov         dword ptr [ebp-18h],eax
  
;将第二块临时空间(ebp-20h)的地址赋值给局部变量s(ebp-0Ch)
lea         ecx,[ebp-20h]  
mov         dword ptr [ebp-0Ch],ecx  
  
  
;C& c = GetC(10);
push        0Ah  
lea         eax,[ebp-1Ch]  
push        eax  
call        GetC 
add         esp,8  
  
;将临时空间(ebp-1Ch)的地址赋值给变量c
lea         ecx,[ebp-1Ch]  
mov         dword ptr [c],ecx

通过与上文的汇编代码进行比较,发现使用引用后不仅没有减少指令,反而增加了两条指令,将临时空间的地址赋值给引用变量。所以得出结论,使用引用来获取对象的效率反而降低了!

 

总结

知道了函数如何返回struct或class对象,我得出下面的编程指导:

①对于大小为1字节、2字节或4字节的struct,可以通过函数来返回。

②对于大小不是1字节、2字节或4字节的struct,不要通过函数来返回。

③对于class,如果复制构造函数的工作量少,可以通过函数来返回;如果复制构造函数的工作量大,则不要通过函数返回。

④对于class,尽量通过在return语句中构造对象来返回。

⑤不要通过引用来获取函数返回的对象!

 

最后再说明一下,不同编译器的处理方式可能会不同,所以上面的指导不一定完全通用。另外,Release版本的代码会经过优化,可能会消除那些降低性能的代码。当然啦,我们不能依赖于编译器的优化,因为不是任何情况都适合优化的。

分享到:
评论

相关推荐

    Desktop_struct与class的区别_

    许多时候,`struct`被用来表示数据聚合体,也就是通常所说的记录或结构体,而`class`则更倾向于作为对象的模板,用于实现面向对象编程。 `this`指针是C++中另一个重要的概念,它在`struct`和`class`中都存在。`this...

    c++面向对象基础二(struct详解)

    在面向对象编程中,`struct` 和 `class` 是两个非常重要的概念,它们都是用于定义数据类型的方式,但它们之间存在一些关键区别。本篇文章将深入探讨C++中的`struct`,并解释如何在面向对象编程中有效地使用它。 ...

    Class to struct

    在软件开发过程中,有时候我们需要将使用C++编写的类转换为C语言环境中可使用的结构体,这通常发生在需要跨语言兼容或移植代码到不支持面向对象特性的环境中。这种转换涉及到如何处理类中的成员变量、构造函数、成员...

    Qt中调用函数如何返回多个值的Qt文件

    如果返回的对象可能需要管理其生命周期,可以使用智能指针(如`std::unique_ptr`或`std::shared_ptr`)来返回一个对象。这可以确保对象在不再需要时被正确销毁。例如: ```cpp std::unique_ptr<Result> get...

    Swift-Class-Struct Swift-Class-Struct

    而自定义的配置对象或简单的数据容器则可能更适合用结构体,以确保不可变性和性能。 总的来说,理解和灵活运用Swift的`Class`和`Struct`对于编写高效、易维护的iOS应用程序至关重要。在设计数据模型、UI组件和业务...

    c# List find()方法返回值的问题说明(返回结果为对象的指针)

    C#中List中泛型T如果是一个对象的话,则利用Find函数返回的将是这个对象的指针,对其返回对象的属性进行操作,也会影响list中相应元素对象的值。验证如下:1.新建一个Class1类,其含有两个姓名和分数两个属性: 代码...

    在驱动模块初始化函数中实现设备节点的自动创建

    class_create 函数在 /drivers/base/class.c 中实现,用于创建一个 struct class 结构体指针,该指针可以在后续的 device_create 函数调用中使用。 device_create 函数可以在 /dev 目录下创建相应的设备节点。这个...

    java通过jna返回结构体例子.rar

    在Java中调用返回结构体的C函数,可以使用`PointerByReference`来接收结果,因为JNA无法直接返回结构体实例。例如: ```java Library lib = Library.INSTANCE; // Library是你的JNA接口,里面定义了C函数的签名 ...

    matlab开发-class2struct

    在本案例中,我们有一个名为"class2struct.m"的函数,它可能就是用来执行这种转换的自定义函数。 MATLAB中的类是一种面向对象编程的构造,允许用户创建具有特定属性和方法的数据类型。类定义了对象的结构和行为,而...

    struct in action

    多态性在struct中也可以实现,但需要借助虚函数和指针或引用来实现。 四、struct与联合(union) 在C++中,struct也可以与union一起使用,union允许同一内存位置存储不同类型的值,这在节省内存和处理硬件特性时很...

    C++中struct和class的区别

     struct能包含成员函数吗? 能!  struct能继承吗? 能!!  struct能实现多态吗? 能!!!  本质的一个区别是默认的访问控制,体现在两个方面:  1)默认的继承访问权限。struct是public的,class是...

    hash_set c++总结(自定义类型struct、class)

    hash_set c++总结(自定义类型stuct、class)。总结自定义struct、class三个案例。find函数测试,hash_set迭代器。

    区分C# 中的 Struct 和 Class

    翻译自 Manju lata Yadav 2019年6月2日 的... 结构体不能有默认构造函数(无参构造函数)或析构函数,构造函数中必须给所有字段赋值。 public struct Coords { public double x; public double y; public Coords

    Android调用Jni返回自定义对象

    然后,你需要在JNI中定义一个函数,接收Java对象作为参数,处理后返回自定义对象。这通常涉及到JNI的`NewObject`函数,用于创建Java对象。例如: ```cpp jobject createCustomObject(JNIEnv *env, int id, const ...

    C类class和结构体struct区别-C教程共3页.pd

    - 当一个`class`或`struct`继承自另一个`class`或`struct`时,`private`成员在子类中仍保持`private`,`protected`成员保持`protected`,而`public`成员保持`public`。 - 不过,如果基类是`struct`,所有成员默认...

    EDA/PLD中的如何在C++中struct与Class的的区别

    在C++中,由于历史原因和C语言的兼容性,`struct`仍然存在,但建议在不需要与C兼容或传递参数给C程序的情况下,优先使用`class`,以更好地体现面向对象的设计思想。同时,类的成员在内存中的布局并不一定按照声明...

    Struct和运算符重载---详细

    在这个例子中,`operator+`是`Complex`的一个成员函数,接受另一个`Complex`对象作为参数,并返回一个新的`Complex`对象,表示两者的和。 2. **友元函数重载运算符**: 如果运算符涉及两个或更多相同类型的操作数...

Global site tag (gtag.js) - Google Analytics