C++中stack的pop()函數(shù)返回值解析
stack的pop()函數(shù)返回值
int temp = s.pop(); cout<<temp<<endl;
運(yùn)行代碼會(huì)提示錯(cuò)誤:error C2440: “初始化”: 無法從“void”轉(zhuǎn)換為“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,其中有兩個(gè)方法:
pop()
, 返回void,top()
,返回棧頂?shù)囊谩?/li>
所以想要提取棧頂元素,直接用s.top()
C++的返回值優(yōu)化
大家都知道“過早的優(yōu)化是萬惡之源”這句話,然而我相信其中的大多數(shù)人都不知道自己是不是在做過早的優(yōu)化。我也無法準(zhǔn)確的定義什么叫做“過早的優(yōu)化”,但我相信這“過早的優(yōu)化”要么是得不償失的,要么干脆是有害無利的。今天我就想舉個(gè)我認(rèn)為是“過早的優(yōu)化”的例子。
從函數(shù)返回值
為了從一個(gè)函數(shù)得到運(yùn)行結(jié)果,常規(guī)的途徑有兩個(gè):通過返回值和通過傳入函數(shù)的引用或指針(當(dāng)然還可以通過全局變量或成員變量,但我覺得這算不上是什么好主意)。
通過傳給函數(shù)一個(gè)引用或指針來承載返回值在很多情況下是無可厚非的,畢竟有時(shí)函數(shù)需要將多個(gè)值返回給用戶。除了這種情況之外,我覺得應(yīng)當(dāng)盡量做到參數(shù)作為函數(shù)輸入,返回值作為函數(shù)輸出(這不是很自然的事情嗎?)。然而,我們總能看到一些“突破常規(guī)”的做法:
首先定義Message類:
struct Message { ? ? int a; ? ? int b; ? ? int c; ? ? int d; ? ? int e; ? ? int f; };
為了從某個(gè)地方(比如一個(gè)隊(duì)列)得到一個(gè)特定Message對象,有些人喜歡寫一個(gè)這樣的getMessage:
void getMessage(Message &msg); // 形式1
雖然只有一個(gè)返回值,但仍然是通過傳入函數(shù)的引用返回給調(diào)用者的。
為什么要這樣呢?“嗯,為了提高性能。你知道,要是這樣定義函數(shù),返回Message對象時(shí)必須要構(gòu)造一個(gè)臨時(shí)對象,這對性能有影響。”
Message getMessage(); // 形式2
我們先不討論這帶來了多少性能提升,先看看形式1相對形式2帶來了哪些弊端。我認(rèn)為有兩點(diǎn):
1. 可讀性變差
略(我希望你能和我一樣認(rèn)為這是顯而易見的)。
2. 將對象的初始化劃分成了兩個(gè)步驟
調(diào)用形式1時(shí),你必然要這樣:
Message msg; ? ? // S1 getMessage(msg); // S2
這給維護(hù)者帶來了犯錯(cuò)的機(jī)會(huì):一些需要在S2語句后面對msg進(jìn)行的操作有可能會(huì)被錯(cuò)誤的放在S1和S2之間。
如果是形式2,維護(hù)者就不可能犯這種錯(cuò)誤:
Message msg = getMessage();
好,現(xiàn)在我們來看性能,形式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; }
你認(rèn)為運(yùn)行時(shí)會(huì)輸出什么呢?是不是這樣:
Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called
并沒有像預(yù)期的輸出那樣。
如果使用MSVC2017編譯,且關(guān)閉優(yōu)化(/Od),確實(shí)可以得到預(yù)期輸入,但是一旦打開優(yōu)化(/O2),輸出就和GCC的一樣了。
我們看看實(shí)際上生成了什么代碼(使用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函數(shù) ? ?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函數(shù)傳入的棧上地址保存到-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函數(shù)傳入的棧上地址傳給Message::Message()函數(shù) ? ?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函數(shù)中構(gòu)造的對象實(shí)際上位于main函數(shù)的棧幀上,并沒有額外構(gòu)造一個(gè)Message對象。這是因?yàn)殚_啟了所謂的返回值優(yōu)化(RVO,Return Value Optimization)的緣故。你想得到的效果編譯器已經(jīng)自動(dòng)幫你完成了,你不必再犧牲什么。
RVO
對于我們這些用戶來說,RVO并不是什么特別復(fù)雜的機(jī)制,主流的GCC和MSVC均支持,也沒什么特別需要注意的地方。它存在的目的是優(yōu)化掉不必要的拷貝復(fù)制函數(shù)的調(diào)用,即使拷貝復(fù)制函數(shù)有什么副作用,例如上面代碼中的打印語句,這可能是唯一需要注意的地方了。從上面的匯編代碼中可以看出來,在GCC中,其基本手段是直接將返回的對象構(gòu)造在調(diào)用者棧幀上,這樣調(diào)用者就可以直接訪問這個(gè)對象而不必復(fù)制。
RVO是有限制條件的,在某些情況下無法進(jìn)行優(yōu)化,在一篇關(guān)于MSVC2005的RVO技術(shù)的文章中,提到了3點(diǎn)導(dǎo)致無法優(yōu)化的情況:
1. 函數(shù)拋異常
關(guān)于這點(diǎn),我是有疑問的。文章中說如果函數(shù)拋異常,開不開RVO結(jié)果都一樣。如果函數(shù)拋異常,無法正常的返回,我當(dāng)然不會(huì)要求編譯器去做RVO了。
2. 函數(shù)可能返回具有不同變量名的對象
Message getMessage_NoRVO1(int in) { ? ? Message msg1; ? ? msg1.a = 1; ? ? ? Message msg2; ? ? msg2.a = 2; ? ? ? if (in % 2) ? ? { ? ? ? ? return msg1; ? ? } ? ? else ? ? { ? ? ? ? return msg2; ? ? } }
經(jīng)過驗(yàn)證,在GCC上確實(shí)也是這樣的,拷貝構(gòu)造函數(shù)被調(diào)用了。但這種情況在很多時(shí)候應(yīng)該都是可以通過重構(gòu)避免的。
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. 函數(shù)有多個(gè)出口
Message getMessage_NoRVO2(int in) { ? ? Message msg; ? ? if (in % 2) ? ? { ? ? ? ? return msg; ? ? } ? ? msg.a = 1; ? ? return msg; }
這個(gè)在GCC上驗(yàn)證發(fā)現(xiàn)RVO仍然生效,查看匯編發(fā)現(xiàn)只有一個(gè)retq指令,多個(gè)出口被優(yōu)化成一個(gè)了。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C/C++編譯報(bào)錯(cuò)printf was not declared in 
這篇文章主要介紹了C/C++編譯報(bào)錯(cuò)printf was not declared in this scope問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08C語言創(chuàng)建和操作單鏈表數(shù)據(jù)結(jié)構(gòu)的實(shí)例教程
這篇文章主要介紹了C語言創(chuàng)建和操作單鏈表數(shù)據(jù)結(jié)構(gòu)的實(shí)例教程,講解使用C語言實(shí)現(xiàn)鏈表結(jié)構(gòu)時(shí)指針的使用,需要的朋友可以參考下2016-04-04C語言數(shù)據(jù)結(jié)構(gòu) 棧的基礎(chǔ)操作
這篇文章主要介紹了C語言數(shù)據(jù)結(jié)構(gòu) 棧的基礎(chǔ)操作的相關(guān)資料,需要的朋友可以參考下2017-05-05C語言用遞歸函數(shù)對素?cái)?shù)進(jìn)行判斷流程
素?cái)?shù)判斷是編程語言學(xué)習(xí)過程中一個(gè)老生常談的話題,而它的實(shí)現(xiàn)也有多種算法,包括經(jīng)典的試除法(以及試除法的幾種優(yōu)化),進(jìn)階的素?cái)?shù)表篩選法,埃拉托斯特尼篩法和歐拉篩法(以及它們的優(yōu)化)等。對以上算法感興趣的朋友們,不妨搜索“素?cái)?shù)判斷的N種境界”來學(xué)習(xí)了解2022-09-09C語言經(jīng)典例程100例(經(jīng)典c程序100例)
這篇文章主要介紹了C語言經(jīng)典例程100例,經(jīng)典c程序100例,學(xué)習(xí)c語言的朋友可以參考一下2018-03-03