C++ 基礎 2:C++ 對 C 語言的拓展
引用
定義及程式設計實踐
引用,是某個已存在變數的另一個名字。
一旦把引用初始化為某個變數,就可以使用該引用名稱或變數名稱來指向變數。
注意:
-
引用沒有定義,是一種關係型宣告。宣告它和原有某一變數(實體)的關
系。因此引用型別必須與原型別保持一致,且不分配記憶體。與被引用的變數有相同的地
址。 -
& 符號前有資料型別時,是引用。其它皆為取地址。
-
可對引用再次引用。多次引用的結果,是某一變數具有多個別名。
建立引用例子如下:
// reference.cpp #include <iostream> using namespace std; int main () { // 宣告簡單的變數 int i; double d; // 宣告引用變數 int& r = i; double& s = d; i = 1; cout << "Value of i : " << i << endl; cout << "Value of i reference : " << r << endl; d = 6.1; cout << "Value of d : " << d << endl; cout << "Value of d reference : " << s << endl; getchar(); return 0; }
執行結果:
引用程式設計實踐如下:
// reference2.cpp #include <iostream> using namespace std; int main() { int a = 1; int& b = a; // b = a = 1 a = 2; int *p = &a; *p = 3; // a = 3 cout << "a = " << a << endl; b = 4; // b = a -> a = 4 cout << "a = " << a << ", b = " << b << endl; getchar(); return 0; }
執行結果:
引用和指標的區別
引用很容易和指標混淆,它們之間有以下不同:
-
不存在空引用。引用必須連線到一塊合法的記憶體。指標可以為空指標。
-
引用必須在建立時被初始化(引用作為函式引數的時候不需要初始化,因為形參一定會被賦值的)。指標可以在任何時間被初始化。
-
一旦引用被初始化為一個物件,就不能被指向到另一個物件。指標可以在任何時候指向到另一個物件。
引用的意義
-
引用作為其他變數的別名而存在,因此在一些場合可以代替指標
-
引用相對於指標來說具有更好的可讀性和實用性
// 無法實現兩資料的交換 void swap(int a,int b); //開闢了兩個指標空間用於交換 void swap(int *a,int *b);
// referenceSwap.cpp,不開闢空間使用引用進行數值交換
#include <iostream>
using namespace std;
void swap(int& a, int& b)
{
int temp;
temp = a;
a = b;
b = temp;
}
int main()
{
int a = 1,b = 2;
cout << "a = " << a << " , b = " << b << endl;
swap(a,b);
cout << "a = " << a << " , b = " << b << endl;
getchar();
return 0;
}
執行結果:
堆變數,棧變數
全域性變數、靜態區域性變數、靜態全域性變數,new 產生的變數都在堆中,動態分配的變數在堆中分配。
區域性變數在棧裡面分配。
程式為棧變數分配動態記憶體,在程式結束為棧變數清除記憶體,但是堆變數不會被清除。
引用作為函式的返回值
當函式返回值為引用時:
- 若返回棧變數引用時,不能成為其他引用的初始值。
如下程式碼所示:
// funcReturnRef.cpp,引用作為函式的返回值,什麼時候可以為其他引用初始化的值
#include <iostream>
using namespace std;
// 返回棧變數
int getA1()
{
int a;
a = 1;
return a;
}
// 返回棧變數引用
int& getA2()
{
int a;
a = 1;
return a;
}
int main()
{
int a1 = 0;
int a2 = 0;
// 值拷貝
a1 = getA1();
// 將 棧變數引用 賦值給 變數,編譯器類似做了如下隱藏操作:a2 = *(getA2())
a2 = getA2();
// 將 棧變數引用 賦值給 另一個引用作為初始值。此時將會有警告:返回區域性變數或臨時變數的地址
int& a3 = getA2();
cout << "a1 = " << a1<< endl;
cout << "a2 = " << a2<< endl;
cout << "a3 = " << a3<< endl;
getchar();
return 0;
}
警告資訊:
第一次執行結果:
第二次執行結果:
- 若返回堆變數引用時,可以成為其他引用的初始值
如下程式碼所示:
// funcReturnRef2.cpp,引用作為函式的返回值,什麼時候可以為其他引用初始化的值
#include <iostream>
using namespace std;
// 返回堆變數
int getA1()
{
static int a;
a = 1;
return a;
}
// 返回棧變數引用
int& getA2()
{
static int a;
a = 1;
return a;
}
int main()
{
int a1 = 0;
int a2 = 0;
// 值拷貝
a1 = getA1();
// 將 棧變數引用 賦值給 變數,編譯器類似做了如下隱藏操作:a2 = *(getA2())
a2 = getA2();
// 將 堆變數引用 賦值給 另一個引用作為初始值。由於是靜態區域,地址不變,記憶體合法。
int& a3 = getA2();
cout << "a1 = " << a1<< endl;
cout << "a2 = " << a2<< endl;
cout << "a3 = " << a3<< endl;
getchar();
return 0;
}
執行結果:
指標引用作函式引數
C++ 中指標引用作函式引數,與 C 語言中二級指標作函式引數的區別。
如下程式碼所示:
// pointReference.cpp
// C++ 中指標引用作函式引數,與 C 語言中二級指標作函式引數的區別
#include <iostream>
using namespace std;
#define AGE 18
// C 語言中的二級指標
int getAge1(int **p)
{
int age = AGE;
int *ptemp = &age;
// p 是實參的地址, *實參的地址,去間接的修改實參
*p = ptemp;
return 0;
}
// C++ 中指標引用
int getAge2(int* &p)
{
int age = AGE;
if(p == NULL)
{
p = (int *)malloc(sizeof(int));
if(p == NULL)
return -1;
}
// 給 p 賦值,相當於給 main 函式中的 pAge 賦值
*p = age;
return 0;
}
int main(void)
{
int *pAge = NULL;
// 1 C 語言中二級指標
getAge1(&pAge);
cout << "age: " << *pAge << endl;
pAge = NULL;
// C++ 中指標引用
getAge2(pAge);
cout << "age: " << *pAge << endl;
pAge = NULL;
getchar();
return 0;
}
執行結果:
const 引用
-
const 物件的引用必須是 const 的。
-
const 引用可以使用相關型別的物件(常量,非同型別的變數或表示式)初始化。這個是 const 引用與普通引用最大的區別。
例:
const int &a = 1;
double x = 1.1;
const int &b = x;
- const 引用限制物件為只讀,不能通過修改 const 引用來修改 const 物件。
inline 行內函數
C 語言中有巨集函式的概念。巨集函式的特點是內嵌到呼叫程式碼中去,避免了函式呼叫的開銷。但是由於巨集函式的處理髮生在預處理階段,確實了語法檢測和有可能帶來的語義差錯,因此 C++ 引入了 inline 行內函數。
行內函數基本概念
-
行內函數宣告時 inline 關鍵詞必須和函式定義結合在一起,否則編譯器會忽略內聯請求。
-
C++ 編譯器直接將函式體插入在函式呼叫的地方。
-
行內函數沒有普通函式呼叫時的額外開銷(壓棧,跳轉,返回)
-
行內函數是一種特殊的函式,具有普通函式的特徵(引數檢查,返回型別等)
-
行內函數由編譯器處理,直接將編譯後的函式體插入在呼叫的地方,巨集函式由前處理器處理,進行簡單的文字替換,沒有任何的編譯過程。
-
C++ 對行內函數的限制:
- 不能存在任何形式的迴圈語句
- 不能存在過多的條件判斷語句
- 函式體不能過於龐大
- 不能對函式進行取址操作
- 函式內聯宣告必須在呼叫語句之前
-
編譯器對於行內函數的限制不是絕對的,行內函數相對於普通函式的優勢知識節省了函式呼叫時壓棧,跳轉,和返回的開銷。因此,當函式體的執行開銷遠大於壓棧,跳轉,和返回的開銷時,那麼行內函數將沒有意義。
例項程式碼如下所示:
// inlineFunction.cpp
// 行內函數示例
#include <iostream>
using namespace std;
inline void func()
{
cout << "this is inlineFunction example!" << endl;
}
int main()
{
func();
getchar();
return 0;
}
執行結果:
函式過載
函式過載:用同一個函式名定義不同的函式,當函式名和不同的引數搭配時函式的含義不同。
過載規則
-
函式名相同
-
引數個數不同,引數的型別不同,引數順序不同,均可構成過載。
-
返回值型別不同則不可以構成過載。
如下所示:
void func(int a); // ok
void func(char a); // ok
void func(char a,int b); // ok
void func(int a,char b); // ok
char func(int a); // 與第一個函式衝突,報錯
呼叫準則
-
嚴格匹配,找到即呼叫。
-
通過隱式轉換尋求一個匹配,找到即呼叫。
過載底層實現
C++ 利用 name mangling(傾軋)技術,來改變函式名,以區分引數不同的同名函式。
實現原理:用 v c i f l d 表示 void char int float long double 及其引用。
如下所示:
void func(char a); // func_c(char a);
void func(char a,int b,double c); // func_cid(char a,int b,double c);
函式指標基本語法
// 方法一:
// 宣告一個函式型別
typedef void (myfunctype)(int a,int b);
// 定義一個函式指標
myfunctype* fp1= NULL;
// 方法二:
// 宣告一個函式指標型別
typedef void (*myfunctype_pointer)(int a,int b)
// 定義一個函式指標
myfunctype_pointer fp2 = NULL;
// 方法三:
// 直接定義一個函式指標
void (*fp3 )(int a,int b);
函式過載和函式指標結合
當使用過載函式名對函式指標進行賦值時,根據過載規則挑選與函式指標引數列表一致的候選者,嚴格匹配候選者的函式型別與函式指標的函式型別。
示例程式碼如下所示:
#include <iostream>
using namespace std;
void func(int a, int b)
{
cout << a << b << endl;
}
void func(int a, int b, int c)
{
cout << a << b << c << endl;
}
void func(int a, int b, int c, int d)
{
cout << a << b << c << d << endl;
}
// 1 定義一個函式型別
typedef void(myfunctype)(int, int); //定義了一個函式型別, 返回值void 引數列表是 int,int ,, void()(int,int)
// 2 定義一個函式指標型別
typedef void(*myfunctype_pointer)(int, int); //定義了一個函式指標型別, 返回值void 引數列表是 int,int ,, void(*)(int,int)
int main(void)
{
//1 定義一個函式指標
myfunctype * fp1 = NULL;
fp1 = func;
fp1(10, 20);
// 2 定義一個函式指標
myfunctype_pointer fp2 = NULL;
fp2 = func;
fp2(10, 20);
// 3 直接定義一個函式指標
void(*fp3)(int, int) = NULL;
fp3 = func;
fp3(10, 20);
cout << " -----------------" << endl;
// 此時的fp3 是 void(*)(int,int)
// fp3(10, 30, 30); // fp3 恆定指向一個 函式入口,void func(int, int) 的函式入口
// fp3(10, 30, 40, 50); // 想要通過函式指標,發生函式過載 是不可能。
fp3(10, 20);
void(*fp4)(int, int, int) = func; // 在堆函式指標賦值的時候,函式指標會根據自己的型別 找到一個過載函式
fp4(10, 10, 10);
// fp4(10, 10, 10, 10);
// 函式指標,呼叫的時候是不能夠發生函式過載的。
void(*fp5)(int, int, int, int) = func; // void func(int ,int ,int ,int )
fp5(10, 10, 10, 10);
return 0;
}
執行結果:
函式過載總結
-
過載函式在本質上是相互獨立的不同函式。
-
函式過載是由函式名和引數列表決定的。