如何編輯網(wǎng)站內(nèi)容國內(nèi)新聞最新消息10條
W...Y的主頁😊
代碼倉庫分享💕?
?🍔前言:
今天我們依舊來完善補充C++,區(qū)分C++與C語言的區(qū)別。上一篇我們講了關(guān)鍵字、命名空間、C++的輸入與輸出、缺省參數(shù)等知識點。今天我們繼續(xù)走進(jìn)C++的世界。
目錄
函數(shù)重載
函數(shù)重載概念
?C++支持函數(shù)重載的原理--名字修飾(name Mangling)
引用
引用概念
引用特性
常引用
使用場景
?做參數(shù)
做返回值
?傳值、傳引用效率比較?
值和引用的作為返回值類型的性能比較?
引用和指針的區(qū)別
內(nèi)聯(lián)函數(shù)?
概念
特性
函數(shù)重載
函數(shù)重載有點像“一詞多義”,我們漢語的博大精深就經(jīng)常會出現(xiàn)一詞多義的現(xiàn)象,就如同一個笑話一樣:以前有一個笑話,國有兩個體育項目大家根本不用看,也不用擔(dān)心。一個是乒乓球,一個
是男足。前者是“誰也贏不了!”,后者是“誰也贏不了!”
函數(shù)重載概念
函數(shù)重載:是函數(shù)的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數(shù),這
些同名函數(shù)的形參列表(參數(shù)個數(shù) 或 類型 或 類型順序)不同,常用來處理實現(xiàn)功能類似數(shù)據(jù)類型
不同的問題。
C語言是不支持同名函數(shù)的,這就是C語言的一個“坑”。比如我們想要實現(xiàn)整數(shù)的相加與浮點數(shù)的相加:
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
當(dāng)我們在C語言中調(diào)用此函數(shù)時就會報錯,但是在C++中就不會。在C++中函數(shù)重載支持的條件是:函數(shù)名相同,參數(shù)不同。而函數(shù)名相同,參數(shù)不同的情況有三種:
1.類型不同。2.個數(shù)不同。3.順序不同。
舉三個不同例子:
#include<iostream>
using namespace std;
// 1、參數(shù)類型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、參數(shù)個數(shù)不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、參數(shù)類型順序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
這串代碼很好詮釋了三種不同,讓我們更容易理解。
注意:如果兩個函數(shù)構(gòu)成重載,不傳參數(shù)時使用函數(shù)參數(shù)的缺省參數(shù)就會存在二義性!!!?
#include<iostream>
using namespace std;
void f()
{cout << "f()" << endl;
}
void f(int a = 0)
{cout << "f(int a)" << endl;
}
int main()
{f();return 0;
}
?上述代碼就是一個二義性代碼,當(dāng)函數(shù)不傳參時兩個函數(shù)都可以調(diào)用,這時就非常危險。所以我們要避免這種情況的發(fā)生!
函數(shù)重載不同點全部在函數(shù)參數(shù)的地方,卻在返回值處沒有做過多工作,這時為什么呢?我們使用反證法假設(shè)返回值不同,但是函數(shù)調(diào)用卻不知道調(diào)用誰,所以只能是參數(shù)不同才可以區(qū)分。
?C++支持函數(shù)重載的原理--名字修飾(name Mangling)
為什么C++支持函數(shù)重載,而C語言不支持函數(shù)重載呢?
在C/C++中,一個程序要運行起來,需要經(jīng)歷以下幾個階段:預(yù)處理、編譯、匯編、鏈接。
1.實際項目通常是由多個頭文件和多個源文件構(gòu)成,而通過C語言階段學(xué)習(xí)的編譯鏈接,我們
可以知道,【當(dāng)前a.cpp中調(diào)用了b.cpp中定義的Add函數(shù)時】,編譯后鏈接前,a.o的目標(biāo)
文件中沒有Add的函數(shù)地址,因為Add是在b.cpp中定義的,所以Add的地址在b.o中。那么
怎么辦呢?
2. 所以鏈接階段就是專門處理這種問題,鏈接器看到a.o調(diào)用Add,但是沒有Add的地址,就
會到b.o的符號表中找Add的地址,然后鏈接到一起。
3. 那么鏈接時,面對Add函數(shù),鏈接接器會使用哪個名字去找呢?這里每個編譯器都有自己的
函數(shù)名修飾規(guī)則。
4. 由于Windows下vs的修飾規(guī)則過于復(fù)雜,而Linux下g++的修飾規(guī)則簡單易懂,下面我們使
用了g++演示了這個修飾后的名字。
5. 通過下面我們可以看出gcc的函數(shù)修飾后名字不變。而g++的函數(shù)修飾后變成【_Z+函數(shù)長度
+函數(shù)名+類型首字母】。?
采用C語言編譯器編譯后結(jié)果
采用C++編譯器編譯后結(jié)果?
結(jié)論:在linux下,采用g++編譯完成后,函數(shù)名字的修飾發(fā)生改變,編譯器將函數(shù)參
數(shù)類型信息添加到修改后的名字中。
Windows下名字修飾規(guī)則 :
對比Linux會發(fā)現(xiàn),windows下vs編譯器對函數(shù)名字修飾規(guī)則相對復(fù)雜難懂,但道理都
是類似的,我們就不做細(xì)致的研究了。
下面是對函數(shù)名修飾:
C/C++的調(diào)用約定http://blog.csdn.net/lioncolumn/article/details/10376891
6. 通過這里就理解了C語言沒辦法支持重載,因為同名函數(shù)沒辦法區(qū)分。而C++是通過函數(shù)修
飾規(guī)則來區(qū)分,只要參數(shù)不同,修飾出來的名字就不一樣,就支持了重載。
7. 如果兩個函數(shù)函數(shù)名和參數(shù)是一樣的,返回值不同是不構(gòu)成重載的,因為調(diào)用時編譯器沒辦
法區(qū)分。?
所以C語言不支持函數(shù)重載,C++支持函數(shù)重載!
引用
引用概念
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內(nèi)存空
間,它和它引用的變量共用同一塊內(nèi)存空間。比如:水滸傳中的林沖,被稱為“豹子頭”。
類型& 引用變量名(對象名) = 引用實體;
void TestRef()
{int a = 10;int& ra = a;//<====定義引用類型printf("%p\n", &a);printf("%p\n", &ra);
}
注意:引用類型必須和引用實體是同種類型的
引用特性
1. 引用在定義時必須初始化
2. 一個變量可以有多個引用
3. 引用一旦引用一個實體,再不能引用其他實體
void TestRef()
{int a = 10;// int& ra; ?// 該條語句編譯時會出錯int& ra = a;int& rra = a;printf("%p %p %p\n", &a, &ra, &rra);
}
?別名與原變量的地址相同,所以a創(chuàng)建了變量,b是a的別名,所以b++就是a++的結(jié)果。
int main()
{int a = 0;int& b = a;a++;b++;cout << a << endl << b << endl;return 0;
}
注意:也可以給別名取別名!!!?
當(dāng)我們創(chuàng)建一個函數(shù)時,參數(shù)是指針時,我們也可以用引用進(jìn)行表示。
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
所以在無哨兵位鏈表時,我們可以用引用代替二級指針!
引用與指針用法非常相似,但是引用能完全替代指針嗎?我們來看一段代碼:?
int main()
{int a = 0;int& c = a;int b = 1;//c變成b的別名還是給c賦值呢?c = b;printf("%d %d %d \n", a, b, c);return 0;
}
?上述代碼是c變成b的別名還是給c進(jìn)行賦值呢?如果c變成b的別名就是改變指向的?如果b給c進(jìn)行賦值就不能改變指向。很明顯不能改變指向。只是單純的賦值,所以引用與指針的區(qū)別出現(xiàn)了,引用無法替代指針。
常引用
什么是常引用,就是對不可修改的常量做引用,這樣做與#define宏定義有點類似,但是底層邏輯是不相同的,宏定義是在預(yù)處理階段進(jìn)行替換,所以常引用可以進(jìn)行調(diào)試出理,但是宏定義就不行。
?那怎樣進(jìn)行常引用定義呢?
void TestConstRef()
{const int a = 10;//int& ra = a; ?// 該語句編譯時會出錯,a為常量const int& ra = a;// int& b = 10; // 該語句編譯時會出錯,b為常量const int& b = 10;double d = 12.34;//int& rd = d; // 該語句編譯時會出錯,類型不同const int& rd = d;
}
以上是常引用定義的錯誤點,就是因為使用了常引用將本沒有權(quán)限的內(nèi)容變得有權(quán)限,導(dǎo)致不合理,所以出現(xiàn)錯誤。
總結(jié):常引用可以將定義的內(nèi)容權(quán)限縮小,但是不能進(jìn)行放大。
使用場景
?做參數(shù)
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
做參數(shù)在上文已經(jīng)提到,這里就不做過多的解釋。
做返回值
int& Count()
{static int n = 0;n++;// ...return n;
}
我們可以將返回值設(shè)置為int&,但是我們沒有進(jìn)行取別名的操作,為什么還可以進(jìn)行返回呢?因為計算機可以幫我們自動取一個別名(我們不知道)然后進(jìn)行函數(shù)返回值的返回。我們一般int的類型的返回值是將返回內(nèi)容進(jìn)行拷貝一份進(jìn)行返回,因為在函數(shù)棧幀調(diào)用后函數(shù)內(nèi)存就會被銷毀!那就有人會聞,函數(shù)棧幀都被銷毀了,取的別名傳的內(nèi)容就是一個錯誤的內(nèi)容嗎?
這就會引出我們下面的問題,下面代碼會輸出什么結(jié)果?
int& Add(int a, int b)
{int c = a + b;return c;
}
int main()
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1, 2) is :"<< ret <<endl;return 0;
}
?那為什么是7,不應(yīng)該是3嗎?
注意:如果函數(shù)返回時,出了函數(shù)作用域,如果返回對象還在(還沒還給系統(tǒng)),則可以使用
引用返回,如果已經(jīng)還給系統(tǒng)了,則必須使用傳值返回。
這里無論是3還是7都是不嚴(yán)謹(jǐn)?shù)?#xff0c;有的程序還會返回錯誤值,這都取決于內(nèi)存是否環(huán)給系統(tǒng)。但是我們這里給ret賦值一次,但是卻出現(xiàn)了第二次調(diào)用的值,就是因為ret是計算機給出c的別名,地址都是相同的,所以只要調(diào)用一次函數(shù)就會更新ret中的值!上面函數(shù)棧幀圖非常清楚,我們可以理解一下!!!?
所以用引用作為返回值去返回臨時變量是非常危險的,所以我們應(yīng)該返回static靜態(tài)變量!?
?傳值、傳引用效率比較?
以值作為參數(shù)或者返回值類型,在傳參和返回期間,函數(shù)不會直接傳遞實參或者將變量本身直
接返回,而是傳遞實參或者返回變量的一份臨時的拷貝,因此用值作為參數(shù)或者返回值類型,效
率是非常低下的,尤其是當(dāng)參數(shù)或者返回值類型非常大時,效率就更低。
#include <time.h>
struct A{ int a[10000]; };
void TestFunc1(A a){}
void TestFunc2(A& a){}
void TestRefAndValue()
{
A a;
// 以值作為函數(shù)參數(shù)
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作為函數(shù)參數(shù)
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分別計算兩個函數(shù)運行結(jié)束后的時間
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
當(dāng)我們進(jìn)行大規(guī)模傳參時,傳值需要進(jìn)行拷貝,而傳引用卻不需要,所以根據(jù)上述代碼我們可以看到傳引用比傳值的效率高很多。?
值和引用的作為返回值類型的性能比較?
#include <time.h>
struct A{ int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a;}
// 引用返回
A& TestFunc2(){ return a;}
void TestReturnByRefOrValue()
{
// 以值作為函數(shù)的返回值類型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作為函數(shù)的返回值類型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 計算兩個函數(shù)運算完成之后的時間
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
?返回值也是如此,傳值時需要將返回值進(jìn)行拷貝而傳引用不用,所以傳引用相率高。
?總結(jié):通過上述代碼的比較,發(fā)現(xiàn)傳值和指針在作為傳參以及返回值類型上效率相差很大。
引用和指針的區(qū)別
在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
上述代碼所打印出的地址相同,所以從語法層面來看引用是沒有空間開辟的。?
在底層實現(xiàn)上實際是有空間的,因為引用是按照指針方式來實現(xiàn)的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
上述代碼我們轉(zhuǎn)成匯編語言進(jìn)行觀看:
轉(zhuǎn)成匯編語言后,發(fā)現(xiàn)引用與指針的邏輯是相同的。?
?引用和指針的不同點:
1. 引用概念上定義一個變量的別名,指針存儲一個變量地址。
2. 引用在定義時必須初始化,指針沒有要求
3. 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何
一個同類型實體
4. 沒有NULL引用,但有NULL指針
5. 在sizeof中含義不同:引用結(jié)果為引用類型的大小,但指針始終是地址空間所占字節(jié)個數(shù)(32
位平臺下占4個字節(jié))
6. 引用自加即引用的實體增加1,指針自加即指針向后偏移一個類型的大小7. 有多級指針,但是沒有多級引用
8. 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
9. 引用比指針使用起來相對更安全
內(nèi)聯(lián)函數(shù)?
概念
以inline修飾的函數(shù)叫做內(nèi)聯(lián)函數(shù),編譯時C++編譯器會在調(diào)用內(nèi)聯(lián)函數(shù)的地方展開,沒有函數(shù)調(diào)
用建立棧幀的開銷,內(nèi)聯(lián)函數(shù)提升程序運行的效率。
如果在上述函數(shù)前增加inline關(guān)鍵字將其改成內(nèi)聯(lián)函數(shù),在編譯期間編譯器會用函數(shù)體替換函數(shù)的
調(diào)用。
查看方式:
1. 在release模式下,查看編譯器生成的匯編代碼中是否存在call Add
2. 在debug模式下,需要對編譯器進(jìn)行設(shè)置,否則不會展開(因為debug模式下,編譯器默認(rèn)不
會對代碼進(jìn)行優(yōu)化,以下給出vs2013的設(shè)置方式)
?
特性
1. inline是一種以空間換時間的做法,如果編譯器將函數(shù)當(dāng)成內(nèi)聯(lián)函數(shù)處理,在編譯階段,會
用函數(shù)體替換函數(shù)調(diào)用,缺陷:可能會使目標(biāo)文件變大,優(yōu)勢:少了調(diào)用開銷,提高程序運
行效率。
2. inline對于編譯器而言只是一個建議,不同編譯器關(guān)于inline實現(xiàn)機制可能不同,一般建
議:將函數(shù)規(guī)模較小(即函數(shù)不是很長,具體沒有準(zhǔn)確的說法,取決于編譯器內(nèi)部實現(xiàn))、不
是遞歸、且頻繁調(diào)用的函數(shù)采用inline修飾,否則編譯器會忽略inline特性。下圖為
《C++prime》第五版關(guān)于inline的建議:
3. inline不建議聲明和定義分離,分離會導(dǎo)致鏈接錯誤。因為inline被展開,就沒有函數(shù)地址
了,鏈接就會找不到?
?所以inline的特性與register非常相似,是一種“請求”,程序會根據(jù)實際進(jìn)行選擇優(yōu)化!!!
以上就是本次博客全部內(nèi)容,感謝大家觀看,一鍵三連支持一下博主吧!!!?