目錄
一. 前言
二. 引用
2.1 引用的概念
2.2 引用的使用
2.3 引用的特性
2.4 常引用
2.5 引用的使用場景
2.6 傳值、傳引用效率比較
2.7 引用和指針的區別
?三. 內聯函數
3.1 內聯函數的概念
3.2 內聯函數的特性?
一. 前言
? ? ? ? 上期說道,C++是在C的基礎之上,容納進去了面向對象編程思想,并增加了許多有用的庫,以及編程范式等。我們介紹了部分C++為了補充C語言語法上的不足而新增的內容,如命名空間,缺省參數,函數重載等等,上期傳送門【C++深入淺出】初識C++(上篇)
http://t.csdn.cn/UjbIo? ? ? ? 本期將繼續介紹C++剩下的一些有趣的功能,如引用,內聯函數等等,這也是為了后面的類和對象打好基礎。
?
? ? ? ? ? 話不多說,直接上菜!!!
二. 引用
2.1 引用的概念
????????引用并不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內存空間,它和它引用的變量共用同一塊內存空間。
????????引用就相當于我們給別人起昵稱。例如你叫你女朋友小笨豬,那么對你而言,小笨豬就是你的女朋友,和叫名字是一個意思,既不是其他任何人,你也不會因此多一個女朋友
2.2 引用的使用
? ? ? ??類型& 引用變量名(對象名) = 引用實體
void Test()
{int a = 10;int& ra = a; //<====定義引用類型,此時ra就是變量a的別名,ra與a是同一塊內存空間printf("a的地址為%p\n", &a);printf("ra的地址為%p\n", &ra);
}int main()
{Test();return 0;
}
我們看到變量a和引用變量ra的地址是一樣的,說明它們共用同一塊內存空間。
?2.3 引用的特性
????????使用引用時需要注意以下幾點特性
? ? ? ? ?1、引用在定義時必須初始化
int main()
{int a = 10;int& b; //錯誤寫法,會報錯int& b=a; //正確寫法return 0;
}
? ? ? ? ?2、一個變量可以有多個引用
int main()
{int a = 10;//下面的b,c,d均是變量a的別名int& b = a;int& c = a;int& d = c;printf("%p %p %p %p\n", &a,&b,&c,&d);return 0;
}
?? ? ? ? ?3、引用一旦引用一個實體,就不能引用其他實體
int main()
{int a = 10;int& b = a; //b是a的別名int c = 20; //能不能將b改成c的別名呢?b = c; //不行,這條語句是將c的值賦給引用變量b,即修改變量a的值,并不是讓b引用cprintf("&a = %p &b = %p &c = %p\n", &a,&b,&c);printf("a = %d b = %d c = %d\n", a, b, c);return 0;
}
?? ? ? ? ?4、引用類型必須和引用實體是同種類型的
int main()
{int a = 10;double& b = a; //這種寫法會報錯return 0;
}
? ? ? ? 我們看到編譯器報錯說非常量限定,那如果我們加上const修飾呢?如下:
const double& b = a;
? ? ? ? 我們驚訝地發現通過了編譯,說明上面不是因為int和double類型不一樣而報錯,那究竟是為什么呢?下面我們來分析分析
? ? ? ? ?實際上,由于引用實體和引用變量的類型不同,編譯器會自動進行隱式類型轉換。編譯器會生成一個double類型的臨時變量tmp,然后將a的內容以某種形式放到臨時變量tmp中,最后再讓b引用臨時變量tmp。
int main()
{int a = 10;const double& b = a;//類似于下面的步驟int a = 10;double tmp = a; //將a的值轉換賦給tmpconst double& b = tmp; //b再引用tmpreturn 0;
}
? ? ? ? ?由于臨時變量具有常屬性,因此tmp的類型就是const double,用double類型的引用變量引用const double類型的變量,這無疑是一種權限的放大,是不被允許的。就好比別人大門緊縮不然你進,你偏偏另辟蹊徑從窗戶翻入,這無疑是犯法的,私闖民宅。這就是為什么編譯器會報出非常量限定的錯誤的原因,引用變量d需要加上const修飾,權限的平移是被允許的。
? ? ? ? 最后,本來臨時變量tmp在當前語句結束后就會被銷毀,但此時被b所引用,其生命周期就自動被延長了。
? ? ? ? ?分析了這么多,下面我們用代碼來進行驗證一下:
int main()
{int a = 10;const double& b = a;printf("&a = %p , &b = %p\n", &a, &b); //求a,b空間的地址printf("修改前 a = %d , b = %.2lf\n", a, b);a = 20;//b = 30; //這句代碼會報錯,被const修飾的變量不可修改printf("修改后 a = %d , b = %.2lf\n", a, b);return 0;
}
我們發現a的地址和b的地址不同,這說明了b并不是變量a的引用,而是引用了新形成的臨時變量。并且,當我們對a進行修改時,b中的內容并沒有發生改變,這也印證了a和b不是同一塊內存空間。最后,當我們想要對b的內容進行修改時,編譯器會直接報錯,說明b所在的空間具有常屬性。
2.4 常引用
? ? ? ? 被const關鍵字修飾的引用變量我們稱為常引用。我們無法通過常引用來修改引用實體的值,如下:
#include<iostream>
using namespace std;
int main()
{int a = 10;const int& b = a;//b++; //會報錯,b是常引用,無法修改a++; //a是普通變量,允許修改cout << "a = " << a <<' ' << "b = " << b;return 0;
}
? ? ? ? 前面我們提到了權限不能放大,也就是說:普通引用不能引用常屬性變量。但是,權限允許平移或者縮小,即常引用可以引用常屬性變量、常引用可以引用普通變量。如下:
#include<iostream>
using namespace std;
int main()
{int a = 10;const int& b = a; //權限的縮小,const引用引用普通變量,編譯正常const int aa = 10;const int& bb = aa;//權限的縮小,const引用引用const變量,編譯正常int& cc = aa;//權限的放大,普通引用引用const變量,報錯return 0;
}
?2.5 引用的使用場景
? ? ? ? 引用的使用場景一般有兩個:做函數參數、做函數返回值。
? ? ? ? 1、引用作為函數參數
????????在C語言中,如果我們調用函數時使用傳值調用,那么形參的改變是不會影響實參的,形參是實參的臨時拷貝。如果我們想在函數中對實參進行修改,那就必須使用傳址調用,通過地址對實參的值進行修改。
? ? ? ? 而在C++中,新增了引用的語法,我們可以使用引用作為函數的形參,此時形參就是實參的一個別名,并不會額外開辟空間,形參和實參共同內存空間,修改形參也就是對實參進行修改。具體實現方式如下
#include<iostream>
using namespace std;
void ModifyFun(int& x) //引用作為函數參數
{x = 100;
}
int main()
{int a = 10;cout << "調用前" << a << endl;ModifyFun(a);cout << "調用后" << a << endl;
}
#include<iostream>
using namespace std;
void Swap(int& x , int& y) //引用作為函數參數
{int tmp = x;x = y;y = tmp;
}
int main()
{int a = 10;int b = 20;cout << "交換前:" << "a = " << a << " b = " << b << endl;Swap(a, b);cout << "交換后:" << "a = " << a << " b = " << b << endl;
}
? ? ? ? 2、引用作為函數返回值
? ? ? ? 引用也可以作為函數的返回值,如下:
#include<iostream>
using namespace std;
int& Count()
{static int n = 0; //n是一個靜態變量,函數調用結束后不會銷毀cout << n << endl;;return n;
}
int main()
{int& k = Count();k++;Count();return 0;
}
在Count()函數通過引用返回n,此時main函數中的引用變量k就是n的別名,當我們在main函數中修改k,就相當于對靜態變量n做修改。
? ? ? ? 但是,如果以下情況使用引用返回會出現什么情況呢??
int& Add(int a, int b)
{int c = a + b; //c是局部變量,Add調用結束后被銷毀return c;
}
int main()
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1, 2) is :" << ret << endl;return 0;
}
?很驚訝的發現,最終ret變量的值不是3而是7,為什么呢?
?這就要來談談上述代碼出現的野引用問題了。
我們通過下圖來進行分析?
?總結:函數返回時,如果出了函數作用域,返回對象還在(還沒銷毀還給系統),則可以使用
引用返回;如果已經還給系統了,則必須使用傳值返回。
2.6 傳值、傳引用效率比較
? ? ? ? 在C/C++中,以值作為參數或者返回值類型,在傳參和返回期間,函數并不會直接傳遞實參或者將變量本身直接返回,而是傳遞實參或者返回變量的一份臨時拷貝,因此用值作為參數或者返回值類型,需要額外進行拷貝,效率是非常低下的,尤其是當參數或者返回值類型非常大時,效率就更低。
? ? ? ? 而如果以引用作為參數或者返回值類型,由于引用是作為變量的別名,并不會額外開辟空間形成拷貝。因此在傳參和返回期間,就相當于直接傳遞實參或將變量本身直接返回,效率大大提升。下面我們通過代碼來更直觀地看看二者的效率差距:
? ? ? ? 值和引用作為函數參數的效率差距
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a)
{;
}
void TestFunc2(A& a)
{;
}
void TestRefAndValue()
{A a;// 以值作為函數參數size_t begin1 = clock(); //clock()函數返回程序運行到調用clock()函數所耗費的時間,單位是msfor (size_t i = 0; i < 100000; ++i)TestFunc1(a);size_t end1 = clock();// 以引用作為函數參數size_t begin2 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc2(a);size_t end2 = clock();// 分別計算兩個函數運行結束后的時間cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{TestRefAndValue();return 0;
}
?? ? ? ? 值和引用作為返回值類型的效率差距
#include <time.h>
struct A
{int a[10000];
}a;
// 值返回
A TestFunc1()
{ return a;
}
// 引用返回
A& TestFunc2()
{ return a;
}
void TestRefAndValue()
{// 以值作為函數的返回值類型size_t begin1 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc1();size_t end1 = clock();// 以引用作為函數的返回值類型size_t begin2 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc2();size_t end2 = clock();// 計算兩個函數運算完成之后的時間cout << "TestFunc1 time:" << end1 - begin1 << endl;cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{TestRefAndValue();return 0;
}
通過上述代碼的比較,我們發現傳值和引用在作為傳參以及返回值類型上效率相差很大。傳引用的效率遠高于傳值。因此能使用引用就盡量使用引用,提高效率。
2.7 引用和指針的區別
????????在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間。
? ? ? ? 但在底層實現上實際是有空間的,因為引用是按照指針方式來實現的。這點我們可以參照二者編譯后生成的匯編代碼證明
int main()
{//引用int a = 10;int& ra = a;//指針ra = 20;int* pa = &a;*pa = 20;return 0;
}
可見,引用和指針的匯編代碼是一模一樣的,最后都是通過變量a的地址來修改a。
不過,引用和指針還是有不同點的,如下:
- 引用概念上定義一個變量的別名,指針存儲一個變量地址。
- 引用在定義時必須初始化,指針沒有要求
- 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體
- 引用必須初始化,故沒有NULL引用,但有NULL指針
- sizeof的含義不同,sizeof(引用變量)的結果為引用實體的類型大小,而sizeof(指針)始終是地址空間所占字節個數(32位平臺下占4個字節,64位平臺下占8個字節)
- 引用自增即引用的實體增加1,指針自增即指針向后偏移一個類型的大小
- 有多級指針,但是沒有多級引用
- 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
- 從安全性的角度,引用比指針使用起來相對更安全
三. 內聯函數
3.1 內聯函數的概念
????????以inline關鍵字修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,不會調用函數建立棧幀,因此內聯函數提升程序運行的效率。
? ? ? ? 我們可以通過匯編代碼來驗證加上inline的函數是否會被調用
? ? ? ? 沒加inline關鍵字:
int Add(int x, int y)
{return x + y;
}
int main()
{int ans = 0;ans = Add(1, 2);return 0;
}
?? ? ? ? ?加上inline關鍵字:
inline int Add(int x, int y)
{return x + y;
}
int main()
{int ans = 0;ans = Add(1, 2);return 0;
}
可以看到,內聯函數并不會生成對應的call指令,而是直接被替換到函數調用處,減少了調用函數建立棧幀的開銷。
?? ? ? ? ?注意事項:
? ? ? ? ??內聯函數的效果需要在release模式才會體現。因為在debug模式下編譯器默認不會對代碼進行優化,顧不會進行展開。當然我們也可以進行設置,方法如下(VS2022):
1、找到當前項目屬性設置頁:
?2、設置調試信息格式:
??3、設置內聯函數擴展:
3.2 內聯函數的特性?
主要有如下幾點特性:
- inline是一種以空間換時間的做法,如果編譯器將函數當成內聯函數處理,在編譯階段,會用函數體替換函數調用。缺陷:可能會使目標文件變大;優勢:少了調用建立棧幀開銷,提高程序運行效率。
- inline對于編譯器而言只是一個建議,不同編譯器關于inline實現機制可能不同,一般建
議:將函數規模較小(即函數不是很長,具體沒有準確的說法,取決于編譯器內部實現)、不是遞歸、且頻繁調用的函數采用inline修飾,否則編譯器會忽略inline特性(編譯器也是很聰明的,可不要貪杯噢)。以下為《C++prime》第五版關于inline的描述:
- inline不建議聲明和定義分離,分離會導致鏈接錯誤。因為inline被展開,符號表中就沒有函數地址了,鏈接就會找不到。
// in.h #include <iostream> using namespace std; inline void f(int i);// in.cpp #include "in.h" void fun(int i) {cout << i << endl; }// main.cpp #include "in.h" int main() {fun(10);return 0; }
報錯原因:由于in.h文件中只有函數的聲明沒有定義,顧在編譯階段main.cpp中的fun() 函數無法進行展開,只能在鏈接階段進行鏈接。但是由于in.cpp的fun()函數被聲明為內聯函數,fun()函數并不會進入符號表,最后就會導致鏈接時找不到函數地址,報錯。
以上,就是本期的全部內容啦🌸
制作不易,能否點個贊再走呢🙏