C++性能榨汁機之指針與引用
來自專欄 C++性能榨汁機95 人贊了文章
前言
C語言的指針讓我們有了直接操控內存的強大能力,同時指針也是使用C語言時最容易出問題的地方。C++在繼承了C語言的指針的同時又給我們提供了另外一個武器:引用。今天我們來探討一下指針與引用的異同以及兩者之間的性能差異。
指針與引用的區別
引用與指針的區別也算是C++中老生常談的話題了,無論是在期末考試的試卷上還是找工作時的筆試面試上,這個問題都是「常客」。對於這個問題更加詳細的解答請參考《More Effective C++》的條款1,本文主要提一下以下三個區別:
- 引用必須初始化,而指針可以不初始化。
我們在定義一個引用的時候必須為其指定一個初始值,但是指針卻不需要。
int &r; //不合法,沒有初始化引用int *p; //合法,但p為野指針,使用需要小心
2. 引用不能為空,而指針可以為空。
由於引用不能為空,所以我們在使用引用的時候不需要測試其合法性,而在使用指針的時候需要首先判斷指針是否為空指針,否則可能會引起程序崩潰。
void test_p(int* p){ if(p != nullptr) //對p所指對象賦值時需先判斷p是否為空指針 *p = 3; return;}void test_r(int& r){ r = 3; //由於引用不能為空,所以此處無需判斷r的有效性就可以對r直接賦值 return;}
3. 引用不能更換目標
指針可以隨時改變指向,但是引用只能指向初始化時指向的對象,無法改變。
int a = 1;int b = 2;int &r = a; //初始化引用r指向變數aint *p = &a; //初始化指針p指向變數ap = &b; //指針p指向了變數br = b; //引用r依然指向a,但a的值變成了b
引用的使用場景
只看兩者區別的話,我們發現引用可以完成的任務都可以使用指針完成,並且在使用引用時限制條件更多,那麼C++為什麼要引入「引用」呢?
限制條件多不一定是缺點,C++的引用在減少了程序員自由度的同時提升了內存操作的安全性和語義的優美性。比如引用強制要求必須初始化,可以讓我們在使用引用的時候不用再去判斷引用是否為空,讓代碼更加簡潔優美,避免了指針滿天飛的情形。除了這種場景之外引用還用於如下兩個場景:
- 引用型參數
一般我們使用const reference參數作為只讀形參,這種情況下既可以避免參數拷貝還可以獲得與傳值參數一樣的調用方式。
void test(const vector<int> &data){ //...}int main(){ vector<int> data{1,2,3,4,5,6,7,8}; test(data);}
2. 引用型返回值
C++提供了重載運算符的功能,我們在重載某些操作符的時候,使用引用型返回值可以獲得跟該操作符原來語法相同的調用方式,保持了操作符語義的一致性。一個例子就是operator []操作符,這個操作符一般需要返回一個引用對象,才能正確的被修改。
vector<int> v(10);v[5] = 10; //[]操作符返回引用,然後vector對應元素才能被修改 //如果[]操作符不返回引用而是指針的話,賦值語句則需要這樣寫*v[5] = 10; //這種書寫方式,完全不符合我們對[]調用的認知,容易產生誤解
指針與引用的性能差距
指針與引用之間有沒有性能差距呢?這種問題就需要進入彙編層面去看一下。我們先寫一個test1函數,參數傳遞使用指針:
void test1(int* p){ *p = 3; //此處應該首先判斷p是否為空,為了測試的需要,此處我們沒加。 return;}
該代碼段對應的彙編代碼如下:
pushq %rbpmovq %rsp, %rbpmovq %rdi, -8(%rbp)movq -8(%rbp), %raxmovl $3, (%rax)noppopq %rbpret
上述代碼1、2行是參數調用保存現場操作;第3行是參數傳遞,函數調用第一個參數一般放在rdi寄存器,此行代碼把rdi寄存器值(指針p的值)寫入棧中;第4行是把棧中p的值寫入rax寄存器;第5行是把立即數3寫入到rax寄存器值所指向的內存中,此處要注意(%rax)兩邊的括弧,這個括弧並並不是可有可無的,(%rax)和%rax完全是兩種意義,(%rax)代表rax寄存器中值所代表地址部分的內存,即相當於C++代碼中的*p,而%rax代表rax寄存器,相當於C++代碼中的p值,所以彙編這裡使用了(%rax)而不是%rax。
我們再寫出參數傳遞使用引用的C++代碼段test2:
void test2(int& r){ r = 3; //賦值前無需判斷reference是否為空 return;}
這段代碼對應的彙編代碼如下:
pushq %rbpmovq %rsp, %rbpmovq %rdi, -8(%rbp)movq -8(%rbp), %raxmovl $3, (%rax)noppopq %rbpret
我們發現test2對應的彙編代碼和test1對應的彙編代碼完全相同,這說明C++編譯器在編譯程序的時候將指針和引用編譯成了完全一樣的機器碼。所以C++中的引用只是C++對指針操作的一個「語法糖」,在底層實現時C++編譯器實現這兩種操作的方法完全相同。
總結
C++中引入了引用操作,在對引用的使用加了更多限制條件的情況下,保證了引用使用的安全性和便捷性,還可以保持代碼的優雅性。在適合的情況使用適合的操作,引用的使用可以一定程度避免「指針滿天飛」的情況,對於提升程序魯棒性也有一定的積極意義。最後,指針與引用底層在實現大部分情況下都是一樣的,不用擔心兩者的性能差距。
推薦閱讀:
※對象沒有默認構造函數,如何定義對象數組?
※C語言中 *p++ = *p 是如何工作的?
※C++ #include " " 與 <>有什麼區別?
※std::vector會在不同dll中傳遞修改時帶來影響嗎?
※C++ primer 第四版這段關於vector的程序是否有未定義的行為?