這章再深入的講些函數相關的。
1.C++內聯函數
編譯器對內聯的函數處理就是將相應的函數的代碼直接替換掉對應函數的調用。對于內聯代碼,程序無需跳到另一個位置處執行代碼,再調回來。因此,內聯函數的運行速度比常規函數調用稍快,但是更占用內存。如果程序在10個不同的位置調用同一個函數,則該程序將包含該函數代碼的10個備份。
應有選擇的地使用內聯函數。如果執行函數代碼的時間比處理函數調用機制的時間長,則節省的時間只占整個過程的很小一部分。如果代碼執行時間很短,則內聯調用就可以節省非內聯調用使用的大部分時間。另一方面,由于這個過程相當快,因此盡管節省了該過程的大部分時間,但節省的時間的絕對值并不大,除非該函數經常被調用。
要使用這項特別,必須采取如下措施之一:
- 在函數聲明前加上關鍵字inline;
- 在函數定義前加上關鍵字inline。
通常的做法是省略原型,將整個定義放在本該提供原型的地方。
當函數過大或者函數有遞歸自己的時候,不應該將此函數聲明為內聯函數。
下面簡單舉個栗子:
inline int square(int a) {
return a * a;
}
int main() {
int aa = square(5);
cout << "aa:" << aa << endl;
return 0;
}
inline工具是C++新增的特性。C語言使用預處理語句#define來提供宏——內聯代碼的原始實現。例如:
#define SQUARE(X) X*X
這并不是通過傳遞參數實現的,而是通過文本替換來實現的。
a = SQUARE(5);// 實際是 a = 5*5
b = SQUARE(1+5);// 實際是 b = 1+5 * 1+5
c = SQUARE(a++);//實際是 a++ * a++
上面只有第一個能正常工作,即使通過改進宏定義
#define SQUARE(X) ((X)*(X))
對應第三個仍然是錯誤的。所以在c++中可以考慮使用將以前用宏定義的函數改為內聯函數實現。
2.引用變量
2.1 創建引用變量
引用是已定義的變量的別名。使用方法如下
int a = 10;
int &b = a;//b是a變量的別名
其中&不是地址運算符,而是類型標識符的一部分。比如char*表示char類型的指針,int&表示int類型的引用。上述聲明表示a和b指向相同的值和內存單元。舉個詳細的栗子:
int main() {
int a = 10;
int &b = a;
//通過b修改變量
b = 20;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
//通過a修改變量
a = 30;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
//查看他倆的地址
cout << "a addr:" << &a << endl;
cout << "b addr:" << &b << endl;
return 0;
}
輸出為
a:20
b:20
a:30
b:30
a addr:0x7fff59892938
b addr:0x7fff59892938
引用變量與指針的差別之一是,必須在聲明引用時將其初始化,如
int a = 10;
int *b;
b = &a;//可以
int &c = a;//可以
int &b;//不可以
b = a
因此引用更接近const指針,必須在創建時初始化,并且一旦關聯起來,就一直效忠于它。
2.2 將引用用作函數參數
引用經常被用作函數參數,這種傳遞參數的方法被稱為按引用傳遞。按引用傳遞允許被調用的函數訪問調用函數中的變量。這種方式和C的按指針傳遞類似。舉個例子:
#include <iostream>
using namespace std;
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 10;
int b = 5;
//按指針傳遞
swap(&a, &b);
cout << "a:" << a << ",b:" << b << endl;
a = 16;
b = 25;
//按引用傳遞
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
return 0;
}
2.3 引用的屬性和特別之處
首先如果意圖是不允許函數修改參數,還想使用引用,則可以用常量引用,例子:
double refcube(const int & n);//n是一個引用,引用的是一個整形常量
上面只是個例子,對于數據比較小的情況,應該使用按值傳遞的方式,只有在數據較大的情況再使用常量引用(既不能通過引用修改原數據,又可以避免大量的數據創建拷貝)。
以下是Primer原文的總結,但在實際實驗結果和描述不一致,可能是現代編譯器更智能了吧。
當函數參數為常量引用時,如果實參與引用參數類型不匹配,C++會生成臨時變量。注意的是參數必須是聲明為常量引用的時候,且類型不匹配才會創建臨時變量,否則編譯器報錯。例子:
#include <iostream>
using namespace std;
int square(const int &n) {
return n * n;
}
int square2(int &n) {
return n * n;
}
int main() {
long a = 10L;
square(a);//不報錯,因為會創建臨時變量,臨時變量是long型的10轉換為int型5
square2(a);//報錯,類型不匹配
return 0;
}
創建臨時變量的情況有
實參的類型正確,但不是左值;-
實參的類型不正確,但是可以轉換為正確的類型。
什么是左值呢,很多,所以就說什么不是左值吧,如101、“string”這種字面量就不是左值,或者 1+1 這種表達式也不是左值。
那為什么只有常量引用才可以創建臨時變量?還是例子:
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = a;
}
int main() {
long a = 10L;
long b = 12L;
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
return 0;
}
實際打印結果:
a:12,b:10
==先說下原文的意思:如果允許創建臨時變量則引用就失效了,臨時比那兩交換不代表原變量交換。但是實際結果是的確交換了,所以這塊還得再研究下。我的測試環境是64位Mac OS,IDE是CLion。==
應該盡可能使用常量引用,原因如下:
- 使用const引用可以避免無意中修改數據的編程錯誤;
- 使用const引用使函數能夠處理const和非const實參,否則將只能接受非const實參;
- 使用const引用使函數能夠正確生成并使用臨時變量。
2.4 將引用用于結構
沒啥太多說的,返回引用時要注意一點,應避免函數終止時不再存在內存單元引用。
2.5 何時使用引用參數
使用引用參數的主要原因有兩個:
- 程序員能夠修改調用函數中的數據對象;
- 通過傳遞引用而不是整個數據對象,可以提高程序的運行速度。
對于使用傳遞的值而不作修改的函數:
- 如果數據對象很小,如內置數據類型或小型結構,則按值傳遞。
- 如果數據對象是數組,則使用指針,因為這是唯一的選擇,并將指針聲明為指向const的指針。
- 如果數據對象是較大的結構,則使用const指針或const引用,以提高程序的效率。這樣可以節省復制結構所需的時間和空間。
- 如果數據對象是類對象,則使用const引用。類設計的語義常常要求使用引用,這是C++新增這項特性的主要原因。因此,傳遞類對象參數的標準方式是按引用傳遞。
對于修改調用函數中數據的函數:
- 如果數據對象是內置數據類型,則使用指針。如果看到諸如fixit(&x)這樣的代碼(其中x是int),則很明顯,該函數將修改X。
- 如果數據對象是數組,則只能使用指針。
- 如果數據對象是結構,則使用引用或指針。
- 如果數據對象是類對象,則使用引用。
3.默認參數
栗子:
#include <iostream>
using namespace std;
int processInt(int a, int b = 1) {
return a * b;
}
int main() {
cout << "result1:" << processInt(10) << endl;
cout << "result2:" << processInt(10, 3) << endl;
return 0;
}
輸出結果為:
result1:10
result2:30
需要注意的是:==對于帶參數列表的函數,必須從右向左添加默認值==。
4.函數重載
概念不說了。函數重載的掛件是函數的參數列表——也成為函數特征標。跟返回值沒關系。注意下兩個函數原型
double process(int a,int b);
double process(int &a,int &b);
這兩個函數是沖突的。
函數匹配時,并不區分const和非const變量。
5.函數模板
就是泛型的使用,栗子:
template<typename _T>
void swap(_T &a, _T &b);
int main() {
using std::cout;
using std::endl;
int a = 10, b = 5;
float c = 11.1f, d = 12.2f;
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
swap(c, d);
cout << "c:" << c << ",d:" << d << endl;
return 0;
}
template<typename _T>
void swap(_T &a, _T &b) {
_T tmp = a;
a = b;
b = tmp;
}
輸出
a:5,b:10
c:12.2,d:11.1
==注意,在mac clion上使用的時候,不要在執行swap的之前寫類似using namespace std;這種代碼,因為std空間中自帶了個swap的函數,參數列表也是兩個引用類型的模板。==
5.1 重載的模板
很簡單,看栗子:
#include <iostream>
template<typename _T>
void swap(_T &a, _T &b);
template<typename T>
void swap(T *a, T *b);
int main() {
using std::cout;
using std::endl;
int a = 10, b = 5;
float c = 11.1f, d = 12.2f;
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
swap(&c, &d);
cout << "c:" << c << ",d:" << d << endl;
return 0;
}
//傳遞引用的swap
template<typename _T>
void swap(_T &a, _T &b) {
_T tmp = a;
a = b;
b = tmp;
}
//傳遞指針的swap
template<typename T>
void swap(T *a, T *b) {
T tmp = *a;
*a = *b;
*b = tmp;
}
輸出
a:5,b:10
c:12.2,d:11.1
5.2 模板的局限性
還是看上面swap的栗子:
template<typename _T>
void swap(_T a, _T b);
{……}
如果傳入函數的是兩個數組,則在函數能不能執行a=b這種,也不能執行a>b,a*b等等。
5.3 顯示具體化
這個話題比較有意思,從來沒想過,舉個栗子,假設有如下結構體
struct Person{
int age;
char name[40];
char id[18];
}
例外需要有個交換的函數swap,則swap和之前寫的一樣,會交換兩個Person的所有屬性,但是如果我們希望有個函數值交換id和name,不交換age,則同樣需要聲明一樣的函數swap(T &a,T &b),就產生沖突了。這個時候可以提供一個具體化函數定義——稱為顯式具體化,其中包含所需的代碼。
C++98有如下定義和規則:
- 對于給定的函數名,可以有非模板函數、模板函數和顯式具體化模板函數以及他們的重載版本。
- 顯式具體化的原型和定義應該以template<>開頭,并通過名稱來指出類型。
- 具體化優先于常規模板,非模板函數優先于具體化和模板。
舉個栗子,都是原型:
void swap(Person &,Person &);//非模板函數
//模板函數
template <Typename T>
void swap(T &,T &);
//具體化模板函數
template <> void swap<Person>(Person &,Person &);
//具體化也可以這么寫,省略函數名后的Person,因為參數列表已經表明了
template <> void swap(Person &,Person &);