C++中stack的pop()函数返回值解析

目录
  • stack的pop()函数返回值
    • 全部demo
    • 分析
  • C++的返回值优化
    • 从函数返回值
    • RVO

stack的pop()函数返回值

    int temp = s.pop();
    cout<<temp<<endl; 

运行代码会提示错误:error C2440: “初始化”: 无法从“void”转换为“int”

全部demo

#include <iostream>
#include <stack>

using namespace std;

int main()
{
	stack<int> s;
	if(s.empty())
		cout<<"empty"<<endl;   //empty
	s.push(1);
	s.push(6);
	s.push(66);
	cout<<s.size()<<endl;   //3
	int temp = s.pop();
	cout<<temp<<endl;	//66
	cout<<s.size()<<endl;	//2
	cout<<s.top()<<endl;	//6
	cout<<s.size()<<endl;	//2
	system("pause");
	return 0;

}

分析

C++中stack,其中有两个方法:

  • pop(), 返回void,
  • top(),返回栈顶的引用。

所以想要提取栈顶元素,直接用s.top()

C++的返回值优化

大家都知道“过早的优化是万恶之源”这句话,然而我相信其中的大多数人都不知道自己是不是在做过早的优化。我也无法准确的定义什么叫做“过早的优化”,但我相信这“过早的优化”要么是得不偿失的,要么干脆是有害无利的。今天我就想举个我认为是“过早的优化”的例子。

从函数返回值

为了从一个函数得到运行结果,常规的途径有两个:通过返回值和通过传入函数的引用或指针(当然还可以通过全局变量或成员变量,但我觉得这算不上是什么好主意)。

通过传给函数一个引用或指针来承载返回值在很多情况下是无可厚非的,毕竟有时函数需要将多个值返回给用户。除了这种情况之外,我觉得应当尽量做到参数作为函数输入,返回值作为函数输出(这不是很自然的事情吗?)。然而,我们总能看到一些“突破常规”的做法:

首先定义Message类:

struct Message
{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
};

为了从某个地方(比如一个队列)得到一个特定Message对象,有些人喜欢写一个这样的getMessage:

void getMessage(Message &msg); // 形式1

虽然只有一个返回值,但仍然是通过传入函数的引用返回给调用者的。

为什么要这样呢?“嗯,为了提高性能。你知道,要是这样定义函数,返回Message对象时必须要构造一个临时对象,这对性能有影响。”

Message getMessage(); // 形式2

我们先不讨论这带来了多少性能提升,先看看形式1相对形式2带来了哪些弊端。我认为有两点:

1. 可读性变差

略(我希望你能和我一样认为这是显而易见的)。

2. 将对象的初始化划分成了两个步骤

调用形式1时,你必然要这样:

Message msg;     // S1
getMessage(msg); // S2

这给维护者带来了犯错的机会:一些需要在S2语句后面对msg进行的操作有可能会被错误的放在S1和S2之间。

如果是形式2,维护者就不可能犯这种错误:

Message msg = getMessage();

好,现在我们来看性能,形式2真的相对形式1性能更差吗?对于下面的代码:

#include <stdio.h>
 
struct Message
{
    Message()
    { 
        printf("Message::Message() is called\n"); 
    }
    Message(const Message &)
    {
        printf("Message::Message(const Message &msg) is called\n");
    }
    Message& operator=(const Message &)
    {
        printf("Message::operator=(const Message &) is called\n");
    }
    ~Message()
    {
        printf("Message::~Message() is called\n");
    }
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
};
 
Message getMessage()
{
    Message result;
    result.a = 0x11111111;
 
    return result;
}
 
int main()
{
    Message msg = getMessage();
    return 0;
}

你认为运行时会输出什么呢?是不是这样:

Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called

并没有像预期的输出那样。

如果使用MSVC2017编译,且关闭优化(/Od),确实可以得到预期输入,但是一旦打开优化(/O2),输出就和GCC的一样了。

我们看看实际上生成了什么代码(使用GCC编译):

(gdb) disassemble main
Dump of assembler code for function main():
   0x0000000000000776 <+0>:    push   %rbp
   0x0000000000000777 <+1>:    mov    %rsp,%rbp
   0x000000000000077a <+4>:    push   %rbx
   0x000000000000077b <+5>:    sub    $0x28,%rsp
   0x000000000000077f <+9>:    mov    %fs:0x28,%rax
   0x0000000000000788 <+18>:    mov    %rax,-0x18(%rbp)
   0x000000000000078c <+22>:    xor    %eax,%eax
   0x000000000000078e <+24>:    lea    -0x30(%rbp),%rax             #将栈上地址-0x30(%rbp)传给getMessage函数
   0x0000000000000792 <+28>:    mov    %rax,%rdi
   0x0000000000000795 <+31>:    callq  0x72a <getMessage()>
   0x000000000000079a <+36>:    mov    $0x0,%ebx
   0x000000000000079f <+41>:    lea    -0x30(%rbp),%rax
   0x00000000000007a3 <+45>:    mov    %rax,%rdi
   0x00000000000007a6 <+48>:    callq  0x7e4 <Message::~Message()>
   0x00000000000007ab <+53>:    mov    %ebx,%eax
   0x00000000000007ad <+55>:    mov    -0x18(%rbp),%rdx
   0x00000000000007b1 <+59>:    xor    %fs:0x28,%rdx
   0x00000000000007ba <+68>:    je     0x7c1 <main()+75>
   0x00000000000007bc <+70>:    callq  0x5f0 <__stack_chk_fail@plt>
   0x00000000000007c1 <+75>:    add    $0x28,%rsp
   0x00000000000007c5 <+79>:    pop    %rbx
   0x00000000000007c6 <+80>:    pop    %rbp
   0x00000000000007c7 <+81>:    retq   
End of assembler dump.
(gdb) disassemble getMessage 
Dump of assembler code for function getMessage():
   0x000000000000072a <+0>:    push   %rbp
   0x000000000000072b <+1>:    mov    %rsp,%rbp
   0x000000000000072e <+4>:    sub    $0x20,%rsp
   0x0000000000000732 <+8>:    mov    %rdi,-0x18(%rbp)                 #将main函数传入的栈上地址保存到-0x18(%rbp)处
   0x0000000000000736 <+12>:    mov    %fs:0x28,%rax
   0x000000000000073f <+21>:    mov    %rax,-0x8(%rbp)
   0x0000000000000743 <+25>:    xor    %eax,%eax
   0x0000000000000745 <+27>:    mov    -0x18(%rbp),%rax             #将main函数传入的栈上地址传给Message::Message()函数
   0x0000000000000749 <+31>:    mov    %rax,%rdi
   0x000000000000074c <+34>:    callq  0x7c8 <Message::Message()>
   0x0000000000000751 <+39>:    mov    -0x18(%rbp),%rax
   0x0000000000000755 <+43>:    movl   $0x11111111,(%rax)
   0x000000000000075b <+49>:    nop
   0x000000000000075c <+50>:    mov    -0x18(%rbp),%rax
   0x0000000000000760 <+54>:    mov    -0x8(%rbp),%rdx
   0x0000000000000764 <+58>:    xor    %fs:0x28,%rdx
   0x000000000000076d <+67>:    je     0x774 <getMessage()+74>
   0x000000000000076f <+69>:    callq  0x5f0 <__stack_chk_fail@plt>
   0x0000000000000774 <+74>:    leaveq 
   0x0000000000000775 <+75>:    retq   
End of assembler dump.

可以看出来,在getMessage函数中构造的对象实际上位于main函数的栈帧上,并没有额外构造一个Message对象。这是因为开启了所谓的返回值优化(RVO,Return Value Optimization)的缘故。你想得到的效果编译器已经自动帮你完成了,你不必再牺牲什么。

RVO

对于我们这些用户来说,RVO并不是什么特别复杂的机制,主流的GCC和MSVC均支持,也没什么特别需要注意的地方。它存在的目的是优化掉不必要的拷贝复制函数的调用,即使拷贝复制函数有什么副作用,例如上面代码中的打印语句,这可能是唯一需要注意的地方了。从上面的汇编代码中可以看出来,在GCC中,其基本手段是直接将返回的对象构造在调用者栈帧上,这样调用者就可以直接访问这个对象而不必复制。

RVO是有限制条件的,在某些情况下无法进行优化,在一篇关于MSVC2005的RVO技术的文章中,提到了3点导致无法优化的情况:

1. 函数抛异常

关于这点,我是有疑问的。文章中说如果函数抛异常,开不开RVO结果都一样。如果函数抛异常,无法正常的返回,我当然不会要求编译器去做RVO了。

2. 函数可能返回具有不同变量名的对象

Message getMessage_NoRVO1(int in)
{
    Message msg1;
    msg1.a = 1;
 
    Message msg2;
    msg2.a = 2;
 
    if (in % 2)
    {
        return msg1;
    }
    else
    {
        return msg2;
    }
}

经过验证,在GCC上确实也是这样的,拷贝构造函数被调用了。但这种情况在很多时候应该都是可以通过重构避免的。

Message::Message() is called
Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called
Message::~Message() is called

3. 函数有多个出口

Message getMessage_NoRVO2(int in)
{
    Message msg;
    if (in % 2)
    {
        return msg;
    }
    msg.a = 1;
    return msg;
}

这个在GCC上验证发现RVO仍然生效,查看汇编发现只有一个retq指令,多个出口被优化成一个了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • C/C++ 引用作为函数的返回值方式

    目录 case1:用返回值方式调用函数 case2:用函数的返回值初始化引用的方式调用函数 case3:用返回引用的方式调用函数 case4:用函数返回的引用作为新引用的初始化值的方式来调用函数 用引用实现多态 函数中返回引用和返回值的区别 主要讨论下面两个函数的区别 说明一下函数返回时 语法:类型 &函数名(形参列表){ 函数体 } 特别注意: 1.引用作为函数的返回值时,必须在定义函数时在函数名前将& 2.用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本 //代码来源:RU

  • 从汇编看c++函数的默认参数的使用说明

    在c++中,可以为函数提供默认参数,这样,在调用函数的时候,如果不提供参数,编译器将为函数提供参数的默认值.下面从汇编看其原理. 下面是c++源码: 复制代码 代码如下: int add(int a = 1, int b = 2) {//参数a b有默认值    return a + b;}int main() {   int c= add();//不提供参数 } 下面是mian函数里面的汇编码: 复制代码 代码如下: ; 4    : int main() { push    ebp    m

  • C++详解实现Stack方法

    目录 栈简介 stack模拟 示例代码 开发环境 运行结果 栈简介 栈本着先进后出的原则,来存取数据.作为数据结构中的一种,这里不多介绍相关栈.仅以此文记录C++中栈的实现,可帮助提升编程能力与对栈的理解. stack模拟 stack是一种容器适配器,专门在具有后进先出的上下文环境中,其删除只能是在一端进行操作. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出 .

  • C++中stack的pop()函数返回值解析

    目录 stack的pop()函数返回值 全部demo 分析 C++的返回值优化 从函数返回值 RVO stack的pop()函数返回值 int temp = s.pop(); cout<<temp<<endl; 运行代码会提示错误:error C2440: “初始化”: 无法从“void”转换为“int” 全部demo #include <iostream> #include <stack> using namespace std; int main() {

  • 详解C语言函数返回值解析

    详解C语言函数返回值解析 程序一: int main() { int *p; int i; int*fun(void); p=fun(); for(i=0;i<3;i++) { printf("%d\n",*p); p++; } return 0; }; int* fun(void) { static int str[]={1,2,3,4,5}; int*q=str; return q; } //不能正确返回 虽然str是在动态变量区,而该动态变量是局部的,函数结束时不保留的.

  • JS在Chrome浏览器中showModalDialog函数返回值为undefined的解决方法

    本文实例讲述了JS在Chrome浏览器中showModalDialog函数返回值为undefined的解决方法.分享给大家供大家参考,具体如下: 主页面: <script type="text/javascript"> function SelectGroupCust() { var temp = window.showModalDialog("Default2.aspx?xx=" + Date(), "", "dialog

  • Python中return函数返回值实例用法

    在学习return函数时候,还是要知道了解它最主要的函数作用,比如,怎么去实现返回一个值,另外还有就是我们经常会用到的使用return能够进行多值输出,这才是我们需要抓住知识的重点,针对上述所提及的内容,都可以来往下看文章,答案都在文章内容获取哦~ return 添加返回值 return 显示返回对象 返回值接受:value = func() 例子:计算学成最高分 listv = [90,80,88,77,66] # 分数计算return高分 def scoreCalculate(values)

  • PHP中mysqli_affected_rows作用行数返回值分析

    本文实例分析了PHP中mysqli_affected_rows作用行数返回值.分享给大家供大家参考.具体分析如下: mysqli中关于update操作影响的行数可以有两种返回形式: 1. 返回匹配的行数 2. 返回影响的行数 默认情况下mysqli_affected_rows返回的值为影响的行数,如果我们需要返回匹配的行数,可以使用mysqli_real_connect函数进行数据库连接的初始化,并在函数的flag参数位加上: MYSQLI_CLIENT_FOUND_ROWS return nu

  • Linux Shell函数返回值

    Shell函数返回值,一般有3种方式:return,argv,echo 1) return 语句 shell函数的返回值,可以和其他语言的返回值一样,通过return语句返回. 示例: #!/bin/bash - function mytest() { echo "arg1 = $1" if [ $1 = "1" ] ;then return 1 else return 0 fi } echo echo "mytest 1" mytest 1 e

  • C语言 用指针作为函数返回值详解

    C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数.下面的例子定义了一个函数 strlong(),用来返回两个字符串中较长的一个: #include <stdio.h> #include <string.h> char *strlong(char *str1, char *str2){ if(strlen(str1) >= strlen(str2)){ return str1; }else{ return str2; } } int main(){ cha

  • python使用threading获取线程函数返回值的实现方法

    threading用于提供线程相关的操作,线程是应用程序中工作的最小单元.python当前版本的多线程库没有实现优先级.线程组,线程也不能被停止.暂停.恢复.中断. threading模块提供的类:  Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local. threading 模块提供的常用方法: threading.currentThread(): 返回当前的线程变量. threading.enumera

  • Numpy中stack(),hstack(),vstack()函数用法介绍及实例

    1.stack()函数 函数原型为:stack(arrays,axis=0),arrays可以传数组和列表.axis的含义我下面会讲解,我们先来看个例子,然后我会分析输出结果. import numpy as np a=[[1,2,3], [4,5,6]] print("列表a如下:") print(a) print("增加一维,新维度的下标为0") c=np.stack(a,axis=0) print(c) print("增加一维,新维度的下标为1&qu

  • Python 函数返回值的示例代码

    0x 00 返回值简介 回顾下,上一节简单介绍了函数及其各种参数,其中也有简单介绍 print 和 return 的区别,print 仅仅是打印在控制台,而 return 则是将 return 后面的部分作为返回值作为函数的输出,可以用变量接走,继续使用该返回值做其它事. 函数需要先定义后调用,函数体中 return 语句的结果就是返回值.如果一个函数没有 reutrn 语句,其实它有一个隐含的 return 语句,返回值是 None,类型也是 'NoneType'. return 语句的作用:

随机推荐