? ? ? ? 上一篇博客我們正式進入C++的學習,這一篇博客我們繼續學習C++入門的基礎內容,一定要學好入門階段的內容,這是后續學習C++的基礎,方便我們后續更加容易的理解C++。
目錄
一、內聯函數
1.0 產生的原因
1.1 概念
1.2 特性
1.3 面試題
二、缺省參數
2.1 缺省參數的概念
2.2 缺省參數的分類
2.2.1?全缺省參數
2.2.2?半缺省參數
2.3 多文件結構的缺省參數函數
2.3.1 缺省參數補充
三、函數重載(重點)
3.0 函數重載的引入
3.1 函數重載概念
3.2?判斷函數重載的規則(編譯器的工作)
3.3?函數重載解析的步驟
3.4 函數重載判斷練習
3.5?C++支持函數重載的原理-名字修飾(name Mangling)
3.5.1 預備知識
3.5.2? ?C語言編譯時函數名修飾約定規則(Windows平臺下)
3.5.2? ?C語言編譯時函數名修飾約定規則(Linux平臺下)
3.5.3? C++編譯時函數名修飾約定規則(Windows平臺下)
3.5.3? C++編譯時函數名修飾約定規則(Linux平臺下)
3.6?C++函數重載的名字粉碎(名字修飾)
3.7?如何指定函數以C方式修飾函數名還是以C++方式修飾函數名
四、函數模板(重點)
4.0 產生的原因? ?
4.1?函數模板定義
4.2?數組的推演及引用的推演
4.3 利用函數模板實現泛型編程
4.4?模板函數的重載與特化(完全特化、部分特化、泛化)
4.5 類模板
五、名字空間:namespace
5.1 C++作用域的劃分
5.2? 命名空間
5.3 命名空間的定義
5.4?命名空間使用
5.4.1?加命名空間名稱及作用域限定符
5.4.2?使用using將命名空間中某個成員引入
5.4.3?使用using namespace 命名空間名稱引入
一、內聯函數
1.0 產生的原因
? ? ? ? 當程序執行函數調用時,系統要建立棧空間,保護現場,傳遞參數以及控制程序執行的轉移等等, 這些工作需要系統時間和空間的開銷。當函數功能簡單,使用頻率很高,為了提高效率,直接將函數的代碼嵌入到程序中。但這個辦法有缺點,一是相同代碼重復書寫,二是程序可讀性往往沒有使用函數的好。因此,便產生了內聯函數,它的作用主要如下:
1、減少函數調用開銷:函數調用通常會有一些額外的開銷,包括壓棧、跳轉、返回等。這些開銷對于頻繁調用的小函數(如訪問器函數、簡單的計算函數)而言,可能會顯得過高。通過將這些小函數定義為內聯函數,可以避免這些開銷,因為內聯函數會在編譯時直接將函數代碼插入到調用點。
2、增強代碼可讀性:通過內聯函數,可以將一些頻繁使用的代碼塊抽象為函數,從而提高代碼的可讀性和可維護性。同時,因為內聯函數在編譯時會被展開,所以在運行時不會引入函數調用的開銷。
#include<ctype.h> //C語言中的字符處理函數庫
#include<iostream>
using namespace std;boo1 IsNumber(char ch)
{return ch >= 'O' && ch <= '9' ? 1 : 0;//return isdigit(ch) ;
}int main()
{char ch;while (cin.get(ch), ch != '\n'){if (IsNumber(ch)){cout << " 是數字字符" <<endl;}else{cout << "不是數字字符 " <<endl;}}return 0;
}
如果上述代碼判斷數字字符函數在程序頻繁調用,那么將會產生一筆不小的空間開銷和時間開銷。為了協調好效率和可讀性之間的矛盾,C++提供 了另一種方法,即定義內聯函數。
1.1 概念
? ? ? ?以inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開(在編譯期間編譯器會用函數體替換函數的調用。),沒有函數調用建立棧幀的開銷,內聯函數提升程序運行的效率。
查看方式:
- ?在release模式下,查看編譯器生成的匯編代碼中是否存在call Add
- 在debug模式下,需要對編譯器進行設置,否則不會展開(因為debug模式下,編譯器默認不會對代碼進行優化,以下給出vs2019的設置方式)
1.2 特性
- inline是一種以空間換時間的做法,如果編譯器將函數當成內聯函數處理,在編譯階段,會用函數體替換函數調用,缺陷:可能會使目標文件變大,優勢:少了調用開銷,提高程序運行效率。
- inline對于編譯器而言只是一個建議,不同編譯器關于inline實現機制可能不同,一般建 議:將函數規模較小(即函數不是很長,具體沒有準確的說法,取決于編譯器內部實現)、不是遞歸、且頻繁調用的函數采用inline修飾,否則編譯器會忽略inline特性。下圖為 《C++prime》第五版關于inline的建議:
- inline不建議聲明和定義分離,分離會導致鏈接錯誤。因為inline被展開,就沒有函數地址了,鏈接就會找不到。
哪什么情況下采用inline處理合適,什么情況下以普通函數形式處理合適呢?這里有個建議給大家。
? ? ? ? ?如果函數的執行開銷小于開棧清棧開銷(函數體較小),使用inline處理效率高。如果函數的執行開銷大于開棧清棧開銷,使用普通函數方式處理。
1.3 面試題
內聯函數與宏定義區別:
- 內聯函數在編譯時展開,帶參的宏在預編譯時展開。
- 內聯函數直接嵌入到目標代碼中,帶參的宏是簡單的做文本替換。
- 內聯函數有類型檢測、語法判斷等功能,宏只是替換。
二、缺省參數
2.1 缺省參數的概念
? ? ? ? ?一般情況下,函數調用時的實參個數應與形參相同,但為了更方便地使用函數,C++也允許定義具有缺省參數的函數,這種函數調用時,實參個數可以與形參不相同。缺省參數指在定義函數時為形參指定缺省值(默認值)。這樣的函數在調用時,對于缺省參數,可以給出實參值,也可以不給出參數值。如果給出實參,將實參傳遞給形參進行調用,如果不給出實參,則按缺省值進行調用。
? ? ? ?缺省參數是聲明或定義函數時為函數的參數指定一個缺省值。在調用該函數時,如果沒有指定實參則采用該形參的缺省值,否則使用指定的實參。
2.2 缺省參數的分類
2.2.1?全缺省參數
2.2.2?半缺省參數
注意事項:
? ? ? 缺省參數可以有多個,但所有缺省參數必須放在參數表的右側,即先定義所有的非缺省參數,再定義缺省參數(半缺省參數必須從右往左依次來給出,不能間隔著給)。這是因為在函數調用時,參數自左向右逐個匹配(函數調用時同樣從左往右給實參,不能間隔著給),當實參和形參個數不一致時只有這樣才不會產生二義性。
2.3 多文件結構的缺省參數函數
? ? ? ?函數的原型(聲明):由返回值類型+函數名+形參表,形參名稱可以省略,但必須有形參類型
指針變量參數為缺省值的時候,指針變量名不可以省略!
? ? ? 缺省參數不能在函數聲明和定義中同時出現,因為如果函數的聲明與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法確定到底該用那個缺省值!
? ? ? 習慣上,缺省參數在公共頭文件包含的函數聲明中指定, 不要函數的定義中指定。如果在函數的定義中指定缺省參數值,在公共頭文件包含的函數聲明中不能再次指定缺省參數
值。
2.3.1 缺省參數補充
? ? ? ? ?缺省實參不一定必須是常量表達式,可以使用任意表達式。當缺省實參是一個表達式時在函數被調用時該表達式被求值。
?
C語言不支持缺省參數(編譯器不支持)
三、函數重載(重點)
? ? ? ?自然語言中,一個詞可以有多重含義,人們可以通過上下文來判斷該詞真實的含義,即該詞被重載了。 比如:以前有一個笑話,中國有兩個體育項目大家根本不用看,也不用擔心。一個是乒乓球,一個是男足。前者是“誰也贏不了!”,后者是“誰也贏不了!”
3.0 函數重載的引入
? ? ? C語言實現int, double,char類型的比較大小函數。如下:
int my_max_i(int a, int b)
{ return a > b ? a : b;
}double my_max_d(double a, double b)
{ return a > b ? a : b;
}char my_max_c(char a, char b)
{ return a > b ? a : b;
}
? ? ? ? ?很容易發現它們的共同特點:這些函數都執行了相同的一般性動作; 都返回兩個形參中的最大值;從用戶的角度來看,只有一種操作,就是判斷最大值,至于怎樣完成其細節,用戶一點也不關心。這種詞匯上的復雜性不是”判斷參數中的最大值“問題本身固有的,而是反映了程序設計環境的一種局限性:在同一個作用域中出現的函數名字必須指向一個唯實體(函數體)。這種復雜性給程序員帶來了一個實際問題,他們必須記住或查找每一個函數名字。函數重載把程序員從這種詞匯復雜性中解放出來。
#include<iostream>
using namespace std;int max(int a, int b)
{ return a > b ? a : b;
}double max(double a, double b)
{ return a > b ? a : b;
}char max(char a, char b)
{ return a > b ? a : b;
}int main()
{cout << max(12, 23) << endl;cout << max(12.23, 23.45) << endl;cout << max('a', 'b') << endl;編譯器在編譯的時候就已經確定數據類型,從而匹配相應的函數return 0;
}
3.1 函數重載概念
? ? ? ?函數重載:是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數 或 類型 或 類型順序)不同,常用來處理實現功能類似數據類型 不同的問題。
#include<iostream>
using namespace std;// 1、參數類型不同構成函數重載
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、參數個數不同構成函數重載
void f()
{cout << "f()" << endl;
}void f(int a)
{cout << "f(int a)" << endl;
}// 3、參數類型順序不同構成函數重載
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;
}
編譯器的工作:
? ? ? ?當一個函數名在同一個域中被聲明多次時,編譯器按如下步驟解釋第二個(以及后續的)的聲明。如果兩個函數的參數表中參數的個數或類型或順序不同,則認為這兩個函數是重載。而不認為是函數的重復聲明(編譯器報錯!)。
3.2?判斷函數重載的規則(編譯器的工作)
? ? ? ?這一小節來學習,編譯器是如何識別函數重載的,只有我們明白了編譯器判斷的規則,我們才能正確使用函數重載!
1.如果兩個函數的參數表相同,但是返回類型不同, 會被標記為編譯錯誤:函數的重復聲明。
int my_max(int a, int b)
{return a > b ? a : b;
}unsigned int my_max(int a, int b) // 編譯報錯,被認為是函數的重復聲明;
{return a > b ? a : b;
}int main()
{int ix = my_max(12, 23);unsigned int =my_max(12, 23); // 編譯無法通過;reutrn 0;
}
為什么呢?
? ? ? ? ?函數的返回類型不能作為函數重載的依據,編譯器區分函數是依靠函數聲明中的函數名和參數的類型,編譯器不知道應該調用哪個函數,編譯器認為是同一個函數重復聲明;
2.參數表的比較過程與形參名無關。
//編譯器會把下面這兩個函數認為是同一個函數,編譯報錯,函數重復聲明
int my_add(int a, int b);
int my_add(int x, int y);
為什么呢?
? ? ? ??函數的參數列表中的參數名不能作為函數重載的依據,同樣,因為編譯器不以參數名稱來區分函數,被認為是同一個函數,編譯器認為是同一個函數重復聲明;
3.如果在兩個函數的參數表中,只有缺省實參不同,編譯器同樣認為是同一個函數重復聲明;也就是說它認為這是一個函數。
//編譯器會把下面這兩個函數認為是同一個函數,編譯報錯,函數重復聲明
void Print(int* br, int n);
void Print(int* br, int len = 10);
4.typedef名為現有的數據類型提供了一個別名,它并沒有創建一個新類型,因此,如果兩個函數參數表的區別只在于一個使用了typedef重命名,而另一個使用了與typedef相應的類型。則該參數表被視為相同的參數列表,編譯器同樣認為是同一個函數重復聲明;也就是說它認為這是一個函數。
//編譯器會把下面這兩個函數認為是同一個函數,編譯報錯,函數重復聲明
typedef unsigned int u_int;
int Print(u_int a);
int Print(unsigned int b);
5.當一個形參類型有const或volatile修飾時,如果形參是按值傳遞方式定義,在識別函數聲明是否相同時,并不考慮const和volatile修飾符.(不考慮CV特性)
//編譯器會把下面這兩個函數認為是同一個函數,編譯報錯,函數重復聲明
void fun(int a);
void fun(const int a);
6.當一個形參類型有const或volatile修飾時,如果形參是按照指針或引用定義時,在識別函數聲明是否相同時,就要考慮const和volatile修飾符.(考慮CV特性),那么又該如何考慮呢?
#include<iostream>
using namespace std;void print(int &a)
{cout<<"左值引用"<<endl;
}void print(const int &b)
{cout<<"常性左值引用(萬能引用)"<<endl;
}void print(int &&b)
{cout<<"右值引用"<<endl;
}int main()
{int x = 10;print(x); //優先匹配左值引用const int y = 10;print(y); //只匹配常性左值引用/常引用!(萬能引用)print(10); //優先匹配右值引用return 0;
}
匹配規則如下:
? ? ? ?首先,C++只有三種引用方式:左值引用(引用的是左值:可以取地址)、常性左值引用/萬能引用(既可以引用左值:可以取地址,又可以引用右值:不可以取地址)、右值引用(引用的是右值:不可以取地址),當存在三個引用的函數時,我們應明確編譯器的匹配順序,根據實參的類型進行匹配!!!
? ? ? ? 對于左值,優先匹配左值引用,其次才是常性左值引用!
? ? ? ? 對于常性左值,直接匹配常性左值引用,不存在,編譯器直接報錯!
? ? ? ? 對于右值引用,優先匹配右值引用,其次才是常性左值引用!
- ?對于普通的變量(它是左值),編譯器首先優先匹配形參為左值引用的函數,如果不存在該函數,然后,匹配形參為常性左值引用(常引用)的函數,如果二者都不存在,編譯器直接報錯!
- ?對于常變量(const修飾),編譯器直接匹配形參為常性左值引用(常引用)的函數,如果該函數不存在,編譯器直接報錯(不能匹配參數為右值引用的函數,右值引用引用的是右值)!
- ?對于右值,編譯器首先優先匹配形參為右值引用的函數,如果該函數不存在,然后,匹配形參為常性左值引用(常引用)的函數,如果二者都不存在,編譯器直接報錯!
7.注意函數調用的二義性; 如果在兩個函數的參數表中,形參類型相同,而形參個數不同,形參默認值將會影響函數的重載.
#include<iostream>
using namespace std;/****函數調用的二義性****/
void fun(int a);
void fun(int a, int b);
void fun(int a, int b = 10);int main()
{fun(12); //這里第一個函數會和第三個函數會發生沖突,編譯器不知道該調用哪一個函數fun(12,13); //這里第二個函數會和第三個函數會發生沖突,編譯器不知道該調用哪一個函數return 0;
}
3.3?函數重載解析的步驟
- 確定函數調用考慮的重載函數的集合,確定函數調用中實參表的屬性(由形參表的屬性確定調用那個函數)。
- 從重載函數集合中選擇函數,該函數可以在(給出實參個數和類型)的情況下可以調用函數。
- 選擇與調用最匹配的函數。
3.4 函數重載判斷練習
#include<iostream>
using namespace std;* /*******下面兩個函數構成重載***********/
void func(int *p) {} //可讀可寫
void func(const int *p) {} //只可讀,不可解引用修改int main()
{int x = 10; //普通變量:可讀可寫func(&x); //優先匹配普通指針的函數(第一個),其次才是const修飾的指針的函數(第二個)const int y = 10; //常變量:只可讀func(&y); //只能匹配const修飾的指針的函數(第二個),如果不存在該函數,直接報錯!return 0;
}
#include<iostream>
using namespace std;
/*******下面兩個函數不構成重載***********///編譯器會報錯,這不是函數的重載,第二個在編譯的時候會直接忽略const ,認為是同一個函數重復聲明void func(int *p) {} //可讀可寫,可以解引用修改數據void func( int* const p) {} //可讀可寫,可以解引用修改數據,const修飾指針自身的時候,編譯器可以忽略const的存在/**如果修改第二個為:void func( const int* const p) {} 那么這便又是函數的重載**********/int main(){int x = 10; //普通變量func(&x); //兩個函數功能一樣,對傳入的數據都可讀可寫,匹配兩個都可以!編譯器不知道調用哪個,就會報錯,認為是同一個函數重復聲明!return 0;}#endif
#include<iostream>
using namespace std;/*****下面兩個函數是重載函數:參數的個數不同******/void func(int a);void func(int a,int b);int main(){func(12);func(12, 23);}
#include<iostream>
using namespace std;void func(int a);
void func(int a, int b=20);int main()
{func(12); //這里第一個函數會和第二個函數會發生沖突,編譯器不知道該調用哪一個函數func(12, 23);//這里第一個函數會和第二個函數會發生沖突,編譯器不知道該調用哪一個函數
}由于函數調用的二義性,不是重載函數,這種問題只要不調用函數就不會報錯,但是只要調用就會出現編譯錯誤!!!
3.5?C++支持函數重載的原理-名字修飾(name Mangling)
3.5.1 預備知識
? ? ? ?“C"或者"C++”函數在內部(編譯和鏈接)通過修飾名識別。修飾名是編譯器在編譯函數定義或者
原型時生成的字符串。修飾名由函數名、類名、調用約定、返回值類型、參數表共同決定。不同的調用方式,形成的修飾名也不同。
- _stdcall調用:Pascal程序的缺省調用方式,通常用于Win32 Api中,函數采用從右到左的壓棧方式,自己在退出時清空堆棧。
- C調用(即用_cdecl 關鍵字說明):按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對于傳送參數的內存棧是由調用者來維護的(正因為如此,實現可變參數的函數只能使用該調用約定)。
- _fastcall 調用:"人”如其名,它的主要特點就是快,因為它是通過寄存器來傳送參數的(實際上,它用ECX和EDX傳送前兩個雙字( DWORD )或更小的參數,剩下的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的內存棧),在函數名修飾約定方面,它和前兩者均不同。
- thiscall調用:僅僅應用于“C++ "類的成員函數。this 指針存放于ECX寄存器,參數從右到左壓。thiscall不是關鍵詞,因此不能被程序員指定。
在C/C++中,一個程序要運行起來,需要經歷以下幾個階段:預處理、編譯、匯編、鏈接。
- ?實際項目通常是由多個頭文件和多個源文件構成,而通過C語言階段學習的編譯鏈接,我們 可以知道,【當前a.cpp中調用了b.cpp中定義的Add函數時】,編譯后鏈接前,a.o的目標文件中沒有Add的函數地址,因為Add是在b.cpp中定義的,所以Add的地址在b.o中。那么怎么辦呢?
- 所以鏈接階段就是專門處理這種問題,鏈接器看到a.o調用Add,但是沒有Add的地址,就會到b.o的符號表中找Add的地址,然后鏈接到一起。
- 那么鏈接時,面對Add函數,鏈接接器會使用哪個名字去找呢?這里每個編譯器都有自己的 函數名修飾規則。
3.5.2? ?C語言編譯時函數名修飾約定規則(Windows平臺下)
? ? ? ?C語言的名字修飾規則非常簡單,_ cdec是C/C++的缺省調用方式,調用約定函數名字前面添加了下劃線前綴。
_stdcall調用約定在輸出函數名前加上一個下劃線前綴,后面加上一個"@”符號和其參數的字節數。
_fastcall調用約定在輸出函數名前加上一個” @ "符號,函數名后面也是一個" @ "符號和其參數的字節數。
3.5.2? ?C語言編譯時函數名修飾約定規則(Linux平臺下)
通過下面我們可以看出gcc的函數修飾后名字不變。
3.5.3? C++編譯時函數名修飾約定規則(Windows平臺下)
3.5.3? C++編譯時函數名修飾約定規則(Linux平臺下)
3.6?C++函數重載的名字粉碎(名字修飾)
面試題:? ? ?
? ? ? ? ?編譯器編譯完是按照修飾名訪問這些函數的,不同的調用方式,編譯器編譯時函數名修飾約定規則不同。
1、什么是函數重載?
? ? ?函數名相同而參數列表不同(類型或者數量),叫函數重載!
2、返回值可不可以作為函數重載的依據?
? ? 不可以!C++的函數調用解析機制僅基于參數類型和數量來匹配函數,而不考慮返回值類型!如果兩個函數函數名和參數是一樣的,返回值不同是不構成重載的,因為調用時編譯器沒辦法區分。
3、為什么C++可以進行函數的重載,而C語言不可以?
? ? ?原因在于:二者的修飾名規則不同!(名字粉碎技術不同),?C語言只是在函數名的 前面加個下劃線,那么對于多個同名函數,他們的修飾名都相同,都是在函數名前面加個下劃線!編譯器在編譯階段無法區分!C++把形參列表的類型和數量作為修飾名的一部分,這樣相同的函數名(函數重載)就會得到不同的修飾名,這樣編譯器在編譯階段就可以區分出來!
3.7?如何指定函數以C方式修飾函數名還是以C++方式修飾函數名
extern "C" int add(int x, int y) { return x + y; } //<==> 按照C的修飾規則,修飾名為:_add
double add(double x, double y) { return x + y; }//<==> 按照C++的修飾規則,修飾名為:?add@@YAHHH@Z
//上面編譯可以通過,因為C和C++修飾名規則不同,編譯器可以區分!extern "C" int add(int x, int y) { return x + y; } //<==>按照C的修飾規則,修飾名為:_add
extern "C" double add(double x, double y) { return x + y; } //<==>按照C的修飾規則,修飾名為:_add
//上面編譯不可以通過,因為二者使用的都是C的修飾名規則,編譯器無法區分!//如何將一批函數指定相同的修飾規則:加個大括號
extern "C"
{int add(int x, int y) //<==> 按照C的修飾規則,修飾名為:_add{ return x + y;} double add(double x, double y) //<==> 按照C的修飾規則,修飾名為:_add{ return x + y; }
}
四、函數模板(重點)
4.0 產生的原因? ?
? ? ? ?為了代碼重用, 代碼就必須是通用的;通用的代碼就必須不受數據類型的限制。那么我們可以把
數據類型改為一個設計參數。這種類型的程序設計稱為參數化(parameterize) 程序設計。
? ? ? ? 軟件模塊由模板構造。包括函數模板(function template)和類模板(class template)。
函數模板可以用來創建一個通用功能的函數, 以支持多種不同形參, 簡化重載函數的設計。
4.1?函數模板定義
? ? ? ?<模板參數表> 尖括號中不能為空,參數可以有多個,用逗號分開。模板參數主要是模板類型參數。模板類型參數代表一種類型,由關鍵字class 或typename 后加一個標識符構成,在這里兩個關鍵字的意義相同,它們表示后面的參數名代表一個潛在的內置或用戶設計的類型。
?
#include<iostream>
using namespace std;template<class T> T my_max(T a,T b){return a > b ? a : b;}int main(){my_max(12, 23);my_max('a', 'b');my_max(12.23, 34.45);return 0;}
? ? ? ? ?編譯階段,編譯器根據實參類型自動的生成如下函數代碼:這也叫模板實參推演,其本質上還是函數重載,由編譯器自動生成代碼。
typedef int T;T my_max<int>(T a, T b){return a > b ? a : b;}typedef char T;T my_max<char>(T a, T b){return a > b ? a : b;}typedef double T;T my_max<double>(T a, T b){return a > b ? a : b;}
在編譯過程中,根據函數模板的實參構造出獨立的函數,稱為模板函數。這個構造過程被稱為模板實例化。
?
#if 0
#include<iostream>
using namespace std;
#include <typeinfo>template<class T>
void print(T a)
{cout << typeid(a).name() << endl;cout << typeid(a).name() << endl;cout << typeid(a).name() << endl;}int main()
{int a = 10;int arr[] = { 1,2,3,4,5 };print(a); //編譯階段,編譯器推演出:T為整型 intprint(&a); //編譯階段,編譯器推演出:T為整型指針 int *print(arr); //數組名代表首元素的地址,也就是一個指針,編譯階段,編譯器推演出:T為整型指針 int *
}
#endif
4.2?數組的推演及引用的推演
#if 1
#include<iostream>
using namespace std;
#include <typeinfo>
template<class T>
void print(T &a) //函數的參數為實參類型的引用(實參的引用)
{cout << typeid(T).name() << endl;cout << typeid(a).name() << endl;cout << sizeof(a) << endl; //20}int main()
{int a = 10;int arr[] = { 1,2,3,4,5 };print(a); //編譯階段,實參a為一個整型, 編譯器推演出:T為整型 int ,函數參數a為整型引用類型:int &print(&a); //編譯階段,編譯器無法推演,實參為一個整型指針也就是:int *, 所以a就是這個整型指針的引用,按道理推演a為:int* & ,在實參里面可以a++,但是這里的&a是一個地址,是一個常量(帶有常性const),所以編譯器無法推演!,print(arr); //編譯階段,數組名代表首元素的地址,也就是一個整型指針,編譯階段,編譯器推演出:數組引用,int [5]&
}
4.3 利用函數模板實現泛型編程
下面這個打印數組的函數代碼適用于一切類型的數組,我都可以打印整個數組,類型由編譯器自動推演#include<iostream>
using namespace std;// typedef int T ==>int
// #define N 5 ==>5template<class T, int N>void print(T(&br)[N]) /*模板類型參數推演時:類型是重命名規則,非類型是宏的規則直接拿數值替換*/{for (int i = 0; i < N; i++){cout << br[i] << " ";}count << endl;}int main(){int arr[5] = { 1,2,3,4,5 };double brr[3] = { 1.2,3.4,5.3 };print(arr); //數組引用:int (&br)[5]=arr;print(brr); //數組引用:double (&br)[3]=brr;}
4.4?模板函數的重載與特化(完全特化、部分特化、泛化)
- ?模板函數的重載是C++中允許通過模板實現多個函數版本的功能。在C++中,函數可以通過相同的名字但不同的參數列表來重載。模板函數也可以通過這種方式實現重載。
- ?函數模板特化是指為特定類型提供一個專門的模板實現,與上面相比,而不是使用通用的模板版本。
#include<iostream>
using namespace std;/*泛化:無任何限制*/template<class T>void func(T a){}/*部分特化:限制常性的任意類型指針*/template<class T>void func(const T *p){}/*完全特化:限制常性的字符類型指針*/void func(const char* p) {}int main(){const char* str = "hello";int a = 10;func(a);func(&a);func(str);return 0;}
4.5 類模板
? ? ? ? ?類模板是C++中一種強大的工具,用于定義通用的類。通過類模板,可以創建能夠處理任意數據類型的類,而無需為每種數據類型編寫單獨的類。類模板使用模板參數來表示類中的數據類型,使得類的實現能夠適用于多種類型。容器是一種數據結構,用于存儲和管理一組對象。在C++標準庫(STL,Standard Template Library)中,容器是實現了特定接口的類模板。這些類模板提供了存儲、訪問和操作其包含的元素的功能。
C++容器庫包括序列容器、關聯容器和無序容器。
?? ? 序列容器(Sequence Containers):用于按照線性順序存儲數據。
- std::vector:動態數組,支持快速隨機訪問和在末尾插入 / 刪除元素。
- std::deque:雙端隊列,支持快速隨機訪問和在兩端插入 / 刪除元素。
- std::list:雙向鏈表,支持在任何位置快速插入 / 刪除元素。
- std::array:固定大小的數組,大小在編譯時確定。
- std::forward_list:單向鏈表,支持在任何位置快速插入 / 刪除元素,但只允許單向遍歷。
?容器庫的特點
- 泛型編程:通過模板實現,容器可以存儲任意類型的對象。
- 自動管理內存:容器會自動管理其所需的內存,開發者無需手動分配和釋放內存。
- 迭代器支持:所有容器都提供迭代器,用于遍歷和操作元素。
- 算法兼容性:STL中的算法可以與容器無縫配合使用,提供諸如排序、搜索、復制等操作。
使用類模板生成任意類型的順序棧template<class T>class SeqStack
{private:T* data;int top;public:SeqStack(int sz = 100){data = (T*)malloc(sizeof(T) * sz);top = -1;}
};int main()
{SeqStack<int> ist; //類模板的類型必須給定SeqStack<double> dst;}/*******編譯器會根據上述模板生成如下代碼:**/class SeqStack<int>
{typedef int T;private:T* data;int top;public:SeqStack(int sz = 100){data = (T*)malloc(sizeof(T) * sz);top = -1;}
};class SeqStack<double>
{typedef double T;private:T* data;int top;public:SeqStack(int sz = 100){data = (T*)malloc(sizeof(T) * sz);top = -1;}
};
五、名字空間:namespace
5.1 C++作用域的劃分
? ? ? ? 在C++中把作用域劃分為:全局作用域,局部作用域、塊作用域,名字空間作用域和類作用域。注意:作用域是針對編譯器來說的,生存周期針對運行的時候來說的。函數被調用時,被調用函數內部的變量才會分配內存空間,當函數執行完畢,被調用函數內部的變量將會歸還給操作系統。我們定義的變量存儲在棧區或者堆區。
- ?全局變量:函數之外定義的變量:存儲在數據區,
- ?局部變量:函數內部定義的變量:存儲在棧區,
- ?靜態局部變量:函數內部定義的變量加static關鍵字修飾,只創建一次,保存上次的值,不會重新初始化:存儲在數據區(字符串常量也是)
- ?花括號內部的變量,只在花括號內部有效:存儲在棧區。
- ?類作用域是指在類定義內部的作用域,決定了類中的成員(包括成員變量和成員函數)的可見性和生命周期。
- ?名字空間作用域:多文件編程時:多個源文件定義相同的全局變量名字或者函數名,在項目進行編譯鏈接時候就會發生全局命名沖突!名字空間域是隨標準C++而引入的。它相當于一個更加靈活的文件域(全局域)。
5.2? 命名空間
? ? ? ?在C/C++中,變量、函數和后面要學到的類都是大量存在的,這些變量、函數和類的名稱將都存 在于全局作用域中,可能會導致很多沖突。使用命名空間的目的是對標識符的名稱進行本地化, 以避免命名沖突或名字污染,namespace關鍵字的出現就是針對這種問題的。
? ? ? ?名字空間域的引入,主要是為了解決全局名字空間污染(global namespace pollution)問題,即防止程序中的全局實體名與其他程序中的全局實體名的命名沖突。于是,便產生了命名空間。
5.3 命名空間的定義
? ? ? ?定義命名空間,需要使用到namespace關鍵字,后面跟命名空間的名字,然后接一對{}即可,{} 中即為命名空間的成員。
// 1. 正常的命名空間定義
namespace p1
{// 命名空間中可以定義變量/函數/類型int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}//2. 命名空間可以嵌套
// test.cpp
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;}}
}//3. 同一個工程中允許存在多個相同名稱的命名空間,編譯器最后會合成同一個命名空間中。
// ps:一個工程中的test.h和上面test.cpp中兩個N1會被合并成一個
// test.h
namespace N1
{int Mul(int left, int right){return left * right;}
}
注意:一個命名空間就定義了一個新的作用域,命名空間中的所有內容都局限于該命名空間中
5.4?命名空間使用
? ? ? ?命名空間中成員該如何使用呢?比如:
5.4.1?加命名空間名稱及作用域限定符
int main()
{int a = yhp: :my_ add(12,23) ;printf("%1f \n",Primer: :pi);printf ("%f \n",yhp: :pi);Primer: :Matrix: :my_ _max('a','b');return 0;
}
5.4.2?使用using將命名空間中某個成員引入
using yhp::pi;using Primer: :Matrix: :my_max;
//名字空間類成員matrix的using聲明
//以后在程序中使用matrix時,就可以直接使用成員名,而不必使用限定修飾名。
int main()
{printf("%1f \n",Primer: :pi) ;printf("%f \n",pi); // yhp::my_max('a','b');return 0;
}
5.4.3?使用using namespace 命名空間名稱引入
? ? ? ? 使用using指示符可以一次性地使名字空間中所有成員都可以直接被使用,比using聲明方便。using指示符;以關鍵字using開頭,后面是關鍵字namespace,然后是名字空間名。
using namespace名字空間名;
using namespace yhp;
int main()
{printf("%1f n",Primer: :pi ) ;printf("%f n" ,pi) ;// yhp: :my_add(12,23); // yhp::return 0;
}
多文件結構示例
?
std命名空間的使用慣例: .
? ? ? ?std是C++標準庫的命名空間,如何展開std使用更合理呢?
1.在日常練習中,建議直接using namespace std即可,這樣就很方便。
2. using namespace std展開,標準庫就全部暴露出來了,如果我們定義跟庫重名的類型/對
象/函數,就存在沖突問題。該問題在日常練習中很少出現,但是項目開發中代碼較多、規模
大,就很容易出現。所以建議在項目開發中使用,像std::cout這樣使用時指定命名空間+
using std::cout展開常用的庫對象/類型等方式。
?
? ? ? ? 這篇博客內容較為豐富,為后續學習好C++做好準備,?如果對此專欄感興趣,點贊加關注!?