【從零學C++11(上)】列表初始化、decltype關鍵字、委派構造等新特性


雖然現在C++迭代到C++20版本了,但是作爲最近的經典還非C++莫屬,因爲它提供了很多好用,便捷的新特性,修復了歷史版本很多的bug

歷史
2003年C++標準委員會曾經提交了一份技術勘誤表(簡稱TC1),使得C++03這個名字已經取代了C++98稱爲C++11之前的最新C++標準名稱。
不過由於TC1主要是對C++98標準中的漏洞進行修復,語言的核心部分則沒有改動,因此人們習慣性的把兩個標準合併稱爲C++98/03標準。從C++0xC++11C++標準當真是十年磨一劍啊~

第二個真正意義上的標準珊珊來遲。相比於C++98/03C++11則帶來了數量可觀的變化,其中包含了約140個新特性!以及對C++03標準中約600個缺陷的修正,這使得C++11更像是從C++98/03中孕育出的一種新語言。
相比較而言,C++11能更好地用於系統開發和庫開發、語法更加泛華和簡單化、更加穩定和安全,不僅功能更強大,而且能提升程序員的開發效率。


下面開始具體介紹:

1. 列表初始化

C++98中,標準允許使用花括號{}對數組元素進行統一的列表初始值設定。比如:

int arrA[] = {1,2,3,4,5,6,7,8,9};
int arrB[5] = {1};

但對於一些自定義的類型,卻無法使用這樣的初始化。比如如下代碼就無法通過編譯

vector<int> v{1,2,3,4,5};

所以每次定義vector時,都需要先把vector定義出來,然後使用循環對其賦初始值,非常不方便。
C++11擴大了用大括號括起的列表(初始化列表)的使用範圍,使其可用於所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=),也可不添加。

內置類型

int main(){
	// 內置類型變量
	int x1 = {10};
	int x2{10};
	int x3 = 1+2;
	int x4 = {1+2};
	int x5{1+2};
	
	// 數組
	int arr1[5] {1,2,3,4,5};	
	int arr2[]{1,2,3,4,5};
	//列表初始化可以在 {} 之前使用等號,其效果與不使用 = 沒有什麼區別
	
	// 動態數組,在C++98中不支持
	int* arr3 = new int[5]{1,2,3,4,5};
	
	// 標準容器
	vector<int> v{1,2,3,4,5};
	map<int, int> m{{1,1}, {2,2},{3,3},{4,4}};
	return 0;
 }

自定義類型

  • 標準庫支持單個對象的列表初始化。
class Point{
public:
	Point(int x = 0, int y = 0)
	 : _x(x)
	 , _y(y)
	{}
private:
	int _x;
	int _y;
};
int main(){
	Pointer p{ 1, 2 };
	return 0;
}
  • 多個對象的列表初始化。
    多個對象想要支持列表初始化,需給該類(模板類)添加一個帶有initializer_list類型參數的構造函數即可。

【注】:initializer_list是系統自定義的類模板,該類模板中主要有三個方法:begin()end()迭代器及獲取區間中元素個數的方法size()

#include <initializer_list>
template<class T>
class Vector {
public:
	Vector(initializer_list<T> l)
	 : _capacity(l.size())
	 , _size(0)
	{
		_array = new T[_capacity];
		for(auto e : l)
			_array[_size++] = e;
	}
	
	Vector<T>& operator=(initializer_list<T> l) {
		delete[] _array;
		size_t i = 0;
		for (auto e : l)
			_array[i++] = e;
		return *this;
	}
private:
	T* _array;
	size_t _capacity;
	size_t _size;
};

2. 變量類型推導

  • 類型推導意義?

  • 在定義變量時,必須先給出變量的實際類型,編譯器才允許定義,但有些情況下可能不知道需要實際類型怎麼設定,或者類型寫起來特別複雜。比如:

#include <map>
#include <string>
int main(){
	short a = 32670;
	short b = 32670;
	
	short c = a + b;
	// c如果設爲short,會造成數據丟失,如果能夠讓編譯器根據a+b的結果推導c的實際類型,就不會存在問題
	
	std::map<std::string, std::string> m{{"apple", "蘋果"}, {"banana","香蕉"}};
	
	 // 使用迭代器遍歷容器, 迭代器類型太繁瑣
	std::map<std::string, std::string>::iterator it = m.begin();
	while(it != m.end()){
		cout << it->first << " " << it->second << endl;
		++it;
	}
	return 0;
}

C++11中,可以使用auto來根據變量初始化表達式類型推導變量的實際類型,可以給程序的書寫提供許多方便。將程序中cit的類型換成auto,程序可以通過編譯,而且更加簡潔。

decltype關鍵字

  • 爲什麼需要decltype關鍵字呢?
  • auto使用的前提是:必須要對auto聲明的類型進行初始化,否則編譯器無法推導出auto的實際類型。
    但有時候可能需要根據表達式運行完成之後結果的類型進行推導,因爲編譯期間,代碼不會運行,此時auto也就無能爲力。

請看代碼:

template<class T1, class T2>
T1 Add(const T1& left, const T2& right){
	return left + right;
}

如果能用兩參數加和之後結果的實際類型,作爲函數的返回值類型就不會出錯,但這需要程序運行完才能知道結果的實際類型,即RTTI(Run-Time Type Identification 運行時類型識別)。

C++98中確實已經支持RTTI

  1. typeid:但它只能查看類型不能用其結果類定義類型
  2. dynamic_cast:只能應用於含有虛函數的繼承體系中。

運行時類型識別的缺陷是降低程序運行的效率

decltype】:
decltype根據表達式的實際類型推演出定義變量時所用的類型,比如:

  1. 推演表達式類型作爲變量的定義類型
int main(){
	int a = 10;
	int b = 20;
	
	// 用decltype推演a+b的實際類型,作爲定義c的類型
	decltype(a+b) c;
	cout << typeid(c).name() << endl;
	return 0;
}
  1. 推演函數返回值的類型
    【如果帶參數列表,推導的是函數返回值的類型。注意:此處 只是推演,不會執行函數
void* GetMemory(size_t size){
	return malloc(size);
}
int main(){
	// 如果沒有帶參數,推導函數的類型
	cout << typeid(decltype(GetMemory)).name() << endl;
	
	// 如果帶參數列表,推導的是函數返回值的類型,注意:此處 只是推演,不會執行函數
	cout << typeid(decltype(GetMemory(0))).name() <<endl;
	
	return 0;
}

3. 基於範圍for的循環

範圍for之前的博客裏面也有使用過。

格式:

for(元素類型 元素對象:容器對象){
  循環體
}
  1. 如果循環體由單條語句或者單個結構塊組成,可以省略花括號
  2. 用元素對象依次結合容器對象中的每一個元素,每結合一個元素,執行依次循環體,直至容器內的所有元素都被結合完爲止。
  3. 不依賴於下標元素,通用。
  4. 看似不需要訪問迭代器,透明,但其底層實現依然要藉助於容器的迭代器,
  5. 不需要定義處理函數,簡潔。

語法簡單,不贅述~


4. final與override

  • final:禁止繼承。
  • override:檢查是否複寫。

之前的博客中有講解:
https://blog.csdn.net/qq_42351880/article/details/99475141


5. 智能指針

之前的博客中有專項講解
https://blog.csdn.net/qq_42351880/article/details/99618459


6. 新增容器:unordered系列

之前的博客中有提及unordered系列底層原理:
https://blog.csdn.net/qq_42351880/article/details/98121700


7. 委派構造函數

委派構造函數:是指委派函數將構造的任務委派給目標構造函數來完成的一種類構造的方式。

委派構造函數也是C++11中對C++的構造函數的一項改進,其目的是爲了:減少程序員書寫構造函數的時間。

請看代碼:

class Info {
public:
	Info(): _type(0), _name('a')
	{ InitRSet();}
	
	Info(int type): _type(type), _name('a')
	{ InitRSet();}
	
	Info(char a): _type(0), _name(a)
	{ InitRSet();}

private:
	void InitRSet() {//初始化其他變量}
private:
	int _type;
	char _name;
};

上述構造函數除了初始化列表不同之外,其他部分都是類似的,代碼重複

初始化列表可以通過:類內部成員初始化進行優化,但是構造函數體的重複在C++98中無法解決。
所以我們打算將構造函數體中重複的代碼提出來,作爲一個基礎版本,在其他構造函數中調用:

class Info{
public:
	// 目標構造函數
	Info()
	 : _type(0)
	 , _a('a')
	{ 
		InitRSet();
	}

	 // 委派構造函數
	Info(int type) 
	 : Info()
	{
		_type = type;
	}

	 // 委派構造函數
	Info(char a)
	 : Info()
	{
	 	 _a = a;
	}

private:
	void InitRSet(){ 
		//初始化其他變量 
	}
private:
	int _type = 0;
	char _a = 'a';
};

在初始化列表中調用”基準版本”的構造函數稱爲委派構造函數,而被調用的”基準版本”則稱爲目標構造函數。通過委派其他構造函數,多構造函數的類編寫更加容易。

【注】:構造函數不能同時”委派使用初始化列表


感謝您閱讀至此,感興趣的看官們可以移步中篇與下篇,繼續瞭解C++11剩餘新特性,後兩篇將主要講解C++11最重要的特性~

【從零學C++11(中)】移動語義右值引用std::move()完美轉發等新特性
https://blog.csdn.net/qq_42351880/article/details/103062922

【從零學C++11(下)】lambda表達式、線程庫原子操作庫等新特性
https://blog.csdn.net/qq_42351880/article/details/100144882

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章