屏幕前的你,一起加油啊?。?!
a.之前的C語言學(xué)習(xí)中我們就了解過全局和局部這部分的知識了,在C++里面他們有一個新的名詞就是域,域就相當(dāng)于一片領(lǐng)地,如果想定義兩個一模一樣的變量在同一個域中,這顯然是不行的,會出現(xiàn)變量重命名的問題,但是這樣的問題還是比較常見的,因為c++和C語言中都有很多的模板,函數(shù)庫等等,難免我們定義的和庫里面定義的,產(chǎn)生命名沖突和名字污染,namespace所創(chuàng)建的命名空間就是用來解決這樣的問題的。
為了防止命名沖突的產(chǎn)生,C++規(guī)定了命名空間的出現(xiàn),這可以很好的解決問題,我們可以把我們想定義的東西都放到我們自己定義的命名空間里面去,這樣就不會產(chǎn)生命名沖突的問題了。
#include#includeint rand = 10;
// C語言沒辦法解決類似這樣的命名沖突問題,所以C++提出了namespace來解決
int main()
{printf("%d\n", rand);//stdlib.h文件里面有rand函數(shù)。
return 0;
}
// 編譯后后報錯:error C2365: “rand”: 重定義;以前的定義是“rand函數(shù)”
b.編譯器有一套自己的查找變量的規(guī)則:
1.默認(rèn)先去局部域找變量,再到全局域里面去找變量
2.如果我們利用域作用限定符限定了編譯器要查找的域,那編譯器就會按照我們設(shè)定的查找規(guī)則來查找
#includeint a = 0;//全局域中定義變量a
int main()
{int a = 1;//局部域中定義變量a,所以在不同的域中是可以定義同一個變量的。
printf("%d\n", a);//先去局部域里面找a,再到全局域里面找a
// ::域作用限定符
printf("%d\n", ::a);//::的左面是空白,代表作用于全局域,指定編譯器查找的位置是全局域
return 0;
}
c.我們現(xiàn)在利用命名空間wyn封裝起來了rand,這時候就不會和stdlib.h文件中的rand()函數(shù)產(chǎn)生命名沖突了。定義的形式請看下面代碼。
命名空間中的rand不是一個局部變量,而是一個全局變量,因為只有定義在函數(shù)里面,存放到棧上的變量才是局部變量。rand存放在靜態(tài)區(qū),并且現(xiàn)在的namespace根本就不是一個函數(shù),自然也就說明rand不是局部變量,而是全局變量。
那么變量定義在命名空間中和定義在全局域中有什么區(qū)別呢?
其實區(qū)別就是編譯器查找的規(guī)則不同,如果你指定查找的域,那編譯器就去你定義的命名空間查找,如果你不指定查找的域,那編譯器就先去局部域查找,再去全局域查找。
d.命名空間也可嵌套定義,一個命名空間當(dāng)中又細(xì)分多個命名空間,這樣也是可以的,下面代碼的wyn空間當(dāng)中就嵌套定義了N1,N1中又嵌套定義了N2。
namespace wyn
{int rand = 10;
int Add(int left, int right)
{return left + right;
}
struct Node
{struct Node* next;
int val;
};
namespace N1
{int a;
int b;
int Add(int left, int right)
{ return left + right;
}
namespace N2
{ int c;
int d;
int Sub(int left, int right)
{ return left - right;
}
}
}
}
e.
同一個工程中的不同的文件允許存在多個相同名稱的命名空間,編譯器最后會合成到同一個命名空間當(dāng)中去。
同一個文件里面的相同名稱的命名空間也是會被編譯器合并的。
C++官方封裝好了一個命名空間叫做std,它和其他的一些命名空間都被封裝到iostream頭文件里面,C++所使用的cin和cout都被封裝在iostream文件中的std命名空間。
這其實變相的幫助我們解決了一個問題,就是如果我們平常中的命名和官方庫產(chǎn)生沖突時,我們也不害怕,因為兩者所處的域是不同的,互不干擾。
a.利用域作用限定符
這種命名空間的使用方式堪稱經(jīng)典,使用語法:域名+域作用限定符+域中的成員名
b.展開整個命名空間
這種使用方式不是很推薦,因為一旦將命名空間全部展開,雖然我們在使用上可以直接使用,但是這會產(chǎn)生極大的命名沖突安全問題隱患,所以如果你只是寫個小算法,小程序等等,可以這么使用。但如果在大型工程里面還是不要這么用了,因為出了問題,就麻煩了。
c.展開域中的部分成員
強(qiáng)烈推薦這樣的使用方式,將我們常用的某些函數(shù)展開,我們在定義時,只要避免和部分展開的重名即可,這樣的使用方式也較為安全,所以強(qiáng)烈推薦。
using namespace std;//這個東西存在的意義就是將命名空間里面的內(nèi)容展開,用起來會方便很多
//當(dāng)然反面的意義就是將命名空間的域拆開了,會產(chǎn)生命名沖突問題的隱患。
//日常練習(xí),寫個算法或小程序等等,這么使用可以,因為一般情況下不會產(chǎn)生命名沖突的問題,但項目里面最好不要這么用。
using std::cout;//將常用的展開,自己在定義的時候,盡量避免和常用的重名即可。
int main()
{//下面的所有訪問都必須在iostream文件展開的基礎(chǔ)上進(jìn)行,只有展開后,那些大量的命名空間才會出現(xiàn),下面的代碼才可以訪問
//命名空間里面的變量、函數(shù)、結(jié)構(gòu)體等等
//第一種訪問方式:指定域訪問
std::cout<< "hello world"<< std::endl;
std::cout<< "hello world"<< std::endl;
std::cout<< "hello world"<< std::endl;
//第二種訪問方式:將域在前面全部展開,編譯器會先在局部找cout,然后去全局找cout,正好域全部展開了,全局自然存在cout
cout<< "hello world"<< endl;
cout<< "hello world"<< endl;
cout<< "hello world"<< endl;
//第三種訪問方式:將域指定展開,只展開域中的某些常用成員,方便我們使用那些常用的函數(shù)或結(jié)構(gòu)體。
cout<< "hello world"<< std::endl;//endl沒有被展開,需要指定訪問的域
cout<< "hello world"<< std::endl;
cout<< "hello world"<< std::endl;
}
二、C++輸入&輸出(iostream+std)a.
使用cout標(biāo)準(zhǔn)輸出對象(控制臺)和cin標(biāo)準(zhǔn)輸入對象(鍵盤)時,必須包含< iostream >頭文件以及按命名空間使用方法使用std。
b.
使用C++輸入輸出更方便,不需要像printf/scanf輸入輸出時那樣,需要手動控制格式。
C++的輸入輸出可以自動識別變量類型。
c.
<<是流插入運算符,>>是流提取運算符,endl是特殊的C++符號,表示換行輸出,他也被包含在iostream頭文件中
注意:
早期標(biāo)準(zhǔn)庫將所有功能在全局域中實現(xiàn),聲明在.h后綴的頭文件中,使用時只需包含對應(yīng)頭文件即可,后來將其實現(xiàn)在std命名空間下,為了和C語言的頭文件區(qū)分,也為了正確使用命名空間,規(guī)定C++頭文件不帶.h;所以我們可以看到iostream是不帶.h的。
#includeusing namespace std;//我們平常寫的時候全部展開std命名空間,以后寫項目還是盡量不要這樣寫。
int main()
{//可以自動識別變量的類型,相比c的優(yōu)勢printf / scanf
//<< 流插入運算符
cout<< "hello world!!!"<< '\n';
cout<< "hello world!!!"<< endl;//兩種寫法等價,C++喜歡下面這種寫法
int a;
double b;
char c;
// >>流提取運算符
cin >>a;
cin >>b >>c;
cout<< a<< endl;
cout<< b<< ' '<< c<< endl;
//C++也有不方便的地方,如果你要輸出小數(shù)點后幾位,建議還是使用C語言來實現(xiàn)
//還有一種場景是要求挨著打印出變量的類型,這時候用C語言也是較方便的。
//C和C++哪個方便就用哪個
printf("%.2f\n", b);
cout<< "int:"<< a<< ' '<< "double:"<< b<< ' '<< "char:"<< (int)c<< endl;//c前面加個強(qiáng)制類型轉(zhuǎn)換,輸出ascll
printf("int:%d double:%.2f char:%d", a, b, c);
return 0;
}
三、缺省參數(shù)在聲明或定義函數(shù)時,給函數(shù)指定一個缺省值。
調(diào)用該函數(shù)時,如果實參沒有指定傳什么,函數(shù)就使用該缺省值,如果指定了,那就使用實參的值。
#includeusing namespace std;
void Func(int a = 0)//缺省值
{cout<< a<< endl;
}
int main()
{Func();//函數(shù)擁有缺省值之后,可以給函數(shù)傳參也可以不給他傳參。
Func(10);
return 0;
}
1.全缺省參數(shù)在給全缺省參數(shù)的函數(shù)傳參時,我們必須從左向右依次傳參,不可以中間空出來,跳躍的傳參,這樣的傳參方式編譯器是不支持的
void Func(int a = 10, int b = 20, int c = 30)
{cout<< "a = "<< a<< endl;
cout<< "b = "<< b<< endl;
cout<< "c = "<< c<< endl;
}
int main()
{Func();
Func(1);//默認(rèn)傳給了a
Func(1, 2);//從左向右依次傳參數(shù)
//Func(1, , 2);//不可以中間空出來,跳過b,只給a和c傳,這樣編譯器是不支持的。
Func(1, 2, 3);
return 0;
}
2.半缺省參數(shù)半缺省參數(shù)的函數(shù)在設(shè)計時,缺省參數(shù)必須得是從右向左連續(xù)缺省。
void Func(int a, int b = 10, int c )//這樣是不可以的
{cout<< "a = "<< a<< endl;
cout<< "b = "<< b<< endl;
cout<< "c = "<< c<< endl;
}
void Func(int a, int b , int c=10)//這樣是可以的
{cout<< "a = "<< a<< endl;
cout<< "b = "<< b<< endl;
cout<< "c = "<< c<< endl;
}
int main()
{Func(1);
Func(1, 2);
Func(1, 2, 3);
return 0;
}
3.(帶有缺省參數(shù))函數(shù)的定義和聲明a.
帶有缺省參數(shù)的函數(shù)在定義和聲明時,C++有特殊的規(guī)定,在函數(shù)的聲明部分中寫出缺省參數(shù),在函數(shù)的定義部分中不寫缺省參數(shù),如下面代碼所示。
b.
如果聲明與定義中同時出現(xiàn)缺省值,而恰巧兩個缺省值是不同的,這樣的話,編譯器是無法確定改用哪個缺省值。
c.
缺省值必須是常量(情況居多)或全局變量,C語言是不支持缺省參數(shù)這種概念的。
C++允許在一個域中同時出現(xiàn)幾個功能類似的同名函數(shù),這些函數(shù)常用來處理實現(xiàn)功能類似數(shù)據(jù)類型不同的問題。
下面的兩個函數(shù)在C++中是支持同時存在的,但在C語言中是不支持的。
void Swap(int* p1, int* p2)
{int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Swap(double* p1, double* p2)
{double tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
1.1 形參的類型不同int add(int x, int y)//int類型
{return x + y;
}
double add(double x, double y)//double類型
{return x + y;
}
1.2 形參的個數(shù)不同void f()//0個形參
{cout<< "f()"<< endl;
}
void f(int a)//一個形參
{cout<< "f(int a)"<< endl;
}
int main()
{f();
f(1);
return 0;
}
1.3 形參的類型順序不同void f(int a, char b)//int char
{cout<< "f(int a,char b)"<< endl;
}
void f(char a, int b)//char int
{cout<< "f(char a,int b)"<< endl;
}
//上面的兩個函數(shù)算函數(shù)重載,因為參數(shù)的類型順序不同
//下面的兩個函數(shù)不算函數(shù)重載,因為編譯器是按照參數(shù)類型識別的,并不是按照參數(shù)的名字來識別的。
void f(int a, int b)//int int
{cout<< "f(int a,int b)"<< endl;
}
void f(int a, int b)//int int
{cout<< "f(int a,int b)"<< endl;
}
int main()
{f(106, 'A');
f('A', 106);
return 0;
}
2.函數(shù)重載+缺省參數(shù)(編譯器懵逼的代碼)下面的兩個函數(shù)確實構(gòu)成函數(shù)重載,但在調(diào)用時,如果我們不指定實參的值,那就會產(chǎn)生二義性,編譯器是不知道該調(diào)用哪個函數(shù)的,所以我們盡量不要寫出這樣的函數(shù)重載。
void f()//0個形參
{cout<< "f()"<< endl;
}
void f(int a = 0, char b = 1)//2個形參
{cout<< "f(int a,char b)"<< endl;
}
int main()
{f(10);
f(10, 20);
f();//這里會報錯:對重載函數(shù)的調(diào)用不明確 -- 產(chǎn)生二義性 -- 編譯器蒙蔽
return 0;
}
3.C++支持函數(shù)重載的原理(匯編下的函數(shù)名修飾規(guī)則)C/C++程序運行原理:
稍微帶大家復(fù)習(xí)一些程序運行原理部分的知識:
假設(shè)當(dāng)前a.cpp中調(diào)用了b.cpp中定義的Add函數(shù),那么在編譯結(jié)束之后,a.cpp和b.cpp都會產(chǎn)生目標(biāo)文件.obj,每個目標(biāo)文件中都會有他們自己的符號表,匯總了全局域里面的函數(shù)名,變量名,結(jié)構(gòu)體名等等。
編譯器看到a.obj中調(diào)用了Add函數(shù),但是沒有Add的地址,這時鏈接器就會到b.obj中找到Add的地址并且把它鏈接到一起,進(jìn)行符號表的合并。
那么在鏈接時遇到函數(shù),編譯器是依靠什么來找到函數(shù)的地址呢?依靠的其實就是函數(shù)名,每個函數(shù)名又都有自己的函數(shù)名修飾規(guī)則,我們接下來用gcc和g++編譯器看一下匯編代碼中的函數(shù)名是如何修飾的。
_z3Addii,
3代表3個字符,緊跟著函數(shù)名Add,然后是參數(shù)類型的縮寫ii分別是int int
_z4funcidpi,
4代表4個字符,緊跟著函數(shù)名func,然后是參數(shù)類型的縮寫idpi分別是int double int*
所以C語言沒辦法支持重載,因為同名函數(shù)在底層匯編中是無法區(qū)分的。
而C++可以通過函數(shù)名修飾規(guī)則,來區(qū)分同名函數(shù)。只要參數(shù)(個數(shù)、類型、類型順序)不同,匯編底層中修飾出來的函數(shù)名就不一樣,也就支持了函數(shù)重載。
4.返回值不同能否構(gòu)成函數(shù)重載?函數(shù)在調(diào)用時指定的是參數(shù)的類型,并沒有指定返回值的類型。
所以在調(diào)用函數(shù)時,編譯器只是通過參數(shù)來確定到底要調(diào)用哪個函數(shù)。
兩個函數(shù)如果只有返回值類型不同的話,編譯器是無法區(qū)分到底要調(diào)用哪個函數(shù)的,這會產(chǎn)生二義性。
int f(int a, int b)
{cout<< "f(int a,int b)"<< endl;
return 0;
}
char f(int a, int b)
{cout<< "f(int a,int b)"<< endl;
return 'A';
}
// 上面的兩個函數(shù)不構(gòu)成函數(shù)重載
int main()
{f(1, 1);
f(2, 2);
return 0;
}
五、引用(三姓家奴)
1.引用概念(不就取別名么)引用不是新定義一個變量,而是給已存在變量取了一個別名。
語法層面上,編譯器不會為引用變量開辟內(nèi)存空間,它和引用實體共用同一塊內(nèi)存空間。
ra和a的地址是相同的,說明ra和a共用同一塊內(nèi)存空間。
void TestRef()
{int a = 10;
int& ra = a;//<====定義引用類型
printf("%p\n", &a);
printf("%p\n", &ra);
}
int main()
{TestRef();
return 0;
}
了解引用后在寫鏈表時,就不需要傳二級指針了,我們可以直接對一級指針進(jìn)行引用,這樣操作的時候引用參數(shù)也可以變成輸出型參數(shù)。
void SlistPushBack(struct ListNode** pphead, int x)//以前C語言的用法
void SlistPushBack(struct ListNode*& phead, int x)//給int*取別名,其實就是給指針變量取別名
{//有了別名之后,完全不需要二級指針了。
}
//有些教材會這樣去寫
typedef struct ListNode
{struct ListNode* next;
int val;
}LTNode,*PLTNode;
void SlistPushBack(PLTNode& phead, int x)//這里其實就是一個結(jié)構(gòu)體指針的引用
{}
2.引用特性a.引用在定義時必須初始化
b.一個變量可以有多個引用
c.一旦引用了某個實體,不可以在引用其他實體
void TestRef()
{int a = 10;
int& ra = a;
int& rrra=a;
int& rrrra=a;//變量a可以有多個引用
int& rra;//必須初始化引用,不能空引用
int b = 20;
ra = b;
//這里是賦值操作,不是修改引用,引用一旦引用一個實體,就不能再引用其他實體,
//ra就是a,a就是ra,修改ra自然就是修改a了。
//C++里面引用無法完全替代指針,鏈表無法用引用實現(xiàn),所以該用指針還得用指針。
//為什么實現(xiàn)不了捏?因為引用無法拿到下一個節(jié)點的地址呀!
}
3.引用的使用場景
3.1 內(nèi)存空間銷毀意味著什么?& 訪問銷毀的內(nèi)存空間會怎樣?內(nèi)存空間銷毀并不是把這塊內(nèi)存空間撕爛了,永久性的毀滅這塊兒空間,內(nèi)存空間是不會消失的,他會原封不動的在那里,只不過當(dāng)內(nèi)存空間銷毀之后,他的使用權(quán)不屬于我們了,我們存到里面的數(shù)據(jù)不被保護(hù)了,有可能發(fā)生改變了。
銷毀之后,我們依然可以訪問到這塊空間,只是訪問的行為是不確定的,我們對空間的數(shù)據(jù)進(jìn)行讀寫的行為是無法預(yù)料的。
銷毀的空間可能發(fā)生的改變:
a.這塊空間沒有分配給別人,我們寫的數(shù)據(jù)并未被清理掉,依舊可以正常訪問到原來的數(shù)據(jù)
b.這塊空間沒有分配給別人,但是空間數(shù)據(jù)已經(jīng)被銷毀了,呈現(xiàn)出隨機(jī)值
c.這塊空間分配給別人,別人寫了其他的數(shù)據(jù),將我們的數(shù)據(jù)覆蓋掉了。
上面的人是我們擬人化了,實際上他就是某些變量或結(jié)構(gòu)體或函數(shù)棧幀等等……
3.2 做返回值(減少拷貝提高效率,修改返回值)一、減少拷貝,提高效率
當(dāng)我們要返回一棵樹的時候,引用返回就可以幫我們大忙了,由于它不用拷貝,所以相比于傳值返回,程序在運行上,效率提升的可不止一點。
下面圖片所得結(jié)論:出了函數(shù)作用域,返回變量存在,可以使用引用返回,不存在,不可以使用引用返回。
匯編解釋函數(shù)返回時利用臨時變量
匯編解釋函數(shù)返回時利用寄存器
寄存器其實也是需要拷貝的,先將局部變量的值拷貝到寄存器,然后再把寄存器的值拷到接收函數(shù)返回變量。
二、修改返回值
要知道,函數(shù)的返回值它是一個值,也就是一個臨時變量,臨時變量是具有常性的,是一個值,并不是一個變量。
所以如果不用引用返回的話,肯定是無法修改返回值的,編譯器會報錯:表達(dá)式必須是可修改的左值。
但是如果你用引用返回的話,我們就可以修改返回值了,因為引用變量是返回值的一個別名,首先引用變量就是這個返回值本身,并且引用還是一個變量,是可以修改的左值,所以我們可以利用引用做返回值來修改返回值,這一點在C語言中是無法做到的,因為C語言中返回值他只是一個值,并不是變量,無法修改,但C++有了引用之后便可做到這一點。
下面的兩段代碼給大家演示了C語言中,返回值無法修改的場景。
int* modify(int*pa)
{int b = 10;
pa = &b;
return pa;
}
int change(int* arr)
{for (int i = 0; i< 3; i++)
{if (arr[i] == 2)
{ return arr[i];
}
}
}
int main()
{int a = 100;
int arr[] = {1,2,3 };
change(arr) *= 2;//報錯,表達(dá)式必須是可修改的左值
modify(&a) *= 2;//報錯,表達(dá)式必須是可修改的左值
}
下面這段代碼給大家演示了C++中利用引用作為返回值來修改返回值的場景。
將數(shù)組中的偶數(shù)全部擴(kuò)大二倍。
int& change(int* arr,int i)
{return arr[i];
}
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i< 10; i++)
{if (arr[i] % 2 == 0)
{ change(arr, i) *= 2;
}
}
for (int i = 0; i< 10; i++)
{cout<< arr[i]<< " ";
}
}
3.3 做參數(shù)(減少拷貝提高效率,可作為輸出型參數(shù))在調(diào)用函數(shù)時,形參是要做拷貝的,在它所在的函數(shù)棧幀里面,所以如果你要是傳值調(diào)用,那必然在調(diào)用函數(shù)時,會做一份實參的臨時拷貝,如果你是傳址調(diào)用,指針變量也要開辟自己的空間,所以這些都是對程序性能的消耗。
但如果我們用引用做參數(shù)就不會有這些問題了,因為操作系統(tǒng)并不會給引用變量單獨開辟一塊空間,并且引用變量也不需要對實參進(jìn)行拷貝,那就可以減少拷貝提高效率。
并且由于引用實質(zhì)上就是實參本身,那么它也可以作為輸出型參數(shù),對函數(shù)外面的實參進(jìn)行修改。
void Swap(int left, int right)
{int temp = left;
left = right;
right = temp;
}
void Swap(int& left, int& right)
{int temp = left;
left = right;
right = temp;
}
4.常引用(帶有const的引用)
4.1 指針/引用在賦值中,權(quán)限可以縮小,但是不能放大權(quán)限的縮小和放大,針對的是從引用實體到引用變量的過程中,權(quán)限的變化
int main()
{int a = 0;
int& ra = a;//ra既可以讀到a,也可以修改a,權(quán)限的平移
const int& rra = a;//rra只能讀到a,并不可以修改a,這里是權(quán)限的縮小
rra++;//rra沒有修改a的權(quán)限,因為他是const引用
a++;//a本身是int修飾,沒有const,可以修改
const int b = 1;//變量b只能被讀取,不能被修改
int& rb = b;//rb沒有const修飾,可以讀寫b,這就是典型的權(quán)限放大,編譯器會報錯
int& rd = 10;//常量不可以被修改,典型的權(quán)限放大。
const int& rb = b;//rb有了const修飾,只能讀b,不能寫b,權(quán)限的平移
return 0;
}
4.2 常引用做參數(shù)a.一般引用做參數(shù)都是用常引用,也就是const+引用,如果不用const會有可能產(chǎn)生權(quán)限放大的問題,而常引用既可以接收只讀的權(quán)限,又可以接收可讀可寫的權(quán)限。
b.常引用做參數(shù)并不是為了修改參數(shù),而是為了減少拷貝提高效率。
4.3 缺省參數(shù)如何引用?缺省參數(shù)如果想做為引用的話,必須用常引用,因為缺省參數(shù)是一個常量,是不允許被修改的,只可以讀。
void func(const int& N = 10)
{}
4.4 臨時變量具有常性不能修改(傳值返回,隱式/強(qiáng)制類型轉(zhuǎn)換)a.常引用接收傳值返回
傳值返回我們前面就提到過,他返回時需要依靠一個臨時變量,而臨時變量具有常性不能修改,所以如果想要用引用接收那就必須用常引用,必須帶上const。
int Count()
{static int n = 0;
n++;
// ...
return n;
}
int main()
{int& ret = Count();
const int& ret = Count();
}
b.常引用接收臨時變量
int main()
{const int& b = 10;
double d = 12.34;
cout<< (int)d<< endl;
//強(qiáng)制類型轉(zhuǎn)換,并不是改變了變量d,而是產(chǎn)生臨時變量,輸出的值也是臨時變量的值。
int i = d;
//隱式類型轉(zhuǎn)換,也是產(chǎn)生了臨時變量。
const int& ri = d;//這里引用的實體其實就是從double d 到int類型轉(zhuǎn)換中間的臨時變量
cout<< ri<< endl;//這里輸出的引用實際上就是double到int中間的臨時變量的別名。
return 0;
}
5.引用和指針的區(qū)別a.語法概念上引用變量就是一個別名,不開空間,和引用實體共用一個空間。
底層實現(xiàn)上引用變量其實是要開空間的,因為引用在底層上是按照指針來實現(xiàn)的
b. 引用概念上定義一個變量的別名,指針存儲一個變量地址。
c. 引用在定義時必須初始化,指針沒有要求
d. 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體
e. 沒有NULL引用,但有NULL指針
f. 在sizeof中含義不同:引用結(jié)果為引用類型的大小,但指針始終是地址空間所占字節(jié)個數(shù)(32位平臺下占4個字節(jié))
g. 引用自加即引用的實體增加1,指針自加即指針向后偏移一個類型的大小
h. 有多級指針,但是沒有多級引用
i. 訪問實體方式不同,指針需要顯式解引用,引用直接使用就好,具體細(xì)節(jié)編譯器會自動處理
j. 引用比指針使用起來相對更安全
六、內(nèi)聯(lián)函數(shù)(不建立函數(shù)棧幀的函數(shù),已經(jīng)不是正常的函數(shù)了) 1.替代C語言中的宏C語言中的宏在書寫時,由于宏是單純的替換,所以導(dǎo)致很容易出問題,例如下面,我們寫一個實現(xiàn)兩數(shù)之和的宏,大概能寫出4種形式,可是這四種形式都是錯的。
因為在不同的使用宏的場景下,對于宏的書寫要求都是很高的。
a.如果加分號,那么在分支語句的判斷部分,會出語法錯誤。
b.如果不加外層括號,可能由于運算符優(yōu)先級的問題,無法得到我們想要的答案。
c.如果內(nèi)層不加括號,僅僅是加減這樣的符號,都要比位操作符優(yōu)先級高,這時候也無法得到我們想要的答案。
這時候,在C++中就提出了內(nèi)聯(lián)函數(shù),內(nèi)聯(lián)函數(shù)在 ( 編譯 ) 期間,編譯器會用函數(shù)體來替換內(nèi)聯(lián)函數(shù)的調(diào)用,而不是宏那樣的單純替換
#define ADD(x,y) x+y
#define ADD(x,y) (x+y)
#define ADD(x,y) (x)+(y)
#define ADD(x,y) ((x)+(y));
int main()
{//不能加分號
if (ADD(1, 2))
{}
//外層括號
ADD(1, 2) * 3;
//內(nèi)層括號
int a = 1, b = 2;
ADD(a | b, a & b);//+運算符優(yōu)先級高于|&
}
2.編譯器根據(jù)函數(shù)體大小來決定是否展開(代碼膨脹)內(nèi)聯(lián)函數(shù)一般適用于頻繁調(diào)用的小函數(shù)。
如果不是內(nèi)聯(lián)函數(shù)還頻繁調(diào)用的話,就會頻繁的開辟函數(shù)棧幀,這會對程序產(chǎn)生不小的開銷,影響程序運行時的效率,內(nèi)聯(lián)函數(shù)不害怕這一點,因為它根本就不建立函數(shù)棧幀
同時如果內(nèi)聯(lián)函數(shù)體過大,編譯器也會將主動權(quán)掌握在自己手里,他會決定是否在內(nèi)聯(lián)函數(shù)調(diào)用的地方展開函數(shù)體。
如果函數(shù)體過大,將不會展開,如果較小,就會展開,這個結(jié)論我們可以通過匯編指令來查看。
inline int Add(int x, int y)//頻繁調(diào)用的小函數(shù),推薦搞成內(nèi)聯(lián)函數(shù)。
{return x + y;
}
inline int func(int x, int y)//編譯期間不會展開
{int ret = x + y;
ret = x + y;
ret += x + y;
ret = x * y;
ret = x + y;
ret *= x - y;
ret = x + y;
ret = x / y;
ret += x + y;
ret /= x + y;
ret *= x + y;
ret = x + y;
return ret;
}
int main()
{int ret = Add(1, 3);
int ret2 = func(1, 2);
return 0;
}
由于debug版本下我們要對代碼進(jìn)行調(diào)試,所以代碼中不會展開內(nèi)聯(lián)函數(shù)體,我們需要先將工程屬性設(shè)置成這樣子,然后打開調(diào)試中的反匯編查看底層的匯編指令,看看編譯器對于內(nèi)聯(lián)函數(shù)體展開的情況。
下面的匯編指令就可以驗證我們之前的結(jié)論,內(nèi)聯(lián)函數(shù)體過大,編譯器不展開內(nèi)聯(lián)函數(shù)調(diào)用的地方,函數(shù)體較小的時候編譯器會在內(nèi)聯(lián)函數(shù)調(diào)用的地方展開。
函數(shù)體較長時,編譯器不會展開是因為代碼膨脹,假設(shè)函數(shù)體中的指令有30行,程序中內(nèi)聯(lián)函數(shù)調(diào)用的地方有10000處,一旦編譯器展開函數(shù)體,程序就會瞬間出現(xiàn)30w行指令,這會瘋狂增加可執(zhí)行程序文件的大小,也就是安裝包的大小,所以編譯器不會讓這樣的事情發(fā)生,即使你對編譯器發(fā)出了內(nèi)聯(lián)的請求,編譯器也不會管你,說了句 ‘’ 莫挨勞資,走遠(yuǎn)點 ‘’
如果下面這部分知識不太清楚的話,可以看看下面這篇博文,補一下基礎(chǔ),因為接下來講的東西需要用到下面的知識。
程序運行原理和預(yù)編譯
如果內(nèi)聯(lián)函數(shù)的聲明和定義分開的話,程序就會報鏈接錯誤,為什么呢?我們前面說過內(nèi)聯(lián)函數(shù)只是有可能將函數(shù)體展開,并不會建立函數(shù)棧幀,所以stack.obj文件的符號表就不會存放Add函數(shù)和它的地址,那在鏈接階段,test.obj會根據(jù)Add的函數(shù)名字到stack.obj文件的符號表中尋找Add函數(shù)的有效地址,但可惜符號表中別說地址了,連函數(shù)名都沒有,自然目標(biāo)文件之間的鏈接就無法成功,編譯器就無法識別test.cpp中的Add到底是什么,光有個函數(shù)聲明,沒有函數(shù)定義編譯器也就會報錯:無法解析的外部符號。
結(jié)論:內(nèi)聯(lián)函數(shù)在定義時不要搞到.c文件里定義了,直接在.h文件里面定義就好,不要把定義和聲明分開,這樣在展開.h文件之后,函數(shù)體就在那里,鏈接階段就不會在去找函數(shù)的地址了,因為函數(shù)就在他自身的目標(biāo)文件里面。
1.補一下C語言芝士
第一行const直接修飾的是指針變量p1,所以指針變量p1本身不能修改,它指向的內(nèi)容還是可以修改的,但p1現(xiàn)在被你搞成const修飾了,所以它必須被初始化,因為它只有一次賦值的機(jī)會,就是在初始化的那個地方,不能說你后面在去給p1賦值,這樣不可以。
第二行const修飾的不是二級指針p2,修飾的是二級指針p2所指向的內(nèi)容,那么指針變量p2是沒有被const修飾的,所以p2可以不初始化,但p2所指向的內(nèi)容是不可以發(fā)生改變的,因為const修飾的是p2指向的內(nèi)容。
注意:語法檢查的時候,是不會先替換typedef內(nèi)容的,他會先直接分析你的代碼是否在語法上存在問題,比如第一行代碼,編譯器是不會把pstring替換為char的,如果替換為char當(dāng)然這句語句就沒有問題了,不初始化也OK,但是編譯器看的不是替換之后的,他在預(yù)編譯之前就發(fā)現(xiàn)你這段代碼語法有問題,所以編譯器就直接會報錯了,因為他認(rèn)為p1就是個變量,你用const修飾了,那就必須給初始值,第二行代碼編譯器認(rèn)為p2是個指針,因為它看到*的存在了,所以它認(rèn)為const修飾的是p2指向的內(nèi)容,不是p2本身
出現(xiàn)分析問題錯誤的原因,其實就是我們思考的是替換之后的結(jié)果,編譯器在分析語法時,只會看到代碼本身,根本不存在替換不替換這么一說。
typedef char* pstring;
int main()
{ const pstring p1; // 編譯成功還是失???
const pstring* p2; // 編譯成功還是失???
pstring* const p2;//如果這樣寫,const修飾的才是p2指針變量本身
return 0;
}
2. auto用于自動推導(dǎo)類型
3.auto利用逗號運算符,一行定義多個變量時,這些變量必須是相同的類型。
因為編譯器實際上只對第一個類型進(jìn)行推導(dǎo),然后用推導(dǎo)出來的類型來定義其他變量,所以你定義的多個變量就必須是同一類型的。
void TestAuto()
{auto a = 1, b = 2; //必須是相同的類型
auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因為c和d的初始化表達(dá)式類型不同
}
4.auto在推導(dǎo)類型時,如果想推導(dǎo)出引用類型則必須在auto后面加&,在推導(dǎo)指針類型時,auto后面加不加*都可以
int main()
{int x = 10;
auto a = &x;
auto* b = &x;//加不加*無所謂
auto& c = x;//必須加&
cout<< typeid(a).name()<< endl;//typeid().name()可以拿到類型的字符串
cout<< typeid(b).name()<< endl;
cout<< typeid(c).name()<< endl;
return 0;
}
5. auto不能作為函數(shù)參數(shù),因為無法事先確定需要開辟函數(shù)棧幀的大小
void TestAuto(auto a)//編譯器無法推導(dǎo)a的類型,開辟棧幀時也就不知道開多大。
{}
6. auto不能用來聲明數(shù)組
void TestAuto()
{int a[] = {1,2,3};
auto b[] = {4,5,6};//這是錯誤的聲明方式
}
八、基于范圍的for循環(huán)a. C++11中引入了基于范圍的for循環(huán),for后面的括號中有兩部分組成,第一部分是在范圍內(nèi)用于迭代的變量,第二部分表示迭代的范圍。
void TestFor()
{int array[] = {1, 2, 3, 4, 5 };
for(auto& e : array)//將迭代變量搞成引用,這樣可以直接操作數(shù)組中的數(shù)據(jù)。
e *= 2;
for(auto e : array)
cout<< e<< " ";
return 0;
}
b. for循環(huán)迭代的范圍必須是確定的
void func(int array[])//穿過來的array不是數(shù)組,而是指針。
{for(auto& e : array)
cout<< e<
九、指針空值nullptr ==>(void*)0在C++98中,字面常量0既可以是一個整形數(shù)字,也可以是無類型的指針(void*)常量,但是編譯器默認(rèn)情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進(jìn)行強(qiáng)轉(zhuǎn)(void *)0。
下面是stddef.h頭文件的部分源碼,所以C++98對于指針空值是沒有確定的值的。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++11為了避免這樣的情況發(fā)生,定義了關(guān)鍵字nullptr來表示指針空值,彌補C++98中有關(guān)NULL空指針的bug。
void f(int)
{cout<< "1"<< endl;
}
void f(int*)
{cout<< "2"<< endl;
}
int main()
{f(0);
f(NULL);//這里原本想調(diào)用輸出2的結(jié)果,但NULL被編譯器默認(rèn)為0,就調(diào)用了輸出為1的函數(shù),所以我們要想調(diào)用輸出2的函數(shù),就用nullptr關(guān)鍵字。
f(nullptr);
//nullptr就是(void*)0
return 0;
}
下面是程序運行結(jié)果
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧