1. 程式人生 > 實用技巧 >C++ 基礎 2:C++ 對 C 語言的拓展

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;
}

執行結果:

引用和指標的區別

引用很容易和指標混淆,它們之間有以下不同:

  • 不存在空引用。引用必須連線到一塊合法的記憶體。指標可以為空指標。

  • 引用必須在建立時被初始化(引用作為函式引數的時候不需要初始化,因為形參一定會被賦值的)。指標可以在任何時間被初始化。

  • 一旦引用被初始化為一個物件,就不能被指向到另一個物件。指標可以在任何時候指向到另一個物件。

引用的意義

  1. 引用作為其他變數的別名而存在,因此在一些場合可以代替指標

  2. 引用相對於指標來說具有更好的可讀性和實用性

// 無法實現兩資料的交換
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 產生的變數都在堆中,動態分配的變數在堆中分配。

區域性變數在棧裡面分配。

程式為棧變數分配動態記憶體,在程式結束為棧變數清除記憶體,但是堆變數不會被清除。

引用作為函式的返回值

當函式返回值為引用時:

  1. 若返回棧變數引用時,不能成為其他引用的初始值。

如下程式碼所示:

// 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;
}

警告資訊:

第一次執行結果:

第二次執行結果:

  1. 若返回堆變數引用時,可以成為其他引用的初始值

如下程式碼所示:

// 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 引用

  1. const 物件的引用必須是 const 的。

  2. const 引用可以使用相關型別的物件(常量,非同型別的變數或表示式)初始化。這個是 const 引用與普通引用最大的區別。

例:

const int &a = 1;
double x = 1.1;
const int &b = x;
  1. const 引用限制物件為只讀,不能通過修改 const 引用來修改 const 物件。

inline 行內函數

C 語言中有巨集函式的概念。巨集函式的特點是內嵌到呼叫程式碼中去,避免了函式呼叫的開銷。但是由於巨集函式的處理髮生在預處理階段,確實了語法檢測和有可能帶來的語義差錯,因此 C++ 引入了 inline 行內函數。

行內函數基本概念

  1. 行內函數宣告時 inline 關鍵詞必須和函式定義結合在一起,否則編譯器會忽略內聯請求。

  2. C++ 編譯器直接將函式體插入在函式呼叫的地方。

  3. 行內函數沒有普通函式呼叫時的額外開銷(壓棧,跳轉,返回)

  4. 行內函數是一種特殊的函式,具有普通函式的特徵(引數檢查,返回型別等)

  5. 行內函數由編譯器處理,直接將編譯後的函式體插入在呼叫的地方,巨集函式由前處理器處理,進行簡單的文字替換,沒有任何的編譯過程。

  6. C++ 對行內函數的限制:

    • 不能存在任何形式的迴圈語句
    • 不能存在過多的條件判斷語句
    • 函式體不能過於龐大
    • 不能對函式進行取址操作
    • 函式內聯宣告必須在呼叫語句之前
  7. 編譯器對於行內函數的限制不是絕對的,行內函數相對於普通函式的優勢知識節省了函式呼叫時壓棧,跳轉,和返回的開銷。因此,當函式體的執行開銷遠大於壓棧,跳轉,和返回的開銷時,那麼行內函數將沒有意義。

例項程式碼如下所示:

// inlineFunction.cpp
// 行內函數示例

#include <iostream>

using namespace std;

inline void func()
{
	cout << "this is inlineFunction example!" << endl;
}

int main()
{
	func();

	getchar();

	return 0;
}

執行結果:

函式過載

函式過載:用同一個函式名定義不同的函式,當函式名和不同的引數搭配時函式的含義不同。

過載規則

  1. 函式名相同

  2. 引數個數不同,引數的型別不同,引數順序不同,均可構成過載。

  3. 返回值型別不同則不可以構成過載。

如下所示:

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);         // 與第一個函式衝突,報錯

呼叫準則

  1. 嚴格匹配,找到即呼叫。

  2. 通過隱式轉換尋求一個匹配,找到即呼叫。

過載底層實現

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;
}

執行結果:

函式過載總結

  • 過載函式在本質上是相互獨立的不同函式。

  • 函式過載是由函式名和引數列表決定的。